@expofp/renderer 3.34.2 → 3.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/Renderer.d.ts +14 -0
  2. package/dist/Renderer.js +16 -4
  3. package/dist/core/shader-source.d.ts +20 -0
  4. package/dist/core/shader-source.js +26 -0
  5. package/dist/debug/components/Gui.d.ts +2 -0
  6. package/dist/debug/components/Gui.js +11 -1
  7. package/dist/external/ExternalCanvas.js +5 -6
  8. package/dist/external/external-camera.js +26 -11
  9. package/dist/index.d.ts +1 -0
  10. package/dist/objects/Background.js +1 -1
  11. package/dist/objects/containers/SlotContainer.js +7 -1
  12. package/dist/objects/containers/hooks.js +1 -1
  13. package/dist/objects/containers/props.d.ts +10 -0
  14. package/dist/objects/images/Images.js +9 -2
  15. package/dist/objects/images/image-container.js +1 -1
  16. package/dist/objects/layer/Layer.js +10 -10
  17. package/dist/objects/layer/mount-scheduler.d.ts +83 -54
  18. package/dist/objects/layer/mount-scheduler.js +204 -60
  19. package/dist/objects/lighting/Lighting.d.ts +92 -0
  20. package/dist/objects/lighting/Lighting.js +385 -0
  21. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  22. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  23. package/dist/objects/lighting/index.d.ts +11 -0
  24. package/dist/objects/lighting/index.js +10 -0
  25. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  26. package/dist/objects/lighting/lighting-gui.js +114 -0
  27. package/dist/objects/lighting/occluder.d.ts +34 -0
  28. package/dist/objects/lighting/occluder.js +62 -0
  29. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  30. package/dist/objects/lighting/occlusion-targets.js +276 -0
  31. package/dist/objects/lines/Lines.js +3 -1
  32. package/dist/objects/materials/ColorMaterial.d.ts +58 -51
  33. package/dist/objects/materials/ColorMaterial.js +74 -52
  34. package/dist/objects/materials/LineMaterial.js +1 -1
  35. package/dist/objects/materials/ModelMaterial.d.ts +30 -18
  36. package/dist/objects/materials/ModelMaterial.js +68 -41
  37. package/dist/objects/materials/TextureMaterial.js +1 -1
  38. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  39. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  40. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  41. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  42. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  43. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  44. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  45. package/dist/objects/materials/effects/ao/index.js +72 -0
  46. package/dist/objects/materials/{dim.d.ts → effects/color-dim.d.ts} +4 -4
  47. package/dist/objects/materials/{dim.js → effects/color-dim.js} +6 -24
  48. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  49. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  50. package/dist/objects/materials/effects/effects-context.d.ts +52 -0
  51. package/dist/objects/materials/effects/effects-context.js +24 -0
  52. package/dist/objects/models/Models.js +18 -2
  53. package/dist/objects/models/model-container.js +1 -1
  54. package/dist/objects/shapes/Shapes.js +20 -2
  55. package/dist/objects/text/Texts.js +3 -1
  56. package/dist/objects/text/text-container.js +1 -1
  57. package/dist/space/Scene.d.ts +3 -0
  58. package/dist/space/Scene.js +17 -5
  59. package/package.json +2 -2
  60. package/dist/objects/Lighting.d.ts +0 -20
  61. package/dist/objects/Lighting.js +0 -85
@@ -1,89 +1,111 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { useDimPatch } from './dim';
3
+ import { applyAmbientOcclusion } from './effects/ao';
4
+ import { useDimPatch } from './effects/color-dim';
5
+ import { applyContactShadow } from './effects/contact-shadow';
6
+ import { useSceneEffects } from './effects/effects-context';
4
7
  import { sharedParams } from './shared';
5
8
  /**
6
- * The material a shape batch draws with, rendered as a child of the mesh so R3F attaches, updates,
7
- * and disposes it. Also troika's base material for text (see `Texts`), which never asks for `solid`.
9
+ * The material a shape batch draws with. It renders as a child of the mesh, so R3F attaches, updates
10
+ * and disposes it. It is also troika's base material for text, which never asks for `solid`. See
11
+ * `Texts`.
8
12
  *
9
- * One component with a branch rather than two files, because the two are the same thing along one
10
- * axis — what a batch of shapes draws with — and the three differences are the whole point, best
11
- * read side by side:
13
+ * One component with a branch rather than two files. The two are the same thing along one axis, which
14
+ * is what a batch of shapes draws with. The three differences read best side by side:
12
15
  *
13
16
  * | | flat (default) | `solid` |
14
17
  * | --- | --- | --- |
15
- * | base | `MeshBasicMaterial`, unlit | `MeshPhysicalMaterial`, lit |
16
- * | `depthWrite` | off, from {@link sharedParams} | **on** |
17
- * | polygon offset | on, from {@link sharedParams} | **off** |
18
+ * | base | `MeshBasicMaterial`, unlit | `MeshLambertMaterial`, lit |
19
+ * | `depthWrite` | off, from {@link sharedParams} | on |
20
+ * | polygon offset | on, from {@link sharedParams} | off |
18
21
  *
19
- * Each difference is load-bearing. A volume is the one kind of content that writes depth: that is how
20
- * its own faces resolve against each other, and what lets 2D content drawn afterwards be occluded by
21
- * it. It carries no polygon offset because solids are what everything else is biased *against* —
22
- * offsetting them too would only move the problem.
22
+ * Each difference carries its own weight. A volume is the one kind of content that writes depth. That
23
+ * is how its own faces resolve against each other, and it is what lets 2D content drawn afterwards be
24
+ * occluded by it. A volume carries no polygon offset, because solids are what everything else is
25
+ * biased against. An offset on the solids too only moves the problem.
23
26
  *
24
- * `ior: 1` and `specularIntensity: 0` drive the lit material's `F0` to zero, so no specular lobe sits
25
- * on top of the diffuse term. That is what makes the lighting identity exact: with the two lights
26
- * summing to intensity `π`, a face square-on to the camera renders at precisely the authored color
27
- * (see `Lighting`). A default `MeshPhysicalMaterial` would put a sheen on every wall and push it
28
- * above its albedo.
27
+ * The lit branch is `MeshLambertMaterial` alone. It has no specular lobe at all, so this never sets
28
+ * `ior` or `specularIntensity`. three evaluates it per fragment with `BRDF_Lambert`, which is
29
+ * `albedo / π`. Its vertex shader carries `batching_vertex` and `color_vertex`, so
30
+ * `BatchedMesh.setColorAt` keeps working. Its fragment shader carries the output chain, so the dim
31
+ * patch keeps its anchor.
29
32
  *
30
- * Per-instance color arrives through `BatchedMesh.setColorAt` either way: three injects the
33
+ * The light rig alone holds a roof to its authored color now. `Lighting` derives both intensities from
34
+ * the one `contrast` control every frame. An up-facing surface then lands on the color it was authored
35
+ * with, and no surface the design accounts for clips.
36
+ *
37
+ * Per-instance color arrives through `BatchedMesh.setColorAt` on either branch. three injects the
31
38
  * batching-color chunk for whatever material the batch draws with.
32
39
  *
33
- * **`translucent` is the one place the array-order contract bends, and it is deliberate.** It sets
34
- * `transparent: true`, which moves the batch into three's transparent list — drawn after *every*
35
- * opaque object in the scene, whatever `renderOrder` says (`WebGLRenderer.renderScene`). That is
36
- * exactly what makes a translucent volume blend with the opaque content behind it even when that
37
- * content lives in a later layer, which no position in the ordered stream can achieve: a translucent
38
- * surface can only blend with what is already in the framebuffer.
40
+ * `translucent` is the one place the array-order contract bends, and it bends on purpose. It sets
41
+ * `transparent: true`, which moves the batch into three's transparent list. three draws that list
42
+ * after every opaque object in the scene, whatever `renderOrder` says. See
43
+ * `WebGLRenderer.renderScene`. That is what lets a translucent volume blend with the opaque content
44
+ * behind it, even when that content lives in a later layer. No position in the ordered stream can do
45
+ * the same, because a translucent surface can only blend with what already sits in the framebuffer.
39
46
  *
40
- * What it costs is the reverse case. A planar def sitting *on* a translucent volume — a label or logo
41
- * on a glass roof — draws in the opaque pass and so lands **before** it: the label composites against
42
- * whatever was under the glass rather than onto it, and the volume then glazes over the label. So a
43
- * translucent solid is right about what is behind it and wrong about what is on it. Flat content is
44
- * never given this flag for the same reason, plus one of its own: 2D is coplanar, so it has no depth
45
- * order to sort by and order is all it has.
47
+ * The reverse case is what it costs. Take a planar def sitting on a translucent volume, such as a
48
+ * label or a logo on a glass roof. It draws in the opaque pass, so it lands before the volume. The
49
+ * label composites against whatever was under the glass rather than onto it, and the volume then
50
+ * glazes over the label. So a translucent solid is right about what is behind it and wrong about what is on it. Flat
51
+ * content never takes this flag for the same reason, and for one of its own: 2D content is coplanar,
52
+ * so it has no depth order to sort by and order is all it has.
46
53
  *
47
- * `forceSinglePass` is switched off here alone. three renders a `DoubleSide` transparent material
48
- * twice — back faces, then front (`WebGLRenderer.js:1348`) — which is a correctly ordered two-pass
49
- * for free, and the reason a closed translucent volume does not blend its own far and near surfaces
50
- * in whatever order the batch happened to pack them.
54
+ * `forceSinglePass` is switched off here alone. three draws a `DoubleSide` transparent material
55
+ * twice, back faces and then front. See `WebGLRenderer.js:1348`. That is a correctly ordered two-pass
56
+ * for free. It is why a closed translucent volume does not blend its own far and near surfaces in
57
+ * whatever order the batch packed them.
51
58
  *
52
- * Depth writes stay **on** even when translucent, which is not three's usual convention. Two rules
53
- * depend on it: a translucent volume must still be occluded by anything nearer, and a farther one
54
- * drawn later must not paint over it. MapLibre makes the same choice for `fill-extrusion`
55
- * (`DepthMode.ReadWrite` in both of its passes). The trade and the two rejected alternatives are
56
- * recorded in the design spec's Decision 2.
59
+ * Depth writes stay on even when translucent, which is not three's usual convention. Two rules depend
60
+ * on it. A translucent volume must still be occluded by anything nearer, and a farther volume drawn
61
+ * later must not paint over it. MapLibre makes the same choice for `fill-extrusion`, with
62
+ * `DepthMode.ReadWrite` in both of its passes. The design spec's Decision 2 records the trade and the
63
+ * two rejected alternatives.
57
64
  *
58
- * The branch is stable for a mounted batch — `Shapes` renders a separate element per kind, each with
59
- * a constant `solid` — so R3F never swaps one base class for the other under a live mesh.
65
+ * The branch is stable for a mounted batch, because `Shapes` renders a separate element per kind and
66
+ * each one carries a constant `solid`. So R3F never swaps one base class for the other under a live
67
+ * mesh.
60
68
  * @param props - The material's base color, and whether it draws volumes.
61
69
  * @returns The material element.
62
70
  */
63
71
  export function ColorMaterial(props) {
64
- // Read-then-default rather than a destructuring default: the React Compiler cannot lower
72
+ // Read and then default, rather than a destructuring default. The React Compiler cannot lower
65
73
  // `const { color = 'white' } = props` (BuildHIR::lowerAssignment, AssignmentPattern), and
66
74
  // floorplan's dev build runs the compiler with `panicThreshold: 'all_errors'`, which turns that
67
75
  // into a hard 500 for the whole app. Storybook's pass defaults to `panicThreshold: 'none'` and
68
- // silently skips the component instead, so only the running app surfaces it.
76
+ // skips the component in silence, so only the running app shows the failure.
69
77
  const color = props.color ?? 'white';
70
- // Before the branch, because it is a hook — and passed to both, because a volume dims exactly as
71
- // flat content does: the patch runs on whatever `gl_FragColor` the material arrived at, which for
72
- // a lit material is the shaded color.
78
+ const solid = props.solid === true;
79
+ // Handed to both branches, because a volume dims the way flat content does. The patch runs on
80
+ // whatever `gl_FragColor` the material arrived at, which for a lit material is the shaded color.
73
81
  const dimPatch = useDimPatch();
74
- // The caller's surgery first, then dimming — so a pre-patch that declares the dim uniform's
75
- // stand-in is already in the shader when the dim patch looks for it. One stable handler, for the
76
- // same reason `useDimPatch` keeps its own.
82
+ // The scene's own effect state, captured once. Its identity never changes, so the compiled program
83
+ // holds the uniform objects a frame and a slider write into, and neither one costs a recompile.
84
+ const effects = useSceneEffects();
85
+ // The caller's surgery first, then dimming. A pre-patch that declares the dim uniform's stand-in
86
+ // then sits in the shader before the dim patch looks for it. One stable handler, for the reason
87
+ // `useDimPatch` keeps its own.
77
88
  const [onBeforeCompile] = useState(() => {
78
89
  const pre = props.onBeforeCompile;
79
90
  return (parameters) => {
80
91
  pre?.(parameters);
92
+ // Volumes only. Flat content has no height to read, and its unit plane sits at `z = 0`, where
93
+ // the contact shadow takes the whole surface. Flat content also stays out of the depth prepass,
94
+ // so an occlusion read finds no stored depth that agrees with it.
95
+ //
96
+ // Both effects land ahead of the dim patch, so the three run in the order they describe: the
97
+ // shadow and the occlusion shade the surface, and dimming desaturates the result. All three
98
+ // insert above the same anchor, so the later install runs last.
99
+ if (solid) {
100
+ applyContactShadow(parameters, effects);
101
+ applyAmbientOcclusion(parameters, effects);
102
+ }
81
103
  dimPatch(parameters);
82
104
  };
83
105
  });
84
- if (props.solid) {
106
+ if (solid) {
85
107
  const translucent = props.translucent ?? false;
86
- return (_jsx("meshPhysicalMaterial", { ...sharedParams, color: color, depthWrite: true, polygonOffset: false, ior: 1, specularIntensity: 0, transparent: translucent, forceSinglePass: !translucent, onBeforeCompile: onBeforeCompile }));
108
+ return (_jsx("meshLambertMaterial", { ...sharedParams, color: color, depthWrite: true, polygonOffset: false, transparent: translucent, forceSinglePass: !translucent, onBeforeCompile: onBeforeCompile }));
87
109
  }
88
110
  return _jsx("meshBasicMaterial", { ...sharedParams, color: color, onBeforeCompile: onBeforeCompile });
89
111
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { extend } from '@react-three/fiber';
3
3
  import { NotEqualStencilFunc, ReplaceStencilOp, } from 'three';
4
4
  import { LineMaterial as LineMaterialBase } from 'three/addons/lines/LineMaterial.js';
5
- import { useDimPatch } from './dim';
5
+ import { useDimPatch } from './effects/color-dim';
6
6
  import { sharedParams } from './shared';
7
7
  /**
8
8
  * The stencil value a line layer stamps on every pixel it covers.
@@ -2,38 +2,50 @@ import { type Material } from 'three';
2
2
  /** Props for {@link ModelMaterial}. */
3
3
  export interface ModelMaterialProps {
4
4
  /**
5
- * The material the GLB authored. Shared across every layer drawing the same model, so it is never
6
- * mutated — this clones it and adjusts the clone.
5
+ * The material the GLB authored. Every layer drawing the same model shares it, so nothing mutates
6
+ * it. This component clones it and adjusts the clone.
7
7
  */
8
8
  source: Material;
9
9
  /**
10
- * The name of a per-instance `float` uniform to multiply alpha by, so one mesh can hold instances
11
- * of differing opacity. The mesh owns the name and declares it; the material only patches its
12
- * shader to read it.
10
+ * The name of a per-instance `float` uniform to multiply alpha by, so one mesh can hold instances of
11
+ * differing opacity. The mesh owns the name and declares it. The material only patches its shader to
12
+ * read it.
13
13
  */
14
14
  opacityUniform: string;
15
15
  /**
16
16
  * Whether any instance drawing this material is translucent.
17
17
  *
18
- * Per-instance opacity is a *fragment* multiplier, but whether alpha blends at all is a **material**
19
- * decision: three drops a `NormalBlending` material with `transparent: false` to `NoBlending`
20
- * (`WebGLState.setMaterial`), which discards the alpha entirely — the instance then composites
21
- * against the page rather than against the scene. So the mesh has to tell the material that at
22
- * least one of its instances needs blending.
18
+ * Per-instance opacity is a fragment multiplier, and whether alpha blends at all is a material
19
+ * decision. three drops a `NormalBlending` material with `transparent: false` to `NoBlending`, in
20
+ * `WebGLState.setMaterial`, which discards the alpha. The instance then composites against the page
21
+ * rather than against the scene. So the mesh has to tell the material that at least one of its
22
+ * instances needs blending.
23
23
  */
24
24
  translucent?: boolean;
25
25
  }
26
26
  /**
27
- * The material a model draws with: the GLB's own, cloned and adjusted for the plan's depth rules and
28
- * for per-instance opacity.
27
+ * The material a model draws with: the GLB's own, cloned and adjusted for the plan's depth rules, for
28
+ * the plan's shading, and for per-instance opacity.
29
29
  *
30
- * A model is one of the two kinds of content that **write depth**, translucent ones included — that
31
- * is what makes it occlude itself, other models, and any 2D content drawn after it. The rest of the
32
- * scene draws with `depthWrite: false` and a small polygon offset precisely so it can be biased
33
- * against surfaces like these, so a model carries no offset of its own.
30
+ * A model is one of the two kinds of content that write depth, translucent models included. That is
31
+ * what makes a model occlude itself, other models, and any 2D content drawn after it. The rest of the
32
+ * scene draws with `depthWrite: false` and a small polygon offset so that it can be biased against
33
+ * surfaces like these, so a model carries no offset of its own.
34
34
  *
35
- * Everything else about the material is left as the GLB authored it — maps, roughness, metalness,
36
- * emissive, side. A model's appearance is the asset's business, not the renderer's.
35
+ * A model takes both darkening terms, the way a solid shape batch does. The GLTF loader produces a
36
+ * `MeshStandardMaterial`, which carries the same output chain and the same placement chunk, so both
37
+ * patches apply unchanged. The contact shadow reads a plan-space height. That is what makes it correct
38
+ * for geometry authored in the GLB's own coordinates and placed by a per-instance matrix.
39
+ *
40
+ * An asset declaring `KHR_materials_unlit` takes both terms too, on purpose. The loader hands such a
41
+ * material over as a `MeshBasicMaterial`, which carries both anchors, so nothing here has to sniff a
42
+ * type to include it. Neither term is a light. They are the plan's own depth cue, applied to whatever
43
+ * color the material arrived at. The design asks for one shade of shadow across the plan. An unlit
44
+ * booth with no darkening at its base reads as floating beside a lit one. That is a worse answer to
45
+ * "unlit" than shading it.
46
+ *
47
+ * Everything else about the material stays as the GLB authored it: maps, roughness, metalness,
48
+ * emissive, side. A model's own appearance is the asset's business. How the plan is shaded is not.
37
49
  * @param props - The source material and the per-instance opacity uniform's name.
38
50
  * @returns The material element.
39
51
  */
@@ -1,82 +1,109 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { LessEqualDepth, } from 'three';
4
- import { useDimPatch } from './dim';
4
+ import { applyAmbientOcclusion } from './effects/ao';
5
+ import { useDimPatch } from './effects/color-dim';
6
+ import { applyContactShadow } from './effects/contact-shadow';
7
+ import { useSceneEffects } from './effects/effects-context';
5
8
  /**
6
- * The material a model draws with: the GLB's own, cloned and adjusted for the plan's depth rules and
7
- * for per-instance opacity.
9
+ * The material a model draws with: the GLB's own, cloned and adjusted for the plan's depth rules, for
10
+ * the plan's shading, and for per-instance opacity.
8
11
  *
9
- * A model is one of the two kinds of content that **write depth**, translucent ones included — that
10
- * is what makes it occlude itself, other models, and any 2D content drawn after it. The rest of the
11
- * scene draws with `depthWrite: false` and a small polygon offset precisely so it can be biased
12
- * against surfaces like these, so a model carries no offset of its own.
12
+ * A model is one of the two kinds of content that write depth, translucent models included. That is
13
+ * what makes a model occlude itself, other models, and any 2D content drawn after it. The rest of the
14
+ * scene draws with `depthWrite: false` and a small polygon offset so that it can be biased against
15
+ * surfaces like these, so a model carries no offset of its own.
13
16
  *
14
- * Everything else about the material is left as the GLB authored it — maps, roughness, metalness,
15
- * emissive, side. A model's appearance is the asset's business, not the renderer's.
17
+ * A model takes both darkening terms, the way a solid shape batch does. The GLTF loader produces a
18
+ * `MeshStandardMaterial`, which carries the same output chain and the same placement chunk, so both
19
+ * patches apply unchanged. The contact shadow reads a plan-space height. That is what makes it correct
20
+ * for geometry authored in the GLB's own coordinates and placed by a per-instance matrix.
21
+ *
22
+ * An asset declaring `KHR_materials_unlit` takes both terms too, on purpose. The loader hands such a
23
+ * material over as a `MeshBasicMaterial`, which carries both anchors, so nothing here has to sniff a
24
+ * type to include it. Neither term is a light. They are the plan's own depth cue, applied to whatever
25
+ * color the material arrived at. The design asks for one shade of shadow across the plan. An unlit
26
+ * booth with no darkening at its base reads as floating beside a lit one. That is a worse answer to
27
+ * "unlit" than shading it.
28
+ *
29
+ * Everything else about the material stays as the GLB authored it: maps, roughness, metalness,
30
+ * emissive, side. A model's own appearance is the asset's business. How the plan is shaded is not.
16
31
  * @param props - The source material and the per-instance opacity uniform's name.
17
32
  * @returns The material element.
18
33
  */
19
34
  export function ModelMaterial(props) {
20
35
  const { source, opacityUniform } = props;
21
36
  const dimPatch = useDimPatch();
37
+ // The scene's own effect state, captured once. Its identity never changes, so the compiled program
38
+ // holds the uniform objects a frame and a slider write into, and neither one costs a recompile.
39
+ const effects = useSceneEffects();
22
40
  const translucent = props.translucent ?? false;
23
- // Cloned once and held in state: R3F would dispose a material it created on unmount, and the
24
- // source belongs to the module-level model cache, which may still be serving other layers. The
25
- // dim patch is installed with it, once — the patch is stable and reads the current amount through
26
- // the uniform it owns.
27
- const [material] = useState(() => adjust(source.clone(), opacityUniform, dimPatch, translucent));
41
+ // Cloned once and held in state. R3F disposes a material it created on unmount, and the source
42
+ // belongs to the module-level model cache, which can still be serving other layers. Every patch
43
+ // goes on with the clone, once. Each patch is stable and reads its current values through the
44
+ // uniforms it owns.
45
+ const [material] = useState(() => adjust(source.clone(), opacityUniform, dimPatch, effects, translucent));
28
46
  useEffect(() => () => material.dispose(), [material]);
29
47
  return _jsx("primitive", { object: material, attach: "material" });
30
48
  }
31
49
  /**
32
- * Applies the plan's depth rules and the per-instance opacity patch to a cloned GLB material.
50
+ * Applies the plan's depth rules, its two darkening terms and the per-instance opacity patch to a
51
+ * cloned GLB material.
33
52
  * @param material - The clone to adjust.
34
53
  * @param opacityUniform - The per-instance uniform's name.
35
54
  * @param dimPatch - Installs dimming on the compiled shader.
55
+ * @param effects - The scene's shared effect state, which both darkening terms read.
36
56
  * @param translucent - Whether any instance drawing it needs its alpha blended.
37
57
  * @returns The same material.
38
58
  */
39
- function adjust(material, opacityUniform, dimPatch, translucent) {
59
+ function adjust(material, opacityUniform, dimPatch, effects, translucent) {
40
60
  material.depthFunc = LessEqualDepth;
41
61
  material.polygonOffset = false;
42
- // `transparent` moves a material into three's transparent list, drawn after *every* opaque object
43
- // in the scene whatever the layer order says — which is what lets a translucent model blend with
44
- // the content behind it instead of with an empty frame. It is a **material** property and never a
45
- // per-instance one, so which list a def belongs in is a partition rather than a flag: `Models`
46
- // keeps a layer's translucent copies in a batch of their own and re-splits when a def's opacity
47
- // crosses 1, exactly as `Shapes` splits its volumes. `ColorMaterial` makes the same trade and
48
- // records what it costs — a planar def sitting *on* a translucent one is drawn before it.
62
+ // `transparent` moves a material into three's transparent list, which three draws after every
63
+ // opaque object in the scene whatever the layer order says. That is what lets a translucent model
64
+ // blend with the content behind it instead of with an empty frame.
65
+ //
66
+ // It is a material property and never a per-instance one, so which list a def belongs in is a
67
+ // partition rather than a flag. `Models` keeps a layer's translucent copies in a batch of their own,
68
+ // the way `Shapes` splits its volumes. It re-splits when a def's opacity crosses 1. `ColorMaterial`
69
+ // makes the same trade and records what it costs: a planar def sitting on a translucent one draws
70
+ // before it.
49
71
  material.transparent =
50
72
  material.transparent || translucent || material.opacity < 1;
51
- // Depth writes stay on even when translucent, which is not three's usual convention and is
52
- // deliberate. Two of the plan's rules depend on it: a translucent model must still be occluded by
53
- // anything nearer the camera, and a planar def drawn after it — a label or logo on a booth roof —
54
- // must be occluded by it. Both are depth-buffer questions, and switching writes off answers them
55
- // wrongly: a *farther* solid drawn later would have nothing to reject it and would paint straight
56
- // over the translucent one in front of it.
73
+ // Depth writes stay on even when translucent, which is not three's usual convention and is the
74
+ // choice this renderer makes. Two of the plan's rules depend on it. A translucent model must still
75
+ // be occluded by anything nearer the camera. A planar def drawn after it, a label or a logo on a
76
+ // booth roof, must be occluded by it. Both are depth-buffer questions, and switching
77
+ // writes off answers them wrongly: a farther solid drawn later has nothing to reject it, and it
78
+ // paints straight over the translucent one in front of it.
57
79
  //
58
- // What it costs is seeing *through* a translucent surface to content drawn after it, which no
59
- // painter's-order renderer can offer — blending needs depth order, and array order is authoring
60
- // order. MapLibre makes the same trade for `fill-extrusion` (`DepthMode.ReadWrite` in both of its
61
- // passes), degrading to a color error rather than a structural one. The obligation it puts on
62
- // callers — order a translucent model's layer after whatever should show through it — is spelled
63
- // out in the design spec's Decision 2, along with the two alternatives and why each loses more
80
+ // What it costs is a view through a translucent surface to content drawn after it, and no
81
+ // painter's-order renderer can offer that. Blending needs depth order, and array order is authoring
82
+ // order. MapLibre makes the same trade for `fill-extrusion`, with `DepthMode.ReadWrite` in both of
83
+ // its passes, and degrades to a color error rather than a structural one. The obligation it puts on
84
+ // callers is to order a translucent model's layer after whatever has to show through it. The design
85
+ // spec's Decision 2 spells that out, along with the two alternatives and why each one loses more
64
86
  // than it gains.
65
87
  material.depthWrite = true;
66
88
  const chained = material.onBeforeCompile.bind(material);
67
89
  material.onBeforeCompile = (parameters, ...rest) => {
68
90
  chained(parameters, ...rest);
69
- // Applied at the very end of the fragment chain, after `<dithering_fragment>`, so it multiplies
70
- // the final composed alpha rather than an intermediate one. The instanced mesh injects the
71
- // uniform fetch at the top of `main()`, so the name is in scope by the time this runs.
91
+ // Applied at the end of the fragment chain, after `<dithering_fragment>`, so it multiplies the
92
+ // final composed alpha rather than an intermediate one. The instanced mesh injects the uniform
93
+ // fetch at the top of `main()`, so the name is in scope by the time this runs.
72
94
  parameters.fragmentShader = parameters.fragmentShader.replace('#include <dithering_fragment>',
73
95
  /*glsl*/ `
74
96
  #include <dithering_fragment>
75
97
  gl_FragColor.a *= ${opacityUniform};
76
98
  `.trim());
77
- // Dim touches rgb and opacity touches alpha, so the two patches are independent however they
78
- // are ordered — and dim lands ahead of the colorspace conversion either way, which is where a
79
- // gamma-corrected darken has to be.
99
+ // The plan's shading, ahead of the dim patch. All three insert above the same anchor, so the later
100
+ // install runs last, and the order they run in is the order they describe: the shadow and the
101
+ // occlusion shade the surface, and dimming desaturates the result.
102
+ applyContactShadow(parameters, effects);
103
+ applyAmbientOcclusion(parameters, effects);
104
+ // Dim touches rgb and opacity touches alpha, so the two patches stay independent in any order. Dim
105
+ // lands ahead of the colorspace conversion either way, which is where a gamma-corrected darken has
106
+ // to be.
80
107
  dimPatch(parameters);
81
108
  };
82
109
  material.needsUpdate = true;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useDimPatch } from './dim';
2
+ import { useDimPatch } from './effects/color-dim';
3
3
  import { sharedParams } from './shared';
4
4
  /**
5
5
  * The material used to render a texture, optionally through a per-instance uv-offset uniform so an
@@ -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