@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,20 +1,25 @@
1
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 { useTexture } from "@react-three/drei";
4
- import { Component, Suspense, useEffect, useMemo, useRef } from "react";
2
+ import { useFrame, useLoader, useThree } from "@react-three/fiber";
3
+ import { Component, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
5
4
  import * as THREE from "three";
5
+ import { parseModelHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility";
6
6
  import { createSpriteClipPlayer } from "@jgengine/core/render/sprite2d";
7
7
  import { reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams";
8
8
  import { useOptionalGameContext } from "@jgengine/react/provider";
9
- import { sharedGltfLoader } from "./modelLoad.js";
9
+ import { detectKtx2Support, sharedGltfLoader } from "./modelLoad.js";
10
+ import { modelAssetRequests } from "./modelAssets.js";
10
11
  import { measureLocalBounds, reportMeasuredBounds } from "./measureBounds.js";
11
12
  import { measureLocalCollisionTriangles, reportMeasuredCollisionMesh } from "./measureCollisionMesh.js";
12
- import { useModelAnimation } from "./useModelAnimation.js";
13
+ import { useModelInstance } from "./useModelInstance.js";
14
+ import { applyMaterialAssignments, useModelMaterialTextures, MaterialTextureLoader, validateMaterialAssignmentTargets } from "./materialAsset.js";
13
15
  import { useFootIk } from "./useFootIk.js";
16
+ import { resolveRigNode } from "./rigNode.js";
14
17
  import { PartMotionRig } from "./PartMotion.js";
18
+ import { syncSpriteFrame } from "./spriteRender.js";
15
19
  import { applyMaterialOverride } from "../materialOverride.js";
16
- import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
20
+ import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeModelScene, ownModelMaterial, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
17
21
  export function EntitySprite({ sprite }) {
22
+ const invalidate = useThree((state) => state.invalidate);
18
23
  const texture = useLoader(THREE.TextureLoader, sprite.clip?.atlas.image ?? sprite.url);
19
24
  const map = useMemo(() => texture.clone(), [texture]);
20
25
  const player = useMemo(() => (sprite.clip === undefined ? null : createSpriteClipPlayer(sprite.clip.atlas)), [sprite.clip]);
@@ -28,7 +33,11 @@ export function EntitySprite({ sprite }) {
28
33
  useEffect(() => {
29
34
  if (player !== null && animation !== undefined)
30
35
  player.play(animation);
31
- }, [player, animation]);
36
+ const frame = player?.frame();
37
+ if (frame !== undefined && sprite.clip !== undefined)
38
+ syncSpriteFrame(map, frame, sprite.clip.atlas.size);
39
+ invalidate();
40
+ }, [player, animation, map, sprite.clip, invalidate]);
32
41
  useFrame((_state, delta) => {
33
42
  if (player === null || sprite.clip === undefined)
34
43
  return;
@@ -36,10 +45,7 @@ export function EntitySprite({ sprite }) {
36
45
  const frame = player.frame();
37
46
  if (frame === undefined)
38
47
  return;
39
- const [atlasWidth, atlasHeight] = sprite.clip.atlas.size;
40
- map.repeat.set(frame.w / atlasWidth, frame.h / atlasHeight);
41
- map.offset.set(frame.x / atlasWidth, 1 - (frame.y + frame.h) / atlasHeight);
42
- map.needsUpdate = true;
48
+ syncSpriteFrame(map, frame, sprite.clip.atlas.size);
43
49
  });
44
50
  useEffect(() => {
45
51
  if (player === null) {
@@ -97,13 +103,14 @@ function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
97
103
  const rz = rotation?.[2] ?? 0;
98
104
  const s = scale ?? 1;
99
105
  useEffect(() => {
100
- const bone = rig.getObjectByName(slot);
101
- if (bone === undefined) {
106
+ const resolved = resolveRigNode(rig, slot);
107
+ if (resolved.node === undefined) {
102
108
  if (typeof console !== "undefined") {
103
- console.warn(`[jgengine] entityModels attachment: bone/slot "${slot}" not found on the rig`);
109
+ console.warn(`[jgengine] entityModels attachment: ${resolved.diagnostic.message}`);
104
110
  }
105
111
  return;
106
112
  }
113
+ const bone = resolved.node;
107
114
  weaponScene.position.set(px, py, pz);
108
115
  weaponScene.rotation.set(rx, ry, rz);
109
116
  weaponScene.scale.setScalar(s);
@@ -112,80 +119,47 @@ function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
112
119
  bone.remove(weaponScene);
113
120
  };
114
121
  }, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
115
- useEffect(() => () => disposeClonedMaterials(weaponScene), [weaponScene]);
122
+ useEffect(() => () => disposeModelScene(weaponScene), [weaponScene]);
116
123
  return null;
117
124
  }
118
125
  /** Renders a static kit-of-parts child at a fixed local offset under the parent model — no bone/rig lookup, unlike `BoneAttachment`. The nested `EntityModel` still applies its own `dims`/anchor centering, so scale/pivot sanity carries over per part. */
119
126
  function ModelPartGroup({ model, position, rotation, scale, }) {
120
127
  return (_jsx("group", { position: position ?? [0, 0, 0], rotation: rotation ?? [0, 0, 0], scale: scale ?? 1, children: _jsx(IsolatedModelPart, { model: model, children: _jsx(EntityModel, { model: model }) }) }));
121
128
  }
122
- function ModelMaterialMapsApplier({ scene, maps }) {
123
- const entries = useMemo(() => {
124
- const record = {};
125
- if (maps.color !== undefined)
126
- record.color = maps.color;
127
- if (maps.normal !== undefined)
128
- record.normal = maps.normal;
129
- if (maps.roughness !== undefined)
130
- record.roughness = maps.roughness;
131
- if (maps.ao !== undefined)
132
- record.ao = maps.ao;
133
- if (maps.metalness !== undefined)
134
- record.metalness = maps.metalness;
135
- if (maps.emissive !== undefined)
136
- record.emissive = maps.emissive;
137
- if (maps.height !== undefined)
138
- record.height = maps.height;
139
- return record;
140
- }, [maps.color, maps.normal, maps.roughness, maps.ao, maps.metalness, maps.emissive, maps.height]);
141
- const textures = useTexture(entries);
142
- useEffect(() => {
143
- if (textures.color !== undefined)
144
- textures.color.colorSpace = THREE.SRGBColorSpace;
145
- applyMaterialOverride(scene, {}, { clone: false, textures });
146
- }, [scene, textures]);
147
- return null;
148
- }
149
129
  export function EntityModel({ model, instanceId, measure, }) {
150
- const gltf = useLoader(sharedGltfLoader, model.url);
130
+ parseModelHiddenNodes(model.hiddenNodes);
131
+ const renderer = useThree(state => state.gl);
132
+ if (renderer.isWebGLRenderer)
133
+ detectKtx2Support(renderer);
134
+ const assets = modelAssetRequests(model);
135
+ for (const url of assets.models)
136
+ useLoader.preload(sharedGltfLoader, url);
151
137
  // Optional, not required: measured bounds and paint strokes are live-world extras, and a model
152
138
  // that threw without a running game could not be inspected outside one — which is how a broken
153
139
  // composition stayed undiagnosable in `EntityPreview` (#1588).
154
140
  const ctx = useOptionalGameContext();
155
141
  const material = model.material;
142
+ const materialAssetKey = JSON.stringify((model.materialAssets ?? []).filter(asset => model.materialAssignments?.some(assignment => assignment.materialId === asset.id)));
143
+ const materialAssets = useMemo(() => (model.materialAssets ?? []).filter(asset => model.materialAssignments?.some(assignment => assignment.materialId === asset.id)), [materialAssetKey]);
144
+ const materialAssignments = model.materialAssignments;
145
+ const assignmentSource = useLoader(sharedGltfLoader, materialAssignments?.length ? [model.url] : [])[0]?.scene;
146
+ const assignmentKey = JSON.stringify(materialAssignments);
147
+ useMemo(() => {
148
+ if (assignmentSource !== undefined)
149
+ validateMaterialAssignmentTargets(assignmentSource, materialAssets, materialAssignments, { baseAlphaMode: material?.alphaMode });
150
+ }, [assignmentSource, materialAssets, assignmentKey, material?.alphaMode]);
151
+ for (const urls of assets.textureGroups)
152
+ useLoader.preload(MaterialTextureLoader, urls);
153
+ const materialTextures = useModelMaterialTextures(model);
156
154
  const baseY = model.y ?? 0;
157
155
  const dims = model.dims;
158
- const shadows = model.shadows;
159
- const scene = useMemo(() => {
160
- const cloned = cloneModelScene(gltf.scene, { shadows });
156
+ const configure = useCallback((content) => {
161
157
  if (material !== undefined)
162
- applyMaterialOverride(cloned, material, { clone: false });
163
- return cloned;
164
- }, [gltf, material, shadows]);
165
- const measured = useMemo(() => {
166
- if (model.targetHeight === undefined)
167
- return null;
168
- const box = new THREE.Box3().setFromObject(scene);
169
- const height = box.max.y - box.min.y;
170
- if (!Number.isFinite(height) || height <= 0)
171
- return null;
172
- return {
173
- normalize: model.targetHeight / height,
174
- minY: box.min.y,
175
- centerX: (box.min.x + box.max.x) / 2,
176
- centerZ: (box.min.z + box.max.z) / 2,
177
- };
178
- }, [scene, model.targetHeight]);
179
- const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
180
- const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
181
- const position = measured !== null
182
- ? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
183
- : centered
184
- ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
185
- : [0, baseY, 0];
186
- useEffect(() => () => {
187
- disposeClonedMaterials(scene);
188
- }, [scene]);
158
+ applyMaterialOverride(content, material, { clone: false, textures: materialTextures.global, ownMaterial: replacement => ownModelMaterial(content, replacement) });
159
+ if (materialAssignments !== undefined)
160
+ applyMaterialAssignments(content, materialAssets ?? [], materialAssignments, materialTextures.assets);
161
+ }, [material, materialTextures, materialAssets, materialAssignments]);
162
+ const { scene, scale, position } = useModelInstance(model, { instanceId, configure });
189
163
  // A model without index-measured dims can't drive the fitted-collider path, so report the live
190
164
  // measurement (the primitive mounts with only this uniform scale + position) instead of letting
191
165
  // the kind fall back to a fixed-size box. Index dims keep priority — skip when they exist.
@@ -193,7 +167,8 @@ export function EntityModel({ model, instanceId, measure, }) {
193
167
  const measureKey = measure?.key;
194
168
  const dimsMaxY = dims?.maxY;
195
169
  const [positionX, positionY, positionZ] = position;
196
- useEffect(() => {
170
+ // Measure before the instance hook's passive animation effects can move imported root nodes.
171
+ useLayoutEffect(() => {
197
172
  if (ctx === null || measureTarget === undefined || measureKey === undefined || dimsMaxY !== undefined)
198
173
  return;
199
174
  const raw = measureLocalBounds(scene);
@@ -207,7 +182,7 @@ export function EntityModel({ model, instanceId, measure, }) {
207
182
  }, [ctx, scene, scale, positionX, positionY, positionZ, measureTarget, measureKey, dimsMaxY]);
208
183
  // Runs with or without index dims: the loaded geometry upgrades whatever box the kind resolved
209
184
  // (fitted or measured) to a hitbox that raycasts the model's own triangles.
210
- useEffect(() => {
185
+ useLayoutEffect(() => {
211
186
  if (ctx === null || measureTarget === undefined || measureKey === undefined)
212
187
  return;
213
188
  const triangles = measureLocalCollisionTriangles(scene, {
@@ -217,8 +192,7 @@ export function EntityModel({ model, instanceId, measure, }) {
217
192
  if (triangles !== null)
218
193
  reportMeasuredCollisionMesh(ctx, measureTarget, measureKey, triangles);
219
194
  }, [ctx, scene, scale, positionX, positionY, positionZ, measureTarget, measureKey]);
220
- useModelAnimation(scene, gltf.animations, model.animation, instanceId);
221
- useFootIk(scene, model.ik, ctx, instanceId);
195
+ useFootIk(scene, model.ik, ctx, instanceId, baseY);
222
196
  const paintCanvasRef = useRef(null);
223
197
  const paintDrawnCountRef = useRef(0);
224
198
  const paintVersionRef = useRef(-1);
@@ -255,7 +229,7 @@ export function EntityModel({ model, instanceId, measure, }) {
255
229
  }
256
230
  paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
257
231
  });
258
- const base = (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), material?.maps !== undefined ? (_jsx(IsolatedModelPart, { model: model, children: _jsx(ModelMaterialMapsApplier, { scene: scene, maps: material.maps }) })) : null, (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(IsolatedModelPart, { model: attachment.model, children: _jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }) }, `${attachment.slot}-${index}`)))] }));
232
+ const base = (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(IsolatedModelPart, { model: attachment.model, children: _jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }) }, `${attachment.slot}-${index}`)))] }));
259
233
  const parts = model.parts ?? [];
260
234
  // Any role-tagged part switches the composition onto the procedural part-motion rig:
261
235
  // root bob/flinch/topple plus per-role limb swing, driven from the entity's live state.
@@ -0,0 +1,91 @@
1
+ import * as THREE from "three";
2
+ import { type BoneTextureLayout } from "@jgengine/core/anim/boneTexture";
3
+ /** A rig to bake: a loaded scene with skinned meshes and its clips. */
4
+ export interface SkinnedCrowdSource {
5
+ scene: THREE.Object3D;
6
+ animations: readonly THREE.AnimationClip[];
7
+ }
8
+ /** Options for {@link bakeSkinnedCrowd}. */
9
+ export interface BakeSkinnedCrowdOptions {
10
+ /** Clip names to bake, in order; default every clip on the rig. */
11
+ clips?: readonly string[];
12
+ /** Baked samples per second. Default 30. */
13
+ fps?: number;
14
+ /** Clips that hold their last pose instead of looping. */
15
+ once?: readonly string[];
16
+ maxTextureSize?: number;
17
+ }
18
+ /** A rig baked for instancing: one merged geometry, the bone-matrix texture and its layout. */
19
+ export interface BakedSkinnedCrowd {
20
+ geometry: THREE.BufferGeometry;
21
+ /** One material per merged mesh, in geometry-group order, not yet patched. */
22
+ materials: THREE.Material[];
23
+ texture: THREE.DataTexture;
24
+ layout: BoneTextureLayout;
25
+ /** Per clip: whether it loops. Same order as `layout.clips`. */
26
+ loops: boolean[];
27
+ clipIndex: ReadonlyMap<string, number>;
28
+ dispose(): void;
29
+ }
30
+ /**
31
+ * Bakes a rig for {@link SkinnedInstances}: merges its skinned meshes into one geometry in bind
32
+ * space, samples every requested clip at `fps`, and writes each bone's model-space skinning matrix
33
+ * into a float texture laid out by `planBoneTexture`. Meshes with several materials keep only the
34
+ * first. Exported for tests and custom crowd renderers.
35
+ */
36
+ export declare function bakeSkinnedCrowd(source: SkinnedCrowdSource, options?: BakeSkinnedCrowdOptions): BakedSkinnedCrowd;
37
+ /** Uniforms shared by every material of one crowd; `crowdTime` advances once per frame. */
38
+ export interface CrowdUniforms {
39
+ crowdBones: {
40
+ value: THREE.DataTexture;
41
+ };
42
+ crowdTime: {
43
+ value: number;
44
+ };
45
+ crowdClips: {
46
+ value: THREE.Vector4[];
47
+ };
48
+ }
49
+ /** Builds the uniforms a crowd material reads: the texture and per-clip start, frames, fps and loop. */
50
+ export declare function createCrowdUniforms(baked: BakedSkinnedCrowd): CrowdUniforms;
51
+ /**
52
+ * Patches a material so each instance skins itself from the crowd texture: per-instance `crowdPlay`
53
+ * is (clip index, time offset, speed). Works on any built-in material, the depth material for
54
+ * shadows included.
55
+ */
56
+ export declare function patchCrowdMaterial<T extends THREE.Material>(material: T, uniforms: CrowdUniforms): T;
57
+ /** One crowd member: where it stands and what it plays. */
58
+ export interface CrowdInstance {
59
+ position: readonly [number, number, number];
60
+ rotationY?: number;
61
+ scale?: number;
62
+ /** A baked clip name. */
63
+ clip: string;
64
+ /** Seconds into the clip at crowd time zero, so members do not move in lockstep. */
65
+ timeOffset?: number;
66
+ /** Playback rate. Default 1. */
67
+ speed?: number;
68
+ }
69
+ /** Props for {@link SkinnedInstances}. */
70
+ export interface SkinnedInstancesProps {
71
+ url: string;
72
+ instances: readonly CrowdInstance[];
73
+ /** Clip names to bake; default every clip on the rig. */
74
+ clips?: readonly string[];
75
+ /** Clips that hold their last pose instead of looping. */
76
+ once?: readonly string[];
77
+ fps?: number;
78
+ /** Uniform scale applied to every member, e.g. to normalize a rig to a height. */
79
+ modelScale?: number;
80
+ castShadow?: boolean;
81
+ receiveShadow?: boolean;
82
+ /** Multiplies crowd time; `0` freezes every member. Default 1. */
83
+ timeScale?: number;
84
+ }
85
+ /**
86
+ * Renders many animated copies of one rigged model in a single instanced draw per material: the
87
+ * rig's clips are baked to a bone-matrix texture once, and each instance samples its own clip, time
88
+ * offset and speed on the GPU. For crowds, spectators and distant NPCs; a character that needs
89
+ * blending, IK or attachments stays an entity model.
90
+ */
91
+ export declare function SkinnedInstances({ url, instances, clips, once, fps, modelScale, castShadow, receiveShadow, timeScale, }: SkinnedInstancesProps): import("react").JSX.Element;
@@ -0,0 +1,249 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useFrame, useLoader } from "@react-three/fiber";
3
+ import { useEffect, useLayoutEffect, useMemo } from "react";
4
+ import * as THREE from "three";
5
+ import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
6
+ import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
7
+ import { planBoneTexture } from "@jgengine/core/anim/boneTexture";
8
+ import { sharedGltfLoader } from "./modelLoad.js";
9
+ function slotFor(slots, bone, inverse) {
10
+ const found = slots.findIndex((slot) => slot.bone === bone && slot.inverse.equals(inverse));
11
+ if (found >= 0)
12
+ return found;
13
+ slots.push({ bone, inverse: inverse.clone() });
14
+ return slots.length - 1;
15
+ }
16
+ function firstMaterial(material) {
17
+ return Array.isArray(material) ? material[0] : material;
18
+ }
19
+ /**
20
+ * Bakes a rig for {@link SkinnedInstances}: merges its skinned meshes into one geometry in bind
21
+ * space, samples every requested clip at `fps`, and writes each bone's model-space skinning matrix
22
+ * into a float texture laid out by `planBoneTexture`. Meshes with several materials keep only the
23
+ * first. Exported for tests and custom crowd renderers.
24
+ */
25
+ export function bakeSkinnedCrowd(source, options = {}) {
26
+ const root = cloneSkinned(source.scene);
27
+ root.position.set(0, 0, 0);
28
+ root.quaternion.identity();
29
+ root.scale.set(1, 1, 1);
30
+ root.updateMatrixWorld(true);
31
+ const meshes = [];
32
+ root.traverse((object) => {
33
+ if (object.isSkinnedMesh === true)
34
+ meshes.push(object);
35
+ });
36
+ if (meshes.length === 0)
37
+ throw new Error("bakeSkinnedCrowd: the source has no skinned mesh");
38
+ const slots = [];
39
+ const geometries = [];
40
+ const materials = [];
41
+ for (const mesh of meshes) {
42
+ const source = mesh.geometry;
43
+ const geometry = new THREE.BufferGeometry();
44
+ geometry.setAttribute("position", source.getAttribute("position").clone());
45
+ if (source.getAttribute("normal") !== undefined)
46
+ geometry.setAttribute("normal", source.getAttribute("normal").clone());
47
+ else
48
+ geometry.setAttribute("normal", new THREE.Float32BufferAttribute(new Float32Array(source.getAttribute("position").count * 3), 3));
49
+ const uv = source.getAttribute("uv");
50
+ geometry.setAttribute("uv", uv !== undefined ? uv.clone() : new THREE.Float32BufferAttribute(new Float32Array(source.getAttribute("position").count * 2), 2));
51
+ if (source.index !== null)
52
+ geometry.setIndex(source.index.clone());
53
+ geometry.applyMatrix4(mesh.bindMatrix);
54
+ const skinIndex = source.getAttribute("skinIndex");
55
+ const skinWeight = source.getAttribute("skinWeight");
56
+ const remapped = new Float32Array(skinIndex.count * 4);
57
+ const weights = new Float32Array(skinWeight.count * 4);
58
+ const bySkeletonIndex = mesh.skeleton.bones.map((bone, i) => slotFor(slots, bone, mesh.skeleton.boneInverses[i]));
59
+ for (let v = 0; v < skinIndex.count; v += 1) {
60
+ for (let c = 0; c < 4; c += 1) {
61
+ remapped[v * 4 + c] = bySkeletonIndex[skinIndex.getComponent(v, c)] ?? 0;
62
+ weights[v * 4 + c] = skinWeight.getComponent(v, c);
63
+ }
64
+ }
65
+ geometry.setAttribute("skinIndex", new THREE.Float32BufferAttribute(remapped, 4));
66
+ geometry.setAttribute("skinWeight", new THREE.Float32BufferAttribute(weights, 4));
67
+ geometries.push(geometry);
68
+ materials.push(firstMaterial(mesh.material));
69
+ }
70
+ const geometry = mergeGeometries(geometries, true);
71
+ for (const part of geometries)
72
+ part.dispose();
73
+ if (geometry === null)
74
+ throw new Error("bakeSkinnedCrowd: could not merge the rig's meshes");
75
+ geometry.computeBoundingBox();
76
+ geometry.computeBoundingSphere();
77
+ const names = options.clips ?? source.animations.map((clip) => clip.name);
78
+ const clips = names.map((name) => {
79
+ const clip = THREE.AnimationClip.findByName(source.animations, name);
80
+ if (clip === null)
81
+ throw new Error(`bakeSkinnedCrowd: no clip named "${name}"`);
82
+ return clip;
83
+ });
84
+ const layout = planBoneTexture({
85
+ boneCount: slots.length,
86
+ clips: clips.map((clip) => ({ name: clip.name, duration: clip.duration })),
87
+ fps: options.fps,
88
+ maxTextureSize: options.maxTextureSize,
89
+ });
90
+ const data = new Float32Array(layout.width * layout.height * 4);
91
+ const mixer = new THREE.AnimationMixer(root);
92
+ const rootInverse = root.matrixWorld.clone().invert();
93
+ const matrix = new THREE.Matrix4();
94
+ clips.forEach((clip, clipIndex) => {
95
+ const baked = layout.clips[clipIndex];
96
+ const action = mixer.clipAction(clip);
97
+ action.play();
98
+ for (let frame = 0; frame < baked.frameCount; frame += 1) {
99
+ action.time = Math.min(frame / layout.fps, clip.duration);
100
+ mixer.update(0);
101
+ root.updateMatrixWorld(true);
102
+ const rowOffset = (baked.startFrame + frame) * layout.width * 4;
103
+ slots.forEach((slot, s) => {
104
+ matrix.multiplyMatrices(rootInverse, slot.bone.matrixWorld).multiply(slot.inverse);
105
+ data.set(matrix.elements, rowOffset + s * 16);
106
+ });
107
+ }
108
+ action.stop();
109
+ });
110
+ mixer.uncacheRoot(root);
111
+ const texture = new THREE.DataTexture(data, layout.width, layout.height, THREE.RGBAFormat, THREE.FloatType);
112
+ texture.minFilter = THREE.NearestFilter;
113
+ texture.magFilter = THREE.NearestFilter;
114
+ texture.generateMipmaps = false;
115
+ texture.needsUpdate = true;
116
+ const once = new Set(options.once ?? []);
117
+ return {
118
+ geometry,
119
+ materials,
120
+ texture,
121
+ layout,
122
+ loops: layout.clips.map((clip) => !once.has(clip.name)),
123
+ clipIndex: new Map(layout.clips.map((clip, index) => [clip.name, index])),
124
+ dispose() {
125
+ geometry.dispose();
126
+ texture.dispose();
127
+ },
128
+ };
129
+ }
130
+ /** Builds the uniforms a crowd material reads: the texture and per-clip start, frames, fps and loop. */
131
+ export function createCrowdUniforms(baked) {
132
+ return {
133
+ crowdBones: { value: baked.texture },
134
+ crowdTime: { value: 0 },
135
+ crowdClips: {
136
+ value: baked.layout.clips.map((clip, index) => new THREE.Vector4(clip.startFrame, clip.frameCount, baked.layout.fps, baked.loops[index] ? 1 : 0)),
137
+ },
138
+ };
139
+ }
140
+ const CROWD_HEADER = /* glsl */ `
141
+ uniform highp sampler2D crowdBones;
142
+ uniform float crowdTime;
143
+ uniform vec4 crowdClips[CROWD_CLIP_COUNT];
144
+ attribute vec4 skinIndex;
145
+ attribute vec4 skinWeight;
146
+ attribute vec3 crowdPlay;
147
+ mat4 crowdMatrix;
148
+ bool crowdReady = false;
149
+ mat4 crowdBone(float slot, float row) {
150
+ int x = int(slot + 0.5) * 4;
151
+ int y = int(row + 0.5);
152
+ return mat4(
153
+ texelFetch(crowdBones, ivec2(x, y), 0),
154
+ texelFetch(crowdBones, ivec2(x + 1, y), 0),
155
+ texelFetch(crowdBones, ivec2(x + 2, y), 0),
156
+ texelFetch(crowdBones, ivec2(x + 3, y), 0));
157
+ }
158
+ mat4 crowdSkin() {
159
+ if (crowdReady) return crowdMatrix;
160
+ vec4 clip = crowdClips[int(crowdPlay.x + 0.5)];
161
+ float span = clip.y - 1.0;
162
+ float raw = (crowdTime * crowdPlay.z + crowdPlay.y) * clip.z;
163
+ float frame = clip.w > 0.5 ? mod(raw, span) : clamp(raw, 0.0, span);
164
+ float a = floor(frame);
165
+ float t = frame - a;
166
+ float rowA = clip.x + a;
167
+ float rowB = clip.x + min(a + 1.0, span);
168
+ crowdMatrix = mat4(0.0);
169
+ for (int i = 0; i < 4; i++) {
170
+ float w = skinWeight[i];
171
+ if (w > 0.0) crowdMatrix += w * ((1.0 - t) * crowdBone(skinIndex[i], rowA) + t * crowdBone(skinIndex[i], rowB));
172
+ }
173
+ crowdReady = true;
174
+ return crowdMatrix;
175
+ }
176
+ `;
177
+ /**
178
+ * Patches a material so each instance skins itself from the crowd texture: per-instance `crowdPlay`
179
+ * is (clip index, time offset, speed). Works on any built-in material, the depth material for
180
+ * shadows included.
181
+ */
182
+ export function patchCrowdMaterial(material, uniforms) {
183
+ const clipCount = Math.max(1, uniforms.crowdClips.value.length);
184
+ material.onBeforeCompile = (shader) => {
185
+ Object.assign(shader.uniforms, uniforms);
186
+ shader.vertexShader = shader.vertexShader
187
+ .replace("#include <common>", `#include <common>\n#define CROWD_CLIP_COUNT ${clipCount}\n${CROWD_HEADER}`)
188
+ .replace("#include <beginnormal_vertex>", "#include <beginnormal_vertex>\nobjectNormal = mat3(crowdSkin()) * objectNormal;")
189
+ .replace("#include <begin_vertex>", "#include <begin_vertex>\ntransformed = (crowdSkin() * vec4(transformed, 1.0)).xyz;");
190
+ };
191
+ material.customProgramCacheKey = () => `jg-crowd-${clipCount}`;
192
+ return material;
193
+ }
194
+ const scratchMatrix = new THREE.Matrix4();
195
+ const scratchQuaternion = new THREE.Quaternion();
196
+ const scratchPosition = new THREE.Vector3();
197
+ const scratchScale = new THREE.Vector3();
198
+ /**
199
+ * Renders many animated copies of one rigged model in a single instanced draw per material: the
200
+ * rig's clips are baked to a bone-matrix texture once, and each instance samples its own clip, time
201
+ * offset and speed on the GPU. For crowds, spectators and distant NPCs; a character that needs
202
+ * blending, IK or attachments stays an entity model.
203
+ */
204
+ export function SkinnedInstances({ url, instances, clips, once, fps, modelScale = 1, castShadow = true, receiveShadow = true, timeScale = 1, }) {
205
+ const gltf = useLoader(sharedGltfLoader, url);
206
+ const clipKey = clips?.join("\u0000");
207
+ const onceKey = once?.join("\u0000");
208
+ const baked = useMemo(() => bakeSkinnedCrowd({ scene: gltf.scene, animations: gltf.animations }, { clips, once, fps }), [gltf, clipKey, onceKey, fps]);
209
+ useEffect(() => () => baked.dispose(), [baked]);
210
+ const uniforms = useMemo(() => createCrowdUniforms(baked), [baked]);
211
+ const materials = useMemo(() => baked.materials.map((material) => patchCrowdMaterial(material.clone(), uniforms)), [baked, uniforms]);
212
+ const depthMaterial = useMemo(() => patchCrowdMaterial(new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking }), uniforms), [uniforms]);
213
+ useEffect(() => () => {
214
+ for (const material of materials)
215
+ material.dispose();
216
+ depthMaterial.dispose();
217
+ }, [materials, depthMaterial]);
218
+ const capacity = Math.max(1, instances.length);
219
+ const mesh = useMemo(() => {
220
+ const instanced = new THREE.InstancedMesh(baked.geometry, materials, capacity);
221
+ instanced.customDepthMaterial = depthMaterial;
222
+ instanced.geometry.setAttribute("crowdPlay", new THREE.InstancedBufferAttribute(new Float32Array(capacity * 3), 3));
223
+ return instanced;
224
+ }, [baked, materials, depthMaterial, capacity]);
225
+ useEffect(() => () => {
226
+ mesh.dispose();
227
+ }, [mesh]);
228
+ useLayoutEffect(() => {
229
+ const play = mesh.geometry.getAttribute("crowdPlay");
230
+ instances.forEach((instance, i) => {
231
+ const scale = (instance.scale ?? 1) * modelScale;
232
+ scratchQuaternion.setFromAxisAngle(THREE.Object3D.DEFAULT_UP, instance.rotationY ?? 0);
233
+ scratchMatrix.compose(scratchPosition.set(instance.position[0], instance.position[1], instance.position[2]), scratchQuaternion, scratchScale.set(scale, scale, scale));
234
+ mesh.setMatrixAt(i, scratchMatrix);
235
+ play.setXYZ(i, baked.clipIndex.get(instance.clip) ?? 0, instance.timeOffset ?? 0, instance.speed ?? 1);
236
+ });
237
+ mesh.count = instances.length;
238
+ mesh.instanceMatrix.needsUpdate = true;
239
+ play.needsUpdate = true;
240
+ mesh.computeBoundingSphere();
241
+ // Poses reach past the bind-pose bounds (arms out, a lunge), so pad the cull sphere.
242
+ if (mesh.boundingSphere !== null)
243
+ mesh.boundingSphere.radius += (baked.geometry.boundingSphere?.radius ?? 0) * modelScale;
244
+ }, [mesh, instances, baked, modelScale]);
245
+ useFrame((_three, delta) => {
246
+ uniforms.crowdTime.value += delta * timeScale;
247
+ });
248
+ return _jsx("primitive", { object: mesh, castShadow: castShadow, receiveShadow: receiveShadow });
249
+ }
@@ -0,0 +1,16 @@
1
+ import * as THREE from "three";
2
+ /**
3
+ * Retune already-loaded textures that still carry the old default anisotropy. Textures a
4
+ * system set on purpose (pixel-art sprites pin 1) keep their value.
5
+ * @internal
6
+ */
7
+ export declare function retuneTextureAnisotropy(root: THREE.Object3D, from: number, to: number): number;
8
+ /**
9
+ * Applies the player's texture-filtering choice. Sets three's construction-time default so
10
+ * every texture loaded afterwards (GLTF, texture loaders) samples anisotropically, and walks the
11
+ * scene once per change for textures loaded before. No per-frame work.
12
+ * @internal shell-internal; driven by `GraphicsProfile.anisotropy`.
13
+ */
14
+ export declare function TextureFiltering({ anisotropy }: {
15
+ anisotropy: number;
16
+ }): null;
@@ -0,0 +1,64 @@
1
+ import { useThree } from "@react-three/fiber";
2
+ import { useLayoutEffect } from "react";
3
+ import * as THREE from "three";
4
+ const TEXTURE_SLOTS = [
5
+ "map",
6
+ "normalMap",
7
+ "roughnessMap",
8
+ "metalnessMap",
9
+ "aoMap",
10
+ "emissiveMap",
11
+ "bumpMap",
12
+ "alphaMap",
13
+ "displacementMap",
14
+ ];
15
+ /**
16
+ * Retune already-loaded textures that still carry the old default anisotropy. Textures a
17
+ * system set on purpose (pixel-art sprites pin 1) keep their value.
18
+ * @internal
19
+ */
20
+ export function retuneTextureAnisotropy(root, from, to) {
21
+ const seen = new Set();
22
+ root.traverse((node) => {
23
+ const mesh = node;
24
+ if (mesh.material === undefined)
25
+ return;
26
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
27
+ for (const material of materials) {
28
+ const slots = material;
29
+ for (const slot of TEXTURE_SLOTS) {
30
+ const texture = slots[slot];
31
+ if (texture === null || texture === undefined || seen.has(texture))
32
+ continue;
33
+ seen.add(texture);
34
+ if (texture.anisotropy !== from)
35
+ continue;
36
+ texture.anisotropy = to;
37
+ texture.needsUpdate = true;
38
+ }
39
+ }
40
+ });
41
+ return seen.size;
42
+ }
43
+ /**
44
+ * Applies the player's texture-filtering choice. Sets three's construction-time default so
45
+ * every texture loaded afterwards (GLTF, texture loaders) samples anisotropically, and walks the
46
+ * scene once per change for textures loaded before. No per-frame work.
47
+ * @internal shell-internal; driven by `GraphicsProfile.anisotropy`.
48
+ */
49
+ export function TextureFiltering({ anisotropy }) {
50
+ const gl = useThree((state) => state.gl);
51
+ const scene = useThree((state) => state.scene);
52
+ useLayoutEffect(() => {
53
+ const target = Math.max(1, Math.min(anisotropy, gl.capabilities.getMaxAnisotropy()));
54
+ const previous = THREE.Texture.DEFAULT_ANISOTROPY;
55
+ if (previous === target)
56
+ return;
57
+ THREE.Texture.DEFAULT_ANISOTROPY = target;
58
+ retuneTextureAnisotropy(scene, previous, target);
59
+ return () => {
60
+ THREE.Texture.DEFAULT_ANISOTROPY = previous;
61
+ };
62
+ }, [gl, scene, anisotropy]);
63
+ return null;
64
+ }