@expofp/renderer 0.0.0-experimental.2d6c681

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +145 -0
  2. package/dist/Renderer.d.ts +102 -0
  3. package/dist/Renderer.js +96 -0
  4. package/dist/controls/CameraControls.d.ts +108 -0
  5. package/dist/controls/CameraControls.js +203 -0
  6. package/dist/controls/Controls.d.ts +53 -0
  7. package/dist/controls/Controls.js +54 -0
  8. package/dist/controls/camera-api.d.ts +226 -0
  9. package/dist/controls/camera-api.js +179 -0
  10. package/dist/controls/gestures.d.ts +71 -0
  11. package/dist/controls/gestures.js +98 -0
  12. package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
  13. package/dist/controls/handlers/DoubleTapZoom.js +82 -0
  14. package/dist/controls/handlers/Inertia.d.ts +60 -0
  15. package/dist/controls/handlers/Inertia.js +222 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +212 -0
  18. package/dist/controls/handlers/Pitch.d.ts +19 -0
  19. package/dist/controls/handlers/Pitch.js +200 -0
  20. package/dist/controls/handlers/Roll.d.ts +30 -0
  21. package/dist/controls/handlers/Roll.js +196 -0
  22. package/dist/controls/handlers/Zoom.d.ts +28 -0
  23. package/dist/controls/handlers/Zoom.js +34 -0
  24. package/dist/controls/hooks.d.ts +136 -0
  25. package/dist/controls/hooks.js +200 -0
  26. package/dist/controls/index.d.ts +9 -0
  27. package/dist/controls/index.js +7 -0
  28. package/dist/controls/zoom-to.d.ts +89 -0
  29. package/dist/controls/zoom-to.js +91 -0
  30. package/dist/core/InstanceUniforms.d.ts +108 -0
  31. package/dist/core/InstanceUniforms.js +298 -0
  32. package/dist/core/SquareDataTexture.d.ts +277 -0
  33. package/dist/core/SquareDataTexture.js +476 -0
  34. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  35. package/dist/core/batch/BatchedMesh.js +155 -0
  36. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  37. package/dist/core/instanced/FrustumCulling.js +246 -0
  38. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  39. package/dist/core/instanced/InstancedMesh.js +497 -0
  40. package/dist/core/instanced/Instances.d.ts +119 -0
  41. package/dist/core/instanced/Instances.js +230 -0
  42. package/dist/core/instanced/Raycasting.d.ts +118 -0
  43. package/dist/core/instanced/Raycasting.js +134 -0
  44. package/dist/core/instanced/instanced-material.d.ts +77 -0
  45. package/dist/core/instanced/instanced-material.js +228 -0
  46. package/dist/core/lines/BatchedLines.d.ts +137 -0
  47. package/dist/core/lines/BatchedLines.js +215 -0
  48. package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
  49. package/dist/core/lines/FrustumCullingLines.js +85 -0
  50. package/dist/core/lines/RaycastingLines.d.ts +45 -0
  51. package/dist/core/lines/RaycastingLines.js +91 -0
  52. package/dist/core/lines/lines-material.d.ts +47 -0
  53. package/dist/core/lines/lines-material.js +124 -0
  54. package/dist/core/shader-source.d.ts +70 -0
  55. package/dist/core/shader-source.js +110 -0
  56. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  57. package/dist/core/sprites/BatchedSprites.js +173 -0
  58. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  59. package/dist/core/sprites/RaycastingSprites.js +71 -0
  60. package/dist/core/sprites/sprite-material.d.ts +59 -0
  61. package/dist/core/sprites/sprite-material.js +192 -0
  62. package/dist/core/text/BatchedText.d.ts +233 -0
  63. package/dist/core/text/BatchedText.js +473 -0
  64. package/dist/core/text/MemberTexture.d.ts +90 -0
  65. package/dist/core/text/MemberTexture.js +289 -0
  66. package/dist/core/text/Text.d.ts +88 -0
  67. package/dist/core/text/Text.js +230 -0
  68. package/dist/core/text/batched-text-material.d.ts +56 -0
  69. package/dist/core/text/batched-text-material.js +123 -0
  70. package/dist/core/text/glyph-packing.d.ts +17 -0
  71. package/dist/core/text/glyph-packing.js +61 -0
  72. package/dist/core/typed-array.d.ts +16 -0
  73. package/dist/core/typed-array.js +20 -0
  74. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  75. package/dist/debug/components/DebugPanelProvider.js +50 -0
  76. package/dist/debug/components/DebugTools.d.ts +22 -0
  77. package/dist/debug/components/DebugTools.js +28 -0
  78. package/dist/debug/components/Gui.d.ts +22 -0
  79. package/dist/debug/components/Gui.js +42 -0
  80. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  81. package/dist/debug/components/MemoryInfo.js +42 -0
  82. package/dist/debug/components/Panel.d.ts +31 -0
  83. package/dist/debug/components/Panel.js +104 -0
  84. package/dist/debug/components/Stats.d.ts +9 -0
  85. package/dist/debug/components/Stats.js +24 -0
  86. package/dist/debug/debug-chunk.d.ts +26 -0
  87. package/dist/debug/debug-chunk.js +32 -0
  88. package/dist/debug/debug-context.d.ts +41 -0
  89. package/dist/debug/debug-context.js +59 -0
  90. package/dist/debug/index.d.ts +41 -0
  91. package/dist/debug/index.js +43 -0
  92. package/dist/events/LayoutDriver.d.ts +36 -0
  93. package/dist/events/LayoutDriver.js +192 -0
  94. package/dist/events/PointerMissed.d.ts +23 -0
  95. package/dist/events/PointerMissed.js +58 -0
  96. package/dist/events/def-culling.d.ts +46 -0
  97. package/dist/events/def-culling.js +69 -0
  98. package/dist/events/layout-targets.d.ts +151 -0
  99. package/dist/events/layout-targets.js +397 -0
  100. package/dist/events/placement/box.d.ts +59 -0
  101. package/dist/events/placement/box.js +96 -0
  102. package/dist/events/placement/grid.d.ts +161 -0
  103. package/dist/events/placement/grid.js +235 -0
  104. package/dist/events/placement/index.d.ts +2 -0
  105. package/dist/events/placement/index.js +1 -0
  106. package/dist/events/placement/pass.d.ts +70 -0
  107. package/dist/events/placement/pass.js +617 -0
  108. package/dist/events/pointer.d.ts +194 -0
  109. package/dist/events/pointer.js +356 -0
  110. package/dist/external/ExternalCanvas.d.ts +56 -0
  111. package/dist/external/ExternalCanvas.js +177 -0
  112. package/dist/external/external-camera.d.ts +18 -0
  113. package/dist/external/external-camera.js +103 -0
  114. package/dist/external/external-events.d.ts +14 -0
  115. package/dist/external/external-events.js +33 -0
  116. package/dist/index.d.ts +13 -0
  117. package/dist/index.js +14 -0
  118. package/dist/objects/Background.d.ts +31 -0
  119. package/dist/objects/Background.js +37 -0
  120. package/dist/objects/containers/SlotContainer.d.ts +312 -0
  121. package/dist/objects/containers/SlotContainer.js +311 -0
  122. package/dist/objects/containers/hooks.d.ts +61 -0
  123. package/dist/objects/containers/hooks.js +196 -0
  124. package/dist/objects/containers/index.d.ts +6 -0
  125. package/dist/objects/containers/index.js +4 -0
  126. package/dist/objects/containers/keys.d.ts +10 -0
  127. package/dist/objects/containers/keys.js +18 -0
  128. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  129. package/dist/objects/containers/mesh-writes.js +97 -0
  130. package/dist/objects/containers/props.d.ts +33 -0
  131. package/dist/objects/containers/props.js +1 -0
  132. package/dist/objects/images/ImageInstance.d.ts +29 -0
  133. package/dist/objects/images/ImageInstance.js +152 -0
  134. package/dist/objects/images/Images.d.ts +49 -0
  135. package/dist/objects/images/Images.js +394 -0
  136. package/dist/objects/images/atlas-packer.d.ts +129 -0
  137. package/dist/objects/images/atlas-packer.js +287 -0
  138. package/dist/objects/images/image-container.d.ts +74 -0
  139. package/dist/objects/images/image-container.js +100 -0
  140. package/dist/objects/images/index.d.ts +2 -0
  141. package/dist/objects/images/index.js +1 -0
  142. package/dist/objects/images/pack-sources.d.ts +190 -0
  143. package/dist/objects/images/pack-sources.js +272 -0
  144. package/dist/objects/images/texture-budget.d.ts +120 -0
  145. package/dist/objects/images/texture-budget.js +215 -0
  146. package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
  147. package/dist/objects/images/worker/atlas-worker.js +123 -0
  148. package/dist/objects/images/worker/client.d.ts +62 -0
  149. package/dist/objects/images/worker/client.js +249 -0
  150. package/dist/objects/images/worker/index.d.ts +10 -0
  151. package/dist/objects/images/worker/index.js +1 -0
  152. package/dist/objects/layer/Layer.d.ts +28 -0
  153. package/dist/objects/layer/Layer.js +134 -0
  154. package/dist/objects/layer/index.d.ts +2 -0
  155. package/dist/objects/layer/index.js +1 -0
  156. package/dist/objects/layer/layer-context.d.ts +74 -0
  157. package/dist/objects/layer/layer-context.js +82 -0
  158. package/dist/objects/layer/mount-scheduler.d.ts +131 -0
  159. package/dist/objects/layer/mount-scheduler.js +289 -0
  160. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  161. package/dist/objects/layer/visibility-gui.js +93 -0
  162. package/dist/objects/lighting/Lighting.d.ts +92 -0
  163. package/dist/objects/lighting/Lighting.js +384 -0
  164. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  165. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  166. package/dist/objects/lighting/index.d.ts +11 -0
  167. package/dist/objects/lighting/index.js +10 -0
  168. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  169. package/dist/objects/lighting/lighting-gui.js +114 -0
  170. package/dist/objects/lighting/occluder.d.ts +34 -0
  171. package/dist/objects/lighting/occluder.js +62 -0
  172. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  173. package/dist/objects/lighting/occlusion-targets.js +276 -0
  174. package/dist/objects/lines/LineInstance.d.ts +21 -0
  175. package/dist/objects/lines/LineInstance.js +73 -0
  176. package/dist/objects/lines/Lines.d.ts +17 -0
  177. package/dist/objects/lines/Lines.js +31 -0
  178. package/dist/objects/lines/index.d.ts +2 -0
  179. package/dist/objects/lines/index.js +1 -0
  180. package/dist/objects/lines/line-container.d.ts +35 -0
  181. package/dist/objects/lines/line-container.js +40 -0
  182. package/dist/objects/materials/ColorMaterial.d.ts +92 -0
  183. package/dist/objects/materials/ColorMaterial.js +124 -0
  184. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  185. package/dist/objects/materials/LineMaterial.js +74 -0
  186. package/dist/objects/materials/ModelMaterial.d.ts +53 -0
  187. package/dist/objects/materials/ModelMaterial.js +111 -0
  188. package/dist/objects/materials/TextureMaterial.d.ts +33 -0
  189. package/dist/objects/materials/TextureMaterial.js +48 -0
  190. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  191. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  192. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  193. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  194. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  195. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  196. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  197. package/dist/objects/materials/effects/ao/index.js +72 -0
  198. package/dist/objects/materials/effects/color-dim.d.ts +115 -0
  199. package/dist/objects/materials/effects/color-dim.js +206 -0
  200. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  201. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  202. package/dist/objects/materials/effects/effects-context.d.ts +53 -0
  203. package/dist/objects/materials/effects/effects-context.js +24 -0
  204. package/dist/objects/materials/shared.d.ts +24 -0
  205. package/dist/objects/materials/shared.js +72 -0
  206. package/dist/objects/models/ModelInstance.d.ts +22 -0
  207. package/dist/objects/models/ModelInstance.js +33 -0
  208. package/dist/objects/models/Models.d.ts +22 -0
  209. package/dist/objects/models/Models.js +186 -0
  210. package/dist/objects/models/gltf-cache.d.ts +17 -0
  211. package/dist/objects/models/gltf-cache.js +50 -0
  212. package/dist/objects/models/index.d.ts +2 -0
  213. package/dist/objects/models/index.js +1 -0
  214. package/dist/objects/models/model-asset.d.ts +48 -0
  215. package/dist/objects/models/model-asset.js +77 -0
  216. package/dist/objects/models/model-container.d.ts +123 -0
  217. package/dist/objects/models/model-container.js +207 -0
  218. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  219. package/dist/objects/shapes/ShapeInstance.js +39 -0
  220. package/dist/objects/shapes/Shapes.d.ts +21 -0
  221. package/dist/objects/shapes/Shapes.js +131 -0
  222. package/dist/objects/shapes/index.d.ts +2 -0
  223. package/dist/objects/shapes/index.js +1 -0
  224. package/dist/objects/shapes/shape-container.d.ts +141 -0
  225. package/dist/objects/shapes/shape-container.js +242 -0
  226. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  227. package/dist/objects/shapes/shape-geometry.js +94 -0
  228. package/dist/objects/text/TextInstance.d.ts +22 -0
  229. package/dist/objects/text/TextInstance.js +95 -0
  230. package/dist/objects/text/Texts.d.ts +30 -0
  231. package/dist/objects/text/Texts.js +43 -0
  232. package/dist/objects/text/index.d.ts +2 -0
  233. package/dist/objects/text/index.js +1 -0
  234. package/dist/objects/text/text-container.d.ts +121 -0
  235. package/dist/objects/text/text-container.js +229 -0
  236. package/dist/objects/text/text-layout.d.ts +92 -0
  237. package/dist/objects/text/text-layout.js +180 -0
  238. package/dist/space/Camera.d.ts +103 -0
  239. package/dist/space/Camera.js +321 -0
  240. package/dist/space/Scene.d.ts +47 -0
  241. package/dist/space/Scene.js +127 -0
  242. package/dist/space/Space.d.ts +325 -0
  243. package/dist/space/Space.js +359 -0
  244. package/dist/space/VisibleRect.d.ts +45 -0
  245. package/dist/space/VisibleRect.js +129 -0
  246. package/dist/space/camera-handle.d.ts +67 -0
  247. package/dist/space/camera-handle.js +63 -0
  248. package/dist/space/camera-kind.d.ts +26 -0
  249. package/dist/space/camera-kind.js +30 -0
  250. package/dist/space/canvas-size.d.ts +24 -0
  251. package/dist/space/canvas-size.js +20 -0
  252. package/dist/space/clipping.d.ts +69 -0
  253. package/dist/space/clipping.js +130 -0
  254. package/dist/space/layout-measure.d.ts +67 -0
  255. package/dist/space/layout-measure.js +175 -0
  256. package/dist/space/viewport.d.ts +112 -0
  257. package/dist/space/viewport.js +67 -0
  258. package/dist/types/defs.d.ts +234 -0
  259. package/dist/types/defs.js +1 -0
  260. package/dist/types/events.d.ts +198 -0
  261. package/dist/types/events.js +1 -0
  262. package/dist/types/guards.d.ts +21 -0
  263. package/dist/types/guards.js +52 -0
  264. package/dist/update/index.d.ts +3 -0
  265. package/dist/update/index.js +2 -0
  266. package/dist/update/slot-writers.d.ts +30 -0
  267. package/dist/update/slot-writers.js +76 -0
  268. package/dist/update/store.d.ts +41 -0
  269. package/dist/update/store.js +104 -0
  270. package/dist/utils/arrays.d.ts +13 -0
  271. package/dist/utils/arrays.js +20 -0
  272. package/dist/utils/colors.d.ts +19 -0
  273. package/dist/utils/colors.js +27 -0
  274. package/dist/utils/hot-state.d.ts +41 -0
  275. package/dist/utils/hot-state.js +37 -0
  276. package/dist/utils/logging.d.ts +29 -0
  277. package/dist/utils/logging.js +55 -0
  278. package/dist/utils/math.d.ts +52 -0
  279. package/dist/utils/math.js +76 -0
  280. 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
+ `;