@navaramap/three 0.0.5 → 0.0.6

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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/fontWorker-JfTCpd38.js +2 -0
  3. package/dist/assets/fontWorker-JfTCpd38.js.map +1 -0
  4. package/dist/assets/{index-TKhRxurv.js → index-B3U_DvcP.js} +2 -2
  5. package/dist/assets/index-B3U_DvcP.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-Kk06ArRB.wasm → navara_wasm_font_worker_bg-DympKYVX.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-BZjLKf4i.wasm → navara_wasm_worker_bg-Bx_l45cW.wasm} +0 -0
  8. package/dist/index.d.ts +510 -374
  9. package/dist/index.js +6150 -5810
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.umd.cjs +1188 -793
  12. package/dist/index.umd.cjs.map +1 -1
  13. package/package.json +12 -12
  14. package/src/core/DescRegistry.ts +3 -2
  15. package/src/core/EffectDesc.ts +10 -0
  16. package/src/core/EffectDescRegistry.ts +14 -0
  17. package/src/core/MeshDesc.ts +39 -2
  18. package/src/core/MeshDescWithSelectiveEffect.ts +14 -2
  19. package/src/core/ViewContext.ts +116 -7
  20. package/src/device.ts +4 -2
  21. package/src/index.ts +123 -11
  22. package/src/layer.ts +7 -5
  23. package/src/layers/effect/MRTPassEffectDesc.ts +34 -3
  24. package/src/layers/effect/SelectiveEffectDesc.ts +10 -2
  25. package/src/layers/effect/TransparentPassEffectDesc.ts +5 -4
  26. package/src/layersManager.ts +32 -1
  27. package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +3 -2
  28. package/src/material/enhancer/polygon/polygonBaseEnhancer/index.ts +0 -6
  29. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +0 -6
  30. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +4 -10
  31. package/src/material/enhancer/polygon/polygonBaseEnhancer/state.ts +1 -4
  32. package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +0 -9
  33. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/coverage.ts +31 -0
  34. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.test.ts +75 -0
  35. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +26 -0
  36. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +1 -1
  37. package/src/material/gbufferLayout.test.ts +171 -0
  38. package/src/material/gbufferLayout.ts +298 -0
  39. package/src/material/index.ts +1 -0
  40. package/src/material/macro/tileShader.test.ts +35 -33
  41. package/src/material/macro/tileShader.ts +33 -10
  42. package/src/material/overrideMaterialsForMRT.ts +113 -71
  43. package/src/mesh/DrapedMesh.test.ts +57 -0
  44. package/src/mesh/DrapedMesh.ts +82 -0
  45. package/src/mesh/model.ts +3 -0
  46. package/src/mesh/polygon.ts +3 -2
  47. package/src/mesh/polyline.ts +3 -0
  48. package/src/mesh/sdfText/layout.ts +1 -3
  49. package/src/mesh/tile/index.ts +8 -4
  50. package/src/passes/CustomRenderPass.test.ts +143 -0
  51. package/src/passes/CustomRenderPass.ts +273 -30
  52. package/src/passes/RenderTargetCopyPass.ts +6 -0
  53. package/src/plugins/AttributionPlugin/index.ts +3 -2
  54. package/src/shaders/index.ts +16 -0
  55. package/src/type/index.ts +9 -51
  56. package/dist/assets/fontWorker-OwvKiQ8o.js +0 -2
  57. package/dist/assets/fontWorker-OwvKiQ8o.js.map +0 -1
  58. package/dist/assets/index-TKhRxurv.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@navaramap/three",
3
3
  "type": "module",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "main": "dist/index.umd.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -30,13 +30,13 @@
30
30
  "astronomy-engine": "2.1.19",
31
31
  "stats.js": "0.17.0",
32
32
  "three-stdlib": "2.36.1",
33
- "@navaramap/core": "0.0.5",
34
- "@navaramap/engine": "0.0.5",
35
- "@navaramap/engine-api": "0.0.5",
36
- "@navaramap/font": "0.0.5",
37
- "@navaramap/three-api": "0.0.5",
38
- "@navaramap/three-csm": "0.0.5",
39
- "@navaramap/worker": "0.0.5"
33
+ "@navaramap/engine-api": "0.0.6",
34
+ "@navaramap/engine": "0.0.6",
35
+ "@navaramap/font": "0.0.6",
36
+ "@navaramap/three-api": "0.0.6",
37
+ "@navaramap/three-csm": "0.0.6",
38
+ "@navaramap/core": "0.0.6",
39
+ "@navaramap/worker": "0.0.6"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@playwright/test": "1.61.1",
@@ -71,10 +71,10 @@
71
71
  "tweakpane": "4.0.5",
72
72
  "vite-plugin-glsl": "1.6.0",
73
73
  "vite-plugin-static-copy": "4.1.1",
74
- "@navaramap/three-default-descs": "0.0.5",
75
- "@navaramap/three-plugins": "0.0.5",
76
- "@navaramap/three-react": "0.0.5",
77
- "@navaramap/three-default-plugin": "0.0.5"
74
+ "@navaramap/three-default-descs": "0.0.6",
75
+ "@navaramap/three-default-plugin": "0.0.6",
76
+ "@navaramap/three-react": "0.0.6",
77
+ "@navaramap/three-plugins": "0.0.6"
78
78
  },
79
79
  "scripts": {
80
80
  "start": "vite",
@@ -80,9 +80,10 @@ export abstract class DescRegistry<
80
80
  }
81
81
 
82
82
  /**
83
- * Get constructor by name (protected method for subclasses)
83
+ * Get constructor by name. Public so `ViewContext.findLight`/`findMesh`
84
+ * can resolve a registered key to its descriptor class.
84
85
  */
85
- protected getConstructor(name: string): TConstructor | undefined {
86
+ getConstructor(name: string): TConstructor | undefined {
86
87
  return this.registry.get(name);
87
88
  }
88
89
  }
@@ -6,6 +6,7 @@ import invariant from "tiny-invariant";
6
6
 
7
7
  import { Pass } from "../effects";
8
8
  import type ThreeView from "../index";
9
+ import type { GBufferName } from "../material/gbufferLayout";
9
10
 
10
11
  import {
11
12
  BaseDesc,
@@ -194,6 +195,15 @@ export abstract class EffectDesc<
194
195
  static insertBefore?: string[];
195
196
  /** Set to `true` to allow multiple instances of this effect in the pipeline. */
196
197
  static allowDuplication?: boolean;
198
+ /**
199
+ * Optional G-buffer attachments this effect reads. The view derives its
200
+ * G-buffer configuration as the union of these across active effects:
201
+ * adding an effect allocates what it needs, removing the last effect that
202
+ * needs a buffer releases it. Changing the configuration reallocates
203
+ * attachments and recompiles shaders, so add effects once and tune them
204
+ * via `update()` instead of re-adding.
205
+ */
206
+ static requiredBuffers?: readonly GBufferName[];
197
207
 
198
208
  private instanceId: string;
199
209
 
@@ -1,4 +1,5 @@
1
1
  import type ThreeView from "../index";
2
+ import type { GBufferName } from "../material/gbufferLayout";
2
3
 
3
4
  import { DescRegistry } from "./DescRegistry";
4
5
  import { EffectDesc, type EffectConfig } from "./EffectDesc";
@@ -28,4 +29,17 @@ export class EffectDescRegistry extends DescRegistry<
28
29
  findEffectType(config: Record<string, unknown>): string | null {
29
30
  return this.findTypeFromConfig(config);
30
31
  }
32
+
33
+ /**
34
+ * Optional G-buffer attachments the effect type declares via the
35
+ * `requiredBuffers` static (empty when it declares none).
36
+ */
37
+ getRequiredBuffers(effectType: string): readonly GBufferName[] {
38
+ const EffectClass = this.getConstructor(effectType) as
39
+ | (EffectDescConstructor & {
40
+ requiredBuffers?: readonly GBufferName[];
41
+ })
42
+ | undefined;
43
+ return EffectClass?.requiredBuffers ?? [];
44
+ }
31
45
  }
@@ -1,8 +1,9 @@
1
1
  import type { BaseEventMap, XYZ } from "@navaramap/core";
2
- import { Euler, Matrix4, Object3D, Vector3 } from "three";
2
+ import { Euler, Matrix4, Object3D, Vector3, type Mesh } from "three";
3
3
  import invariant from "tiny-invariant";
4
4
 
5
5
  import type ThreeView from "../index";
6
+ import { applyLitOption } from "../material";
6
7
  import type { Scenes } from "../scene";
7
8
 
8
9
  import {
@@ -44,11 +45,18 @@ export type MeshConfig = {
44
45
  * Disables Three.js's auto matrix-world update.
45
46
  */
46
47
  matrixWorld?: Matrix4;
48
+ /**
49
+ * Lighting override for every material of the mesh. `false` outputs albedo
50
+ * only, while normals and the shadow G-buffer keep being written.
51
+ * `undefined` follows `view.lit` — passing it to `update()` resets a mesh
52
+ * to inheriting again.
53
+ */
54
+ lit?: boolean;
47
55
  } & BaseDescConfig;
48
56
 
49
57
  export type MeshUpdate = Pick<
50
58
  MeshConfig,
51
- "position" | "scale" | "rotation" | "matrix" | "matrixWorld"
59
+ "position" | "scale" | "rotation" | "matrix" | "matrixWorld" | "lit"
52
60
  > &
53
61
  BaseDescConfigUpdate;
54
62
 
@@ -214,6 +222,8 @@ export abstract class MeshDesc<
214
222
  public rotation?: XYZ;
215
223
  public matrix?: Matrix4;
216
224
  public matrixWorld?: Matrix4;
225
+ /** Three-state `lit` override (see {@link MeshConfig.lit}). */
226
+ public lit?: boolean;
217
227
  private prevPassKey?: PassKey;
218
228
 
219
229
  constructor(view: ThreeView, ctx: ViewContext, config?: Config) {
@@ -224,6 +234,25 @@ export abstract class MeshDesc<
224
234
  this.rotation = resolvedConfig.rotation;
225
235
  this.matrix = resolvedConfig.matrix;
226
236
  this.matrixWorld = resolvedConfig.matrixWorld;
237
+ this.lit = resolvedConfig.lit;
238
+ }
239
+
240
+ /**
241
+ * Applies {@link lit} to every material under the mesh. Subclasses that
242
+ * attach materials asynchronously (glTF) call it again once they exist.
243
+ */
244
+ protected applyLit(): void {
245
+ const root = this.raw;
246
+ if (!root) return;
247
+ root.traverse((object) => {
248
+ const material = (object as Partial<Mesh>).material;
249
+ if (!material) return;
250
+ if (Array.isArray(material)) {
251
+ for (const m of material) applyLitOption(m, this.lit);
252
+ } else {
253
+ applyLitOption(material, this.lit);
254
+ }
255
+ });
227
256
  }
228
257
 
229
258
  /**
@@ -272,6 +301,7 @@ export abstract class MeshDesc<
272
301
  invariant(this.raw);
273
302
 
274
303
  this.applyTransform();
304
+ this.applyLit();
275
305
 
276
306
  this._instance.visible = this.visible;
277
307
 
@@ -391,6 +421,12 @@ export abstract class MeshDesc<
391
421
  if (updates.scale !== undefined) this.scale = updates.scale;
392
422
  if (updates.rotation !== undefined) this.rotation = updates.rotation;
393
423
 
424
+ // Key presence, not value: `!== undefined` could not express a reset.
425
+ if ("lit" in updates) {
426
+ this.lit = updates.lit;
427
+ this.applyLit();
428
+ }
429
+
394
430
  if (spatialChanged) {
395
431
  // With a frame present, the effective transform depends on the
396
432
  // combination of all stored fields — recompose the whole thing.
@@ -428,6 +464,7 @@ export abstract class MeshDesc<
428
464
  }
429
465
  this.prevPassKey = nextPassKey;
430
466
  this.addToScene(nextPassKey);
467
+ this.ctx.emit("meshPassKeyChanged");
431
468
  }
432
469
 
433
470
  onDestroy(): void {
@@ -39,6 +39,10 @@ export abstract class MeshDescWithSelectiveEffect<
39
39
  this.onPassKeyChange();
40
40
  };
41
41
 
42
+ private _onGBufferChanged = () => {
43
+ this.onPassKeyChange();
44
+ };
45
+
42
46
  constructor(view: ThreeView, ctx: ViewContext, config?: Config) {
43
47
  const resolvedConfig = config ?? ({} as Config);
44
48
  super(view, ctx, resolvedConfig);
@@ -51,11 +55,16 @@ export abstract class MeshDescWithSelectiveEffect<
51
55
  // ordering between SE participants stays consistent — an empty
52
56
  // effectIds list still has to coexist with non-empty siblings, and is
53
57
  // expressed by effectIdsMask=0 rather than by falling back to opaque.
54
- // When no effect is registered, MRT participation is pure overhead, so
55
- // defer to the parent's default (opaque) pass.
56
58
  if (this.ctx.selectiveEffectRegistry.slotCount > 0) {
57
59
  return "mrt";
58
60
  }
61
+ // Likewise while any optional G-buffer is allocated: outside the MRT pass
62
+ // the mesh leaves the G-buffer holding what lies behind it, and effects
63
+ // reading those buffers shade "through" it.
64
+ if (Object.values(this.view.buffers).some(Boolean)) {
65
+ return "mrt";
66
+ }
67
+ // Otherwise MRT participation is pure overhead.
59
68
  return super.getPassKey();
60
69
  }
61
70
 
@@ -67,6 +76,8 @@ export abstract class MeshDescWithSelectiveEffect<
67
76
 
68
77
  // Recompute mask when slot assignments change (effect added/removed)
69
78
  this.ctx.on("effectSlotsChanged", this._onSlotsChanged);
79
+ // G-buffer allocation changes also flip getPassKey() (mrt <-> opaque).
80
+ this.ctx.on("gbufferChanged", this._onGBufferChanged);
70
81
  }
71
82
 
72
83
  override onUpdateConfig(updates: UpdateConfig): void {
@@ -122,6 +133,7 @@ export abstract class MeshDescWithSelectiveEffect<
122
133
 
123
134
  // Unsubscribe from slot changes
124
135
  this.ctx.off("effectSlotsChanged", this._onSlotsChanged);
136
+ this.ctx.off("gbufferChanged", this._onGBufferChanged);
125
137
 
126
138
  super.onDestroy();
127
139
  }
@@ -2,17 +2,22 @@ import { EventHandler } from "@navaramap/core";
2
2
  import type { Core } from "@navaramap/engine";
3
3
  import type { ConcurrencyManager } from "@navaramap/worker";
4
4
  import type { Pass as PostProcessingPass } from "postprocessing";
5
- import type { Material, Object3D, WebGLRenderer } from "three";
5
+ import type { Material, Object3D, Texture, WebGLRenderer } from "three";
6
6
  import invariant from "tiny-invariant";
7
7
 
8
8
  import type { LayersManager } from "../layersManager";
9
+ import { type ResolvedGBufferOptions } from "../material/gbufferLayout";
9
10
  import type { PickableMesh } from "../mesh/pickableMesh";
10
11
  import type { RenderPassOrchestrator } from "../orchestrators";
11
12
  import type { CustomRenderPass } from "../passes";
12
13
  import type { Scenes } from "../scene";
13
14
  import type { MeshCache } from "../type";
14
15
 
15
- import type { EffectHandle } from "./BaseHandle";
16
+ import type { EffectHandle, LightHandle, MeshHandle } from "./BaseHandle";
17
+ import type { EffectDesc } from "./EffectDesc";
18
+ import type { LightDesc } from "./LightDesc";
19
+ import type { MeshDesc } from "./MeshDesc";
20
+ import type { Registries } from "./Registries";
16
21
  import { SelectiveEffectRegistry } from "./SelectiveEffectRegistry";
17
22
 
18
23
  type ViewContextEvents = {
@@ -31,6 +36,18 @@ type ViewContextEvents = {
31
36
  * Listeners should recompute effectIdsMask to stay in sync.
32
37
  */
33
38
  effectSlotsChanged: () => void;
39
+ /**
40
+ * Emitted when the derived G-buffer configuration changes. Meshes re-evaluate
41
+ * their pass placement: outside the MRT pass they would leave the G-buffer
42
+ * holding whatever lies behind them.
43
+ */
44
+ gbufferChanged: () => void;
45
+ /**
46
+ * Emitted when a mesh moves between render scenes. The view re-derives the
47
+ * buffer configuration from it: draped meshes need the globe normal but,
48
+ * not being effects, cannot declare `requiredBuffers` themselves.
49
+ */
50
+ meshPassKeyChanged: () => void;
34
51
  };
35
52
 
36
53
  /**
@@ -49,6 +66,7 @@ type ViewContextEvents = {
49
66
  export class ViewContext extends EventHandler<ViewContextEvents> {
50
67
  private _selectiveEffectRegistry: SelectiveEffectRegistry;
51
68
  private _renderPass?: CustomRenderPass;
69
+ private _registries?: Registries;
52
70
 
53
71
  constructor(
54
72
  /** Scene containers for different rendering passes. */
@@ -133,6 +151,26 @@ export class ViewContext extends EventHandler<ViewContextEvents> {
133
151
  this._renderPass = renderPass;
134
152
  }
135
153
 
154
+ /** @internal */
155
+ _setRegistries(registries: Registries) {
156
+ this._registries = registries;
157
+ }
158
+
159
+ /**
160
+ * @internal
161
+ * No-op until the pass exists — it reads the view's configuration at
162
+ * creation time.
163
+ */
164
+ _setGBufferOptions(buffers: ResolvedGBufferOptions): void {
165
+ this._renderPass?.setBuffers(buffers);
166
+ this.emit("gbufferChanged");
167
+ }
168
+
169
+ /** @internal No-op until the pass exists (see {@link _setGBufferOptions}). */
170
+ _setLit(lit: boolean): void {
171
+ this._renderPass?.setLit(lit);
172
+ }
173
+
136
174
  /**
137
175
  * Gets the globe depth texture for post-processing effects.
138
176
  */
@@ -162,23 +200,47 @@ export class ViewContext extends EventHandler<ViewContextEvents> {
162
200
  */
163
201
  getNormalTexture() {
164
202
  invariant(this._renderPass, "CustomRenderPass isn't initialized yet.");
165
- return this._renderPass.gbufferRenderTarget.textures[1];
203
+ const index = this._renderPass.textureIndex.normal;
204
+ return index === undefined
205
+ ? undefined
206
+ : this._renderPass.gbufferRenderTarget.textures[index];
166
207
  }
167
208
 
168
209
  /**
169
210
  * Gets the effect IDs texture from the G-buffer.
211
+ * `undefined` when the view disabled `buffers.selectiveEffect`.
170
212
  */
171
- getEffectIdsTexture() {
213
+ getEffectIdsTexture(): Texture | undefined {
172
214
  invariant(this._renderPass, "CustomRenderPass isn't initialized yet.");
173
- return this._renderPass.gbufferRenderTarget.textures[2];
215
+ const index = this._renderPass.textureIndex.effectIds;
216
+ return index === undefined
217
+ ? undefined
218
+ : this._renderPass.gbufferRenderTarget.textures[index];
174
219
  }
175
220
 
176
221
  /**
177
222
  * Gets the emissive texture from the G-buffer.
223
+ * `undefined` when the view disabled `buffers.emissive`.
224
+ */
225
+ getEmissiveTexture(): Texture | undefined {
226
+ invariant(this._renderPass, "CustomRenderPass isn't initialized yet.");
227
+ const index = this._renderPass.textureIndex.emissive;
228
+ return index === undefined
229
+ ? undefined
230
+ : this._renderPass.gbufferRenderTarget.textures[index];
231
+ }
232
+
233
+ /**
234
+ * Gets the shadow texture (R=shadow amount, 0=lit..1=fully shadowed) from
235
+ * the G-buffer. `undefined` unless an active effect requires the `shadow`
236
+ * buffer.
178
237
  */
179
- getEmissiveTexture() {
238
+ getShadowTexture(): Texture | undefined {
180
239
  invariant(this._renderPass, "CustomRenderPass isn't initialized yet.");
181
- return this._renderPass.gbufferRenderTarget.textures[3];
240
+ const index = this._renderPass.textureIndex.shadow;
241
+ return index === undefined
242
+ ? undefined
243
+ : this._renderPass.gbufferRenderTarget.textures[index];
182
244
  }
183
245
 
184
246
  // --- Descriptor query ---
@@ -188,6 +250,53 @@ export class ViewContext extends EventHandler<ViewContextEvents> {
188
250
  return this.layersManager.getEffectDescs();
189
251
  }
190
252
 
253
+ /** @internal Iterate over all registered light descriptors. */
254
+ _getLights(): Generator<LightHandle> {
255
+ return this.layersManager.getLightDescs();
256
+ }
257
+
258
+ /** @internal Iterate over all registered mesh descriptors. */
259
+ _getMeshes(): Generator<MeshHandle> {
260
+ return this.layersManager.getMeshDescs();
261
+ }
262
+
263
+ /**
264
+ * Finds an active effect descriptor by its `key` (e.g. `"mrt"`,
265
+ * `"selectiveBloom"`). Returns the first match, or `undefined`.
266
+ */
267
+ findEffect<T extends EffectDesc = EffectDesc>(key: string): T | undefined {
268
+ for (const handle of this._getEffects()) {
269
+ if (handle.ref.getKey() === key) return handle.ref as T;
270
+ }
271
+ return undefined;
272
+ }
273
+
274
+ /**
275
+ * Finds the first active light descriptor registered under `key` (e.g.
276
+ * `"sun"`), for inheriting the scene's lighting in a custom effect.
277
+ */
278
+ findLight<T extends LightDesc = LightDesc>(key: string): T | undefined {
279
+ const LightClass = this._registries?.light.getConstructor(key);
280
+ if (!LightClass) return undefined;
281
+ for (const handle of this._getLights()) {
282
+ if (handle.ref instanceof LightClass) return handle.ref as T;
283
+ }
284
+ return undefined;
285
+ }
286
+
287
+ /**
288
+ * Finds an active mesh descriptor by its registered key (e.g. `"box"`,
289
+ * `"gltfModel"`). Returns the first match, or `undefined`.
290
+ */
291
+ findMesh<T extends MeshDesc = MeshDesc>(key: string): T | undefined {
292
+ const MeshClass = this._registries?.mesh.getConstructor(key);
293
+ if (!MeshClass) return undefined;
294
+ for (const handle of this._getMeshes()) {
295
+ if (handle.ref instanceof MeshClass) return handle.ref as T;
296
+ }
297
+ return undefined;
298
+ }
299
+
191
300
  /**
192
301
  * Register a material for CSM shadow rendering.
193
302
  * @experimental This API may change or be removed in future versions.
package/src/device.ts CHANGED
@@ -53,8 +53,10 @@ const MOBILE_MAX_PIXEL_RATIO = 1.0;
53
53
  /**
54
54
  * Default pixel-ratio cap applied on every mobile device (without opting into
55
55
  * `mobileOptimization`). The fixed screen-sized render-target stack (composer
56
- * buffers, gbuffer MRT, depth textures — ~100 B/px, counted by the ledger as
57
- * `fixed_gpu_bytes`) grows quadratically with the drawing-buffer size: at
56
+ * buffers, gbuffer MRT and depth textures, counted by the ledger as
57
+ * `fixed_gpu_bytes`) grows quadratically with the drawing-buffer size — and
58
+ * with the derived `buffers` configuration, which starts at two attachments
59
+ * (color + normal) and grows as effects declare `requiredBuffers`: at
58
60
  * DPR 3 it alone can exceed a mobile tile-cache budget and permanently close
59
61
  * the load gate. Above DPR 2 the sharpness gain is marginal on phone screens.
60
62
  */
package/src/index.ts CHANGED
@@ -108,7 +108,14 @@ import {
108
108
  import { FinalCopyEffectDesc } from "./layers/effect/FinalCopyEffectDesc";
109
109
  import { LayersManager } from "./layersManager";
110
110
  import { disposeTexture } from "./loaders";
111
- import { overrideMaterialsForMRT } from "./material";
111
+ import {
112
+ overrideMaterialsForMRT,
113
+ GBUFFER_ATTACHMENT_NAMES,
114
+ resolveGBufferOptions,
115
+ unionGBufferRequirements,
116
+ type GBufferName,
117
+ type ResolvedGBufferOptions,
118
+ } from "./material";
112
119
  import type { TileMesh } from "./mesh/tile";
113
120
  import { RenderPassOrchestrator } from "./orchestrators/RenderPassOrchestrator";
114
121
  import { PickHelper } from "./pick/pickHelper";
@@ -245,11 +252,6 @@ export type WorkerMemoryStats = {
245
252
  fontWorker: FontWorkerMemoryStats | undefined;
246
253
  };
247
254
 
248
- // NOTE:
249
- // This overrides all materials to output a normal buffer, meaning Navara operates using MRT (Multiple Render Targets).
250
- // Currently, Navara requires two buffers, so your shader must output them.
251
- overrideMaterialsForMRT();
252
-
253
255
  /**
254
256
  * Configuration options for initializing ThreeView.
255
257
  */
@@ -792,6 +794,15 @@ export default class ThreeView<
792
794
  /** Built-in attribution UI; unset when disabled or in a worker (no DOM). */
793
795
  private _attribution?: AttributionPlugin;
794
796
 
797
+ /**
798
+ * Derived G-buffer attachment configuration — the union of the
799
+ * `requiredBuffers` declared by the active effects.
800
+ */
801
+ private _buffers: ResolvedGBufferOptions;
802
+
803
+ /** Scene-level default of the `lit` material option (see {@link lit}). */
804
+ private _lit = true;
805
+
795
806
  private pixelRatioMatchedMedia?: MediaQueryList;
796
807
 
797
808
  private fixedGpuBytesReportScheduled = false;
@@ -808,9 +819,43 @@ export default class ThreeView<
808
819
  return this._attribution;
809
820
  }
810
821
 
822
+ /**
823
+ * Buffers currently allocated. Not configurable — derived as the union of
824
+ * the active effects' `requiredBuffers`. All but `globeNormal` are G-buffer
825
+ * attachments.
826
+ */
827
+ get buffers(): ResolvedGBufferOptions {
828
+ return this._buffers;
829
+ }
830
+
831
+ /**
832
+ * Scene-level default of the `lit` material option. `false` makes every
833
+ * material without an explicit `lit` output plain albedo, while normals and
834
+ * the shadow G-buffer keep being written – the input for deferred lighting.
835
+ * Toggling recompiles the affected shaders once.
836
+ * @defaultValue true
837
+ */
838
+ get lit(): boolean {
839
+ return this._lit;
840
+ }
841
+
842
+ set lit(value: boolean) {
843
+ if (this._lit === value) return;
844
+ this._lit = value;
845
+ this.viewContext._setLit(value);
846
+ this.forceUpdate();
847
+ }
848
+
811
849
  constructor(options: Options = {}) {
812
850
  super();
813
851
 
852
+ // Patches the global ShaderLib, but the injected source is
853
+ // view-independent (defines carry the layout). Here rather than at module
854
+ // import so importing the library has no side effects.
855
+ overrideMaterialsForMRT();
856
+
857
+ this._buffers = resolveGBufferOptions();
858
+
814
859
  if (!options.canvas) {
815
860
  const div = document.createElement("div");
816
861
  div.id = "navara-root";
@@ -1412,6 +1457,8 @@ export default class ThreeView<
1412
1457
  this._meshes,
1413
1458
  );
1414
1459
  this.registries = new Registries(this, this.viewContext);
1460
+ this.viewContext.on("meshPassKeyChanged", this._syncGBuffers);
1461
+ this.viewContext._setRegistries(this.registries);
1415
1462
  this.eventContext = new EventContext({
1416
1463
  eventManager: this._eventManager,
1417
1464
  scenes: this._scenes,
@@ -2039,9 +2086,26 @@ export default class ThreeView<
2039
2086
  throw new UnknownTypeError("effect", config);
2040
2087
  }
2041
2088
 
2089
+ // Reject the effect while nothing is created yet if its required
2090
+ // G-buffers would exceed the device's MRT attachment limit
2091
+ // (gl.MAX_DRAW_BUFFERS – spec minimum 4, nearly all devices expose 8).
2092
+ this._assertGBufferCapacity(effectType);
2093
+
2042
2094
  // Create effect descriptor instance
2043
2095
  const effectDesc = this.registries.effect.create(effectType, config);
2044
2096
 
2097
+ const l = new EffectHandle(effectDesc);
2098
+
2099
+ // Store the effect descriptor
2100
+ this.layersManager.add(l);
2101
+
2102
+ // Allocate before `onCreate()`: `createPass()` reads the buffers this
2103
+ // effect declared, so they have to exist by then. Reallocates attachments
2104
+ // and recompiles shaders when the derived configuration actually changes,
2105
+ // so tune effects via `update()` rather than re-adding them.
2106
+ this._syncGBuffers();
2107
+ l.on("deleted", this._syncGBuffers);
2108
+
2045
2109
  // Initialize the effect
2046
2110
  effectDesc.onCreate();
2047
2111
 
@@ -2053,15 +2117,63 @@ export default class ThreeView<
2053
2117
  // Trigger re-render
2054
2118
  effectDesc.on("needsUpdate", this.forceUpdate);
2055
2119
 
2056
- const l = new EffectHandle(effectDesc);
2057
-
2058
- // Store the effect descriptor
2059
- this.layersManager.add(l);
2060
-
2061
2120
  // Return handle for imperative access
2062
2121
  return l as EffectHandle<L>;
2063
2122
  }
2064
2123
 
2124
+ /**
2125
+ * Re-derives the G-buffer configuration from the union of the active
2126
+ * effects' `requiredBuffers` and applies it to the MRT pass when changed.
2127
+ */
2128
+ private _syncGBuffers = (): void => {
2129
+ const requirements: (readonly GBufferName[])[] = [];
2130
+ for (const handle of this.layersManager.getEffectDescs()) {
2131
+ const EffectClass = handle.ref.constructor as {
2132
+ requiredBuffers?: readonly GBufferName[];
2133
+ };
2134
+ if (EffectClass.requiredBuffers) {
2135
+ requirements.push(EffectClass.requiredBuffers);
2136
+ }
2137
+ }
2138
+ // Draped meshes are not effects, so they cannot declare this themselves:
2139
+ // they read the terrain normal through the globe-normal copy.
2140
+ if (this._scenes.draped.children.length > 0) {
2141
+ requirements.push(["globeNormal"]);
2142
+ }
2143
+ this._buffers = unionGBufferRequirements(requirements);
2144
+ this.viewContext._setGBufferOptions(this._buffers);
2145
+ };
2146
+
2147
+ /**
2148
+ * Throws when enabling `effectType`'s required G-buffers would push the
2149
+ * attachment count (color + normal + optional buffers) past the device's
2150
+ * `MAX_DRAW_BUFFERS` limit.
2151
+ */
2152
+ private _assertGBufferCapacity(effectType: string): void {
2153
+ const required = this.registries.effect.getRequiredBuffers(effectType);
2154
+ if (required.length === 0) return;
2155
+ const prospective = unionGBufferRequirements([
2156
+ (Object.keys(this._buffers) as GBufferName[]).filter(
2157
+ (name) => this._buffers[name],
2158
+ ),
2159
+ required,
2160
+ ]);
2161
+ const attachmentCount =
2162
+ 2 + GBUFFER_ATTACHMENT_NAMES.filter((name) => prospective[name]).length;
2163
+ // Navara requires WebGL2 (MRT), so the context is always WebGL2.
2164
+ const gl = this.viewContext
2165
+ .getRenderer()
2166
+ .getContext() as WebGL2RenderingContext;
2167
+ const maxDrawBuffers = gl.getParameter(gl.MAX_DRAW_BUFFERS) as number;
2168
+ if (attachmentCount > maxDrawBuffers) {
2169
+ throw new Error(
2170
+ `The "${effectType}" effect requires G-buffers [${required.join(", ")}], ` +
2171
+ `but the resulting ${attachmentCount} MRT attachments exceed this ` +
2172
+ `device's MAX_DRAW_BUFFERS (${maxDrawBuffers}).`,
2173
+ );
2174
+ }
2175
+ }
2176
+
2065
2177
  /**
2066
2178
  * Finds a resource layer by its ID.
2067
2179
  * Only returns resource layers added via `addLayer()` (not mesh, light, or
package/src/layer.ts CHANGED
@@ -51,12 +51,14 @@ export type LayerEvent = {
51
51
  *
52
52
  * @example
53
53
  * ```typescript
54
- * // Add a GeoJSON layer
55
- * const geoJsonLayer = view.addLayer({
54
+ * // Register a GeoJSON source, then render it with a vector layer
55
+ * const geoJsonSource = view.addSource({
56
56
  * type: "geojson",
57
- * data: {
58
- * url: "https://example.com/data.geojson",
59
- * },
57
+ * url: "https://example.com/data.geojson",
58
+ * });
59
+ * const geoJsonLayer = view.addLayer({
60
+ * type: "vector",
61
+ * source: geoJsonSource,
60
62
  * point: { color: 0xff0000 }
61
63
  * });
62
64
  *