@expofp/renderer 0.0.0-experimental.cf063fc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -0
- package/dist/Renderer.d.ts +102 -0
- package/dist/Renderer.js +96 -0
- package/dist/controls/CameraControls.d.ts +108 -0
- package/dist/controls/CameraControls.js +203 -0
- package/dist/controls/Controls.d.ts +53 -0
- package/dist/controls/Controls.js +54 -0
- package/dist/controls/camera-api.d.ts +226 -0
- package/dist/controls/camera-api.js +179 -0
- package/dist/controls/gestures.d.ts +71 -0
- package/dist/controls/gestures.js +98 -0
- package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
- package/dist/controls/handlers/DoubleTapZoom.js +82 -0
- package/dist/controls/handlers/Inertia.d.ts +60 -0
- package/dist/controls/handlers/Inertia.js +222 -0
- package/dist/controls/handlers/Pan.d.ts +23 -0
- package/dist/controls/handlers/Pan.js +212 -0
- package/dist/controls/handlers/Pitch.d.ts +19 -0
- package/dist/controls/handlers/Pitch.js +200 -0
- package/dist/controls/handlers/Roll.d.ts +30 -0
- package/dist/controls/handlers/Roll.js +196 -0
- package/dist/controls/handlers/Zoom.d.ts +28 -0
- package/dist/controls/handlers/Zoom.js +34 -0
- package/dist/controls/hooks.d.ts +136 -0
- package/dist/controls/hooks.js +200 -0
- package/dist/controls/index.d.ts +9 -0
- package/dist/controls/index.js +7 -0
- package/dist/controls/zoom-to.d.ts +89 -0
- package/dist/controls/zoom-to.js +91 -0
- package/dist/core/InstanceUniforms.d.ts +108 -0
- package/dist/core/InstanceUniforms.js +298 -0
- package/dist/core/SquareDataTexture.d.ts +277 -0
- package/dist/core/SquareDataTexture.js +476 -0
- package/dist/core/batch/BatchedMesh.d.ts +74 -0
- package/dist/core/batch/BatchedMesh.js +155 -0
- package/dist/core/instanced/FrustumCulling.d.ts +141 -0
- package/dist/core/instanced/FrustumCulling.js +246 -0
- package/dist/core/instanced/InstancedMesh.d.ts +302 -0
- package/dist/core/instanced/InstancedMesh.js +497 -0
- package/dist/core/instanced/Instances.d.ts +119 -0
- package/dist/core/instanced/Instances.js +230 -0
- package/dist/core/instanced/Raycasting.d.ts +118 -0
- package/dist/core/instanced/Raycasting.js +134 -0
- package/dist/core/instanced/instanced-material.d.ts +77 -0
- package/dist/core/instanced/instanced-material.js +228 -0
- package/dist/core/lines/BatchedLines.d.ts +137 -0
- package/dist/core/lines/BatchedLines.js +215 -0
- package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
- package/dist/core/lines/FrustumCullingLines.js +85 -0
- package/dist/core/lines/RaycastingLines.d.ts +45 -0
- package/dist/core/lines/RaycastingLines.js +91 -0
- package/dist/core/lines/lines-material.d.ts +47 -0
- package/dist/core/lines/lines-material.js +124 -0
- package/dist/core/shader-source.d.ts +70 -0
- package/dist/core/shader-source.js +110 -0
- package/dist/core/sprites/BatchedSprites.d.ts +115 -0
- package/dist/core/sprites/BatchedSprites.js +173 -0
- package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
- package/dist/core/sprites/RaycastingSprites.js +71 -0
- package/dist/core/sprites/sprite-material.d.ts +59 -0
- package/dist/core/sprites/sprite-material.js +192 -0
- package/dist/core/text/BatchedText.d.ts +233 -0
- package/dist/core/text/BatchedText.js +473 -0
- package/dist/core/text/MemberTexture.d.ts +90 -0
- package/dist/core/text/MemberTexture.js +289 -0
- package/dist/core/text/Text.d.ts +88 -0
- package/dist/core/text/Text.js +230 -0
- package/dist/core/text/batched-text-material.d.ts +56 -0
- package/dist/core/text/batched-text-material.js +123 -0
- package/dist/core/text/glyph-packing.d.ts +17 -0
- package/dist/core/text/glyph-packing.js +61 -0
- package/dist/core/typed-array.d.ts +16 -0
- package/dist/core/typed-array.js +20 -0
- package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
- package/dist/debug/components/DebugPanelProvider.js +50 -0
- package/dist/debug/components/DebugTools.d.ts +22 -0
- package/dist/debug/components/DebugTools.js +28 -0
- package/dist/debug/components/Gui.d.ts +22 -0
- package/dist/debug/components/Gui.js +42 -0
- package/dist/debug/components/MemoryInfo.d.ts +22 -0
- package/dist/debug/components/MemoryInfo.js +42 -0
- package/dist/debug/components/Panel.d.ts +31 -0
- package/dist/debug/components/Panel.js +104 -0
- package/dist/debug/components/Stats.d.ts +9 -0
- package/dist/debug/components/Stats.js +24 -0
- package/dist/debug/debug-chunk.d.ts +26 -0
- package/dist/debug/debug-chunk.js +32 -0
- package/dist/debug/debug-context.d.ts +41 -0
- package/dist/debug/debug-context.js +59 -0
- package/dist/debug/index.d.ts +41 -0
- package/dist/debug/index.js +43 -0
- package/dist/events/LayoutDriver.d.ts +36 -0
- package/dist/events/LayoutDriver.js +192 -0
- package/dist/events/PointerMissed.d.ts +23 -0
- package/dist/events/PointerMissed.js +58 -0
- package/dist/events/def-culling.d.ts +46 -0
- package/dist/events/def-culling.js +69 -0
- package/dist/events/layout-targets.d.ts +151 -0
- package/dist/events/layout-targets.js +397 -0
- package/dist/events/placement/box.d.ts +59 -0
- package/dist/events/placement/box.js +96 -0
- package/dist/events/placement/grid.d.ts +161 -0
- package/dist/events/placement/grid.js +235 -0
- package/dist/events/placement/index.d.ts +2 -0
- package/dist/events/placement/index.js +1 -0
- package/dist/events/placement/pass.d.ts +70 -0
- package/dist/events/placement/pass.js +617 -0
- package/dist/events/pointer.d.ts +194 -0
- package/dist/events/pointer.js +356 -0
- package/dist/external/ExternalCanvas.d.ts +56 -0
- package/dist/external/ExternalCanvas.js +177 -0
- package/dist/external/external-camera.d.ts +18 -0
- package/dist/external/external-camera.js +103 -0
- package/dist/external/external-events.d.ts +14 -0
- package/dist/external/external-events.js +33 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/objects/Background.d.ts +31 -0
- package/dist/objects/Background.js +37 -0
- package/dist/objects/containers/SlotContainer.d.ts +312 -0
- package/dist/objects/containers/SlotContainer.js +311 -0
- package/dist/objects/containers/hooks.d.ts +61 -0
- package/dist/objects/containers/hooks.js +196 -0
- package/dist/objects/containers/index.d.ts +6 -0
- package/dist/objects/containers/index.js +4 -0
- package/dist/objects/containers/keys.d.ts +10 -0
- package/dist/objects/containers/keys.js +18 -0
- package/dist/objects/containers/mesh-writes.d.ts +106 -0
- package/dist/objects/containers/mesh-writes.js +97 -0
- package/dist/objects/containers/props.d.ts +33 -0
- package/dist/objects/containers/props.js +1 -0
- package/dist/objects/images/ImageInstance.d.ts +29 -0
- package/dist/objects/images/ImageInstance.js +152 -0
- package/dist/objects/images/Images.d.ts +49 -0
- package/dist/objects/images/Images.js +394 -0
- package/dist/objects/images/atlas-packer.d.ts +129 -0
- package/dist/objects/images/atlas-packer.js +287 -0
- package/dist/objects/images/image-container.d.ts +74 -0
- package/dist/objects/images/image-container.js +100 -0
- package/dist/objects/images/index.d.ts +2 -0
- package/dist/objects/images/index.js +1 -0
- package/dist/objects/images/pack-sources.d.ts +190 -0
- package/dist/objects/images/pack-sources.js +272 -0
- package/dist/objects/images/texture-budget.d.ts +120 -0
- package/dist/objects/images/texture-budget.js +215 -0
- package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
- package/dist/objects/images/worker/atlas-worker.js +123 -0
- package/dist/objects/images/worker/client.d.ts +62 -0
- package/dist/objects/images/worker/client.js +249 -0
- package/dist/objects/images/worker/index.d.ts +10 -0
- package/dist/objects/images/worker/index.js +1 -0
- package/dist/objects/layer/Layer.d.ts +28 -0
- package/dist/objects/layer/Layer.js +134 -0
- package/dist/objects/layer/index.d.ts +2 -0
- package/dist/objects/layer/index.js +1 -0
- package/dist/objects/layer/layer-context.d.ts +74 -0
- package/dist/objects/layer/layer-context.js +82 -0
- package/dist/objects/layer/mount-scheduler.d.ts +131 -0
- package/dist/objects/layer/mount-scheduler.js +289 -0
- package/dist/objects/layer/visibility-gui.d.ts +19 -0
- package/dist/objects/layer/visibility-gui.js +93 -0
- package/dist/objects/lighting/Lighting.d.ts +92 -0
- package/dist/objects/lighting/Lighting.js +384 -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/LineInstance.d.ts +21 -0
- package/dist/objects/lines/LineInstance.js +73 -0
- package/dist/objects/lines/Lines.d.ts +17 -0
- package/dist/objects/lines/Lines.js +31 -0
- package/dist/objects/lines/index.d.ts +2 -0
- package/dist/objects/lines/index.js +1 -0
- package/dist/objects/lines/line-container.d.ts +35 -0
- package/dist/objects/lines/line-container.js +40 -0
- package/dist/objects/materials/ColorMaterial.d.ts +92 -0
- package/dist/objects/materials/ColorMaterial.js +124 -0
- package/dist/objects/materials/LineMaterial.d.ts +44 -0
- package/dist/objects/materials/LineMaterial.js +74 -0
- package/dist/objects/materials/ModelMaterial.d.ts +53 -0
- package/dist/objects/materials/ModelMaterial.js +111 -0
- package/dist/objects/materials/TextureMaterial.d.ts +33 -0
- package/dist/objects/materials/TextureMaterial.js +48 -0
- 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/effects/color-dim.d.ts +115 -0
- package/dist/objects/materials/effects/color-dim.js +206 -0
- 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 +53 -0
- package/dist/objects/materials/effects/effects-context.js +24 -0
- package/dist/objects/materials/shared.d.ts +24 -0
- package/dist/objects/materials/shared.js +72 -0
- package/dist/objects/models/ModelInstance.d.ts +22 -0
- package/dist/objects/models/ModelInstance.js +33 -0
- package/dist/objects/models/Models.d.ts +22 -0
- package/dist/objects/models/Models.js +186 -0
- package/dist/objects/models/gltf-cache.d.ts +17 -0
- package/dist/objects/models/gltf-cache.js +50 -0
- package/dist/objects/models/index.d.ts +2 -0
- package/dist/objects/models/index.js +1 -0
- package/dist/objects/models/model-asset.d.ts +48 -0
- package/dist/objects/models/model-asset.js +77 -0
- package/dist/objects/models/model-container.d.ts +123 -0
- package/dist/objects/models/model-container.js +207 -0
- package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
- package/dist/objects/shapes/ShapeInstance.js +39 -0
- package/dist/objects/shapes/Shapes.d.ts +21 -0
- package/dist/objects/shapes/Shapes.js +131 -0
- package/dist/objects/shapes/index.d.ts +2 -0
- package/dist/objects/shapes/index.js +1 -0
- package/dist/objects/shapes/shape-container.d.ts +141 -0
- package/dist/objects/shapes/shape-container.js +242 -0
- package/dist/objects/shapes/shape-geometry.d.ts +27 -0
- package/dist/objects/shapes/shape-geometry.js +94 -0
- package/dist/objects/text/TextInstance.d.ts +22 -0
- package/dist/objects/text/TextInstance.js +95 -0
- package/dist/objects/text/Texts.d.ts +30 -0
- package/dist/objects/text/Texts.js +43 -0
- package/dist/objects/text/index.d.ts +2 -0
- package/dist/objects/text/index.js +1 -0
- package/dist/objects/text/text-container.d.ts +121 -0
- package/dist/objects/text/text-container.js +229 -0
- package/dist/objects/text/text-layout.d.ts +92 -0
- package/dist/objects/text/text-layout.js +180 -0
- package/dist/space/Camera.d.ts +103 -0
- package/dist/space/Camera.js +321 -0
- package/dist/space/Scene.d.ts +47 -0
- package/dist/space/Scene.js +127 -0
- package/dist/space/Space.d.ts +325 -0
- package/dist/space/Space.js +359 -0
- package/dist/space/VisibleRect.d.ts +45 -0
- package/dist/space/VisibleRect.js +129 -0
- package/dist/space/camera-handle.d.ts +67 -0
- package/dist/space/camera-handle.js +63 -0
- package/dist/space/camera-kind.d.ts +26 -0
- package/dist/space/camera-kind.js +30 -0
- package/dist/space/canvas-size.d.ts +24 -0
- package/dist/space/canvas-size.js +20 -0
- package/dist/space/clipping.d.ts +69 -0
- package/dist/space/clipping.js +130 -0
- package/dist/space/layout-measure.d.ts +67 -0
- package/dist/space/layout-measure.js +175 -0
- package/dist/space/viewport.d.ts +112 -0
- package/dist/space/viewport.js +67 -0
- package/dist/types/defs.d.ts +234 -0
- package/dist/types/defs.js +1 -0
- package/dist/types/events.d.ts +198 -0
- package/dist/types/events.js +1 -0
- package/dist/types/guards.d.ts +21 -0
- package/dist/types/guards.js +52 -0
- package/dist/update/index.d.ts +3 -0
- package/dist/update/index.js +2 -0
- package/dist/update/slot-writers.d.ts +30 -0
- package/dist/update/slot-writers.js +76 -0
- package/dist/update/store.d.ts +41 -0
- package/dist/update/store.js +104 -0
- package/dist/utils/arrays.d.ts +13 -0
- package/dist/utils/arrays.js +20 -0
- package/dist/utils/colors.d.ts +19 -0
- package/dist/utils/colors.js +27 -0
- package/dist/utils/hot-state.d.ts +41 -0
- package/dist/utils/hot-state.js +37 -0
- package/dist/utils/logging.d.ts +29 -0
- package/dist/utils/logging.js +55 -0
- package/dist/utils/math.d.ts +52 -0
- package/dist/utils/math.js +76 -0
- package/package.json +79 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Vector2 } from 'three';
|
|
2
|
+
import { NOISE_TILE } from './ambient-occlusion-pass';
|
|
3
|
+
/**
|
|
4
|
+
* Builds one blur pass's uniforms: the two textures it reads, its own texel step, and the scene's
|
|
5
|
+
* own kernel side.
|
|
6
|
+
*
|
|
7
|
+
* Per mount rather than per module, for the reason `createAoPassUniforms` is. These carry textures
|
|
8
|
+
* allocated in whichever GL context drew them, and a second mounted scene has a second set.
|
|
9
|
+
*
|
|
10
|
+
* {@link AmbientOcclusion.aoBlur} arrives by identity rather than by copy, so the debug GUI's write
|
|
11
|
+
* lands in this shader with nothing in between. The frame writes into everything else in place, so
|
|
12
|
+
* a frame costs no allocation and no shader recompile.
|
|
13
|
+
* @param occlusion - The mounted scene's own occlusion state, for the kernel side the debug GUI
|
|
14
|
+
* tunes.
|
|
15
|
+
* @returns The uniform record, ready to hand to a `ShaderMaterial`.
|
|
16
|
+
*/
|
|
17
|
+
export function createBlurPassUniforms(occlusion) {
|
|
18
|
+
return {
|
|
19
|
+
/** The raw term the AO pass wrote, which this pass smooths. `null` until the AO pass runs. */
|
|
20
|
+
uOcclusion: { value: null },
|
|
21
|
+
/** The prepass's depth, which weights every tap. `null` until the prepass runs. */
|
|
22
|
+
uDepth: { value: null },
|
|
23
|
+
/** One texel of this pass's own target in uv, the step the kernel walks. */
|
|
24
|
+
uTexel: { value: new Vector2(1, 1) },
|
|
25
|
+
/** The kernel's side in texels, the scene's own object. See {@link AmbientOcclusion.aoBlur}. */
|
|
26
|
+
uBlur: occlusion.aoBlur,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* How far apart two depths sit before a read stops treating them as one surface. Measured as a
|
|
31
|
+
* share of the range still ahead of the nearer one.
|
|
32
|
+
*
|
|
33
|
+
* One number for both readers, because they ask the same question. The blur pass asks it of every
|
|
34
|
+
* tap, so occlusion does not bleed across the edge between a wall and whatever stands behind it.
|
|
35
|
+
* The lit read asks it once, so a translucent surface, which drew into no prepass, takes no
|
|
36
|
+
* occlusion from whatever stands behind it.
|
|
37
|
+
*
|
|
38
|
+
* Too tight, and the tile's rotation survives the blur as visible noise, because the pass throws
|
|
39
|
+
* most taps away. Too loose, and occlusion bleeds a few pixels past a silhouette. The bleed is the
|
|
40
|
+
* gentler failure, so this number errs generous.
|
|
41
|
+
*/
|
|
42
|
+
export const AO_EDGE_TOLERANCE = 0.05;
|
|
43
|
+
/**
|
|
44
|
+
* The blur pass's fragment stage: the depth-weighted square kernel that smooths the raw term.
|
|
45
|
+
*
|
|
46
|
+
* The kernel runs here rather than inside the lit material's read. There it bills per lit fragment
|
|
47
|
+
* at full resolution and once per overdraw, at `2 · side²` fetches. Measured on a real plan: `2 ms`
|
|
48
|
+
* with no occlusion at all, `4 ms` at side `1`, and `8 ms` at side `4`. The AO pass's tap count
|
|
49
|
+
* makes no measurable difference. Here the kernel bills once per texel of a quarter-area target
|
|
50
|
+
* with no overdraw, and the lit read drops to two fetches.
|
|
51
|
+
*
|
|
52
|
+
* The pass samples the depth texture while it writes a color texture, so it binds a target with no
|
|
53
|
+
* depth attachment. A write through the prepass's own target is a rendering feedback loop, which
|
|
54
|
+
* WebGL2 leaves undefined and browsers refuse. That is what the third render target is for, see
|
|
55
|
+
* `occlusion-targets.ts`.
|
|
56
|
+
*
|
|
57
|
+
* This fragment's own depth comes from the texture, because the pass carries no geometry that
|
|
58
|
+
* writes one. One fetch, at this texel's own center.
|
|
59
|
+
*/
|
|
60
|
+
export const BLUR_FRAGMENT_SHADER = /* glsl */ `
|
|
61
|
+
uniform sampler2D uOcclusion;
|
|
62
|
+
uniform sampler2D uDepth;
|
|
63
|
+
uniform vec2 uTexel;
|
|
64
|
+
uniform int uBlur;
|
|
65
|
+
varying vec2 vUv;
|
|
66
|
+
|
|
67
|
+
const float AO_EDGE = ${AO_EDGE_TOLERANCE};
|
|
68
|
+
|
|
69
|
+
void main() {
|
|
70
|
+
float depth = texture2D(uDepth, vUv).x;
|
|
71
|
+
// Symmetric at any side, and this texel alone at side 1.
|
|
72
|
+
float center = float(uBlur - 1) * .5;
|
|
73
|
+
float occluded = 0.;
|
|
74
|
+
float total = 0.;
|
|
75
|
+
// GLSL ES 1.00 on the old Android WebViews this SDK reaches needs constant loop bounds, and the
|
|
76
|
+
// break is what skips the fetches.
|
|
77
|
+
for (int y = 0; y < ${NOISE_TILE}; y++) {
|
|
78
|
+
if (y >= uBlur) break;
|
|
79
|
+
for (int x = 0; x < ${NOISE_TILE}; x++) {
|
|
80
|
+
if (x >= uBlur) break;
|
|
81
|
+
vec2 at = vUv + (vec2(float(x), float(y)) - center) * uTexel;
|
|
82
|
+
// Depth-aware, so occlusion does not bleed across the edge between a wall and whatever
|
|
83
|
+
// stands behind it. Stored depth is not linear, so the difference is a share of the range
|
|
84
|
+
// still ahead of this texel.
|
|
85
|
+
float stored = texture2D(uDepth, at).x;
|
|
86
|
+
float apart = abs(stored - depth) / max(1. - depth, 1e-6);
|
|
87
|
+
float weight = 1. - smoothstep(0., AO_EDGE, apart);
|
|
88
|
+
occluded += texture2D(uOcclusion, at).r * weight;
|
|
89
|
+
total += weight;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Nothing around this texel describes the surface it is on, so there is no term to write and no
|
|
94
|
+
// total to divide by. Open, which a lit read of it leaves alone.
|
|
95
|
+
if (total <= 0.) {
|
|
96
|
+
gl_FragColor = vec4(0., 0., 0., 1.);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
gl_FragColor = vec4(occluded / total, 0., 0., 1.);
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
@@ -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 under 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 under a tenth of a booth wide, where a junction's shadow reads as shading rather than as
|
|
51
|
+
* an outline.
|
|
52
|
+
*/
|
|
53
|
+
radius: 8,
|
|
54
|
+
/** How far the read blends an occluded fragment toward the shadow color, from `0` to `1`. */
|
|
55
|
+
strength: 0.35,
|
|
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
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type IUniform, type WebGLProgramParametersWithUniforms } from 'three';
|
|
2
|
+
import { type UniformSchemaShader } from '../../../core/InstanceUniforms';
|
|
3
|
+
import { type RenderableDef } from '../../../types/defs';
|
|
4
|
+
/**
|
|
5
|
+
* Patches a compiled shader to dim what it draws. Returned by {@link useDimPatch} and called from a
|
|
6
|
+
* material's own `onBeforeCompile`, so a material that already patches its shader composes the two
|
|
7
|
+
* rather than choosing between them.
|
|
8
|
+
*/
|
|
9
|
+
export type DimPatch = (parameters: WebGLProgramParametersWithUniforms) => void;
|
|
10
|
+
/**
|
|
11
|
+
* The dim patch every material installs, following the layer amount from {@link useDimChain}.
|
|
12
|
+
*
|
|
13
|
+
* Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
|
|
14
|
+
* so threading it as a prop would put a `dim` on each material, each container, and each of their
|
|
15
|
+
* inner components, and any new one would have to remember to forward it. What a material needs to
|
|
16
|
+
* know is only *how* to be dimmed; what it is dimmed *by* belongs to the layer it draws under.
|
|
17
|
+
*
|
|
18
|
+
* The amount is held in one `IUniform` object per material and mutated in place, never replaced, so
|
|
19
|
+
* an animated dim — floorplan eases the venue's dim over 300ms — costs a number per material per
|
|
20
|
+
* frame with no recompile. The amount is followed by subscription, not read in render, so that frame
|
|
21
|
+
* renders nothing either.
|
|
22
|
+
* @returns The patch to call from the material's `onBeforeCompile`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useDimPatch(): DimPatch;
|
|
25
|
+
/**
|
|
26
|
+
* Installs dimming into a shader about to be compiled: the layer's uniform, the declarations, and
|
|
27
|
+
* the application.
|
|
28
|
+
*
|
|
29
|
+
* **Applies only once the mesh's uniform prologue is in the shader**, and that guard is what makes
|
|
30
|
+
* installation order stop mattering. The applied line reads the per-instance amount the prologue
|
|
31
|
+
* declares as a *local of `main`*, so it has to land in the same `main`. Most meshes inject their
|
|
32
|
+
* prologue and then call the material's handler, so a material-installed patch is already last. Text
|
|
33
|
+
* is the exception: troika calls the base material's handler first and then rebuilds `main` around
|
|
34
|
+
* it — renaming the original `troikaOrigMain` and calling it from a new one — so a patch installed
|
|
35
|
+
* there would land in a scope the prologue's local never reaches. With this guard that early call
|
|
36
|
+
* simply does nothing, and `BatchedTextImpl` runs the same patch again once its prologue is in.
|
|
37
|
+
* @param parameters - The shader being compiled.
|
|
38
|
+
* @param uniform - The layer's dim amount.
|
|
39
|
+
* @returns Whether the shader was patched.
|
|
40
|
+
* @throws {Error} If the prologue is present but the shader has no output chain, which would leave
|
|
41
|
+
* dimming silently absent from a material that asked for it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyDim(parameters: WebGLProgramParametersWithUniforms, uniform: IUniform<number>): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Orders a layer's defs so the dimmed ones draw first and the undimmed ones last, on top.
|
|
46
|
+
*
|
|
47
|
+
* What this is for: dimming the venue behind a search leaves the few undimmed defs as the subject of
|
|
48
|
+
* the frame, and a subject that a dimmed neighbor happens to overlap would be covered by it. Paint
|
|
49
|
+
* order is the only thing that resolves overlap in a 2D layer — 2D content never writes depth,
|
|
50
|
+
* leaving depth testing nothing of its own to compare against — so being undimmed has to lift a def
|
|
51
|
+
* above the defs it is being distinguished from.
|
|
52
|
+
*
|
|
53
|
+
* **Stable**, and that is load-bearing rather than a nicety: within each group the defs keep their
|
|
54
|
+
* array order, which is the order the caller authored and the only thing that decides overlap
|
|
55
|
+
* between two defs that are both dimmed or both lit. A dim is allowed to lift a def over the dimmed
|
|
56
|
+
* ones; it is not allowed to reshuffle anything else.
|
|
57
|
+
*
|
|
58
|
+
* Returns the input array itself when the rule changes nothing — every def in one group, which is
|
|
59
|
+
* every layer that is uniformly dimmed or uniformly not. Callers key their order pass on the
|
|
60
|
+
* identity of what comes back, so an unchanged layer costs neither an allocation nor a re-label.
|
|
61
|
+
* @param defs - The layer's defs, in authored order.
|
|
62
|
+
* @param layerDimmed - Whether the layer is dimmed, which is what a def without a `dim` of its own
|
|
63
|
+
* inherits. Only the fact is needed, never the amount: this is a two-way split, and every amount
|
|
64
|
+
* above zero lands on the same side of it. That is also what keeps an eased layer dim cheap — the
|
|
65
|
+
* order pass re-runs when the value crosses zero, not on each of the frames between.
|
|
66
|
+
* @returns The defs in draw order.
|
|
67
|
+
*/
|
|
68
|
+
export declare function dimOrdered<T extends RenderableDef>(defs: readonly T[], layerDimmed: boolean): readonly T[];
|
|
69
|
+
/**
|
|
70
|
+
* Whether a def draws dimmed at all — the one question paint order asks about dimming, as opposed
|
|
71
|
+
* to the amount, which only the shader reads.
|
|
72
|
+
* @param dim - The def's own `dim`; `undefined` inherits.
|
|
73
|
+
* @param layerDimmed - Whether the layer it draws under is dimmed.
|
|
74
|
+
* @returns Whether it draws dimmed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isDimmed(dim: number | undefined, layerDimmed: boolean): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Encodes a def's own `dim` for its per-instance uniform.
|
|
79
|
+
* @param dim - The def's `dim`; `undefined` defers to the layer.
|
|
80
|
+
* @returns The value to write.
|
|
81
|
+
*/
|
|
82
|
+
export declare function instanceDim(dim: number | undefined): number;
|
|
83
|
+
/**
|
|
84
|
+
* Clamps a dim amount into the range the shader interpolates over, so `mix` never extrapolates into
|
|
85
|
+
* a color that is not a dim of the original.
|
|
86
|
+
* @param dim - The amount to clamp.
|
|
87
|
+
* @returns The amount, within `[0, 1]`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function clampDim(dim: number): number;
|
|
90
|
+
/**
|
|
91
|
+
* The per-instance uniform a def's own dim is written to. Declared by every container that draws
|
|
92
|
+
* dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
|
|
93
|
+
* declare it before any slot writes one.
|
|
94
|
+
*/
|
|
95
|
+
export declare const DIM_UNIFORM = "instanceDim";
|
|
96
|
+
/**
|
|
97
|
+
* What an instance writes to defer to its layer.
|
|
98
|
+
*
|
|
99
|
+
* Any negative value would do; the sentinel only has to fall outside `[0, 1]`, which is why the
|
|
100
|
+
* amount itself can be stored as-is rather than packed around a zero that means "inherit". The
|
|
101
|
+
* imperative renderer needed that packing because its texture zero-filled and nothing wrote an
|
|
102
|
+
* instance on mount — here every instance writes its own value through `useDefSync`, so the fill is
|
|
103
|
+
* never what a draw reads.
|
|
104
|
+
*/
|
|
105
|
+
export declare const DIM_INHERIT = -1;
|
|
106
|
+
/**
|
|
107
|
+
* The schema a container declares to give its instances a dim of their own.
|
|
108
|
+
*
|
|
109
|
+
* Declared in the **vertex** stage although only the fragment shader reads it: that is what makes
|
|
110
|
+
* `InstanceUniforms` fetch it once per vertex and deliver it as a varying, instead of a texel fetch
|
|
111
|
+
* per fragment. It also moves any sibling uniform the same way, which costs nothing — images, lines
|
|
112
|
+
* and models already declare vertex uniforms, and shapes and text declare none besides this.
|
|
113
|
+
*/
|
|
114
|
+
export declare const DIM_SCHEMA: UniformSchemaShader;
|
|
115
|
+
//# sourceMappingURL=color-dim.d.ts.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useThree } from '@react-three/fiber';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { declareBeforeMain, sceneColorAnchor } from '../../../core/shader-source';
|
|
4
|
+
import { resolveDim, subscribeDim, useDimChain } from '../../layer/layer-context';
|
|
5
|
+
/**
|
|
6
|
+
* The dim patch every material installs, following the layer amount from {@link useDimChain}.
|
|
7
|
+
*
|
|
8
|
+
* Dimming is deliberately **not** a material prop. It cuts across every material and every def type,
|
|
9
|
+
* so threading it as a prop would put a `dim` on each material, each container, and each of their
|
|
10
|
+
* inner components, and any new one would have to remember to forward it. What a material needs to
|
|
11
|
+
* know is only *how* to be dimmed; what it is dimmed *by* belongs to the layer it draws under.
|
|
12
|
+
*
|
|
13
|
+
* The amount is held in one `IUniform` object per material and mutated in place, never replaced, so
|
|
14
|
+
* an animated dim — floorplan eases the venue's dim over 300ms — costs a number per material per
|
|
15
|
+
* frame with no recompile. The amount is followed by subscription, not read in render, so that frame
|
|
16
|
+
* renders nothing either.
|
|
17
|
+
* @returns The patch to call from the material's `onBeforeCompile`.
|
|
18
|
+
*/
|
|
19
|
+
export function useDimPatch() {
|
|
20
|
+
const chain = useDimChain();
|
|
21
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
22
|
+
// A ref, not state: this object is written after render, every frame of an eased dim, and a ref is
|
|
23
|
+
// the container React sanctions that for. Its identity is what three holds onto, so the value can
|
|
24
|
+
// move underneath a program that is already built.
|
|
25
|
+
const uniformRef = useRef({ value: 0 });
|
|
26
|
+
// Created once and kept: R3F writes every changed prop onto the material, so handing it a fresh
|
|
27
|
+
// handler each render risks a recompile per frame — the one thing an animated dim must not cost.
|
|
28
|
+
const [patch] = useState(() => (parameters) => applyDim(parameters, uniformRef.current));
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
/** Writes the layer's current amount into the uniform, and asks for a frame when it moved. */
|
|
31
|
+
function follow() {
|
|
32
|
+
const amount = clampDim(resolveDim(chain));
|
|
33
|
+
if (uniformRef.current.value === amount)
|
|
34
|
+
return;
|
|
35
|
+
uniformRef.current.value = amount;
|
|
36
|
+
invalidate();
|
|
37
|
+
}
|
|
38
|
+
follow();
|
|
39
|
+
return subscribeDim(chain, follow);
|
|
40
|
+
}, [chain, invalidate]);
|
|
41
|
+
return patch;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Installs dimming into a shader about to be compiled: the layer's uniform, the declarations, and
|
|
45
|
+
* the application.
|
|
46
|
+
*
|
|
47
|
+
* **Applies only once the mesh's uniform prologue is in the shader**, and that guard is what makes
|
|
48
|
+
* installation order stop mattering. The applied line reads the per-instance amount the prologue
|
|
49
|
+
* declares as a *local of `main`*, so it has to land in the same `main`. Most meshes inject their
|
|
50
|
+
* prologue and then call the material's handler, so a material-installed patch is already last. Text
|
|
51
|
+
* is the exception: troika calls the base material's handler first and then rebuilds `main` around
|
|
52
|
+
* it — renaming the original `troikaOrigMain` and calling it from a new one — so a patch installed
|
|
53
|
+
* there would land in a scope the prologue's local never reaches. With this guard that early call
|
|
54
|
+
* simply does nothing, and `BatchedTextImpl` runs the same patch again once its prologue is in.
|
|
55
|
+
* @param parameters - The shader being compiled.
|
|
56
|
+
* @param uniform - The layer's dim amount.
|
|
57
|
+
* @returns Whether the shader was patched.
|
|
58
|
+
* @throws {Error} If the prologue is present but the shader has no output chain, which would leave
|
|
59
|
+
* dimming silently absent from a material that asked for it.
|
|
60
|
+
*/
|
|
61
|
+
export function applyDim(parameters, uniform) {
|
|
62
|
+
const fragment = parameters.fragmentShader;
|
|
63
|
+
// Nothing to read, so nothing to do — either the mesh declared no per-instance dim, or its
|
|
64
|
+
// prologue has not been injected yet.
|
|
65
|
+
if (!fragment.includes(DIM_UNIFORM))
|
|
66
|
+
return false;
|
|
67
|
+
// Already installed, which is how a second call after the prologue arrives stays harmless.
|
|
68
|
+
if (fragment.includes(DIM_FUNCTION))
|
|
69
|
+
return false;
|
|
70
|
+
const anchor = sceneColorAnchor(fragment);
|
|
71
|
+
if (!anchor)
|
|
72
|
+
throw new Error('applyDim: the shader has no output chain to dim before.');
|
|
73
|
+
parameters.uniforms[LAYER_DIM] = uniform;
|
|
74
|
+
// Ahead of `main`, because these are file-scope declarations — and `dimColor` takes the amount as a
|
|
75
|
+
// parameter precisely so it needs nothing `main` declares.
|
|
76
|
+
parameters.fragmentShader = declareBeforeMain(fragment, DIM_DEFS).replace(anchor, `${DIM_APPLY}\n${anchor}`);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Orders a layer's defs so the dimmed ones draw first and the undimmed ones last, on top.
|
|
81
|
+
*
|
|
82
|
+
* What this is for: dimming the venue behind a search leaves the few undimmed defs as the subject of
|
|
83
|
+
* the frame, and a subject that a dimmed neighbor happens to overlap would be covered by it. Paint
|
|
84
|
+
* order is the only thing that resolves overlap in a 2D layer — 2D content never writes depth,
|
|
85
|
+
* leaving depth testing nothing of its own to compare against — so being undimmed has to lift a def
|
|
86
|
+
* above the defs it is being distinguished from.
|
|
87
|
+
*
|
|
88
|
+
* **Stable**, and that is load-bearing rather than a nicety: within each group the defs keep their
|
|
89
|
+
* array order, which is the order the caller authored and the only thing that decides overlap
|
|
90
|
+
* between two defs that are both dimmed or both lit. A dim is allowed to lift a def over the dimmed
|
|
91
|
+
* ones; it is not allowed to reshuffle anything else.
|
|
92
|
+
*
|
|
93
|
+
* Returns the input array itself when the rule changes nothing — every def in one group, which is
|
|
94
|
+
* every layer that is uniformly dimmed or uniformly not. Callers key their order pass on the
|
|
95
|
+
* identity of what comes back, so an unchanged layer costs neither an allocation nor a re-label.
|
|
96
|
+
* @param defs - The layer's defs, in authored order.
|
|
97
|
+
* @param layerDimmed - Whether the layer is dimmed, which is what a def without a `dim` of its own
|
|
98
|
+
* inherits. Only the fact is needed, never the amount: this is a two-way split, and every amount
|
|
99
|
+
* above zero lands on the same side of it. That is also what keeps an eased layer dim cheap — the
|
|
100
|
+
* order pass re-runs when the value crosses zero, not on each of the frames between.
|
|
101
|
+
* @returns The defs in draw order.
|
|
102
|
+
*/
|
|
103
|
+
export function dimOrdered(defs, layerDimmed) {
|
|
104
|
+
const dimmed = [];
|
|
105
|
+
const lit = [];
|
|
106
|
+
for (const def of defs)
|
|
107
|
+
(isDimmed(def.dim, layerDimmed) ? dimmed : lit).push(def);
|
|
108
|
+
return dimmed.length === 0 || lit.length === 0 ? defs : [...dimmed, ...lit];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether a def draws dimmed at all — the one question paint order asks about dimming, as opposed
|
|
112
|
+
* to the amount, which only the shader reads.
|
|
113
|
+
* @param dim - The def's own `dim`; `undefined` inherits.
|
|
114
|
+
* @param layerDimmed - Whether the layer it draws under is dimmed.
|
|
115
|
+
* @returns Whether it draws dimmed.
|
|
116
|
+
*/
|
|
117
|
+
export function isDimmed(dim, layerDimmed) {
|
|
118
|
+
return dim === undefined ? layerDimmed : dim > 0;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Encodes a def's own `dim` for its per-instance uniform.
|
|
122
|
+
* @param dim - The def's `dim`; `undefined` defers to the layer.
|
|
123
|
+
* @returns The value to write.
|
|
124
|
+
*/
|
|
125
|
+
export function instanceDim(dim) {
|
|
126
|
+
return dim === undefined ? DIM_INHERIT : clampDim(dim);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Clamps a dim amount into the range the shader interpolates over, so `mix` never extrapolates into
|
|
130
|
+
* a color that is not a dim of the original.
|
|
131
|
+
* @param dim - The amount to clamp.
|
|
132
|
+
* @returns The amount, within `[0, 1]`.
|
|
133
|
+
*/
|
|
134
|
+
export function clampDim(dim) {
|
|
135
|
+
return Math.min(Math.max(dim, 0), 1);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The per-instance uniform a def's own dim is written to. Declared by every container that draws
|
|
139
|
+
* dimmable defs and read by {@link DIM_APPLY}; the material owns the name, the mesh only has to
|
|
140
|
+
* declare it before any slot writes one.
|
|
141
|
+
*/
|
|
142
|
+
export const DIM_UNIFORM = 'instanceDim';
|
|
143
|
+
/**
|
|
144
|
+
* What an instance writes to defer to its layer.
|
|
145
|
+
*
|
|
146
|
+
* Any negative value would do; the sentinel only has to fall outside `[0, 1]`, which is why the
|
|
147
|
+
* amount itself can be stored as-is rather than packed around a zero that means "inherit". The
|
|
148
|
+
* imperative renderer needed that packing because its texture zero-filled and nothing wrote an
|
|
149
|
+
* instance on mount — here every instance writes its own value through `useDefSync`, so the fill is
|
|
150
|
+
* never what a draw reads.
|
|
151
|
+
*/
|
|
152
|
+
export const DIM_INHERIT = -1;
|
|
153
|
+
/**
|
|
154
|
+
* The schema a container declares to give its instances a dim of their own.
|
|
155
|
+
*
|
|
156
|
+
* Declared in the **vertex** stage although only the fragment shader reads it: that is what makes
|
|
157
|
+
* `InstanceUniforms` fetch it once per vertex and deliver it as a varying, instead of a texel fetch
|
|
158
|
+
* per fragment. It also moves any sibling uniform the same way, which costs nothing — images, lines
|
|
159
|
+
* and models already declare vertex uniforms, and shapes and text declare none besides this.
|
|
160
|
+
*/
|
|
161
|
+
export const DIM_SCHEMA = { vertex: { [DIM_UNIFORM]: 'float' } };
|
|
162
|
+
/** The uniform carrying the layer-level amount an instance without one of its own falls back to. */
|
|
163
|
+
const LAYER_DIM = 'uDim';
|
|
164
|
+
/** The signature {@link applyDim} looks for to tell an already-patched shader from a fresh one. */
|
|
165
|
+
const DIM_FUNCTION = 'vec4 dimColor(';
|
|
166
|
+
/**
|
|
167
|
+
* Dimming's file-scope declarations.
|
|
168
|
+
*
|
|
169
|
+
* Dim is a desaturation toward gray, not a darkening. That is why it cannot be baked into a
|
|
170
|
+
* per-instance tint: a texel sampled per fragment has no tint to bake into.
|
|
171
|
+
*
|
|
172
|
+
* `dimColor` works on `gl_FragColor` whichever way it is stored, and hands it back stored the same
|
|
173
|
+
* way. It only ever scales `col.rgb`: `mix(c, dot(w, c) / k, t)` is linear and homogeneous in `c`,
|
|
174
|
+
* so scaling the input by any constant scales the output by that same constant. That homogeneity
|
|
175
|
+
* is also why a premultiplied, `SRGBColorSpace`-tagged atlas texture dims to the same composited
|
|
176
|
+
* pixels as the straight-alpha texture this renderer used before it. Measuring old against new
|
|
177
|
+
* leaves a small residual: at most about 8 of 255 on one channel, across every alpha and dim
|
|
178
|
+
* amount tested.
|
|
179
|
+
*
|
|
180
|
+
* `stories/objects/layer/Dim.stories.tsx` does not cover any of this. Its image layer draws
|
|
181
|
+
* `markerCanvas(tint)`, whose whole surface is opaque, so no pixel in that story ever carries a
|
|
182
|
+
* partial alpha for `dimColor` to distort.
|
|
183
|
+
*
|
|
184
|
+
* The imperative renderer divided rgb by alpha here and multiplied it back at the end. The round
|
|
185
|
+
* trip cancels for the same reason: `f(c / a) * a == f(c)` for every alpha. Dividing by alpha was
|
|
186
|
+
* also the one step that could produce a NaN, at `a == 0`, which SDF text edges generate in
|
|
187
|
+
* quantity. Dropping it changes no pixel.
|
|
188
|
+
*/
|
|
189
|
+
const DIM_DEFS = /* glsl */ `
|
|
190
|
+
uniform float ${LAYER_DIM};
|
|
191
|
+
|
|
192
|
+
const vec3 grayWeights = vec3(0.299, 0.587, 0.114);
|
|
193
|
+
const float darkenFactor = pow(2., 2.2); // Gamma corrected
|
|
194
|
+
|
|
195
|
+
${DIM_FUNCTION}vec4 col, float amount) {
|
|
196
|
+
vec3 gray = vec3(dot(grayWeights, col.rgb));
|
|
197
|
+
return vec4(mix(col.rgb, gray / darkenFactor, amount), col.a);
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
/**
|
|
201
|
+
* Dimming applied while the color is still linear and scene-referred, which is what `darkenFactor`'s
|
|
202
|
+
* gamma correction assumes. See {@link sceneColorAnchor}.
|
|
203
|
+
*/
|
|
204
|
+
const DIM_APPLY = /* glsl */ `
|
|
205
|
+
gl_FragColor = dimColor(gl_FragColor, ${DIM_UNIFORM} < 0. ? ${LAYER_DIM} : ${DIM_UNIFORM});
|
|
206
|
+
`;
|