@navaramap/three 0.0.0 → 0.0.2
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/README.md +70 -0
- package/dist/assets/fontWorker-8oWvyvwL.js +2 -0
- package/dist/assets/fontWorker-8oWvyvwL.js.map +1 -0
- package/dist/assets/index-D9G7kIEj.js +25 -0
- package/dist/assets/index-D9G7kIEj.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-665OLxjy.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.wasm → navara_wasm_worker_bg-Q8zrkLCR.wasm} +0 -0
- package/dist/draco_decoder.wasm +0 -0
- package/dist/draco_decoder2.wasm +0 -0
- package/dist/index.d.ts +587 -67
- package/dist/index.js +5640 -4944
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +238 -211
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +35 -34
- package/src/core/EffectDesc.ts +2 -4
- package/src/core/MeshDesc.ts +1 -2
- package/src/core/MeshDescWithSelectiveEffect.ts +1 -2
- package/src/declutter/DeclutterManager.test.ts +235 -0
- package/src/declutter/DeclutterManager.ts +256 -0
- package/src/declutter/index.ts +7 -0
- package/src/declutter/kernel.ts +46 -0
- package/src/declutter/types.ts +61 -0
- package/src/declutter/wasmKernel.ts +13 -0
- package/src/device.ts +18 -4
- package/src/evaluations/FeatureEvaluator.ts +21 -0
- package/src/event/context.ts +17 -0
- package/src/event/features/modelGpuBytes.ts +1 -3
- package/src/event/hillshade.test.ts +7 -13
- package/src/event/index.ts +2 -3
- package/src/event/tileHandler.ts +54 -0
- package/src/index.ts +143 -33
- package/src/material/enhancer/ShaderReplacer.ts +1 -2
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +6 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +11 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +19 -4
- package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
- package/src/mesh/batchTexture.ts +2 -4
- package/src/mesh/batchedSdfText.ts +61 -1
- package/src/mesh/sdfText.ts +170 -5
- package/src/mesh/sprite/instancedSprite.ts +245 -3
- 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/orchestrators/RenderPassOrchestrator.ts +25 -0
- package/src/passes/CustomRenderPass.ts +21 -5
- package/src/passes/RenderTargetCopyPass.ts +24 -2
- package/src/test-utils/engine.ts +53 -0
- package/src/tileTexture/TileTextureCache.test.ts +2 -0
- package/src/tileTexture/TileTextureCache.ts +1 -0
- package/src/tileTexture/TileTextureCompositor.test.ts +107 -0
- package/src/tileTexture/TileTextureCompositor.ts +248 -32
- 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/src/utils/fixedGpuFootprint.test.ts +110 -0
- package/src/utils/fixedGpuFootprint.ts +162 -0
- package/src/worker/tasks/toCreasedNormals.ts +2 -7
- 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,423 @@
|
|
|
1
|
+
import { generate_id_from_ind_gen, type TileHandle } from "@navaramap/core";
|
|
2
|
+
import {
|
|
3
|
+
NearestFilter,
|
|
4
|
+
NoColorSpace,
|
|
5
|
+
RGBAFormat,
|
|
6
|
+
SRGBColorSpace,
|
|
7
|
+
type Texture,
|
|
8
|
+
WebGLRenderTarget,
|
|
9
|
+
} from "three";
|
|
10
|
+
|
|
11
|
+
import type { TileHandler } from "../../event/context";
|
|
12
|
+
import type { TextureOptions } from "../../textures";
|
|
13
|
+
import type {
|
|
14
|
+
RasterBakeSlot,
|
|
15
|
+
TileTextureCompositor,
|
|
16
|
+
} from "../../tileTexture/TileTextureCompositor";
|
|
17
|
+
import { demNoDataColorBytes } from "../../utils/demNoDataColor";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
configureTexturizedTexture,
|
|
21
|
+
type DrapeResolver,
|
|
22
|
+
} from "./drapeResolver";
|
|
23
|
+
|
|
24
|
+
import type { TileMaterial } from ".";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One WebMercator raster tile baked into a baked layer's render target, resolved
|
|
28
|
+
* by Rust (`getRasterTileStates`). `fragmentId` keys the loaded texture in
|
|
29
|
+
* `loadedTexs`; `uvOffset`/`uvScale` are the **mercator** affine framing the
|
|
30
|
+
* terrain tile's sub-region of that (possibly ancestor-resolved) raster tile,
|
|
31
|
+
* driving the bake camera.
|
|
32
|
+
*/
|
|
33
|
+
type RasterSource = {
|
|
34
|
+
fragmentId: string;
|
|
35
|
+
uvOffset: [number, number];
|
|
36
|
+
uvScale: [number, number];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One baked raster layer draped on this terrain tile. Rust emits one
|
|
41
|
+
* fragment-less composite slot per baked layer; the k-th such slot pairs with
|
|
42
|
+
* `ordinal == k` here, and every overlapping WM tile is baked into the slot's
|
|
43
|
+
* one render target. `reproject` carries the terrain `[south, north]` band for
|
|
44
|
+
* the composite's Mercator latitude remap.
|
|
45
|
+
*/
|
|
46
|
+
type RasterSlot = {
|
|
47
|
+
ordinal: number;
|
|
48
|
+
sources: RasterSource[];
|
|
49
|
+
reproject?: [number, number];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The raster half of the {@link DrapeResolver} family: how a terrain tile's
|
|
54
|
+
* raster (non-hillshade) layers get their textures onto its composite material
|
|
55
|
+
* slots. Two implementations express the WebMercator vs Geographic
|
|
56
|
+
* (quantized-mesh terrain) rendering difference; the resolver is chosen once at
|
|
57
|
+
* tile-mesh creation from `Globe.isGeographicTiling` — a runtime scheme flip
|
|
58
|
+
* rebuilds every tile mesh (`init_globe_tiling` drains the tiling), so it
|
|
59
|
+
* never changes mid-life.
|
|
60
|
+
*
|
|
61
|
+
* WebMercator terrain: raster tiles and terrain tiles share the tiling scheme,
|
|
62
|
+
* so each layer drapes as a single identity (or ancestor-fallback) texture
|
|
63
|
+
* through the regular per-slot material path — there is nothing to resolve,
|
|
64
|
+
* bake, or reproject on this side.
|
|
65
|
+
*/
|
|
66
|
+
export class DirectRasterDrapeResolver implements DrapeResolver {
|
|
67
|
+
update(): void {}
|
|
68
|
+
syncMaterialSlots(): void {}
|
|
69
|
+
slotReproject(): undefined {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
invalidate(): void {}
|
|
73
|
+
liveRenderTargetCount(): number {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
dispose(): void {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The slice of TileMesh the baked resolver works against. `material` is an
|
|
80
|
+
* accessor because the tile's material instance can be swapped over its life
|
|
81
|
+
* (Basic ↔ Lambert). */
|
|
82
|
+
export type RasterDrapeHost = {
|
|
83
|
+
handle: TileHandle;
|
|
84
|
+
tileHandler: TileHandler;
|
|
85
|
+
compositor: TileTextureCompositor;
|
|
86
|
+
/** Decoded source textures keyed by fragment id (shared event-context map). */
|
|
87
|
+
loadedTexs: Map<string, Texture>;
|
|
88
|
+
textureOptions: TextureOptions;
|
|
89
|
+
/** Drape render target side length in texels. */
|
|
90
|
+
drapeRtSize: number;
|
|
91
|
+
/** End of the raster slot region (`[0, texturizedSceneIndexFrom)`). */
|
|
92
|
+
texturizedSceneIndexFrom: number;
|
|
93
|
+
material: () => TileMaterial;
|
|
94
|
+
/** Re-report the tile's drape GPU footprint after the RT pool changed. */
|
|
95
|
+
reportDrapeGpuBytes: () => void;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Geographic (quantized-mesh) terrain: WebMercator raster draped cross-scheme.
|
|
100
|
+
* Owns the baked-slot pairing, the per-layer drape render-target pool and the
|
|
101
|
+
* revision/signature gates, mirroring the texturized-vector drape path.
|
|
102
|
+
* The material's k-th fragment-less non-hillshade slot (`bakedSlotIndices[k]`)
|
|
103
|
+
* is backed by `rasterSlots[k]` baked into `renderTargets[k]`.
|
|
104
|
+
*/
|
|
105
|
+
export class BakedRasterDrapeResolver implements DrapeResolver {
|
|
106
|
+
// Per-ordinal slots resolved by Rust and their render-target pool.
|
|
107
|
+
private rasterSlots: (RasterSlot | undefined)[] = [];
|
|
108
|
+
private renderTargets: (WebGLRenderTarget | undefined)[] = [];
|
|
109
|
+
// The kind each pooled render target was configured for (heatmap = nearest /
|
|
110
|
+
// NoColorSpace, color = linear / sRGB). Color space is fixed at GL allocation,
|
|
111
|
+
// so a layer flipping kinds must get a fresh target, not a reconfigured one.
|
|
112
|
+
private renderTargetIsHeatmap: (boolean | undefined)[] = [];
|
|
113
|
+
// Absolute composite slot index per baked ordinal, derived from the material
|
|
114
|
+
// in `syncMaterialSlots`.
|
|
115
|
+
private bakedSlotIndices: number[] = [];
|
|
116
|
+
private bakedSlotIsHeatmap: boolean[] = [];
|
|
117
|
+
private prevSignature = "";
|
|
118
|
+
private lastRevision = -1;
|
|
119
|
+
// True once every resolved source's texture was present at bake time: the
|
|
120
|
+
// signature can then only change via a slot refresh (revision bump / material
|
|
121
|
+
// slot change), so the per-frame signature hashing is skipped until one.
|
|
122
|
+
private signatureSettled = false;
|
|
123
|
+
|
|
124
|
+
constructor(private readonly host: RasterDrapeHost) {}
|
|
125
|
+
|
|
126
|
+
update(): void {
|
|
127
|
+
if (this.bakedSlotIndices.length === 0) return;
|
|
128
|
+
|
|
129
|
+
// Gated exactly like the vector drape: refresh on the raster resolve
|
|
130
|
+
// revision, re-bake on a slot/texture signature change.
|
|
131
|
+
const revision = this.host.tileHandler.rasterRevision();
|
|
132
|
+
if (revision !== this.lastRevision) {
|
|
133
|
+
this.lastRevision = revision;
|
|
134
|
+
this.refreshSlots();
|
|
135
|
+
this.signatureSettled = false;
|
|
136
|
+
}
|
|
137
|
+
if (!this.signatureSettled) {
|
|
138
|
+
const signature = this.signature();
|
|
139
|
+
if (signature !== this.prevSignature) {
|
|
140
|
+
this.prevSignature = signature;
|
|
141
|
+
this.bakeSlots();
|
|
142
|
+
}
|
|
143
|
+
// Once every resolved source's texture is present (and thus baked), the
|
|
144
|
+
// signature is stable until the next refresh — skip hashing per frame.
|
|
145
|
+
// A vanished texture always comes with a Rust-side destroy bump, which
|
|
146
|
+
// refreshes and un-settles.
|
|
147
|
+
this.signatureSettled = this.allTexturesLoaded();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
syncMaterialSlots(
|
|
152
|
+
textureFragments: readonly (string | null | undefined)[] | undefined,
|
|
153
|
+
): void {
|
|
154
|
+
// A non-hillshade raster slot with no fragment entity is backed by a
|
|
155
|
+
// per-layer baked render target instead of a direct texture. The k-th such
|
|
156
|
+
// slot pairs with `layer_ordinal == k` from `getRasterTileStates` — both
|
|
157
|
+
// sides derive k from the same sorted layer list.
|
|
158
|
+
const ud = this.host.material().userData;
|
|
159
|
+
const prevIndices = this.bakedSlotIndices;
|
|
160
|
+
const prevIsHeatmap = this.bakedSlotIsHeatmap;
|
|
161
|
+
this.bakedSlotIndices = [];
|
|
162
|
+
this.bakedSlotIsHeatmap = [];
|
|
163
|
+
const isHillshades: boolean[] = ud.isHillshades?.value ?? [];
|
|
164
|
+
const isHeatmaps: boolean[] = ud.isElevationHeatmaps?.value ?? [];
|
|
165
|
+
const len = Math.min(
|
|
166
|
+
textureFragments?.length ?? 0,
|
|
167
|
+
this.host.texturizedSceneIndexFrom,
|
|
168
|
+
);
|
|
169
|
+
for (let i = 0; i < len; i++) {
|
|
170
|
+
if (!textureFragments?.[i] && !isHillshades[i]) {
|
|
171
|
+
this.bakedSlotIndices.push(i);
|
|
172
|
+
this.bakedSlotIsHeatmap.push(!!isHeatmaps[i]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// The RT pool follows the baked slot count (disposes leftovers when layers
|
|
176
|
+
// vanish).
|
|
177
|
+
this.syncRenderTargets();
|
|
178
|
+
const slotsChanged =
|
|
179
|
+
prevIndices.length !== this.bakedSlotIndices.length ||
|
|
180
|
+
this.bakedSlotIndices.some((v, i) => v !== prevIndices[i]) ||
|
|
181
|
+
this.bakedSlotIsHeatmap.some((v, i) => v !== prevIsHeatmap[i]);
|
|
182
|
+
if (slotsChanged) {
|
|
183
|
+
// Slot pairing changed (layers added/removed/reordered): re-resolve + re-bake.
|
|
184
|
+
this.prevSignature = "";
|
|
185
|
+
this.signatureSettled = false;
|
|
186
|
+
} else {
|
|
187
|
+
// Same slots, and the render targets still hold the last bake — but the
|
|
188
|
+
// material texture reset nulled the bindings, so rebind them without
|
|
189
|
+
// paying a re-bake. Material updates fire on every tile activation during
|
|
190
|
+
// camera motion; an unconditional signature reset here re-baked every such
|
|
191
|
+
// tile every time (an FPS killer).
|
|
192
|
+
this.bindSlots();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
slotReproject(absSlot: number): [number, number] | undefined {
|
|
197
|
+
// Stashed by `setupTextures` from the Rust material: the per-slot flag plus
|
|
198
|
+
// the tile-wide terrain `[south, north]` band. Baked slots have identity
|
|
199
|
+
// UV, so the band recovered from the affine is exactly the terrain band.
|
|
200
|
+
const ud = this.host.material().userData;
|
|
201
|
+
const layerReproject: number[] = ud.layerReproject ?? [];
|
|
202
|
+
const terrainLatRange: number[] = ud.terrainLatRange ?? [];
|
|
203
|
+
return layerReproject[absSlot] === 1 && terrainLatRange.length === 2
|
|
204
|
+
? [terrainLatRange[0], terrainLatRange[1]]
|
|
205
|
+
: undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
invalidate(): void {
|
|
209
|
+
this.prevSignature = "";
|
|
210
|
+
this.signatureSettled = false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
liveRenderTargetCount(): number {
|
|
214
|
+
let count = 0;
|
|
215
|
+
for (const rt of this.renderTargets) {
|
|
216
|
+
if (rt) count++;
|
|
217
|
+
}
|
|
218
|
+
return count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
dispose(): void {
|
|
222
|
+
for (const rt of this.renderTargets) {
|
|
223
|
+
rt?.dispose();
|
|
224
|
+
}
|
|
225
|
+
this.renderTargets.length = 0;
|
|
226
|
+
this.renderTargetIsHeatmap.length = 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Pull the Rust-resolved baked-raster states for this terrain tile into
|
|
231
|
+
* {@link rasterSlots}: one slot per baked (non-hillshade) raster layer,
|
|
232
|
+
* grouped by `layer_ordinal` — the raster twin of the vector-slot refresh.
|
|
233
|
+
* An unloaded layer leaves a hole (its ordinal stays paired with its material
|
|
234
|
+
* slot; the slot just stays transparent).
|
|
235
|
+
*/
|
|
236
|
+
private refreshSlots() {
|
|
237
|
+
const states =
|
|
238
|
+
this.host.tileHandler.getRasterTileStates(this.host.handle) ?? [];
|
|
239
|
+
|
|
240
|
+
const byOrdinal: (RasterSlot | undefined)[] = [];
|
|
241
|
+
for (const state of states) {
|
|
242
|
+
const ordinal = state.layer_ordinal;
|
|
243
|
+
let slot = byOrdinal[ordinal];
|
|
244
|
+
if (!slot) {
|
|
245
|
+
const reproject = state.reproject_terrain_lat;
|
|
246
|
+
slot = {
|
|
247
|
+
ordinal,
|
|
248
|
+
sources: [],
|
|
249
|
+
reproject:
|
|
250
|
+
reproject.length >= 2 ? [reproject[0], reproject[1]] : undefined,
|
|
251
|
+
};
|
|
252
|
+
byOrdinal[ordinal] = slot;
|
|
253
|
+
}
|
|
254
|
+
// Read each wasm getter once — every access crosses the boundary and the
|
|
255
|
+
// uv getters allocate a fresh Float32Array.
|
|
256
|
+
const uvOffset = state.uv_offset;
|
|
257
|
+
const uvScale = state.uv_scale;
|
|
258
|
+
slot.sources.push({
|
|
259
|
+
fragmentId: generate_id_from_ind_gen(
|
|
260
|
+
state.fragment_ind,
|
|
261
|
+
state.fragment_gen,
|
|
262
|
+
),
|
|
263
|
+
uvOffset: [uvOffset[0] ?? 0, uvOffset[1] ?? 0],
|
|
264
|
+
uvScale: [uvScale[0] ?? 1, uvScale[1] ?? 1],
|
|
265
|
+
});
|
|
266
|
+
// wasm-bindgen objects hold Rust-heap memory; release it deterministically
|
|
267
|
+
// instead of waiting for the GC's finalizer (see guide/WASM_API_POLICY.md).
|
|
268
|
+
state.free();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.rasterSlots = byOrdinal;
|
|
272
|
+
this.syncRenderTargets();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Size the render-target pool to the live baked slots: allocate lazily per
|
|
277
|
+
* ordinal with sources, dispose when a layer vanishes. Heatmap layers get a
|
|
278
|
+
* value-preserving target (nearest, `NoColorSpace`, no mips) so the encoded
|
|
279
|
+
* DEM survives the bake; color layers mirror the vector drape targets
|
|
280
|
+
* (`configureTexturizedTexture`) plus an sRGB declaration so the composite
|
|
281
|
+
* decodes them exactly like the direct (non-baked) textures.
|
|
282
|
+
*/
|
|
283
|
+
private syncRenderTargets() {
|
|
284
|
+
const rts = this.renderTargets;
|
|
285
|
+
const kinds = this.renderTargetIsHeatmap;
|
|
286
|
+
const count = this.bakedSlotIndices.length;
|
|
287
|
+
for (let ordinal = 0; ordinal < count; ordinal++) {
|
|
288
|
+
const active = !!this.rasterSlots[ordinal]?.sources.length;
|
|
289
|
+
const isHeatmap = this.bakedSlotIsHeatmap[ordinal];
|
|
290
|
+
// A layer that flipped kinds (color ↔ heatmap via updateLayer) must not
|
|
291
|
+
// reuse the old target: its color space is baked into the GL allocation,
|
|
292
|
+
// and a heatmap baked into an sRGB/linear-filtered target decodes to
|
|
293
|
+
// garbage elevations. Dispose and let the branch below re-allocate.
|
|
294
|
+
if (rts[ordinal] && kinds[ordinal] !== isHeatmap) {
|
|
295
|
+
rts[ordinal]?.dispose();
|
|
296
|
+
rts[ordinal] = undefined;
|
|
297
|
+
}
|
|
298
|
+
if (active && !rts[ordinal]) {
|
|
299
|
+
const rt = new WebGLRenderTarget(
|
|
300
|
+
this.host.drapeRtSize,
|
|
301
|
+
this.host.drapeRtSize,
|
|
302
|
+
{
|
|
303
|
+
format: RGBAFormat,
|
|
304
|
+
depthBuffer: false,
|
|
305
|
+
stencilBuffer: false,
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
if (isHeatmap) {
|
|
309
|
+
rt.texture.minFilter = NearestFilter;
|
|
310
|
+
rt.texture.magFilter = NearestFilter;
|
|
311
|
+
rt.texture.generateMipmaps = false;
|
|
312
|
+
rt.texture.colorSpace = NoColorSpace;
|
|
313
|
+
} else {
|
|
314
|
+
configureTexturizedTexture(rt.texture, this.host.textureOptions);
|
|
315
|
+
rt.texture.colorSpace = SRGBColorSpace;
|
|
316
|
+
}
|
|
317
|
+
rts[ordinal] = rt;
|
|
318
|
+
kinds[ordinal] = isHeatmap;
|
|
319
|
+
} else if (!active && rts[ordinal]) {
|
|
320
|
+
rts[ordinal]?.dispose();
|
|
321
|
+
rts[ordinal] = undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Layers removed: drop targets past the live baked-slot count.
|
|
325
|
+
for (let ordinal = count; ordinal < rts.length; ordinal++) {
|
|
326
|
+
rts[ordinal]?.dispose();
|
|
327
|
+
}
|
|
328
|
+
rts.length = count;
|
|
329
|
+
kinds.length = count;
|
|
330
|
+
|
|
331
|
+
this.host.reportDrapeGpuBytes();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Identity of the baked slot set + each source texture's availability. A change
|
|
336
|
+
* means the sources must be re-baked and re-bound — including a texture arriving
|
|
337
|
+
* in `loadedTexs` after the resolve (decode is async).
|
|
338
|
+
*/
|
|
339
|
+
private signature(): string {
|
|
340
|
+
const { loadedTexs } = this.host;
|
|
341
|
+
return this.bakedSlotIndices
|
|
342
|
+
.map((absSlot, ordinal) => {
|
|
343
|
+
const sourceSig = this.rasterSlots[ordinal]?.sources
|
|
344
|
+
.map(
|
|
345
|
+
(s) =>
|
|
346
|
+
`${s.fragmentId}@${s.uvOffset[0]},${s.uvOffset[1]},${s.uvScale[0]},${s.uvScale[1]}#${loadedTexs.has(s.fragmentId) ? 1 : 0}`,
|
|
347
|
+
)
|
|
348
|
+
.join(",");
|
|
349
|
+
return `${absSlot}:${this.bakedSlotIsHeatmap[ordinal] ? 1 : 0}[${sourceSig ?? ""}]`;
|
|
350
|
+
})
|
|
351
|
+
.join("|");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Whether every resolved source's texture is present in `loadedTexs`. */
|
|
355
|
+
private allTexturesLoaded(): boolean {
|
|
356
|
+
const { loadedTexs } = this.host;
|
|
357
|
+
for (const slot of this.rasterSlots) {
|
|
358
|
+
if (!slot) continue;
|
|
359
|
+
for (const source of slot.sources) {
|
|
360
|
+
if (!loadedTexs.has(source.fragmentId)) return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Bake each baked layer's loaded source textures into its render target and
|
|
368
|
+
* bind the targets to the material slots — the raster twin of the
|
|
369
|
+
* `renderVectorScenes` + `bindVectorSlots` pair.
|
|
370
|
+
*/
|
|
371
|
+
private bakeSlots() {
|
|
372
|
+
const { loadedTexs } = this.host;
|
|
373
|
+
const ud = this.host.material().userData;
|
|
374
|
+
// The decoder's no-data color: painted under a baked heatmap's sources so
|
|
375
|
+
// uncovered regions decode as "no elevation" (rendered transparent by the
|
|
376
|
+
// composite) — the DEM alpha channel is never used for coverage.
|
|
377
|
+
const noDataColor =
|
|
378
|
+
demNoDataColorBytes(
|
|
379
|
+
ud.elevationRGBScaler?.value ?? { x: 0, y: 0, z: 0 },
|
|
380
|
+
ud.elevationMinMaxHeightAndBoundary?.value.z ?? 0,
|
|
381
|
+
) ?? undefined;
|
|
382
|
+
const bakeSlots: (RasterBakeSlot | undefined)[] = this.bakedSlotIndices.map(
|
|
383
|
+
(_, ordinal) => {
|
|
384
|
+
const slot = this.rasterSlots[ordinal];
|
|
385
|
+
// No resolved sources → no live render target for this ordinal.
|
|
386
|
+
if (!slot?.sources.length) return undefined;
|
|
387
|
+
const isElevationHeatmap = this.bakedSlotIsHeatmap[ordinal];
|
|
388
|
+
// Loaded textures only; a slot whose textures are all still decoding
|
|
389
|
+
// keeps its (cleared + no-data-painted) target until they land.
|
|
390
|
+
const sources = slot.sources.flatMap((s) => {
|
|
391
|
+
const texture = loadedTexs.get(s.fragmentId);
|
|
392
|
+
return texture
|
|
393
|
+
? [{ texture, uvOffset: s.uvOffset, uvScale: s.uvScale }]
|
|
394
|
+
: [];
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
isElevationHeatmap,
|
|
398
|
+
noDataColor: isElevationHeatmap ? noDataColor : undefined,
|
|
399
|
+
sources,
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
this.host.compositor.renderRasterTiles(bakeSlots, this.renderTargets);
|
|
404
|
+
this.bindSlots();
|
|
405
|
+
this.host.compositor.markDirty(this.host.handle, "raster-revision");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Bind each baked layer's render target to its material slot. Shows, colors
|
|
410
|
+
* and opacities already cross via the material's per-slot arrays (the baked
|
|
411
|
+
* slot is a regular material slot, just fragment-less); only the texture is
|
|
412
|
+
* bound here. A slot with no target yet stays `null`, which the composite
|
|
413
|
+
* skips — transparent until the first source texture arrives.
|
|
414
|
+
*/
|
|
415
|
+
private bindSlots() {
|
|
416
|
+
const textures = this.host.material()?.userData?.textures?.value;
|
|
417
|
+
if (!textures) return;
|
|
418
|
+
for (let ordinal = 0; ordinal < this.bakedSlotIndices.length; ordinal++) {
|
|
419
|
+
const absSlot = this.bakedSlotIndices[ordinal];
|
|
420
|
+
textures[absSlot] = this.renderTargets[ordinal]?.texture ?? null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|