@pascal-app/viewer 1.0.4 → 1.0.6

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 (183) hide show
  1. package/dist/capture/capture-runtime.js +1 -1
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.d.ts +1 -1
  4. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/node-renderer.js +4 -1
  6. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  7. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
  8. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  9. package/dist/components/viewer/glb-interactive.js +1 -1
  10. package/dist/components/viewer/glb-reference-nodes.js +1 -1
  11. package/dist/components/viewer/glb-replace-instances.js +1 -1
  12. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-scene.js +2 -3
  14. package/dist/components/viewer/glb-walkthrough-controller.js +1 -1
  15. package/dist/components/viewer/index.d.ts +10 -1
  16. package/dist/components/viewer/index.d.ts.map +1 -1
  17. package/dist/components/viewer/index.js +28 -12
  18. package/dist/components/viewer/lights.d.ts.map +1 -1
  19. package/dist/components/viewer/lights.js +107 -21
  20. package/dist/components/viewer/perf-panel.d.ts.map +1 -1
  21. package/dist/components/viewer/perf-panel.js +1 -1
  22. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  23. package/dist/components/viewer/post-processing.js +96 -33
  24. package/dist/components/viewer/registered-systems.js +1 -1
  25. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  26. package/dist/components/viewer/scene-bvh.js +1 -1
  27. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  28. package/dist/components/viewer/scene-environment.js +31 -11
  29. package/dist/components/viewer/selection-manager.js +15 -7
  30. package/dist/components/viewer/underground-ambiance.d.ts +9 -0
  31. package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
  32. package/dist/components/viewer/underground-ambiance.js +105 -0
  33. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  34. package/dist/components/viewer/viewer-camera.js +1 -1
  35. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
  36. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  37. package/dist/hooks/use-gltf-ktx2.js +2 -0
  38. package/dist/hooks/use-node-events.d.ts.map +1 -1
  39. package/dist/index.d.ts +16 -7
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +16 -7
  42. package/dist/lib/backdrop.d.ts +17 -0
  43. package/dist/lib/backdrop.d.ts.map +1 -1
  44. package/dist/lib/backdrop.js +18 -1
  45. package/dist/lib/construction-dust.d.ts +57 -0
  46. package/dist/lib/construction-dust.d.ts.map +1 -0
  47. package/dist/lib/construction-dust.js +187 -0
  48. package/dist/lib/csg-utils.js +1 -1
  49. package/dist/lib/foundation-materials.d.ts +26 -0
  50. package/dist/lib/foundation-materials.d.ts.map +1 -0
  51. package/dist/lib/foundation-materials.js +65 -0
  52. package/dist/lib/gltf-unlit.d.ts +10 -0
  53. package/dist/lib/gltf-unlit.d.ts.map +1 -0
  54. package/dist/lib/gltf-unlit.js +23 -0
  55. package/dist/lib/ground-window.d.ts +33 -0
  56. package/dist/lib/ground-window.d.ts.map +1 -0
  57. package/dist/lib/ground-window.js +114 -0
  58. package/dist/lib/hero-pose.d.ts +1 -1
  59. package/dist/lib/hero-pose.d.ts.map +1 -1
  60. package/dist/lib/ink-edges.d.ts +2 -1
  61. package/dist/lib/ink-edges.d.ts.map +1 -1
  62. package/dist/lib/ktx2-loader.d.ts +7 -0
  63. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  64. package/dist/lib/ktx2-loader.js +35 -0
  65. package/dist/lib/layer-pass.d.ts.map +1 -1
  66. package/dist/lib/layers.d.ts +11 -7
  67. package/dist/lib/layers.d.ts.map +1 -1
  68. package/dist/lib/layers.js +16 -6
  69. package/dist/lib/materials.d.ts +5 -0
  70. package/dist/lib/materials.d.ts.map +1 -1
  71. package/dist/lib/materials.js +34 -4
  72. package/dist/lib/merged-outline-node.d.ts +5 -0
  73. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  74. package/dist/lib/merged-outline-node.js +80 -2
  75. package/dist/lib/pointer-events.d.ts +20 -4
  76. package/dist/lib/pointer-events.d.ts.map +1 -1
  77. package/dist/lib/pointer-events.js +78 -14
  78. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  79. package/dist/lib/reveal-candidates.d.ts +80 -0
  80. package/dist/lib/reveal-candidates.d.ts.map +1 -0
  81. package/dist/lib/reveal-candidates.js +524 -0
  82. package/dist/lib/reveal-ghost.d.ts +9 -0
  83. package/dist/lib/reveal-ghost.d.ts.map +1 -0
  84. package/dist/lib/reveal-ghost.js +62 -0
  85. package/dist/lib/reveal-pending.d.ts +12 -0
  86. package/dist/lib/reveal-pending.d.ts.map +1 -0
  87. package/dist/lib/reveal-pending.js +12 -0
  88. package/dist/lib/reveal-pose.d.ts +70 -0
  89. package/dist/lib/reveal-pose.d.ts.map +1 -0
  90. package/dist/lib/reveal-pose.js +248 -0
  91. package/dist/lib/reveal-schedule.d.ts +208 -0
  92. package/dist/lib/reveal-schedule.d.ts.map +1 -0
  93. package/dist/lib/reveal-schedule.js +410 -0
  94. package/dist/lib/reveal-texture-scale.d.ts +5 -0
  95. package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
  96. package/dist/lib/reveal-texture-scale.js +113 -0
  97. package/dist/lib/reveal-windows.d.ts +23 -0
  98. package/dist/lib/reveal-windows.d.ts.map +1 -0
  99. package/dist/lib/reveal-windows.js +31 -0
  100. package/dist/lib/reveal-wipe.d.ts +71 -0
  101. package/dist/lib/reveal-wipe.d.ts.map +1 -0
  102. package/dist/lib/reveal-wipe.js +105 -0
  103. package/dist/lib/scene-pass-mrt.d.ts +12 -0
  104. package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
  105. package/dist/lib/scene-pass-mrt.js +23 -0
  106. package/dist/lib/scene-themes.d.ts +30 -8
  107. package/dist/lib/scene-themes.d.ts.map +1 -1
  108. package/dist/lib/scene-themes.js +36 -0
  109. package/dist/lib/scene-visibility.d.ts +11 -1
  110. package/dist/lib/scene-visibility.d.ts.map +1 -1
  111. package/dist/lib/scene-visibility.js +38 -0
  112. package/dist/lib/site-surfaces.d.ts +32 -0
  113. package/dist/lib/site-surfaces.d.ts.map +1 -0
  114. package/dist/lib/site-surfaces.js +70 -0
  115. package/dist/lib/snapshot-pipeline.d.ts +2 -1
  116. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  117. package/dist/lib/snapshot-pipeline.js +35 -15
  118. package/dist/lib/tsl-compat.d.ts +2 -2
  119. package/dist/lib/tsl-compat.d.ts.map +1 -1
  120. package/dist/lib/underground-ambiance.d.ts +168 -0
  121. package/dist/lib/underground-ambiance.d.ts.map +1 -0
  122. package/dist/lib/underground-ambiance.js +334 -0
  123. package/dist/store/use-viewer.d.ts +184 -27
  124. package/dist/store/use-viewer.d.ts.map +1 -1
  125. package/dist/store/use-viewer.js +155 -19
  126. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  127. package/dist/systems/ceiling/ceiling-system.js +2 -0
  128. package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
  129. package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
  130. package/dist/systems/construction-reveal/construction-dust.js +92 -0
  131. package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
  132. package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
  133. package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
  134. package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
  135. package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
  136. package/dist/systems/construction-reveal/dust-material.js +17 -0
  137. package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
  138. package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
  139. package/dist/systems/construction-reveal/ghost-dress.js +24 -0
  140. package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
  141. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  142. package/dist/systems/elevator/elevator-interaction-system.js +18 -1
  143. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  144. package/dist/systems/interactive/control-widget.d.ts.map +1 -1
  145. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  146. package/dist/systems/interactive/interactive-system.js +16 -6
  147. package/dist/systems/interactive/scripted-clips.js +1 -1
  148. package/dist/systems/item-light/item-light-system.js +1 -1
  149. package/dist/systems/level/level-hold.d.ts +4 -0
  150. package/dist/systems/level/level-hold.d.ts.map +1 -0
  151. package/dist/systems/level/level-hold.js +14 -0
  152. package/dist/systems/level/level-system.d.ts.map +1 -1
  153. package/dist/systems/level/level-system.js +16 -1
  154. package/dist/systems/level/level-utils.d.ts +13 -3
  155. package/dist/systems/level/level-utils.d.ts.map +1 -1
  156. package/dist/systems/level/level-utils.js +56 -8
  157. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
  158. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
  159. package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
  160. package/dist/systems/roof/roof-materials.d.ts +4 -2
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +18 -24
  163. package/dist/systems/roof/roof-system.d.ts +8 -1
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +195 -45
  166. package/dist/systems/wall/wall-cutout-cache.d.ts +32 -3
  167. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  168. package/dist/systems/wall/wall-cutout-cache.js +95 -2
  169. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  170. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
  171. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  172. package/dist/systems/wall/wall-materials.js +39 -4
  173. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
  174. package/dist/systems/wall/wall-system.d.ts +5 -0
  175. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  176. package/dist/systems/wall/wall-system.js +42 -19
  177. package/dist/systems/window/corner-frame.d.ts +10 -0
  178. package/dist/systems/window/corner-frame.d.ts.map +1 -0
  179. package/dist/systems/window/corner-frame.js +17 -0
  180. package/dist/systems/window/window-system.d.ts +1 -1
  181. package/dist/systems/window/window-system.d.ts.map +1 -1
  182. package/dist/systems/window/window-system.js +31 -6
  183. package/package.json +8 -8
@@ -0,0 +1,105 @@
1
+ import { positionWorld, uniform } from 'three/tsl';
2
+ /** A node that rises less than this (m) is flat: it sweeps along its longer plan side. */
3
+ export const COVER_MIN_RISE_M = 0.6;
4
+ /** The sweep of a node drawn inside `box` (world space). */
5
+ export function coverSweepOf(box) {
6
+ // Nothing drawn yet: an empty sweep, so the covering has nothing to close.
7
+ if (!(box.max.y >= box.min.y))
8
+ return { axis: 'y', from: 0, to: 0 };
9
+ if (box.max.y - box.min.y >= COVER_MIN_RISE_M) {
10
+ return { axis: 'y', from: box.min.y, to: box.max.y };
11
+ }
12
+ const alongX = box.max.x - box.min.x >= box.max.z - box.min.z;
13
+ return alongX
14
+ ? { axis: 'x', from: box.min.x, to: box.max.x }
15
+ : { axis: 'z', from: box.min.z, to: box.max.z };
16
+ }
17
+ /** The time a covering of `sweep` takes in `range`. */
18
+ export function coverDurationMs(sweep, range) {
19
+ const length = Math.max(0, sweep.to - sweep.from);
20
+ return Math.min(range.maxMs, Math.max(range.minMs, length * range.perMeterMs));
21
+ }
22
+ /**
23
+ * How much of the sweep the covering has closed `elapsedMs` into `durationMs`, as a share in [0, 1].
24
+ * It moves continuously: an ease in and out (a sine), so it starts and stops without a jolt, and it
25
+ * is exactly 0 at the start and 1 at the end.
26
+ */
27
+ export function coverShareAt(elapsedMs, durationMs) {
28
+ if (!(durationMs > 0))
29
+ return elapsedMs >= 0 ? 1 : 0;
30
+ const t = Math.min(1, Math.max(0, elapsedMs / durationMs));
31
+ return 0.5 - 0.5 * Math.cos(Math.PI * t);
32
+ }
33
+ const worn = new WeakMap();
34
+ const sweeps = new WeakMap();
35
+ const eachOf = (material) => Array.isArray(material) ? material : [material];
36
+ const isMesh = (object) => object.isMesh === true && Boolean(object.material);
37
+ function maskOf(sweep) {
38
+ const coordinate = sweep.axis === 'y' ? positionWorld.y : sweep.axis === 'x' ? positionWorld.x : positionWorld.z;
39
+ return coordinate.lessThanEqual(sweep.level);
40
+ }
41
+ function wipedClone(material, mask) {
42
+ const clone = material.clone();
43
+ clone.maskNode = clone.maskNode ? clone.maskNode.and(mask) : mask;
44
+ return clone;
45
+ }
46
+ /**
47
+ * Shows what is below `at` (world metres along the sweep's axis) of everything drawn under `root`,
48
+ * and hides the rest. Idempotent: a frame later it only moves the level; a mesh whose renderer put
49
+ * its own materials back is dressed again. Cleared by `clearRevealWipe`.
50
+ */
51
+ export function setRevealWipe(root, sweep, at) {
52
+ let state = sweeps.get(root);
53
+ if (!state || state.axis !== sweep.axis) {
54
+ if (state)
55
+ clearRevealWipe(root);
56
+ state = { level: uniform(at), axis: sweep.axis };
57
+ sweeps.set(root, state);
58
+ }
59
+ state.level.value = at;
60
+ const mask = maskOf(state);
61
+ root.traverse((child) => {
62
+ if (!isMesh(child))
63
+ return;
64
+ let entry = worn.get(child);
65
+ if (entry && child.material !== entry.wiped) {
66
+ // Its renderer set the real materials again: they are the originals now.
67
+ for (const material of eachOf(entry.wiped))
68
+ material.dispose();
69
+ entry = undefined;
70
+ }
71
+ if (!entry) {
72
+ const original = child.material;
73
+ const wiped = Array.isArray(original)
74
+ ? original.map((material) => wipedClone(material, mask))
75
+ : wipedClone(original, mask);
76
+ entry = { original, wiped, cast: child.castShadow };
77
+ worn.set(child, entry);
78
+ child.material = wiped;
79
+ }
80
+ // The depth pass has no mask: a row not yet laid would still throw its shadow.
81
+ child.castShadow = false;
82
+ });
83
+ }
84
+ /** Gives every mesh under `root` its own materials and its shadow back. */
85
+ export function clearRevealWipe(root) {
86
+ if (!sweeps.delete(root))
87
+ return;
88
+ root.traverse((child) => {
89
+ if (!isMesh(child))
90
+ return;
91
+ const entry = worn.get(child);
92
+ if (!entry)
93
+ return;
94
+ if (child.material === entry.wiped)
95
+ child.material = entry.original;
96
+ for (const material of eachOf(entry.wiped))
97
+ material.dispose();
98
+ child.castShadow = entry.cast;
99
+ worn.delete(child);
100
+ });
101
+ }
102
+ /** The world coordinate the covering has reached on `root`, or null when it is not under a wipe. */
103
+ export function revealWipeLevelOf(root) {
104
+ return sweeps.get(root)?.level.value ?? null;
105
+ }
@@ -0,0 +1,12 @@
1
+ import { mrt } from 'three/tsl';
2
+ /** Unlit assets already contain their lighting; screen-space AO/GI must not light them again. */
3
+ export declare function sceneLightingMask(): import("three/webgpu").VarNode<"vec4", import("three/webgpu").JoinNode<"vec4">>;
4
+ /**
5
+ * The scene pass's attachments. three blends `output` by the material and leaves every other
6
+ * attachment unblended, so a transparent draw overwrites the whole of what it covers in the normals
7
+ * and the diffuse colour, however faint it is: a dust sprite's square was inked as an edge. With the
8
+ * material's blending on them, a draw that writes alpha 0 there (the dust's `mrtNode`) leaves them
9
+ * as they are, and an opaque draw, which has no blending, still overwrites.
10
+ */
11
+ export declare function scenePassMrt(outputs: Parameters<typeof mrt>[0]): import("three/webgpu").MRTNode;
12
+ //# sourceMappingURL=scene-pass-mrt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-pass-mrt.d.ts","sourceRoot":"","sources":["../../src/lib/scene-pass-mrt.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,GAAG,EAAiB,MAAM,WAAW,CAAA;AAM5D,iGAAiG;AACjG,wBAAgB,iBAAiB,oFAKhC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,kCAK9D"}
@@ -0,0 +1,23 @@
1
+ import { MaterialBlending } from 'three';
2
+ import { diffuseColor, mrt, uniform, vec4 } from 'three/tsl';
3
+ import { BlendMode } from 'three/webgpu';
4
+ import { importedUnlitMaterial } from './gltf-unlit';
5
+ const materialBlend = new BlendMode(MaterialBlending);
6
+ /** Unlit assets already contain their lighting; screen-space AO/GI must not light them again. */
7
+ export function sceneLightingMask() {
8
+ const weight = uniform(1).onObjectUpdate(({ material }) => importedUnlitMaterial(material) ? 0 : 1);
9
+ return vec4(weight, 0, 0, diffuseColor.a);
10
+ }
11
+ /**
12
+ * The scene pass's attachments. three blends `output` by the material and leaves every other
13
+ * attachment unblended, so a transparent draw overwrites the whole of what it covers in the normals
14
+ * and the diffuse colour, however faint it is: a dust sprite's square was inked as an edge. With the
15
+ * material's blending on them, a draw that writes alpha 0 there (the dust's `mrtNode`) leaves them
16
+ * as they are, and an opaque draw, which has no blending, still overwrites.
17
+ */
18
+ export function scenePassMrt(outputs) {
19
+ return mrt(outputs)
20
+ .setBlendMode('diffuseColor', materialBlend)
21
+ .setBlendMode('normal', materialBlend)
22
+ .setBlendMode('lighting', materialBlend);
23
+ }
@@ -1,10 +1,10 @@
1
- import type { SurfaceRole } from '@pascal-app/core';
1
+ import type { SitePresentation, SurfaceRole } from '@pascal-app/core';
2
2
  export type SceneTheme = {
3
3
  id: string;
4
4
  name: string;
5
5
  appearance: 'light' | 'dark';
6
6
  background: string;
7
- backgroundSky?: string;
7
+ backgroundSky?: string | null;
8
8
  ground: string;
9
9
  ambient: {
10
10
  color: string;
@@ -15,16 +15,38 @@ export type SceneTheme = {
15
15
  ground: string;
16
16
  intensity: number;
17
17
  };
18
- lights: Array<{
19
- position: [number, number, number];
20
- color: string;
21
- intensity: number;
22
- castShadow?: boolean;
23
- }>;
18
+ lights: NonNullable<NonNullable<SitePresentation['lighting']>['lights']>;
24
19
  toneMappingExposure: number;
20
+ environment?: NonNullable<SitePresentation['lighting']>['environment'];
21
+ postprocessing?: SitePresentation['postprocessing'];
25
22
  clayTints?: Partial<Record<SurfaceRole, string>>;
26
23
  };
24
+ export declare function projectSceneTheme(theme: SceneTheme, presentation?: SitePresentation): SceneTheme;
27
25
  export declare const SCENE_THEMES: SceneTheme[];
28
26
  export declare const SCENE_THEME_IDS: string[];
29
27
  export declare function getSceneTheme(id: string): SceneTheme;
28
+ /**
29
+ * The dark mode the eye dives into under the ground: a neutral anthracite / slate grey, no hue.
30
+ * Neither dark scene theme is neutral (`night` is navy, `twilight` is purple), and a theme would
31
+ * be a thing the person picks, so the underground has its own palette, here beside the theme
32
+ * tokens (`lib/underground-ambiance.ts` is its one reader). Every stop is a grey (R = G = B);
33
+ * the backdrop is the theme backdrop's own gradient (`lib/backdrop.ts`) in these stops.
34
+ */
35
+ export declare const UNDERGROUND_PALETTE: {
36
+ /** The backdrop looking down, most of the frame under the lid: slate grey. */
37
+ readonly background: '#4a4a4a';
38
+ /** The thin band hugging the horizon. */
39
+ readonly haze: '#666666';
40
+ /** The sky above the horizon, and its zenith. */
41
+ readonly sky: '#1b1b1b';
42
+ readonly skyDeep: '#111111';
43
+ /** The corners the vignette closes in to. */
44
+ readonly vignette: '#0c0c0c';
45
+ /** The thin rim along the cut of the hole in the ground, from above. */
46
+ readonly rim: '#3c3c3c';
47
+ /** The grade ring under the ground: a light rim where the ground meets the level. */
48
+ readonly ring: '#b4b4b4';
49
+ /** The sky light's tint (ambient and hemisphere pulled toward it under the ground). */
50
+ readonly tint: '#c8c8c8';
51
+ };
30
52
  //# sourceMappingURL=scene-themes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-themes.d.ts","sourceRoot":"","sources":["../../src/lib/scene-themes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAEnD,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IAIZ,UAAU,EAAE,OAAO,GAAG,MAAM,CAAA;IAC5B,UAAU,EAAE,MAAM,CAAA;IAIlB,aAAa,CAAC,EAAE,MAAM,CAAA;IAItB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,MAAM,EAAE,KAAK,CAAC;QACZ,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAClC,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAC,CAAA;IACF,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,EAoMpC,CAAA;AAED,eAAO,MAAM,eAAe,UAAwC,CAAA;AAIpE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAEpD"}
1
+ {"version":3,"file":"scene-themes.d.ts","sourceRoot":"","sources":["../../src/lib/scene-themes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IAIZ,UAAU,EAAE,OAAO,GAAG,MAAM,CAAA;IAC5B,UAAU,EAAE,MAAM,CAAA;IAIlB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAI7B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;IACxE,mBAAmB,EAAE,MAAM,CAAA;IAC3B,WAAW,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;IACtE,cAAc,CAAC,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,CAAA;IACnD,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD,CAAA;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,gBAAgB,GAAG,UAAU,CAUhG;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,EAoMpC,CAAA;AAED,eAAO,MAAM,eAAe,UAAwC,CAAA;AAIpE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAEpD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;IAC9B,8EAA8E;aAC9E,UAAU,EAAE,SAAS;IACrB,yCAAyC;aACzC,IAAI,EAAE,SAAS;IACf,iDAAiD;aACjD,GAAG,EAAE,SAAS;aACd,OAAO,EAAE,SAAS;IAClB,6CAA6C;aAC7C,QAAQ,EAAE,SAAS;IACnB,wEAAwE;aACxE,GAAG,EAAE,SAAS;IACd,qFAAqF;aACrF,IAAI,EAAE,SAAS;IACf,uFAAuF;aACvF,IAAI,EAAE,SAAS;CACP,CAAA"}
@@ -1,3 +1,15 @@
1
+ export function projectSceneTheme(theme, presentation) {
2
+ if (!presentation)
3
+ return theme;
4
+ const { backgroundSky, ...lighting } = presentation.lighting ?? {};
5
+ return {
6
+ ...theme,
7
+ ...lighting,
8
+ ...(backgroundSky === undefined ? {} : { backgroundSky }),
9
+ toneMappingExposure: presentation.exposure ?? theme.toneMappingExposure,
10
+ postprocessing: presentation.postprocessing,
11
+ };
12
+ }
1
13
  export const SCENE_THEMES = [
2
14
  {
3
15
  id: 'studio',
@@ -200,3 +212,27 @@ const SCENE_THEME_BY_ID = new Map(SCENE_THEMES.map((theme) => [theme.id, theme])
200
212
  export function getSceneTheme(id) {
201
213
  return SCENE_THEME_BY_ID.get(id) ?? SCENE_THEMES[0];
202
214
  }
215
+ /**
216
+ * The dark mode the eye dives into under the ground: a neutral anthracite / slate grey, no hue.
217
+ * Neither dark scene theme is neutral (`night` is navy, `twilight` is purple), and a theme would
218
+ * be a thing the person picks, so the underground has its own palette, here beside the theme
219
+ * tokens (`lib/underground-ambiance.ts` is its one reader). Every stop is a grey (R = G = B);
220
+ * the backdrop is the theme backdrop's own gradient (`lib/backdrop.ts`) in these stops.
221
+ */
222
+ export const UNDERGROUND_PALETTE = {
223
+ /** The backdrop looking down, most of the frame under the lid: slate grey. */
224
+ background: '#4a4a4a',
225
+ /** The thin band hugging the horizon. */
226
+ haze: '#666666',
227
+ /** The sky above the horizon, and its zenith. */
228
+ sky: '#1b1b1b',
229
+ skyDeep: '#111111',
230
+ /** The corners the vignette closes in to. */
231
+ vignette: '#0c0c0c',
232
+ /** The thin rim along the cut of the hole in the ground, from above. */
233
+ rim: '#3c3c3c',
234
+ /** The grade ring under the ground: a light rim where the ground meets the level. */
235
+ ring: '#b4b4b4',
236
+ /** The sky light's tint (ambient and hemisphere pulled toward it under the ground). */
237
+ tint: '#c8c8c8',
238
+ };
@@ -5,8 +5,10 @@ import type { Object3D } from 'three';
5
5
  * - `isolated` — outside the focused subtree of the viewer's isolation filter.
6
6
  * - `shadow-only` — solo mode: out of the color passes, still casting shadows.
7
7
  * - `batched` — a level's merged wall mesh draws this wall now.
8
+ * - `ground-aside` — the site's ground while a level below grade is active: the floor is seen,
9
+ * and clicked, where the ground was.
8
10
  */
9
- export type HiddenReason = 'isolated' | 'shadow-only' | 'batched' | 'wall-batched';
11
+ export type HiddenReason = 'isolated' | 'shadow-only' | 'batched' | 'wall-batched' | 'ground-aside';
10
12
  /** Holds `obj` off the scene layer for `reason`. Idempotent per reason. */
11
13
  export declare function hideFromScene(obj: Object3D, reason: HiddenReason): void;
12
14
  /** Drops `reason`, restoring the mask `obj` had before the first one arrived. */
@@ -18,4 +20,12 @@ export declare function temporarilyShowShadowOnly(root: Object3D): () => void;
18
20
  * whole, whatever the user isolated.
19
21
  */
20
22
  export declare function temporarilyShowPresentation(root: Object3D): () => void;
23
+ /**
24
+ * The site's ground surfaces set aside, or stood back on, for one capture, whatever the person's
25
+ * view holds: a picture of a floor below grade sees through the ground, one of the whole building
26
+ * stands on it. The returned function gives the person's view back.
27
+ */
28
+ export declare function temporarilySetGroundAside(surfaces: readonly Object3D[], aside: boolean): () => void;
29
+ /** Add a capture-only mask without changing the live visibility reasons or batching. */
30
+ export declare function temporarilyHideFromScene(root: Object3D, reason: HiddenReason): () => void;
21
31
  //# sourceMappingURL=scene-visibility.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-visibility.d.ts","sourceRoot":"","sources":["../../src/lib/scene-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGrC;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,cAAc,CAAA;AAmBlF,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAMvE;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAarE;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,IAAI,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,IAAI,CAEtE"}
1
+ {"version":3,"file":"scene-visibility.d.ts","sourceRoot":"","sources":["../../src/lib/scene-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGrC;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,cAAc,GAAG,cAAc,CAAA;AAmBnG,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAMvE;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAarE;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,IAAI,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,IAAI,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,SAAS,QAAQ,EAAE,EAC7B,KAAK,EAAE,OAAO,GACb,MAAM,IAAI,CAYZ;AAED,wFAAwF;AACxF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,IAAI,CAczF"}
@@ -44,6 +44,44 @@ export function temporarilyShowShadowOnly(root) {
44
44
  export function temporarilyShowPresentation(root) {
45
45
  return temporarilyShowHeld(root, ['shadow-only', 'isolated']);
46
46
  }
47
+ /**
48
+ * The site's ground surfaces set aside, or stood back on, for one capture, whatever the person's
49
+ * view holds: a picture of a floor below grade sees through the ground, one of the whole building
50
+ * stands on it. The returned function gives the person's view back.
51
+ */
52
+ export function temporarilySetGroundAside(surfaces, aside) {
53
+ if (!aside) {
54
+ const restores = surfaces.map((surface) => temporarilyShowHeld(surface, ['ground-aside']));
55
+ return () => {
56
+ for (const restore of restores)
57
+ restore();
58
+ };
59
+ }
60
+ const held = surfaces.filter((surface) => !surface[HOLD]?.reasons.has('ground-aside'));
61
+ for (const surface of held)
62
+ hideFromScene(surface, 'ground-aside');
63
+ return () => {
64
+ for (const surface of held)
65
+ showInScene(surface, 'ground-aside');
66
+ };
67
+ }
68
+ /** Add a capture-only mask without changing the live visibility reasons or batching. */
69
+ export function temporarilyHideFromScene(root, reason) {
70
+ const masks = new Map();
71
+ root.traverse((obj) => {
72
+ masks.set(obj, obj.layers.mask);
73
+ const hold = obj[HOLD];
74
+ const reasons = new Set(hold?.reasons);
75
+ // Changing level presentation must not reveal isolated geometry or duplicate batch shadows.
76
+ reasons.delete('shadow-only');
77
+ reasons.add(reason);
78
+ applyHold(obj, { original: hold?.original ?? obj.layers.mask, reasons });
79
+ });
80
+ return () => {
81
+ for (const [obj, mask] of masks)
82
+ obj.layers.mask = mask;
83
+ };
84
+ }
47
85
  function temporarilyShowHeld(root, lifted) {
48
86
  const masks = new Map();
49
87
  root.traverse((obj) => {
@@ -0,0 +1,32 @@
1
+ import type { Object3D } from 'three';
2
+ /**
3
+ * `userData` key marking the site's ground surfaces with their {@link SiteGroundRole}: the lines
4
+ * drawn on the ground (property line, setbacks, contours) carry none.
5
+ */
6
+ export declare const SITE_GROUND = "siteGround";
7
+ /**
8
+ * `userData` key marking the generic foundation under the buildings (footing and stem, drawn under
9
+ * the ground): a capture that sees through the veil leaves it out, as it left it out before it was drawn.
10
+ */
11
+ export declare const SITE_FOUNDATION = "siteFoundation";
12
+ /**
13
+ * `solid`: the lot's fill, its sculpted terrain, the horizon disc. `veil`: what is drawn in their
14
+ * place over a level below grade, the grade ring with it.
15
+ */
16
+ export type SiteGroundRole = 'solid' | 'veil';
17
+ /**
18
+ * The site's own shown surfaces (ground, horizon disc, property and setback lines, contours), not
19
+ * its registered children (buildings, items), which are groups.
20
+ */
21
+ export declare function siteSurfaces(): Object3D[];
22
+ /** The site's generic foundation meshes, shown or not. */
23
+ export declare function siteFoundation(): Object3D[];
24
+ /** The site's ground by role, shown or not: the veil is hidden until a level below grade is shown. */
25
+ export declare function siteGround(): Record<SiteGroundRole, Object3D[]>;
26
+ /**
27
+ * The ground veiled, or solid, for one capture, whatever the person's view shows: a picture of a
28
+ * floor below grade sees it through the veil, a picture of the whole building stands on solid
29
+ * ground. The returned function gives the person's view back.
30
+ */
31
+ export declare function temporarilyVeilGround(veiled: boolean): () => void;
32
+ //# sourceMappingURL=site-surfaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-surfaces.d.ts","sourceRoot":"","sources":["../../src/lib/site-surfaces.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGrC;;;GAGG;AACH,eAAO,MAAM,WAAW,eAAe,CAAA;AAEvC;;;GAGG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAE/C;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,CAAA;AAE7C;;;GAGG;AACH,wBAAgB,YAAY,IAAI,QAAQ,EAAE,CAUzC;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,IAAI,QAAQ,EAAE,CAQ3C;AAED,sGAAsG;AACtG,wBAAgB,UAAU,IAAI,MAAM,CAAC,cAAc,EAAE,QAAQ,EAAE,CAAC,CAS/D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,CAYjE"}
@@ -0,0 +1,70 @@
1
+ import { sceneRegistry } from '@pascal-app/core';
2
+ import { temporarilySetGroundAside } from './scene-visibility';
3
+ /**
4
+ * `userData` key marking the site's ground surfaces with their {@link SiteGroundRole}: the lines
5
+ * drawn on the ground (property line, setbacks, contours) carry none.
6
+ */
7
+ export const SITE_GROUND = 'siteGround';
8
+ /**
9
+ * `userData` key marking the generic foundation under the buildings (footing and stem, drawn under
10
+ * the ground): a capture that sees through the veil leaves it out, as it left it out before it was drawn.
11
+ */
12
+ export const SITE_FOUNDATION = 'siteFoundation';
13
+ /**
14
+ * The site's own shown surfaces (ground, horizon disc, property and setback lines, contours), not
15
+ * its registered children (buildings, items), which are groups.
16
+ */
17
+ export function siteSurfaces() {
18
+ const surfaces = [];
19
+ for (const siteId of sceneRegistry.byType.site ?? []) {
20
+ for (const child of sceneRegistry.nodes.get(siteId)?.children ?? []) {
21
+ if (child.visible && !(child.type === 'Group' && child.children.length > 0)) {
22
+ surfaces.push(child);
23
+ }
24
+ }
25
+ }
26
+ return surfaces;
27
+ }
28
+ /** The site's generic foundation meshes, shown or not. */
29
+ export function siteFoundation() {
30
+ const foundation = [];
31
+ for (const siteId of sceneRegistry.byType.site ?? []) {
32
+ for (const child of sceneRegistry.nodes.get(siteId)?.children ?? []) {
33
+ if (child.userData[SITE_FOUNDATION] === true)
34
+ foundation.push(child);
35
+ }
36
+ }
37
+ return foundation;
38
+ }
39
+ /** The site's ground by role, shown or not: the veil is hidden until a level below grade is shown. */
40
+ export function siteGround() {
41
+ const ground = { solid: [], veil: [] };
42
+ for (const siteId of sceneRegistry.byType.site ?? []) {
43
+ for (const child of sceneRegistry.nodes.get(siteId)?.children ?? []) {
44
+ const role = child.userData[SITE_GROUND];
45
+ if (role === 'solid' || role === 'veil')
46
+ ground[role].push(child);
47
+ }
48
+ }
49
+ return ground;
50
+ }
51
+ /**
52
+ * The ground veiled, or solid, for one capture, whatever the person's view shows: a picture of a
53
+ * floor below grade sees it through the veil, a picture of the whole building stands on solid
54
+ * ground. The returned function gives the person's view back.
55
+ */
56
+ export function temporarilyVeilGround(veiled) {
57
+ const { solid, veil } = siteGround();
58
+ const restoreSolid = temporarilySetGroundAside(solid, veiled);
59
+ // Seen through a veil the footing under a floor below grade would show: the picture has none.
60
+ const restoreFoundation = veiled ? temporarilySetGroundAside(siteFoundation(), true) : () => { };
61
+ const flipped = veil.filter((surface) => surface.visible !== veiled);
62
+ for (const surface of flipped)
63
+ surface.visible = veiled;
64
+ return () => {
65
+ for (const surface of flipped)
66
+ surface.visible = !veiled;
67
+ restoreFoundation();
68
+ restoreSolid();
69
+ };
70
+ }
@@ -2,6 +2,7 @@ import { type Camera, type Scene } from 'three';
2
2
  import { type WebGPURenderer } from 'three/webgpu';
3
3
  import type { SceneAtmosphereSource } from '../components/viewer/scene-atmosphere';
4
4
  import { type EdgeMode } from './edge-style';
5
+ import type { SceneTheme } from './scene-themes';
5
6
  export declare const THUMBNAIL_WIDTH = 1920;
6
7
  export declare const THUMBNAIL_HEIGHT = 1080;
7
8
  /**
@@ -46,7 +47,7 @@ export type StudioBackdrop = {
46
47
  };
47
48
  export type SnapshotPipeline = {
48
49
  applyEnvironment: ({ theme, transparent, grade, edges, camera, }: {
49
- theme: string;
50
+ theme: SceneTheme;
50
51
  transparent: boolean;
51
52
  grade: boolean;
52
53
  edges: EdgeMode;
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot-pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/snapshot-pipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"snapshot-pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/snapshot-pipeline.ts"],"names":[],"mappings":"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"}
@@ -2,13 +2,13 @@ import { Color, Matrix4, SRGBColorSpace, UnsignedByteType } from 'three';
2
2
  import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
3
3
  import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js';
4
4
  import { fxaa } from 'three/examples/jsm/tsl/display/FXAANode.js';
5
- import { clamp, convertToTexture, diffuseColor, float, mix, mrt, normalView, output, renderOutput, sample, saturation, screenUV, smoothstep, uniform, vec2, vec3, vec4, } from 'three/tsl';
5
+ import { clamp, convertToTexture, diffuseColor, float, mix, normalView, output, renderOutput, sample, saturation, screenUV, smoothstep, uniform, vec2, vec3, vec4, } from 'three/tsl';
6
6
  import { PassNode, RenderPipeline, RenderTarget, } from 'three/webgpu';
7
7
  import { GRADE_PARAMS, SSGI_PARAMS } from '../components/viewer/post-processing';
8
- import { backdropGradient, deepSkyColor, horizonHazeColor } from './backdrop';
8
+ import { backdropColors, backdropGradient, flatBackdropOutput } from './backdrop';
9
9
  import { edgeColorFor, edgeOpacityScaleFor } from './edge-style';
10
10
  import { createEdgeDepthSampler, inkedEdges } from './ink-edges';
11
- import { getSceneTheme } from './scene-themes';
11
+ import { sceneLightingMask, scenePassMrt } from './scene-pass-mrt';
12
12
  import { packNormalToRGB, unpackRGBToNormal } from './tsl-compat';
13
13
  export const THUMBNAIL_WIDTH = 1920;
14
14
  export const THUMBNAIL_HEIGHT = 1080;
@@ -64,7 +64,9 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
64
64
  const bgProjInvUniform = uniform(new Matrix4());
65
65
  const bgCamWorldUniform = uniform(new Matrix4());
66
66
  const bgMixUniform = uniform(1);
67
+ const flatBgMixUniform = uniform(0);
67
68
  const gradeMixUniform = uniform(0);
69
+ const aoMixUniform = uniform(1);
68
70
  const inkMixUniform = uniform(0);
69
71
  const inkColorUniform = uniform(new Color('#1a1d24'));
70
72
  const inkOpacityUniform = uniform(0.5);
@@ -72,10 +74,11 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
72
74
  // The scene pass owns MRT and its transparent clear, including when nested
73
75
  // inside FXAA's intermediate render target.
74
76
  const scenePass = new SnapshotScenePass('color', scene, camera);
75
- scenePass.setMRT(mrt({
77
+ scenePass.setMRT(scenePassMrt({
76
78
  output,
77
79
  diffuseColor,
78
80
  normal: packNormalToRGB(normalView),
81
+ lighting: sceneLightingMask(),
79
82
  }));
80
83
  const scenePassColor = scenePass.getTextureNode('output');
81
84
  const scenePassDepth = scenePass.getTextureNode('depth');
@@ -83,10 +86,12 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
83
86
  const scenePassNormal = scenePass.getTextureNode('normal');
84
87
  scenePass.getTexture('diffuseColor').type = UnsignedByteType;
85
88
  scenePass.getTexture('normal').type = UnsignedByteType;
89
+ scenePass.getTexture('lighting').type = UnsignedByteType;
86
90
  const sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)));
87
91
  const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera);
88
- giPass.sliceCount.value = SSGI_PARAMS.sliceCount;
89
- giPass.stepCount.value = SSGI_PARAMS.stepCount;
92
+ // A saved still has no later frames to hide the viewport's sampling noise.
93
+ giPass.sliceCount.value = 3;
94
+ giPass.stepCount.value = 8;
90
95
  giPass.radius.value = SSGI_PARAMS.radius;
91
96
  giPass.expFactor.value = SSGI_PARAMS.expFactor;
92
97
  giPass.thickness.value = SSGI_PARAMS.thickness;
@@ -107,13 +112,13 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
107
112
  // horizon picks up a visible AO line in captures.
108
113
  const aoFarFade = smoothstep(float(0.9994), float(0.9998), sampleEdgeDepth(screenUV));
109
114
  const ao = mix(denoisePass.r, float(1), aoFarFade);
110
- const aoRgb = scenePassColor.rgb.mul(ao);
115
+ const aoRgb = scenePassColor.rgb.mul(mix(float(1), ao, scenePass.getTextureNode('lighting').r.mul(aoMixUniform)));
111
116
  // Ink edges, mirroring the viewport pipeline (AO → ink → grade) so
112
117
  // captures carry the same soft/strong edge look the canvas shows.
113
118
  // Uniform-gated like grade/backdrop: one cached pipeline serves all modes.
114
119
  // Radius scales with render height: a supersampled capture (4K → 1080p)
115
120
  // would otherwise halve the apparent line weight vs the viewport's 1px.
116
- const inkRadius = Math.max(1, Math.round(renderer.domElement.height / 1080));
121
+ const inkRadius = uniform(1);
117
122
  const inkedRgb = inkedEdges({
118
123
  sceneRgb: aoRgb,
119
124
  sampleDepth: sampleEdgeDepth,
@@ -167,7 +172,17 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
167
172
  else {
168
173
  // FXAA requires a texture node as input; convertToTexture renders finalOutput
169
174
  // into an intermediate RT so FXAA can sample it with neighbour UV offsets.
170
- pipeline.outputNode = fxaa(convertToTexture(finalOutput));
175
+ const flatOutput = flatBackdropOutput({
176
+ background: bgColorUniform,
177
+ foreground: sceneRgb,
178
+ alpha,
179
+ toneMapping: renderer.toneMapping,
180
+ outputColorSpace: renderer.outputColorSpace,
181
+ });
182
+ const antialiased = fxaa(convertToTexture(mix(finalOutput, flatOutput, flatBgMixUniform)));
183
+ // One FXAA pass serves both inputs; transform only the linear input at output.
184
+ pipeline.outputColorTransform = false;
185
+ pipeline.outputNode = mix(renderOutput(antialiased, renderer.toneMapping, renderer.outputColorSpace), antialiased, flatBgMixUniform);
171
186
  }
172
187
  // Dedicated render target — pipeline outputs here instead of the canvas,
173
188
  // so R3F's main render loop can never overwrite our capture.
@@ -175,17 +190,21 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
175
190
  const renderTarget = new RenderTarget(width, height, { depthBuffer: true });
176
191
  return {
177
192
  applyEnvironment: ({ theme, transparent, grade, edges, camera: captureCamera }) => {
178
- const sceneTheme = getSceneTheme(theme);
193
+ const sceneTheme = theme;
194
+ const backdrop = backdropColors(sceneTheme);
179
195
  inkMixUniform.value = edges === 'off' ? 0 : 1;
180
196
  inkOpacityUniform.value = edges === 'strong' ? 1 : 0.5;
181
197
  inkColorUniform.value.set(edgeColorFor(sceneTheme.background));
182
198
  inkOpacityScaleUniform.value = edgeOpacityScaleFor(sceneTheme.background);
183
199
  bgColorUniform.value.set(sceneTheme.background);
184
- bgSkyUniform.value.set(sceneTheme.backgroundSky ?? sceneTheme.background);
185
- bgSkyDeepUniform.value.set(deepSkyColor(sceneTheme.backgroundSky ?? sceneTheme.background));
186
- bgHazeUniform.value.set(horizonHazeColor(sceneTheme.backgroundSky ?? sceneTheme.background, sceneTheme.appearance));
200
+ bgSkyUniform.value.set(backdrop.sky);
201
+ bgSkyDeepUniform.value.set(backdrop.skyDeep);
202
+ bgHazeUniform.value.set(backdrop.haze);
187
203
  bgMixUniform.value = transparent ? 0 : 1;
188
- gradeMixUniform.value = grade ? 1 : 0;
204
+ flatBgMixUniform.value =
205
+ !transparent && !atmosphere && sceneTheme.backgroundSky === null ? 1 : 0;
206
+ gradeMixUniform.value = grade && sceneTheme.postprocessing?.colorGrading !== false ? 1 : 0;
207
+ aoMixUniform.value = sceneTheme.postprocessing?.ambientOcclusion === false ? 0 : 1;
189
208
  // The capture camera never joins the scene graph, so its matrixWorld
190
209
  // is only refreshed by the render itself — too late for the backdrop
191
210
  // uniforms below.
@@ -197,6 +216,7 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
197
216
  const standardW = standardSize?.w ?? THUMBNAIL_WIDTH;
198
217
  const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT;
199
218
  const { width: captureWidth, height: captureHeight } = renderer.domElement;
219
+ inkRadius.value = Math.max(1, Math.round(captureHeight / 1080));
200
220
  // Resize RT if the canvas dimensions changed
201
221
  if (renderTarget.width !== captureWidth || renderTarget.height !== captureHeight) {
202
222
  renderTarget.setSize(captureWidth, captureHeight);
@@ -351,7 +371,7 @@ export async function createPlainSnapshotPipeline({ renderer, scene, camera, })
351
371
  return {
352
372
  applyEnvironment: ({ theme, transparent: alpha }) => {
353
373
  transparent = alpha;
354
- background.set(getSceneTheme(theme).background);
374
+ background.set(theme.background);
355
375
  },
356
376
  capture: async ({ captureMode, cropRegion, standardSize }) => {
357
377
  const standardW = standardSize?.w ?? THUMBNAIL_WIDTH;
@@ -7,6 +7,6 @@
7
7
  * @types/three pin) once tsgo handles the 0.185 types; the `typeof` references
8
8
  * to the removed old names will fail the build as a reminder.
9
9
  */
10
- export declare const packNormalToRGB: (node: import("three/webgpu").Node) => import("three/webgpu").Node<"vec3">;
11
- export declare const unpackRGBToNormal: (node: import("three/webgpu").Node) => import("three/webgpu").Node<"vec3">;
10
+ export declare const packNormalToRGB: (node: import("three/webgpu").Node<"vec3"> | import("three").Vector3) => import("three/webgpu").Node<"vec3">;
11
+ export declare const unpackRGBToNormal: (node: import("three/webgpu").Node<"vec3"> | import("three/webgpu").Node<"vec4"> | import("three").Vector3) => import("three/webgpu").Node<"vec3">;
12
12
  //# sourceMappingURL=tsl-compat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tsl-compat.d.ts","sourceRoot":"","sources":["../../src/lib/tsl-compat.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,4EACV,CAAA;AAClB,eAAO,MAAM,iBAAiB,4EAEX,CAAA"}
1
+ {"version":3,"file":"tsl-compat.d.ts","sourceRoot":"","sources":["../../src/lib/tsl-compat.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,8GACV,CAAA;AAClB,eAAO,MAAM,iBAAiB,oJAEX,CAAA"}