@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,129 @@
1
+ import { type DepthTexture, type IUniform, type Object3D, type Texture, Vector2 } from 'three';
2
+ export * from './ambient-occlusion-pass';
3
+ export * from './ambient-occlusion-patch';
4
+ export * from './blur-pass';
5
+ /**
6
+ * The scene-wide state this term owns: the uniforms every lit material reads, the values the debug
7
+ * GUI tunes, and the registry of what asks for the term.
8
+ *
9
+ * The term declares its own state, so it sits here rather than in `effects-context.ts`, and
10
+ * `OCCLUSION_DEFAULTS` sits here for the same reason.
11
+ *
12
+ * Every field keeps its `ao` prefix because the bundle is flat, and the contact shadow brings a
13
+ * strength and a color of its own. The last four fields carry no prefix, because no shader reads
14
+ * them.
15
+ *
16
+ * A uniform here is a uniform some shader captures by identity, so a GUI write lands straight in a
17
+ * shader. A value a pass reads by copy is a plain number instead.
18
+ */
19
+ export interface AmbientOcclusion {
20
+ /**
21
+ * The smoothed occlusion the blur pass wrote, or `null` before the first frame. `0` is open, `1`
22
+ * is closed. The blur pass writes this one, and the AO pass writes the raw term behind it.
23
+ */
24
+ aoMap: IUniform<Texture | null>;
25
+ /**
26
+ * The prepass's depth, or `null` before the first frame.
27
+ *
28
+ * A depth texture rather than any texture, so nothing assigns this and the map to each other by
29
+ * mistake. three binds it like any other texture.
30
+ *
31
+ * The lit read weights the term with it, which keeps a translucent surface from taking the
32
+ * occlusion of whatever stands behind it. The main pass binds no framebuffer that carries this
33
+ * texture, so the sample there is legal.
34
+ */
35
+ aoDepth: IUniform<DepthTexture | null>;
36
+ /**
37
+ * `1` while the passes drew this frame, `0` when they did not. Every gate clears it before it
38
+ * returns, so a frame that skipped the passes shades correctly rather than sampling a stale
39
+ * texture.
40
+ */
41
+ aoActive: IUniform<number>;
42
+ /**
43
+ * The guard band, `g`: how much wider than the main frustum the two passes run.
44
+ *
45
+ * The main pass finds its own fragment in the AO texture with this one scalar rather than a
46
+ * matrix: `((gl_FragCoord.xy / viewport) * 2 - 1) / (1 + g) * 0.5 + 0.5`.
47
+ */
48
+ aoGuard: IUniform<number>;
49
+ /** How far the read blends an occluded fragment toward the shadow color, from `0` to `1`. */
50
+ aoStrength: IUniform<number>;
51
+ /**
52
+ * The blur kernel's side length in texels, from `1` to {@link NOISE_TILE}.
53
+ *
54
+ * A measurement dial rather than a free quality setting. The blur pass costs `2 · side²` texture
55
+ * fetches a texel, and it bills per texel of a quarter-area target rather than per lit fragment.
56
+ * Side `1` is the floor, where the pass copies the raw term through.
57
+ *
58
+ * A `4 × 4` kernel averages the `4 × 4` rotation tile, which turns rotated sampling into a smooth
59
+ * result. A smaller side leaves structured noise behind.
60
+ */
61
+ aoBlur: IUniform<number>;
62
+ /** The drawing buffer's size in pixels, the unit `gl_FragCoord` counts in. */
63
+ aoViewport: IUniform<Vector2>;
64
+ /** Whether the term runs at all. The debug GUI's switch, not a capability check. */
65
+ enabled: boolean;
66
+ /** How far the term looks for an occluder, in plan units. */
67
+ radius: number;
68
+ /**
69
+ * How many of the kernel's {@link AO_TAPS} offsets the AO pass walks. Fewer taps measure less of
70
+ * the hemisphere, so the pass misses a narrow occluder more often.
71
+ */
72
+ taps: number;
73
+ /**
74
+ * Every mesh that asks for occlusion, which is every mesh the depth prepass draws.
75
+ *
76
+ * A set rather than a count, so React's StrictMode double mount cannot make it drift.
77
+ * `useOccluder` fills it. An empty set means nothing on this plan asked, so `OcclusionPasses`
78
+ * holds no render targets and draws no pass.
79
+ */
80
+ occluders: Set<Object3D>;
81
+ }
82
+ /**
83
+ * Builds this term's scene-wide state, at the agreed defaults.
84
+ *
85
+ * `createAoPassUniforms` builds what the AO pass alone reads. This builds what the whole scene
86
+ * shares, and what `createSceneEffects` spreads into the bundle it publishes.
87
+ * @returns A fresh object, whose identity outlives every write into it.
88
+ */
89
+ export declare function createAmbientOcclusion(): AmbientOcclusion;
90
+ /**
91
+ * The occlusion values this design agreed, and what each mount starts from.
92
+ *
93
+ * `radius` and `strength` are starting points rather than settled numbers, and the GUI tunes both
94
+ * per plan. `radius` is an absolute figure in plan units, so it suits one plan's booth sizes and
95
+ * not another's.
96
+ *
97
+ * `blur` and `taps` start at each loop's own bound, which is the shipped look.
98
+ */
99
+ export declare const OCCLUSION_DEFAULTS: {
100
+ /** Whether the term runs at all. */
101
+ enabled: boolean;
102
+ /**
103
+ * How far the term looks for an occluder, in plan units.
104
+ *
105
+ * Chosen against the story plans, whose booths run about a hundred plan units across. That makes
106
+ * a band about a tenth of a booth wide, where a junction's shadow reads as shading rather than as
107
+ * an outline.
108
+ */
109
+ radius: number;
110
+ /** How far the read blends an occluded fragment toward the shadow color, from `0` to `1`. */
111
+ strength: number;
112
+ /**
113
+ * The blur kernel's side length in texels. It starts at the side that covers the rotation tile,
114
+ * see {@link AmbientOcclusion.aoBlur}.
115
+ */
116
+ blur: number;
117
+ /** How many of the kernel's offsets the AO pass walks, see {@link AmbientOcclusion.taps}. */
118
+ taps: number;
119
+ /**
120
+ * How much wider than the main frustum the two passes run.
121
+ *
122
+ * Without the band, geometry just outside the frustum does not occlude geometry inside it, and a
123
+ * seam of missing darkening runs down the screen edges. `0.15` covers the widest reach a tap
124
+ * takes at half resolution. The cost is a lower effective resolution and more geometry in the
125
+ * frustum.
126
+ */
127
+ guard: number;
128
+ };
129
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { Vector2 } from 'three';
2
+ import { AO_TAPS, NOISE_TILE } from './ambient-occlusion-pass';
3
+ // The folder's public face. One specifier carries every member the outside asks for.
4
+ //
5
+ // The three siblings must never import a value from here. Both pass shaders and the lit read are
6
+ // template literals that interpolate constants at module initialization. A cycle through this file
7
+ // evaluates one of those constants before its own module assigns it. The emit erases the
8
+ // `import type` the siblings use, so that import forms no cycle.
9
+ export * from './ambient-occlusion-pass';
10
+ export * from './ambient-occlusion-patch';
11
+ export * from './blur-pass';
12
+ /**
13
+ * Builds this term's scene-wide state, at the agreed defaults.
14
+ *
15
+ * `createAoPassUniforms` builds what the AO pass alone reads. This builds what the whole scene
16
+ * shares, and what `createSceneEffects` spreads into the bundle it publishes.
17
+ * @returns A fresh object, whose identity outlives every write into it.
18
+ */
19
+ export function createAmbientOcclusion() {
20
+ return {
21
+ aoMap: { value: null },
22
+ aoDepth: { value: null },
23
+ aoActive: { value: 0 },
24
+ aoGuard: { value: OCCLUSION_DEFAULTS.guard },
25
+ aoStrength: { value: OCCLUSION_DEFAULTS.strength },
26
+ aoBlur: { value: OCCLUSION_DEFAULTS.blur },
27
+ aoViewport: { value: new Vector2(1, 1) },
28
+ enabled: OCCLUSION_DEFAULTS.enabled,
29
+ radius: OCCLUSION_DEFAULTS.radius,
30
+ taps: OCCLUSION_DEFAULTS.taps,
31
+ occluders: new Set(),
32
+ };
33
+ }
34
+ /**
35
+ * The occlusion values this design agreed, and what each mount starts from.
36
+ *
37
+ * `radius` and `strength` are starting points rather than settled numbers, and the GUI tunes both
38
+ * per plan. `radius` is an absolute figure in plan units, so it suits one plan's booth sizes and
39
+ * not another's.
40
+ *
41
+ * `blur` and `taps` start at each loop's own bound, which is the shipped look.
42
+ */
43
+ export const OCCLUSION_DEFAULTS = {
44
+ /** Whether the term runs at all. */
45
+ enabled: true,
46
+ /**
47
+ * How far the term looks for an occluder, in plan units.
48
+ *
49
+ * Chosen against the story plans, whose booths run about a hundred plan units across. That makes
50
+ * a band about a tenth of a booth wide, where a junction's shadow reads as shading rather than as
51
+ * an outline.
52
+ */
53
+ radius: 12,
54
+ /** How far the read blends an occluded fragment toward the shadow color, from `0` to `1`. */
55
+ strength: 0.5,
56
+ /**
57
+ * The blur kernel's side length in texels. It starts at the side that covers the rotation tile,
58
+ * see {@link AmbientOcclusion.aoBlur}.
59
+ */
60
+ blur: NOISE_TILE,
61
+ /** How many of the kernel's offsets the AO pass walks, see {@link AmbientOcclusion.taps}. */
62
+ taps: AO_TAPS,
63
+ /**
64
+ * How much wider than the main frustum the two passes run.
65
+ *
66
+ * Without the band, geometry just outside the frustum does not occlude geometry inside it, and a
67
+ * seam of missing darkening runs down the screen edges. `0.15` covers the widest reach a tap
68
+ * takes at half resolution. The cost is a lower effective resolution and more geometry in the
69
+ * frustum.
70
+ */
71
+ guard: 0.15,
72
+ };
@@ -1,6 +1,6 @@
1
1
  import { type IUniform, type WebGLProgramParametersWithUniforms } from 'three';
2
- import { type UniformSchemaShader } from '../../core/InstanceUniforms';
3
- import { type RenderableDef } from '../../types/defs';
2
+ import { type UniformSchemaShader } from '../../../core/InstanceUniforms';
3
+ import { type RenderableDef } from '../../../types/defs';
4
4
  /**
5
5
  * Patches a compiled shader to dim what it draws. Returned by {@link useDimPatch} and called from a
6
6
  * material's own `onBeforeCompile`, so a material that already patches its shader composes the two
@@ -36,7 +36,7 @@ export declare function useDimPatch(): DimPatch;
36
36
  * @param parameters - The shader being compiled.
37
37
  * @param uniform - The layer's dim amount.
38
38
  * @returns Whether the shader was patched.
39
- * @throws {Error} If the prologue is present but the shader converts no color, which would leave
39
+ * @throws {Error} If the prologue is present but the shader has no output chain, which would leave
40
40
  * dimming silently absent from a material that asked for it.
41
41
  */
42
42
  export declare function applyDim(parameters: WebGLProgramParametersWithUniforms, uniform: IUniform<number>): boolean;
@@ -120,4 +120,4 @@ export declare const DIM_INHERIT = -1;
120
120
  * and models already declare vertex uniforms, and shapes and text declare none besides this.
121
121
  */
122
122
  export declare const DIM_SCHEMA: UniformSchemaShader;
123
- //# sourceMappingURL=dim.d.ts.map
123
+ //# sourceMappingURL=color-dim.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { useThree } from '@react-three/fiber';
2
2
  import { createContext, use, useEffect, useRef, useState } from 'react';
3
- import { declareBeforeMain } from '../../core/shader-source';
3
+ import { declareBeforeMain, sceneColorAnchor } from '../../../core/shader-source';
4
4
  /**
5
5
  * The dim patch every material installs, reading the layer amount from {@link LayerDimContext}.
6
6
  *
@@ -46,7 +46,7 @@ export function useDimPatch() {
46
46
  * @param parameters - The shader being compiled.
47
47
  * @param uniform - The layer's dim amount.
48
48
  * @returns Whether the shader was patched.
49
- * @throws {Error} If the prologue is present but the shader converts no color, which would leave
49
+ * @throws {Error} If the prologue is present but the shader has no output chain, which would leave
50
50
  * dimming silently absent from a material that asked for it.
51
51
  */
52
52
  export function applyDim(parameters, uniform) {
@@ -58,9 +58,9 @@ export function applyDim(parameters, uniform) {
58
58
  // Already installed, which is how a second call after the prologue arrives stays harmless.
59
59
  if (fragment.includes(DIM_FUNCTION))
60
60
  return false;
61
- const anchor = colorspaceAnchor(fragment);
61
+ const anchor = sceneColorAnchor(fragment);
62
62
  if (!anchor)
63
- throw new Error('applyDim: the shader has no colorspace conversion to dim before.');
63
+ throw new Error('applyDim: the shader has no output chain to dim before.');
64
64
  parameters.uniforms[LAYER_DIM] = uniform;
65
65
  // Ahead of `main`, because these are file-scope declarations — and `dimColor` takes the amount as a
66
66
  // parameter precisely so it needs nothing `main` declares.
@@ -125,24 +125,6 @@ export function instanceDim(dim) {
125
125
  export function clampDim(dim) {
126
126
  return Math.min(Math.max(dim, 0), 1);
127
127
  }
128
- /**
129
- * Where the fragment's color is final and still linear — the point dimming has to land in front of,
130
- * spelled whichever way the shader currently spells it.
131
- *
132
- * Two spellings because three resolves `#include` late, after every `onBeforeCompile`, while troika
133
- * resolves them itself while rebuilding `main`. A patch running after troika's rebuild therefore
134
- * finds the conversion already expanded to its chunk body. Both name the same line; neither is a
135
- * private marker of troika's.
136
- * @param fragmentShader - The shader to search.
137
- * @returns The anchor found, or `undefined` if the shader converts no color.
138
- */
139
- function colorspaceAnchor(fragmentShader) {
140
- const include = /*glsl*/ `#include <colorspace_fragment>`;
141
- const expanded = /*glsl*/ `gl_FragColor = linearToOutputTexel( gl_FragColor );`;
142
- if (fragmentShader.includes(include))
143
- return include;
144
- return fragmentShader.includes(expanded) ? expanded : undefined;
145
- }
146
128
  /**
147
129
  * The dim amount a layer draws at, which every material beneath it reads.
148
130
  *
@@ -205,8 +187,8 @@ const DIM_DEFS = /* glsl */ `
205
187
  }
206
188
  `;
207
189
  /**
208
- * Dimming applied to the fragment, while the color is still linear — which is what
209
- * `darkenFactor`'s gamma correction assumes.
190
+ * Dimming applied while the color is still linear and scene-referred, which is what `darkenFactor`'s
191
+ * gamma correction assumes. See {@link sceneColorAnchor}.
210
192
  */
211
193
  const DIM_APPLY = /* glsl */ `
212
194
  gl_FragColor = dimColor(gl_FragColor, ${DIM_UNIFORM} < 0. ? ${LAYER_DIM} : ${DIM_UNIFORM});
@@ -0,0 +1,100 @@
1
+ import { Color, type IUniform, Matrix4, type WebGLProgramParametersWithUniforms } from 'three';
2
+ /**
3
+ * The scene-wide state this term owns: the uniforms every lit material reads, and the hex string a
4
+ * lil-gui color controller writes.
5
+ *
6
+ * Declared here rather than in `effects-context.ts`, because these are the term's own — the bundle
7
+ * that publishes them only assembles what each term declares. {@link CONTACT_SHADOW_DEFAULTS} sits
8
+ * here for the same reason.
9
+ *
10
+ * **Every field carries a `shadow` prefix**, because the bundle is flat and ambient occlusion brings
11
+ * a `strength` and a `color` of its own; the prefix is what keeps the two apart.
12
+ * {@link ContactShadow.planFromWorld} is the exception, because it names the plan rather than the
13
+ * shadow: it is the plan's own matrix, and the shadow is only its one reader today.
14
+ */
15
+ export interface ContactShadow {
16
+ /** The shadow's reach above a volume's base, in plan units. */
17
+ shadowDistance: IUniform<number>;
18
+ /** The shadow's depth, from `0` to `1`. */
19
+ shadowStrength: IUniform<number>;
20
+ /**
21
+ * The shade both darkening terms blend toward.
22
+ *
23
+ * Shared rather than one per term, so a plan has one shade of shadow rather than two — ambient
24
+ * occlusion binds this very object under a name of its own.
25
+ */
26
+ shadowColor: IUniform<Color>;
27
+ /**
28
+ * The shade as a hex string, which is what a lil-gui color controller reads and writes.
29
+ *
30
+ * Beside the uniform rather than inside it because three holds a `Color` in the working space
31
+ * while lil-gui writes display sRGB, and `Color.set` on a string is the conversion.
32
+ */
33
+ shadowHex: string;
34
+ /**
35
+ * World space back to the plan's own, which is the space's own `Space.matrixInverse`.
36
+ *
37
+ * `SceneGraph` points this uniform at that matrix once. The space recomputes the matrix in place
38
+ * on every refit and never replaces it, so the shader reads the current fit with no copy. Never
39
+ * write through this value, because the write lands in the space itself.
40
+ *
41
+ * The height a volume is shaded by is a height in the plan, and nothing else in the vertex shader
42
+ * knows the plan. A mesh's own matrices place it in the world, and the world is the fit, whose z
43
+ * the fit mirrors. This uniform takes a placed vertex back.
44
+ */
45
+ planFromWorld: IUniform<Matrix4>;
46
+ }
47
+ /**
48
+ * Installs the contact shadow into a lit shader about to be compiled.
49
+ *
50
+ * A fake, not a measurement. It reads the vertex's height above the plan and darkens toward the base
51
+ * of every volume, which separates a wall side from a wall top and stands in for the shadow a wall
52
+ * casts onto the floor it meets. Ambient occlusion is a separate term, applied at the same anchor by
53
+ * `ao/ambient-occlusion-patch.ts`.
54
+ *
55
+ * The reach is a fixed world distance rather than each volume's own height. It needs no new data and
56
+ * stays consistent across a plan. A roof above the reach keeps its authored color exactly; a volume
57
+ * shorter than the reach blends its roof as it blends the rest of its height.
58
+ *
59
+ * The blend goes toward an authored color rather than multiplying down toward black, so the shade is
60
+ * a choice and not only its depth. MapLibre multiplies toward black and clamps.
61
+ *
62
+ * The patch lands above three's output chain, where the dim patch lands, so the color it changes is
63
+ * still linear and scene-referred.
64
+ *
65
+ * **This reaches solid shape batches and models.** Floors and zones are planar defs drawn with
66
+ * `MeshBasicMaterial`, which no light and no darkening term reaches — and whose unit plane sits at
67
+ * `z = 0`, where it would take the whole shadow.
68
+ * @param parameters - The shader being compiled.
69
+ * @param shadow - The mount's own shadow state, whose uniform objects the compiled program captures.
70
+ * @throws {Error} If the shader has no output chain, or no placement to read a vertex's height
71
+ * after, either of which would leave the shadow silently absent from a material that asked for it.
72
+ */
73
+ export declare function applyContactShadow(parameters: WebGLProgramParametersWithUniforms, shadow: ContactShadow): void;
74
+ /**
75
+ * Builds this term's scene-wide state, at the agreed defaults.
76
+ *
77
+ * One object per mounted scene, never module state: a texture is not the only thing two `Renderer`
78
+ * mounts on one page would fight over — a program built in one context captures these very objects,
79
+ * so a singleton would let one scene's slider move the other's materials. `createSceneEffects`
80
+ * spreads this into the bundle it publishes.
81
+ * @returns A fresh object, whose identity is meant to outlive every write into it.
82
+ */
83
+ export declare function createContactShadow(): ContactShadow;
84
+ /**
85
+ * The contact shadow values this design agreed.
86
+ *
87
+ * Each one is a starting point rather than a settled number. The distance is an absolute figure in
88
+ * plan units, so it suits one plan's booth heights and not another's: a plan whose walls are a few
89
+ * units tall wants a few units of reach, and one whose walls are hundreds wants hundreds. It is
90
+ * tuned per plan through the GUI.
91
+ */
92
+ export declare const CONTACT_SHADOW_DEFAULTS: {
93
+ /** How far above a volume's base the shadow reaches, in plan units. */
94
+ distance: number;
95
+ /** How far the base is blended toward the shadow color, from `0` to `1`. */
96
+ strength: number;
97
+ /** The shade the shadow blends toward. */
98
+ color: string;
99
+ };
100
+ //# sourceMappingURL=contact-shadow.d.ts.map
@@ -0,0 +1,167 @@
1
+ import { Color, Matrix4 } from 'three';
2
+ import { declareBeforeMain, replaceAnchor, sceneColorAnchor } from '../../../core/shader-source';
3
+ /**
4
+ * Installs the contact shadow into a lit shader about to be compiled.
5
+ *
6
+ * A fake, not a measurement. It reads the vertex's height above the plan and darkens toward the base
7
+ * of every volume, which separates a wall side from a wall top and stands in for the shadow a wall
8
+ * casts onto the floor it meets. Ambient occlusion is a separate term, applied at the same anchor by
9
+ * `ao/ambient-occlusion-patch.ts`.
10
+ *
11
+ * The reach is a fixed world distance rather than each volume's own height. It needs no new data and
12
+ * stays consistent across a plan. A roof above the reach keeps its authored color exactly; a volume
13
+ * shorter than the reach blends its roof as it blends the rest of its height.
14
+ *
15
+ * The blend goes toward an authored color rather than multiplying down toward black, so the shade is
16
+ * a choice and not only its depth. MapLibre multiplies toward black and clamps.
17
+ *
18
+ * The patch lands above three's output chain, where the dim patch lands, so the color it changes is
19
+ * still linear and scene-referred.
20
+ *
21
+ * **This reaches solid shape batches and models.** Floors and zones are planar defs drawn with
22
+ * `MeshBasicMaterial`, which no light and no darkening term reaches — and whose unit plane sits at
23
+ * `z = 0`, where it would take the whole shadow.
24
+ * @param parameters - The shader being compiled.
25
+ * @param shadow - The mount's own shadow state, whose uniform objects the compiled program captures.
26
+ * @throws {Error} If the shader has no output chain, or no placement to read a vertex's height
27
+ * after, either of which would leave the shadow silently absent from a material that asked for it.
28
+ */
29
+ export function applyContactShadow(parameters, shadow) {
30
+ // Already installed, which is how a repeated compile stays harmless.
31
+ if (parameters.fragmentShader.includes(SHADOW_FUNCTION))
32
+ return;
33
+ const anchor = sceneColorAnchor(parameters.fragmentShader);
34
+ if (!anchor) {
35
+ throw new Error('applyContactShadow: the shader has no output chain to darken before.');
36
+ }
37
+ // Both rewrites before any write, so this either installs the shadow or leaves the parameters
38
+ // exactly as it found them. A shader whose vertex stage places its vertices some other way throws
39
+ // from `replaceAnchor` below, and a caller that means to carry on — a material whose asset is not
40
+ // one three built — must not be left holding four bindings for a program nothing patched.
41
+ const vertexShader = replaceAnchor(declareBeforeMain(parameters.vertexShader, VERTEX_DEFS), PLACEMENT_ANCHOR, `${PLACEMENT_ANCHOR}\n${VERTEX_APPLY}`);
42
+ const fragmentShader = declareBeforeMain(parameters.fragmentShader, FRAGMENT_DEFS).replace(anchor, `${SHADOW_APPLY}\n${anchor}`);
43
+ parameters.uniforms[SHADOW_DISTANCE] = shadow.shadowDistance;
44
+ parameters.uniforms[SHADOW_STRENGTH] = shadow.shadowStrength;
45
+ parameters.uniforms[SHADOW_COLOR] = shadow.shadowColor;
46
+ parameters.uniforms[PLAN_FROM_WORLD] = shadow.planFromWorld;
47
+ parameters.vertexShader = vertexShader;
48
+ parameters.fragmentShader = fragmentShader;
49
+ }
50
+ /**
51
+ * Builds this term's scene-wide state, at the agreed defaults.
52
+ *
53
+ * One object per mounted scene, never module state: a texture is not the only thing two `Renderer`
54
+ * mounts on one page would fight over — a program built in one context captures these very objects,
55
+ * so a singleton would let one scene's slider move the other's materials. `createSceneEffects`
56
+ * spreads this into the bundle it publishes.
57
+ * @returns A fresh object, whose identity is meant to outlive every write into it.
58
+ */
59
+ export function createContactShadow() {
60
+ return {
61
+ shadowDistance: { value: CONTACT_SHADOW_DEFAULTS.distance },
62
+ shadowStrength: { value: CONTACT_SHADOW_DEFAULTS.strength },
63
+ shadowColor: { value: new Color(CONTACT_SHADOW_DEFAULTS.color) },
64
+ shadowHex: CONTACT_SHADOW_DEFAULTS.color,
65
+ planFromWorld: { value: new Matrix4() },
66
+ };
67
+ }
68
+ /**
69
+ * The contact shadow values this design agreed.
70
+ *
71
+ * Each one is a starting point rather than a settled number. The distance is an absolute figure in
72
+ * plan units, so it suits one plan's booth heights and not another's: a plan whose walls are a few
73
+ * units tall wants a few units of reach, and one whose walls are hundreds wants hundreds. It is
74
+ * tuned per plan through the GUI.
75
+ */
76
+ export const CONTACT_SHADOW_DEFAULTS = {
77
+ /** How far above a volume's base the shadow reaches, in plan units. */
78
+ distance: 3,
79
+ /** How far the base is blended toward the shadow color, from `0` to `1`. */
80
+ strength: 0.35,
81
+ /** The shade the shadow blends toward. */
82
+ color: '#4a5260',
83
+ };
84
+ /** The uniform carrying the shadow's reach. */
85
+ const SHADOW_DISTANCE = 'uShadowDistance';
86
+ /** The uniform carrying the shadow's depth. */
87
+ const SHADOW_STRENGTH = 'uShadowStrength';
88
+ /** The uniform carrying the shade the shadow blends toward. */
89
+ const SHADOW_COLOR = 'uShadowColor';
90
+ /** The uniform carrying world space back to the plan's own. */
91
+ const PLAN_FROM_WORLD = 'uPlanFromWorld';
92
+ /** The signature {@link applyContactShadow} looks for, to tell a patched shader from a fresh one. */
93
+ const SHADOW_FUNCTION = 'vec3 contactShadow(';
94
+ /** The varying carrying a vertex's height above the plan. */
95
+ const HEIGHT_VARYING = 'vHeight';
96
+ /** The local three's own placement leaves the vertex's world position in. */
97
+ const WORLD_POSITION = 'worldPosition';
98
+ /**
99
+ * Where the height read lands: three's own placement of the vertex into world space.
100
+ *
101
+ * `<worldpos_vertex>` declares {@link WORLD_POSITION} and applies the batching matrix, the instance
102
+ * matrix and the model matrix to it, so the read below composes nothing of its own. This package
103
+ * widens that chunk's instancing guard to fire under indirect instancing as well. See
104
+ * `instanced-material.ts`.
105
+ *
106
+ * The read cannot ride on `openMain` the way the rest of this package's vertex injections do.
107
+ * `instanceMatrix` is the local that the indirect instancing prologue declares at the top of `main`,
108
+ * and it shadows the placeholder attribute three declares for any `isInstancedMesh` object.
109
+ * `openMain` inserts at the top and the last caller runs first, so a patch installed from a
110
+ * material's own handler would land above the mesh's prologue and read the empty attribute.
111
+ */
112
+ const PLACEMENT_ANCHOR = /* glsl */ `#include <worldpos_vertex>`;
113
+ /**
114
+ * The vertex-stage declarations, and the define that makes three build the world position at all.
115
+ *
116
+ * three guards the whole body of `<worldpos_vertex>` behind five conditions: `USE_ENVMAP`,
117
+ * `DISTANCE`, `USE_SHADOWMAP`, `USE_TRANSMISSION`, and a spot light coord count above zero. This
118
+ * renderer sets none of them, so the chunk expands to nothing and the shader declares no world
119
+ * position to read. `DISTANCE` is the one of the five that no chunk this renderer includes reads, in
120
+ * either stage, so defining it buys the world position and changes nothing else. three's own
121
+ * `distance` material defines it for itself, and nothing here draws with that material.
122
+ *
123
+ * `declareBeforeMain` puts this above `main`, which is above every `#include` inside `main`, so the
124
+ * preprocessor reads the define before it reaches the guard.
125
+ */
126
+ const VERTEX_DEFS = /* glsl */ `
127
+ #define DISTANCE
128
+ uniform mat4 ${PLAN_FROM_WORLD};
129
+ varying float ${HEIGHT_VARYING};
130
+ `;
131
+ /**
132
+ * The vertex-stage read: the vertex's height in the plan's own space.
133
+ *
134
+ * `<worldpos_vertex>` runs one line above and leaves the vertex's world position in
135
+ * {@link WORLD_POSITION}. {@link PLAN_FROM_WORLD} takes that position back to the plan, and the
136
+ * height is its `z`.
137
+ *
138
+ * three's chunk composes the placement, which is what makes the read correct for content whose
139
+ * geometry is not authored in plan coordinates. A model carries GLB-local coordinates and a
140
+ * per-instance placement matrix, so its own `position.z` means nothing there. The solid shape batches
141
+ * do carry absolute plan coordinates, and they drew right only while their instance transform stayed
142
+ * identity. Nothing enforces that.
143
+ */
144
+ const VERTEX_APPLY = /* glsl */ `
145
+ ${HEIGHT_VARYING} = (${PLAN_FROM_WORLD} * ${WORLD_POSITION}).z;
146
+ `;
147
+ /** The fragment-stage declarations. */
148
+ const FRAGMENT_DEFS = /* glsl */ `
149
+ uniform float ${SHADOW_DISTANCE};
150
+ uniform float ${SHADOW_STRENGTH};
151
+ uniform vec3 ${SHADOW_COLOR};
152
+ varying float ${HEIGHT_VARYING};
153
+
154
+ ${SHADOW_FUNCTION}vec3 rgb, float height) {
155
+ float lit = clamp(height / max(${SHADOW_DISTANCE}, 1e-4), 0., 1.);
156
+ return mix(rgb, ${SHADOW_COLOR}, ${SHADOW_STRENGTH} * (1. - lit));
157
+ }
158
+ `;
159
+ /**
160
+ * The shadow applied while the color is still linear and scene-referred.
161
+ *
162
+ * The call is spelled out rather than built from {@link SHADOW_FUNCTION}, which stays the
163
+ * already-patched detector and nothing else.
164
+ */
165
+ const SHADOW_APPLY = /* glsl */ `
166
+ gl_FragColor.rgb = contactShadow(gl_FragColor.rgb, ${HEIGHT_VARYING});
167
+ `;
@@ -0,0 +1,52 @@
1
+ import { type AmbientOcclusion } from './ao';
2
+ import { type ContactShadow } from './contact-shadow';
3
+ /**
4
+ * Everything one mounted scene's shared effects hold: the tuned values, the registry of what asks
5
+ * for ambient occlusion, and the uniforms the lit shaders read.
6
+ *
7
+ * **This is scene-wide shading state, not post processing.** Nothing here runs over the finished
8
+ * image: the ambient occlusion pass is a *pre*pass, and every term is applied inside the lit shader
9
+ * itself, so there is no compositing stage for this to be a part of.
10
+ *
11
+ * **Each term owns its own state, and this only assembles it.** Every field here is declared by the
12
+ * term that owns it, and this alias gathers them. The two terms share one shade of shadow, which the
13
+ * contact shadow declares and ambient occlusion binds under a name of its own.
14
+ *
15
+ * **An alias over an intersection, rather than an interface that extends.** An intersection states
16
+ * what the bundle is, an assembly of what the terms declare, where `extends` reads as a subtype
17
+ * relationship, which is not what is meant. And an interface that extends exactly one parent and
18
+ * declares nothing of its own is what `@typescript-eslint/no-empty-object-type` exists to flag, so
19
+ * that form would need a suppression this one does not.
20
+ *
21
+ * **One object per mount, never module state.** Two `Renderer` mounts on one page have two GL
22
+ * contexts and two sets of render targets, and a module singleton would hand both of their materials
23
+ * whichever texture wrote last. The identity never changes once created, so the passes write into it
24
+ * every frame with no React render and no shader recompile behind them — the discipline
25
+ * `contact-shadow.ts` uses for its three uniforms, published the way `SpaceContext` publishes a
26
+ * `Space` that is refitted in place.
27
+ *
28
+ * **`LayerDimContext` stays where it is and is not merged in.** It carries a single number, it is
29
+ * re-provided by every `Layer`, and it nests. This bundle is one object per renderer mount whose
30
+ * identity must never change, which is what lets a material capture it in `onBeforeCompile` and a
31
+ * frame write `.value` with no recompile. Merging the two would either re-provide this object at
32
+ * every layer boundary or flatten dim's nesting.
33
+ */
34
+ export type SceneEffects = AmbientOcclusion & ContactShadow;
35
+ /**
36
+ * Builds one scene's effect state, from each term's own factory.
37
+ * @returns A fresh object, whose identity is meant to outlive every write into it.
38
+ */
39
+ export declare function createSceneEffects(): SceneEffects;
40
+ /**
41
+ * The effect state of the enclosing scene.
42
+ * @returns The object `Scene` published, or a standalone one when rendered outside a scene.
43
+ */
44
+ export declare function useSceneEffects(): SceneEffects;
45
+ /**
46
+ * The effect state every lit material and both passes read.
47
+ *
48
+ * The default is a standalone object rather than `null`, so a material rendered outside a scene
49
+ * still compiles and simply shades with the terms switched off — the same shape `SpaceContext` takes.
50
+ */
51
+ export declare const SceneEffectsContext: import("react").Context<SceneEffects>;
52
+ //# sourceMappingURL=effects-context.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { createContext, use } from 'react';
2
+ import { createAmbientOcclusion } from './ao';
3
+ import { createContactShadow } from './contact-shadow';
4
+ /**
5
+ * Builds one scene's effect state, from each term's own factory.
6
+ * @returns A fresh object, whose identity is meant to outlive every write into it.
7
+ */
8
+ export function createSceneEffects() {
9
+ return { ...createAmbientOcclusion(), ...createContactShadow() };
10
+ }
11
+ /**
12
+ * The effect state of the enclosing scene.
13
+ * @returns The object `Scene` published, or a standalone one when rendered outside a scene.
14
+ */
15
+ export function useSceneEffects() {
16
+ return use(SceneEffectsContext);
17
+ }
18
+ /**
19
+ * The effect state every lit material and both passes read.
20
+ *
21
+ * The default is a standalone object rather than `null`, so a material rendered outside a scene
22
+ * still compiles and simply shades with the terms switched off — the same shape `SpaceContext` takes.
23
+ */
24
+ export const SceneEffectsContext = createContext(createSceneEffects());