@navaramap/three 0.0.0 → 0.0.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/assets/fontWorker-CCWL5pbx.js +2 -0
- package/dist/assets/fontWorker-CCWL5pbx.js.map +1 -0
- package/dist/assets/index-BdtHOOT9.js +12 -0
- package/dist/assets/index-BdtHOOT9.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-665OLxjy.wasm → navara_wasm_font_worker_bg-Bnck8OGf.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.wasm → navara_wasm_worker_bg-DG9ORAmj.wasm} +0 -0
- package/dist/index.d.ts +142 -51
- package/dist/index.js +4772 -4510
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +226 -218
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +13 -13
- package/src/event/context.ts +5 -0
- package/src/index.ts +10 -2
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +6 -1
- package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
- package/src/mesh/tile/drapeResolver.ts +76 -0
- package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
- package/src/mesh/{tile.ts → tile/index.ts} +124 -392
- package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
- package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
- package/src/tileTexture/TileTextureCache.test.ts +2 -0
- package/src/tileTexture/TileTextureCache.ts +1 -0
- package/src/tileTexture/TileTextureCompositor.test.ts +105 -0
- package/src/tileTexture/TileTextureCompositor.ts +241 -31
- package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
- package/src/tileTexture/types.ts +7 -0
- package/src/utils/demNoDataColor.test.ts +47 -0
- package/src/utils/demNoDataColor.ts +48 -0
- package/dist/assets/fontWorker-CEJEfRpb.js +0 -2
- package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
- package/dist/assets/index-BUB-ijMO.js +0 -12
- package/dist/assets/index-BUB-ijMO.js.map +0 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type { TileHandle } from "@navaramap/core";
|
|
2
|
+
import {
|
|
3
|
+
Color,
|
|
4
|
+
DataTexture,
|
|
5
|
+
RGBAFormat,
|
|
6
|
+
WebGLRenderTarget,
|
|
7
|
+
type Object3D,
|
|
8
|
+
type Vector2,
|
|
9
|
+
} from "three";
|
|
10
|
+
|
|
11
|
+
import type { TileHandler } from "../../event/context";
|
|
12
|
+
import type { TexturizedSceneByTileCoordinates } from "../../scene";
|
|
13
|
+
import type { TextureOptions } from "../../textures";
|
|
14
|
+
import type { TileTextureCompositor } from "../../tileTexture";
|
|
15
|
+
import { PolygonMesh } from "../polygon";
|
|
16
|
+
import { PolylineMesh } from "../polyline";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
configureTexturizedTexture,
|
|
20
|
+
type DrapeResolver,
|
|
21
|
+
} from "./drapeResolver";
|
|
22
|
+
|
|
23
|
+
import type { TileMaterial } from ".";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One WebMercator vector source tile baked into a layer's render target.
|
|
27
|
+
* `tileHandle` keys the cached offscreen scene; `uvOffset`/`uvScale` are the
|
|
28
|
+
* **mercator** affine framing the terrain tile's sub-region of that (possibly
|
|
29
|
+
* coarser, ancestor-resolved) source tile, driving the bake camera (identity for
|
|
30
|
+
* an exact same-tile drape).
|
|
31
|
+
*/
|
|
32
|
+
type VectorSource = {
|
|
33
|
+
tileHandle: TileHandle;
|
|
34
|
+
uvOffset: [number, number];
|
|
35
|
+
uvScale: [number, number];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One clamp-to-ground vector layer draped on this terrain tile, resolved by Rust.
|
|
40
|
+
* A layer is backed by one source on WebMercator terrain, but by several when the
|
|
41
|
+
* terrain is Geographic (N:M): every overlapping WM vector tile is baked into the
|
|
42
|
+
* layer's single render target, each framed by its own `uvOffset`/`uvScale`, so
|
|
43
|
+
* the RT ends up spanning the terrain tile's extent. `reproject` carries the
|
|
44
|
+
* terrain `[south, north]` band on Geographic terrain (undefined on WM) so the
|
|
45
|
+
* composite paste can remap the latitude axis WebMercator→Geographic, exactly
|
|
46
|
+
* like a raster slot.
|
|
47
|
+
*/
|
|
48
|
+
type VectorSlot = {
|
|
49
|
+
layerId: string;
|
|
50
|
+
sources: VectorSource[];
|
|
51
|
+
reproject?: [number, number];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Shared 1×1 transparent texture bound to texturized sampler slots that have
|
|
55
|
+
* no drape render target yet (they are allocated lazily). WebGL requires every
|
|
56
|
+
* sampler to reference a valid texture; the shader gates sampling with `shows`,
|
|
57
|
+
* so this placeholder's content is never visible. */
|
|
58
|
+
const EMPTY_TEXTURIZED_TEXTURE = new DataTexture(
|
|
59
|
+
new Uint8Array([0, 0, 0, 0]),
|
|
60
|
+
1,
|
|
61
|
+
1,
|
|
62
|
+
RGBAFormat,
|
|
63
|
+
);
|
|
64
|
+
EMPTY_TEXTURIZED_TEXTURE.needsUpdate = true;
|
|
65
|
+
|
|
66
|
+
/** The slice of TileMesh the vector resolver works against. `material` is an
|
|
67
|
+
* accessor because the tile's material instance can be swapped over its life
|
|
68
|
+
* (Basic ↔ Lambert). */
|
|
69
|
+
export type VectorDrapeHost = {
|
|
70
|
+
handle: TileHandle;
|
|
71
|
+
tileHandler: TileHandler;
|
|
72
|
+
compositor: TileTextureCompositor;
|
|
73
|
+
/** Offscreen per-(tile, layer) vector scenes the bake rasterizes. */
|
|
74
|
+
texturizedScenes: TexturizedSceneByTileCoordinates;
|
|
75
|
+
textureOptions: TextureOptions;
|
|
76
|
+
/** Drape render target side length in texels. */
|
|
77
|
+
drapeRtSize: number;
|
|
78
|
+
/** Texturized-vector slot budget (the region's width). */
|
|
79
|
+
numTexturizedVector: number;
|
|
80
|
+
/** First texturized-vector slot index (the region is `[from, maxTextures)`). */
|
|
81
|
+
texturizedSceneIndexFrom: number;
|
|
82
|
+
material: () => TileMaterial;
|
|
83
|
+
/** Re-report the tile's drape GPU footprint after the RT pool changed. */
|
|
84
|
+
reportDrapeGpuBytes: () => void;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clamp-to-ground vector layers: always the baked path, on every tiling scheme.
|
|
89
|
+
* Vector content is an offscreen three.js scene, so rasterizing into a
|
|
90
|
+
* per-layer render target is intrinsic — WebMercator terrain is just the
|
|
91
|
+
* degenerate case (one source, identity affine, no reprojection band) of the
|
|
92
|
+
* Geographic N:M mosaic. That is why, unlike the raster side, there is no
|
|
93
|
+
* direct/baked split here.
|
|
94
|
+
*/
|
|
95
|
+
export class VectorDrapeResolver implements DrapeResolver {
|
|
96
|
+
// Per-layer texturized-vector slots resolved by Rust: one slot per
|
|
97
|
+
// clamp-to-ground layer, keyed by the WM vector tile whose offscreen scene
|
|
98
|
+
// backs it. Ancestor fallback is already resolved in Rust, so this side just
|
|
99
|
+
// caches + bakes.
|
|
100
|
+
private vectorSlots: VectorSlot[] = [];
|
|
101
|
+
private renderTargets: WebGLRenderTarget[] = [];
|
|
102
|
+
// Signature of the last baked slot set; a change drives a re-bake + re-bind.
|
|
103
|
+
private prevSignature = "";
|
|
104
|
+
// The vector resolution revision at which `vectorSlots` was last fetched.
|
|
105
|
+
// Re-fetching is skipped while the global revision is unchanged (the resolve
|
|
106
|
+
// result can only change when a traverse runs or a scene becomes ready).
|
|
107
|
+
// `-1` forces a fetch on the first frame.
|
|
108
|
+
private lastRevision = -1;
|
|
109
|
+
private warnedExceededTextures = false;
|
|
110
|
+
|
|
111
|
+
constructor(private readonly host: VectorDrapeHost) {}
|
|
112
|
+
|
|
113
|
+
update(): void {
|
|
114
|
+
// Re-fetch the Rust-resolved slots only when the global vector resolution
|
|
115
|
+
// revision changed; otherwise reuse the cached slots. This skips a per-tile
|
|
116
|
+
// WASM-boundary resolve every frame, the dominant steady-state cost when
|
|
117
|
+
// many terrain tiles are visible.
|
|
118
|
+
const revision = this.host.tileHandler.vectorRevision();
|
|
119
|
+
if (revision !== this.lastRevision) {
|
|
120
|
+
this.lastRevision = revision;
|
|
121
|
+
this.refreshSlots();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Re-bake the offscreen vector scenes only when the resolved slots or their
|
|
125
|
+
// backing scenes changed; otherwise the existing render targets stay valid.
|
|
126
|
+
const signature = this.signature();
|
|
127
|
+
if (signature !== this.prevSignature) {
|
|
128
|
+
this.prevSignature = signature;
|
|
129
|
+
this.host.compositor.renderVectorScenes(
|
|
130
|
+
this.vectorSlots,
|
|
131
|
+
this.renderTargets,
|
|
132
|
+
);
|
|
133
|
+
this.bindSlots();
|
|
134
|
+
this.host.compositor.markDirty(this.host.handle, "vector-revision");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
syncMaterialSlots(): void {
|
|
139
|
+
const m = this.host.material();
|
|
140
|
+
const textures = m.userData.textures.value;
|
|
141
|
+
for (let i = 0; i < this.host.numTexturizedVector; i++) {
|
|
142
|
+
// Every texturized sampler slot must reference a valid texture (GLSL/WebGL
|
|
143
|
+
// requires it even for a sampler never read). Drape render targets are
|
|
144
|
+
// allocated lazily, so a slot without one binds the shared empty texture;
|
|
145
|
+
// `bindSlots` swaps in the real RT texture once a layer drapes here.
|
|
146
|
+
const lastIndex = this.host.texturizedSceneIndexFrom + i;
|
|
147
|
+
const rt = this.renderTargets[i];
|
|
148
|
+
textures[lastIndex] = rt
|
|
149
|
+
? configureTexturizedTexture(rt.texture, this.host.textureOptions)
|
|
150
|
+
: EMPTY_TEXTURIZED_TEXTURE;
|
|
151
|
+
|
|
152
|
+
// Per-slot visibility + material attributes are bound by bindSlots once
|
|
153
|
+
// the Rust-resolved slots are known; default to hidden here.
|
|
154
|
+
m.userData.shows.value[lastIndex] = 0;
|
|
155
|
+
m.userData.colors.value[lastIndex] = new Color(0xffffff);
|
|
156
|
+
m.userData.opacities.value[lastIndex] = 1.0;
|
|
157
|
+
}
|
|
158
|
+
// Re-bind vector slots on the next frame now that render targets were rebound.
|
|
159
|
+
this.prevSignature = "";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
slotReproject(absSlot: number): [number, number] | undefined {
|
|
163
|
+
// Baked-vector slots (whose RT spans the terrain extent) carry the
|
|
164
|
+
// Rust-resolved terrain `[south, north]` band directly on the slot;
|
|
165
|
+
// undefined on WebMercator terrain.
|
|
166
|
+
return this.vectorSlots[absSlot - this.host.texturizedSceneIndexFrom]
|
|
167
|
+
?.reproject;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
invalidate(): void {
|
|
171
|
+
this.prevSignature = "";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
liveRenderTargetCount(): number {
|
|
175
|
+
return this.renderTargets.length;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
dispose(): void {
|
|
179
|
+
for (const rt of this.renderTargets) {
|
|
180
|
+
rt.dispose();
|
|
181
|
+
}
|
|
182
|
+
this.renderTargets.length = 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Pull the Rust-resolved texturized-vector slots for this terrain tile into
|
|
187
|
+
* {@link vectorSlots}: one bake slot per clamp-to-ground layer, keyed by the
|
|
188
|
+
* rendered WM vector tile that backs it (ancestor fallback already resolved in
|
|
189
|
+
* Rust). Only layers with an actual draped scene take a slot — Rust also returns
|
|
190
|
+
* non-draped layers (rendered in the MRT scene), which must not consume the budget.
|
|
191
|
+
* Capped to the GPU slot budget; warns once when more draped layers are requested.
|
|
192
|
+
*/
|
|
193
|
+
private refreshSlots() {
|
|
194
|
+
const states =
|
|
195
|
+
this.host.tileHandler.getVectorTileStates(this.host.handle) ?? [];
|
|
196
|
+
|
|
197
|
+
// Group the flattened per-(layer, source) states back into one slot per
|
|
198
|
+
// layer, each carrying every overlapping WM source tile (N:M on Geographic
|
|
199
|
+
// terrain). First-seen layer order assigns the render-target index, kept
|
|
200
|
+
// consistent by renderVectorScenes / bindSlots / buildCompositeLayers.
|
|
201
|
+
const byLayer = new Map<string, VectorSlot>();
|
|
202
|
+
const droppedLayers = new Set<string>();
|
|
203
|
+
for (const state of states) {
|
|
204
|
+
// Read each wasm getter once — every access crosses the boundary and the
|
|
205
|
+
// uv getters allocate a fresh Float32Array.
|
|
206
|
+
const layerId = state.layer_id;
|
|
207
|
+
const tileHandle = state.tile_handle;
|
|
208
|
+
let slot = byLayer.get(layerId);
|
|
209
|
+
if (!slot) {
|
|
210
|
+
// Rust returns a state for every vector layer with a rendered tile, including
|
|
211
|
+
// non-draped layers (those render in the MRT scene and never create a texturized
|
|
212
|
+
// scene). Only a layer that actually has a draped scene may consume a bake slot;
|
|
213
|
+
// otherwise non-draped layers would occupy the limited slot budget and crowd out
|
|
214
|
+
// genuinely-draped layers past the cap. A draped layer whose scene isn't ready yet
|
|
215
|
+
// simply gets no slot this pass and is picked up once its scene exists.
|
|
216
|
+
const hasScene =
|
|
217
|
+
this.host.texturizedScenes.findSceneByLayerId(tileHandle, layerId) !=
|
|
218
|
+
null;
|
|
219
|
+
if (!hasScene) {
|
|
220
|
+
state.free();
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Cap the layer fan-out to the GPU slot budget; extra layers are dropped.
|
|
224
|
+
if (byLayer.size >= this.host.numTexturizedVector) {
|
|
225
|
+
droppedLayers.add(layerId);
|
|
226
|
+
state.free();
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const reproject = state.reproject_terrain_lat;
|
|
230
|
+
slot = {
|
|
231
|
+
layerId,
|
|
232
|
+
sources: [],
|
|
233
|
+
reproject:
|
|
234
|
+
reproject.length >= 2 ? [reproject[0], reproject[1]] : undefined,
|
|
235
|
+
};
|
|
236
|
+
byLayer.set(layerId, slot);
|
|
237
|
+
}
|
|
238
|
+
const uvOffset = state.uv_offset;
|
|
239
|
+
const uvScale = state.uv_scale;
|
|
240
|
+
slot.sources.push({
|
|
241
|
+
tileHandle,
|
|
242
|
+
uvOffset: [uvOffset[0] ?? 0, uvOffset[1] ?? 0],
|
|
243
|
+
uvScale: [uvScale[0] ?? 1, uvScale[1] ?? 1],
|
|
244
|
+
});
|
|
245
|
+
// wasm-bindgen objects hold Rust-heap memory; release it deterministically
|
|
246
|
+
// instead of waiting for the GC's finalizer (see guide/WASM_API_POLICY.md).
|
|
247
|
+
state.free();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (droppedLayers.size > 0) {
|
|
251
|
+
if (!this.warnedExceededTextures) {
|
|
252
|
+
this.warnedExceededTextures = true;
|
|
253
|
+
console.warn(
|
|
254
|
+
`[TileMesh] Exceeded maximum MVT texture slots: ${byLayer.size + droppedLayers.size} layers requested, ` +
|
|
255
|
+
`but only ${this.host.numTexturizedVector} slots available. ` +
|
|
256
|
+
`Some MVT layers will not be rendered.`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
this.warnedExceededTextures = false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.vectorSlots = [...byLayer.values()];
|
|
264
|
+
this.syncRenderTargets();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Size the drape render-target pool to the live slot count (already capped to
|
|
269
|
+
* `numTexturizedVector` by {@link refreshSlots}): allocate the ones a
|
|
270
|
+
* newly-draped layer needs, dispose the ones a vanished layer freed. Then
|
|
271
|
+
* report the pool's GPU footprint to the memory ledger so it tracks drape
|
|
272
|
+
* memory that scales with terrain subdivision past the vector maxZoom — a cost
|
|
273
|
+
* per-vector-tile accounting cannot see. Reports only when the count changes.
|
|
274
|
+
*/
|
|
275
|
+
private syncRenderTargets() {
|
|
276
|
+
const target = this.vectorSlots.length;
|
|
277
|
+
const rts = this.renderTargets;
|
|
278
|
+
while (rts.length < target) {
|
|
279
|
+
const rt = new WebGLRenderTarget(
|
|
280
|
+
this.host.drapeRtSize,
|
|
281
|
+
this.host.drapeRtSize,
|
|
282
|
+
{
|
|
283
|
+
format: RGBAFormat,
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
// Match the sampler settings syncMaterialSlots applies to rebound RTs,
|
|
287
|
+
// so the lazily-grown one binds identically when bindSlots picks it up.
|
|
288
|
+
configureTexturizedTexture(rt.texture, this.host.textureOptions);
|
|
289
|
+
rts.push(rt);
|
|
290
|
+
}
|
|
291
|
+
while (rts.length > target) {
|
|
292
|
+
rts.pop()?.dispose();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.host.reportDrapeGpuBytes();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Identity of the current slot set + the backing scenes' content. A change means
|
|
300
|
+
* the offscreen scenes must be re-baked and the per-slot material state re-bound.
|
|
301
|
+
*/
|
|
302
|
+
private signature(): string {
|
|
303
|
+
return this.vectorSlots
|
|
304
|
+
.map((slot) => {
|
|
305
|
+
const sourceSig = slot.sources
|
|
306
|
+
.map((s) => {
|
|
307
|
+
const scene = this.host.texturizedScenes.findSceneByLayerId(
|
|
308
|
+
s.tileHandle,
|
|
309
|
+
slot.layerId,
|
|
310
|
+
);
|
|
311
|
+
return `${s.tileHandle}@${s.uvOffset[0]},${s.uvOffset[1]},${s.uvScale[0]},${s.uvScale[1]}#${scene?.revision ?? -1}:${scene?.children.length ?? 0}`;
|
|
312
|
+
})
|
|
313
|
+
.join(",");
|
|
314
|
+
return `${slot.layerId}[${sourceSig}]`;
|
|
315
|
+
})
|
|
316
|
+
.join("|");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Drive each vector slot's main-shader state: texture, per-slot UV transform,
|
|
321
|
+
* visibility and the draped mesh's material attributes. One slot per layer,
|
|
322
|
+
* sourced from the WM vector tile the Rust resolve picked. The resolve walks
|
|
323
|
+
* up to the nearest rendered tile (readiness derived from ECS activation /
|
|
324
|
+
* the Rust resolve, not a JS-side scene_ready flag), so the slot's scene is
|
|
325
|
+
* always bakeable (its own render target was framed by the bake) and the UV
|
|
326
|
+
* is identity — the ancestor LOD fallback lives entirely in Rust.
|
|
327
|
+
*/
|
|
328
|
+
private bindSlots() {
|
|
329
|
+
const m = this.host.material();
|
|
330
|
+
if (!m || !m.userData || !m.userData.textures?.value) return;
|
|
331
|
+
const textures = m.userData.textures.value;
|
|
332
|
+
const uvOffsets: Vector2[] = m.userData.layerUvOffset?.value ?? [];
|
|
333
|
+
const uvScales: Vector2[] = m.userData.layerUvScale?.value ?? [];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < this.host.numTexturizedVector; i++) {
|
|
336
|
+
const lastIdx = this.host.texturizedSceneIndexFrom + i;
|
|
337
|
+
const ownRt = this.renderTargets[i];
|
|
338
|
+
// Slots past the live pool (lazily sized to the draped-layer count) have
|
|
339
|
+
// no render target: hide the slot and rebind the shared empty texture so
|
|
340
|
+
// the shader never samples a stale / disposed texture from a layer that
|
|
341
|
+
// is no longer draped here.
|
|
342
|
+
if (!ownRt) {
|
|
343
|
+
m.userData.shows.value[lastIdx] = 0;
|
|
344
|
+
textures[lastIdx] = EMPTY_TEXTURIZED_TEXTURE;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
textures[lastIdx] = ownRt.texture;
|
|
349
|
+
// The bake framed every source into this RT so it spans the terrain tile's
|
|
350
|
+
// extent: the paste samples it 1:1 in longitude (identity UV here); the
|
|
351
|
+
// latitude axis is reprojected WebMercator→Geographic in the composite
|
|
352
|
+
// shader via the slot's reproject band (see `slotReproject`).
|
|
353
|
+
uvOffsets[lastIdx]?.set(0, 0);
|
|
354
|
+
uvScales[lastIdx]?.set(1, 1);
|
|
355
|
+
|
|
356
|
+
const slot = this.vectorSlots[i];
|
|
357
|
+
const mesh = slot ? this.representativeMesh(slot) : undefined;
|
|
358
|
+
m.userData.shows.value[lastIdx] = mesh ? 1 : 0;
|
|
359
|
+
if (mesh) this.copyMeshAttrs(lastIdx, mesh);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* First available draped mesh backing any of a layer's sources. A clamp-to-ground
|
|
365
|
+
* layer's material attributes (water/specular/emissive/…) are uniform across its
|
|
366
|
+
* tiles, so any source's mesh is a faithful representative for the slot.
|
|
367
|
+
*/
|
|
368
|
+
private representativeMesh(slot: VectorSlot): Object3D | undefined {
|
|
369
|
+
for (const source of slot.sources) {
|
|
370
|
+
const scene = this.host.texturizedScenes.findSceneByLayerId(
|
|
371
|
+
source.tileHandle,
|
|
372
|
+
slot.layerId,
|
|
373
|
+
);
|
|
374
|
+
if (scene && !scene.removed && scene.children.length) {
|
|
375
|
+
return scene.children[0];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Copy a draped mesh's material attributes into the main shader's slot `lastIdx`. */
|
|
382
|
+
private copyMeshAttrs(lastIdx: number, mesh: Object3D) {
|
|
383
|
+
const m = this.host.material();
|
|
384
|
+
if (mesh instanceof PolygonMesh) {
|
|
385
|
+
// Use PolygonMesh getters that expose material enhancer state
|
|
386
|
+
m.userData.reflectivities.value[lastIdx] = mesh.reflectivity;
|
|
387
|
+
m.userData.roughnesses.value[lastIdx] = mesh.roughness;
|
|
388
|
+
m.userData.waters.value[lastIdx] = mesh.water;
|
|
389
|
+
m.userData.waterScaleNormals.value[lastIdx] = mesh.waterScaleNormal;
|
|
390
|
+
m.userData.waterSpeeds.value[lastIdx] = mesh.waterSpeed;
|
|
391
|
+
m.userData.shininesses.value[lastIdx] = mesh.shininess;
|
|
392
|
+
m.userData.specularStrengths.value[lastIdx] = mesh.specularStrength;
|
|
393
|
+
m.userData.applyWaterNormals.value[lastIdx] = mesh.applyWaterNormal;
|
|
394
|
+
m.userData.speculars.value[lastIdx] = mesh.specular;
|
|
395
|
+
m.userData.emissiveIntensities.value[lastIdx] = mesh.emissiveIntensity;
|
|
396
|
+
m.userData.emissiveColors.value[lastIdx].set(mesh.emissiveColor);
|
|
397
|
+
m.userData.effectIdsMasks.value[lastIdx] = mesh.effectIdsMask;
|
|
398
|
+
} else if (mesh instanceof PolylineMesh) {
|
|
399
|
+
m.userData.emissiveIntensities.value[lastIdx] = mesh.emissiveIntensity;
|
|
400
|
+
m.userData.emissiveColors.value[lastIdx].set(mesh.emissiveColor);
|
|
401
|
+
m.userData.effectIdsMasks.value[lastIdx] = mesh.effectIdsMask;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -46,6 +46,7 @@ describe("TileTextureCache.acquire", () => {
|
|
|
46
46
|
expect(reasons?.has("material")).toBe(true);
|
|
47
47
|
expect(reasons?.has("texture-binding")).toBe(true);
|
|
48
48
|
expect(reasons?.has("vector-revision")).toBe(true);
|
|
49
|
+
expect(reasons?.has("raster-revision")).toBe(true);
|
|
49
50
|
expect(reasons?.has("hillshade")).toBe(true);
|
|
50
51
|
});
|
|
51
52
|
});
|
|
@@ -108,6 +109,7 @@ describe("TileTextureCache atlas pooling", () => {
|
|
|
108
109
|
expect(reasons?.has("material")).toBe(true);
|
|
109
110
|
expect(reasons?.has("texture-binding")).toBe(true);
|
|
110
111
|
expect(reasons?.has("vector-revision")).toBe(true);
|
|
112
|
+
expect(reasons?.has("raster-revision")).toBe(true);
|
|
111
113
|
expect(reasons?.has("hillshade")).toBe(true);
|
|
112
114
|
});
|
|
113
115
|
|
|
@@ -258,3 +258,108 @@ describe("TileTextureCompositor.renderVectorScenes", () => {
|
|
|
258
258
|
expect(renderer.clear).toHaveBeenCalledTimes(1);
|
|
259
259
|
});
|
|
260
260
|
});
|
|
261
|
+
|
|
262
|
+
describe("TileTextureCompositor.renderRasterTiles", () => {
|
|
263
|
+
const fakeRT = (): unknown => ({ texture: { needsUpdate: false } });
|
|
264
|
+
// A bake source texture: `configureRasterBakeSource` mutates these sampler
|
|
265
|
+
// fields in place, so a plain object stands in for a three.js Texture.
|
|
266
|
+
const fakeTex = (name: string): unknown => ({
|
|
267
|
+
name,
|
|
268
|
+
colorSpace: "",
|
|
269
|
+
minFilter: 0,
|
|
270
|
+
magFilter: 0,
|
|
271
|
+
generateMipmaps: false,
|
|
272
|
+
needsUpdate: false,
|
|
273
|
+
});
|
|
274
|
+
/** The texture bound to the bake quad at each render call, in draw order. */
|
|
275
|
+
const trackDrawnMaps = (renderer: ReturnType<typeof makeRenderer>) => {
|
|
276
|
+
const maps: unknown[] = [];
|
|
277
|
+
renderer.render.mockImplementation((scene) => {
|
|
278
|
+
const quad = (scene as { children: { material: { map: unknown } }[] })
|
|
279
|
+
.children[0];
|
|
280
|
+
maps.push(quad.material.map);
|
|
281
|
+
});
|
|
282
|
+
return maps;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
it("paints the no-data underlay across a heatmap target before its sources", () => {
|
|
286
|
+
const { compositor, renderer } = setup();
|
|
287
|
+
const maps = trackDrawnMaps(renderer);
|
|
288
|
+
const src = fakeTex("dem");
|
|
289
|
+
|
|
290
|
+
compositor.renderRasterTiles(
|
|
291
|
+
[
|
|
292
|
+
{
|
|
293
|
+
isElevationHeatmap: true,
|
|
294
|
+
noDataColor: [1, 2, 3],
|
|
295
|
+
sources: [
|
|
296
|
+
{ texture: src as never, uvOffset: [0, 0], uvScale: [1, 1] },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
[fakeRT() as never],
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// First draw: the 1×1 no-data texture (decoder boundary color, opaque
|
|
304
|
+
// alpha) spanning the whole target; the DEM source then overwrites the
|
|
305
|
+
// covered region, leaving uncovered regions decoding as no-data.
|
|
306
|
+
expect(maps).toHaveLength(2);
|
|
307
|
+
const underlay = maps[0] as { image: { data: Uint8Array } };
|
|
308
|
+
expect(Array.from(underlay.image.data)).toEqual([1, 2, 3, 255]);
|
|
309
|
+
expect(maps[1]).toBe(src);
|
|
310
|
+
// The underlay is framed over the full target (identity camera window).
|
|
311
|
+
const cam = renderer.render.mock.calls[0][1] as {
|
|
312
|
+
left: number;
|
|
313
|
+
right: number;
|
|
314
|
+
bottom: number;
|
|
315
|
+
top: number;
|
|
316
|
+
};
|
|
317
|
+
expect([cam.left, cam.right, cam.bottom, cam.top]).toEqual([-1, 1, -1, 1]);
|
|
318
|
+
// Cleared once: underlay and source accumulate into the same target.
|
|
319
|
+
expect(renderer.clear).toHaveBeenCalledTimes(1);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("draws sources coarse-first so finer tiles overwrite the ancestor", () => {
|
|
323
|
+
const { compositor, renderer } = setup();
|
|
324
|
+
const maps = trackDrawnMaps(renderer);
|
|
325
|
+
// A finer-than-terrain tile (uvScale > 1) and a coarse ancestor fallback
|
|
326
|
+
// (uvScale < 1) covering the whole rect, listed fine-first: the bake must
|
|
327
|
+
// reorder by ascending uvScale or the ancestor buries the finer tile.
|
|
328
|
+
const fine = fakeTex("fine");
|
|
329
|
+
const coarse = fakeTex("coarse");
|
|
330
|
+
|
|
331
|
+
compositor.renderRasterTiles(
|
|
332
|
+
[
|
|
333
|
+
{
|
|
334
|
+
isElevationHeatmap: false,
|
|
335
|
+
sources: [
|
|
336
|
+
{ texture: fine as never, uvOffset: [0, 0], uvScale: [2, 2] },
|
|
337
|
+
{
|
|
338
|
+
texture: coarse as never,
|
|
339
|
+
uvOffset: [0, 0],
|
|
340
|
+
uvScale: [0.5, 0.5],
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
[fakeRT() as never],
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(maps).toEqual([coarse, fine]);
|
|
349
|
+
expect(renderer.clear).toHaveBeenCalledTimes(1);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("skips a never-touched render target that has no slot (no GPU allocation)", () => {
|
|
353
|
+
const { compositor, renderer } = setup();
|
|
354
|
+
|
|
355
|
+
const rt = fakeRT();
|
|
356
|
+
compositor.renderRasterTiles([], [rt as never]);
|
|
357
|
+
|
|
358
|
+
// Render-targeting the RT would create its GL framebuffer; an empty slot
|
|
359
|
+
// whose target was never baked must not pay that cost (same invariant as
|
|
360
|
+
// the vector bake — both run through the shared bake loop).
|
|
361
|
+
expect(renderer.setRenderTarget).not.toHaveBeenCalledWith(rt);
|
|
362
|
+
expect(renderer.render).not.toHaveBeenCalled();
|
|
363
|
+
expect(renderer.clear).not.toHaveBeenCalled();
|
|
364
|
+
});
|
|
365
|
+
});
|