@expofp/renderer 3.34.1 → 3.35.0

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 (61) hide show
  1. package/dist/Renderer.d.ts +14 -0
  2. package/dist/Renderer.js +16 -4
  3. package/dist/core/shader-source.d.ts +20 -0
  4. package/dist/core/shader-source.js +26 -0
  5. package/dist/debug/components/Gui.d.ts +2 -0
  6. package/dist/debug/components/Gui.js +11 -1
  7. package/dist/external/ExternalCanvas.js +5 -6
  8. package/dist/external/external-camera.js +26 -11
  9. package/dist/index.d.ts +1 -0
  10. package/dist/objects/Background.js +1 -1
  11. package/dist/objects/containers/SlotContainer.js +7 -1
  12. package/dist/objects/containers/hooks.js +1 -1
  13. package/dist/objects/containers/props.d.ts +10 -0
  14. package/dist/objects/images/Images.js +9 -2
  15. package/dist/objects/images/image-container.js +1 -1
  16. package/dist/objects/layer/Layer.js +10 -10
  17. package/dist/objects/layer/mount-scheduler.d.ts +83 -54
  18. package/dist/objects/layer/mount-scheduler.js +204 -60
  19. package/dist/objects/lighting/Lighting.d.ts +92 -0
  20. package/dist/objects/lighting/Lighting.js +385 -0
  21. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  22. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  23. package/dist/objects/lighting/index.d.ts +11 -0
  24. package/dist/objects/lighting/index.js +10 -0
  25. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  26. package/dist/objects/lighting/lighting-gui.js +114 -0
  27. package/dist/objects/lighting/occluder.d.ts +34 -0
  28. package/dist/objects/lighting/occluder.js +62 -0
  29. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  30. package/dist/objects/lighting/occlusion-targets.js +276 -0
  31. package/dist/objects/lines/Lines.js +3 -1
  32. package/dist/objects/materials/ColorMaterial.d.ts +58 -51
  33. package/dist/objects/materials/ColorMaterial.js +74 -52
  34. package/dist/objects/materials/LineMaterial.js +1 -1
  35. package/dist/objects/materials/ModelMaterial.d.ts +30 -18
  36. package/dist/objects/materials/ModelMaterial.js +68 -41
  37. package/dist/objects/materials/TextureMaterial.js +1 -1
  38. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  39. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  40. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  41. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  42. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  43. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  44. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  45. package/dist/objects/materials/effects/ao/index.js +72 -0
  46. package/dist/objects/materials/{dim.d.ts → effects/color-dim.d.ts} +4 -4
  47. package/dist/objects/materials/{dim.js → effects/color-dim.js} +6 -24
  48. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  49. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  50. package/dist/objects/materials/effects/effects-context.d.ts +52 -0
  51. package/dist/objects/materials/effects/effects-context.js +24 -0
  52. package/dist/objects/models/Models.js +18 -2
  53. package/dist/objects/models/model-container.js +1 -1
  54. package/dist/objects/shapes/Shapes.js +20 -2
  55. package/dist/objects/text/Texts.js +3 -1
  56. package/dist/objects/text/text-container.js +1 -1
  57. package/dist/space/Scene.d.ts +3 -0
  58. package/dist/space/Scene.js +17 -5
  59. package/package.json +2 -2
  60. package/dist/objects/Lighting.d.ts +0 -20
  61. package/dist/objects/Lighting.js +0 -85
@@ -0,0 +1,143 @@
1
+ import { Vector2, type WebGLRenderer, WebGLRenderTarget } from 'three';
2
+ /**
3
+ * The three render targets the occlusion passes draw into, over two color textures.
4
+ *
5
+ * | Target | Color texture | Depth attachment | Written by | Read by |
6
+ * | ------- | ---------------------- | ---------------------- | ---------- | --------------- |
7
+ * | `depth` | its own, never written | `DepthTexture`, 24-bit | prepass | nothing |
8
+ * | `ao` | its own | none | AO pass | the blur pass |
9
+ * | `blur` | the `depth` target's | none | blur pass | the lit shaders |
10
+ *
11
+ * The sharing brings rules, and they live here because this class is the only place that can keep
12
+ * them.
13
+ *
14
+ * A target must not carry a texture that the pass drawing into it samples. WebGL2 calls that a
15
+ * rendering feedback loop, leaves the result undefined, and browsers refuse the draw. Both the AO
16
+ * pass and the blur pass sample the prepass depth, so neither one can bind a target that carries it.
17
+ * That is why two of these three targets carry no depth attachment.
18
+ *
19
+ * Three targets over two color textures. The prepass writes depth with `colorWrite` off, so nothing
20
+ * writes its color attachment and nothing reads it. The blur pass writes what the lit shaders read,
21
+ * so it takes that attachment. The handover costs one framebuffer and no GL texture at all. three's
22
+ * `setupRenderTarget` creates a texture only for a target that has none. `RenderTarget` has a
23
+ * `texture` setter, so this is supported API rather than a reach into internals.
24
+ *
25
+ * Two textures and not one, because the blur pass reads the AO pass's output while it writes its own.
26
+ * One texture for both is the feedback loop above.
27
+ *
28
+ * The set costs `1.5` bytes for each canvas pixel: `1.0` for 24-bit depth and `0.25` for each `R8`
29
+ * color texture, all at a quarter of the canvas area. A `2880 × 1800` canvas therefore holds about
30
+ * `7.8 MB` of them.
31
+ *
32
+ * Never call `setSize`. It calls `dispose()`, and three's deallocation deletes the GL texture of
33
+ * every entry in `renderTarget.textures`, a shared one included. A size change rebuilds all three
34
+ * instead.
35
+ *
36
+ * Dispose all three together, from here. The first disposal of a shared texture deletes it and
37
+ * removes its properties, and the second disposal then finds nothing to delete. No one of the three
38
+ * can outlive the others.
39
+ *
40
+ * The completeness check stays, because a required format is no promise that a particular driver
41
+ * reports the combination complete.
42
+ */
43
+ export declare class OcclusionTargets {
44
+ /** The prepass's target, or `null` until this builds a complete set. */
45
+ private depthTarget;
46
+ /** The AO pass's target, over a color texture of its own, or `null` beside it. */
47
+ private aoTarget;
48
+ /** The blur pass's target, over the prepass's color texture, or `null` beside them. */
49
+ private blurTarget;
50
+ /** The set's pixel size, which is the canvas size at {@link CANVAS_FRACTION}. */
51
+ private readonly targetSize;
52
+ /** Whether this context cannot give a complete set. Once this turns true it stays true. */
53
+ private unavailable;
54
+ /** The prepass's target, or `null` until {@link OcclusionTargets.sync} returns `true`. */
55
+ get depth(): WebGLRenderTarget | null;
56
+ /** The AO pass's target, or `null` until {@link OcclusionTargets.sync} returns `true`. */
57
+ get ao(): WebGLRenderTarget | null;
58
+ /** The blur pass's target, or `null` until {@link OcclusionTargets.sync} returns `true`. */
59
+ get blur(): WebGLRenderTarget | null;
60
+ /** The set's pixel size. Read-only in spirit: the set owns it and rewrites it on a rebuild. */
61
+ get size(): Vector2;
62
+ /**
63
+ * Whether a set is allocated right now.
64
+ *
65
+ * A caller frees the set once when the debug GUI switches occlusion off, rather than on every frame
66
+ * after. This getter tells the caller which frame that is. A flag kept beside the set drifts the
67
+ * first time a sync fails or a rebuild releases the set on its own.
68
+ */
69
+ get held(): boolean;
70
+ /**
71
+ * Frees all three targets and the depth texture, and forgets their size.
72
+ *
73
+ * This takes no renderer. A `RenderTarget` disposal is an event three's own renderer listens for,
74
+ * so an unmount effect with nothing else in hand can call it.
75
+ */
76
+ dispose(): void;
77
+ /**
78
+ * Brings the set to the canvas's current size, and rebuilds it when that size moves.
79
+ * @param gl - The renderer, for the canvas size and for the completeness check.
80
+ * @returns Whether a complete set now exists to draw into.
81
+ */
82
+ sync(gl: WebGLRenderer): boolean;
83
+ /**
84
+ * Allocates the set at a size, at one of the two depth widths.
85
+ * @param width - The set's width in pixels.
86
+ * @param height - Its height.
87
+ * @param depthType - The depth texture's type, which decides its bit width.
88
+ */
89
+ private allocate;
90
+ /**
91
+ * Rebuilds the set, and falls back to 16-bit depth once before it gives up for good.
92
+ * @param gl - The renderer, for the completeness check.
93
+ * @param width - The set's width in pixels.
94
+ * @param height - Its height.
95
+ * @returns Whether a complete set now exists.
96
+ */
97
+ private build;
98
+ /**
99
+ * Frees whatever is allocated, together and in one place, and forgets its size.
100
+ */
101
+ private release;
102
+ /**
103
+ * Asks the driver whether the two framebuffer combinations this set uses came back complete, once
104
+ * per allocation.
105
+ *
106
+ * Binding each target through three is what builds its framebuffer, so the check runs against the
107
+ * real thing rather than against an intention. This puts the previously bound target back, which
108
+ * keeps a shared context unaware that the check ran. The MapLibre embed is that shared context.
109
+ *
110
+ * **Two checks cover three targets, because two of the targets are one combination.** The depth
111
+ * target is a color attachment beside a sampleable depth texture, at whichever of the two widths
112
+ * `build` is trying, and it is the only one of the three carrying depth at all. That is its own
113
+ * risk and it takes its own check. The blur target is a single `R8` color attachment over a
114
+ * texture the depth target already owns. The AO target is that same single `R8` attachment over a
115
+ * texture of its own, so the blur target covers the AO target's combination and adds the sharing,
116
+ * which is the part of this design a driver is most likely to refuse. A third bind would ask a
117
+ * question the second one already answers.
118
+ *
119
+ * **three's own renderer never calls `checkFramebufferStatus`**, so this is the only thing that
120
+ * detects a driver refusing either combination. What it buys is the 16-bit retry and, after that,
121
+ * occlusion switching itself off, rather than a plan drawing out of an incomplete framebuffer.
122
+ *
123
+ * A context that throws while three binds it answers the same question the status code answers, so
124
+ * this catches the throw. Occlusion switching itself off is the documented outcome. Taking the
125
+ * frame loop down with it is the worse one.
126
+ * @param gl - The renderer whose context answers.
127
+ * @returns Whether both combinations are complete.
128
+ */
129
+ private verify;
130
+ }
131
+ /**
132
+ * The share of the canvas size each axis of the set takes.
133
+ *
134
+ * Half, so each pass bills a quarter of the texels a full-size pass bills. Occlusion is a
135
+ * low-frequency signal. The lit read finds its own fragment by normalized coordinates, so the texel
136
+ * count behind that read changes nothing anyone can see.
137
+ *
138
+ * The canvas size and not the drawing buffer's, which is what `gl.getSize()` reports. A device pixel
139
+ * ratio of `2` squares into four times the texels, and both passes bill per texel. A set sized from
140
+ * the buffer therefore costs four times as much on a retina screen, for detail nothing asks of it.
141
+ */
142
+ export declare const CANVAS_FRACTION = 0.5;
143
+ //# sourceMappingURL=occlusion-targets.d.ts.map
@@ -0,0 +1,276 @@
1
+ import { DepthTexture, NearestFilter, RedFormat, UnsignedByteType, UnsignedIntType, UnsignedShortType, Vector2, WebGLRenderTarget, } from 'three';
2
+ import { createLogger } from '../../utils/logging';
3
+ const logger = createLogger('occlusion');
4
+ /**
5
+ * The three render targets the occlusion passes draw into, over two color textures.
6
+ *
7
+ * | Target | Color texture | Depth attachment | Written by | Read by |
8
+ * | ------- | ---------------------- | ---------------------- | ---------- | --------------- |
9
+ * | `depth` | its own, never written | `DepthTexture`, 24-bit | prepass | nothing |
10
+ * | `ao` | its own | none | AO pass | the blur pass |
11
+ * | `blur` | the `depth` target's | none | blur pass | the lit shaders |
12
+ *
13
+ * The sharing brings rules, and they live here because this class is the only place that can keep
14
+ * them.
15
+ *
16
+ * A target must not carry a texture that the pass drawing into it samples. WebGL2 calls that a
17
+ * rendering feedback loop, leaves the result undefined, and browsers refuse the draw. Both the AO
18
+ * pass and the blur pass sample the prepass depth, so neither one can bind a target that carries it.
19
+ * That is why two of these three targets carry no depth attachment.
20
+ *
21
+ * Three targets over two color textures. The prepass writes depth with `colorWrite` off, so nothing
22
+ * writes its color attachment and nothing reads it. The blur pass writes what the lit shaders read,
23
+ * so it takes that attachment. The handover costs one framebuffer and no GL texture at all. three's
24
+ * `setupRenderTarget` creates a texture only for a target that has none. `RenderTarget` has a
25
+ * `texture` setter, so this is supported API rather than a reach into internals.
26
+ *
27
+ * Two textures and not one, because the blur pass reads the AO pass's output while it writes its own.
28
+ * One texture for both is the feedback loop above.
29
+ *
30
+ * The set costs `1.5` bytes for each canvas pixel: `1.0` for 24-bit depth and `0.25` for each `R8`
31
+ * color texture, all at a quarter of the canvas area. A `2880 × 1800` canvas therefore holds about
32
+ * `7.8 MB` of them.
33
+ *
34
+ * Never call `setSize`. It calls `dispose()`, and three's deallocation deletes the GL texture of
35
+ * every entry in `renderTarget.textures`, a shared one included. A size change rebuilds all three
36
+ * instead.
37
+ *
38
+ * Dispose all three together, from here. The first disposal of a shared texture deletes it and
39
+ * removes its properties, and the second disposal then finds nothing to delete. No one of the three
40
+ * can outlive the others.
41
+ *
42
+ * The completeness check stays, because a required format is no promise that a particular driver
43
+ * reports the combination complete.
44
+ */
45
+ export class OcclusionTargets {
46
+ /** The prepass's target, or `null` until this builds a complete set. */
47
+ depthTarget = null;
48
+ /** The AO pass's target, over a color texture of its own, or `null` beside it. */
49
+ aoTarget = null;
50
+ /** The blur pass's target, over the prepass's color texture, or `null` beside them. */
51
+ blurTarget = null;
52
+ /** The set's pixel size, which is the canvas size at {@link CANVAS_FRACTION}. */
53
+ targetSize = new Vector2();
54
+ /** Whether this context cannot give a complete set. Once this turns true it stays true. */
55
+ unavailable = false;
56
+ /** The prepass's target, or `null` until {@link OcclusionTargets.sync} returns `true`. */
57
+ get depth() {
58
+ return this.depthTarget;
59
+ }
60
+ /** The AO pass's target, or `null` until {@link OcclusionTargets.sync} returns `true`. */
61
+ get ao() {
62
+ return this.aoTarget;
63
+ }
64
+ /** The blur pass's target, or `null` until {@link OcclusionTargets.sync} returns `true`. */
65
+ get blur() {
66
+ return this.blurTarget;
67
+ }
68
+ /** The set's pixel size. Read-only in spirit: the set owns it and rewrites it on a rebuild. */
69
+ get size() {
70
+ return this.targetSize;
71
+ }
72
+ /**
73
+ * Whether a set is allocated right now.
74
+ *
75
+ * A caller frees the set once when the debug GUI switches occlusion off, rather than on every frame
76
+ * after. This getter tells the caller which frame that is. A flag kept beside the set drifts the
77
+ * first time a sync fails or a rebuild releases the set on its own.
78
+ */
79
+ get held() {
80
+ return this.depthTarget !== null;
81
+ }
82
+ /**
83
+ * Frees all three targets and the depth texture, and forgets their size.
84
+ *
85
+ * This takes no renderer. A `RenderTarget` disposal is an event three's own renderer listens for,
86
+ * so an unmount effect with nothing else in hand can call it.
87
+ */
88
+ dispose() {
89
+ this.release();
90
+ }
91
+ /**
92
+ * Brings the set to the canvas's current size, and rebuilds it when that size moves.
93
+ * @param gl - The renderer, for the canvas size and for the completeness check.
94
+ * @returns Whether a complete set now exists to draw into.
95
+ */
96
+ sync(gl) {
97
+ if (this.unavailable)
98
+ return false;
99
+ const canvas = gl.getSize(_canvas);
100
+ const width = Math.floor(canvas.x * CANVAS_FRACTION);
101
+ const height = Math.floor(canvas.y * CANVAS_FRACTION);
102
+ // A canvas with no area yet, during a layout or before one. The next resize can undo that, so
103
+ // this is no reason to give up for good.
104
+ if (width < 1 || height < 1)
105
+ return false;
106
+ if (this.depthTarget && this.targetSize.x === width && this.targetSize.y === height) {
107
+ return true;
108
+ }
109
+ return this.build(gl, width, height);
110
+ }
111
+ /**
112
+ * Allocates the set at a size, at one of the two depth widths.
113
+ * @param width - The set's width in pixels.
114
+ * @param height - Its height.
115
+ * @param depthType - The depth texture's type, which decides its bit width.
116
+ */
117
+ allocate(width, height, depthType) {
118
+ this.depthTarget = new WebGLRenderTarget(width, height, {
119
+ ...SINGLE_CHANNEL_TEXTURE,
120
+ depthBuffer: true,
121
+ depthTexture: new DepthTexture(width, height, depthType),
122
+ });
123
+ this.aoTarget = new WebGLRenderTarget(width, height, {
124
+ ...SINGLE_CHANNEL_TEXTURE,
125
+ depthBuffer: false,
126
+ });
127
+ // No format of its own. The next line drops the texture this target is built with, so the
128
+ // missing depth attachment is the only option that outlives the constructor.
129
+ this.blurTarget = new WebGLRenderTarget(width, height, { depthBuffer: false });
130
+ // The sharing, in one line. `RenderTarget` has a `texture` setter, so this is supported API
131
+ // rather than a reach into internals. This drops the target's own fresh texture, which never
132
+ // uploaded. Nothing writes the prepass's color attachment and nothing reads it, so the blur pass
133
+ // can take it.
134
+ this.blurTarget.texture = this.depthTarget.texture;
135
+ this.targetSize.set(width, height);
136
+ }
137
+ /**
138
+ * Rebuilds the set, and falls back to 16-bit depth once before it gives up for good.
139
+ * @param gl - The renderer, for the completeness check.
140
+ * @param width - The set's width in pixels.
141
+ * @param height - Its height.
142
+ * @returns Whether a complete set now exists.
143
+ */
144
+ build(gl, width, height) {
145
+ this.release();
146
+ for (const depthType of DEPTH_TYPES) {
147
+ this.allocate(width, height, depthType);
148
+ if (this.verify(gl))
149
+ return true;
150
+ this.release();
151
+ }
152
+ // Off for good, and silent as far as a caller can tell. The SDK runs in old Android WebViews,
153
+ // and nobody can do anything about a driver that will not report these targets complete. The
154
+ // debug log is for whoever has to find out why a plan shipped without occlusion.
155
+ logger.debug('ambient occlusion off: this context gives no complete render targets');
156
+ this.unavailable = true;
157
+ return false;
158
+ }
159
+ /**
160
+ * Frees whatever is allocated, together and in one place, and forgets its size.
161
+ */
162
+ release() {
163
+ // The order does not matter. The first disposal of the shared texture deletes it and removes its
164
+ // properties, and the second one finds nothing left to delete. All three still have to happen.
165
+ // The AO target's own texture belongs to nobody else, so either order deletes it once.
166
+ this.depthTarget?.dispose();
167
+ this.aoTarget?.dispose();
168
+ this.blurTarget?.dispose();
169
+ this.depthTarget?.depthTexture?.dispose();
170
+ this.depthTarget = null;
171
+ this.aoTarget = null;
172
+ this.blurTarget = null;
173
+ this.targetSize.set(0, 0);
174
+ }
175
+ /**
176
+ * Asks the driver whether the two framebuffer combinations this set uses came back complete, once
177
+ * per allocation.
178
+ *
179
+ * Binding each target through three is what builds its framebuffer, so the check runs against the
180
+ * real thing rather than against an intention. This puts the previously bound target back, which
181
+ * keeps a shared context unaware that the check ran. The MapLibre embed is that shared context.
182
+ *
183
+ * **Two checks cover three targets, because two of the targets are one combination.** The depth
184
+ * target is a color attachment beside a sampleable depth texture, at whichever of the two widths
185
+ * `build` is trying, and it is the only one of the three carrying depth at all. That is its own
186
+ * risk and it takes its own check. The blur target is a single `R8` color attachment over a
187
+ * texture the depth target already owns. The AO target is that same single `R8` attachment over a
188
+ * texture of its own, so the blur target covers the AO target's combination and adds the sharing,
189
+ * which is the part of this design a driver is most likely to refuse. A third bind would ask a
190
+ * question the second one already answers.
191
+ *
192
+ * **three's own renderer never calls `checkFramebufferStatus`**, so this is the only thing that
193
+ * detects a driver refusing either combination. What it buys is the 16-bit retry and, after that,
194
+ * occlusion switching itself off, rather than a plan drawing out of an incomplete framebuffer.
195
+ *
196
+ * A context that throws while three binds it answers the same question the status code answers, so
197
+ * this catches the throw. Occlusion switching itself off is the documented outcome. Taking the
198
+ * frame loop down with it is the worse one.
199
+ * @param gl - The renderer whose context answers.
200
+ * @returns Whether both combinations are complete.
201
+ */
202
+ verify(gl) {
203
+ const context = webgl2(gl);
204
+ if (!context)
205
+ return false;
206
+ const previous = gl.getRenderTarget();
207
+ try {
208
+ let complete = true;
209
+ for (const target of [this.depthTarget, this.blurTarget]) {
210
+ gl.setRenderTarget(target);
211
+ if (context.checkFramebufferStatus(context.FRAMEBUFFER) !== context.FRAMEBUFFER_COMPLETE) {
212
+ complete = false;
213
+ }
214
+ }
215
+ return complete;
216
+ }
217
+ catch {
218
+ return false;
219
+ }
220
+ finally {
221
+ gl.setRenderTarget(previous);
222
+ }
223
+ }
224
+ }
225
+ /**
226
+ * The renderer's context, when it is one these targets can be built on.
227
+ *
228
+ * A runtime feature detection, and not a type narrowing a reader can drop. The SDK still runs in
229
+ * WebViews whose context can be neither WebGL2 nor able to carry a readable depth texture. The
230
+ * constructor itself can be missing, so this checks for it before it uses it.
231
+ * @param gl - The renderer to read.
232
+ * @returns The context, or `undefined` when the targets cannot be built on it.
233
+ */
234
+ function webgl2(gl) {
235
+ const context = gl.getContext();
236
+ if (typeof WebGL2RenderingContext !== 'function')
237
+ return undefined;
238
+ return context instanceof WebGL2RenderingContext ? context : undefined;
239
+ }
240
+ /**
241
+ * The share of the canvas size each axis of the set takes.
242
+ *
243
+ * Half, so each pass bills a quarter of the texels a full-size pass bills. Occlusion is a
244
+ * low-frequency signal. The lit read finds its own fragment by normalized coordinates, so the texel
245
+ * count behind that read changes nothing anyone can see.
246
+ *
247
+ * The canvas size and not the drawing buffer's, which is what `gl.getSize()` reports. A device pixel
248
+ * ratio of `2` squares into four times the texels, and both passes bill per texel. A set sized from
249
+ * the buffer therefore costs four times as much on a retina screen, for detail nothing asks of it.
250
+ */
251
+ export const CANVAS_FRACTION = 0.5;
252
+ /**
253
+ * The depth widths this tries, in order.
254
+ *
255
+ * 24 bits first, because 16-bit depth at this size over a plan's range bands visibly in the
256
+ * occlusion. 16 bits is the one retry worth making before occlusion latches off for good. It saves
257
+ * memory rather than costing it, which makes it a fallback rather than a default.
258
+ */
259
+ const DEPTH_TYPES = [UnsignedIntType, UnsignedShortType];
260
+ /**
261
+ * The shared settings for the two color textures the occlusion passes draw into.
262
+ *
263
+ * `R8` holds one byte a texel. It is a color-renderable sized internal format in WebGL 2, so it is
264
+ * required to work rather than merely likely to.
265
+ *
266
+ * Both the blur pass and the lit read weight their samples by depth, so hardware filtering only
267
+ * smears occlusion across the edges those reads respect.
268
+ */
269
+ const SINGLE_CHANNEL_TEXTURE = {
270
+ format: RedFormat,
271
+ type: UnsignedByteType,
272
+ minFilter: NearestFilter,
273
+ magFilter: NearestFilter,
274
+ };
275
+ /** Scratch for the canvas size, read once per sync. */
276
+ const _canvas = new Vector2();
@@ -17,6 +17,8 @@ import { LineInstance } from './LineInstance';
17
17
  */
18
18
  export function Lines(props) {
19
19
  const { defs } = props;
20
+ // Only the instances are staged; the container is sized and ordered from the whole membership.
21
+ const mounted = props.mounted ?? defs.length;
20
22
  // Sized from the first render's defs; membership changes grow the batch in place, never rebuild
21
23
  // it. The container is bookkeeping only, so the copy StrictMode's double-invoked initializer
22
24
  // discards is plain garbage.
@@ -25,5 +27,5 @@ export function Lines(props) {
25
27
  // claimed slot — it is a frozen snapshot, so that never happens. The `ref` arrow is
26
28
  // deliberate: `ref={container.bind}` would make the React Compiler infer `container` itself as a
27
29
  // ref and reject its render-time reads.
28
- return (_jsxs(_Fragment, { children: [_jsx(BatchedLines, { args: container.args, ref: (mesh) => container.bind(mesh), customSort: container.customSort, children: _jsx(LineMaterial, {}) }), defs.map((def) => (_jsx(LineInstance, { def: def, container: container }, defKey(def))))] }));
30
+ return (_jsxs(_Fragment, { children: [_jsx(BatchedLines, { args: container.args, ref: (mesh) => container.bind(mesh), customSort: container.customSort, children: _jsx(LineMaterial, {}) }), defs.slice(0, mounted).map((def) => (_jsx(LineInstance, { def: def, container: container }, defKey(def))))] }));
29
31
  }
@@ -4,80 +4,87 @@ export interface ColorMaterialProps {
4
4
  /** Base color. Defaults to white, which is what a per-instance tint multiplies against. */
5
5
  color?: ColorRepresentation;
6
6
  /**
7
- * Whether this batch draws **volumes** — meshes carrying real z, which need shading to read as
8
- * solid and depth testing to occlude themselves and each other. Defaults to `false`, the flat
9
- * content that is every 2D primitive in a plan.
7
+ * Whether this batch draws volumes, which are the meshes carrying real z. A volume needs shading
8
+ * to read as solid, and depth testing to occlude itself and its neighbors. Defaults to `false`,
9
+ * which is the flat content every 2D primitive in a plan draws as.
10
10
  */
11
11
  solid?: boolean;
12
12
  /**
13
- * Whether those volumes are translucent, which moves them into three's transparent pass. Only
14
- * meaningful together with {@link solid}; flat content never uses it (see the note below on why 2D
15
- * stays in the opaque list).
13
+ * Whether those volumes are translucent, which moves them into three's transparent pass.
14
+ *
15
+ * This means something only together with {@link solid}. Flat content never uses it, for the reason
16
+ * the note below gives.
16
17
  */
17
18
  translucent?: boolean;
18
19
  /**
19
- * Runs on the shader before the dim patch does — the slot for a caller's own shader surgery,
20
- * like the background quad's clip-space projection and its dim-uniform stand-in. Captured at
21
- * first render and fixed for the material's lifetime, like every other axis of this component:
22
- * a changing handler identity would risk a recompile per render.
20
+ * Runs on the shader before the dim patch runs. This is the slot for a caller's own shader surgery,
21
+ * such as the background quad's clip-space projection and its dim-uniform stand-in.
22
+ *
23
+ * This component captures the handler at the first render and keeps it for the material's lifetime,
24
+ * the way it keeps every other axis. A handler identity that changes risks a recompile per render.
23
25
  */
24
26
  onBeforeCompile?: (parameters: WebGLProgramParametersWithUniforms) => void;
25
27
  }
26
28
  /**
27
- * The material a shape batch draws with, rendered as a child of the mesh so R3F attaches, updates,
28
- * and disposes it. Also troika's base material for text (see `Texts`), which never asks for `solid`.
29
+ * The material a shape batch draws with. It renders as a child of the mesh, so R3F attaches, updates
30
+ * and disposes it. It is also troika's base material for text, which never asks for `solid`. See
31
+ * `Texts`.
29
32
  *
30
- * One component with a branch rather than two files, because the two are the same thing along one
31
- * axis — what a batch of shapes draws with — and the three differences are the whole point, best
32
- * read side by side:
33
+ * One component with a branch rather than two files. The two are the same thing along one axis, which
34
+ * is what a batch of shapes draws with. The three differences read best side by side:
33
35
  *
34
36
  * | | flat (default) | `solid` |
35
37
  * | --- | --- | --- |
36
- * | base | `MeshBasicMaterial`, unlit | `MeshPhysicalMaterial`, lit |
37
- * | `depthWrite` | off, from {@link sharedParams} | **on** |
38
- * | polygon offset | on, from {@link sharedParams} | **off** |
38
+ * | base | `MeshBasicMaterial`, unlit | `MeshLambertMaterial`, lit |
39
+ * | `depthWrite` | off, from {@link sharedParams} | on |
40
+ * | polygon offset | on, from {@link sharedParams} | off |
39
41
  *
40
- * Each difference is load-bearing. A volume is the one kind of content that writes depth: that is how
41
- * its own faces resolve against each other, and what lets 2D content drawn afterwards be occluded by
42
- * it. It carries no polygon offset because solids are what everything else is biased *against* —
43
- * offsetting them too would only move the problem.
42
+ * Each difference carries its own weight. A volume is the one kind of content that writes depth. That
43
+ * is how its own faces resolve against each other, and it is what lets 2D content drawn afterwards be
44
+ * occluded by it. A volume carries no polygon offset, because solids are what everything else is
45
+ * biased against. An offset on the solids too only moves the problem.
44
46
  *
45
- * `ior: 1` and `specularIntensity: 0` drive the lit material's `F0` to zero, so no specular lobe sits
46
- * on top of the diffuse term. That is what makes the lighting identity exact: with the two lights
47
- * summing to intensity `π`, a face square-on to the camera renders at precisely the authored color
48
- * (see `Lighting`). A default `MeshPhysicalMaterial` would put a sheen on every wall and push it
49
- * above its albedo.
47
+ * The lit branch is `MeshLambertMaterial` alone. It has no specular lobe at all, so this never sets
48
+ * `ior` or `specularIntensity`. three evaluates it per fragment with `BRDF_Lambert`, which is
49
+ * `albedo / π`. Its vertex shader carries `batching_vertex` and `color_vertex`, so
50
+ * `BatchedMesh.setColorAt` keeps working. Its fragment shader carries the output chain, so the dim
51
+ * patch keeps its anchor.
50
52
  *
51
- * Per-instance color arrives through `BatchedMesh.setColorAt` either way: three injects the
53
+ * The light rig alone holds a roof to its authored color now. `Lighting` derives both intensities from
54
+ * the one `contrast` control every frame. An up-facing surface then lands on the color it was authored
55
+ * with, and no surface the design accounts for clips.
56
+ *
57
+ * Per-instance color arrives through `BatchedMesh.setColorAt` on either branch. three injects the
52
58
  * batching-color chunk for whatever material the batch draws with.
53
59
  *
54
- * **`translucent` is the one place the array-order contract bends, and it is deliberate.** It sets
55
- * `transparent: true`, which moves the batch into three's transparent list — drawn after *every*
56
- * opaque object in the scene, whatever `renderOrder` says (`WebGLRenderer.renderScene`). That is
57
- * exactly what makes a translucent volume blend with the opaque content behind it even when that
58
- * content lives in a later layer, which no position in the ordered stream can achieve: a translucent
59
- * surface can only blend with what is already in the framebuffer.
60
+ * `translucent` is the one place the array-order contract bends, and it bends on purpose. It sets
61
+ * `transparent: true`, which moves the batch into three's transparent list. three draws that list
62
+ * after every opaque object in the scene, whatever `renderOrder` says. See
63
+ * `WebGLRenderer.renderScene`. That is what lets a translucent volume blend with the opaque content
64
+ * behind it, even when that content lives in a later layer. No position in the ordered stream can do
65
+ * the same, because a translucent surface can only blend with what already sits in the framebuffer.
60
66
  *
61
- * What it costs is the reverse case. A planar def sitting *on* a translucent volume — a label or logo
62
- * on a glass roof — draws in the opaque pass and so lands **before** it: the label composites against
63
- * whatever was under the glass rather than onto it, and the volume then glazes over the label. So a
64
- * translucent solid is right about what is behind it and wrong about what is on it. Flat content is
65
- * never given this flag for the same reason, plus one of its own: 2D is coplanar, so it has no depth
66
- * order to sort by and order is all it has.
67
+ * The reverse case is what it costs. Take a planar def sitting on a translucent volume, such as a
68
+ * label or a logo on a glass roof. It draws in the opaque pass, so it lands before the volume. The
69
+ * label composites against whatever was under the glass rather than onto it, and the volume then
70
+ * glazes over the label. So a translucent solid is right about what is behind it and wrong about what is on it. Flat
71
+ * content never takes this flag for the same reason, and for one of its own: 2D content is coplanar,
72
+ * so it has no depth order to sort by and order is all it has.
67
73
  *
68
- * `forceSinglePass` is switched off here alone. three renders a `DoubleSide` transparent material
69
- * twice — back faces, then front (`WebGLRenderer.js:1348`) — which is a correctly ordered two-pass
70
- * for free, and the reason a closed translucent volume does not blend its own far and near surfaces
71
- * in whatever order the batch happened to pack them.
74
+ * `forceSinglePass` is switched off here alone. three draws a `DoubleSide` transparent material
75
+ * twice, back faces and then front. See `WebGLRenderer.js:1348`. That is a correctly ordered two-pass
76
+ * for free. It is why a closed translucent volume does not blend its own far and near surfaces in
77
+ * whatever order the batch packed them.
72
78
  *
73
- * Depth writes stay **on** even when translucent, which is not three's usual convention. Two rules
74
- * depend on it: a translucent volume must still be occluded by anything nearer, and a farther one
75
- * drawn later must not paint over it. MapLibre makes the same choice for `fill-extrusion`
76
- * (`DepthMode.ReadWrite` in both of its passes). The trade and the two rejected alternatives are
77
- * recorded in the design spec's Decision 2.
79
+ * Depth writes stay on even when translucent, which is not three's usual convention. Two rules depend
80
+ * on it. A translucent volume must still be occluded by anything nearer, and a farther volume drawn
81
+ * later must not paint over it. MapLibre makes the same choice for `fill-extrusion`, with
82
+ * `DepthMode.ReadWrite` in both of its passes. The design spec's Decision 2 records the trade and the
83
+ * two rejected alternatives.
78
84
  *
79
- * The branch is stable for a mounted batch — `Shapes` renders a separate element per kind, each with
80
- * a constant `solid` — so R3F never swaps one base class for the other under a live mesh.
85
+ * The branch is stable for a mounted batch, because `Shapes` renders a separate element per kind and
86
+ * each one carries a constant `solid`. So R3F never swaps one base class for the other under a live
87
+ * mesh.
81
88
  * @param props - The material's base color, and whether it draws volumes.
82
89
  * @returns The material element.
83
90
  */