@jgengine/shell 0.18.1 → 0.21.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 (261) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +132 -17
  5. package/dist/Shell3dPresentation.d.ts +5 -11
  6. package/dist/Shell3dPresentation.js +73 -11
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +20 -2
  10. package/dist/ShellHudPresentation.js +41 -6
  11. package/dist/audio/AudioComponents.js +6 -2
  12. package/dist/audio/audioEngine.d.ts +24 -0
  13. package/dist/audio/audioEngine.js +231 -83
  14. package/dist/audio/audioWire.d.ts +3 -0
  15. package/dist/audio/audioWire.js +9 -3
  16. package/dist/audio/loopParams.d.ts +39 -0
  17. package/dist/audio/loopParams.js +43 -0
  18. package/dist/audio/synthEngine.d.ts +7 -1
  19. package/dist/audio/synthEngine.js +103 -38
  20. package/dist/camera/GameCameraRig.js +3 -2
  21. package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
  22. package/dist/camera/GameFirstPersonCamera.js +68 -27
  23. package/dist/camera/Viewports.d.ts +34 -0
  24. package/dist/camera/Viewports.js +123 -0
  25. package/dist/camera/cameraRigs.js +76 -62
  26. package/dist/camera/chaseHeading.d.ts +9 -0
  27. package/dist/camera/chaseHeading.js +10 -0
  28. package/dist/camera/customMuzzle.d.ts +14 -0
  29. package/dist/camera/customMuzzle.js +68 -0
  30. package/dist/camera/index.d.ts +1 -0
  31. package/dist/camera/index.js +1 -0
  32. package/dist/camera/orbitCameraMath.js +2 -2
  33. package/dist/camera/rigMath.d.ts +82 -4
  34. package/dist/camera/rigMath.js +133 -15
  35. package/dist/camera/rigResolve.d.ts +3 -0
  36. package/dist/camera/rigResolve.js +11 -0
  37. package/dist/combat/ProjectileModels.d.ts +31 -0
  38. package/dist/combat/ProjectileModels.js +70 -0
  39. package/dist/commandSink.d.ts +1 -2
  40. package/dist/commandSink.js +3 -2
  41. package/dist/defineGame.d.ts +19 -3
  42. package/dist/defineGame.js +39 -7
  43. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  44. package/dist/devtools/DevtoolsOverlay.js +15 -167
  45. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  46. package/dist/devtools/DevtoolsRuntime.js +61 -0
  47. package/dist/devtools/agentBridge.d.ts +6 -0
  48. package/dist/devtools/agentBridge.js +16 -2
  49. package/dist/devtools/devtoolsReports.d.ts +68 -0
  50. package/dist/devtools/devtoolsReports.js +100 -0
  51. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  52. package/dist/devtools/latencyInstrumentation.js +11 -0
  53. package/dist/devtools/playtestProbe.d.ts +4 -0
  54. package/dist/devtools/playtestProbe.js +22 -0
  55. package/dist/diagnostics/FpsCounter.d.ts +17 -0
  56. package/dist/diagnostics/FpsCounter.js +32 -0
  57. package/dist/drivers/FrameDriver.js +35 -20
  58. package/dist/drivers/FrameRateLimiter.d.ts +9 -0
  59. package/dist/drivers/FrameRateLimiter.js +39 -0
  60. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  61. package/dist/drivers/HudOnlyDriver.js +32 -14
  62. package/dist/environment/DayNightSky.js +5 -5
  63. package/dist/environment/Daylight.d.ts +35 -1
  64. package/dist/environment/Daylight.js +98 -28
  65. package/dist/environment/EnvironmentScene.js +11 -10
  66. package/dist/environment/VolumetricClouds.js +20 -2
  67. package/dist/environment/daylightCycle.d.ts +20 -0
  68. package/dist/environment/daylightCycle.js +59 -7
  69. package/dist/environment/sunShadowMath.d.ts +45 -0
  70. package/dist/environment/sunShadowMath.js +50 -0
  71. package/dist/input/gamepadPoll.d.ts +47 -0
  72. package/dist/input/gamepadPoll.js +142 -0
  73. package/dist/input/gamepadSource.d.ts +11 -1
  74. package/dist/input/gamepadSource.js +117 -71
  75. package/dist/input/mouseLook.js +2 -1
  76. package/dist/input/padHaptics.d.ts +17 -0
  77. package/dist/input/padHaptics.js +32 -0
  78. package/dist/input/pointerLock.d.ts +11 -0
  79. package/dist/input/pointerLock.js +19 -0
  80. package/dist/inputSink.d.ts +6 -6
  81. package/dist/inputSink.js +60 -21
  82. package/dist/lazyShell3dPresentation.d.ts +9 -0
  83. package/dist/lazyShell3dPresentation.js +24 -0
  84. package/dist/materialOverride.d.ts +42 -0
  85. package/dist/materialOverride.js +95 -36
  86. package/dist/multiplayer.d.ts +33 -3
  87. package/dist/multiplayer.js +105 -22
  88. package/dist/pointer/pointerService.d.ts +2 -0
  89. package/dist/pointer/pointerService.js +12 -2
  90. package/dist/postfx/PostProcessing.d.ts +11 -1
  91. package/dist/postfx/PostProcessing.js +14 -42
  92. package/dist/postfx/gradeShader.d.ts +1 -1
  93. package/dist/postfx/gradeShader.js +30 -2
  94. package/dist/postfx/shadowPrepass.d.ts +3 -0
  95. package/dist/postfx/shadowPrepass.js +18 -0
  96. package/dist/postfx/stylizePass.d.ts +23 -0
  97. package/dist/postfx/stylizePass.js +216 -0
  98. package/dist/presentationRecovery.d.ts +75 -0
  99. package/dist/presentationRecovery.js +204 -0
  100. package/dist/presentationRecovery3d.d.ts +4 -0
  101. package/dist/presentationRecovery3d.js +32 -0
  102. package/dist/presentationResolve.d.ts +8 -1
  103. package/dist/presentationResolve.js +5 -2
  104. package/dist/render/CascadedShadows.d.ts +7 -2
  105. package/dist/render/CascadedShadows.js +98 -20
  106. package/dist/render/EnvironmentLighting.js +74 -35
  107. package/dist/render/HairCards.d.ts +7 -0
  108. package/dist/render/HairCards.js +23 -0
  109. package/dist/render/SceneModels.js +52 -78
  110. package/dist/render/SkinnedInstances.d.ts +91 -0
  111. package/dist/render/SkinnedInstances.js +249 -0
  112. package/dist/render/TextureFiltering.d.ts +16 -0
  113. package/dist/render/TextureFiltering.js +64 -0
  114. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  115. package/dist/render/authoredSurfaceMaterial.js +108 -0
  116. package/dist/render/displayColor.d.ts +33 -0
  117. package/dist/render/displayColor.js +138 -0
  118. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  119. package/dist/render/materialAppearanceSignals.js +58 -0
  120. package/dist/render/materialAsset.d.ts +98 -0
  121. package/dist/render/materialAsset.js +454 -0
  122. package/dist/render/measureBounds.js +16 -3
  123. package/dist/render/measureCollisionMesh.js +8 -1
  124. package/dist/render/modelAssets.d.ts +8 -0
  125. package/dist/render/modelAssets.js +43 -0
  126. package/dist/render/modelBindPose.d.ts +5 -0
  127. package/dist/render/modelBindPose.js +35 -0
  128. package/dist/render/modelLoad.d.ts +32 -0
  129. package/dist/render/modelLoad.js +70 -1
  130. package/dist/render/modelNodeVisibility.d.ts +6 -0
  131. package/dist/render/modelNodeVisibility.js +18 -0
  132. package/dist/render/modelRender.d.ts +18 -1
  133. package/dist/render/modelRender.js +81 -3
  134. package/dist/render/projectedOverdraw.d.ts +35 -0
  135. package/dist/render/projectedOverdraw.js +204 -0
  136. package/dist/render/resolveModel.d.ts +12 -2
  137. package/dist/render/resolveModel.js +25 -4
  138. package/dist/render/rigNode.d.ts +26 -0
  139. package/dist/render/rigNode.js +32 -0
  140. package/dist/render/spriteRender.d.ts +4 -0
  141. package/dist/render/spriteRender.js +5 -0
  142. package/dist/render/staticShapeInstances.d.ts +34 -0
  143. package/dist/render/staticShapeInstances.js +78 -0
  144. package/dist/render/useFootIk.d.ts +53 -4
  145. package/dist/render/useFootIk.js +262 -70
  146. package/dist/render/useModelAnimation.d.ts +50 -0
  147. package/dist/render/useModelAnimation.js +245 -99
  148. package/dist/render/useModelInstance.d.ts +25 -0
  149. package/dist/render/useModelInstance.js +43 -0
  150. package/dist/scatter/scatterModels.d.ts +3 -2
  151. package/dist/scatter/scatterModels.js +15 -7
  152. package/dist/scene/AuthoredScene.d.ts +23 -6
  153. package/dist/scene/AuthoredScene.js +46 -9
  154. package/dist/scene/CityRenderer.js +6 -6
  155. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  156. package/dist/settings/QuickControls.js +1 -1
  157. package/dist/settings/SettingsMenu.js +11 -18
  158. package/dist/settings/SettingsRuntime.d.ts +5 -0
  159. package/dist/settings/SettingsRuntime.js +29 -2
  160. package/dist/settings/appliedSettings.d.ts +9 -3
  161. package/dist/settings/appliedSettings.js +6 -5
  162. package/dist/settings/settingsController.d.ts +3 -0
  163. package/dist/settings/settingsController.js +47 -3
  164. package/dist/shellControlSuspension.d.ts +18 -0
  165. package/dist/shellControlSuspension.js +28 -0
  166. package/dist/shellInputPublication.d.ts +28 -0
  167. package/dist/shellInputPublication.js +49 -0
  168. package/dist/shellPointerInput.d.ts +18 -0
  169. package/dist/shellPointerInput.js +49 -0
  170. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  171. package/dist/structures/BuildingKitBatch.js +35 -56
  172. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  173. package/dist/structures/BuildingRenderBudget.js +54 -0
  174. package/dist/structures/GeneratedBuilding.d.ts +3 -15
  175. package/dist/structures/GeneratedBuilding.js +33 -22
  176. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  177. package/dist/structures/buildingKitMaterials.js +70 -0
  178. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  179. package/dist/structures/buildingSpatialBatch.js +45 -0
  180. package/dist/structures/buildingSurface.d.ts +21 -0
  181. package/dist/structures/buildingSurface.js +134 -0
  182. package/dist/terrain/GrassField.d.ts +2 -1
  183. package/dist/terrain/GrassField.js +59 -5
  184. package/dist/terrain/grassGeometry.d.ts +10 -0
  185. package/dist/terrain/grassGeometry.js +55 -0
  186. package/dist/terrain/grassMaterial.js +2 -2
  187. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  188. package/dist/terrain/terrainDetailMaterial.js +7 -7
  189. package/dist/touch/TouchControlsOverlay.js +4 -4
  190. package/dist/useShellMultiplayerSync.d.ts +1 -1
  191. package/dist/useShellMultiplayerSync.js +9 -3
  192. package/dist/vfx/ParticleField.d.ts +23 -20
  193. package/dist/vfx/ParticleField.js +168 -50
  194. package/dist/visibility/CullingProvider.js +2 -62
  195. package/dist/visibility/cullingDriver.d.ts +9 -0
  196. package/dist/visibility/cullingDriver.js +68 -0
  197. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  198. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  199. package/dist/weather/DustField.d.ts +2 -1
  200. package/dist/weather/DustField.js +17 -3
  201. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  202. package/dist/weather/FireSpreadLayer.js +5 -11
  203. package/dist/weather/LightningStrike.d.ts +4 -1
  204. package/dist/weather/LightningStrike.js +13 -17
  205. package/dist/weather/RainField.d.ts +2 -1
  206. package/dist/weather/RainField.js +17 -3
  207. package/dist/weather/RainImpactField.d.ts +16 -0
  208. package/dist/weather/RainImpactField.js +89 -0
  209. package/dist/weather/SnowField.d.ts +2 -1
  210. package/dist/weather/SnowField.js +17 -3
  211. package/dist/weather/WeatherLayer.d.ts +11 -2
  212. package/dist/weather/WeatherLayer.js +30 -6
  213. package/dist/weather/index.d.ts +2 -0
  214. package/dist/weather/index.js +2 -0
  215. package/dist/weather/weatherMath.d.ts +2 -0
  216. package/dist/weather/weatherMath.js +8 -0
  217. package/dist/weather/weatherShelter.d.ts +18 -0
  218. package/dist/weather/weatherShelter.js +75 -0
  219. package/dist/weather/weatherUniforms.d.ts +40 -1
  220. package/dist/weather/weatherUniforms.js +29 -9
  221. package/dist/world/AuthoredFlocks.d.ts +22 -0
  222. package/dist/world/AuthoredFlocks.js +87 -0
  223. package/dist/world/StaticObjectModels.d.ts +8 -0
  224. package/dist/world/StaticObjectModels.js +75 -0
  225. package/dist/world/WorldHud.d.ts +14 -6
  226. package/dist/world/WorldHud.js +25 -12
  227. package/dist/world/WorldParticles.d.ts +10 -23
  228. package/dist/world/WorldParticles.js +210 -62
  229. package/dist/world/WorldScene.d.ts +3 -1
  230. package/dist/world/WorldScene.js +41 -26
  231. package/dist/world/staticObjectBatches.d.ts +35 -0
  232. package/dist/world/staticObjectBatches.js +163 -0
  233. package/dist/world/worldBarSamples.d.ts +37 -33
  234. package/dist/world/worldBarSamples.js +117 -75
  235. package/package.json +4 -4
  236. package/skills/ce-handoff/SKILL.md +2 -2
  237. package/skills/game-design/SKILL.md +2 -2
  238. package/skills/jgengine/SKILL.md +28 -6
  239. package/skills/jgengine/api.md +139 -99
  240. package/skills/jgengine/capabilities.md +28 -0
  241. package/skills/jgengine/recipes/minimal-game.md +10 -2
  242. package/skills/jgengine-ui/SKILL.md +16 -17
  243. package/skills/jgengine-ui/api.md +603 -365
  244. package/skills/jgengine-ui/capabilities.md +131 -1
  245. package/skills/jgengine-ui/reference-primitives.md +179 -0
  246. package/skills/jgengine-ui/reference.md +4 -81
  247. package/skills/jgengine-world/SKILL.md +25 -4
  248. package/skills/jgengine-world/api.md +1045 -655
  249. package/skills/jgengine-world/capabilities.md +246 -9
  250. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  251. package/skills/jgengine-world/recipes/controls.md +48 -0
  252. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  253. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  254. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  255. package/skills/jgengine-world/reference.md +180 -4
  256. package/skills/jgengine-world/visibility.md +139 -0
  257. package/skills/level-design/SKILL.md +1 -1
  258. package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
  259. package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
  260. package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
  261. package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
@@ -7,7 +7,8 @@ import { GTAOPass } from "three/examples/jsm/postprocessing/GTAOPass.js";
7
7
  import { BokehPass } from "three/examples/jsm/postprocessing/BokehPass.js";
8
8
  import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
9
9
  import { createGradePass } from "./gradeShader.js";
10
- import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
10
+ import { StylizePass } from "./stylizePass.js";
11
+ import { renderShadowlessPrepass } from "./shadowPrepass.js";
11
12
  /**
12
13
  * GTAO renders the scene with an `overrideMaterial` normal/depth prepass, which stamps
13
14
  * transparent overlay quads (additive VFX sprites, telegraphs, debug gizmos) into the AO
@@ -18,18 +19,14 @@ import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
18
19
  class OverlayAwareGTAOPass extends GTAOPass {
19
20
  hiddenOverlays = [];
20
21
  render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
21
- hidePostfxOverlays(this.scene, this.hiddenOverlays);
22
- super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive);
23
- restorePostfxOverlays(this.hiddenOverlays);
22
+ renderShadowlessPrepass(renderer, this.scene, this.hiddenOverlays, () => super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive));
24
23
  }
25
24
  }
26
25
  /** DOF's depth prepass has the same `overrideMaterial` overlay-stamping problem as GTAO. */
27
26
  class OverlayAwareBokehPass extends BokehPass {
28
27
  hiddenOverlays = [];
29
28
  render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
30
- hidePostfxOverlays(this.scene, this.hiddenOverlays);
31
- super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive);
32
- restorePostfxOverlays(this.hiddenOverlays);
29
+ renderShadowlessPrepass(renderer, this.scene, this.hiddenOverlays, () => super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive));
33
30
  }
34
31
  }
35
32
  const TONE_MAPPING = {
@@ -40,27 +37,21 @@ const TONE_MAPPING = {
40
37
  linear: THREE.LinearToneMapping,
41
38
  none: THREE.NoToneMapping,
42
39
  };
43
- function disposeGraph(built) {
40
+ /** Release each pass and the composer's owned targets. @internal */
41
+ export function disposeGraph(built) {
44
42
  for (const pass of built.composer.passes)
45
43
  pass.dispose();
46
- if (built.dof !== null) {
47
- built.dof.renderTargetDepth.dispose();
48
- built.dof.materialDepth.dispose();
49
- built.dof.materialBokeh.dispose();
50
- built.dof.fsQuad.dispose();
51
- }
52
44
  built.composer.dispose();
53
45
  }
54
- function syncSize(built, width, height, pixelRatio) {
46
+ /** Resize the composer and its passes at the renderer's pixel ratio. @internal */
47
+ export function syncSize(built, width, height, pixelRatio) {
55
48
  built.composer.setPixelRatio(pixelRatio);
56
49
  built.composer.setSize(width, height);
57
- if (built.dof !== null)
58
- built.dof.renderTargetDepth.setSize(width * pixelRatio, height * pixelRatio);
59
50
  }
60
51
  /**
61
52
  * Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
62
53
  * (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
63
- * post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
54
+ * post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only
64
55
  * when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
65
56
  *
66
57
  * `stages` (resolved from the player's graphics profile) controls expensive passes
@@ -101,10 +92,9 @@ export function PostProcessing({ config, quality = "high", stages }) {
101
92
  const b = config.bloom ?? {};
102
93
  composer.addPass(new UnrealBloomPass(new THREE.Vector2(width, height), b.strength ?? 0.32, b.radius ?? 0.55, b.threshold ?? 0.85));
103
94
  }
104
- let dof = null;
105
95
  if (resolvedStages.dof && config.dof !== undefined && config.dof !== false) {
106
96
  const d = config.dof;
107
- dof = new OverlayAwareBokehPass(scene, camera, {
97
+ const dof = new OverlayAwareBokehPass(scene, camera, {
108
98
  focus: d.focus ?? 18,
109
99
  aperture: d.aperture ?? (quality === "high" ? 0.00025 : 0.00012),
110
100
  maxblur: d.maxBlur ?? 0.01,
@@ -116,12 +106,15 @@ export function PostProcessing({ config, quality = "high", stages }) {
116
106
  composer.addPass(new SMAAPass());
117
107
  }
118
108
  composer.addPass(new OutputPass());
109
+ if (config.stylize !== undefined && config.stylize !== false) {
110
+ composer.addPass(new StylizePass(scene, camera, config.stylize));
111
+ }
119
112
  let grade = null;
120
113
  if (config.grade !== false) {
121
114
  grade = createGradePass(config.grade ?? {});
122
115
  composer.addPass(grade);
123
116
  }
124
- const graph = { composer, grade, dof };
117
+ const graph = { composer, grade };
125
118
  syncSize(graph, width, height, gl.getPixelRatio());
126
119
  builtRef.current = graph;
127
120
  return () => {
@@ -129,27 +122,6 @@ export function PostProcessing({ config, quality = "high", stages }) {
129
122
  builtRef.current = null;
130
123
  };
131
124
  }, [gl, scene, camera, config, quality, stages]);
132
- useEffect(() => {
133
- const lut = config.grade !== undefined && config.grade !== false ? config.grade.lut : undefined;
134
- const grade = builtRef.current?.grade;
135
- if (lut === undefined || grade === null || grade === undefined)
136
- return;
137
- let cancelled = false;
138
- const loader = new THREE.TextureLoader();
139
- loader.loadAsync(lut.url).then((texture) => {
140
- if (cancelled) {
141
- texture.dispose();
142
- return;
143
- }
144
- texture.colorSpace = THREE.SRGBColorSpace;
145
- texture.minFilter = THREE.NearestFilter;
146
- texture.magFilter = THREE.NearestFilter;
147
- texture.generateMipmaps = false;
148
- grade.uniforms.uLut.value = texture;
149
- grade.uniforms.uLutSize.value = lut.size ?? 32;
150
- }).catch(() => undefined);
151
- return () => { cancelled = true; };
152
- }, [config.grade]);
153
125
  useEffect(() => {
154
126
  const prevTone = gl.toneMapping;
155
127
  const prevExposure = gl.toneMappingExposure;
@@ -1,4 +1,4 @@
1
1
  import { ShaderPass } from "three-stdlib";
2
2
  import type { GradeConfig } from "@jgengine/core/render/postProcessing";
3
- /** Build the display-space colour-grade pass (lift/gain/gamma, saturation, vignette, grain). Advance `uniforms.uTime.value` each frame to animate the grain. */
3
+ /** Build the display-space colour-grade pass and load its optional LUT. Dispose the pass to release its LUT; advance `uniforms.uTime.value` each frame to animate grain. */
4
4
  export declare function createGradePass(config?: GradeConfig): ShaderPass;
@@ -61,11 +61,11 @@ const vertexShader = /* glsl */ `
61
61
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
62
62
  }
63
63
  `;
64
- /** Build the display-space colour-grade pass (lift/gain/gamma, saturation, vignette, grain). Advance `uniforms.uTime.value` each frame to animate the grain. */
64
+ /** Build the display-space colour-grade pass and load its optional LUT. Dispose the pass to release its LUT; advance `uniforms.uTime.value` each frame to animate grain. */
65
65
  export function createGradePass(config = {}) {
66
66
  const lift = config.lift ?? DEFAULT_LIFT;
67
67
  const gain = config.gain ?? DEFAULT_GAIN;
68
- return new ShaderPass({
68
+ const pass = new ShaderPass({
69
69
  uniforms: {
70
70
  tDiffuse: { value: null },
71
71
  uLift: { value: new THREE.Vector3(lift[0], lift[1], lift[2]) },
@@ -82,4 +82,32 @@ export function createGradePass(config = {}) {
82
82
  vertexShader,
83
83
  fragmentShader,
84
84
  });
85
+ let disposed = false;
86
+ let lutTexture = null;
87
+ const dispose = pass.dispose.bind(pass);
88
+ pass.dispose = () => {
89
+ if (disposed)
90
+ return;
91
+ disposed = true;
92
+ lutTexture?.dispose();
93
+ lutTexture = null;
94
+ dispose();
95
+ };
96
+ if (config.lut !== undefined) {
97
+ const lut = config.lut;
98
+ void new THREE.TextureLoader().loadAsync(lut.url).then((texture) => {
99
+ if (disposed) {
100
+ texture.dispose();
101
+ return;
102
+ }
103
+ lutTexture = texture;
104
+ texture.colorSpace = THREE.SRGBColorSpace;
105
+ texture.minFilter = THREE.NearestFilter;
106
+ texture.magFilter = THREE.NearestFilter;
107
+ texture.generateMipmaps = false;
108
+ pass.uniforms.uLut.value = texture;
109
+ pass.uniforms.uLutSize.value = lut.size ?? 32;
110
+ }).catch(() => undefined);
111
+ }
112
+ return pass;
85
113
  }
@@ -0,0 +1,3 @@
1
+ import type * as THREE from "three";
2
+ /** Reuse the beauty pass's shadow maps while drawing a scene prepass. @internal */
3
+ export declare function renderShadowlessPrepass<T>(renderer: Pick<THREE.WebGLRenderer, "shadowMap">, scene: THREE.Object3D, hiddenOverlays: THREE.Object3D[], render: () => T): T;
@@ -0,0 +1,18 @@
1
+ import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
2
+ /** Reuse the beauty pass's shadow maps while drawing a scene prepass. @internal */
3
+ export function renderShadowlessPrepass(renderer, scene, hiddenOverlays, render) {
4
+ const shadowMap = renderer.shadowMap;
5
+ const autoUpdate = shadowMap.autoUpdate;
6
+ const needsUpdate = shadowMap.needsUpdate;
7
+ hidePostfxOverlays(scene, hiddenOverlays);
8
+ shadowMap.autoUpdate = false;
9
+ shadowMap.needsUpdate = false;
10
+ try {
11
+ return render();
12
+ }
13
+ finally {
14
+ shadowMap.autoUpdate = autoUpdate;
15
+ shadowMap.needsUpdate = needsUpdate;
16
+ restorePostfxOverlays(hiddenOverlays);
17
+ }
18
+ }
@@ -0,0 +1,23 @@
1
+ import * as THREE from "three";
2
+ import { Pass } from "three-stdlib";
3
+ import type { StylizeConfig } from "@jgengine/core/render/postProcessing";
4
+ /**
5
+ * Art-style post pass: ink outlines from a normal+depth prepass, cel luminance bands, and
6
+ * pixelation. Runs on the display-space frame after `OutputPass`. The prepass renders only
7
+ * when outlines are on, and reuses the frame's shadow maps instead of re-rendering them.
8
+ */
9
+ export declare class StylizePass extends Pass {
10
+ private readonly scene;
11
+ private readonly camera;
12
+ private readonly quad;
13
+ private readonly material;
14
+ private readonly normalTarget;
15
+ private readonly normalMaterial;
16
+ private readonly hidden;
17
+ private readonly clearColor;
18
+ constructor(scene: THREE.Scene, camera: THREE.Camera, config: StylizeConfig);
19
+ setSize(width: number, height: number): void;
20
+ render(renderer: THREE.WebGLRenderer, writeBuffer: THREE.WebGLRenderTarget, readBuffer: THREE.WebGLRenderTarget): void;
21
+ private renderPrepass;
22
+ dispose(): void;
23
+ }
@@ -0,0 +1,216 @@
1
+ import * as THREE from "three";
2
+ import { FullScreenQuad, Pass } from "three-stdlib";
3
+ import { isPostfxOverlay } from "./postfxOverlay.js";
4
+ const DEFAULT_OUTLINE_COLOR = "#101014";
5
+ const DEFAULT_DEPTH_THRESHOLD = 0.08;
6
+ const DEFAULT_NORMAL_THRESHOLD = 0.35;
7
+ const DEFAULT_FADE_DISTANCE = 160;
8
+ const fragmentShader = /* glsl */ `
9
+ #include <packing>
10
+ uniform sampler2D tDiffuse;
11
+ uniform sampler2D tNormal;
12
+ uniform sampler2D tDepth;
13
+ uniform vec2 uResolution;
14
+ uniform float uNear;
15
+ uniform float uFar;
16
+ uniform bool uOrtho;
17
+ uniform bool uOutline;
18
+ uniform vec3 uOutlineColor;
19
+ uniform float uOutlineOpacity;
20
+ uniform float uThickness;
21
+ uniform float uDepthThreshold;
22
+ uniform float uNormalThreshold;
23
+ uniform float uFade;
24
+ uniform float uBands;
25
+ uniform float uPixel;
26
+ varying vec2 vUv;
27
+
28
+ float viewDepth(vec2 uv) {
29
+ float d = texture2D(tDepth, uv).x;
30
+ return -(uOrtho ? orthographicDepthToViewZ(d, uNear, uFar) : perspectiveDepthToViewZ(d, uNear, uFar));
31
+ }
32
+
33
+ vec3 viewNormal(vec2 uv) {
34
+ return texture2D(tNormal, uv).xyz * 2.0 - 1.0;
35
+ }
36
+
37
+ // Ink a normal crease only on its nearer side, so a thin or tiny feature stays one line wide
38
+ // instead of stamping a plus-shaped blot onto the surface behind it.
39
+ float crease(vec3 nc, float dc, vec2 uv, float dn) {
40
+ return (1.0 - dot(nc, viewNormal(uv))) * step(dc, dn + dc * 0.02);
41
+ }
42
+
43
+ void main() {
44
+ vec2 uv = vUv;
45
+ if (uPixel > 1.0) {
46
+ vec2 block = uPixel / uResolution;
47
+ uv = (floor(uv / block) + 0.5) * block;
48
+ }
49
+ vec3 c = texture2D(tDiffuse, uv).rgb;
50
+
51
+ if (uBands > 0.5) {
52
+ float luma = dot(c, vec3(0.2126, 0.7152, 0.0722));
53
+ float x = min(luma * uBands, uBands - 0.001);
54
+ // Each band takes its centre brightness: the darkest band keeps colour instead of going black
55
+ // and the top band never clips a bright sky to flat white. A narrow soft step hides aliasing.
56
+ float q = (floor(x) + 0.5 + smoothstep(0.92, 1.0, fract(x))) / uBands;
57
+ c *= q / max(luma, 1e-4);
58
+ }
59
+
60
+ if (uOutline) {
61
+ vec2 px = uThickness / uResolution;
62
+ float dc = viewDepth(uv);
63
+ float dl = viewDepth(uv - vec2(px.x, 0.0));
64
+ float dr = viewDepth(uv + vec2(px.x, 0.0));
65
+ float dd = viewDepth(uv - vec2(0.0, px.y));
66
+ float du = viewDepth(uv + vec2(0.0, px.y));
67
+ // Laplacian is zero across flat planes at any angle, so only real depth breaks draw lines.
68
+ float lap = abs(dl + dr + dd + du - 4.0 * dc) / max(dc, 1e-3);
69
+ float nearest = min(dc, min(min(dl, dr), min(dd, du)));
70
+ // Same nearer-side rule as creases: the background around a silhouette stays clean.
71
+ float depthEdge = smoothstep(uDepthThreshold, uDepthThreshold * 2.0, lap) * step(dc, nearest + dc * 0.03);
72
+
73
+ vec3 nc = viewNormal(uv);
74
+ float nd = max(
75
+ max(crease(nc, dc, uv - vec2(px.x, 0.0), dl), crease(nc, dc, uv + vec2(px.x, 0.0), dr)),
76
+ max(crease(nc, dc, uv - vec2(0.0, px.y), dd), crease(nc, dc, uv + vec2(0.0, px.y), du))
77
+ );
78
+ float normalEdge = smoothstep(uNormalThreshold, uNormalThreshold + 0.25, nd);
79
+
80
+ float fade = 1.0 - smoothstep(uFade * 0.5, uFade, nearest);
81
+ float edge = max(depthEdge, normalEdge) * fade * uOutlineOpacity;
82
+ c = mix(c, uOutlineColor, clamp(edge, 0.0, 1.0));
83
+ }
84
+
85
+ gl_FragColor = vec4(c, 1.0);
86
+ }
87
+ `;
88
+ const vertexShader = /* glsl */ `
89
+ varying vec2 vUv;
90
+ void main() {
91
+ vUv = uv;
92
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
93
+ }
94
+ `;
95
+ function skipsDepthPrepass(material) {
96
+ const list = Array.isArray(material) ? material : [material];
97
+ // Shader materials place vertices in their own code, which the override material cannot reproduce.
98
+ return list.some((m) => m.transparent || m.alphaTest > 0 || !m.depthWrite || !m.colorWrite || m.isShaderMaterial === true);
99
+ }
100
+ /**
101
+ * Hide what the outline prepass must not see: marked overlays, points/lines/sprites, custom
102
+ * shader meshes, and transparent or alpha-cut meshes (foliage cards would otherwise outline as
103
+ * solid quads).
104
+ */
105
+ function hideForPrepass(scene, out) {
106
+ out.length = 0;
107
+ scene.traverse((object) => {
108
+ if (!object.visible)
109
+ return;
110
+ const drawable = object;
111
+ const hide = isPostfxOverlay(object) ||
112
+ object.isPoints === true ||
113
+ object.isLine === true ||
114
+ object.isSprite === true ||
115
+ (drawable.isMesh === true && skipsDepthPrepass(drawable.material));
116
+ if (!hide)
117
+ return;
118
+ object.visible = false;
119
+ out.push(object);
120
+ });
121
+ }
122
+ /**
123
+ * Art-style post pass: ink outlines from a normal+depth prepass, cel luminance bands, and
124
+ * pixelation. Runs on the display-space frame after `OutputPass`. The prepass renders only
125
+ * when outlines are on, and reuses the frame's shadow maps instead of re-rendering them.
126
+ */
127
+ export class StylizePass extends Pass {
128
+ scene;
129
+ camera;
130
+ quad;
131
+ material;
132
+ normalTarget;
133
+ normalMaterial = new THREE.MeshNormalMaterial();
134
+ hidden = [];
135
+ clearColor = new THREE.Color();
136
+ constructor(scene, camera, config) {
137
+ super();
138
+ this.scene = scene;
139
+ this.camera = camera;
140
+ const outline = config.outline === undefined || config.outline === false ? null : config.outline;
141
+ this.normalTarget = outline === null
142
+ ? null
143
+ : new THREE.WebGLRenderTarget(1, 1, { depthTexture: new THREE.DepthTexture(1, 1), type: THREE.UnsignedByteType });
144
+ this.material = new THREE.ShaderMaterial({
145
+ uniforms: {
146
+ tDiffuse: { value: null },
147
+ tNormal: { value: this.normalTarget?.texture ?? null },
148
+ tDepth: { value: this.normalTarget?.depthTexture ?? null },
149
+ uResolution: { value: new THREE.Vector2(1, 1) },
150
+ uNear: { value: 0.1 },
151
+ uFar: { value: 1000 },
152
+ uOrtho: { value: false },
153
+ uOutline: { value: outline !== null },
154
+ uOutlineColor: { value: new THREE.Color(outline?.color ?? DEFAULT_OUTLINE_COLOR) },
155
+ uOutlineOpacity: { value: outline?.opacity ?? 1 },
156
+ uThickness: { value: Math.max(0.5, outline?.thickness ?? 1) },
157
+ uDepthThreshold: { value: outline?.depthThreshold ?? DEFAULT_DEPTH_THRESHOLD },
158
+ uNormalThreshold: { value: outline?.normalThreshold ?? DEFAULT_NORMAL_THRESHOLD },
159
+ uFade: { value: outline?.fadeDistance ?? DEFAULT_FADE_DISTANCE },
160
+ uBands: { value: Math.max(0, Math.floor(config.bands ?? 0)) },
161
+ uPixel: { value: Math.max(0, config.pixelSize ?? 0) },
162
+ },
163
+ vertexShader,
164
+ fragmentShader,
165
+ depthTest: false,
166
+ depthWrite: false,
167
+ });
168
+ this.quad = new FullScreenQuad(this.material);
169
+ }
170
+ setSize(width, height) {
171
+ this.material.uniforms.uResolution.value.set(width, height);
172
+ this.normalTarget?.setSize(width, height);
173
+ }
174
+ render(renderer, writeBuffer, readBuffer) {
175
+ const uniforms = this.material.uniforms;
176
+ if (this.normalTarget !== null)
177
+ this.renderPrepass(renderer, this.normalTarget);
178
+ const camera = this.camera;
179
+ uniforms.uNear.value = camera.near;
180
+ uniforms.uFar.value = camera.far;
181
+ uniforms.uOrtho.value = camera.isOrthographicCamera === true;
182
+ uniforms.tDiffuse.value = readBuffer.texture;
183
+ renderer.setRenderTarget(this.renderToScreen ? null : writeBuffer);
184
+ this.quad.render(renderer);
185
+ }
186
+ renderPrepass(renderer, target) {
187
+ const scene = this.scene;
188
+ const previousOverride = scene.overrideMaterial;
189
+ const previousBackground = scene.background;
190
+ const previousShadowUpdate = renderer.shadowMap.autoUpdate;
191
+ const previousClearAlpha = renderer.getClearAlpha();
192
+ renderer.getClearColor(this.clearColor);
193
+ hideForPrepass(scene, this.hidden);
194
+ scene.overrideMaterial = this.normalMaterial;
195
+ scene.background = null;
196
+ renderer.shadowMap.autoUpdate = false;
197
+ renderer.setRenderTarget(target);
198
+ renderer.setClearColor(0x8080ff, 1);
199
+ renderer.clear();
200
+ renderer.render(scene, this.camera);
201
+ renderer.setClearColor(this.clearColor, previousClearAlpha);
202
+ renderer.shadowMap.autoUpdate = previousShadowUpdate;
203
+ scene.background = previousBackground;
204
+ scene.overrideMaterial = previousOverride;
205
+ for (const object of this.hidden)
206
+ object.visible = true;
207
+ this.hidden.length = 0;
208
+ }
209
+ dispose() {
210
+ this.material.dispose();
211
+ this.normalMaterial.dispose();
212
+ this.quad.dispose();
213
+ this.normalTarget?.depthTexture?.dispose();
214
+ this.normalTarget?.dispose();
215
+ }
216
+ }
@@ -0,0 +1,75 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from "react";
2
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
3
+ /** Marks only a rejected presentation import for page reload; browser module failures are cached. @internal */
4
+ export declare function markPresentationCodeLoadFailure(error: unknown): Error;
5
+ /** @internal Distinguishes module loading from recoverable assets without guessing from error text. */
6
+ export declare function presentationRecoveryAction(error: unknown): "reload" | "retry";
7
+ /** @internal Classifies a reported React error after its owning boundary commits. */
8
+ export declare function deferUnhandledPresentationError(error: unknown, report: () => void): void;
9
+ /** @internal Exact pending texture groups owned by one presentation, including uncommitted Suspense. */
10
+ export declare function createPresentationRetryRegistry(defaultClear?: (inputs: string[]) => void): {
11
+ register(inputs: readonly string[], clear?: ((inputs: string[]) => void) | undefined): () => void;
12
+ retry(): void;
13
+ };
14
+ type RetryRegistry = ReturnType<typeof createPresentationRetryRegistry>;
15
+ type PresentationRetryScope = {
16
+ registry: RetryRegistry;
17
+ fail: (error: unknown, stack?: string) => void;
18
+ recoveredDraw: () => void;
19
+ };
20
+ /** Renderer-only loader adapters share the DOM owner without importing their libraries here. @internal */
21
+ export declare function usePresentationRetryScope(): PresentationRetryScope | null;
22
+ /** @internal Forwards renderer-local loader errors to the owning DOM presentation. */
23
+ export declare function PresentationAssetBoundary({ children }: {
24
+ children: ReactNode;
25
+ }): ReactNode;
26
+ /** @internal Retires only diagnostics belonging to this exact handled presentation error. */
27
+ export declare function createPresentationDiagnosticOwnership(): {
28
+ register(error: unknown, id: number, phase: string): void;
29
+ recovered(error: unknown): ReadonlySet<number>;
30
+ };
31
+ /** Restores an owned recovered surface only after Suspense has attached and revealed it. @internal */
32
+ export declare function createPlaySurfaceFocusOwnership<Owner>(): {
33
+ request(owner: Owner): void;
34
+ reveal(owner: Owner, surface: HTMLElement | null, active: Element | null, body: HTMLElement): boolean;
35
+ };
36
+ /** Reports the owning presentation attempt and recovery lifecycle. @internal */
37
+ export type PresentationRecoveryEvent = {
38
+ phase: string;
39
+ attempt: number;
40
+ scope: number;
41
+ sameOwner: boolean;
42
+ pending: boolean;
43
+ failed: boolean;
44
+ };
45
+ type RecoveryProps = {
46
+ ctx: GameContext;
47
+ children: ReactNode;
48
+ onRuntimeError: (error: unknown, phase: string, componentStack?: string) => void;
49
+ onRetryCommitted?: () => void;
50
+ onRecoveredDraw?: (error: unknown) => void;
51
+ onRecoveryEvent?: (event: PresentationRecoveryEvent) => void;
52
+ };
53
+ type RecoveryState = {
54
+ owner: GameContext;
55
+ failed: boolean;
56
+ error: unknown;
57
+ registry: RetryRegistry;
58
+ };
59
+ /** @internal Retains the live context for asset Retry; failed browser modules require page reload. */
60
+ export declare class PresentationRecovery extends Component<RecoveryProps, RecoveryState> {
61
+ state: RecoveryState;
62
+ static getDerivedStateFromProps(props: RecoveryProps, state: RecoveryState): Partial<RecoveryState> | null;
63
+ static getDerivedStateFromError(error: unknown): Partial<RecoveryState>;
64
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
65
+ componentDidUpdate(previousProps: RecoveryProps, previousState: RecoveryState): void;
66
+ componentWillUnmount(): void;
67
+ private attempt;
68
+ private scopeId;
69
+ private trace;
70
+ private scope;
71
+ private pendingRecovery;
72
+ private retry;
73
+ render(): ReactNode;
74
+ }
75
+ export {};