@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
|
@@ -2,6 +2,7 @@ import { Mesh, BoxGeometry, MeshBasicMaterial } from "three";
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { TexturizedSceneByTileCoordinates } from "../scene";
|
|
5
|
+
import { createTestTileHandler } from "../test-utils/engine";
|
|
5
6
|
|
|
6
7
|
import { TileTextureCompositor } from "./TileTextureCompositor";
|
|
7
8
|
import type { AtlasFactory, CompositeAtlas } from "./types";
|
|
@@ -47,6 +48,7 @@ function setup() {
|
|
|
47
48
|
typeof TileTextureCompositor
|
|
48
49
|
>[0]["renderer"],
|
|
49
50
|
texturizedSceneByTileCoordinates: texturizedScenes,
|
|
51
|
+
mercatorY: createTestTileHandler().mercatorY,
|
|
50
52
|
atlasFactory: fakeFactory,
|
|
51
53
|
});
|
|
52
54
|
return { compositor, renderer, texturizedScenes };
|
|
@@ -258,3 +260,108 @@ describe("TileTextureCompositor.renderVectorScenes", () => {
|
|
|
258
260
|
expect(renderer.clear).toHaveBeenCalledTimes(1);
|
|
259
261
|
});
|
|
260
262
|
});
|
|
263
|
+
|
|
264
|
+
describe("TileTextureCompositor.renderRasterTiles", () => {
|
|
265
|
+
const fakeRT = (): unknown => ({ texture: { needsUpdate: false } });
|
|
266
|
+
// A bake source texture: `configureRasterBakeSource` mutates these sampler
|
|
267
|
+
// fields in place, so a plain object stands in for a three.js Texture.
|
|
268
|
+
const fakeTex = (name: string): unknown => ({
|
|
269
|
+
name,
|
|
270
|
+
colorSpace: "",
|
|
271
|
+
minFilter: 0,
|
|
272
|
+
magFilter: 0,
|
|
273
|
+
generateMipmaps: false,
|
|
274
|
+
needsUpdate: false,
|
|
275
|
+
});
|
|
276
|
+
/** The texture bound to the bake quad at each render call, in draw order. */
|
|
277
|
+
const trackDrawnMaps = (renderer: ReturnType<typeof makeRenderer>) => {
|
|
278
|
+
const maps: unknown[] = [];
|
|
279
|
+
renderer.render.mockImplementation((scene) => {
|
|
280
|
+
const quad = (scene as { children: { material: { map: unknown } }[] })
|
|
281
|
+
.children[0];
|
|
282
|
+
maps.push(quad.material.map);
|
|
283
|
+
});
|
|
284
|
+
return maps;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
it("paints the no-data underlay across a heatmap target before its sources", () => {
|
|
288
|
+
const { compositor, renderer } = setup();
|
|
289
|
+
const maps = trackDrawnMaps(renderer);
|
|
290
|
+
const src = fakeTex("dem");
|
|
291
|
+
|
|
292
|
+
compositor.renderRasterTiles(
|
|
293
|
+
[
|
|
294
|
+
{
|
|
295
|
+
isElevationHeatmap: true,
|
|
296
|
+
noDataColor: [1, 2, 3],
|
|
297
|
+
sources: [
|
|
298
|
+
{ texture: src as never, uvOffset: [0, 0], uvScale: [1, 1] },
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
[fakeRT() as never],
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// First draw: the 1×1 no-data texture (decoder boundary color, opaque
|
|
306
|
+
// alpha) spanning the whole target; the DEM source then overwrites the
|
|
307
|
+
// covered region, leaving uncovered regions decoding as no-data.
|
|
308
|
+
expect(maps).toHaveLength(2);
|
|
309
|
+
const underlay = maps[0] as { image: { data: Uint8Array } };
|
|
310
|
+
expect(Array.from(underlay.image.data)).toEqual([1, 2, 3, 255]);
|
|
311
|
+
expect(maps[1]).toBe(src);
|
|
312
|
+
// The underlay is framed over the full target (identity camera window).
|
|
313
|
+
const cam = renderer.render.mock.calls[0][1] as {
|
|
314
|
+
left: number;
|
|
315
|
+
right: number;
|
|
316
|
+
bottom: number;
|
|
317
|
+
top: number;
|
|
318
|
+
};
|
|
319
|
+
expect([cam.left, cam.right, cam.bottom, cam.top]).toEqual([-1, 1, -1, 1]);
|
|
320
|
+
// Cleared once: underlay and source accumulate into the same target.
|
|
321
|
+
expect(renderer.clear).toHaveBeenCalledTimes(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("draws sources coarse-first so finer tiles overwrite the ancestor", () => {
|
|
325
|
+
const { compositor, renderer } = setup();
|
|
326
|
+
const maps = trackDrawnMaps(renderer);
|
|
327
|
+
// A finer-than-terrain tile (uvScale > 1) and a coarse ancestor fallback
|
|
328
|
+
// (uvScale < 1) covering the whole rect, listed fine-first: the bake must
|
|
329
|
+
// reorder by ascending uvScale or the ancestor buries the finer tile.
|
|
330
|
+
const fine = fakeTex("fine");
|
|
331
|
+
const coarse = fakeTex("coarse");
|
|
332
|
+
|
|
333
|
+
compositor.renderRasterTiles(
|
|
334
|
+
[
|
|
335
|
+
{
|
|
336
|
+
isElevationHeatmap: false,
|
|
337
|
+
sources: [
|
|
338
|
+
{ texture: fine as never, uvOffset: [0, 0], uvScale: [2, 2] },
|
|
339
|
+
{
|
|
340
|
+
texture: coarse as never,
|
|
341
|
+
uvOffset: [0, 0],
|
|
342
|
+
uvScale: [0.5, 0.5],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
[fakeRT() as never],
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(maps).toEqual([coarse, fine]);
|
|
351
|
+
expect(renderer.clear).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("skips a never-touched render target that has no slot (no GPU allocation)", () => {
|
|
355
|
+
const { compositor, renderer } = setup();
|
|
356
|
+
|
|
357
|
+
const rt = fakeRT();
|
|
358
|
+
compositor.renderRasterTiles([], [rt as never]);
|
|
359
|
+
|
|
360
|
+
// Render-targeting the RT would create its GL framebuffer; an empty slot
|
|
361
|
+
// whose target was never baked must not pay that cost (same invariant as
|
|
362
|
+
// the vector bake — both run through the shared bake loop).
|
|
363
|
+
expect(renderer.setRenderTarget).not.toHaveBeenCalledWith(rt);
|
|
364
|
+
expect(renderer.render).not.toHaveBeenCalled();
|
|
365
|
+
expect(renderer.clear).not.toHaveBeenCalled();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -3,12 +3,18 @@ import {
|
|
|
3
3
|
Color,
|
|
4
4
|
DataTexture,
|
|
5
5
|
GLSL3,
|
|
6
|
+
LinearFilter,
|
|
6
7
|
Mesh,
|
|
8
|
+
MeshBasicMaterial,
|
|
9
|
+
NearestFilter,
|
|
10
|
+
NoBlending,
|
|
11
|
+
NoColorSpace,
|
|
7
12
|
OrthographicCamera,
|
|
8
13
|
PlaneGeometry,
|
|
9
14
|
RGBAFormat,
|
|
10
15
|
Scene,
|
|
11
16
|
ShaderMaterial,
|
|
17
|
+
SRGBColorSpace,
|
|
12
18
|
type Texture,
|
|
13
19
|
UnsignedByteType,
|
|
14
20
|
WebGLRenderTarget,
|
|
@@ -71,6 +77,10 @@ export const defaultAtlasFactory =
|
|
|
71
77
|
export type TileTextureCompositorOptions = {
|
|
72
78
|
renderer: WebGLRenderer;
|
|
73
79
|
texturizedSceneByTileCoordinates: TexturizedSceneByTileCoordinates;
|
|
80
|
+
/** Rust's clamped WebMercator northing (`TileHandler.mercatorY`), handed to
|
|
81
|
+
* the core uniform mutates so the paste-side reprojection constants come
|
|
82
|
+
* from the same implementation as the Rust bake affine. */
|
|
83
|
+
mercatorY: (lat: number) => number;
|
|
74
84
|
/** Atlas RT side length (defaults to 512). */
|
|
75
85
|
size?: number;
|
|
76
86
|
/** Test seam: replace MRT RT creation. */
|
|
@@ -89,6 +99,30 @@ type CachedMaterial = {
|
|
|
89
99
|
placeholderTexture: Texture;
|
|
90
100
|
};
|
|
91
101
|
|
|
102
|
+
/** One WebMercator raster tile texture to bake into a layer's render target,
|
|
103
|
+
* framed by the Rust-resolved mercator affine (terrain `[0,1]` UV → source UV). */
|
|
104
|
+
export type RasterBakeSource = {
|
|
105
|
+
texture: Texture;
|
|
106
|
+
uvOffset: [number, number];
|
|
107
|
+
uvScale: [number, number];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** One baked raster layer: its overlapping WM tile textures, mosaicked into one
|
|
111
|
+
* render target. Heatmap layers carry encoded DEM, copied raw (nearest, no
|
|
112
|
+
* color conversion) so the composite decode stays valid. */
|
|
113
|
+
export type RasterBakeSlot = {
|
|
114
|
+
isElevationHeatmap: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* The decoder's no-data (boundary) color for heatmap slots: painted across
|
|
117
|
+
* the render target before the sources so uncovered regions decode as
|
|
118
|
+
* "no elevation" — the no-data story lives entirely in the encoded-elevation
|
|
119
|
+
* domain, keeping the alpha channel free for future RGBA encodings. Absent
|
|
120
|
+
* for color slots and for decoders with no byte-representable boundary.
|
|
121
|
+
*/
|
|
122
|
+
noDataColor?: [number, number, number];
|
|
123
|
+
sources: RasterBakeSource[];
|
|
124
|
+
};
|
|
125
|
+
|
|
92
126
|
/**
|
|
93
127
|
* Integration layer for per-tile texture composition.
|
|
94
128
|
*
|
|
@@ -112,9 +146,11 @@ export class TileTextureCompositor {
|
|
|
112
146
|
* per-tile GPU byte cost it reports to the memory ledger. */
|
|
113
147
|
readonly size: number;
|
|
114
148
|
private readonly texturizedScenes: TexturizedSceneByTileCoordinates;
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
149
|
+
private readonly mercatorY: (lat: number) => number;
|
|
150
|
+
// Single camera shared by every drape bake (vector and raster), re-framed
|
|
151
|
+
// per source by `frameBakeCamera`. Ancestor fallback is resolved in Rust,
|
|
152
|
+
// so no per-tile camera transform is needed.
|
|
153
|
+
private readonly bakeCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
118
154
|
|
|
119
155
|
// Composite-pass machinery (lazily created on first use so tests that
|
|
120
156
|
// don't render don't pay the allocation).
|
|
@@ -122,15 +158,43 @@ export class TileTextureCompositor {
|
|
|
122
158
|
private readonly quadCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
123
159
|
private readonly quadMesh: Mesh;
|
|
124
160
|
private readonly materialCache = new Map<string, CachedMaterial>();
|
|
125
|
-
//
|
|
161
|
+
// Drape render targets this compositor has ever rendered to. three.js
|
|
126
162
|
// allocates a render target's GL framebuffer on first setRenderTarget, so
|
|
127
163
|
// an empty slot whose target was never touched must be skipped entirely —
|
|
128
164
|
// clearing it would allocate size² × 4 bytes of GPU memory for nothing.
|
|
129
|
-
private readonly
|
|
165
|
+
private readonly touchedTargets = new WeakSet<WebGLRenderTarget>();
|
|
166
|
+
|
|
167
|
+
// Raster-bake machinery (lazily used): a textured quad drawn once per source
|
|
168
|
+
// through the same windowing camera as the vector bake. `toneMapped: false` +
|
|
169
|
+
// per-kind texture color spaces keep the bake a value-preserving copy.
|
|
170
|
+
//
|
|
171
|
+
// `transparent: true` + `blending: NoBlending` carries the source ALPHA into
|
|
172
|
+
// the render target: an opaque material (transparent=false + NormalBlending)
|
|
173
|
+
// gets three.js's `OPAQUE` define, whose shader forces `diffuseColor.a = 1.0`
|
|
174
|
+
// and destroys the tile's transparency. NoBlending keeps replace semantics —
|
|
175
|
+
// sources are disjoint WM tiles (plus a coarse-first ancestor underlay), so
|
|
176
|
+
// each draw must overwrite, not blend, and straight (r, g, b, a) texels land
|
|
177
|
+
// in the target for the composite's own alpha blend.
|
|
178
|
+
private readonly rasterBakeScene = new Scene();
|
|
179
|
+
private readonly rasterBakeMaterial = new MeshBasicMaterial({
|
|
180
|
+
toneMapped: false,
|
|
181
|
+
depthTest: false,
|
|
182
|
+
depthWrite: false,
|
|
183
|
+
transparent: true,
|
|
184
|
+
blending: NoBlending,
|
|
185
|
+
});
|
|
186
|
+
// 1×1 no-data underlay for baked heatmap targets, drawn through the same
|
|
187
|
+
// value-preserving bake pipeline as the sources — a texture upload is
|
|
188
|
+
// byte-exact, whereas a clear color would pass through the renderer's color
|
|
189
|
+
// management and could land one byte off (fatal for positional encodings,
|
|
190
|
+
// where the high byte weighs 65536 in decoded x). Cached per color.
|
|
191
|
+
private demNoDataTexture: DataTexture | null = null;
|
|
192
|
+
private demNoDataTextureKey = "";
|
|
130
193
|
|
|
131
194
|
constructor(opts: TileTextureCompositorOptions) {
|
|
132
195
|
this.renderer = opts.renderer;
|
|
133
196
|
this.texturizedScenes = opts.texturizedSceneByTileCoordinates;
|
|
197
|
+
this.mercatorY = opts.mercatorY;
|
|
134
198
|
this.size = opts.size ?? 512;
|
|
135
199
|
this.cache = new TileTextureCache({
|
|
136
200
|
size: this.size,
|
|
@@ -141,7 +205,15 @@ export class TileTextureCompositor {
|
|
|
141
205
|
this.quadMesh = new Mesh(new PlaneGeometry(2, 2));
|
|
142
206
|
this.quadScene.add(this.quadMesh);
|
|
143
207
|
this.quadCamera.position.z = 1;
|
|
144
|
-
this.
|
|
208
|
+
this.bakeCamera.position.z = 1;
|
|
209
|
+
|
|
210
|
+
// Raster-bake quad: spans the source tile's NDC frame like a vector scene,
|
|
211
|
+
// so the same windowing camera math frames it. Shares the fullscreen quad
|
|
212
|
+
// geometry with `quadMesh` (both are static [-1, 1] planes; the windowing
|
|
213
|
+
// lives in the camera), so dispose() frees one geometry for both.
|
|
214
|
+
this.rasterBakeScene.add(
|
|
215
|
+
new Mesh(this.quadMesh.geometry, this.rasterBakeMaterial),
|
|
216
|
+
);
|
|
145
217
|
}
|
|
146
218
|
|
|
147
219
|
// ---------------------------------------------------------------------
|
|
@@ -204,7 +276,88 @@ export class TileTextureCompositor {
|
|
|
204
276
|
}[],
|
|
205
277
|
renderTargets: WebGLRenderTarget[],
|
|
206
278
|
): void {
|
|
207
|
-
|
|
279
|
+
this.bakeSlotTargets(slots, renderTargets, (slot) => {
|
|
280
|
+
for (const source of slot.sources) {
|
|
281
|
+
const scene = this.texturizedScenes.findSceneByLayerId(
|
|
282
|
+
source.tileHandle,
|
|
283
|
+
slot.layerId,
|
|
284
|
+
);
|
|
285
|
+
if (!scene || scene.removed || !scene.children.length) continue;
|
|
286
|
+
this.renderer.render(
|
|
287
|
+
scene,
|
|
288
|
+
this.frameBakeCamera(source.uvOffset, source.uvScale),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Bake each raster layer's overlapping WM tile textures into ONE render
|
|
296
|
+
* target (slot `i` → `renderTargets[i]`), mirroring {@link renderVectorScenes}:
|
|
297
|
+
* every source is a textured quad framed by the Rust-supplied mercator affine
|
|
298
|
+
* `uvOffset`/`uvScale`, so the render target ends up spanning the terrain
|
|
299
|
+
* tile's (Mercator-projected) extent and the composite treats the whole layer
|
|
300
|
+
* as one slot with the terrain-band reprojection — instead of one composite
|
|
301
|
+
* slot per overlapping tile, which overflowed the GPU slot budget on
|
|
302
|
+
* Geographic terrain once 3+ layers were draped.
|
|
303
|
+
*
|
|
304
|
+
* Sources are drawn coarse-first (ascending `uvScale`): an ancestor fallback
|
|
305
|
+
* covers the whole terrain rect, so painting it first lets the finer tiles
|
|
306
|
+
* overwrite it instead of being buried under coarse texels. The bake is a
|
|
307
|
+
* value-preserving copy (`toneMapped: false`; per-kind color spaces below), so
|
|
308
|
+
* heatmap DEM texels survive it bit-faithfully:
|
|
309
|
+
* - color imagery: source + target declared sRGB — decode at fetch, encode at
|
|
310
|
+
* write, decode again at the composite fetch: the same linear values the
|
|
311
|
+
* direct (non-baked) path produced.
|
|
312
|
+
* - elevation heatmap: everything `NoColorSpace` + nearest — raw texels, no
|
|
313
|
+
* interpolation, so the composite's decode-then-interpolate keeps working.
|
|
314
|
+
*/
|
|
315
|
+
renderRasterTiles(
|
|
316
|
+
slots: (RasterBakeSlot | undefined)[],
|
|
317
|
+
renderTargets: (WebGLRenderTarget | undefined)[],
|
|
318
|
+
): void {
|
|
319
|
+
this.bakeSlotTargets(slots, renderTargets, (slot) => {
|
|
320
|
+
// Heatmap targets: paint the decoder's no-data color across the whole
|
|
321
|
+
// target first, so regions no source covers decode as "no elevation"
|
|
322
|
+
// (the composite renders them transparent) instead of decoding the
|
|
323
|
+
// cleared black as a height.
|
|
324
|
+
if (slot.isElevationHeatmap && slot.noDataColor) {
|
|
325
|
+
this.drawRasterBakeQuad(
|
|
326
|
+
this.demNoDataUnderlay(slot.noDataColor),
|
|
327
|
+
true,
|
|
328
|
+
[0, 0],
|
|
329
|
+
[1, 1],
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Coarse-first painter's order: a shared ancestor spans every finer
|
|
334
|
+
// source's rect, so it must not be drawn over them.
|
|
335
|
+
const sources = [...slot.sources].sort(
|
|
336
|
+
(a, b) => a.uvScale[0] - b.uvScale[0],
|
|
337
|
+
);
|
|
338
|
+
for (const source of sources) {
|
|
339
|
+
this.drawRasterBakeQuad(
|
|
340
|
+
source.texture,
|
|
341
|
+
slot.isElevationHeatmap,
|
|
342
|
+
source.uvOffset,
|
|
343
|
+
source.uvScale,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Shared bake loop over per-layer drape render targets (slot `i` →
|
|
351
|
+
* `renderTargets[i]`): saves/restores the renderer state, clears each live
|
|
352
|
+
* target once and lets `drawSlot` accumulate that slot's sources into it —
|
|
353
|
+
* autoClear stays off so the sources mosaic instead of overwriting one
|
|
354
|
+
* another.
|
|
355
|
+
*/
|
|
356
|
+
private bakeSlotTargets<S>(
|
|
357
|
+
slots: readonly (S | undefined)[],
|
|
358
|
+
renderTargets: readonly (WebGLRenderTarget | undefined)[],
|
|
359
|
+
drawSlot: (slot: S) => void,
|
|
360
|
+
): void {
|
|
208
361
|
const prevTarget = this.renderer.getRenderTarget();
|
|
209
362
|
const prevClear = this.renderer.getClearColor(PREV_CLEAR_COLOR);
|
|
210
363
|
const prevClearAlpha = this.renderer.getClearAlpha();
|
|
@@ -220,35 +373,14 @@ export class TileTextureCompositor {
|
|
|
220
373
|
// An empty slot only needs a clear when the target holds stale content
|
|
221
374
|
// from a previous bake; a never-touched target has no GL storage yet
|
|
222
375
|
// and must not be render-targeted (that would allocate it).
|
|
223
|
-
if (!slot && !this.
|
|
224
|
-
this.
|
|
376
|
+
if (!slot && !this.touchedTargets.has(renderTarget)) continue;
|
|
377
|
+
this.touchedTargets.add(renderTarget);
|
|
225
378
|
|
|
226
379
|
this.renderer.setRenderTarget(renderTarget);
|
|
227
380
|
this.renderer.setClearColor(0x000, 0);
|
|
228
381
|
this.renderer.clear();
|
|
229
382
|
|
|
230
|
-
if (slot)
|
|
231
|
-
for (const source of slot.sources) {
|
|
232
|
-
const scene = this.texturizedScenes.findSceneByLayerId(
|
|
233
|
-
source.tileHandle,
|
|
234
|
-
slot.layerId,
|
|
235
|
-
);
|
|
236
|
-
if (!scene || scene.removed || !scene.children.length) continue;
|
|
237
|
-
// Frame the terrain tile's sub-rect of this source tile from the
|
|
238
|
-
// mercator uvOffset/uvScale: meshUv ∈ [0,1] → vectorUv = uvOffset +
|
|
239
|
-
// meshUv·uvScale, source scene NDC = 2·vectorUv − 1. A source finer than
|
|
240
|
-
// the terrain tile yields a camera wider than [-1, 1], so it draws into
|
|
241
|
-
// only its sub-rect and leaves the rest of the RT transparent.
|
|
242
|
-
const [ox, oy] = source.uvOffset;
|
|
243
|
-
const [sx, sy] = source.uvScale;
|
|
244
|
-
camera.left = 2 * ox - 1;
|
|
245
|
-
camera.right = 2 * (ox + sx) - 1;
|
|
246
|
-
camera.bottom = 2 * oy - 1;
|
|
247
|
-
camera.top = 2 * (oy + sy) - 1;
|
|
248
|
-
camera.updateProjectionMatrix();
|
|
249
|
-
this.renderer.render(scene, camera);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
383
|
+
if (slot) drawSlot(slot);
|
|
252
384
|
|
|
253
385
|
renderTarget.texture.needsUpdate = true;
|
|
254
386
|
}
|
|
@@ -258,6 +390,86 @@ export class TileTextureCompositor {
|
|
|
258
390
|
this.renderer.setClearColor(prevClear, prevClearAlpha);
|
|
259
391
|
}
|
|
260
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Frame the shared bake camera on the terrain tile's sub-rect of a source
|
|
395
|
+
* from the mercator affine: meshUv ∈ [0,1] → sourceUv = uvOffset +
|
|
396
|
+
* meshUv·uvScale, source NDC = 2·sourceUv − 1. A source finer than the
|
|
397
|
+
* terrain tile yields a camera wider than [-1, 1], so it draws into only
|
|
398
|
+
* its sub-rect and leaves the rest of the render target transparent.
|
|
399
|
+
*/
|
|
400
|
+
private frameBakeCamera(
|
|
401
|
+
uvOffset: [number, number],
|
|
402
|
+
uvScale: [number, number],
|
|
403
|
+
): OrthographicCamera {
|
|
404
|
+
const [ox, oy] = uvOffset;
|
|
405
|
+
const [sx, sy] = uvScale;
|
|
406
|
+
const camera = this.bakeCamera;
|
|
407
|
+
camera.left = 2 * ox - 1;
|
|
408
|
+
camera.right = 2 * (ox + sx) - 1;
|
|
409
|
+
camera.bottom = 2 * oy - 1;
|
|
410
|
+
camera.top = 2 * (oy + sy) - 1;
|
|
411
|
+
camera.updateProjectionMatrix();
|
|
412
|
+
return camera;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Draw one textured quad into the current render target through the bake
|
|
416
|
+
* camera window — the value-preserving copy described on
|
|
417
|
+
* {@link renderRasterTiles}. */
|
|
418
|
+
private drawRasterBakeQuad(
|
|
419
|
+
texture: Texture,
|
|
420
|
+
isElevationHeatmap: boolean,
|
|
421
|
+
uvOffset: [number, number],
|
|
422
|
+
uvScale: [number, number],
|
|
423
|
+
): void {
|
|
424
|
+
const material = this.rasterBakeMaterial;
|
|
425
|
+
this.configureRasterBakeSource(texture, isElevationHeatmap);
|
|
426
|
+
// First map assignment flips USE_MAP; later swaps are uniform-only.
|
|
427
|
+
if (material.map === null) material.needsUpdate = true;
|
|
428
|
+
material.map = texture;
|
|
429
|
+
this.renderer.render(
|
|
430
|
+
this.rasterBakeScene,
|
|
431
|
+
this.frameBakeCamera(uvOffset, uvScale),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** The cached 1×1 no-data texture, (re)created when the color changes. */
|
|
436
|
+
private demNoDataUnderlay(color: [number, number, number]): DataTexture {
|
|
437
|
+
const key = color.join(",");
|
|
438
|
+
if (!this.demNoDataTexture || this.demNoDataTextureKey !== key) {
|
|
439
|
+
this.demNoDataTexture?.dispose();
|
|
440
|
+
const texture = new DataTexture(
|
|
441
|
+
new Uint8Array([...color, 255]),
|
|
442
|
+
1,
|
|
443
|
+
1,
|
|
444
|
+
RGBAFormat,
|
|
445
|
+
UnsignedByteType,
|
|
446
|
+
);
|
|
447
|
+
texture.needsUpdate = true;
|
|
448
|
+
this.demNoDataTexture = texture;
|
|
449
|
+
this.demNoDataTextureKey = key;
|
|
450
|
+
}
|
|
451
|
+
return this.demNoDataTexture;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Value-preserving sampler settings for a bake source texture (see
|
|
455
|
+
* {@link renderRasterTiles}); idempotent so re-bakes don't re-upload. */
|
|
456
|
+
private configureRasterBakeSource(tex: Texture, isElevationHeatmap: boolean) {
|
|
457
|
+
const colorSpace = isElevationHeatmap ? NoColorSpace : SRGBColorSpace;
|
|
458
|
+
const filter = isElevationHeatmap ? NearestFilter : LinearFilter;
|
|
459
|
+
if (
|
|
460
|
+
tex.colorSpace !== colorSpace ||
|
|
461
|
+
tex.minFilter !== filter ||
|
|
462
|
+
tex.magFilter !== filter ||
|
|
463
|
+
tex.generateMipmaps
|
|
464
|
+
) {
|
|
465
|
+
tex.colorSpace = colorSpace;
|
|
466
|
+
tex.minFilter = filter;
|
|
467
|
+
tex.magFilter = filter;
|
|
468
|
+
tex.generateMipmaps = false;
|
|
469
|
+
tex.needsUpdate = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
261
473
|
// ---------------------------------------------------------------------
|
|
262
474
|
// MRT composite pass
|
|
263
475
|
// ---------------------------------------------------------------------
|
|
@@ -354,7 +566,7 @@ export class TileTextureCompositor {
|
|
|
354
566
|
// uniform ownership: each active enhancer attaches its own uniform refs and
|
|
355
567
|
// contributes the matching GLSL, so a new expression touches only its module.
|
|
356
568
|
const chain = createCompositeLayerEnhancers(features);
|
|
357
|
-
const core = createCoreUniformMutates();
|
|
569
|
+
const core = createCoreUniformMutates(this.mercatorY);
|
|
358
570
|
|
|
359
571
|
const uniforms: CompositeUniformTarget = {};
|
|
360
572
|
core.attachUniforms(uniforms, numTextures, placeholderTexture);
|
|
@@ -416,6 +628,10 @@ export class TileTextureCompositor {
|
|
|
416
628
|
m.placeholderTexture.dispose();
|
|
417
629
|
}
|
|
418
630
|
this.materialCache.clear();
|
|
631
|
+
// Also frees the raster-bake quad, which shares this geometry.
|
|
419
632
|
this.quadMesh.geometry.dispose();
|
|
633
|
+
this.rasterBakeMaterial.dispose();
|
|
634
|
+
this.demNoDataTexture?.dispose();
|
|
635
|
+
this.demNoDataTexture = null;
|
|
420
636
|
}
|
|
421
637
|
}
|
|
@@ -234,7 +234,13 @@ float interpolateDEMHeights(float h00, float h10, float h01, float h11, vec2 fra
|
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
float sampleElevationBilinear(sampler2D demTexture, vec2 uv, ivec2 texSize, out bool valid) {
|
|
238
244
|
|
|
239
245
|
DEMBilinearData data = prepareDEMBilinear(texSize, uv);
|
|
240
246
|
|
|
@@ -251,7 +257,8 @@ float interpolateDEMHeights(float h00, float h10, float h01, float h11, vec2 fra
|
|
|
251
257
|
h = interpolateDEMHeights(h00, h10, h01, h11, data.frac);
|
|
252
258
|
}
|
|
253
259
|
|
|
254
|
-
|
|
260
|
+
valid = isValidHeight(h);
|
|
261
|
+
if (!valid) {
|
|
255
262
|
return 0.0;
|
|
256
263
|
}
|
|
257
264
|
|
|
@@ -329,8 +336,9 @@ void main() {
|
|
|
329
336
|
vec4 texColor0;
|
|
330
337
|
if (uIsElevationHeatmaps[0]) {
|
|
331
338
|
ivec2 demTexSize = textureSize(uTextures[0], 0);
|
|
332
|
-
|
|
333
|
-
|
|
339
|
+
bool demValid0;
|
|
340
|
+
float normalized_h = sampleElevationBilinear(uTextures[0], texUv0, demTexSize, demValid0);
|
|
341
|
+
texColor0 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, demValid0 ? 1.0 : 0.0);
|
|
334
342
|
} else {
|
|
335
343
|
texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
|
|
336
344
|
}
|
|
@@ -397,8 +405,9 @@ void main() {
|
|
|
397
405
|
vec4 texColor1;
|
|
398
406
|
if (uIsElevationHeatmaps[1]) {
|
|
399
407
|
ivec2 demTexSize = textureSize(uTextures[1], 0);
|
|
400
|
-
|
|
401
|
-
|
|
408
|
+
bool demValid1;
|
|
409
|
+
float normalized_h = sampleElevationBilinear(uTextures[1], texUv1, demTexSize, demValid1);
|
|
410
|
+
texColor1 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, demValid1 ? 1.0 : 0.0);
|
|
402
411
|
} else {
|
|
403
412
|
texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
|
|
404
413
|
}
|
|
@@ -465,8 +474,9 @@ void main() {
|
|
|
465
474
|
vec4 texColor2;
|
|
466
475
|
if (uIsElevationHeatmaps[2]) {
|
|
467
476
|
ivec2 demTexSize = textureSize(uTextures[2], 0);
|
|
468
|
-
|
|
469
|
-
|
|
477
|
+
bool demValid2;
|
|
478
|
+
float normalized_h = sampleElevationBilinear(uTextures[2], texUv2, demTexSize, demValid2);
|
|
479
|
+
texColor2 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, demValid2 ? 1.0 : 0.0);
|
|
470
480
|
} else {
|
|
471
481
|
texColor2 = texture2D(uTextures[2], texUv2) * vec4(uColors[2], 1.0);
|
|
472
482
|
}
|
|
@@ -533,8 +543,9 @@ void main() {
|
|
|
533
543
|
vec4 texColor3;
|
|
534
544
|
if (uIsElevationHeatmaps[3]) {
|
|
535
545
|
ivec2 demTexSize = textureSize(uTextures[3], 0);
|
|
536
|
-
|
|
537
|
-
|
|
546
|
+
bool demValid3;
|
|
547
|
+
float normalized_h = sampleElevationBilinear(uTextures[3], texUv3, demTexSize, demValid3);
|
|
548
|
+
texColor3 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, demValid3 ? 1.0 : 0.0);
|
|
538
549
|
} else {
|
|
539
550
|
texColor3 = texture2D(uTextures[3], texUv3) * vec4(uColors[3], 1.0);
|
|
540
551
|
}
|
|
@@ -857,7 +868,13 @@ float interpolateDEMHeights(float h00, float h10, float h01, float h11, vec2 fra
|
|
|
857
868
|
|
|
858
869
|
|
|
859
870
|
|
|
860
|
-
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
float sampleElevationBilinear(sampler2D demTexture, vec2 uv, ivec2 texSize, out bool valid) {
|
|
861
878
|
|
|
862
879
|
DEMBilinearData data = prepareDEMBilinear(texSize, uv);
|
|
863
880
|
|
|
@@ -874,7 +891,8 @@ float interpolateDEMHeights(float h00, float h10, float h01, float h11, vec2 fra
|
|
|
874
891
|
h = interpolateDEMHeights(h00, h10, h01, h11, data.frac);
|
|
875
892
|
}
|
|
876
893
|
|
|
877
|
-
|
|
894
|
+
valid = isValidHeight(h);
|
|
895
|
+
if (!valid) {
|
|
878
896
|
return 0.0;
|
|
879
897
|
}
|
|
880
898
|
|
|
@@ -952,8 +970,9 @@ void main() {
|
|
|
952
970
|
vec4 texColor0;
|
|
953
971
|
if (uIsElevationHeatmaps[0]) {
|
|
954
972
|
ivec2 demTexSize = textureSize(uTextures[0], 0);
|
|
955
|
-
|
|
956
|
-
|
|
973
|
+
bool demValid0;
|
|
974
|
+
float normalized_h = sampleElevationBilinear(uTextures[0], texUv0, demTexSize, demValid0);
|
|
975
|
+
texColor0 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, demValid0 ? 1.0 : 0.0);
|
|
957
976
|
} else {
|
|
958
977
|
texColor0 = texture2D(uTextures[0], texUv0) * vec4(uColors[0], 1.0);
|
|
959
978
|
}
|
|
@@ -1016,8 +1035,9 @@ void main() {
|
|
|
1016
1035
|
vec4 texColor1;
|
|
1017
1036
|
if (uIsElevationHeatmaps[1]) {
|
|
1018
1037
|
ivec2 demTexSize = textureSize(uTextures[1], 0);
|
|
1019
|
-
|
|
1020
|
-
|
|
1038
|
+
bool demValid1;
|
|
1039
|
+
float normalized_h = sampleElevationBilinear(uTextures[1], texUv1, demTexSize, demValid1);
|
|
1040
|
+
texColor1 = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, demValid1 ? 1.0 : 0.0);
|
|
1021
1041
|
} else {
|
|
1022
1042
|
texColor1 = texture2D(uTextures[1], texUv1) * vec4(uColors[1], 1.0);
|
|
1023
1043
|
}
|
package/src/tileTexture/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type DirtyReason =
|
|
|
16
16
|
| "material"
|
|
17
17
|
| "texture-binding"
|
|
18
18
|
| "vector-revision"
|
|
19
|
+
| "raster-revision"
|
|
19
20
|
| "hillshade";
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -121,6 +122,12 @@ export type HillshadeCompositeLayer = CompositeLayerBase & {
|
|
|
121
122
|
export type ElevationHeatmapCompositeLayer = CompositeLayerBase & {
|
|
122
123
|
kind: "elevationHeatmap";
|
|
123
124
|
opacity: number;
|
|
125
|
+
/**
|
|
126
|
+
* Same Mercator latitude reprojection band as {@link RasterCompositeLayer}:
|
|
127
|
+
* the heatmap decode samples the reprojected per-slot UV, so a DEM draped on
|
|
128
|
+
* Geographic terrain (baked or direct) remaps identically to color imagery.
|
|
129
|
+
*/
|
|
130
|
+
reproject?: [number, number];
|
|
124
131
|
};
|
|
125
132
|
|
|
126
133
|
export type CompositeLayer =
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { demNoDataColorBytes } from "./demNoDataColor";
|
|
4
|
+
|
|
5
|
+
describe("demNoDataColorBytes", () => {
|
|
6
|
+
it("solves the Japan GSI boundary (2^23) to (128, 0, 0)", () => {
|
|
7
|
+
expect(demNoDataColorBytes({ x: 65536, y: 256, z: 1 }, 8388608)).toEqual([
|
|
8
|
+
128, 0, 0,
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("solves the Mapbox boundary (10000) to (0, 39, 16)", () => {
|
|
13
|
+
expect(demNoDataColorBytes({ x: 65536, y: 256, z: 1 }, 10000)).toEqual([
|
|
14
|
+
0, 39, 16,
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("solves the Terrarium boundary (0) to black", () => {
|
|
19
|
+
expect(demNoDataColorBytes({ x: 256, y: 1, z: 1 / 256 }, 0)).toEqual([
|
|
20
|
+
0, 0, 0,
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("keeps channel order independent of scaler magnitude order", () => {
|
|
25
|
+
// Same positional system with permuted channels: G is the coarse digit.
|
|
26
|
+
expect(demNoDataColorBytes({ x: 256, y: 65536, z: 1 }, 8388608)).toEqual([
|
|
27
|
+
0, 128, 0,
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("accepts a non-exact color inside the shader's no-data band", () => {
|
|
32
|
+
// 0.75 has no exact byte decomposition with unit scalers, but black
|
|
33
|
+
// decodes to 0 and |0 - 0.75| <= 1, so the shader still flags it no-data.
|
|
34
|
+
expect(demNoDataColorBytes({ x: 1, y: 1, z: 1 }, 0.75)).toEqual([0, 0, 0]);
|
|
35
|
+
// Band edge: the closest representable value is exactly 1 away
|
|
36
|
+
// ((1,0,0) decodes to 2, |2 - 3| == 1), which the shader's
|
|
37
|
+
// `abs(x - boundary) <= 1` check still classifies as no-data.
|
|
38
|
+
expect(demNoDataColorBytes({ x: 2, y: 0, z: 0 }, 3)).toEqual([1, 0, 0]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns null when no byte color decodes inside the no-data band", () => {
|
|
42
|
+
// Boundary beyond the encodable range (max dot product is 765).
|
|
43
|
+
expect(demNoDataColorBytes({ x: 1, y: 1, z: 1 }, 100000)).toBeNull();
|
|
44
|
+
// Coarse scalers leave the nearest representable value a full step away.
|
|
45
|
+
expect(demNoDataColorBytes({ x: 4, y: 0, z: 0 }, 6)).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|