@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
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useFrame, useThree } from "@react-three/fiber";
3
- import { useEffect, useMemo, useRef } from "react";
3
+ import { useEffect, useMemo } from "react";
4
4
  import * as THREE from "three";
5
5
  const VERTEX_SHADER = /* glsl */ `
6
6
  attribute float aSize;
@@ -21,71 +21,189 @@ const FRAGMENT_SHADER = /* glsl */ `
21
21
  varying float vAlpha;
22
22
  varying vec3 vColor;
23
23
  void main() {
24
- // Soft round sprite: radial falloff from the point center, discard the corners.
25
- vec2 d = gl_PointCoord - vec2(0.5);
26
- float r = length(d);
24
+ float r = length(gl_PointCoord - vec2(0.5));
27
25
  if (r > 0.5) discard;
28
- float soft = smoothstep(0.5, 0.0, r);
29
- gl_FragColor = vec4(vColor, vAlpha * soft);
26
+ gl_FragColor = vec4(vColor, vAlpha * smoothstep(0.5, 0.0, r));
30
27
  }
31
28
  `;
29
+ const QUAD_VERTEX = /* glsl */ `
30
+ attribute vec3 aCenter;
31
+ attribute vec3 aEnd;
32
+ attribute vec3 aVelocity;
33
+ attribute float aId;
34
+ attribute float aSize;
35
+ attribute float aAlpha;
36
+ attribute vec3 aColor;
37
+ uniform float uShape;
38
+ uniform float uStretch;
39
+ varying vec2 vUv;
40
+ varying float vAlpha;
41
+ varying vec3 vColor;
42
+ void main() {
43
+ vUv = uv;
44
+ vAlpha = aAlpha;
45
+ vColor = aColor;
46
+ vec4 center = modelViewMatrix * vec4(aCenter, 1.0);
47
+ vec4 view = center;
48
+ if (uShape == 1.0 || uShape == 5.0) {
49
+ vec3 end = uShape == 5.0 ? aEnd : aCenter - aVelocity * uStretch;
50
+ vec4 tail = modelViewMatrix * vec4(end, 1.0);
51
+ vec2 delta = center.xy - tail.xy;
52
+ float len = length(delta);
53
+ vec2 side = len > 0.00001 ? vec2(-delta.y, delta.x) / len : vec2(1.0, 0.0);
54
+ view = mix(tail, center, uv.y);
55
+ view.xy += side * position.x * aSize;
56
+ } else if (uShape == 6.0) {
57
+ view = modelViewMatrix * vec4(aCenter + vec3(position.x, 0.002, position.y) * aSize * 2.0, 1.0);
58
+ } else {
59
+ vec2 corner = position.xy * aSize;
60
+ if (uShape == 2.0) {
61
+ float angle = aId * 2.399963;
62
+ corner = mat2(cos(angle), -sin(angle), sin(angle), cos(angle)) * corner;
63
+ }
64
+ if (uShape == 3.0) {
65
+ corner.y *= 2.7;
66
+ corner.x += sin(aId * 1.7 + uv.y * 7.0) * aSize * 0.14 * uv.y;
67
+ }
68
+ if (uShape == 4.0) corner *= 1.65;
69
+ view.xy += corner;
70
+ }
71
+ gl_Position = projectionMatrix * view;
72
+ }
73
+ `;
74
+ const QUAD_FRAGMENT = /* glsl */ `
75
+ uniform float uShape;
76
+ varying vec2 vUv;
77
+ varying float vAlpha;
78
+ varying vec3 vColor;
79
+ void main() {
80
+ vec2 d = vUv - 0.5;
81
+ float alpha;
82
+ if (uShape == 1.0 || uShape == 5.0) {
83
+ alpha = smoothstep(0.5, 0.0, abs(d.x)) * smoothstep(0.0, 0.12, vUv.y) * smoothstep(1.0, 0.7, vUv.y);
84
+ } else if (uShape == 2.0) {
85
+ float angle = atan(d.y, d.x);
86
+ float edge = 0.26 + 0.12 * pow(abs(cos(angle * 3.0)), 4.0);
87
+ alpha = smoothstep(edge, edge * 0.62, length(d));
88
+ } else if (uShape == 3.0) {
89
+ float width = (1.0 - vUv.y) * 0.42 + 0.04;
90
+ alpha = smoothstep(width, 0.0, abs(d.x)) * smoothstep(0.0, 0.18, vUv.y) * smoothstep(1.0, 0.5, vUv.y);
91
+ } else if (uShape == 6.0) {
92
+ float r = length(d);
93
+ alpha = smoothstep(0.07, 0.0, abs(r - 0.38));
94
+ } else {
95
+ float r = length(d);
96
+ alpha = smoothstep(0.5, 0.08, r);
97
+ if (uShape == 4.0) alpha *= 0.65 + 0.35 * sin(vUv.x * 15.0 + sin(vUv.y * 12.0));
98
+ }
99
+ alpha *= vAlpha;
100
+ if (alpha < 0.001) discard;
101
+ gl_FragColor = vec4(vColor, alpha);
102
+ }
103
+ `;
104
+ const SHAPES = { point: 0, streak: 1, flake: 2, flame: 3, smoke: 4, ribbon: 5, ripple: 6 };
32
105
  /**
33
- * Renders a core `ParticleSystem` as a GPU point cloud: one draw call, per-particle
34
- * size / color / alpha via a soft-round shader, and a `drawRange` clamped to the live
35
- * count so dead particles cost nothing. The simulation stays engine-side and
36
- * genre-agnostic; this is purely how it reaches the screen. By default it advances
37
- * the sim with the frame delta — pass `advance={false}` to drive it from your own
38
- * fixed loop instead.
39
- *
40
- * @capability particle-field render a core particle system as a soft-point GPU cloud with per-particle size, color, and alpha
106
+ * Render one pooled simulation through a point cloud or instanced quads. Output
107
+ * selection does not replace spawn, forces, collisions, or deterministic state.
108
+ * @capability particle-field render a core particle pool as soft points, velocity streaks, connected ribbons, flakes, flames, smoke sprites, or ground ripples
41
109
  */
42
- export function ParticleField({ system, advance = true, blending = "additive", scale = 300, depthWrite = false, }) {
43
- const pointsRef = useRef(null);
110
+ export function ParticleField({ system, advance = true, blending = "additive", scale = 300, depthWrite = false, render, metrics }) {
44
111
  const dpr = useThree((state) => state.viewport.dpr);
45
- const { geometry, material } = useMemo(() => {
46
- const initial = system.buffers();
47
- const max = initial.positions.length / 3;
48
- const geo = new THREE.BufferGeometry();
49
- geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
50
- geo.setAttribute("aSize", new THREE.BufferAttribute(new Float32Array(max), 1));
51
- geo.setAttribute("aColor", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
52
- geo.setAttribute("aAlpha", new THREE.BufferAttribute(new Float32Array(max), 1));
53
- geo.setDrawRange(0, 0);
54
- const mat = new THREE.ShaderMaterial({
55
- vertexShader: VERTEX_SHADER,
56
- fragmentShader: FRAGMENT_SHADER,
57
- uniforms: { uScale: { value: scale } },
58
- transparent: true,
59
- depthWrite,
60
- blending: blending === "additive" ? THREE.AdditiveBlending : THREE.NormalBlending,
61
- });
62
- return { geometry: geo, material: mat };
63
- }, [system, scale, depthWrite, blending]);
112
+ const shape = render?.shape ?? "point";
113
+ const quad = shape !== "point";
114
+ const { geometry, material, indexById } = useMemo(() => {
115
+ const max = system.buffers().positions.length / 3;
116
+ const geo = quad ? new THREE.InstancedBufferGeometry() : new THREE.BufferGeometry();
117
+ if (quad) {
118
+ geo.setAttribute("position", new THREE.Float32BufferAttribute([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0], 3));
119
+ geo.setAttribute("uv", new THREE.Float32BufferAttribute([0, 0, 1, 0, 1, 1, 0, 1], 2));
120
+ geo.setIndex([0, 1, 2, 0, 2, 3]);
121
+ geo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(new Float32Array(max * 3), 3));
122
+ geo.setAttribute("aEnd", new THREE.InstancedBufferAttribute(new Float32Array(max * 3), 3));
123
+ geo.setAttribute("aVelocity", new THREE.InstancedBufferAttribute(new Float32Array(max * 3), 3));
124
+ geo.setAttribute("aId", new THREE.InstancedBufferAttribute(new Float32Array(max), 1));
125
+ geo.instanceCount = 0;
126
+ }
127
+ else {
128
+ geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
129
+ geo.setDrawRange(0, 0);
130
+ }
131
+ const attribute = (array, size) => quad ? new THREE.InstancedBufferAttribute(array, size) : new THREE.BufferAttribute(array, size);
132
+ geo.setAttribute("aSize", attribute(new Float32Array(max), 1));
133
+ geo.setAttribute("aColor", attribute(new Float32Array(max * 3), 3));
134
+ geo.setAttribute("aAlpha", attribute(new Float32Array(max), 1));
135
+ const mat = new THREE.ShaderMaterial({ vertexShader: quad ? QUAD_VERTEX : VERTEX_SHADER, fragmentShader: quad ? QUAD_FRAGMENT : FRAGMENT_SHADER,
136
+ uniforms: { uScale: { value: scale }, uShape: { value: SHAPES[shape] }, uStretch: { value: render?.stretch ?? 0.08 } },
137
+ transparent: true, depthWrite, side: THREE.DoubleSide,
138
+ blending: blending === "additive" ? THREE.AdditiveBlending : THREE.NormalBlending });
139
+ return { geometry: geo, material: mat, indexById: new Map() };
140
+ }, [system, quad, shape, scale, depthWrite, blending]);
64
141
  useEffect(() => {
142
+ const bytes = Object.values(geometry.attributes).reduce((total, attribute) => total + attribute.array.byteLength, geometry.index?.array.byteLength ?? 0);
143
+ if (metrics !== undefined) {
144
+ metrics.fieldsMounted++;
145
+ metrics.liveFields++;
146
+ metrics.attributeBytes += bytes;
147
+ }
65
148
  return () => {
66
149
  geometry.dispose();
67
150
  material.dispose();
151
+ if (metrics !== undefined) {
152
+ metrics.fieldsUnmounted++;
153
+ metrics.liveFields--;
154
+ metrics.attributeBytes -= bytes;
155
+ }
68
156
  };
69
- }, [geometry, material]);
157
+ }, [geometry, material, metrics]);
70
158
  useFrame((_, delta) => {
159
+ const started = metrics === undefined ? 0 : performance.now();
71
160
  if (advance)
72
161
  system.update(Math.min(delta, 0.1));
73
162
  const buffers = system.buffers();
74
163
  const count = buffers.count;
75
- const pos = geometry.getAttribute("position");
76
- const size = geometry.getAttribute("aSize");
77
- const color = geometry.getAttribute("aColor");
78
- const alpha = geometry.getAttribute("aAlpha");
79
- pos.array.set(buffers.positions.subarray(0, count * 3));
80
- size.array.set(buffers.sizes.subarray(0, count));
81
- color.array.set(buffers.colors.subarray(0, count * 3));
82
- alpha.array.set(buffers.alphas.subarray(0, count));
83
- pos.needsUpdate = true;
84
- size.needsUpdate = true;
85
- color.needsUpdate = true;
86
- alpha.needsUpdate = true;
87
- geometry.setDrawRange(0, count);
164
+ const upload = (name, values, n) => {
165
+ const attr = geometry.getAttribute(name);
166
+ attr.array.set(values.subarray(0, n));
167
+ attr.needsUpdate = true;
168
+ };
169
+ upload(quad ? "aCenter" : "position", buffers.positions, count * 3);
170
+ upload("aSize", buffers.sizes, count);
171
+ upload("aColor", buffers.colors, count * 3);
172
+ upload("aAlpha", buffers.alphas, count);
173
+ if (quad) {
174
+ upload("aVelocity", buffers.velocities, count * 3);
175
+ const end = geometry.getAttribute("aEnd");
176
+ const ids = geometry.getAttribute("aId");
177
+ if (shape === "ribbon") {
178
+ for (let i = 0; i < count; i++)
179
+ indexById.set(buffers.ids[i], i);
180
+ for (const id of indexById.keys()) {
181
+ const index = indexById.get(id);
182
+ if (index >= count || buffers.ids[index] !== id)
183
+ indexById.delete(id);
184
+ }
185
+ }
186
+ for (let i = 0; i < count; i++) {
187
+ ids.setX(i, buffers.ids[i]);
188
+ const previousIndex = shape === "ribbon" ? indexById.get(buffers.ids[i] - 1) : undefined;
189
+ const source = previousIndex === undefined ? buffers.previousPositions : buffers.positions;
190
+ const offset = (previousIndex ?? i) * 3;
191
+ end.setXYZ(i, source[offset], source[offset + 1], source[offset + 2]);
192
+ }
193
+ end.needsUpdate = true;
194
+ ids.needsUpdate = true;
195
+ geometry.instanceCount = count;
196
+ }
197
+ else
198
+ geometry.setDrawRange(0, count);
88
199
  material.uniforms.uScale.value = scale * dpr;
200
+ material.uniforms.uStretch.value = render?.stretch ?? 0.08;
201
+ if (metrics !== undefined) {
202
+ const elapsed = performance.now() - started;
203
+ metrics.totalUploadMs += elapsed;
204
+ metrics.maxUploadMs = Math.max(metrics.maxUploadMs, elapsed);
205
+ metrics.uploadSamples++;
206
+ }
89
207
  });
90
- return _jsx("points", { ref: pointsRef, geometry: geometry, material: material, frustumCulled: false });
208
+ return quad ? _jsx("mesh", { geometry: geometry, material: material, frustumCulled: false }) : _jsx("points", { geometry: geometry, material: material, frustumCulled: false });
91
209
  }
@@ -3,7 +3,7 @@ import { createContext, useContext, useMemo, useRef } from "react";
3
3
  import { useThree, useFrame } from "@react-three/fiber";
4
4
  import * as THREE from "three";
5
5
  import { useGameContext } from "@jgengine/react/provider";
6
- import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
6
+ import { buildCullingDriver } from "./cullingDriver.js";
7
7
  import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
8
8
  const ALWAYS_VISIBLE = () => true;
9
9
  const alwaysVisibleRef = { current: ALWAYS_VISIBLE };
@@ -16,66 +16,6 @@ const CullingContext = createContext(null);
16
16
  export function useRenderVisibility() {
17
17
  return useContext(CullingContext) ?? alwaysVisibleRef;
18
18
  }
19
- const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
20
- function objectRadius(scale) {
21
- if (scale === undefined)
22
- return 1;
23
- const s = typeof scale === "number" ? scale : Math.max(scale[0], scale[1], scale[2]);
24
- return Math.max(0.75, s);
25
- }
26
- function buildSystem(ctx, config) {
27
- const versions = new Map();
28
- let currentView = null;
29
- const scratch = { id: "", position: [0, 0, 0], version: 0 };
30
- const objectSpec = { kind: "sphere", radius: 1, offset: [0, 0.5, 0] };
31
- function bump(id, position, rot) {
32
- const prev = versions.get(id);
33
- if (prev !== undefined && prev.x === position[0] && prev.y === position[1] && prev.z === position[2] && prev.rot === rot) {
34
- return prev.version;
35
- }
36
- const version = (prev?.version ?? 0) + 1;
37
- versions.set(id, { x: position[0], y: position[1], z: position[2], rot, version });
38
- return version;
39
- }
40
- function* renderables() {
41
- const entityOverrides = config?.entities;
42
- for (const entity of ctx.scene.entity.list()) {
43
- scratch.id = entity.id;
44
- scratch.position = entity.position;
45
- scratch.version = bump(entity.id, entity.position, entity.rotationY);
46
- scratch.bounds = ENTITY_BOUNDS;
47
- scratch.overrides = entityOverrides?.[entity.name];
48
- scratch.layer = "entity";
49
- yield scratch;
50
- }
51
- const objectOverrides = config?.objects;
52
- for (const object of ctx.scene.object.list()) {
53
- scratch.id = object.instanceId;
54
- scratch.position = object.position;
55
- scratch.version = bump(object.instanceId, object.position, object.rotationY);
56
- objectSpec.radius = objectRadius(object.visual?.scale);
57
- scratch.bounds = objectSpec;
58
- scratch.overrides = objectOverrides?.[object.catalogId];
59
- scratch.layer = "object";
60
- yield scratch;
61
- }
62
- }
63
- const cameras = () => (currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }]);
64
- const options = {
65
- renderables,
66
- cameras,
67
- ...(config?.culling !== undefined ? { settings: config.culling } : {}),
68
- ...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
69
- };
70
- const system = createVisibilitySystem(options);
71
- return {
72
- system,
73
- setView(view) {
74
- currentView = view;
75
- },
76
- };
77
- }
78
- const EMPTY_CAMERAS = [];
79
19
  const tmpDir = new THREE.Vector3();
80
20
  function viewFromCamera(camera) {
81
21
  camera.getWorldDirection(tmpDir);
@@ -120,7 +60,7 @@ export function CullingProvider({ config, drawDistance, children }) {
120
60
  return config;
121
61
  return { ...config, culling: { ...config?.culling, defaultMaxRenderDistance: config?.culling?.defaultMaxRenderDistance ?? drawDistance } };
122
62
  }, [config, drawDistance]);
123
- const driver = useMemo(() => (enabled ? buildSystem(ctx, resolvedConfig) : null), [ctx, resolvedConfig, enabled]);
63
+ const driver = useMemo(() => (enabled ? buildCullingDriver(ctx, resolvedConfig) : null), [ctx, resolvedConfig, enabled]);
124
64
  useFrame(() => {
125
65
  if (driver === null) {
126
66
  predicateRef.current = ALWAYS_VISIBLE;
@@ -0,0 +1,9 @@
1
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
+ import type { CameraView } from "@jgengine/core/visibility/frustum";
3
+ import type { VisibilityConfig } from "@jgengine/core/visibility/config";
4
+ /** Builds the shell's live scene adapter for headless visibility. @internal */
5
+ export declare function buildCullingDriver(ctx: GameContext, config: VisibilityConfig | undefined): {
6
+ system: import("@jgengine/core/visibility/visibilitySystem").VisibilitySystem;
7
+ setView(view: CameraView | null): void;
8
+ trackedCount(): number;
9
+ };
@@ -0,0 +1,68 @@
1
+ import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
2
+ const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
3
+ const EMPTY_CAMERAS = [];
4
+ function objectRadius(scale) {
5
+ if (scale === undefined)
6
+ return 1;
7
+ const size = typeof scale === "number" ? Math.abs(scale) : Math.max(Math.abs(scale[0]), Math.abs(scale[1]), Math.abs(scale[2]));
8
+ return Math.max(0.75, size);
9
+ }
10
+ /** Builds the shell's live scene adapter for headless visibility. @internal */
11
+ export function buildCullingDriver(ctx, config) {
12
+ const entries = new Map();
13
+ let frame = 0;
14
+ let currentView = null;
15
+ function update(id, position, rotation, radius, overrides, layer) {
16
+ let entry = entries.get(id);
17
+ if (entry === undefined) {
18
+ entry = { renderable: { id, position, version: 0 }, bounds: { kind: "sphere", radius, offset: [0, 0.5, 0] }, x: NaN, y: NaN, z: NaN, rotation: NaN, radius: NaN, seen: frame };
19
+ entries.set(id, entry);
20
+ }
21
+ const previous = entry.renderable;
22
+ const changed = entry.x !== position[0] || entry.y !== position[1] || entry.z !== position[2] || entry.rotation !== rotation || entry.radius !== radius || previous.overrides !== overrides || previous.layer !== layer;
23
+ if (changed) {
24
+ // A retained renderable must belong to one row: the core may keep it for always-visible processing.
25
+ entry.bounds.radius = radius;
26
+ previous.position = position;
27
+ previous.version += 1;
28
+ previous.bounds = layer === "entity" ? ENTITY_BOUNDS : entry.bounds;
29
+ previous.overrides = overrides;
30
+ previous.layer = layer;
31
+ entry.x = position[0];
32
+ entry.y = position[1];
33
+ entry.z = position[2];
34
+ entry.rotation = rotation;
35
+ entry.radius = radius;
36
+ }
37
+ entry.seen = frame;
38
+ return entry.renderable;
39
+ }
40
+ function* renderables() {
41
+ frame += 1;
42
+ for (const entity of ctx.scene.entity.list()) {
43
+ yield update(entity.id, entity.position, entity.rotationY, 2, config?.entities?.[entity.name], "entity");
44
+ }
45
+ for (const object of ctx.scene.object.list()) {
46
+ yield update(object.instanceId, object.position, object.rotationY, objectRadius(object.visual?.scale), config?.objects?.[object.catalogId], "object");
47
+ }
48
+ for (const [id, entry] of entries) {
49
+ if (entry.seen !== frame)
50
+ entries.delete(id);
51
+ }
52
+ }
53
+ const system = createVisibilitySystem({
54
+ renderables,
55
+ cameras: () => currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }],
56
+ ...(config?.culling !== undefined ? { settings: config.culling } : {}),
57
+ ...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
58
+ });
59
+ return {
60
+ system,
61
+ setView(view) {
62
+ currentView = view;
63
+ },
64
+ trackedCount() {
65
+ return entries.size;
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,16 @@
1
+ import type { EditorDocument } from "@jgengine/core/editor/types";
2
+ import { type WeatherLayerProps } from "./WeatherLayer.js";
3
+ /** Authored weather document and optional precipitation appearance and ground sampling. */
4
+ export interface AuthoredWeatherLayerProps {
5
+ document: EditorDocument;
6
+ rain?: WeatherLayerProps["rain"];
7
+ snow?: WeatherLayerProps["snow"];
8
+ dust?: WeatherLayerProps["dust"];
9
+ heightAt?: (x: number, z: number) => number;
10
+ /** Register actual procedural weather pool and impact query counters. */
11
+ diagnostics?: boolean;
12
+ }
13
+ /** Render pooled precipitation from the same authored document and clock as gameplay.
14
+ * @capability authored-weather-rendering Render authored weather from shared gameplay samples, simulation time and shelter volumes.
15
+ */
16
+ export declare function AuthoredWeatherLayer({ document, rain, snow, dust, heightAt, diagnostics }: AuthoredWeatherLayerProps): import("react").JSX.Element | null;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef } from "react";
3
+ import { useGameContext } from "@jgengine/react/provider";
4
+ import { devtools } from "@jgengine/core/devtools/devtools";
5
+ import { warnOnce } from "@jgengine/core/devtools/warnOnce";
6
+ import { WEATHER_SHELTER_BUDGET } from "./weatherShelter.js";
7
+ import { WeatherLayer } from "./WeatherLayer.js";
8
+ /** Render pooled precipitation from the same authored document and clock as gameplay.
9
+ * @capability authored-weather-rendering Render authored weather from shared gameplay samples, simulation time and shelter volumes.
10
+ */
11
+ export function AuthoredWeatherLayer({ document, rain, snow, dust, heightAt, diagnostics = false }) {
12
+ const ctx = useGameContext();
13
+ const config = document.simulation?.weather;
14
+ const metrics = useRef({ rain: { count: 0, capacity: 0 }, snow: { count: 0, capacity: 0 }, dust: { count: 0, capacity: 0 }, impacts: { count: 0, capacity: 0, heightQueries: 0, exposureQueries: 0, queryBudget: 0 } });
15
+ const mounted = config !== undefined;
16
+ useEffect(() => {
17
+ if (!diagnostics || !mounted)
18
+ return;
19
+ return devtools.probes.register("weatherParticles", () => ({ scope: "procedural-weather", ...structuredClone(metrics.current) }));
20
+ }, [diagnostics, mounted]);
21
+ const shelters = useMemo(() => document.volumes.filter((volume) => volume.kind === "shelter"), [document.volumes]);
22
+ useEffect(() => {
23
+ if (shelters.length > WEATHER_SHELTER_BUDGET)
24
+ warnOnce("weather-shelter-budget", `[jgengine] Weather renders the closest ${WEATHER_SHELTER_BUDGET} of ${shelters.length} shelters; gameplay uses all authored shelters.`);
25
+ }, [shelters.length]);
26
+ const sample = useCallback((x, z) => ctx.environment.sample(x, z), [ctx.environment]);
27
+ const exposureAt = useCallback((x, y, z) => ctx.environment.exposureAt(x, y, z), [ctx.environment]);
28
+ if (config === undefined)
29
+ return null;
30
+ return _jsx(WeatherLayer, { sample: sample, metrics: diagnostics ? metrics.current : undefined, shelters: shelters, rain: rain, snow: snow, dust: dust, impacts: heightAt === undefined ? false : { heightAt, exposureAt } });
31
+ }
@@ -26,6 +26,7 @@ export interface DustFieldProps {
26
26
  */
27
27
  groundBias?: number;
28
28
  timeScale?: number;
29
+ timeSeconds?: number | (() => number);
29
30
  seed?: number;
30
31
  renderOrder?: number;
31
32
  frustumCulled?: boolean;
@@ -38,4 +39,4 @@ export interface DustFieldProps {
38
39
  *
39
40
  * @capability weather-dust airborne particulate that turns still air into visible wind
40
41
  */
41
- export declare function DustField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, groundBias, timeScale, seed, renderOrder, frustumCulled, }: DustFieldProps): import("react").JSX.Element;
42
+ export declare function DustField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, groundBias, timeScale, timeSeconds, seed, renderOrder, frustumCulled, }: DustFieldProps): import("react").JSX.Element;
@@ -4,6 +4,7 @@ import * as THREE from "three";
4
4
  import { useDisposable } from "../render/useDisposable.js";
5
5
  import { createWeatherQuadGeometry } from "./weatherGeometry.js";
6
6
  import { DEFAULT_DUST_COUNT, DEFAULT_DUST_DENSITY, resolveWeatherInstanceCount } from "./weatherMath.js";
7
+ import { WEATHER_SHELTER_SHADER } from "./weatherShelter.js";
7
8
  import { useWeatherUniformSet } from "./weatherUniforms.js";
8
9
  const DEFAULT_VOLUME = [70, 22, 70];
9
10
  const DEFAULT_ORIGIN = [0, 0, 0];
@@ -16,13 +17,16 @@ const DEFAULT_DUST_COLOR = "#c8a878";
16
17
  *
17
18
  * @capability weather-dust airborne particulate that turns still air into visible wind
18
19
  */
19
- export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 2.4, size = 0.16, sway = 0.5, opacity = 0.3, color = DEFAULT_DUST_COLOR, groundBias = 0.65, timeScale, seed = 51407, renderOrder = 10, frustumCulled, }) {
20
+ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 2.4, size = 0.16, sway = 0.5, opacity = 0.3, color = DEFAULT_DUST_COLOR, groundBias = 0.65, timeScale, timeSeconds, seed = 51407, renderOrder = 10, frustumCulled, }) {
20
21
  const { camera } = useThree();
21
- const shared = useWeatherUniformSet({ wind, timeScale });
22
+ const shared = useWeatherUniformSet({ wind, timeScale, timeSeconds });
22
23
  const geometry = useDisposable(() => createWeatherQuadGeometry(count, seed), [count, seed]);
23
24
  const material = useDisposable(() => {
24
25
  const uniforms = {
25
26
  uTime: shared.time,
27
+ uShelterCenters: shared.shelterCenters,
28
+ uShelterShapes: shared.shelterShapes,
29
+ uShelterCount: shared.shelterCount,
26
30
  uWind: shared.wind,
27
31
  uAnchor: { value: new THREE.Vector3() },
28
32
  uVolume: { value: new THREE.Vector3() },
@@ -39,6 +43,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
39
43
  depthWrite: false,
40
44
  blending: THREE.NormalBlending,
41
45
  vertexShader: `
46
+ ${WEATHER_SHELTER_SHADER}
42
47
  uniform float uTime;
43
48
  uniform vec3 uWind;
44
49
  uniform vec3 uAnchor;
@@ -51,6 +56,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
51
56
  attribute vec3 aSpawn;
52
57
  attribute float aDrift;
53
58
 
59
+ varying float vExposure;
54
60
  varying vec2 vUv;
55
61
  varying float vDrift;
56
62
  varying float vFade;
@@ -76,6 +82,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
76
82
  vec3 travel = normalize(vec3(uWind.x, 0.0, uWind.z) + vec3(0.0001, 0.0, 0.0)) * carry;
77
83
  vec3 local = spawn * uVolume + (uWind + travel) * uTime + bob;
78
84
  vec3 worldCenter = mod(local - origin, uVolume) + origin;
85
+ vExposure = weatherExposure(worldCenter);
79
86
 
80
87
  // Fade at the top of the volume so recycled motes do not pop into view against the sky.
81
88
  float height = (worldCenter.y - origin.y) / max(uVolume.y, 0.001);
@@ -93,6 +100,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
93
100
  uniform float uOpacity;
94
101
  uniform vec3 uColor;
95
102
 
103
+ varying float vExposure;
96
104
  varying vec2 vUv;
97
105
  varying float vDrift;
98
106
  varying float vFade;
@@ -102,6 +110,7 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
102
110
  // No hard core: dust is diffuse, and a bright centre reads as snow.
103
111
  float puff = smoothstep(1.0, 0.05, distanceFromCenter);
104
112
  float alpha = puff * puff * uOpacity * (0.35 + 0.65 * vDrift) * vFade;
113
+ alpha *= vExposure;
105
114
  if (alpha < 0.001) discard;
106
115
  gl_FragColor = vec4(uColor, alpha);
107
116
  }
@@ -123,7 +132,12 @@ export function DustField({ count = DEFAULT_DUST_COUNT, density = DEFAULT_DUST_D
123
132
  uniforms.uOpacity.value = opacity;
124
133
  uniforms.uGroundBias.value = groundBias;
125
134
  uniforms.uColor.value.set(color);
126
- geometry.instanceCount = resolveWeatherInstanceCount(count, density, budget);
135
+ geometry.instanceCount = resolveWeatherInstanceCount(count, density * shared.dust.value, budget);
136
+ const metrics = shared.metrics?.dust;
137
+ if (metrics !== undefined) {
138
+ metrics.count = geometry.instanceCount;
139
+ metrics.capacity = geometry.getAttribute("aSpawn").count;
140
+ }
127
141
  });
128
142
  return (_jsx("mesh", { geometry: geometry, material: material, frustumCulled: frustumCulled ?? !followCamera, renderOrder: renderOrder }));
129
143
  }
@@ -2,7 +2,7 @@ import * as THREE from "three";
2
2
  import type { FireGrid } from "@jgengine/core/world/weather";
3
3
  export interface FireSpreadLayerProps {
4
4
  grid: FireGrid;
5
- cellSize: number;
5
+ cellSize?: number;
6
6
  origin?: readonly [number, number];
7
7
  /** Sample ground height so flames sit on terrain; defaults to y=0. */
8
8
  heightAt?: (x: number, z: number) => number;
@@ -10,5 +10,7 @@ export interface FireSpreadLayerProps {
10
10
  flameHeight?: number;
11
11
  burningColor?: THREE.ColorRepresentation;
12
12
  emberColor?: THREE.ColorRepresentation;
13
+ /** Absolute authoritative simulation clock; use for pause and snapshot restore. */
14
+ timeSeconds?: () => number;
13
15
  }
14
- export declare function FireSpreadLayer({ grid, cellSize, origin, heightAt, flameHeight, burningColor, emberColor, }: FireSpreadLayerProps): import("react").JSX.Element;
16
+ export declare function FireSpreadLayer({ grid, cellSize, origin, heightAt, flameHeight, burningColor, emberColor, timeSeconds, }: FireSpreadLayerProps): import("react").JSX.Element;
@@ -3,8 +3,7 @@ import { useEffect, useMemo, useRef } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import * as THREE from "three";
5
5
  import { setBillboardQuaternion } from "./fireSpreadPose.js";
6
- const HIDDEN = new THREE.Matrix4().makeScale(0, 0, 0);
7
- export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }) {
6
+ export function FireSpreadLayer({ grid, cellSize = grid.cellSize, origin = grid.origin, heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", timeSeconds, }) {
8
7
  const flames = useRef(null);
9
8
  const scorch = useRef(null);
10
9
  const capacity = grid.cols * grid.rows;
@@ -33,13 +32,12 @@ export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, fla
33
32
  const scorchMesh = scorch.current;
34
33
  if (flameMesh === null || scorchMesh === null)
35
34
  return;
36
- const cells = grid.snapshot();
37
- const flicker = 0.85 + 0.15 * Math.sin(state.clock.elapsedTime * 12);
35
+ const flicker = 0.85 + 0.15 * Math.sin((timeSeconds?.() ?? state.clock.elapsedTime) * 12);
38
36
  let flameIndex = 0;
39
37
  let scorchIndex = 0;
40
38
  for (let row = 0; row < grid.rows; row += 1) {
41
39
  for (let col = 0; col < grid.cols; col += 1) {
42
- const cell = cells[row * grid.cols + col];
40
+ const cell = grid.cell(col, row);
43
41
  const x = origin[0] + col * cellSize;
44
42
  const z = origin[1] + row * cellSize;
45
43
  const groundY = heightAt?.(x, z) ?? 0;
@@ -60,12 +58,8 @@ export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, fla
60
58
  }
61
59
  }
62
60
  }
63
- for (let i = flameIndex; i < capacity; i += 1)
64
- flameMesh.setMatrixAt(i, HIDDEN);
65
- for (let i = scorchIndex; i < capacity; i += 1)
66
- scorchMesh.setMatrixAt(i, HIDDEN);
67
- flameMesh.count = capacity;
68
- scorchMesh.count = capacity;
61
+ flameMesh.count = flameIndex;
62
+ scorchMesh.count = scorchIndex;
69
63
  flameMesh.instanceMatrix.needsUpdate = true;
70
64
  scorchMesh.instanceMatrix.needsUpdate = true;
71
65
  });
@@ -13,5 +13,8 @@ export interface LightningStrikeProps {
13
13
  jaggedness?: number;
14
14
  impactLight?: number;
15
15
  renderOrder?: number;
16
+ /** Supply both fields to replay the strike from authoritative simulation state. */
17
+ timeSeconds?: () => number;
18
+ startedAt?: number;
16
19
  }
17
- export declare function LightningStrike({ origin, target, strikeKey, seed, visible, duration, color, glow, branches, jaggedness, impactLight, renderOrder, }: LightningStrikeProps): import("react").JSX.Element;
20
+ export declare function LightningStrike({ origin, target, strikeKey, seed, visible, duration, color, glow, branches, jaggedness, impactLight, renderOrder, timeSeconds, startedAt, }: LightningStrikeProps): import("react").JSX.Element;