@jgengine/shell 0.19.0 → 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 (203) hide show
  1. package/CHANGELOG.md +346 -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 +24 -9
  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 +11 -10
  49. package/dist/inputSink.d.ts +6 -6
  50. package/dist/inputSink.js +60 -21
  51. package/dist/lazyShell3dPresentation.d.ts +9 -0
  52. package/dist/lazyShell3dPresentation.js +24 -0
  53. package/dist/materialOverride.d.ts +42 -0
  54. package/dist/materialOverride.js +95 -36
  55. package/dist/multiplayer.d.ts +33 -3
  56. package/dist/multiplayer.js +105 -22
  57. package/dist/pointer/pointerService.d.ts +2 -0
  58. package/dist/pointer/pointerService.js +12 -2
  59. package/dist/postfx/PostProcessing.d.ts +10 -0
  60. package/dist/postfx/PostProcessing.js +9 -41
  61. package/dist/postfx/gradeShader.d.ts +1 -1
  62. package/dist/postfx/gradeShader.js +30 -2
  63. package/dist/postfx/shadowPrepass.d.ts +3 -0
  64. package/dist/postfx/shadowPrepass.js +18 -0
  65. package/dist/presentationRecovery.d.ts +75 -0
  66. package/dist/presentationRecovery.js +204 -0
  67. package/dist/presentationRecovery3d.d.ts +4 -0
  68. package/dist/presentationRecovery3d.js +32 -0
  69. package/dist/presentationResolve.d.ts +8 -1
  70. package/dist/presentationResolve.js +5 -2
  71. package/dist/render/CascadedShadows.d.ts +7 -2
  72. package/dist/render/CascadedShadows.js +98 -20
  73. package/dist/render/EnvironmentLighting.js +74 -35
  74. package/dist/render/HairCards.d.ts +7 -0
  75. package/dist/render/HairCards.js +23 -0
  76. package/dist/render/SceneModels.js +51 -77
  77. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  78. package/dist/render/authoredSurfaceMaterial.js +108 -0
  79. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  80. package/dist/render/materialAppearanceSignals.js +58 -0
  81. package/dist/render/materialAsset.d.ts +98 -0
  82. package/dist/render/materialAsset.js +454 -0
  83. package/dist/render/measureBounds.js +16 -3
  84. package/dist/render/measureCollisionMesh.js +8 -1
  85. package/dist/render/modelAssets.d.ts +8 -0
  86. package/dist/render/modelAssets.js +43 -0
  87. package/dist/render/modelBindPose.d.ts +5 -0
  88. package/dist/render/modelBindPose.js +35 -0
  89. package/dist/render/modelLoad.d.ts +3 -0
  90. package/dist/render/modelLoad.js +9 -0
  91. package/dist/render/modelNodeVisibility.d.ts +6 -0
  92. package/dist/render/modelNodeVisibility.js +18 -0
  93. package/dist/render/modelRender.d.ts +18 -1
  94. package/dist/render/modelRender.js +81 -3
  95. package/dist/render/projectedOverdraw.d.ts +35 -0
  96. package/dist/render/projectedOverdraw.js +204 -0
  97. package/dist/render/resolveModel.d.ts +12 -2
  98. package/dist/render/resolveModel.js +21 -4
  99. package/dist/render/rigNode.d.ts +26 -0
  100. package/dist/render/rigNode.js +32 -0
  101. package/dist/render/spriteRender.d.ts +4 -0
  102. package/dist/render/spriteRender.js +5 -0
  103. package/dist/render/staticShapeInstances.d.ts +34 -0
  104. package/dist/render/staticShapeInstances.js +78 -0
  105. package/dist/render/useFootIk.js +76 -11
  106. package/dist/render/useModelAnimation.d.ts +28 -2
  107. package/dist/render/useModelAnimation.js +194 -69
  108. package/dist/render/useModelInstance.d.ts +25 -0
  109. package/dist/render/useModelInstance.js +43 -0
  110. package/dist/scatter/scatterModels.d.ts +3 -2
  111. package/dist/scatter/scatterModels.js +15 -7
  112. package/dist/scene/AuthoredScene.d.ts +14 -6
  113. package/dist/scene/AuthoredScene.js +30 -9
  114. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  115. package/dist/settings/SettingsMenu.js +11 -18
  116. package/dist/settings/SettingsRuntime.d.ts +5 -0
  117. package/dist/settings/SettingsRuntime.js +29 -2
  118. package/dist/shellControlSuspension.d.ts +18 -0
  119. package/dist/shellControlSuspension.js +28 -0
  120. package/dist/shellInputPublication.d.ts +28 -0
  121. package/dist/shellInputPublication.js +49 -0
  122. package/dist/shellPointerInput.d.ts +18 -0
  123. package/dist/shellPointerInput.js +49 -0
  124. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  125. package/dist/structures/BuildingKitBatch.js +34 -59
  126. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  127. package/dist/structures/BuildingRenderBudget.js +54 -0
  128. package/dist/structures/GeneratedBuilding.js +13 -8
  129. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  130. package/dist/structures/buildingKitMaterials.js +70 -0
  131. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  132. package/dist/structures/buildingSpatialBatch.js +45 -0
  133. package/dist/terrain/GrassField.d.ts +2 -1
  134. package/dist/terrain/GrassField.js +59 -5
  135. package/dist/terrain/grassGeometry.d.ts +10 -0
  136. package/dist/terrain/grassGeometry.js +55 -0
  137. package/dist/terrain/grassMaterial.js +2 -2
  138. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  139. package/dist/terrain/terrainDetailMaterial.js +7 -7
  140. package/dist/touch/TouchControlsOverlay.js +4 -4
  141. package/dist/useShellMultiplayerSync.js +4 -0
  142. package/dist/vfx/ParticleField.d.ts +23 -20
  143. package/dist/vfx/ParticleField.js +168 -50
  144. package/dist/visibility/CullingProvider.js +2 -62
  145. package/dist/visibility/cullingDriver.d.ts +9 -0
  146. package/dist/visibility/cullingDriver.js +68 -0
  147. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  148. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  149. package/dist/weather/DustField.d.ts +2 -1
  150. package/dist/weather/DustField.js +17 -3
  151. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  152. package/dist/weather/FireSpreadLayer.js +5 -11
  153. package/dist/weather/LightningStrike.d.ts +4 -1
  154. package/dist/weather/LightningStrike.js +13 -17
  155. package/dist/weather/RainField.d.ts +2 -1
  156. package/dist/weather/RainField.js +17 -3
  157. package/dist/weather/RainImpactField.d.ts +16 -0
  158. package/dist/weather/RainImpactField.js +89 -0
  159. package/dist/weather/SnowField.d.ts +2 -1
  160. package/dist/weather/SnowField.js +17 -3
  161. package/dist/weather/WeatherLayer.d.ts +11 -2
  162. package/dist/weather/WeatherLayer.js +30 -6
  163. package/dist/weather/index.d.ts +2 -0
  164. package/dist/weather/index.js +2 -0
  165. package/dist/weather/weatherMath.d.ts +2 -0
  166. package/dist/weather/weatherMath.js +8 -0
  167. package/dist/weather/weatherShelter.d.ts +18 -0
  168. package/dist/weather/weatherShelter.js +75 -0
  169. package/dist/weather/weatherUniforms.d.ts +40 -1
  170. package/dist/weather/weatherUniforms.js +29 -9
  171. package/dist/world/AuthoredFlocks.d.ts +22 -0
  172. package/dist/world/AuthoredFlocks.js +87 -0
  173. package/dist/world/StaticObjectModels.d.ts +8 -0
  174. package/dist/world/StaticObjectModels.js +75 -0
  175. package/dist/world/WorldHud.d.ts +14 -6
  176. package/dist/world/WorldHud.js +25 -12
  177. package/dist/world/WorldParticles.d.ts +10 -23
  178. package/dist/world/WorldParticles.js +209 -61
  179. package/dist/world/WorldScene.d.ts +3 -1
  180. package/dist/world/WorldScene.js +41 -26
  181. package/dist/world/staticObjectBatches.d.ts +35 -0
  182. package/dist/world/staticObjectBatches.js +163 -0
  183. package/dist/world/worldBarSamples.d.ts +37 -33
  184. package/dist/world/worldBarSamples.js +117 -75
  185. package/package.json +4 -4
  186. package/skills/ce-handoff/SKILL.md +1 -1
  187. package/skills/jgengine/SKILL.md +22 -2
  188. package/skills/jgengine/api.md +28 -17
  189. package/skills/jgengine/recipes/minimal-game.md +7 -2
  190. package/skills/jgengine-ui/SKILL.md +3 -1
  191. package/skills/jgengine-ui/api.md +204 -67
  192. package/skills/jgengine-ui/capabilities.md +115 -1
  193. package/skills/jgengine-ui/reference-primitives.md +93 -3
  194. package/skills/jgengine-ui/reference.md +2 -0
  195. package/skills/jgengine-world/SKILL.md +12 -3
  196. package/skills/jgengine-world/api.md +200 -57
  197. package/skills/jgengine-world/capabilities.md +116 -3
  198. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  199. package/skills/jgengine-world/recipes/controls.md +4 -0
  200. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  201. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  202. package/skills/jgengine-world/reference.md +152 -5
  203. package/skills/jgengine-world/visibility.md +28 -1
@@ -223,6 +223,7 @@ const DEFAULT_DRACO_DECODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/e
223
223
  const DEFAULT_KTX2_TRANSCODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/libs/basis/";
224
224
  let configuredModelLoaders;
225
225
  let ktx2Loader;
226
+ const ktx2DetectedRenderers = new WeakMap();
226
227
  /** Configures shared Draco and KTX2 loaders. Repeated calls with the same paths are no-ops. */
227
228
  export function configureModelLoaders(options = {}) {
228
229
  const next = {
@@ -242,7 +243,15 @@ export function configureModelLoaders(options = {}) {
242
243
  /** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
243
244
  export function detectKtx2Support(renderer) {
244
245
  configureModelLoaders();
246
+ if (ktx2DetectedRenderers.get(renderer) === ktx2Loader)
247
+ return;
245
248
  ktx2Loader?.detectSupport(renderer);
249
+ ktx2DetectedRenderers.set(renderer, ktx2Loader);
250
+ }
251
+ /** Shared transcoder for authored material maps; GPU support is detected by the canvas. @internal */
252
+ export function materialKtx2Loader() {
253
+ configureModelLoaders();
254
+ return ktx2Loader;
246
255
  }
247
256
  // LoadingManager reports queue transitions, not per-item events: `onStart` fires when the
248
257
  // queue goes busy and `onLoad` when it drains (errors included, via itemEnd), so a flag plus
@@ -0,0 +1,6 @@
1
+ import type { Object3D } from "three";
2
+ import { type RigNodeDiagnostic } from "./rigNode.js";
3
+ /** Order and duplicate references do not change a model instance's visibility selection. @internal */
4
+ export declare function modelHiddenNodesKey(names?: readonly string[]): string;
5
+ /** Apply visibility only to uniquely resolved nodes on an owned clone, retaining imported hidden state and rig hierarchy. @internal */
6
+ export declare function applyModelNodeVisibility(root: Object3D, names?: readonly string[]): RigNodeDiagnostic[];
@@ -0,0 +1,18 @@
1
+ import { parseModelHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility";
2
+ import { resolveRigNode } from "./rigNode.js";
3
+ /** Order and duplicate references do not change a model instance's visibility selection. @internal */
4
+ export function modelHiddenNodesKey(names = []) {
5
+ return JSON.stringify([...new Set(names)].sort());
6
+ }
7
+ /** Apply visibility only to uniquely resolved nodes on an owned clone, retaining imported hidden state and rig hierarchy. @internal */
8
+ export function applyModelNodeVisibility(root, names = []) {
9
+ const diagnostics = [];
10
+ for (const name of new Set(parseModelHiddenNodes(names))) {
11
+ const resolved = resolveRigNode(root, name);
12
+ if (resolved.node === undefined)
13
+ diagnostics.push(resolved.diagnostic);
14
+ else
15
+ resolved.node.visible = false;
16
+ }
17
+ return diagnostics;
18
+ }
@@ -1,13 +1,30 @@
1
1
  import * as THREE from "three";
2
2
  import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
3
+ import type { ModelConfig } from "@jgengine/core/game/playableGame";
4
+ import { type MeasuredLocalBounds } from "./measureBounds.js";
3
5
  export declare const PAINT_TEXTURE_SIZE = 512;
4
6
  /** Shadow participation applied to every mesh of a cloned model; mirrors `ModelConfig.shadows`. */
5
7
  export type ModelShadowMode = "cast" | "receive" | "both" | "none";
6
- /** @internal */
8
+ /** Register an instance replacement without transferring borrowed geometry or textures. @internal */
9
+ export declare function ownModelMaterial(root: THREE.Object3D, material: THREE.Material): boolean;
10
+ /** Resolve placement from bind bounds in the frame containing the imported root, shared by rendering and collider measurement. @internal */
11
+ export declare function modelPlacementTransform(root: THREE.Object3D, model: ModelConfig, bindBounds?: MeasuredLocalBounds | null): {
12
+ scale: number;
13
+ position: [number, number, number];
14
+ };
15
+ /**
16
+ * Clone a model with independent pose and materials, retaining shared geometry and textures.
17
+ * @capability model-instance clone a loaded model for independent styling and animation
18
+ */
7
19
  export declare function cloneModelScene(source: THREE.Object3D, options?: {
8
20
  cloneMaterials?: boolean;
9
21
  shadows?: ModelShadowMode;
10
22
  }): THREE.Object3D;
23
+ /**
24
+ * Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
25
+ * @capability model-instance release a cloned model without disposing cached geometry or textures
26
+ */
27
+ export declare function disposeModelScene(root: THREE.Object3D): void;
11
28
  /** @internal */
12
29
  export declare function disposeClonedMaterials(root: THREE.Object3D): void;
13
30
  /** @internal */
@@ -1,25 +1,103 @@
1
1
  import * as THREE from "three";
2
2
  import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
3
+ import { measureLocalBounds } from "./measureBounds.js";
4
+ import { copyModelBindPose } from "./modelBindPose.js";
3
5
  export const PAINT_TEXTURE_SIZE = 512;
4
- /** @internal */
6
+ const modelResources = new WeakMap();
7
+ /** Register an instance replacement without transferring borrowed geometry or textures. @internal */
8
+ export function ownModelMaterial(root, material) {
9
+ const resources = modelResources.get(root);
10
+ if (resources === undefined)
11
+ return false;
12
+ resources.materials.add(material);
13
+ return true;
14
+ }
15
+ /** Resolve placement from bind bounds in the frame containing the imported root, shared by rendering and collider measurement. @internal */
16
+ export function modelPlacementTransform(root, model, bindBounds) {
17
+ let scale = model.scale ?? 1;
18
+ let minY = 0;
19
+ let centerX = 0;
20
+ let centerZ = 0;
21
+ const bounds = model.targetHeight === undefined ? null : bindBounds === undefined ? measureLocalBounds(root) : bindBounds;
22
+ const height = bounds === null ? 0 : bounds.max[1] - bounds.min[1];
23
+ if (bounds !== null && Number.isFinite(height) && height > 0) {
24
+ scale *= model.targetHeight / height;
25
+ minY = bounds.min[1];
26
+ centerX = (bounds.min[0] + bounds.max[0]) / 2;
27
+ centerZ = (bounds.min[2] + bounds.max[2]) / 2;
28
+ }
29
+ else if ((model.anchor ?? "center") === "center" && model.dims !== undefined) {
30
+ minY = model.dims.minY;
31
+ centerX = model.dims.center.x;
32
+ centerZ = model.dims.center.z;
33
+ }
34
+ return { scale, position: [-scale * centerX, (model.y ?? 0) - scale * minY, -scale * centerZ] };
35
+ }
36
+ /**
37
+ * Clone a model with independent pose and materials, retaining shared geometry and textures.
38
+ * @capability model-instance clone a loaded model for independent styling and animation
39
+ */
5
40
  export function cloneModelScene(source, options) {
6
41
  const clone = cloneSkinned(source);
42
+ copyModelBindPose(source, clone);
7
43
  const shadows = options?.shadows ?? "both";
8
44
  const cloneMaterials = options?.cloneMaterials !== false;
45
+ const materials = new Map();
46
+ const sourceSkeletons = [];
47
+ source.traverse((node) => {
48
+ if (node.isSkinnedMesh === true)
49
+ sourceSkeletons.push(node.skeleton);
50
+ });
51
+ const skeletons = new Map();
52
+ let skinnedIndex = 0;
53
+ const cloneMaterial = (source) => {
54
+ let material = materials.get(source);
55
+ if (material === undefined) {
56
+ material = source.clone();
57
+ material.onBeforeCompile = source.onBeforeCompile;
58
+ material.customProgramCacheKey = source.customProgramCacheKey;
59
+ materials.set(source, material);
60
+ }
61
+ return material;
62
+ };
9
63
  clone.traverse((node) => {
10
64
  const mesh = node;
11
65
  if (!mesh.isMesh)
12
66
  return;
67
+ if (mesh.isSkinnedMesh === true) {
68
+ const skinned = mesh;
69
+ const sourceSkeleton = sourceSkeletons[skinnedIndex++];
70
+ const shared = skeletons.get(sourceSkeleton);
71
+ if (shared === undefined)
72
+ skeletons.set(sourceSkeleton, skinned.skeleton);
73
+ else
74
+ skinned.skeleton = shared;
75
+ }
13
76
  mesh.castShadow = shadows === "cast" || shadows === "both";
14
77
  mesh.receiveShadow = shadows === "receive" || shadows === "both";
15
78
  if (!cloneMaterials)
16
79
  return;
17
80
  mesh.material = Array.isArray(mesh.material)
18
- ? mesh.material.map((material) => material.clone())
19
- : mesh.material.clone();
81
+ ? mesh.material.map(cloneMaterial)
82
+ : cloneMaterial(mesh.material);
20
83
  });
84
+ modelResources.set(clone, { materials: new Set(materials.values()), skeletons: new Set(skeletons.values()) });
21
85
  return clone;
22
86
  }
87
+ /**
88
+ * Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
89
+ * @capability model-instance release a cloned model without disposing cached geometry or textures
90
+ */
91
+ export function disposeModelScene(root) {
92
+ const resources = modelResources.get(root);
93
+ if (resources === undefined)
94
+ return;
95
+ for (const material of resources.materials)
96
+ material.dispose();
97
+ // Effect replay can recreate a bone texture on this same scene, so retain its ownership record.
98
+ for (const skeleton of resources.skeletons)
99
+ skeleton.dispose();
100
+ }
23
101
  /** @internal */
24
102
  export function disposeClonedMaterials(root) {
25
103
  const seen = new Set();
@@ -0,0 +1,35 @@
1
+ import * as THREE from "three";
2
+ /** Sampling resolution; lower resolutions change thin geometry and alpha-mask raster coverage. */
3
+ export interface ProjectedOverdrawOptions {
4
+ width?: number;
5
+ height?: number;
6
+ }
7
+ /** Projected mesh-fragment overlap, with depth rejection disabled; this is not production early-Z overdraw or GPU timing. */
8
+ export interface ProjectedOverdrawReport {
9
+ width: number;
10
+ height: number;
11
+ sampledPixels: number;
12
+ coveredPixels: number;
13
+ fragmentSamples: number;
14
+ mean: number;
15
+ meanCovered: number;
16
+ max: number;
17
+ meshes: number;
18
+ materialSlots: number;
19
+ /** Native opaque render-queue slots, including masked slots. */
20
+ opaqueSlots: number;
21
+ /** Slots with alphaTest/alphaHash; this can overlap queue counts. */
22
+ maskedSlots: number;
23
+ blendedSlots: number;
24
+ transmissionSlots: number;
25
+ caveats: readonly string[];
26
+ }
27
+ /** Convert finite nonnegative RGBA32F sample counts to overlap statistics. @internal */
28
+ export declare function summarizeProjectedOverdraw(pixels: Float32Array, width: number, height: number): Pick<ProjectedOverdrawReport, "sampledPixels" | "coveredPixels" | "fragmentSamples" | "mean" | "meanCovered" | "max">;
29
+ /**
30
+ * Opt in to a synchronous offscreen count of projected mesh fragments. Native PBR/unlit vertex transforms and alpha discard remain; depth rejection, shadow/refraction passes and postprocessing do not participate.
31
+ * Blended texels count equally, including zero opacity when native alphaTest/alphaHash does not discard them. Pure shader-modifying onBeforeCompile callbacks are retained; render callbacks, custom shader materials, stencil and alpha-to-coverage are rejected.
32
+ * The float readback stalls the GPU. Temporary materials/target are disposed and renderer draw counters/state restored; borrowed assets may become resident while measured.
33
+ * @capability projected-overdraw sample actual mesh/camera fragment overlap with native coverage and explicit depth/blending/transmission limits
34
+ */
35
+ export declare function measureProjectedOverdraw(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, options?: ProjectedOverdrawOptions): ProjectedOverdrawReport;
@@ -0,0 +1,204 @@
1
+ import * as THREE from "three";
2
+ function supported(material) {
3
+ const flags = material;
4
+ return flags.isMeshStandardMaterial === true || flags.isMeshBasicMaterial === true;
5
+ }
6
+ /** Convert finite nonnegative RGBA32F sample counts to overlap statistics. @internal */
7
+ export function summarizeProjectedOverdraw(pixels, width, height) {
8
+ if (pixels.length !== width * height * 4)
9
+ throw new Error("Projected-overdraw readback dimensions do not match the RGBA buffer");
10
+ let coveredPixels = 0, fragmentSamples = 0, max = 0;
11
+ for (let index = 0; index < pixels.length; index += 4) {
12
+ const count = pixels[index];
13
+ if (!Number.isFinite(count) || count < 0)
14
+ throw new Error("Projected-overdraw float readback returned invalid samples");
15
+ if (count > 0)
16
+ coveredPixels++;
17
+ fragmentSamples += count;
18
+ max = Math.max(max, count);
19
+ }
20
+ const sampledPixels = width * height;
21
+ return { sampledPixels, coveredPixels, fragmentSamples, mean: fragmentSamples / sampledPixels, meanCovered: coveredPixels === 0 ? 0 : fragmentSamples / coveredPixels, max };
22
+ }
23
+ function countMaterial(source) {
24
+ const material = source.clone();
25
+ const compile = source.onBeforeCompile;
26
+ const key = source.customProgramCacheKey();
27
+ material.onBeforeCompile = (shader, renderer) => {
28
+ compile.call(material, shader, renderer);
29
+ const tail = /#include <dithering_fragment>(\s*})\s*$/;
30
+ if (!tail.test(shader.fragmentShader))
31
+ throw new Error("Projected overdraw requires the native final fragment chunk; this shader callback changes the fragment main tail");
32
+ shader.fragmentShader = shader.fragmentShader.replace(tail, "#include <dithering_fragment>\ngl_FragColor = vec4(1.0);$1");
33
+ };
34
+ material.customProgramCacheKey = () => `${key}|jg-projected-overdraw:v1`;
35
+ material.transparent = true;
36
+ material.blending = THREE.CustomBlending;
37
+ material.blendEquation = THREE.AddEquation;
38
+ material.blendSrc = THREE.OneFactor;
39
+ material.blendDst = THREE.OneFactor;
40
+ material.blendEquationAlpha = THREE.AddEquation;
41
+ material.blendSrcAlpha = THREE.OneFactor;
42
+ material.blendDstAlpha = THREE.OneFactor;
43
+ material.depthTest = false;
44
+ material.depthWrite = false;
45
+ material.colorWrite = true;
46
+ material.forceSinglePass = true;
47
+ material.toneMapped = false;
48
+ material.premultipliedAlpha = false;
49
+ material.alphaToCoverage = false;
50
+ if (material.isMeshPhysicalMaterial)
51
+ material.transmission = 0;
52
+ return material;
53
+ }
54
+ /**
55
+ * Opt in to a synchronous offscreen count of projected mesh fragments. Native PBR/unlit vertex transforms and alpha discard remain; depth rejection, shadow/refraction passes and postprocessing do not participate.
56
+ * Blended texels count equally, including zero opacity when native alphaTest/alphaHash does not discard them. Pure shader-modifying onBeforeCompile callbacks are retained; render callbacks, custom shader materials, stencil and alpha-to-coverage are rejected.
57
+ * The float readback stalls the GPU. Temporary materials/target are disposed and renderer draw counters/state restored; borrowed assets may become resident while measured.
58
+ * @capability projected-overdraw sample actual mesh/camera fragment overlap with native coverage and explicit depth/blending/transmission limits
59
+ */
60
+ export function measureProjectedOverdraw(renderer, scene, camera, options = {}) {
61
+ const width = options.width ?? 128;
62
+ const height = options.height ?? 128;
63
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > 2048 || height > 2048)
64
+ throw new Error("Projected-overdraw resolution must contain integer dimensions from 1 to 2048");
65
+ if (!renderer.extensions.has("EXT_color_buffer_float") || !renderer.extensions.has("EXT_float_blend"))
66
+ throw new Error("Projected overdraw requires RGBA32F rendering/readback and EXT_float_blend; this renderer cannot provide a fragment count");
67
+ if (camera.isArrayCamera)
68
+ throw new Error("Projected overdraw requires one camera projection; measure array-camera views separately");
69
+ if (scene.overrideMaterial !== null)
70
+ throw new Error("Projected overdraw does not support an active scene overrideMaterial");
71
+ const meshes = [];
72
+ const originals = new Map();
73
+ const materials = new Set();
74
+ let materialSlots = 0, opaqueSlots = 0, maskedSlots = 0, blendedSlots = 0, transmissionSlots = 0;
75
+ if (scene.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender || scene.onAfterRender !== THREE.Object3D.prototype.onAfterRender)
76
+ throw new Error("Projected overdraw rejects scene render callbacks");
77
+ scene.traverseVisible(node => {
78
+ if (!node.layers.test(camera.layers))
79
+ return;
80
+ const flags = node;
81
+ if (flags.geometry?.drawRange.count === 0 || (flags.isInstancedMesh && flags.count === 0))
82
+ return;
83
+ if (flags.material !== undefined && (Array.isArray(flags.material) ? flags.material : [flags.material]).every(material => !material.visible))
84
+ return;
85
+ if (flags.isPoints || flags.isLine || flags.isSprite || flags.isBatchedMesh)
86
+ throw new Error(`Projected overdraw does not support ${node.type}`);
87
+ if (!flags.isMesh)
88
+ return;
89
+ if (node.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender || node.onAfterRender !== THREE.Object3D.prototype.onAfterRender)
90
+ throw new Error(`Projected overdraw rejects render callbacks on ${node.name || node.type}`);
91
+ meshes.push(flags);
92
+ originals.set(flags, flags.material);
93
+ for (const material of Array.isArray(flags.material) ? flags.material : [flags.material]) {
94
+ if (!material.visible)
95
+ continue;
96
+ if (!supported(material))
97
+ throw new Error(`Projected overdraw requires native PBR or unlit material; ${material.name || material.type} is unsupported`);
98
+ if (material.onBeforeRender !== THREE.Material.prototype.onBeforeRender)
99
+ throw new Error(`Projected overdraw rejects material render callbacks on ${material.name || material.type}`);
100
+ if (material.stencilWrite || material.alphaToCoverage)
101
+ throw new Error(`Projected overdraw cannot reproduce stencil or multisample alpha coverage on ${material.name || material.type}`);
102
+ materials.add(material);
103
+ materialSlots++;
104
+ if (material.alphaTest > 0 || material.alphaHash)
105
+ maskedSlots++;
106
+ if (material.transparent)
107
+ blendedSlots++;
108
+ else
109
+ opaqueSlots++;
110
+ if (material.transmission > 0)
111
+ transmissionSlots++;
112
+ }
113
+ });
114
+ const target = new THREE.WebGLRenderTarget(width, height, { type: THREE.FloatType, format: THREE.RGBAFormat, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, depthBuffer: false, stencilBuffer: false, generateMipmaps: false });
115
+ target.texture.colorSpace = THREE.NoColorSpace;
116
+ const replacements = new Map();
117
+ const userData = new Map([...materials].map(material => [material, { object: material.userData, entries: { ...material.userData } }]));
118
+ const context = renderer.getContext();
119
+ const state = {
120
+ target: renderer.getRenderTarget(), face: renderer.getActiveCubeFace(), mip: renderer.getActiveMipmapLevel(),
121
+ viewport: renderer.getCurrentViewport(new THREE.Vector4()),
122
+ scissor: new THREE.Vector4().fromArray(context.getParameter(context.SCISSOR_BOX)), scissorTest: context.getParameter(context.SCISSOR_TEST),
123
+ clearColor: renderer.getClearColor(new THREE.Color()), clearAlpha: renderer.getClearAlpha(), autoClear: renderer.autoClear,
124
+ autoClearColor: renderer.autoClearColor, autoClearDepth: renderer.autoClearDepth, autoClearStencil: renderer.autoClearStencil,
125
+ xr: renderer.xr.enabled, shadows: renderer.shadowMap.enabled, infoAutoReset: renderer.info.autoReset, render: { ...renderer.info.render },
126
+ background: scene.background, fog: scene.fog, shaderCheck: renderer.debug.checkShaderErrors, shaderError: renderer.debug.onShaderError,
127
+ };
128
+ try {
129
+ const shaderErrors = [];
130
+ renderer.debug.checkShaderErrors = true;
131
+ renderer.debug.onShaderError = (gl, program, vertex, fragment) => { shaderErrors.push([gl.getProgramInfoLog(program), gl.getShaderInfoLog(vertex), gl.getShaderInfoLog(fragment)].filter(Boolean).join("\n")); };
132
+ renderer.xr.enabled = false;
133
+ renderer.shadowMap.enabled = false;
134
+ renderer.info.autoReset = false;
135
+ renderer.autoClear = false;
136
+ renderer.setRenderTarget(target);
137
+ renderer.setClearColor(0x000000, 0.25);
138
+ renderer.clear(true, false, false);
139
+ const probe = new Float32Array(4).fill(NaN);
140
+ renderer.readRenderTargetPixels(target, 0, 0, 1, 1, probe);
141
+ if (!Number.isFinite(probe[3]) || Math.abs(probe[3] - 0.25) > 0.001)
142
+ throw new Error("Projected overdraw RGBA32F readback is unsupported or incomplete");
143
+ renderer.setClearColor(0x000000, 0);
144
+ renderer.clear(true, false, false);
145
+ for (const source of materials)
146
+ replacements.set(source, countMaterial(source));
147
+ for (const mesh of meshes) {
148
+ const source = originals.get(mesh);
149
+ mesh.material = Array.isArray(source) ? source.map(material => replacements.get(material) ?? material) : replacements.get(source) ?? source;
150
+ }
151
+ scene.background = null;
152
+ scene.fog = null;
153
+ renderer.render(scene, camera);
154
+ if (shaderErrors.length > 0)
155
+ throw new Error(`Projected-overdraw shader compilation failed: ${shaderErrors.join("\n")}`);
156
+ const pixels = new Float32Array(width * height * 4).fill(NaN);
157
+ renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
158
+ return { width, height, ...summarizeProjectedOverdraw(pixels, width, height), meshes: meshes.length, materialSlots, opaqueSlots, maskedSlots, blendedSlots, transmissionSlots,
159
+ caveats: ["Depth rejection is disabled: projected overlap is an upper bound on production opaque shading, not early-Z overdraw.", "Native alphaTest/alphaHash discard is retained at this sampled resolution; blended texels count equally, including zero opacity.", "Transmission refraction/background, shadow, postprocessing, sky and MSAA passes are excluded. No GPU timing is measured.", "Native vertex transforms and pure shader-modifying material callbacks remain; borrowed assets may be uploaded during this diagnostic."] };
160
+ }
161
+ finally {
162
+ for (const [mesh, material] of originals)
163
+ mesh.material = material;
164
+ for (const [material, saved] of userData) {
165
+ material.userData = saved.object;
166
+ for (const key of Object.keys(saved.object))
167
+ if (!(key in saved.entries))
168
+ delete saved.object[key];
169
+ Object.assign(saved.object, saved.entries);
170
+ }
171
+ scene.background = state.background;
172
+ scene.fog = state.fog;
173
+ renderer.xr.enabled = state.xr;
174
+ renderer.shadowMap.enabled = state.shadows;
175
+ renderer.autoClear = state.autoClear;
176
+ renderer.autoClearColor = state.autoClearColor;
177
+ renderer.autoClearDepth = state.autoClearDepth;
178
+ renderer.autoClearStencil = state.autoClearStencil;
179
+ renderer.info.autoReset = state.infoAutoReset;
180
+ renderer.debug.checkShaderErrors = state.shaderCheck;
181
+ renderer.debug.onShaderError = state.shaderError;
182
+ for (const material of replacements.values())
183
+ material.dispose();
184
+ target.dispose();
185
+ if (state.target === null)
186
+ renderer.setRenderTarget(null, state.face, state.mip);
187
+ else {
188
+ const viewport = state.target.viewport.clone(), scissor = state.target.scissor.clone(), scissorTest = state.target.scissorTest;
189
+ state.target.viewport.copy(state.viewport);
190
+ state.target.scissor.copy(state.scissor);
191
+ state.target.scissorTest = state.scissorTest;
192
+ try {
193
+ renderer.setRenderTarget(state.target, state.face, state.mip);
194
+ }
195
+ finally {
196
+ state.target.viewport.copy(viewport);
197
+ state.target.scissor.copy(scissor);
198
+ state.target.scissorTest = scissorTest;
199
+ }
200
+ }
201
+ renderer.setClearColor(state.clearColor, state.clearAlpha);
202
+ Object.assign(renderer.info.render, state.render);
203
+ }
204
+ }
@@ -1,5 +1,6 @@
1
- import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
1
+ import type { AssetCatalog, ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
2
2
  import type { ModelConfig } from "@jgengine/core/game/playableGame";
3
+ import type { SceneObject } from "@jgengine/core/scene/objectStore";
3
4
  import type { GameContextModels } from "@jgengine/core/runtime/gameContext";
4
5
  export interface ModelResolveContext {
5
6
  seam: "entityModels" | "objectModels" | "scatterModels";
@@ -16,6 +17,14 @@ export declare function resolveModel(value: string | ModelConfig | undefined, as
16
17
  * @internal
17
18
  */
18
19
  export declare function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined;
20
+ /** @internal */
21
+ export interface ObjectModelCacheEntry {
22
+ input: string | ModelConfig | undefined;
23
+ ref: ModelAssetRef | null;
24
+ model: ModelConfig | undefined;
25
+ }
26
+ /** Resolve a live object's catalog and animation, retaining unchanged base model identities. @internal */
27
+ export declare function resolveObjectModel(object: SceneObject, models: Record<string, string | ModelConfig> | undefined, assets: AssetCatalog, cache: Map<string, ObjectModelCacheEntry>): ModelConfig | undefined;
19
28
  /**
20
29
  * Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
21
30
  * so mounts receive fully-resolved `ModelConfig`s.
@@ -45,7 +54,8 @@ export declare function resolveModelPlan(assets: AssetCatalog, plan: Record<stri
45
54
  * The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
46
55
  * object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
47
56
  * ids (a missing catalog id keeps the default collider instead of throwing — the render path already
48
- * reports it), passes inline configs through. `undefined` when the playable maps no models at all.
57
+ * reports it), passes inline configs through. Unmapped objects resolve their own catalog id,
58
+ * matching WorldScene; `undefined` when both the catalog and model maps are empty.
49
59
  * @internal
50
60
  */
51
61
  export declare function contextModels(playable: {
@@ -8,6 +8,7 @@ import { warnOnce } from "@jgengine/core/devtools/warnOnce";
8
8
  function catalogModelConfig(ref) {
9
9
  return {
10
10
  url: ref.url,
11
+ ...(ref.anchor === undefined ? {} : { anchor: ref.anchor }),
11
12
  ...(ref.dims === undefined ? {} : { dims: ref.dims }),
12
13
  ...(ref.collisionMesh === undefined ? {} : { collisionMesh: ref.collisionMesh }),
13
14
  animation: "auto",
@@ -40,6 +41,18 @@ export function tryResolveCatalogModel(id, assets) {
40
41
  return undefined;
41
42
  return catalogModelConfig(ref);
42
43
  }
44
+ /** Resolve a live object's catalog and animation, retaining unchanged base model identities. @internal */
45
+ export function resolveObjectModel(object, models, assets, cache) {
46
+ const input = models?.[object.catalogId];
47
+ const ref = assets.resolve(typeof input === "string" ? input : object.catalogId);
48
+ const cached = cache.get(object.catalogId);
49
+ const resolved = cached !== undefined && cached.input === input && cached.ref === ref ? cached.model :
50
+ resolveModel(input, assets, { seam: "objectModels", key: object.catalogId }) ?? tryResolveCatalogModel(object.catalogId, assets);
51
+ if (cached === undefined || cached.model !== resolved || cached.input !== input || cached.ref !== ref) {
52
+ cache.set(object.catalogId, { input, ref, model: resolved });
53
+ }
54
+ return resolved !== undefined && object.animation !== undefined ? { ...resolved, animation: object.animation } : resolved;
55
+ }
43
56
  /**
44
57
  * Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
45
58
  * so mounts receive fully-resolved `ModelConfig`s.
@@ -114,17 +127,21 @@ function softModelLookup(map, assets) {
114
127
  * The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
115
128
  * object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
116
129
  * ids (a missing catalog id keeps the default collider instead of throwing — the render path already
117
- * reports it), passes inline configs through. `undefined` when the playable maps no models at all.
130
+ * reports it), passes inline configs through. Unmapped objects resolve their own catalog id,
131
+ * matching WorldScene; `undefined` when both the catalog and model maps are empty.
118
132
  * @internal
119
133
  */
120
134
  export function contextModels(playable) {
121
135
  const entity = softModelLookup(playable.entityModels, playable.game.assets);
122
- const object = softModelLookup(playable.objectModels, playable.game.assets);
123
- if (entity === undefined && object === undefined)
136
+ const explicitObject = softModelLookup(playable.objectModels, playable.game.assets);
137
+ if (entity === undefined && explicitObject === undefined && playable.game.assets.ids().length === 0)
124
138
  return undefined;
139
+ const object = (key) => playable.objectModels?.[key] === undefined
140
+ ? tryResolveCatalogModel(key, playable.game.assets)
141
+ : explicitObject?.(key);
125
142
  return {
126
143
  ...(entity === undefined ? {} : { entity }),
127
- ...(object === undefined ? {} : { object }),
144
+ object,
128
145
  };
129
146
  }
130
147
  /**
@@ -0,0 +1,26 @@
1
+ import type { Object3D } from "three";
2
+ /** A missing or nonunique node reference that a rig author can repair. */
3
+ export interface RigNodeDiagnostic {
4
+ code: "missing-node" | "ambiguous-node";
5
+ message: string;
6
+ }
7
+ /** A node on this model instance, or a diagnostic instead of an arbitrary attachment target. */
8
+ export type RigNodeResolution = {
9
+ node: Object3D;
10
+ matchedBy: "runtime" | "authored";
11
+ diagnostic?: undefined;
12
+ } | {
13
+ node: undefined;
14
+ matchedBy?: undefined;
15
+ diagnostic: RigNodeDiagnostic;
16
+ };
17
+ /**
18
+ * Resolves a bone or attachment slot by its exact runtime name first, then by the original
19
+ * imported name preserved as `userData.name`. GLTFLoader sanitizes names such as `handslot.r`
20
+ * to `handslotr`; authored references remain usable without renaming the animated hierarchy.
21
+ * Both lookups require a unique target. Call on model load or slot changes, not each frame,
22
+ * and keep the result with that model instance rather than reusing a cached asset's node.
23
+ *
24
+ * @capability imported-rig-node resolve original imported bone and slot names with ambiguity diagnostics
25
+ */
26
+ export declare function resolveRigNode(rig: Object3D, name: string): RigNodeResolution;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolves a bone or attachment slot by its exact runtime name first, then by the original
3
+ * imported name preserved as `userData.name`. GLTFLoader sanitizes names such as `handslot.r`
4
+ * to `handslotr`; authored references remain usable without renaming the animated hierarchy.
5
+ * Both lookups require a unique target. Call on model load or slot changes, not each frame,
6
+ * and keep the result with that model instance rather than reusing a cached asset's node.
7
+ *
8
+ * @capability imported-rig-node resolve original imported bone and slot names with ambiguity diagnostics
9
+ */
10
+ export function resolveRigNode(rig, name) {
11
+ const exact = [];
12
+ const authored = [];
13
+ rig.traverse((node) => {
14
+ if (node.name === name)
15
+ exact.push(node);
16
+ if (node.userData.name === name)
17
+ authored.push(node);
18
+ });
19
+ const matches = exact.length > 0 ? exact : authored;
20
+ const matchedBy = exact.length > 0 ? "runtime" : "authored";
21
+ if (matches.length === 1)
22
+ return { node: matches[0], matchedBy };
23
+ if (matches.length > 1)
24
+ return {
25
+ node: undefined,
26
+ diagnostic: { code: "ambiguous-node", message: `bone/slot "${name}" matches ${matches.length} ${matchedBy} names on this rig. Choose a unique runtime node name or repair duplicate imported names; no node was selected.` },
27
+ };
28
+ return {
29
+ node: undefined,
30
+ diagnostic: { code: "missing-node", message: `bone/slot "${name}" was not found by runtime or original imported name on this rig. Check the model's node names and configure a supported slot.` },
31
+ };
32
+ }
@@ -0,0 +1,4 @@
1
+ import type * as THREE from "three";
2
+ import type { SpriteAtlas } from "@jgengine/core/assets/spriteAtlas";
3
+ /** Update atlas UVs without invalidating the shared image upload. @internal */
4
+ export declare function syncSpriteFrame(texture: THREE.Texture, frame: SpriteAtlas["frames"][string], size: readonly [number, number]): void;
@@ -0,0 +1,5 @@
1
+ /** Update atlas UVs without invalidating the shared image upload. @internal */
2
+ export function syncSpriteFrame(texture, frame, size) {
3
+ texture.repeat.set(frame.w / size[0], frame.h / size[1]);
4
+ texture.offset.set(frame.x / size[0], 1 - (frame.y + frame.h) / size[1]);
5
+ }
@@ -0,0 +1,34 @@
1
+ import * as THREE from "three";
2
+ import type { ModelShadowMode } from "./modelRender.js";
3
+ import { type AuthoredSurfaceConfig, type SurfaceShape } from "./authoredSurfaceMaterial.js";
4
+ /** One editor-derived static primitive. A box spans one unit; a cylinder has unit radius and height before scale. */
5
+ export interface StaticShapeInstance {
6
+ shape: SurfaceShape;
7
+ position: readonly [number, number, number];
8
+ rotation?: readonly [number, number, number];
9
+ scale: readonly [number, number, number];
10
+ surface: AuthoredSurfaceConfig;
11
+ shadows?: ModelShadowMode;
12
+ /** Cylinder tessellation; default 16. */
13
+ radialSegments?: number;
14
+ }
15
+ /** A compatible static draw group. @internal */
16
+ export interface StaticShapeBatch {
17
+ key: string;
18
+ shape: SurfaceShape;
19
+ radialSegments: number;
20
+ surface: AuthoredSurfaceConfig;
21
+ shadows: ModelShadowMode;
22
+ instances: readonly StaticShapeInstance[];
23
+ }
24
+ /** Plan compatible material/shape/shadow groups without changing authored transforms. @internal */
25
+ export declare function groupStaticShapeInstances(instances: readonly StaticShapeInstance[]): StaticShapeBatch[];
26
+ /** Upload static transforms and refresh conservative aggregate bounds after a prop change. @internal */
27
+ export declare function writeStaticShapeInstances(mesh: THREE.InstancedMesh, instances: readonly StaticShapeInstance[]): void;
28
+ /**
29
+ * Batch editor-derived boxes/cylinders by shape, PBR metadata and shadows. Uploads and bounds run only when instances change; map loading stays isolated per draw group.
30
+ * @capability static-shape-instances render authored primitive transforms as static material-compatible instance batches
31
+ */
32
+ export declare function StaticShapeInstances({ instances }: {
33
+ instances: readonly StaticShapeInstance[];
34
+ }): import("react").JSX.Element;