@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.
- package/dist/Renderer.d.ts +14 -0
- package/dist/Renderer.js +16 -4
- package/dist/core/shader-source.d.ts +20 -0
- package/dist/core/shader-source.js +26 -0
- package/dist/debug/components/Gui.d.ts +2 -0
- package/dist/debug/components/Gui.js +11 -1
- package/dist/external/ExternalCanvas.js +5 -6
- package/dist/external/external-camera.js +26 -11
- package/dist/index.d.ts +1 -0
- package/dist/objects/Background.js +1 -1
- package/dist/objects/containers/SlotContainer.js +7 -1
- package/dist/objects/containers/hooks.js +1 -1
- package/dist/objects/containers/props.d.ts +10 -0
- package/dist/objects/images/Images.js +9 -2
- package/dist/objects/images/image-container.js +1 -1
- package/dist/objects/layer/Layer.js +10 -10
- package/dist/objects/layer/mount-scheduler.d.ts +83 -54
- package/dist/objects/layer/mount-scheduler.js +204 -60
- package/dist/objects/lighting/Lighting.d.ts +92 -0
- package/dist/objects/lighting/Lighting.js +385 -0
- package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
- package/dist/objects/lighting/OcclusionPasses.js +271 -0
- package/dist/objects/lighting/index.d.ts +11 -0
- package/dist/objects/lighting/index.js +10 -0
- package/dist/objects/lighting/lighting-gui.d.ts +24 -0
- package/dist/objects/lighting/lighting-gui.js +114 -0
- package/dist/objects/lighting/occluder.d.ts +34 -0
- package/dist/objects/lighting/occluder.js +62 -0
- package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
- package/dist/objects/lighting/occlusion-targets.js +276 -0
- package/dist/objects/lines/Lines.js +3 -1
- package/dist/objects/materials/ColorMaterial.d.ts +58 -51
- package/dist/objects/materials/ColorMaterial.js +74 -52
- package/dist/objects/materials/LineMaterial.js +1 -1
- package/dist/objects/materials/ModelMaterial.d.ts +30 -18
- package/dist/objects/materials/ModelMaterial.js +68 -41
- package/dist/objects/materials/TextureMaterial.js +1 -1
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
- package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
- package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
- package/dist/objects/materials/effects/ao/index.d.ts +129 -0
- package/dist/objects/materials/effects/ao/index.js +72 -0
- package/dist/objects/materials/{dim.d.ts → effects/color-dim.d.ts} +4 -4
- package/dist/objects/materials/{dim.js → effects/color-dim.js} +6 -24
- package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
- package/dist/objects/materials/effects/contact-shadow.js +167 -0
- package/dist/objects/materials/effects/effects-context.d.ts +52 -0
- package/dist/objects/materials/effects/effects-context.js +24 -0
- package/dist/objects/models/Models.js +18 -2
- package/dist/objects/models/model-container.js +1 -1
- package/dist/objects/shapes/Shapes.js +20 -2
- package/dist/objects/text/Texts.js +3 -1
- package/dist/objects/text/text-container.js +1 -1
- package/dist/space/Scene.d.ts +3 -0
- package/dist/space/Scene.js +17 -5
- package/package.json +2 -2
- package/dist/objects/Lighting.d.ts +0 -20
- 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 '
|
|
3
|
-
import { type RenderableDef } from '
|
|
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
|
|
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 '
|
|
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
|
|
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 =
|
|
61
|
+
const anchor = sceneColorAnchor(fragment);
|
|
62
62
|
if (!anchor)
|
|
63
|
-
throw new Error('applyDim: the shader has no
|
|
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
|
|
209
|
-
*
|
|
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());
|