@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,77 @@
1
+ import { type IUniform, Matrix4, type Texture, Vector2, Vector3 } from 'three';
2
+ /** The AO pass's uniforms, as {@link createAoPassUniforms} builds them. */
3
+ export type AoPassUniforms = ReturnType<typeof createAoPassUniforms>;
4
+ /**
5
+ * Builds one AO pass's uniforms: its own kernel, its own rotation tile, and the matrices and sizes
6
+ * `OcclusionPasses` refreshes every frame.
7
+ *
8
+ * Per mount rather than per module: a `DataTexture` uploads into whichever GL context first draws
9
+ * with it, and disposal from one mounted scene takes it away from the other. The caller owns the
10
+ * rotation tile's disposal.
11
+ *
12
+ * The frame writes into the matrices and vectors rather than replacing them, so a frame costs no
13
+ * allocation and no shader recompile.
14
+ * @returns The uniform record, ready to hand to a `ShaderMaterial`.
15
+ */
16
+ export declare function createAoPassUniforms(): {
17
+ /** The prepass's depth. The shader unprojects it per tap. `null` until the first pass runs. */
18
+ uDepth: IUniform<Texture | null>;
19
+ /** The rotation tile, which repeats over the target. See {@link createNoiseTexture}. */
20
+ uNoise: IUniform<Texture>;
21
+ /** The guard band's projection, which places a view-space tap back on the depth texture. */
22
+ uProjection: IUniform<Matrix4>;
23
+ /** Its inverse, which turns a stored depth back into a view position. */
24
+ uUnprojection: IUniform<Matrix4>;
25
+ /** One texel of the target in uv, the step the normal reconstruction walks. */
26
+ uTexel: IUniform<Vector2>;
27
+ /** The target's size in rotation tiles, so the tile repeats every {@link NOISE_TILE} texels. */
28
+ uNoiseScale: IUniform<Vector2>;
29
+ /** How far the kernel reaches, in view units. */
30
+ uRadius: IUniform<number>;
31
+ /**
32
+ * How many of the kernel's offsets to walk, from `1` to {@link AO_TAPS}. `OcclusionPasses`
33
+ * rewrites it each frame from `AmbientOcclusion.taps`, the way it rewrites the radius.
34
+ */
35
+ uTaps: IUniform<number>;
36
+ /** The hemisphere kernel around `+z`, which the shader rotates per fragment. */
37
+ uKernel: IUniform<Vector3[]>;
38
+ };
39
+ /** How many taps the kernel holds, which is the AO pass loop's bound and its default count. */
40
+ export declare const AO_TAPS = 16;
41
+ /**
42
+ * The rotation tile's edge in texels, which is the blur pass's kernel bound and its default side.
43
+ *
44
+ * One decision with the blur kernel rather than two: a `4 × 4` tile needs a `4 × 4` kernel to
45
+ * average out. The GUI reaches a smaller kernel, which trades structured noise for the fetches it
46
+ * skips.
47
+ */
48
+ export declare const NOISE_TILE = 4;
49
+ /**
50
+ * Both full-screen passes' vertex stage: a clip-space quad, the way `Background` draws one.
51
+ *
52
+ * `position.xy` of a `2 × 2` plane already spans clip space, so the stage reads no matrix and the
53
+ * camera means nothing. `z` sits at `0` rather than the far plane, because the passes draw with no
54
+ * depth test.
55
+ */
56
+ export declare const AO_VERTEX_SHADER = "\n varying vec2 vUv;\n\n void main() {\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n }\n";
57
+ /**
58
+ * The AO pass's fragment stage: a hemisphere-kernel occlusion pass, with the one substitution a
59
+ * floor plan renderer forces.
60
+ *
61
+ * There is no G-buffer. The pass unprojects the fragment's own view position from the prepass
62
+ * depth, and `normalAt` reconstructs the normal from that same depth.
63
+ *
64
+ * Every position and length below sits in view space, which is where three's own lighting works and
65
+ * where this pass's inverse projection lands. View space looks down `-z`, so a larger `z` is nearer
66
+ * the camera, and `occlusionTap` compares depths that way.
67
+ *
68
+ * The pass writes occlusion, not visibility: `0` is open, `1` is closed, raw and unblurred. A pass
69
+ * of its own smooths it, see `BLUR_FRAGMENT_SHADER`.
70
+ *
71
+ * `AO_BIAS` is the slack that keeps a tap on the fragment's own flat face from reading as an
72
+ * occluder. It is a share of the radius, so it holds at any zoom. A tap keeps its full value until
73
+ * the surface it found is a radius away in depth, then falls to nothing. That keeps a wall from
74
+ * darkening whatever stands far behind it.
75
+ */
76
+ export declare const AO_FRAGMENT_SHADER = "\n uniform sampler2D uDepth;\n uniform sampler2D uNoise;\n uniform mat4 uProjection;\n uniform mat4 uUnprojection;\n uniform vec2 uTexel;\n uniform vec2 uNoiseScale;\n uniform float uRadius;\n uniform int uTaps;\n uniform vec3 uKernel[16];\n varying vec2 vUv;\n\n const float AO_BIAS = 0.02;\n\n // Where the grazing fade starts and ends, as the absolute dot of the normal against the view\n // direction: fully faded about seventeen degrees from edge on, fully open by about forty. See the\n // fade at the end of main.\n const float AO_GRAZING_MIN = 0.30;\n const float AO_GRAZING_MAX = 0.64;\n\n // How far apart the two neighbors of an axis have to be before blendAxis takes the nearer one\n // outright. Measured as a share of their sum. Larger crosses over more gently and softens a\n // silhouette. Smaller sharpens the silhouette and brings the normal jump back.\n const float AO_CROSSOVER = 0.45;\n\n float depthAt(vec2 uv) {\n return texture2D(uDepth, uv).x;\n }\n\n vec3 viewAt(vec2 uv, float depth) {\n vec4 clip = vec4(uv * 2. - 1., depth * 2. - 1., 1.);\n vec4 view = uUnprojection * clip;\n return view.xyz / view.w;\n }\n\n vec3 blendAxis(vec3 back, vec3 forward, float backApart, float forwardApart) {\n // Positive when the backward neighbor is the worse of the two, which moves the result toward\n // the forward one. Normalized by the sum, so AO_CROSSOVER is a share rather than a depth.\n float ratio = (backApart - forwardApart) / max(backApart + forwardApart, 1e-6);\n return mix(back, forward, smoothstep(-AO_CROSSOVER, AO_CROSSOVER, ratio));\n }\n\n vec3 normalAt(vec2 uv, vec3 here, float depth) {\n vec2 stepX = vec2(uTexel.x, 0.);\n vec2 stepY = vec2(0., uTexel.y);\n float left = depthAt(uv - stepX);\n float right = depthAt(uv + stepX);\n float down = depthAt(uv - stepY);\n float up = depthAt(uv + stepY);\n\n // blendAxis leans toward the neighbor that stays on this surface rather than switching to it. A\n // hard pick makes a face turned side on jump: both depth differences are large and nearly equal\n // there, so a small camera rotation crosses the comparison and the normal jumps with it. The\n // smoothstep over their difference crosses that middle continuously.\n vec3 alongX = blendAxis(here - viewAt(uv - stepX, left), viewAt(uv + stepX, right) - here,\n abs(depth - left), abs(right - depth));\n vec3 alongY = blendAxis(here - viewAt(uv - stepY, down), viewAt(uv + stepY, up) - here,\n abs(depth - down), abs(up - depth));\n\n vec3 normal = normalize(cross(alongX, alongY));\n // A surface the camera sees faces the camera, and view space looks down -z. The flip matters in\n // external mode, where the host's projection can mirror an axis and turn the cross product with\n // it. The wrong side samples the hemisphere behind the surface.\n return normal.z < 0. ? -normal : normal;\n }\n\n float occlusionTap(vec3 here, vec3 at) {\n vec4 clip = uProjection * vec4(at, 1.);\n // At or behind the eye, where the perspective divide has no answer. Orthographic keeps w at 1,\n // so this costs a comparison there and nothing else.\n if (clip.w <= 0.) return 0.;\n vec2 uv = clip.xy / clip.w * .5 + .5;\n // Screen space sees only what is on screen. A tap off the edge has no depth of its own, and\n // clamped sampling answers with the border texel's depth, so the tap contributes nothing.\n if (uv.x < 0. || uv.x > 1. || uv.y < 0. || uv.y > 1.) return 0.;\n float stored = depthAt(uv);\n // The prepass clears to the far plane, so a tap over open background lands there. This rejects\n // that tap rather than darkening an edge.\n if (stored >= 1.) return 0.;\n float storedZ = viewAt(uv, stored).z;\n // Nothing in front of the tap: this surface is open in that direction.\n if (storedZ < at.z + uRadius * AO_BIAS) return 0.;\n return smoothstep(0., 1., uRadius / abs(here.z - storedZ));\n }\n\n void main() {\n float depth = depthAt(vUv);\n // Nothing here, or nothing to reach with. The prepass clears to the far plane. A radius of zero\n // puts every tap on the fragment itself, where the divide by its distance has no answer.\n if (depth >= 1. || uRadius <= 0.) {\n gl_FragColor = vec4(0., 0., 0., 1.);\n return;\n }\n\n vec3 here = viewAt(vUv, depth);\n vec3 normal = normalAt(vUv, here, depth);\n\n // The tile's own rotation, turned into a basis around the normal. One kernel becomes a\n // different kernel per pixel, with no second kernel to upload.\n vec3 rotation = vec3(texture2D(uNoise, vUv * uNoiseScale).xy, 0.);\n vec3 tangent = cross(normal, rotation);\n // Degenerate only where the rotation lies along the normal, which is a wall seen edge on. The\n // fallback is degenerate only where the normal is the view axis, the case this one covers, so\n // between them there is always a tangent.\n if (dot(tangent, tangent) < 1e-6) tangent = cross(normal, vec3(0., 0., 1.));\n tangent = normalize(tangent);\n mat3 orient = mat3(tangent, cross(normal, tangent), normal);\n\n float occluded = 0.;\n for (int tap = 0; tap < 16; tap++) {\n // GLSL ES 1.00 on the old Android WebViews this SDK reaches needs a constant loop bound, and\n // the break is what skips the taps.\n if (tap >= uTaps) break;\n occluded += occlusionTap(here, here + orient * uKernel[tap] * uRadius);\n }\n\n // The average divides by what the loop walked, not by what the kernel holds. The floor stops a\n // tap count of zero from dividing by nothing. No control offers zero, and a stale upload can\n // still hold it.\n float term = occluded / max(float(uTaps), 1.);\n\n // The fade drops the term where the surface turns edge on to the camera, because the normal\n // that oriented this kernel stops being reliable there. A face turned nearly side on gives\n // normalAt two neighbors that disagree by almost the same amount. A small camera rotation then\n // still moves the normal, and the term moves with it.\n //\n // The fragment sits in view space here, where the camera is the origin, so its own direction is\n // the view vector. The dot of that against the normal says how square on the surface is: one\n // face on, zero edge on. Taken as an absolute value, since a normal reconstructed at a depth\n // discontinuity can come back pointing away.\n //\n // An ordinary wall under this plan's camera reads about 0.82 square on at a 55 degree pitch.\n // That is well clear of the band, so walls keep their full term.\n float facing = abs(dot(normal, normalize(here)));\n gl_FragColor = vec4(term * smoothstep(AO_GRAZING_MIN, AO_GRAZING_MAX, facing), 0., 0., 1.);\n }\n";
77
+ //# sourceMappingURL=ambient-occlusion-pass.d.ts.map
@@ -0,0 +1,296 @@
1
+ import { DataTexture, FloatType, Matrix4, NearestFilter, RepeatWrapping, RGBAFormat, Vector2, Vector3, } from 'three';
2
+ /**
3
+ * Builds one AO pass's uniforms: its own kernel, its own rotation tile, and the matrices and sizes
4
+ * `OcclusionPasses` refreshes every frame.
5
+ *
6
+ * Per mount rather than per module: a `DataTexture` uploads into whichever GL context first draws
7
+ * with it, and disposal from one mounted scene takes it away from the other. The caller owns the
8
+ * rotation tile's disposal.
9
+ *
10
+ * The frame writes into the matrices and vectors rather than replacing them, so a frame costs no
11
+ * allocation and no shader recompile.
12
+ * @returns The uniform record, ready to hand to a `ShaderMaterial`.
13
+ */
14
+ export function createAoPassUniforms() {
15
+ return {
16
+ /** The prepass's depth. The shader unprojects it per tap. `null` until the first pass runs. */
17
+ uDepth: { value: null },
18
+ /** The rotation tile, which repeats over the target. See {@link createNoiseTexture}. */
19
+ uNoise: { value: createNoiseTexture() },
20
+ /** The guard band's projection, which places a view-space tap back on the depth texture. */
21
+ uProjection: { value: new Matrix4() },
22
+ /** Its inverse, which turns a stored depth back into a view position. */
23
+ uUnprojection: { value: new Matrix4() },
24
+ /** One texel of the target in uv, the step the normal reconstruction walks. */
25
+ uTexel: { value: new Vector2(1, 1) },
26
+ /** The target's size in rotation tiles, so the tile repeats every {@link NOISE_TILE} texels. */
27
+ uNoiseScale: { value: new Vector2(1, 1) },
28
+ /** How far the kernel reaches, in view units. */
29
+ uRadius: { value: 1 },
30
+ /**
31
+ * How many of the kernel's offsets to walk, from `1` to {@link AO_TAPS}. `OcclusionPasses`
32
+ * rewrites it each frame from `AmbientOcclusion.taps`, the way it rewrites the radius.
33
+ */
34
+ uTaps: { value: AO_TAPS },
35
+ /** The hemisphere kernel around `+z`, which the shader rotates per fragment. */
36
+ uKernel: { value: createKernel() },
37
+ };
38
+ }
39
+ /**
40
+ * Builds the rotation tile: {@link NOISE_TILE} squared texels, each a unit vector in the tangent
41
+ * plane, `RGBA` floats at {@link NearestFilter} and {@link RepeatWrapping}. 256 bytes, and no asset
42
+ * ships with it.
43
+ *
44
+ * A texture rather than a hash of `gl_FragCoord`, for two reasons. The tile size and the blur
45
+ * kernel have to match: a `4 × 4` blur averages a `4 × 4` tile repeated over the screen. A hash
46
+ * gives every pixel its own rotation, and no fixed kernel averages that. And a hash leans on
47
+ * high-precision float arithmetic, which some drivers on the old Android WebViews this SDK reaches
48
+ * run at `mediump`.
49
+ *
50
+ * The angles step by the golden ratio's conjugate, so two neighboring texels sit about `222°`
51
+ * apart. Deterministic, because a screenshot baseline has to reproduce.
52
+ * @returns The tile, ready to upload.
53
+ */
54
+ function createNoiseTexture() {
55
+ const texels = NOISE_TILE * NOISE_TILE;
56
+ const data = new Float32Array(texels * RGBA_CHANNELS);
57
+ for (let texel = 0; texel < texels; texel++) {
58
+ const angle = 2 * Math.PI * fraction(texel * GOLDEN_CONJUGATE);
59
+ data[texel * RGBA_CHANNELS] = Math.cos(angle);
60
+ data[texel * RGBA_CHANNELS + 1] = Math.sin(angle);
61
+ data[texel * RGBA_CHANNELS + 2] = 0;
62
+ data[texel * RGBA_CHANNELS + 3] = 1;
63
+ }
64
+ const noise = new DataTexture(data, NOISE_TILE, NOISE_TILE, RGBAFormat, FloatType);
65
+ noise.minFilter = NearestFilter;
66
+ noise.magFilter = NearestFilter;
67
+ noise.wrapS = RepeatWrapping;
68
+ noise.wrapT = RepeatWrapping;
69
+ noise.needsUpdate = true;
70
+ return noise;
71
+ }
72
+ /**
73
+ * Builds the hemisphere kernel: {@link AO_TAPS} offsets around `+z`, in view units of the radius.
74
+ *
75
+ * The directions come from the two-dimensional Roberts sequence, whose two irrational strides keep
76
+ * the pair of coordinates low-discrepancy. One stride drives the azimuth and the other the
77
+ * elevation's cosine, and the sequence's own half-step offset keeps the first tap off the tangent
78
+ * plane. The lengths climb with the index as a square, which clusters the taps near the fragment
79
+ * where an occluder matters most.
80
+ *
81
+ * Generated rather than tabulated, so the count and the pattern cannot drift apart. Deterministic,
82
+ * so a screenshot baseline reproduces.
83
+ * @returns The kernel, in the order the shader walks it.
84
+ */
85
+ function createKernel() {
86
+ const kernel = [];
87
+ for (let tap = 0; tap < AO_TAPS; tap++) {
88
+ const azimuth = 2 * Math.PI * fraction(SEQUENCE_OFFSET + tap * ROBERTS_X);
89
+ const elevation = fraction(SEQUENCE_OFFSET + tap * ROBERTS_Y);
90
+ const ring = Math.sqrt(1 - elevation * elevation);
91
+ const length = KERNEL_FLOOR + (1 - KERNEL_FLOOR) * ((tap + 1) / AO_TAPS) ** 2;
92
+ kernel.push(new Vector3(Math.cos(azimuth) * ring * length, Math.sin(azimuth) * ring * length, elevation * length));
93
+ }
94
+ return kernel;
95
+ }
96
+ /**
97
+ * The fractional part of a number, which turns a low-discrepancy stride into a sequence.
98
+ * @param value - The number to take the fraction of.
99
+ * @returns Its fractional part, in `[0, 1)`.
100
+ */
101
+ function fraction(value) {
102
+ return value - Math.floor(value);
103
+ }
104
+ /** How many taps the kernel holds, which is the AO pass loop's bound and its default count. */
105
+ export const AO_TAPS = 16;
106
+ /**
107
+ * The rotation tile's edge in texels, which is the blur pass's kernel bound and its default side.
108
+ *
109
+ * One decision with the blur kernel rather than two: a `4 × 4` tile needs a `4 × 4` kernel to
110
+ * average out. The GUI reaches a smaller kernel, which trades structured noise for the fetches it
111
+ * skips.
112
+ */
113
+ export const NOISE_TILE = 4;
114
+ /**
115
+ * Both full-screen passes' vertex stage: a clip-space quad, the way `Background` draws one.
116
+ *
117
+ * `position.xy` of a `2 × 2` plane already spans clip space, so the stage reads no matrix and the
118
+ * camera means nothing. `z` sits at `0` rather than the far plane, because the passes draw with no
119
+ * depth test.
120
+ */
121
+ export const AO_VERTEX_SHADER = /* glsl */ `
122
+ varying vec2 vUv;
123
+
124
+ void main() {
125
+ vUv = uv;
126
+ gl_Position = vec4(position.xy, 0.0, 1.0);
127
+ }
128
+ `;
129
+ /**
130
+ * The AO pass's fragment stage: a hemisphere-kernel occlusion pass, with the one substitution a
131
+ * floor plan renderer forces.
132
+ *
133
+ * There is no G-buffer. The pass unprojects the fragment's own view position from the prepass
134
+ * depth, and `normalAt` reconstructs the normal from that same depth.
135
+ *
136
+ * Every position and length below sits in view space, which is where three's own lighting works and
137
+ * where this pass's inverse projection lands. View space looks down `-z`, so a larger `z` is nearer
138
+ * the camera, and `occlusionTap` compares depths that way.
139
+ *
140
+ * The pass writes occlusion, not visibility: `0` is open, `1` is closed, raw and unblurred. A pass
141
+ * of its own smooths it, see `BLUR_FRAGMENT_SHADER`.
142
+ *
143
+ * `AO_BIAS` is the slack that keeps a tap on the fragment's own flat face from reading as an
144
+ * occluder. It is a share of the radius, so it holds at any zoom. A tap keeps its full value until
145
+ * the surface it found is a radius away in depth, then falls to nothing. That keeps a wall from
146
+ * darkening whatever stands far behind it.
147
+ */
148
+ export const AO_FRAGMENT_SHADER = /* glsl */ `
149
+ uniform sampler2D uDepth;
150
+ uniform sampler2D uNoise;
151
+ uniform mat4 uProjection;
152
+ uniform mat4 uUnprojection;
153
+ uniform vec2 uTexel;
154
+ uniform vec2 uNoiseScale;
155
+ uniform float uRadius;
156
+ uniform int uTaps;
157
+ uniform vec3 uKernel[${AO_TAPS}];
158
+ varying vec2 vUv;
159
+
160
+ const float AO_BIAS = 0.02;
161
+
162
+ // Where the grazing fade starts and ends, as the absolute dot of the normal against the view
163
+ // direction: fully faded about seventeen degrees from edge on, fully open by about forty. See the
164
+ // fade at the end of main.
165
+ const float AO_GRAZING_MIN = 0.30;
166
+ const float AO_GRAZING_MAX = 0.64;
167
+
168
+ // How far apart the two neighbors of an axis have to be before blendAxis takes the nearer one
169
+ // outright. Measured as a share of their sum. Larger crosses over more gently and softens a
170
+ // silhouette. Smaller sharpens the silhouette and brings the normal jump back.
171
+ const float AO_CROSSOVER = 0.45;
172
+
173
+ float depthAt(vec2 uv) {
174
+ return texture2D(uDepth, uv).x;
175
+ }
176
+
177
+ vec3 viewAt(vec2 uv, float depth) {
178
+ vec4 clip = vec4(uv * 2. - 1., depth * 2. - 1., 1.);
179
+ vec4 view = uUnprojection * clip;
180
+ return view.xyz / view.w;
181
+ }
182
+
183
+ vec3 blendAxis(vec3 back, vec3 forward, float backApart, float forwardApart) {
184
+ // Positive when the backward neighbor is the worse of the two, which moves the result toward
185
+ // the forward one. Normalized by the sum, so AO_CROSSOVER is a share rather than a depth.
186
+ float ratio = (backApart - forwardApart) / max(backApart + forwardApart, 1e-6);
187
+ return mix(back, forward, smoothstep(-AO_CROSSOVER, AO_CROSSOVER, ratio));
188
+ }
189
+
190
+ vec3 normalAt(vec2 uv, vec3 here, float depth) {
191
+ vec2 stepX = vec2(uTexel.x, 0.);
192
+ vec2 stepY = vec2(0., uTexel.y);
193
+ float left = depthAt(uv - stepX);
194
+ float right = depthAt(uv + stepX);
195
+ float down = depthAt(uv - stepY);
196
+ float up = depthAt(uv + stepY);
197
+
198
+ // blendAxis leans toward the neighbor that stays on this surface rather than switching to it. A
199
+ // hard pick makes a face turned side on jump: both depth differences are large and nearly equal
200
+ // there, so a small camera rotation crosses the comparison and the normal jumps with it. The
201
+ // smoothstep over their difference crosses that middle continuously.
202
+ vec3 alongX = blendAxis(here - viewAt(uv - stepX, left), viewAt(uv + stepX, right) - here,
203
+ abs(depth - left), abs(right - depth));
204
+ vec3 alongY = blendAxis(here - viewAt(uv - stepY, down), viewAt(uv + stepY, up) - here,
205
+ abs(depth - down), abs(up - depth));
206
+
207
+ vec3 normal = normalize(cross(alongX, alongY));
208
+ // A surface the camera sees faces the camera, and view space looks down -z. The flip matters in
209
+ // external mode, where the host's projection can mirror an axis and turn the cross product with
210
+ // it. The wrong side samples the hemisphere behind the surface.
211
+ return normal.z < 0. ? -normal : normal;
212
+ }
213
+
214
+ float occlusionTap(vec3 here, vec3 at) {
215
+ vec4 clip = uProjection * vec4(at, 1.);
216
+ // At or behind the eye, where the perspective divide has no answer. Orthographic keeps w at 1,
217
+ // so this costs a comparison there and nothing else.
218
+ if (clip.w <= 0.) return 0.;
219
+ vec2 uv = clip.xy / clip.w * .5 + .5;
220
+ // Screen space sees only what is on screen. A tap off the edge has no depth of its own, and
221
+ // clamped sampling answers with the border texel's depth, so the tap contributes nothing.
222
+ if (uv.x < 0. || uv.x > 1. || uv.y < 0. || uv.y > 1.) return 0.;
223
+ float stored = depthAt(uv);
224
+ // The prepass clears to the far plane, so a tap over open background lands there. This rejects
225
+ // that tap rather than darkening an edge.
226
+ if (stored >= 1.) return 0.;
227
+ float storedZ = viewAt(uv, stored).z;
228
+ // Nothing in front of the tap: this surface is open in that direction.
229
+ if (storedZ < at.z + uRadius * AO_BIAS) return 0.;
230
+ return smoothstep(0., 1., uRadius / abs(here.z - storedZ));
231
+ }
232
+
233
+ void main() {
234
+ float depth = depthAt(vUv);
235
+ // Nothing here, or nothing to reach with. The prepass clears to the far plane. A radius of zero
236
+ // puts every tap on the fragment itself, where the divide by its distance has no answer.
237
+ if (depth >= 1. || uRadius <= 0.) {
238
+ gl_FragColor = vec4(0., 0., 0., 1.);
239
+ return;
240
+ }
241
+
242
+ vec3 here = viewAt(vUv, depth);
243
+ vec3 normal = normalAt(vUv, here, depth);
244
+
245
+ // The tile's own rotation, turned into a basis around the normal. One kernel becomes a
246
+ // different kernel per pixel, with no second kernel to upload.
247
+ vec3 rotation = vec3(texture2D(uNoise, vUv * uNoiseScale).xy, 0.);
248
+ vec3 tangent = cross(normal, rotation);
249
+ // Degenerate only where the rotation lies along the normal, which is a wall seen edge on. The
250
+ // fallback is degenerate only where the normal is the view axis, the case this one covers, so
251
+ // between them there is always a tangent.
252
+ if (dot(tangent, tangent) < 1e-6) tangent = cross(normal, vec3(0., 0., 1.));
253
+ tangent = normalize(tangent);
254
+ mat3 orient = mat3(tangent, cross(normal, tangent), normal);
255
+
256
+ float occluded = 0.;
257
+ for (int tap = 0; tap < ${AO_TAPS}; tap++) {
258
+ // GLSL ES 1.00 on the old Android WebViews this SDK reaches needs a constant loop bound, and
259
+ // the break is what skips the taps.
260
+ if (tap >= uTaps) break;
261
+ occluded += occlusionTap(here, here + orient * uKernel[tap] * uRadius);
262
+ }
263
+
264
+ // The average divides by what the loop walked, not by what the kernel holds. The floor stops a
265
+ // tap count of zero from dividing by nothing. No control offers zero, and a stale upload can
266
+ // still hold it.
267
+ float term = occluded / max(float(uTaps), 1.);
268
+
269
+ // The fade drops the term where the surface turns edge on to the camera, because the normal
270
+ // that oriented this kernel stops being reliable there. A face turned nearly side on gives
271
+ // normalAt two neighbors that disagree by almost the same amount. A small camera rotation then
272
+ // still moves the normal, and the term moves with it.
273
+ //
274
+ // The fragment sits in view space here, where the camera is the origin, so its own direction is
275
+ // the view vector. The dot of that against the normal says how square on the surface is: one
276
+ // face on, zero edge on. Taken as an absolute value, since a normal reconstructed at a depth
277
+ // discontinuity can come back pointing away.
278
+ //
279
+ // An ordinary wall under this plan's camera reads about 0.82 square on at a 55 degree pitch.
280
+ // That is well clear of the band, so walls keep their full term.
281
+ float facing = abs(dot(normal, normalize(here)));
282
+ gl_FragColor = vec4(term * smoothstep(AO_GRAZING_MIN, AO_GRAZING_MAX, facing), 0., 0., 1.);
283
+ }
284
+ `;
285
+ /** Channels one `RGBA` texel carries. */
286
+ const RGBA_CHANNELS = 4;
287
+ /** The golden ratio's conjugate, whose multiples are the rotation tile's low-discrepancy angles. */
288
+ const GOLDEN_CONJUGATE = 0.6180339887498949;
289
+ /** The sequence's own half-step offset, which both Roberts strides count from. */
290
+ const SEQUENCE_OFFSET = 0.5;
291
+ /** The two-dimensional Roberts sequence's first stride, which drives the kernel's azimuth. */
292
+ const ROBERTS_X = 0.7548776662466927;
293
+ /** Its second stride, which drives the kernel's elevation. */
294
+ const ROBERTS_Y = 0.5698402909980532;
295
+ /** The shortest kernel offset, as a share of the radius, so no tap lands on its own texel. */
296
+ const KERNEL_FLOOR = 0.1;
@@ -0,0 +1,29 @@
1
+ import { type WebGLProgramParametersWithUniforms } from 'three';
2
+ import { type ContactShadow } from '../contact-shadow';
3
+ import { type AmbientOcclusion } from './index';
4
+ /**
5
+ * Installs this term's read into a lit shader about to be compiled: the upsample and the blend, in
6
+ * two texture fetches.
7
+ *
8
+ * The blur is not here. It is a pass of its own, see `BLUR_FRAGMENT_SHADER`, and that pass bills
9
+ * per texel of a quarter-area target. Everything in this function bills per lit fragment at full
10
+ * resolution and once per overdraw. A kernel folded in here costs `2 · side²` fetches a fragment.
11
+ *
12
+ * The depth fetch stays. Translucent content draws into no prepass, so no stored depth agrees with
13
+ * it, the weight falls to zero, and the term leaves it alone. The main pass binds no framebuffer
14
+ * that carries the depth texture, so the sample is legal. Two of the three occlusion targets carry
15
+ * no depth attachment for that reason.
16
+ *
17
+ * The term lands at the anchor the contact shadow and the dim patch use, above three's output
18
+ * chain. It mixes toward the shade the contact shadow blends toward, so a plan has one shadow
19
+ * color. three has a line where occlusion physically belongs, `#include <aomap_fragment>`, and the
20
+ * design spec's D8 records why it loses here: it multiplies a light term rather than mixing toward
21
+ * a color, and the fill it scales is a minority of a lit wall's light.
22
+ * @param parameters - The shader being compiled.
23
+ * @param occlusion - The mount's own effect state, whose uniform objects the compiled program
24
+ * captures. The shade is the contact shadow's, because the two terms share one.
25
+ * @throws {Error} If the shader has no output chain, which leaves the term silently absent from a
26
+ * material that asked for it.
27
+ */
28
+ export declare function applyAmbientOcclusion(parameters: WebGLProgramParametersWithUniforms, occlusion: AmbientOcclusion & ContactShadow): void;
29
+ //# sourceMappingURL=ambient-occlusion-patch.d.ts.map
@@ -0,0 +1,115 @@
1
+ import { declareBeforeMain, sceneColorAnchor } from '../../../../core/shader-source';
2
+ import { AO_EDGE_TOLERANCE } from './blur-pass';
3
+ /**
4
+ * Installs this term's read into a lit shader about to be compiled: the upsample and the blend, in
5
+ * two texture fetches.
6
+ *
7
+ * The blur is not here. It is a pass of its own, see `BLUR_FRAGMENT_SHADER`, and that pass bills
8
+ * per texel of a quarter-area target. Everything in this function bills per lit fragment at full
9
+ * resolution and once per overdraw. A kernel folded in here costs `2 · side²` fetches a fragment.
10
+ *
11
+ * The depth fetch stays. Translucent content draws into no prepass, so no stored depth agrees with
12
+ * it, the weight falls to zero, and the term leaves it alone. The main pass binds no framebuffer
13
+ * that carries the depth texture, so the sample is legal. Two of the three occlusion targets carry
14
+ * no depth attachment for that reason.
15
+ *
16
+ * The term lands at the anchor the contact shadow and the dim patch use, above three's output
17
+ * chain. It mixes toward the shade the contact shadow blends toward, so a plan has one shadow
18
+ * color. three has a line where occlusion physically belongs, `#include <aomap_fragment>`, and the
19
+ * design spec's D8 records why it loses here: it multiplies a light term rather than mixing toward
20
+ * a color, and the fill it scales is a minority of a lit wall's light.
21
+ * @param parameters - The shader being compiled.
22
+ * @param occlusion - The mount's own effect state, whose uniform objects the compiled program
23
+ * captures. The shade is the contact shadow's, because the two terms share one.
24
+ * @throws {Error} If the shader has no output chain, which leaves the term silently absent from a
25
+ * material that asked for it.
26
+ */
27
+ export function applyAmbientOcclusion(parameters, occlusion) {
28
+ // Already installed, which is how a repeated compile stays harmless.
29
+ if (parameters.fragmentShader.includes(AO_FUNCTION))
30
+ return;
31
+ const anchor = sceneColorAnchor(parameters.fragmentShader);
32
+ if (!anchor) {
33
+ throw new Error('applyAmbientOcclusion: the shader has no output chain to darken before.');
34
+ }
35
+ // The rewrite runs before any write, so this either installs the term or leaves the parameters as
36
+ // it found them. `declareBeforeMain` throws on a shader with no `main`, and a caller that carries
37
+ // on must not hold seven bindings for a program nothing patched.
38
+ const fragmentShader = declareBeforeMain(parameters.fragmentShader, AO_LIT_DEFS).replace(anchor, `${AO_LIT_APPLY}\n${anchor}`);
39
+ parameters.uniforms[AO_MAP] = occlusion.aoMap;
40
+ parameters.uniforms[AO_DEPTH] = occlusion.aoDepth;
41
+ parameters.uniforms[AO_ACTIVE] = occlusion.aoActive;
42
+ parameters.uniforms[AO_GUARD] = occlusion.aoGuard;
43
+ parameters.uniforms[AO_STRENGTH] = occlusion.aoStrength;
44
+ parameters.uniforms[AO_VIEWPORT] = occlusion.aoViewport;
45
+ // The contact shadow's own object, under a name of this term's. One shade for both terms, and no
46
+ // GLSL contract between two patches about who declares it.
47
+ parameters.uniforms[AO_COLOR] = occlusion.shadowColor;
48
+ parameters.fragmentShader = fragmentShader;
49
+ }
50
+ /** The uniform carrying the smoothed occlusion the blur pass wrote. */
51
+ const AO_MAP = 'uAoMap';
52
+ /** The uniform carrying the prepass's depth, which weights the one sample this read takes. */
53
+ const AO_DEPTH = 'uAoDepth';
54
+ /** The uniform that gates the read, `1` while the passes drew this frame. */
55
+ const AO_ACTIVE = 'uAoActive';
56
+ /** The uniform carrying the guard band, which turns `gl_FragCoord` into a uv. */
57
+ const AO_GUARD = 'uAoGuard';
58
+ /** The uniform carrying how far an occluded fragment blends toward the shade. */
59
+ const AO_STRENGTH = 'uAoStrength';
60
+ /** The uniform carrying the drawing buffer's size, the unit `gl_FragCoord` counts in. */
61
+ const AO_VIEWPORT = 'uAoViewport';
62
+ /** The uniform carrying the shade to blend toward, which is the contact shadow's own. */
63
+ const AO_COLOR = 'uAoColor';
64
+ /** The signature {@link applyAmbientOcclusion} looks for in an already patched shader. */
65
+ const AO_FUNCTION = 'vec3 ambientOcclusion(';
66
+ /**
67
+ * The lit-stage declarations: this term's uniforms, and the read that upsamples and blends in two
68
+ * texture fetches.
69
+ *
70
+ * The fragment's own depth is `gl_FragCoord.z` rather than another read of the depth texture. That
71
+ * is exact, where a read of a half-resolution texture is an approximation. The comparison buys
72
+ * translucent content: it drew into no prepass, so the stored depth disagrees, the weight falls to
73
+ * zero, and the term leaves it alone. The prepass keeps the same rule on the casting side.
74
+ */
75
+ const AO_LIT_DEFS = /* glsl */ `
76
+ uniform sampler2D ${AO_MAP};
77
+ uniform sampler2D ${AO_DEPTH};
78
+ uniform float ${AO_ACTIVE};
79
+ uniform float ${AO_GUARD};
80
+ uniform float ${AO_STRENGTH};
81
+ uniform vec2 ${AO_VIEWPORT};
82
+ uniform vec3 ${AO_COLOR};
83
+
84
+ const float AO_EDGE = ${AO_EDGE_TOLERANCE};
85
+
86
+ ${AO_FUNCTION}vec3 rgb, float depth) {
87
+ // A frame that skipped the passes shades correctly rather than sampling a stale texture.
88
+ if (${AO_ACTIVE} < .5) return rgb;
89
+
90
+ // The guard band undone. Every pass ran on a frustum (1 + g) wider than this one. This
91
+ // fragment's place in the AO texture is its ndc shrunk by that factor.
92
+ vec2 uv = ((gl_FragCoord.xy / ${AO_VIEWPORT}) * 2. - 1.) / (1. + ${AO_GUARD}) * .5 + .5;
93
+
94
+ // Whether the term measured the surface this fragment shades. Stored depth is not linear, so
95
+ // the difference is a share of the range still ahead of this fragment.
96
+ float stored = texture2D(${AO_DEPTH}, uv).x;
97
+ float apart = abs(stored - depth) / max(1. - depth, 1e-6);
98
+ float weight = 1. - smoothstep(0., AO_EDGE, apart);
99
+ // Nothing here describes the surface this fragment is on, a translucent one most of all. There
100
+ // is no term to apply and no second fetch worth taking.
101
+ if (weight <= 0.) return rgb;
102
+
103
+ return mix(rgb, ${AO_COLOR}, ${AO_STRENGTH} * texture2D(${AO_MAP}, uv).r * weight);
104
+ }
105
+ `;
106
+ /**
107
+ * The call that applies the term while the color is still linear and scene-referred, after the
108
+ * contact shadow and before the dim patch.
109
+ *
110
+ * The call is spelled out rather than built from {@link AO_FUNCTION}, which stays the
111
+ * already-patched detector and nothing else.
112
+ */
113
+ const AO_LIT_APPLY = /* glsl */ `
114
+ gl_FragColor.rgb = ambientOcclusion(gl_FragColor.rgb, gl_FragCoord.z);
115
+ `;
@@ -0,0 +1,61 @@
1
+ import { type IUniform, type Texture, Vector2 } from 'three';
2
+ import { type AmbientOcclusion } from './index';
3
+ /** The blur pass's uniforms, as {@link createBlurPassUniforms} builds them. */
4
+ export type BlurPassUniforms = ReturnType<typeof createBlurPassUniforms>;
5
+ /**
6
+ * Builds one blur pass's uniforms: the two textures it reads, its own texel step, and the scene's
7
+ * own kernel side.
8
+ *
9
+ * Per mount rather than per module, for the reason `createAoPassUniforms` is. These carry textures
10
+ * allocated in whichever GL context drew them, and a second mounted scene has a second set.
11
+ *
12
+ * {@link AmbientOcclusion.aoBlur} arrives by identity rather than by copy, so the debug GUI's write
13
+ * lands in this shader with nothing in between. The frame writes into everything else in place, so
14
+ * a frame costs no allocation and no shader recompile.
15
+ * @param occlusion - The mounted scene's own occlusion state, for the kernel side the debug GUI
16
+ * tunes.
17
+ * @returns The uniform record, ready to hand to a `ShaderMaterial`.
18
+ */
19
+ export declare function createBlurPassUniforms(occlusion: AmbientOcclusion): {
20
+ /** The raw term the AO pass wrote, which this pass smooths. `null` until the AO pass runs. */
21
+ uOcclusion: IUniform<Texture | null>;
22
+ /** The prepass's depth, which weights every tap. `null` until the prepass runs. */
23
+ uDepth: IUniform<Texture | null>;
24
+ /** One texel of this pass's own target in uv, the step the kernel walks. */
25
+ uTexel: IUniform<Vector2>;
26
+ /** The kernel's side in texels, the scene's own object. See {@link AmbientOcclusion.aoBlur}. */
27
+ uBlur: IUniform<number>;
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 declare 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 declare const BLUR_FRAGMENT_SHADER = "\n uniform sampler2D uOcclusion;\n uniform sampler2D uDepth;\n uniform vec2 uTexel;\n uniform int uBlur;\n varying vec2 vUv;\n\n const float AO_EDGE = 0.05;\n\n void main() {\n float depth = texture2D(uDepth, vUv).x;\n // Symmetric at any side, and this texel alone at side 1.\n float center = float(uBlur - 1) * .5;\n float occluded = 0.;\n float total = 0.;\n // GLSL ES 1.00 on the old Android WebViews this SDK reaches needs constant loop bounds, and the\n // break is what skips the fetches.\n for (int y = 0; y < 4; y++) {\n if (y >= uBlur) break;\n for (int x = 0; x < 4; x++) {\n if (x >= uBlur) break;\n vec2 at = vUv + (vec2(float(x), float(y)) - center) * uTexel;\n // Depth-aware, so occlusion does not bleed across the edge between a wall and whatever\n // stands behind it. Stored depth is not linear, so the difference is a share of the range\n // still ahead of this texel.\n float stored = texture2D(uDepth, at).x;\n float apart = abs(stored - depth) / max(1. - depth, 1e-6);\n float weight = 1. - smoothstep(0., AO_EDGE, apart);\n occluded += texture2D(uOcclusion, at).r * weight;\n total += weight;\n }\n }\n\n // Nothing around this texel describes the surface it is on, so there is no term to write and no\n // total to divide by. Open, which a lit read of it leaves alone.\n if (total <= 0.) {\n gl_FragColor = vec4(0., 0., 0., 1.);\n return;\n }\n gl_FragColor = vec4(occluded / total, 0., 0., 1.);\n }\n";
61
+ //# sourceMappingURL=blur-pass.d.ts.map