@navaramap/three 0.0.1 → 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-Bnck8OGf.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-DG9ORAmj.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 +445 -16
- package/dist/index.js +6151 -5717
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +241 -222
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +34 -33
- 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 +12 -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 +137 -35
- 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 +13 -3
- 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/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/TileTextureCompositor.test.ts +2 -0
- package/src/tileTexture/TileTextureCompositor.ts +7 -1
- 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-CCWL5pbx.js +0 -2
- package/dist/assets/fontWorker-CCWL5pbx.js.map +0 -1
- package/dist/assets/index-BdtHOOT9.js +0 -12
- package/dist/assets/index-BdtHOOT9.js.map +0 -1
|
@@ -10,14 +10,22 @@ import {
|
|
|
10
10
|
Object3D,
|
|
11
11
|
ShaderMaterial,
|
|
12
12
|
BufferAttribute,
|
|
13
|
+
type BufferGeometry,
|
|
13
14
|
Color,
|
|
15
|
+
type Material,
|
|
14
16
|
PerspectiveCamera,
|
|
15
17
|
Vector2,
|
|
16
18
|
} from "three";
|
|
17
19
|
import invariant from "tiny-invariant";
|
|
18
20
|
|
|
21
|
+
import {
|
|
22
|
+
DECLUTTER_FADE_MS,
|
|
23
|
+
type DeclutterCandidate,
|
|
24
|
+
type DeclutterParticipant,
|
|
25
|
+
} from "../../declutter/types";
|
|
19
26
|
import type { EventContext } from "../../event/context";
|
|
20
27
|
import { createInstancedSpriteMaterialEnhancer } from "../../material/enhancer";
|
|
28
|
+
import type { CustomObject3DEventMap } from "../../object3DEvent";
|
|
21
29
|
import { PickableMesh } from "../pickableMesh";
|
|
22
30
|
|
|
23
31
|
import { BillboardAtlas, type AtlasRect } from "./billboardAtlas";
|
|
@@ -46,7 +54,10 @@ type PositionsInfo = {
|
|
|
46
54
|
const _tmpSize = new Vector2();
|
|
47
55
|
|
|
48
56
|
// Coupled with crates/navara_feature/src/geometry/point.rs::pixel_to_world
|
|
49
|
-
export class InstancedSpriteMesh
|
|
57
|
+
export class InstancedSpriteMesh
|
|
58
|
+
extends Mesh<BufferGeometry, Material | Material[], CustomObject3DEventMap>
|
|
59
|
+
implements PickableMesh, DeclutterParticipant
|
|
60
|
+
{
|
|
50
61
|
private _batchIdToInstance = new Map<number, number>();
|
|
51
62
|
private _initialColor: Color = new Color(0xffffff);
|
|
52
63
|
private _initialHeight = 0.0;
|
|
@@ -72,15 +83,182 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
72
83
|
private _enhancedMaterial?: ReturnType<
|
|
73
84
|
typeof createInstancedSpriteMaterialEnhancer
|
|
74
85
|
>;
|
|
86
|
+
/** Per-instance world anchors in ECEF meters (f64, 3 per instance), kept in
|
|
87
|
+
* sync with the position attributes for the declutter pass. */
|
|
88
|
+
private _anchors: Float64Array | null = null;
|
|
89
|
+
/** Whether this mesh's instances participate in screen-space decluttering. */
|
|
90
|
+
private _declutter = false;
|
|
91
|
+
/** Layer-level placement priority from the material. */
|
|
92
|
+
private _declutterPriority = 0;
|
|
93
|
+
/** Per-instance priorities set through the evaluator (NaN = no override,
|
|
94
|
+
* fall back to the layer value). Lazily allocated on first use. */
|
|
95
|
+
private _declutterPriorityOverrides: Float32Array | null = null;
|
|
96
|
+
/** Per-instance fade targets for `instanceDeclutterHide` (0 = shown,
|
|
97
|
+
* 1 = hidden); the attribute animates toward these in stepDeclutterFade. */
|
|
98
|
+
private _declutterTargets: Float32Array | null = null;
|
|
99
|
+
/** True while any instance's hide factor may differ from its target. */
|
|
100
|
+
private _declutterAnimating = false;
|
|
101
|
+
|
|
75
102
|
constructor(options: InstancedSpriteOptions) {
|
|
76
103
|
super();
|
|
77
104
|
this.renderOrder = options.renderOrder ?? this.renderOrder;
|
|
78
105
|
this.ctx = options.ctx;
|
|
106
|
+
this.ctx.declutter?.register(this);
|
|
107
|
+
// `processObjectRemoved` dispatches this for every removed mesh; it is the
|
|
108
|
+
// reliable teardown signal (this class's dispose() is not called there).
|
|
109
|
+
this.addEventListener("removedFromWorld", () => {
|
|
110
|
+
this.ctx.declutter?.unregister(this);
|
|
111
|
+
});
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
setActive(active: boolean) {
|
|
82
115
|
this._active = active;
|
|
83
116
|
this.updateVisibility();
|
|
117
|
+
this.ctx.declutter?.markDirty();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- DeclutterParticipant ---
|
|
121
|
+
|
|
122
|
+
collectDeclutterCandidates(out: DeclutterCandidate[]): void {
|
|
123
|
+
if (!this.visible || !this._declutter || !this._anchors) return;
|
|
124
|
+
const enhancer = this._enhancedMaterial;
|
|
125
|
+
const params = this.geometry?.getAttribute("instanceParams") as
|
|
126
|
+
InstancedBufferAttribute | undefined;
|
|
127
|
+
if (!enhancer || !params) return;
|
|
128
|
+
|
|
129
|
+
const state = enhancer.states();
|
|
130
|
+
const cx = Math.min(Math.max(state.center[0], -0.5), 0.5);
|
|
131
|
+
const cy = Math.min(Math.max(state.center[1], -0.5), 0.5);
|
|
132
|
+
// Mirror of instancedSprite.vert.glsl:100-104 — aspect is per-instance
|
|
133
|
+
// (from the atlas rect), not a material-level uniform; there is no
|
|
134
|
+
// material-wide "aspect" state to read.
|
|
135
|
+
const uvRect = state.billboard
|
|
136
|
+
? (this.geometry?.getAttribute("instanceUvRect") as
|
|
137
|
+
InstancedBufferAttribute | undefined)
|
|
138
|
+
: undefined;
|
|
139
|
+
const anchors = this._anchors;
|
|
140
|
+
const overrides = this._declutterPriorityOverrides;
|
|
141
|
+
const targets = this._declutterTargets;
|
|
142
|
+
const count = Math.min(params.count, anchors.length / 3);
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < count; i++) {
|
|
145
|
+
if (params.getZ(i) <= 0.5) continue; // hidden by user `show`
|
|
146
|
+
const instanceSize = params.getY(i);
|
|
147
|
+
const size = instanceSize >= 0.0 ? instanceSize : state.scale;
|
|
148
|
+
if (size <= 0.0) continue;
|
|
149
|
+
|
|
150
|
+
const override = overrides ? overrides[i] : Number.NaN;
|
|
151
|
+
const rectH = uvRect ? uvRect.getW(i) : 0;
|
|
152
|
+
const aspect = uvRect && rectH > 0.0 ? uvRect.getZ(i) / rectH : 1.0;
|
|
153
|
+
|
|
154
|
+
// Mirror of instancedSprite.vert.glsl:95-97 — the quad spans
|
|
155
|
+
// (position.xy - center) * vec2(aspect, 1) * size around the anchor.
|
|
156
|
+
out.push({
|
|
157
|
+
anchorX: anchors[i * 3],
|
|
158
|
+
anchorY: anchors[i * 3 + 1],
|
|
159
|
+
anchorZ: anchors[i * 3 + 2],
|
|
160
|
+
addHeight: params.getX(i),
|
|
161
|
+
minX: (-0.5 - cx) * aspect * size,
|
|
162
|
+
maxX: (0.5 - cx) * aspect * size,
|
|
163
|
+
minY: (-0.5 - cy) * size,
|
|
164
|
+
maxY: (0.5 - cy) * size,
|
|
165
|
+
sizeInMeters: state.sizeInMeters,
|
|
166
|
+
// NaN-safe: an unset override falls back to the layer priority.
|
|
167
|
+
priority: Number.isNaN(override) ? this._declutterPriority : override,
|
|
168
|
+
isShown: targets ? targets[i] === 0 : true,
|
|
169
|
+
owner: this,
|
|
170
|
+
handle: i,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
applyDeclutter(handle: number, hidden: boolean): void {
|
|
176
|
+
this.setDeclutterHiddenByInstance(handle, hidden);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
stepDeclutterFade(deltaMs: number): boolean {
|
|
180
|
+
if (!this._declutterAnimating) return false;
|
|
181
|
+
const attr = this.geometry?.getAttribute("instanceDeclutterHide") as
|
|
182
|
+
InstancedBufferAttribute | undefined;
|
|
183
|
+
const targets = this._declutterTargets;
|
|
184
|
+
if (!attr || !targets) {
|
|
185
|
+
this._declutterAnimating = false;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const step = deltaMs / DECLUTTER_FADE_MS;
|
|
190
|
+
const count = Math.min(attr.count, targets.length);
|
|
191
|
+
let stillAnimating = false;
|
|
192
|
+
let changed = false;
|
|
193
|
+
for (let i = 0; i < count; i++) {
|
|
194
|
+
let value = attr.getX(i);
|
|
195
|
+
const target = targets[i];
|
|
196
|
+
if (value === target) continue;
|
|
197
|
+
value =
|
|
198
|
+
value < target
|
|
199
|
+
? Math.min(value + step, target)
|
|
200
|
+
: Math.max(value - step, target);
|
|
201
|
+
attr.setX(i, value);
|
|
202
|
+
changed = true;
|
|
203
|
+
if (value !== target) stillAnimating = true;
|
|
204
|
+
}
|
|
205
|
+
if (changed) attr.needsUpdate = true;
|
|
206
|
+
this._declutterAnimating = stillAnimating;
|
|
207
|
+
return stillAnimating;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private _cacheDeclutterState(m: NavaraPointMesh | NavaraBillboardMesh) {
|
|
211
|
+
const nextDeclutter = m.material.declutter ?? true;
|
|
212
|
+
if (this._declutter && !nextDeclutter) {
|
|
213
|
+
// Leaving declutter mode: clear hides the pass applied — the mesh stops
|
|
214
|
+
// producing candidates, so nothing else would ever re-show them.
|
|
215
|
+
this._clearDeclutterHidden();
|
|
216
|
+
}
|
|
217
|
+
this._declutter = nextDeclutter;
|
|
218
|
+
this._declutterPriority = m.material.declutterPriority ?? 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private _clearDeclutterHidden(): void {
|
|
222
|
+
const targets = this._declutterTargets;
|
|
223
|
+
if (!targets) return;
|
|
224
|
+
targets.fill(0.0);
|
|
225
|
+
// Everything fades back in from wherever its hide factor currently is.
|
|
226
|
+
this._declutterAnimating = true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Reconstruct absolute ECEF anchors from the same arrays the position
|
|
230
|
+
* attributes receive (RTE high+low split, or RTC-relative + center). */
|
|
231
|
+
private _cacheAnchors(
|
|
232
|
+
positionsInfo: PositionsInfo,
|
|
233
|
+
transform: { tx: number; ty: number; tz: number },
|
|
234
|
+
): void {
|
|
235
|
+
const { nPositions, positionSize, RTE } = positionsInfo;
|
|
236
|
+
const anchors =
|
|
237
|
+
this._anchors && this._anchors.length === nPositions * 3
|
|
238
|
+
? this._anchors
|
|
239
|
+
: new Float64Array(nPositions * 3);
|
|
240
|
+
|
|
241
|
+
if (RTE) {
|
|
242
|
+
const pos = positionsInfo.position as {
|
|
243
|
+
high: Float32Array<ArrayBufferLike>;
|
|
244
|
+
low: Float32Array<ArrayBufferLike>;
|
|
245
|
+
};
|
|
246
|
+
for (let i = 0; i < nPositions; i++) {
|
|
247
|
+
const s = i * positionSize;
|
|
248
|
+
anchors[i * 3] = pos.high[s] + pos.low[s];
|
|
249
|
+
anchors[i * 3 + 1] = pos.high[s + 1] + pos.low[s + 1];
|
|
250
|
+
anchors[i * 3 + 2] = (pos.high[s + 2] ?? 0.0) + (pos.low[s + 2] ?? 0.0);
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
const pos = positionsInfo.position as Float32Array<ArrayBufferLike>;
|
|
254
|
+
for (let i = 0; i < nPositions; i++) {
|
|
255
|
+
const s = i * positionSize;
|
|
256
|
+
anchors[i * 3] = pos[s] + transform.tx;
|
|
257
|
+
anchors[i * 3 + 1] = pos[s + 1] + transform.ty;
|
|
258
|
+
anchors[i * 3 + 2] = (pos[s + 2] ?? 0.0) + transform.tz;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this._anchors = anchors;
|
|
84
262
|
}
|
|
85
263
|
|
|
86
264
|
async _init(m: NavaraPointMesh | NavaraBillboardMesh) {
|
|
@@ -90,6 +268,9 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
90
268
|
return;
|
|
91
269
|
}
|
|
92
270
|
|
|
271
|
+
this._cacheDeclutterState(m);
|
|
272
|
+
this._cacheAnchors(positionsInfo, m.transform);
|
|
273
|
+
|
|
93
274
|
// Create Geometry
|
|
94
275
|
this.geometry = this._initGeometry(positionsInfo, m);
|
|
95
276
|
|
|
@@ -97,12 +278,15 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
97
278
|
this.material = await this._initMaterial(positionsInfo, m);
|
|
98
279
|
|
|
99
280
|
this.frustumCulled = false; // Disable since bounding box doesn't account for instance positions
|
|
281
|
+
this.ctx.declutter?.markDirty();
|
|
100
282
|
}
|
|
101
283
|
|
|
102
284
|
async _update(m: NavaraPointMesh | NavaraBillboardMesh) {
|
|
103
285
|
const enhancer = this.getEnhancer();
|
|
104
286
|
const material = this.material as ShaderMaterial;
|
|
105
287
|
|
|
288
|
+
this._cacheDeclutterState(m);
|
|
289
|
+
|
|
106
290
|
if (material.visible !== m.material.show) {
|
|
107
291
|
material.visible = m.material.show ?? true;
|
|
108
292
|
this.updateVisibility();
|
|
@@ -162,6 +346,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
162
346
|
const positionsInfo = this.extractPositions(m);
|
|
163
347
|
|
|
164
348
|
if (positionsInfo) {
|
|
349
|
+
this._cacheAnchors(positionsInfo, m.transform);
|
|
165
350
|
if (positionsInfo.RTE) {
|
|
166
351
|
const pos = positionsInfo.position as {
|
|
167
352
|
high: Float32Array<ArrayBufferLike>;
|
|
@@ -198,6 +383,8 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
198
383
|
await this._setDefaultImage(m.material.url);
|
|
199
384
|
}
|
|
200
385
|
}
|
|
386
|
+
|
|
387
|
+
this.ctx.declutter?.markDirty();
|
|
201
388
|
}
|
|
202
389
|
|
|
203
390
|
private _initGeometry(
|
|
@@ -307,6 +494,16 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
307
494
|
"instanceColor",
|
|
308
495
|
new InstancedBufferAttribute(colorBuffer, 3),
|
|
309
496
|
);
|
|
497
|
+
// Declutter hide factors (0 = shown … 1 = hidden). Decluttered instances
|
|
498
|
+
// start hidden and fade in once the placement pass grants them space, so
|
|
499
|
+
// dense tiles don't flash their full clutter before the first pass runs.
|
|
500
|
+
const initialHide = this._declutter ? 1.0 : 0.0;
|
|
501
|
+
const declutterBuffer = new Float32Array(instanceCount).fill(initialHide);
|
|
502
|
+
this._declutterTargets = new Float32Array(instanceCount).fill(initialHide);
|
|
503
|
+
instancedGeometry.setAttribute(
|
|
504
|
+
"instanceDeclutterHide",
|
|
505
|
+
new InstancedBufferAttribute(declutterBuffer, 1),
|
|
506
|
+
);
|
|
310
507
|
instancedGeometry.setAttribute(
|
|
311
508
|
"instanceBatchID",
|
|
312
509
|
new InstancedBufferAttribute(
|
|
@@ -572,6 +769,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
572
769
|
) as InstancedBufferAttribute;
|
|
573
770
|
paramsAttr.setZ(instanceId, rawVisible ? 1.0 : 0.0);
|
|
574
771
|
paramsAttr.needsUpdate = true;
|
|
772
|
+
this.ctx.declutter?.markDirty();
|
|
575
773
|
}
|
|
576
774
|
|
|
577
775
|
setFeatureOpacityByBatchId(batchId: number, opacity: number) {
|
|
@@ -599,6 +797,46 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
599
797
|
const sanitizedHeight = Number.isFinite(height) ? height : 0.0;
|
|
600
798
|
paramsAttr.setX(instanceId, sanitizedHeight);
|
|
601
799
|
paramsAttr.needsUpdate = true;
|
|
800
|
+
this.ctx.declutter?.markDirty();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Set one instance's declutter fade target; the attribute animates toward
|
|
805
|
+
* it in {@link stepDeclutterFade}. Deliberately separate from the `show`
|
|
806
|
+
* component of `instanceParams` so user-driven visibility and declutter
|
|
807
|
+
* results compose instead of clobbering each other.
|
|
808
|
+
*/
|
|
809
|
+
setDeclutterHiddenByInstance(instanceIndex: number, hidden: boolean) {
|
|
810
|
+
const targets = this._declutterTargets;
|
|
811
|
+
if (!targets || instanceIndex < 0 || instanceIndex >= targets.length) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
targets[instanceIndex] = hidden ? 1.0 : 0.0;
|
|
815
|
+
// Cheap over-approximation; stepDeclutterFade clears it when everything
|
|
816
|
+
// has reached its target.
|
|
817
|
+
this._declutterAnimating = true;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Set a per-feature placement priority (higher wins), overriding the
|
|
822
|
+
* layer-level `declutterPriority` for this instance. Driven by the feature
|
|
823
|
+
* evaluator.
|
|
824
|
+
*/
|
|
825
|
+
setFeatureDeclutterPriorityByBatchId(batchId: number, priority: number) {
|
|
826
|
+
const instanceId = this._batchIdToInstance.get(batchId);
|
|
827
|
+
if (instanceId === undefined) return;
|
|
828
|
+
|
|
829
|
+
if (!this._declutterPriorityOverrides) {
|
|
830
|
+
const count = this._anchors ? this._anchors.length / 3 : 0;
|
|
831
|
+
if (count === 0) return;
|
|
832
|
+
this._declutterPriorityOverrides = new Float32Array(count).fill(
|
|
833
|
+
Number.NaN,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (instanceId >= this._declutterPriorityOverrides.length) return;
|
|
837
|
+
if (this._declutterPriorityOverrides[instanceId] === priority) return;
|
|
838
|
+
this._declutterPriorityOverrides[instanceId] = priority;
|
|
839
|
+
this.ctx.declutter?.markDirty();
|
|
602
840
|
}
|
|
603
841
|
|
|
604
842
|
setFeatureSizeByBatchId(batchId: number, size: number) {
|
|
@@ -616,6 +854,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
616
854
|
: -1.0;
|
|
617
855
|
paramsAttr.setY(instanceId, sanitizedSize);
|
|
618
856
|
paramsAttr.needsUpdate = true;
|
|
857
|
+
this.ctx.declutter?.markDirty();
|
|
619
858
|
}
|
|
620
859
|
|
|
621
860
|
/**
|
|
@@ -634,8 +873,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
634
873
|
if (instanceId === undefined) return;
|
|
635
874
|
|
|
636
875
|
const rectAttr = this.geometry.getAttribute("instanceUvRect") as
|
|
637
|
-
|
|
|
638
|
-
| undefined;
|
|
876
|
+
InstancedBufferAttribute | undefined;
|
|
639
877
|
if (!rectAttr) return;
|
|
640
878
|
|
|
641
879
|
if (url == null) {
|
|
@@ -669,6 +907,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
669
907
|
}
|
|
670
908
|
|
|
671
909
|
dispose(): void {
|
|
910
|
+
this.ctx.declutter?.unregister(this);
|
|
672
911
|
this.geometry?.dispose();
|
|
673
912
|
|
|
674
913
|
// The material's uTexture points at the atlas texture; the atlas owns it.
|
|
@@ -688,6 +927,9 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
688
927
|
|
|
689
928
|
// Clear internal collections to release references
|
|
690
929
|
this._batchIdToInstance.clear();
|
|
930
|
+
this._anchors = null;
|
|
931
|
+
this._declutterTargets = null;
|
|
932
|
+
this._declutterPriorityOverrides = null;
|
|
691
933
|
this._imageOverrides.clear();
|
|
692
934
|
this._requestedImageUrls.clear();
|
|
693
935
|
}
|
|
@@ -2,6 +2,8 @@ import { NamedIndexMap } from "@navaramap/core";
|
|
|
2
2
|
import { EffectComposer, Pass as PostProcessingPass } from "postprocessing";
|
|
3
3
|
import { HalfFloatType, Scene, WebGLRenderer, Group } from "three";
|
|
4
4
|
|
|
5
|
+
import { estimateFixedGpuBytes } from "../utils/fixedGpuFootprint";
|
|
6
|
+
|
|
5
7
|
export type RenderPassOrchestratorOptions = {
|
|
6
8
|
halfFloat?: boolean;
|
|
7
9
|
multisampling?: number;
|
|
@@ -34,6 +36,12 @@ export class RenderPassOrchestrator {
|
|
|
34
36
|
};
|
|
35
37
|
effectComposer: EffectComposer;
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Invoked whenever the pass list changes (add/insert/remove/clear), so the
|
|
41
|
+
* owner can re-report the fixed GPU footprint to the memory ledger.
|
|
42
|
+
*/
|
|
43
|
+
onPassesChanged?: () => void;
|
|
44
|
+
|
|
37
45
|
private passMap = new NamedIndexMap<NamedPass>();
|
|
38
46
|
|
|
39
47
|
constructor(renderer: WebGLRenderer, options: RenderPassOrchestratorOptions) {
|
|
@@ -60,6 +68,7 @@ export class RenderPassOrchestrator {
|
|
|
60
68
|
const namedPass: NamedPass = { name, pass };
|
|
61
69
|
this.passMap.add(namedPass);
|
|
62
70
|
this.effectComposer.addPass(pass);
|
|
71
|
+
this.onPassesChanged?.();
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/**
|
|
@@ -73,6 +82,7 @@ export class RenderPassOrchestrator {
|
|
|
73
82
|
const namedPass: NamedPass = { name, pass };
|
|
74
83
|
const targetIndex = this.passMap.insertBefore(targetName, namedPass);
|
|
75
84
|
this.effectComposer.addPass(pass, targetIndex);
|
|
85
|
+
this.onPassesChanged?.();
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
/**
|
|
@@ -86,6 +96,7 @@ export class RenderPassOrchestrator {
|
|
|
86
96
|
const namedPass: NamedPass = { name, pass };
|
|
87
97
|
const targetIndex = this.passMap.insertAfter(targetName, namedPass);
|
|
88
98
|
this.effectComposer.addPass(pass, targetIndex);
|
|
99
|
+
this.onPassesChanged?.();
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/**
|
|
@@ -100,6 +111,7 @@ export class RenderPassOrchestrator {
|
|
|
100
111
|
this.effectComposer.removePass(targetPass.pass);
|
|
101
112
|
this.passMap.list = this.passMap.list.filter((p) => p.name !== name);
|
|
102
113
|
this.rebuildIndexMap();
|
|
114
|
+
this.onPassesChanged?.();
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
/**
|
|
@@ -125,6 +137,19 @@ export class RenderPassOrchestrator {
|
|
|
125
137
|
}
|
|
126
138
|
this.passMap.list = [];
|
|
127
139
|
this.passMap.indexMap = {};
|
|
140
|
+
this.onPassesChanged?.();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Estimates the resident GPU bytes of the fixed, screen-sized render-target
|
|
145
|
+
* stack (composer buffers + every target reachable from the passes), for
|
|
146
|
+
* reporting into the memory ledger's `fixed_gpu_bytes` term.
|
|
147
|
+
*/
|
|
148
|
+
estimateFixedGpuBytes(): number {
|
|
149
|
+
return estimateFixedGpuBytes(
|
|
150
|
+
this.effectComposer,
|
|
151
|
+
this.passMap.list.map((p) => p.pass),
|
|
152
|
+
);
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
/**
|
|
@@ -2,11 +2,13 @@ import { Globe } from "@navaramap/core";
|
|
|
2
2
|
import { DepthCopyPass } from "postprocessing";
|
|
3
3
|
import {
|
|
4
4
|
Color,
|
|
5
|
+
DepthStencilFormat,
|
|
5
6
|
DepthTexture,
|
|
6
7
|
HalfFloatType,
|
|
7
8
|
NearestFilter,
|
|
8
9
|
RGBADepthPacking,
|
|
9
10
|
Scene,
|
|
11
|
+
UnsignedInt248Type,
|
|
10
12
|
WebGLRenderTarget,
|
|
11
13
|
type PerspectiveCamera,
|
|
12
14
|
type WebGLRenderer,
|
|
@@ -90,7 +92,20 @@ export class CustomRenderPass extends RenderPass {
|
|
|
90
92
|
emissiveTexture.type = HalfFloatType;
|
|
91
93
|
this.gbufferRenderTarget.textures.push(emissiveTexture);
|
|
92
94
|
|
|
95
|
+
// The gbuffer owns its depth texture instead of sharing the composer's one
|
|
96
|
+
// (postprocessing >= 6.39 no longer exposes a writable shared depth
|
|
97
|
+
// texture). Depth is propagated to the composer's input buffer explicitly
|
|
98
|
+
// by the copy pass below. Stencil is required for draped-mesh rendering.
|
|
99
|
+
const gbufferDepthTexture = new DepthTexture(
|
|
100
|
+
this.gbufferRenderTarget.width,
|
|
101
|
+
this.gbufferRenderTarget.height,
|
|
102
|
+
UnsignedInt248Type,
|
|
103
|
+
);
|
|
104
|
+
gbufferDepthTexture.format = DepthStencilFormat;
|
|
105
|
+
this.gbufferRenderTarget.depthTexture = gbufferDepthTexture;
|
|
106
|
+
|
|
93
107
|
this.copyPass = new RenderTargetCopyPass(this.gbufferRenderTarget);
|
|
108
|
+
this.copyPass.setDepthTexture(gbufferDepthTexture);
|
|
94
109
|
|
|
95
110
|
this.globeDepthCopyPass = new DepthCopyPass({
|
|
96
111
|
depthPacking: RGBADepthPacking,
|
|
@@ -240,11 +255,12 @@ export class CustomRenderPass extends RenderPass {
|
|
|
240
255
|
this.allDepthCopyPass.render(renderer, null, null);
|
|
241
256
|
}
|
|
242
257
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
// The composer-provided depth texture is intentionally ignored: the gbuffer
|
|
259
|
+
// owns its depth texture and depth is copied into the input buffer by the
|
|
260
|
+
// copy pass. `needsDepthTexture` stays true so the composer still attaches a
|
|
261
|
+
// depth texture to the input buffer, which `allDepthCopyPass` reads after
|
|
262
|
+
// the opaque scene has been rendered.
|
|
263
|
+
setDepthTexture(): void {}
|
|
248
264
|
|
|
249
265
|
setSize(width: number, height: number) {
|
|
250
266
|
this.gbufferRenderTarget.setSize(width, height);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import PostProcessingCommon from "@shaders/glsl/postprocessingCommon.vert.glsl";
|
|
2
2
|
import { Pass } from "postprocessing";
|
|
3
3
|
import {
|
|
4
|
+
AlwaysDepth,
|
|
4
5
|
NoBlending,
|
|
5
6
|
ShaderMaterial,
|
|
6
7
|
Texture,
|
|
@@ -10,13 +11,18 @@ import {
|
|
|
10
11
|
|
|
11
12
|
const fragmentShader = `
|
|
12
13
|
uniform sampler2D color;
|
|
14
|
+
|
|
15
|
+
#ifdef COPY_DEPTH
|
|
13
16
|
uniform sampler2D depth;
|
|
17
|
+
#endif
|
|
14
18
|
|
|
15
19
|
varying vec2 vUv;
|
|
16
20
|
|
|
17
21
|
void main() {
|
|
18
22
|
gl_FragColor = texture2D(color, vUv);
|
|
19
|
-
|
|
23
|
+
#ifdef COPY_DEPTH
|
|
24
|
+
gl_FragDepth = texture2D(depth, vUv).r;
|
|
25
|
+
#endif
|
|
20
26
|
}
|
|
21
27
|
`;
|
|
22
28
|
|
|
@@ -71,8 +77,24 @@ export class RenderTargetCopyPass extends Pass {
|
|
|
71
77
|
this._material.uniforms.color.value = texture;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
/**
|
|
81
|
+
* When a depth texture is set, the pass copies its depth into the target's
|
|
82
|
+
* depth buffer via gl_FragDepth. Depth test must be enabled (with AlwaysDepth)
|
|
83
|
+
* because GL discards depth writes while the depth test is disabled.
|
|
84
|
+
*/
|
|
85
|
+
setDepthTexture(texture: Texture | null): void {
|
|
75
86
|
this._material.uniforms.depth.value = texture;
|
|
87
|
+
const copyDepth = texture !== null;
|
|
88
|
+
if ("COPY_DEPTH" in this._material.defines === copyDepth) return;
|
|
89
|
+
if (copyDepth) {
|
|
90
|
+
this._material.defines.COPY_DEPTH = "";
|
|
91
|
+
} else {
|
|
92
|
+
delete this._material.defines.COPY_DEPTH;
|
|
93
|
+
}
|
|
94
|
+
this._material.depthWrite = copyDepth;
|
|
95
|
+
this._material.depthTest = copyDepth;
|
|
96
|
+
this._material.depthFunc = AlwaysDepth;
|
|
97
|
+
this._material.needsUpdate = true;
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
render(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
import { Core, initSync } from "@navaramap/engine";
|
|
5
|
+
|
|
6
|
+
import type { TileHandler } from "../event/context";
|
|
7
|
+
import { createTileHandler } from "../event/tileHandler";
|
|
8
|
+
|
|
9
|
+
let engineInitialized = false;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize the engine WASM once per vitest worker. The web-target module
|
|
13
|
+
* normally streams the binary over `fetch` (`initCore`), which Node cannot do
|
|
14
|
+
* for workspace files — so the bytes are read from the `@navaramap/engine`
|
|
15
|
+
* package and compiled synchronously. Repeat calls are no-ops; every test
|
|
16
|
+
* file that touches Rust-backed functions can call this without coordination.
|
|
17
|
+
*/
|
|
18
|
+
export function initTestEngine(): void {
|
|
19
|
+
if (engineInitialized) return;
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const wasmPath = require.resolve("@navaramap/engine/navara_wasm_bg.wasm");
|
|
22
|
+
initSync({ module: readFileSync(wasmPath) });
|
|
23
|
+
engineInitialized = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let sharedCore: Core | undefined;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lazily created `Core` shared across a vitest worker's test files.
|
|
30
|
+
* `Core::new` only builds the Bevy app (no DOM access), so it is safe in the
|
|
31
|
+
* Node test environment. Shared deliberately: tests exercise Rust-backed
|
|
32
|
+
* queries against one engine instead of paying a per-file world setup.
|
|
33
|
+
*/
|
|
34
|
+
export function getTestCore(): Core {
|
|
35
|
+
initTestEngine();
|
|
36
|
+
sharedCore ??= new Core("navara-test");
|
|
37
|
+
return sharedCore;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The production {@link TileHandler} assembled over the shared test engine —
|
|
42
|
+
* tests reach the same Rust functions (e.g. `mercatorY`) the app wires through
|
|
43
|
+
* the event context, instead of stubbing them. The `Core` is created lazily on
|
|
44
|
+
* first Core-backed call, so pure-function consumers never construct it.
|
|
45
|
+
*/
|
|
46
|
+
export function createTestTileHandler(): TileHandler {
|
|
47
|
+
initTestEngine();
|
|
48
|
+
return createTileHandler({
|
|
49
|
+
getCore: () => getTestCore(),
|
|
50
|
+
getVectorRevision: () => 0,
|
|
51
|
+
getRasterRevision: () => 0,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -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 };
|
|
@@ -77,6 +77,10 @@ export const defaultAtlasFactory =
|
|
|
77
77
|
export type TileTextureCompositorOptions = {
|
|
78
78
|
renderer: WebGLRenderer;
|
|
79
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;
|
|
80
84
|
/** Atlas RT side length (defaults to 512). */
|
|
81
85
|
size?: number;
|
|
82
86
|
/** Test seam: replace MRT RT creation. */
|
|
@@ -142,6 +146,7 @@ export class TileTextureCompositor {
|
|
|
142
146
|
* per-tile GPU byte cost it reports to the memory ledger. */
|
|
143
147
|
readonly size: number;
|
|
144
148
|
private readonly texturizedScenes: TexturizedSceneByTileCoordinates;
|
|
149
|
+
private readonly mercatorY: (lat: number) => number;
|
|
145
150
|
// Single camera shared by every drape bake (vector and raster), re-framed
|
|
146
151
|
// per source by `frameBakeCamera`. Ancestor fallback is resolved in Rust,
|
|
147
152
|
// so no per-tile camera transform is needed.
|
|
@@ -189,6 +194,7 @@ export class TileTextureCompositor {
|
|
|
189
194
|
constructor(opts: TileTextureCompositorOptions) {
|
|
190
195
|
this.renderer = opts.renderer;
|
|
191
196
|
this.texturizedScenes = opts.texturizedSceneByTileCoordinates;
|
|
197
|
+
this.mercatorY = opts.mercatorY;
|
|
192
198
|
this.size = opts.size ?? 512;
|
|
193
199
|
this.cache = new TileTextureCache({
|
|
194
200
|
size: this.size,
|
|
@@ -560,7 +566,7 @@ export class TileTextureCompositor {
|
|
|
560
566
|
// uniform ownership: each active enhancer attaches its own uniform refs and
|
|
561
567
|
// contributes the matching GLSL, so a new expression touches only its module.
|
|
562
568
|
const chain = createCompositeLayerEnhancers(features);
|
|
563
|
-
const core = createCoreUniformMutates();
|
|
569
|
+
const core = createCoreUniformMutates(this.mercatorY);
|
|
564
570
|
|
|
565
571
|
const uniforms: CompositeUniformTarget = {};
|
|
566
572
|
core.attachUniforms(uniforms, numTextures, placeholderTexture);
|