@jgengine/shell 0.19.0 → 0.22.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 (215) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +113 -15
  5. package/dist/Shell3dPresentation.d.ts +2 -2
  6. package/dist/Shell3dPresentation.js +49 -7
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +11 -1
  10. package/dist/ShellHudPresentation.js +20 -6
  11. package/dist/audio/audioEngine.d.ts +16 -0
  12. package/dist/audio/audioEngine.js +178 -89
  13. package/dist/audio/audioWire.js +2 -2
  14. package/dist/audio/synthEngine.d.ts +7 -1
  15. package/dist/audio/synthEngine.js +103 -38
  16. package/dist/camera/GameCameraRig.js +2 -1
  17. package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
  18. package/dist/camera/GameFirstPersonCamera.js +17 -13
  19. package/dist/camera/cameraRigs.js +8 -2
  20. package/dist/camera/chaseHeading.d.ts +9 -0
  21. package/dist/camera/chaseHeading.js +10 -0
  22. package/dist/camera/customMuzzle.d.ts +14 -0
  23. package/dist/camera/customMuzzle.js +68 -0
  24. package/dist/camera/index.d.ts +1 -0
  25. package/dist/camera/index.js +1 -0
  26. package/dist/camera/rigResolve.d.ts +3 -0
  27. package/dist/camera/rigResolve.js +11 -0
  28. package/dist/combat/ProjectileModels.d.ts +31 -0
  29. package/dist/combat/ProjectileModels.js +70 -0
  30. package/dist/commandSink.d.ts +1 -2
  31. package/dist/commandSink.js +3 -2
  32. package/dist/defineGame.d.ts +12 -5
  33. package/dist/defineGame.js +25 -10
  34. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  35. package/dist/devtools/DevtoolsOverlay.js +14 -168
  36. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  37. package/dist/devtools/DevtoolsRuntime.js +61 -0
  38. package/dist/devtools/agentBridge.js +1 -1
  39. package/dist/devtools/devtoolsReports.d.ts +68 -0
  40. package/dist/devtools/devtoolsReports.js +100 -0
  41. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  42. package/dist/devtools/latencyInstrumentation.js +11 -0
  43. package/dist/devtools/playtestProbe.d.ts +4 -0
  44. package/dist/devtools/playtestProbe.js +22 -0
  45. package/dist/drivers/FrameDriver.js +25 -21
  46. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  47. package/dist/drivers/HudOnlyDriver.js +32 -14
  48. package/dist/environment/EnvironmentScene.js +19 -15
  49. package/dist/environment/terrainGroundColor.d.ts +5 -4
  50. package/dist/environment/terrainGroundColor.js +19 -8
  51. package/dist/inputSink.d.ts +6 -6
  52. package/dist/inputSink.js +60 -21
  53. package/dist/lazyShell3dPresentation.d.ts +9 -0
  54. package/dist/lazyShell3dPresentation.js +24 -0
  55. package/dist/materialOverride.d.ts +42 -0
  56. package/dist/materialOverride.js +95 -36
  57. package/dist/multiplayer.d.ts +33 -3
  58. package/dist/multiplayer.js +105 -22
  59. package/dist/pointer/pointerService.d.ts +2 -0
  60. package/dist/pointer/pointerService.js +12 -2
  61. package/dist/postfx/PostProcessing.d.ts +10 -0
  62. package/dist/postfx/PostProcessing.js +9 -41
  63. package/dist/postfx/gradeShader.d.ts +1 -1
  64. package/dist/postfx/gradeShader.js +30 -2
  65. package/dist/postfx/shadowPrepass.d.ts +3 -0
  66. package/dist/postfx/shadowPrepass.js +18 -0
  67. package/dist/presentationRecovery.d.ts +75 -0
  68. package/dist/presentationRecovery.js +204 -0
  69. package/dist/presentationRecovery3d.d.ts +4 -0
  70. package/dist/presentationRecovery3d.js +32 -0
  71. package/dist/presentationResolve.d.ts +8 -1
  72. package/dist/presentationResolve.js +5 -2
  73. package/dist/render/CascadedShadows.d.ts +7 -2
  74. package/dist/render/CascadedShadows.js +98 -20
  75. package/dist/render/EnvironmentLighting.js +74 -35
  76. package/dist/render/HairCards.d.ts +7 -0
  77. package/dist/render/HairCards.js +23 -0
  78. package/dist/render/SceneModels.js +51 -77
  79. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  80. package/dist/render/authoredSurfaceMaterial.js +108 -0
  81. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  82. package/dist/render/materialAppearanceSignals.js +58 -0
  83. package/dist/render/materialAsset.d.ts +98 -0
  84. package/dist/render/materialAsset.js +454 -0
  85. package/dist/render/measureBounds.js +16 -3
  86. package/dist/render/measureCollisionMesh.js +8 -1
  87. package/dist/render/modelAssets.d.ts +8 -0
  88. package/dist/render/modelAssets.js +43 -0
  89. package/dist/render/modelBindPose.d.ts +5 -0
  90. package/dist/render/modelBindPose.js +35 -0
  91. package/dist/render/modelLoad.d.ts +3 -0
  92. package/dist/render/modelLoad.js +9 -0
  93. package/dist/render/modelNodeVisibility.d.ts +6 -0
  94. package/dist/render/modelNodeVisibility.js +18 -0
  95. package/dist/render/modelRender.d.ts +18 -1
  96. package/dist/render/modelRender.js +81 -3
  97. package/dist/render/projectedOverdraw.d.ts +35 -0
  98. package/dist/render/projectedOverdraw.js +204 -0
  99. package/dist/render/resolveModel.d.ts +12 -2
  100. package/dist/render/resolveModel.js +21 -4
  101. package/dist/render/rigNode.d.ts +26 -0
  102. package/dist/render/rigNode.js +32 -0
  103. package/dist/render/spriteRender.d.ts +4 -0
  104. package/dist/render/spriteRender.js +5 -0
  105. package/dist/render/staticShapeInstances.d.ts +34 -0
  106. package/dist/render/staticShapeInstances.js +78 -0
  107. package/dist/render/useFootIk.js +76 -11
  108. package/dist/render/useModelAnimation.d.ts +28 -2
  109. package/dist/render/useModelAnimation.js +194 -69
  110. package/dist/render/useModelInstance.d.ts +25 -0
  111. package/dist/render/useModelInstance.js +43 -0
  112. package/dist/scatter/scatterModels.d.ts +3 -2
  113. package/dist/scatter/scatterModels.js +15 -7
  114. package/dist/scene/AuthoredScene.d.ts +14 -6
  115. package/dist/scene/AuthoredScene.js +30 -9
  116. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  117. package/dist/scene/GrassFieldRenderer.js +20 -2
  118. package/dist/settings/SettingsMenu.js +11 -18
  119. package/dist/settings/SettingsRuntime.d.ts +5 -0
  120. package/dist/settings/SettingsRuntime.js +29 -2
  121. package/dist/shellControlSuspension.d.ts +18 -0
  122. package/dist/shellControlSuspension.js +28 -0
  123. package/dist/shellInputPublication.d.ts +28 -0
  124. package/dist/shellInputPublication.js +49 -0
  125. package/dist/shellPointerInput.d.ts +18 -0
  126. package/dist/shellPointerInput.js +49 -0
  127. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  128. package/dist/structures/BuildingKitBatch.js +34 -59
  129. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  130. package/dist/structures/BuildingRenderBudget.js +54 -0
  131. package/dist/structures/GeneratedBuilding.js +13 -8
  132. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  133. package/dist/structures/buildingKitMaterials.js +70 -0
  134. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  135. package/dist/structures/buildingSpatialBatch.js +45 -0
  136. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  137. package/dist/terrain/CarvedTerrain.js +2 -2
  138. package/dist/terrain/GrassField.d.ts +3 -1
  139. package/dist/terrain/GrassField.js +61 -6
  140. package/dist/terrain/grassGeometry.d.ts +13 -0
  141. package/dist/terrain/grassGeometry.js +57 -1
  142. package/dist/terrain/grassMaterial.js +2 -2
  143. package/dist/terrain/index.d.ts +1 -0
  144. package/dist/terrain/index.js +1 -0
  145. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  146. package/dist/terrain/terrainDetailMaterial.js +7 -7
  147. package/dist/terrain/terrainMath.d.ts +2 -0
  148. package/dist/terrain/terrainMath.js +2 -0
  149. package/dist/terrain/terrainPaintColor.d.ts +11 -0
  150. package/dist/terrain/terrainPaintColor.js +47 -0
  151. package/dist/touch/TouchControlsOverlay.js +4 -4
  152. package/dist/useShellMultiplayerSync.js +4 -0
  153. package/dist/vfx/ParticleField.d.ts +23 -20
  154. package/dist/vfx/ParticleField.js +168 -50
  155. package/dist/visibility/CullingProvider.js +2 -62
  156. package/dist/visibility/cullingDriver.d.ts +9 -0
  157. package/dist/visibility/cullingDriver.js +68 -0
  158. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  159. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  160. package/dist/weather/DustField.d.ts +2 -1
  161. package/dist/weather/DustField.js +17 -3
  162. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  163. package/dist/weather/FireSpreadLayer.js +5 -11
  164. package/dist/weather/LightningStrike.d.ts +4 -1
  165. package/dist/weather/LightningStrike.js +13 -17
  166. package/dist/weather/RainField.d.ts +2 -1
  167. package/dist/weather/RainField.js +17 -3
  168. package/dist/weather/RainImpactField.d.ts +16 -0
  169. package/dist/weather/RainImpactField.js +89 -0
  170. package/dist/weather/SnowField.d.ts +2 -1
  171. package/dist/weather/SnowField.js +17 -3
  172. package/dist/weather/WeatherLayer.d.ts +11 -2
  173. package/dist/weather/WeatherLayer.js +30 -6
  174. package/dist/weather/index.d.ts +2 -0
  175. package/dist/weather/index.js +2 -0
  176. package/dist/weather/weatherMath.d.ts +2 -0
  177. package/dist/weather/weatherMath.js +8 -0
  178. package/dist/weather/weatherShelter.d.ts +18 -0
  179. package/dist/weather/weatherShelter.js +75 -0
  180. package/dist/weather/weatherUniforms.d.ts +40 -1
  181. package/dist/weather/weatherUniforms.js +29 -9
  182. package/dist/world/AuthoredFlocks.d.ts +22 -0
  183. package/dist/world/AuthoredFlocks.js +87 -0
  184. package/dist/world/StaticObjectModels.d.ts +8 -0
  185. package/dist/world/StaticObjectModels.js +75 -0
  186. package/dist/world/WorldHud.d.ts +14 -6
  187. package/dist/world/WorldHud.js +25 -12
  188. package/dist/world/WorldParticles.d.ts +10 -23
  189. package/dist/world/WorldParticles.js +209 -61
  190. package/dist/world/WorldScene.d.ts +3 -1
  191. package/dist/world/WorldScene.js +41 -26
  192. package/dist/world/staticObjectBatches.d.ts +35 -0
  193. package/dist/world/staticObjectBatches.js +163 -0
  194. package/dist/world/worldBarSamples.d.ts +37 -33
  195. package/dist/world/worldBarSamples.js +117 -75
  196. package/package.json +4 -4
  197. package/skills/ce-handoff/SKILL.md +1 -1
  198. package/skills/game-design/SKILL.md +2 -0
  199. package/skills/jgengine/SKILL.md +23 -2
  200. package/skills/jgengine/api.md +28 -17
  201. package/skills/jgengine/recipes/minimal-game.md +7 -2
  202. package/skills/jgengine-ui/SKILL.md +3 -1
  203. package/skills/jgengine-ui/api.md +219 -70
  204. package/skills/jgengine-ui/capabilities.md +119 -1
  205. package/skills/jgengine-ui/reference-primitives.md +93 -3
  206. package/skills/jgengine-ui/reference.md +2 -0
  207. package/skills/jgengine-world/SKILL.md +12 -3
  208. package/skills/jgengine-world/api.md +201 -58
  209. package/skills/jgengine-world/capabilities.md +116 -3
  210. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  211. package/skills/jgengine-world/recipes/controls.md +4 -0
  212. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  213. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  214. package/skills/jgengine-world/reference.md +167 -5
  215. package/skills/jgengine-world/visibility.md +28 -1
@@ -14,8 +14,9 @@ export interface SnowFieldProps {
14
14
  opacity?: number;
15
15
  color?: THREE.ColorRepresentation;
16
16
  timeScale?: number;
17
+ timeSeconds?: number | (() => number);
17
18
  seed?: number;
18
19
  renderOrder?: number;
19
20
  frustumCulled?: boolean;
20
21
  }
21
- export declare function SnowField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, timeScale, seed, renderOrder, frustumCulled, }: SnowFieldProps): import("react").JSX.Element;
22
+ export declare function SnowField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, timeScale, timeSeconds, seed, renderOrder, frustumCulled, }: SnowFieldProps): import("react").JSX.Element;
@@ -4,17 +4,21 @@ import * as THREE from "three";
4
4
  import { useDisposable } from "../render/useDisposable.js";
5
5
  import { createWeatherQuadGeometry } from "./weatherGeometry.js";
6
6
  import { DEFAULT_SNOW_COUNT, DEFAULT_SNOW_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 = [52, 38, 52];
9
10
  const DEFAULT_ORIGIN = [0, 0, 0];
10
11
  const DEFAULT_SNOW_COLOR = "#ffffff";
11
- export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, frustumCulled, }) {
12
+ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, timeSeconds, seed = 72931, renderOrder = 11, frustumCulled, }) {
12
13
  const { camera } = useThree();
13
- const shared = useWeatherUniformSet({ wind, timeScale });
14
+ const shared = useWeatherUniformSet({ wind, timeScale, timeSeconds });
14
15
  const geometry = useDisposable(() => createWeatherQuadGeometry(count, seed), [count, seed]);
15
16
  const material = useDisposable(() => {
16
17
  const uniforms = {
17
18
  uTime: shared.time,
19
+ uShelterCenters: shared.shelterCenters,
20
+ uShelterShapes: shared.shelterShapes,
21
+ uShelterCount: shared.shelterCount,
18
22
  uWind: shared.wind,
19
23
  uAnchor: { value: new THREE.Vector3() },
20
24
  uVolume: { value: new THREE.Vector3() },
@@ -30,6 +34,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
30
34
  depthWrite: false,
31
35
  blending: THREE.NormalBlending,
32
36
  vertexShader: `
37
+ ${WEATHER_SHELTER_SHADER}
33
38
  uniform float uTime;
34
39
  uniform vec3 uWind;
35
40
  uniform vec3 uAnchor;
@@ -41,6 +46,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
41
46
  attribute vec3 aSpawn;
42
47
  attribute float aDrift;
43
48
 
49
+ varying float vExposure;
44
50
  varying vec2 vUv;
45
51
  varying float vDrift;
46
52
 
@@ -58,6 +64,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
58
64
  ) * uSway * (0.45 + 0.55 * aDrift);
59
65
  vec3 local = aSpawn * uVolume + vec3(uWind.x, -fallSpeed, uWind.z) * uTime + wander;
60
66
  vec3 worldCenter = mod(local - origin, uVolume) + origin;
67
+ vExposure = weatherExposure(worldCenter);
61
68
  float flakeSize = uSize * (0.48 + 1.08 * aDrift);
62
69
  vec3 right = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
63
70
  vec3 up = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
@@ -70,6 +77,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
70
77
  uniform float uOpacity;
71
78
  uniform vec3 uColor;
72
79
 
80
+ varying float vExposure;
73
81
  varying vec2 vUv;
74
82
  varying float vDrift;
75
83
 
@@ -78,6 +86,7 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
78
86
  float disc = smoothstep(1.0, 0.12, distanceFromCenter);
79
87
  float core = smoothstep(0.54, 0.0, distanceFromCenter) * 0.42;
80
88
  float alpha = (disc + core) * uOpacity * (0.56 + 0.44 * vDrift);
89
+ alpha *= vExposure;
81
90
  if (alpha < 0.001) discard;
82
91
  gl_FragColor = vec4(uColor, alpha);
83
92
  }
@@ -98,7 +107,12 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
98
107
  uniforms.uSway.value = sway;
99
108
  uniforms.uOpacity.value = opacity;
100
109
  uniforms.uColor.value.set(color);
101
- geometry.instanceCount = resolveWeatherInstanceCount(count, density, budget);
110
+ geometry.instanceCount = resolveWeatherInstanceCount(count, density * shared.snow.value, budget);
111
+ const metrics = shared.metrics?.snow;
112
+ if (metrics !== undefined) {
113
+ metrics.count = geometry.instanceCount;
114
+ metrics.capacity = geometry.getAttribute("aSpawn").count;
115
+ }
102
116
  });
103
117
  return (_jsx("mesh", { geometry: geometry, material: material, frustumCulled: frustumCulled ?? !followCamera, renderOrder: renderOrder }));
104
118
  }
@@ -1,8 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { EditorVolume } from "@jgengine/core/editor/types";
3
+ import type { AuthoredWeatherSample } from "@jgengine/core/world/authoredWeather";
2
4
  import { type DustFieldProps } from "./DustField.js";
3
5
  import { type RainFieldProps } from "./RainField.js";
6
+ import { type RainImpactFieldProps } from "./RainImpactField.js";
4
7
  import { type SnowFieldProps } from "./SnowField.js";
5
- import { type WeatherVector } from "./weatherUniforms.js";
8
+ import { type WeatherVector, type WeatherParticleMetrics } from "./weatherUniforms.js";
6
9
  /** `"mixed"` runs rain and snow together; `"dust"` is airborne particulate and composes with either via `dust`. */
7
10
  export type WeatherLayerMode = "clear" | "rain" | "snow" | "mixed" | "dust";
8
11
  export interface WeatherLayerProps {
@@ -11,13 +14,19 @@ export interface WeatherLayerProps {
11
14
  wind?: WeatherVector;
12
15
  lightning?: number;
13
16
  timeScale?: number;
17
+ timeSeconds?: number | (() => number);
18
+ /** Read authored conditions at the camera from the authoritative simulation clock. */
19
+ sample?: (x: number, z: number) => AuthoredWeatherSample;
20
+ shelters?: readonly EditorVolume[];
21
+ metrics?: WeatherParticleMetrics;
14
22
  rain?: Omit<RainFieldProps, "wind" | "lightning" | "timeScale"> | false;
15
23
  snow?: Omit<SnowFieldProps, "wind" | "timeScale"> | false;
16
24
  /** Airborne particulate. Drawn in `"dust"` mode, and alongside rain/snow whenever `dustAlways` is set. */
17
25
  dust?: Omit<DustFieldProps, "wind" | "timeScale"> | false;
18
26
  /** Keep dust in the air in every mode — a world that is dusty whatever else the sky is doing. */
19
27
  dustAlways?: boolean;
28
+ impacts?: RainImpactFieldProps | false;
20
29
  enabled?: boolean;
21
30
  children?: ReactNode;
22
31
  }
23
- export declare function WeatherLayer({ mode, intensity, wind, lightning, timeScale, rain, snow, dust, dustAlways, enabled, children, }: WeatherLayerProps): import("react").JSX.Element | null;
32
+ export declare function WeatherLayer({ mode, intensity, wind, lightning, timeScale, timeSeconds, sample, shelters, metrics, rain, snow, dust, dustAlways, impacts, enabled, children, }: WeatherLayerProps): import("react").JSX.Element | null;
@@ -1,19 +1,43 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DustField } from "./DustField.js";
3
3
  import { RainField } from "./RainField.js";
4
+ import { RainImpactField } from "./RainImpactField.js";
4
5
  import { SnowField } from "./SnowField.js";
5
6
  import { WeatherUniformProvider } from "./weatherUniforms.js";
7
+ function clearMetrics(metrics, layer) {
8
+ metrics[layer].count = 0;
9
+ metrics[layer].capacity = 0;
10
+ if (layer === "impacts") {
11
+ metrics.impacts.heightQueries = 0;
12
+ metrics.impacts.exposureQueries = 0;
13
+ metrics.impacts.queryBudget = 0;
14
+ }
15
+ }
6
16
  function resolveLayerDensity(value, fallback, intensity) {
7
17
  return (value ?? fallback) * intensity;
8
18
  }
9
- export function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, rain, snow, dust, dustAlways = false, enabled = true, children, }) {
10
- if (!enabled)
19
+ export function WeatherLayer({ mode = "clear", intensity = 1, wind, lightning, timeScale, timeSeconds, sample, shelters, metrics, rain, snow, dust, dustAlways = false, impacts, enabled = true, children, }) {
20
+ if (!enabled) {
21
+ if (metrics !== undefined)
22
+ for (const layer of ["rain", "snow", "dust", "impacts"])
23
+ clearMetrics(metrics, layer);
11
24
  return null;
25
+ }
12
26
  const rainProps = rain === false ? null : (rain ?? {});
13
27
  const snowProps = snow === false ? null : (snow ?? {});
14
- const showRain = rainProps !== null && (mode === "rain" || mode === "mixed");
15
- const showSnow = snowProps !== null && (mode === "snow" || mode === "mixed");
28
+ const showRain = rainProps !== null && (sample !== undefined || mode === "rain" || mode === "mixed");
29
+ const showSnow = snowProps !== null && (sample !== undefined || mode === "snow" || mode === "mixed");
16
30
  const dustProps = dust === false ? null : (dust ?? {});
17
- const showDust = dustProps !== null && (mode === "dust" || dustAlways);
18
- return (_jsxs(WeatherUniformProvider, { wind: wind, lightning: lightning, timeScale: timeScale, children: [showRain ? (_jsx(RainField, { ...rainProps, density: resolveLayerDensity(rainProps.density, 0.45, intensity) })) : null, showSnow ? (_jsx(SnowField, { ...snowProps, density: resolveLayerDensity(snowProps.density, 0.5, intensity) })) : null, showDust ? (_jsx(DustField, { ...dustProps, density: resolveLayerDensity(dustProps.density, 0.4, intensity) })) : null, children] }));
31
+ const showDust = dustProps !== null && (sample !== undefined || mode === "dust" || dustAlways);
32
+ if (metrics !== undefined) {
33
+ if (!showRain)
34
+ clearMetrics(metrics, "rain");
35
+ if (!showSnow)
36
+ clearMetrics(metrics, "snow");
37
+ if (!showDust)
38
+ clearMetrics(metrics, "dust");
39
+ if (!showRain || impacts === undefined || impacts === false)
40
+ clearMetrics(metrics, "impacts");
41
+ }
42
+ return (_jsxs(WeatherUniformProvider, { wind: wind, lightning: lightning, timeScale: timeScale, timeSeconds: timeSeconds, sample: sample, shelters: shelters, metrics: metrics, children: [showRain ? (_jsx(RainField, { ...rainProps, density: resolveLayerDensity(rainProps.density, 0.45, intensity) })) : null, showSnow ? (_jsx(SnowField, { ...snowProps, density: resolveLayerDensity(snowProps.density, 0.5, intensity) })) : null, showDust ? (_jsx(DustField, { ...dustProps, density: resolveLayerDensity(dustProps.density, 0.4, intensity) })) : null, showRain && impacts !== undefined && impacts !== false ? _jsx(RainImpactField, { ...impacts }) : null, children] }));
19
43
  }
@@ -1,4 +1,6 @@
1
1
  export { DustField, type DustFieldProps } from "./DustField.js";
2
+ export { AuthoredWeatherLayer, type AuthoredWeatherLayerProps } from "./AuthoredWeatherLayer.js";
3
+ export { RainImpactField, type RainImpactFieldProps } from "./RainImpactField.js";
2
4
  export { FireSpreadLayer, type FireSpreadLayerProps } from "./FireSpreadLayer.js";
3
5
  export { LightningStrike, type LightningStrikeProps } from "./LightningStrike.js";
4
6
  export { RainField, type RainFieldProps } from "./RainField.js";
@@ -1,4 +1,6 @@
1
1
  export { DustField } from "./DustField.js";
2
+ export { AuthoredWeatherLayer } from "./AuthoredWeatherLayer.js";
3
+ export { RainImpactField } from "./RainImpactField.js";
2
4
  export { FireSpreadLayer } from "./FireSpreadLayer.js";
3
5
  export { LightningStrike } from "./LightningStrike.js";
4
6
  export { RainField } from "./RainField.js";
@@ -16,3 +16,5 @@ export interface WeatherSeedAttributes {
16
16
  }
17
17
  /** @internal */
18
18
  export declare function createWeatherSeedAttributes(maxCount: number, seed: number): WeatherSeedAttributes;
19
+ /** @internal */
20
+ export declare function lightningStrikeEnvelope(timeSeconds: number, startedAt: number, duration: number, seed: number): number;
@@ -40,3 +40,11 @@ export function createWeatherSeedAttributes(maxCount, seed) {
40
40
  }
41
41
  return { spawn, drift };
42
42
  }
43
+ /** @internal */
44
+ export function lightningStrikeEnvelope(timeSeconds, startedAt, duration, seed) {
45
+ const elapsed = timeSeconds - startedAt;
46
+ if (elapsed < 0 || duration <= 0 || elapsed >= duration)
47
+ return 0;
48
+ const flicker = 0.62 + (Math.sin(elapsed * 97 + seed) * 0.5 + 0.5) * 0.38;
49
+ return (1 - elapsed / duration) * flicker;
50
+ }
@@ -0,0 +1,18 @@
1
+ import * as THREE from "three";
2
+ import type { EditorVolume } from "@jgengine/core/editor/types";
3
+ /** Cosmetic roof clipping is bounded independently of authoritative shelter queries. */
4
+ export declare const WEATHER_SHELTER_BUDGET = 32;
5
+ /** Reusable buffers for the nearest authored shelters submitted to weather shaders. */
6
+ export interface WeatherShelterUniforms {
7
+ shelterCenters: THREE.IUniform<THREE.Vector4[]>;
8
+ shelterShapes: THREE.IUniform<THREE.Vector4[]>;
9
+ shelterCount: THREE.IUniform<number>;
10
+ shelterIndices: Int32Array;
11
+ shelterDistances: Float64Array;
12
+ }
13
+ /** @internal */
14
+ export declare function createWeatherShelterUniforms(): WeatherShelterUniforms;
15
+ /** @internal */
16
+ export declare function updateWeatherShelters(uniforms: WeatherShelterUniforms, shelters: readonly EditorVolume[], x: number, y: number, z: number): void;
17
+ /** @internal */
18
+ export declare const WEATHER_SHELTER_SHADER = "\n uniform vec4 uShelterCenters[32];\n uniform vec4 uShelterShapes[32];\n uniform int uShelterCount;\n float weatherExposure(vec3 point) {\n float exposure = 1.0;\n for (int i = 0; i < 32; i++) {\n if (i >= uShelterCount) break;\n vec4 center = uShelterCenters[i];\n vec4 shape = uShelterShapes[i];\n vec3 delta = abs(point - center.xyz);\n bool inside = center.w < 0.5 ? all(lessThanEqual(delta, shape.xyz)) : center.w < 1.5 ? length(delta) <= shape.x : length(delta.xz) <= shape.x && delta.y <= shape.y;\n if (inside) exposure = min(exposure, shape.w);\n }\n return exposure;\n }\n";
@@ -0,0 +1,75 @@
1
+ import * as THREE from "three";
2
+ /** Cosmetic roof clipping is bounded independently of authoritative shelter queries. */
3
+ export const WEATHER_SHELTER_BUDGET = 32;
4
+ /** @internal */
5
+ export function createWeatherShelterUniforms() {
6
+ return {
7
+ shelterCenters: { value: Array.from({ length: WEATHER_SHELTER_BUDGET }, () => new THREE.Vector4()) },
8
+ shelterShapes: { value: Array.from({ length: WEATHER_SHELTER_BUDGET }, () => new THREE.Vector4()) },
9
+ shelterCount: { value: 0 },
10
+ shelterIndices: new Int32Array(WEATHER_SHELTER_BUDGET),
11
+ shelterDistances: new Float64Array(WEATHER_SHELTER_BUDGET),
12
+ };
13
+ }
14
+ /** @internal */
15
+ export function updateWeatherShelters(uniforms, shelters, x, y, z) {
16
+ const indices = uniforms.shelterIndices;
17
+ const distances = uniforms.shelterDistances;
18
+ distances.fill(Infinity);
19
+ indices.fill(-1);
20
+ for (let index = 0; index < shelters.length; index += 1) {
21
+ const shelter = shelters[index];
22
+ if (shelter.kind !== "shelter")
23
+ continue;
24
+ const hx = shelter.shape === "box" ? shelter.halfExtents?.x ?? 0 : shelter.radius ?? 0;
25
+ const hy = shelter.shape === "box" ? shelter.halfExtents?.y ?? 0 : shelter.shape === "sphere" ? shelter.radius ?? 0 : (shelter.height ?? 0) * 0.5;
26
+ const hz = shelter.shape === "box" ? shelter.halfExtents?.z ?? 0 : shelter.radius ?? 0;
27
+ const dx = Math.max(0, Math.abs(x - shelter.center.x) - hx);
28
+ const dy = Math.max(0, Math.abs(y - shelter.center.y) - hy);
29
+ const dz = Math.max(0, Math.abs(z - shelter.center.z) - hz);
30
+ const distance = dx * dx + dy * dy + dz * dz;
31
+ for (let slot = 0; slot < WEATHER_SHELTER_BUDGET; slot += 1) {
32
+ if (distance >= distances[slot])
33
+ continue;
34
+ for (let move = WEATHER_SHELTER_BUDGET - 1; move > slot; move -= 1) {
35
+ distances[move] = distances[move - 1];
36
+ indices[move] = indices[move - 1];
37
+ }
38
+ distances[slot] = distance;
39
+ indices[slot] = index;
40
+ break;
41
+ }
42
+ }
43
+ let count = 0;
44
+ for (let slot = 0; slot < WEATHER_SHELTER_BUDGET; slot += 1) {
45
+ const index = indices[slot];
46
+ if (index < 0)
47
+ break;
48
+ const shelter = shelters[index];
49
+ const center = shelter.center;
50
+ uniforms.shelterCenters.value[slot].set(center.x, center.y, center.z, shelter.shape === "box" ? 0 : shelter.shape === "sphere" ? 1 : 2);
51
+ const exposure = typeof shelter.meta?.exposure === "number" ? Math.max(0, Math.min(1, shelter.meta.exposure)) : 0;
52
+ const extents = shelter.halfExtents;
53
+ uniforms.shelterShapes.value[slot].set(shelter.shape === "box" ? extents?.x ?? 0 : shelter.radius ?? 0, shelter.shape === "box" ? extents?.y ?? 0 : (shelter.height ?? 0) * 0.5, shelter.shape === "box" ? extents?.z ?? 0 : 0, exposure);
54
+ count += 1;
55
+ }
56
+ uniforms.shelterCount.value = count;
57
+ }
58
+ /** @internal */
59
+ export const WEATHER_SHELTER_SHADER = `
60
+ uniform vec4 uShelterCenters[32];
61
+ uniform vec4 uShelterShapes[32];
62
+ uniform int uShelterCount;
63
+ float weatherExposure(vec3 point) {
64
+ float exposure = 1.0;
65
+ for (int i = 0; i < 32; i++) {
66
+ if (i >= uShelterCount) break;
67
+ vec4 center = uShelterCenters[i];
68
+ vec4 shape = uShelterShapes[i];
69
+ vec3 delta = abs(point - center.xyz);
70
+ bool inside = center.w < 0.5 ? all(lessThanEqual(delta, shape.xyz)) : center.w < 1.5 ? length(delta) <= shape.x : length(delta.xz) <= shape.x && delta.y <= shape.y;
71
+ if (inside) exposure = min(exposure, shape.w);
72
+ }
73
+ return exposure;
74
+ }
75
+ `;
@@ -1,19 +1,58 @@
1
1
  import { type ReactNode } from "react";
2
2
  import * as THREE from "three";
3
+ import type { EditorVolume } from "@jgengine/core/editor/types";
4
+ import { type WeatherShelterUniforms } from "./weatherShelter.js";
5
+ import type { AuthoredWeatherSample } from "@jgengine/core/world/authoredWeather";
3
6
  export type WeatherVector = readonly [number, number, number];
4
- export interface WeatherUniformSet {
7
+ /** Actual procedural weather submissions and per-frame impact query work. */
8
+ export interface WeatherParticleMetrics {
9
+ rain: {
10
+ count: number;
11
+ capacity: number;
12
+ };
13
+ snow: {
14
+ count: number;
15
+ capacity: number;
16
+ };
17
+ dust: {
18
+ count: number;
19
+ capacity: number;
20
+ };
21
+ impacts: {
22
+ count: number;
23
+ capacity: number;
24
+ heightQueries: number;
25
+ exposureQueries: number;
26
+ queryBudget: number;
27
+ };
28
+ }
29
+ export interface WeatherUniformSet extends WeatherShelterUniforms {
5
30
  time: THREE.IUniform<number>;
6
31
  wind: THREE.IUniform<THREE.Vector3>;
7
32
  lightning: THREE.IUniform<number>;
33
+ rain: THREE.IUniform<number>;
34
+ snow: THREE.IUniform<number>;
35
+ dust: THREE.IUniform<number>;
36
+ controlsWind: boolean;
37
+ metrics?: WeatherParticleMetrics;
8
38
  }
9
39
  export interface WeatherUniformOptions {
10
40
  wind?: WeatherVector;
11
41
  lightning?: number;
12
42
  timeScale?: number;
43
+ /** Absolute authoritative simulation seconds; no render delta is accumulated when supplied. */
44
+ timeSeconds?: number | (() => number);
45
+ sample?: (x: number, z: number) => AuthoredWeatherSample;
46
+ /** The closest 32 authored shelter volumes clip cosmetic particles. */
47
+ shelters?: readonly EditorVolume[];
48
+ /** Opt-in counters read from submitted geometry and actual query calls. */
49
+ metrics?: WeatherParticleMetrics;
13
50
  }
14
51
  /** @internal */
15
52
  export declare function createWeatherUniformSet(options?: WeatherUniformOptions): WeatherUniformSet;
16
53
  /** @internal */
54
+ export declare function updateWeatherUniformSet(uniforms: WeatherUniformSet, options: WeatherUniformOptions, delta: number, x?: number, z?: number, y?: number): void;
55
+ /** @internal */
17
56
  export declare function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & {
18
57
  children: ReactNode;
19
58
  }): import("react").JSX.Element;
@@ -2,25 +2,44 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import * as THREE from "three";
5
+ import { createWeatherShelterUniforms, updateWeatherShelters } from "./weatherShelter.js";
5
6
  const WeatherUniformContext = createContext(null);
6
7
  /** @internal */
7
8
  export function createWeatherUniformSet(options = {}) {
8
9
  const wind = options.wind ?? [0, 0, 0];
9
10
  return {
10
- time: { value: 0 },
11
+ ...createWeatherShelterUniforms(),
12
+ time: { value: typeof options.timeSeconds === "function" ? options.timeSeconds() : options.timeSeconds ?? 0 },
11
13
  wind: { value: new THREE.Vector3(wind[0], wind[1], wind[2]) },
12
14
  lightning: { value: options.lightning ?? 0 },
15
+ rain: { value: options.sample === undefined ? 1 : 0 },
16
+ snow: { value: options.sample === undefined ? 1 : 0 },
17
+ dust: { value: options.sample === undefined ? 1 : 0 },
18
+ controlsWind: options.wind !== undefined || options.sample !== undefined,
19
+ metrics: options.metrics,
13
20
  };
14
21
  }
22
+ /** @internal */
23
+ export function updateWeatherUniformSet(uniforms, options, delta, x = 0, z = 0, y = 0) {
24
+ const sample = options.sample?.(x, z);
25
+ const explicitTime = sample?.timeSeconds ?? (typeof options.timeSeconds === "function" ? options.timeSeconds() : options.timeSeconds);
26
+ uniforms.time.value = explicitTime ?? uniforms.time.value + delta * (options.timeScale ?? 1);
27
+ const wind = sample === undefined ? options.wind ?? [0, 0, 0] : [sample.wind[0], 0, sample.wind[1]];
28
+ uniforms.wind.value.set(wind[0], wind[1], wind[2]);
29
+ uniforms.lightning.value = sample?.lightning ?? options.lightning ?? 0;
30
+ uniforms.rain.value = sample?.rain ?? 1;
31
+ uniforms.snow.value = sample?.snow ?? 1;
32
+ uniforms.dust.value = sample?.dust ?? 1;
33
+ uniforms.controlsWind = options.wind !== undefined || options.sample !== undefined;
34
+ uniforms.metrics = options.metrics;
35
+ updateWeatherShelters(uniforms, options.shelters ?? [], x, y, z);
36
+ }
15
37
  function useUniformTicker(uniforms, options, enabled = true) {
16
- useFrame((_state, delta) => {
38
+ useFrame((state, delta) => {
17
39
  if (!enabled)
18
40
  return;
19
- const wind = options.wind ?? [0, 0, 0];
20
- uniforms.time.value += delta * (options.timeScale ?? 1);
21
- uniforms.wind.value.set(wind[0], wind[1], wind[2]);
22
- uniforms.lightning.value = options.lightning ?? 0;
23
- });
41
+ updateWeatherUniformSet(uniforms, options, delta, state.camera.position.x, state.camera.position.z, state.camera.position.y);
42
+ }, -1);
24
43
  }
25
44
  /** @internal */
26
45
  export function WeatherUniformProvider({ children, ...options }) {
@@ -32,6 +51,7 @@ export function WeatherUniformProvider({ children, ...options }) {
32
51
  export function useWeatherUniformSet(options = {}) {
33
52
  const shared = useContext(WeatherUniformContext);
34
53
  const local = useMemo(() => createWeatherUniformSet(options), []);
35
- useUniformTicker(local, options, shared === null);
36
- return shared ?? local;
54
+ useUniformTicker(local, options, shared === null || (!shared.controlsWind && options.wind !== undefined));
55
+ const inherited = useMemo(() => shared === null ? local : { ...shared, wind: local.wind }, [shared, local]);
56
+ return shared === null ? local : !shared.controlsWind && options.wind !== undefined ? inherited : shared;
37
57
  }
@@ -0,0 +1,22 @@
1
+ import { type ReactElement } from "react";
2
+ import * as THREE from "three";
3
+ import type { FlockStepAgent } from "@jgengine/core/ai/flock";
4
+ import type { ModelConfig } from "@jgengine/core/game/playableGame";
5
+ import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
6
+ /** Caller-owned species models for authored habitat agents. */
7
+ export interface AuthoredFlocksProps {
8
+ models: Record<string, string | ModelConfig>;
9
+ assets?: AssetCatalog;
10
+ /** Model nose axis. Default -Z. */
11
+ forward?: readonly [number, number, number];
12
+ maxInstances?: number;
13
+ }
14
+ /** @internal Orient habitat agents from their full authoritative velocity. */
15
+ export declare function applyFlockPose(group: THREE.Group, agent: FlockStepAgent, forward: THREE.Vector3, direction: THREE.Vector3): void;
16
+ /**
17
+ * Render bounded authored habitat agents with game-selected species models. The
18
+ * environment owns movement and lifetime; existing model owners load, animate,
19
+ * and dispose the native GLB. Unmapped species have no substitute geometry.
20
+ * @capability authored-flocks render authored habitat agents through caller-selected animated species models with velocity orientation and bounded model lifetime
21
+ */
22
+ export declare function AuthoredFlocks({ models, assets, forward, maxInstances }: AuthoredFlocksProps): ReactElement;
@@ -0,0 +1,87 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useMemo, useRef, useState } from "react";
4
+ import * as THREE from "three";
5
+ import { useGameContext } from "@jgengine/react/provider";
6
+ import { IsolatedEntityModel } from "../render/SceneModels.js";
7
+ import { resolveModel } from "../render/resolveModel.js";
8
+ /** @internal Orient habitat agents from their full authoritative velocity. */
9
+ export function applyFlockPose(group, agent, forward, direction) {
10
+ group.position.set(...agent.position);
11
+ direction.set(...agent.velocity);
12
+ if (direction.lengthSq() > 1e-12)
13
+ group.quaternion.setFromUnitVectors(forward, direction.normalize());
14
+ }
15
+ /**
16
+ * Render bounded authored habitat agents with game-selected species models. The
17
+ * environment owns movement and lifetime; existing model owners load, animate,
18
+ * and dispose the native GLB. Unmapped species have no substitute geometry.
19
+ * @capability authored-flocks render authored habitat agents through caller-selected animated species models with velocity orientation and bounded model lifetime
20
+ */
21
+ export function AuthoredFlocks({ models, assets, forward = [0, 0, -1], maxInstances = 256 }) {
22
+ const ctx = useGameContext();
23
+ const speciesModels = useMemo(() => {
24
+ const resolved = new Map();
25
+ for (const [species, value] of Object.entries(models)) {
26
+ if (typeof value === "string" && assets === undefined)
27
+ throw new Error(`[jgengine] flock species ${species} requires an asset catalog to resolve ${value}`);
28
+ const model = typeof value === "string" ? resolveModel(value, assets) : value;
29
+ resolved.set(species, model.animation === undefined ? { ...model, animation: "auto" } : model);
30
+ }
31
+ return resolved;
32
+ }, [models, assets]);
33
+ const entries = useRef(new Map());
34
+ const seen = useRef(new Set());
35
+ const direction = useRef(new THREE.Vector3());
36
+ const nose = useRef(new THREE.Vector3());
37
+ const [, setGeneration] = useState(0);
38
+ nose.current.set(...forward).normalize();
39
+ if (nose.current.lengthSq() === 0)
40
+ nose.current.set(0, 0, -1);
41
+ const limit = Number.isFinite(maxInstances) ? Math.max(0, Math.min(1024, Math.floor(maxInstances))) : 256;
42
+ useFrame(() => {
43
+ let changed = false;
44
+ let count = 0;
45
+ seen.current.clear();
46
+ for (const flock of ctx.environment.flocks()) {
47
+ if (flock.role !== "cosmetic")
48
+ continue;
49
+ const model = speciesModels.get(flock.species);
50
+ if (model === undefined)
51
+ continue;
52
+ for (let i = 0; i < flock.agents.length && count < limit; i++, count++) {
53
+ const key = `${flock.id}:${i}`;
54
+ const agent = flock.agents[i];
55
+ seen.current.add(key);
56
+ const current = entries.current.get(key);
57
+ if (current === undefined) {
58
+ entries.current.set(key, { agent, model, group: null });
59
+ changed = true;
60
+ }
61
+ else {
62
+ current.agent = agent;
63
+ if (current.model !== model) {
64
+ current.model = model;
65
+ changed = true;
66
+ }
67
+ if (current.group !== null)
68
+ applyFlockPose(current.group, agent, nose.current, direction.current);
69
+ }
70
+ }
71
+ if (count >= limit)
72
+ break;
73
+ }
74
+ for (const key of entries.current.keys())
75
+ if (!seen.current.has(key)) {
76
+ entries.current.delete(key);
77
+ changed = true;
78
+ }
79
+ if (changed)
80
+ setGeneration((n) => n + 1);
81
+ });
82
+ return _jsx(_Fragment, { children: Array.from(entries.current, ([key, entry]) => _jsx("group", { ref: (group) => {
83
+ entry.group = group;
84
+ if (group !== null)
85
+ applyFlockPose(group, entry.agent, nose.current, direction.current);
86
+ }, children: _jsx(IsolatedEntityModel, { model: entry.model }) }, key)) });
87
+ }
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from "react";
2
+ import type { SceneObject } from "@jgengine/core/scene/objectStore";
3
+ import { type StaticObjectBatch } from "./staticObjectBatches.js";
4
+ /** Render compatible authored props as shared model draws, preserving unsupported models. @internal */
5
+ export declare function StaticObjectModels({ batch, single }: {
6
+ batch: StaticObjectBatch;
7
+ single: (object: SceneObject) => ReactNode;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame, useLoader } from "@react-three/fiber";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import * as THREE from "three";
5
+ import { useGameContext } from "@jgengine/react/provider";
6
+ import { sharedGltfLoader } from "../render/modelLoad.js";
7
+ import { IsolatedModelPart } from "../render/SceneModels.js";
8
+ import { measureLocalBounds, reportMeasuredBounds } from "../render/measureBounds.js";
9
+ import { measureLocalCollisionTriangles, reportMeasuredCollisionMesh } from "../render/measureCollisionMesh.js";
10
+ import { buildScatterModelSources, disposeScatterModelSources } from "../scatter/scatterModels.js";
11
+ import { useRenderVisibility } from "../visibility/CullingProvider.js";
12
+ import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
13
+ import { canInstanceStaticScene, incompatibleStaticObjectIds, isCompatibleStaticObject, syncStaticObjectSource } from "./staticObjectBatches.js";
14
+ function ObjectSource({ source, objects, batch }) {
15
+ const ctx = useGameContext();
16
+ const visibility = useRenderVisibility();
17
+ const ref = useRef(null);
18
+ const scratch = useMemo(() => new THREE.Object3D(), []);
19
+ useEffect(() => {
20
+ const mesh = ref.current;
21
+ return () => { mesh?.dispose(); };
22
+ }, [objects.length, source]);
23
+ useFrame(() => {
24
+ if (ref.current !== null)
25
+ syncStaticObjectSource(ref.current, source, objects, (id) => {
26
+ const object = ctx.scene.object.get(id);
27
+ return object !== null && object.catalogId === batch.objects[0].catalogId &&
28
+ isCompatibleStaticObject(object, batch.model, ctx.scene.object.catalog(id)) ? object : null;
29
+ }, visibility.current, scratch);
30
+ }, CAMERA_POST_FRAME_PRIORITY + 0.1);
31
+ return _jsx("instancedMesh", { ref: ref, args: [source.geometry, source.material, objects.length], castShadow: source.castShadow, receiveShadow: source.receiveShadow, dispose: null });
32
+ }
33
+ function LoadedStaticObjects({ batch, single }) {
34
+ const ctx = useGameContext();
35
+ const [excluded, setExcluded] = useState([]);
36
+ const fallbackObjects = useRef(new Map());
37
+ useFrame(() => {
38
+ const next = incompatibleStaticObjectIds(batch, ctx.scene.object.get, ctx.scene.object.catalog, excluded, fallbackObjects.current);
39
+ if (next !== excluded)
40
+ setExcluded(next);
41
+ });
42
+ const objects = useMemo(() => batch.objects.filter((object) => !excluded.includes(object.instanceId)), [batch, excluded]);
43
+ const gltf = useLoader(sharedGltfLoader, batch.model.url);
44
+ const supported = useMemo(() => canInstanceStaticScene(gltf.scene, gltf.animations), [gltf]);
45
+ const harvested = useMemo(() => supported ? buildScatterModelSources(gltf.scene, batch.model) : null, [supported, gltf, batch.model]);
46
+ useEffect(() => () => { if (harvested !== null)
47
+ disposeScatterModelSources(harvested.root); }, [harvested]);
48
+ useEffect(() => {
49
+ if (harvested === null)
50
+ return;
51
+ const root = new THREE.Group();
52
+ for (const source of harvested.sources) {
53
+ const mesh = new THREE.Mesh(source.geometry, source.material);
54
+ mesh.matrixAutoUpdate = false;
55
+ mesh.matrix.copy(source.localMatrix);
56
+ root.add(mesh);
57
+ }
58
+ const key = batch.objects[0].catalogId;
59
+ if (batch.model.dims?.maxY === undefined) {
60
+ const bounds = measureLocalBounds(root);
61
+ if (bounds !== null)
62
+ reportMeasuredBounds(ctx, "object", key, bounds);
63
+ }
64
+ const triangles = measureLocalCollisionTriangles(root);
65
+ if (triangles !== null)
66
+ reportMeasuredCollisionMesh(ctx, "object", key, triangles);
67
+ }, [ctx, harvested, batch]);
68
+ if (harvested === null)
69
+ return _jsx(_Fragment, { children: batch.objects.map(single) });
70
+ return _jsxs(_Fragment, { children: [harvested.sources.map((source, index) => objects.length === 0 ? null : _jsx(ObjectSource, { source: source, objects: objects, batch: batch }, index)), excluded.map((id) => { const live = ctx.scene.object.get(id); return live === null ? null : single(live); })] });
71
+ }
72
+ /** Render compatible authored props as shared model draws, preserving unsupported models. @internal */
73
+ export function StaticObjectModels({ batch, single }) {
74
+ return _jsx(IsolatedModelPart, { model: batch.model, seam: "object", children: _jsx(LoadedStaticObjects, { batch: batch, single: single }) });
75
+ }