@jgengine/shell 0.19.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +113 -15
  5. package/dist/Shell3dPresentation.d.ts +2 -2
  6. package/dist/Shell3dPresentation.js +49 -7
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +11 -1
  10. package/dist/ShellHudPresentation.js +20 -6
  11. package/dist/audio/audioEngine.d.ts +16 -0
  12. package/dist/audio/audioEngine.js +178 -89
  13. package/dist/audio/audioWire.js +2 -2
  14. package/dist/audio/synthEngine.d.ts +7 -1
  15. package/dist/audio/synthEngine.js +103 -38
  16. package/dist/camera/GameCameraRig.js +2 -1
  17. package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
  18. package/dist/camera/GameFirstPersonCamera.js +17 -13
  19. package/dist/camera/cameraRigs.js +8 -2
  20. package/dist/camera/chaseHeading.d.ts +9 -0
  21. package/dist/camera/chaseHeading.js +10 -0
  22. package/dist/camera/customMuzzle.d.ts +14 -0
  23. package/dist/camera/customMuzzle.js +68 -0
  24. package/dist/camera/index.d.ts +1 -0
  25. package/dist/camera/index.js +1 -0
  26. package/dist/camera/rigResolve.d.ts +3 -0
  27. package/dist/camera/rigResolve.js +11 -0
  28. package/dist/combat/ProjectileModels.d.ts +31 -0
  29. package/dist/combat/ProjectileModels.js +70 -0
  30. package/dist/commandSink.d.ts +1 -2
  31. package/dist/commandSink.js +3 -2
  32. package/dist/defineGame.d.ts +12 -5
  33. package/dist/defineGame.js +25 -10
  34. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  35. package/dist/devtools/DevtoolsOverlay.js +14 -168
  36. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  37. package/dist/devtools/DevtoolsRuntime.js +61 -0
  38. package/dist/devtools/agentBridge.js +1 -1
  39. package/dist/devtools/devtoolsReports.d.ts +68 -0
  40. package/dist/devtools/devtoolsReports.js +100 -0
  41. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  42. package/dist/devtools/latencyInstrumentation.js +11 -0
  43. package/dist/devtools/playtestProbe.d.ts +4 -0
  44. package/dist/devtools/playtestProbe.js +22 -0
  45. package/dist/drivers/FrameDriver.js +25 -21
  46. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  47. package/dist/drivers/HudOnlyDriver.js +32 -14
  48. package/dist/environment/EnvironmentScene.js +19 -15
  49. package/dist/environment/terrainGroundColor.d.ts +5 -4
  50. package/dist/environment/terrainGroundColor.js +19 -8
  51. package/dist/inputSink.d.ts +6 -6
  52. package/dist/inputSink.js +60 -21
  53. package/dist/lazyShell3dPresentation.d.ts +9 -0
  54. package/dist/lazyShell3dPresentation.js +24 -0
  55. package/dist/materialOverride.d.ts +42 -0
  56. package/dist/materialOverride.js +95 -36
  57. package/dist/multiplayer.d.ts +33 -3
  58. package/dist/multiplayer.js +105 -22
  59. package/dist/pointer/pointerService.d.ts +2 -0
  60. package/dist/pointer/pointerService.js +12 -2
  61. package/dist/postfx/PostProcessing.d.ts +10 -0
  62. package/dist/postfx/PostProcessing.js +9 -41
  63. package/dist/postfx/gradeShader.d.ts +1 -1
  64. package/dist/postfx/gradeShader.js +30 -2
  65. package/dist/postfx/shadowPrepass.d.ts +3 -0
  66. package/dist/postfx/shadowPrepass.js +18 -0
  67. package/dist/presentationRecovery.d.ts +75 -0
  68. package/dist/presentationRecovery.js +204 -0
  69. package/dist/presentationRecovery3d.d.ts +4 -0
  70. package/dist/presentationRecovery3d.js +32 -0
  71. package/dist/presentationResolve.d.ts +8 -1
  72. package/dist/presentationResolve.js +5 -2
  73. package/dist/render/CascadedShadows.d.ts +7 -2
  74. package/dist/render/CascadedShadows.js +98 -20
  75. package/dist/render/EnvironmentLighting.js +74 -35
  76. package/dist/render/HairCards.d.ts +7 -0
  77. package/dist/render/HairCards.js +23 -0
  78. package/dist/render/SceneModels.js +51 -77
  79. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  80. package/dist/render/authoredSurfaceMaterial.js +108 -0
  81. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  82. package/dist/render/materialAppearanceSignals.js +58 -0
  83. package/dist/render/materialAsset.d.ts +98 -0
  84. package/dist/render/materialAsset.js +454 -0
  85. package/dist/render/measureBounds.js +16 -3
  86. package/dist/render/measureCollisionMesh.js +8 -1
  87. package/dist/render/modelAssets.d.ts +8 -0
  88. package/dist/render/modelAssets.js +43 -0
  89. package/dist/render/modelBindPose.d.ts +5 -0
  90. package/dist/render/modelBindPose.js +35 -0
  91. package/dist/render/modelLoad.d.ts +3 -0
  92. package/dist/render/modelLoad.js +9 -0
  93. package/dist/render/modelNodeVisibility.d.ts +6 -0
  94. package/dist/render/modelNodeVisibility.js +18 -0
  95. package/dist/render/modelRender.d.ts +18 -1
  96. package/dist/render/modelRender.js +81 -3
  97. package/dist/render/projectedOverdraw.d.ts +35 -0
  98. package/dist/render/projectedOverdraw.js +204 -0
  99. package/dist/render/resolveModel.d.ts +12 -2
  100. package/dist/render/resolveModel.js +21 -4
  101. package/dist/render/rigNode.d.ts +26 -0
  102. package/dist/render/rigNode.js +32 -0
  103. package/dist/render/spriteRender.d.ts +4 -0
  104. package/dist/render/spriteRender.js +5 -0
  105. package/dist/render/staticShapeInstances.d.ts +34 -0
  106. package/dist/render/staticShapeInstances.js +78 -0
  107. package/dist/render/useFootIk.js +76 -11
  108. package/dist/render/useModelAnimation.d.ts +28 -2
  109. package/dist/render/useModelAnimation.js +194 -69
  110. package/dist/render/useModelInstance.d.ts +25 -0
  111. package/dist/render/useModelInstance.js +43 -0
  112. package/dist/scatter/scatterModels.d.ts +3 -2
  113. package/dist/scatter/scatterModels.js +15 -7
  114. package/dist/scene/AuthoredScene.d.ts +14 -6
  115. package/dist/scene/AuthoredScene.js +30 -9
  116. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  117. package/dist/scene/GrassFieldRenderer.js +20 -2
  118. package/dist/settings/SettingsMenu.js +11 -18
  119. package/dist/settings/SettingsRuntime.d.ts +5 -0
  120. package/dist/settings/SettingsRuntime.js +29 -2
  121. package/dist/shellControlSuspension.d.ts +18 -0
  122. package/dist/shellControlSuspension.js +28 -0
  123. package/dist/shellInputPublication.d.ts +28 -0
  124. package/dist/shellInputPublication.js +49 -0
  125. package/dist/shellPointerInput.d.ts +18 -0
  126. package/dist/shellPointerInput.js +49 -0
  127. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  128. package/dist/structures/BuildingKitBatch.js +34 -59
  129. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  130. package/dist/structures/BuildingRenderBudget.js +54 -0
  131. package/dist/structures/GeneratedBuilding.js +13 -8
  132. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  133. package/dist/structures/buildingKitMaterials.js +70 -0
  134. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  135. package/dist/structures/buildingSpatialBatch.js +45 -0
  136. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  137. package/dist/terrain/CarvedTerrain.js +2 -2
  138. package/dist/terrain/GrassField.d.ts +3 -1
  139. package/dist/terrain/GrassField.js +61 -6
  140. package/dist/terrain/grassGeometry.d.ts +13 -0
  141. package/dist/terrain/grassGeometry.js +57 -1
  142. package/dist/terrain/grassMaterial.js +2 -2
  143. package/dist/terrain/index.d.ts +1 -0
  144. package/dist/terrain/index.js +1 -0
  145. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  146. package/dist/terrain/terrainDetailMaterial.js +7 -7
  147. package/dist/terrain/terrainMath.d.ts +2 -0
  148. package/dist/terrain/terrainMath.js +2 -0
  149. package/dist/terrain/terrainPaintColor.d.ts +11 -0
  150. package/dist/terrain/terrainPaintColor.js +47 -0
  151. package/dist/touch/TouchControlsOverlay.js +4 -4
  152. package/dist/useShellMultiplayerSync.js +4 -0
  153. package/dist/vfx/ParticleField.d.ts +23 -20
  154. package/dist/vfx/ParticleField.js +168 -50
  155. package/dist/visibility/CullingProvider.js +2 -62
  156. package/dist/visibility/cullingDriver.d.ts +9 -0
  157. package/dist/visibility/cullingDriver.js +68 -0
  158. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  159. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  160. package/dist/weather/DustField.d.ts +2 -1
  161. package/dist/weather/DustField.js +17 -3
  162. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  163. package/dist/weather/FireSpreadLayer.js +5 -11
  164. package/dist/weather/LightningStrike.d.ts +4 -1
  165. package/dist/weather/LightningStrike.js +13 -17
  166. package/dist/weather/RainField.d.ts +2 -1
  167. package/dist/weather/RainField.js +17 -3
  168. package/dist/weather/RainImpactField.d.ts +16 -0
  169. package/dist/weather/RainImpactField.js +89 -0
  170. package/dist/weather/SnowField.d.ts +2 -1
  171. package/dist/weather/SnowField.js +17 -3
  172. package/dist/weather/WeatherLayer.d.ts +11 -2
  173. package/dist/weather/WeatherLayer.js +30 -6
  174. package/dist/weather/index.d.ts +2 -0
  175. package/dist/weather/index.js +2 -0
  176. package/dist/weather/weatherMath.d.ts +2 -0
  177. package/dist/weather/weatherMath.js +8 -0
  178. package/dist/weather/weatherShelter.d.ts +18 -0
  179. package/dist/weather/weatherShelter.js +75 -0
  180. package/dist/weather/weatherUniforms.d.ts +40 -1
  181. package/dist/weather/weatherUniforms.js +29 -9
  182. package/dist/world/AuthoredFlocks.d.ts +22 -0
  183. package/dist/world/AuthoredFlocks.js +87 -0
  184. package/dist/world/StaticObjectModels.d.ts +8 -0
  185. package/dist/world/StaticObjectModels.js +75 -0
  186. package/dist/world/WorldHud.d.ts +14 -6
  187. package/dist/world/WorldHud.js +25 -12
  188. package/dist/world/WorldParticles.d.ts +10 -23
  189. package/dist/world/WorldParticles.js +209 -61
  190. package/dist/world/WorldScene.d.ts +3 -1
  191. package/dist/world/WorldScene.js +41 -26
  192. package/dist/world/staticObjectBatches.d.ts +35 -0
  193. package/dist/world/staticObjectBatches.js +163 -0
  194. package/dist/world/worldBarSamples.d.ts +37 -33
  195. package/dist/world/worldBarSamples.js +117 -75
  196. package/package.json +4 -4
  197. package/skills/ce-handoff/SKILL.md +1 -1
  198. package/skills/game-design/SKILL.md +2 -0
  199. package/skills/jgengine/SKILL.md +23 -2
  200. package/skills/jgengine/api.md +28 -17
  201. package/skills/jgengine/recipes/minimal-game.md +7 -2
  202. package/skills/jgengine-ui/SKILL.md +3 -1
  203. package/skills/jgengine-ui/api.md +219 -70
  204. package/skills/jgengine-ui/capabilities.md +119 -1
  205. package/skills/jgengine-ui/reference-primitives.md +93 -3
  206. package/skills/jgengine-ui/reference.md +2 -0
  207. package/skills/jgengine-world/SKILL.md +12 -3
  208. package/skills/jgengine-world/api.md +201 -58
  209. package/skills/jgengine-world/capabilities.md +116 -3
  210. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  211. package/skills/jgengine-world/recipes/controls.md +4 -0
  212. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  213. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  214. package/skills/jgengine-world/reference.md +167 -5
  215. package/skills/jgengine-world/visibility.md +28 -1
@@ -24,6 +24,7 @@ export function resolveGrassBladeGeometryOptions(options = {}) {
24
24
  edgeFeather: Math.max(0, options.edgeFeather ?? 1.5),
25
25
  exclude: options.exclude ?? [],
26
26
  heightAt: options.heightAt ?? (() => 0),
27
+ coverageAt: options.coverageAt ?? (() => 1),
27
28
  };
28
29
  }
29
30
  /** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
@@ -123,7 +124,7 @@ export function createGrassBladeGeometry(options = {}) {
123
124
  const d = Math.max(Math.abs(x - zone.center[0]) - zone.half[0], Math.abs(z - zone.center[1]) - zone.half[1]);
124
125
  heightScale *= Math.min(1, Math.max(0, d / Math.max(0.001, zone.feather ?? 1.5)));
125
126
  }
126
- heights[index] = heightScale;
127
+ heights[index] = heightScale * Math.max(0, Math.min(1, resolved.coverageAt(x, z)));
127
128
  widths[index] = randomRange(random, resolved.width);
128
129
  bend[index] = randomRange(random, resolved.bend);
129
130
  phase[index] = random() * Math.PI * 2;
@@ -140,3 +141,58 @@ export function createGrassBladeGeometry(options = {}) {
140
141
  geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.max(size.width, size.depth) * 0.75 + resolved.height[1]);
141
142
  return geometry;
142
143
  }
144
+ /** Partition one existing seeded bake into at most 64 draws without resampling any tuft. @internal */
145
+ export function createGrassGeometryChunks(options = {}) {
146
+ const source = createGrassBladeGeometry(options);
147
+ const size = resolveTerrainSize(options.area ?? 40);
148
+ const columns = Math.max(1, Math.min(8, Math.ceil(size.width / 16)));
149
+ const rows = Math.max(1, Math.min(8, Math.ceil(size.depth / 16)));
150
+ const buckets = Array.from({ length: columns * rows }, () => []);
151
+ const offsets = source.getAttribute("instanceOffset");
152
+ if (source.instanceCount === 0)
153
+ return [{ geometry: source, indices: new Uint32Array(), roots: new THREE.Box3(), bounds: new THREE.Box3(), bladeExtent: 0 }];
154
+ for (let i = 0; i < source.instanceCount; i++) {
155
+ const x = Math.max(0, Math.min(columns - 1, Math.floor(size.width > 0 ? (offsets.getX(i) / size.width + 0.5) * columns : 0)));
156
+ const z = Math.max(0, Math.min(rows - 1, Math.floor(size.depth > 0 ? (offsets.getZ(i) / size.depth + 0.5) * rows : 0)));
157
+ buckets[z * columns + x].push(i);
158
+ }
159
+ const vary = source.getAttribute("bladeVary");
160
+ const bendScale = source.getAttribute("bladeBendScale");
161
+ const bladeOffset = source.getAttribute("bladeOffset");
162
+ let heightScale = 0, widthScale = 0, bendFactor = 0, radius = 0;
163
+ for (let i = 0; i < vary.count; i++) {
164
+ heightScale = Math.max(heightScale, Math.abs(vary.getY(i)));
165
+ widthScale = Math.max(widthScale, Math.abs(vary.getZ(i)));
166
+ bendFactor = Math.max(bendFactor, Math.abs(bendScale.getX(i)));
167
+ radius = Math.max(radius, Math.hypot(bladeOffset.getX(i), bladeOffset.getY(i)));
168
+ }
169
+ const point = new THREE.Vector3();
170
+ const chunks = buckets.filter((indices) => indices.length > 0).map((indices) => {
171
+ const geometry = new THREE.InstancedBufferGeometry();
172
+ geometry.setIndex(source.index);
173
+ for (const [name, attribute] of Object.entries(source.attributes)) {
174
+ if (!(attribute instanceof THREE.InstancedBufferAttribute)) {
175
+ geometry.setAttribute(name, attribute);
176
+ continue;
177
+ }
178
+ const values = new Float32Array(indices.length * attribute.itemSize);
179
+ for (let i = 0; i < indices.length; i++) {
180
+ values.set(attribute.array.subarray(indices[i] * attribute.itemSize, (indices[i] + 1) * attribute.itemSize), i * attribute.itemSize);
181
+ }
182
+ geometry.setAttribute(name, new THREE.InstancedBufferAttribute(values, attribute.itemSize));
183
+ }
184
+ const roots = new THREE.Box3();
185
+ let extent = radius;
186
+ for (const i of indices) {
187
+ roots.expandByPoint(point.set(offsets.getX(i), offsets.getY(i), offsets.getZ(i)));
188
+ const height = Math.abs(source.getAttribute("instanceHeight").getX(i)) * heightScale;
189
+ const bend = Math.abs(source.getAttribute("instanceBend").getX(i)) * bendFactor;
190
+ const width = Math.abs(source.getAttribute("instanceWidth").getX(i)) * widthScale * 1.2;
191
+ extent = Math.max(extent, radius + width + height * (1 + bend * 1.18));
192
+ }
193
+ geometry.instanceCount = indices.length;
194
+ return { geometry, indices: Uint32Array.from(indices), roots, bounds: roots.clone(), bladeExtent: extent };
195
+ });
196
+ source.dispose();
197
+ return chunks;
198
+ }
@@ -111,7 +111,7 @@ void jgComputeGrassBlade() {
111
111
  // triangle blades leave the ground visible between tufts and read as stubble.
112
112
  float taper = (1.0 - 0.3 * t) * (1.0 - smoothstep(0.5, 1.0, t) * 0.95);
113
113
  // Surviving distant tufts widen to hold silhouette coverage as neighbors thin out.
114
- float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4);
114
+ float width = instanceWidth * bladeVary.z * taper * (1.0 + fadeT * 1.4) * keep;
115
115
  float arc = instanceBend * bladeBendScale * t * t;
116
116
  vec3 localPosition = vec3(side * width, heightScale * t - arc * heightScale * 0.18, arc * heightScale);
117
117
  vec3 localNormal = normalize(vec3(0.0, -instanceBend * bladeBendScale * t, 1.0));
@@ -134,7 +134,7 @@ ${wind.layered
134
134
  + sin(gustPhase * 1.7 + crossPhase * 0.4 + 4.3) * 0.13;`
135
135
  : ` float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;`}
136
136
  float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
137
- vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
137
+ vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y) * keep;
138
138
  jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
139
139
  // Lifting normals toward straight-up lights the field like a continuous meadow surface —
140
140
  // side-lit individual blade planes read as dark stubble, not turf.
@@ -9,3 +9,4 @@ export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMa
9
9
  export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
10
10
  export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainSurfaceColorOptions, type TerrainVertexColorOptions, } from "./terrainMath.js";
11
11
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
12
+ export { createTerrainPaintColorSampler, type TerrainPaintColorSampler, type TerrainPaintSource } from "./terrainPaintColor.js";
@@ -9,3 +9,4 @@ export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./gr
9
9
  export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
10
10
  export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
11
11
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
12
+ export { createTerrainPaintColorSampler } from "./terrainPaintColor.js";
@@ -7,13 +7,13 @@ export interface TerrainDetailMaterialHandle {
7
7
  /**
8
8
  * Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller
9
9
  * (a React component, via `useTexture`/`useLoader`) owns loading and disposal; `core` never touches
10
- * `THREE.Texture` and this shell function never fetches a URL itself.
10
+ * `THREE.Texture` and this shell function never fetches a URL itself. Omitted AO/roughness skip sampling.
11
11
  */
12
12
  export interface TerrainDetailMaterialTextures {
13
13
  color: THREE.Texture;
14
14
  normal: THREE.Texture;
15
- roughness: THREE.Texture;
16
- ao: THREE.Texture;
15
+ roughness?: THREE.Texture;
16
+ ao?: THREE.Texture;
17
17
  }
18
18
  /**
19
19
  * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
@@ -60,8 +60,8 @@ export function createTerrainDetailMaterial(detail, textures) {
60
60
  ? {
61
61
  uMatColor: { value: textures.color },
62
62
  uMatNormal: { value: textures.normal },
63
- uMatRoughness: { value: textures.roughness },
64
- uMatAo: { value: textures.ao },
63
+ ...(textures.roughness !== undefined ? { uMatRoughness: { value: textures.roughness } } : {}),
64
+ ...(textures.ao !== undefined ? { uMatAo: { value: textures.ao } } : {}),
65
65
  uMatRepeat: { value: Math.max(0.01, repeat) },
66
66
  uMatStrength: { value: strength },
67
67
  uMatTint: { value: tint },
@@ -88,8 +88,8 @@ vJgWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
88
88
  ? `
89
89
  uniform sampler2D uMatColor;
90
90
  uniform sampler2D uMatNormal;
91
- uniform sampler2D uMatRoughness;
92
- uniform sampler2D uMatAo;
91
+ ${textures.roughness !== undefined ? "uniform sampler2D uMatRoughness;" : ""}
92
+ ${textures.ao !== undefined ? "uniform sampler2D uMatAo;" : ""}
93
93
  uniform float uMatRepeat;
94
94
  uniform float uMatStrength;
95
95
  uniform float uMatTint;`
@@ -106,7 +106,7 @@ float jgMatLum = dot(jgMatColor, vec3(0.2126, 0.7152, 0.0722));
106
106
  jgCol *= mix(1.0, clamp(jgMatLum / 0.35, 0.55, 1.7), uMatStrength * (1.0 - uMatTint));
107
107
  jgCol = mix(jgCol, jgMatColor, uMatStrength * uMatTint);`
108
108
  : "";
109
- const materialRoughnessGlsl = hasTexture
109
+ const materialRoughnessGlsl = hasTexture && textures.roughness !== undefined
110
110
  ? `
111
111
  float jgMatRough = texture2D(uMatRoughness, jgMatUv).g;
112
112
  roughnessFactor = mix(roughnessFactor, jgMatRough, uMatStrength);`
@@ -116,7 +116,7 @@ roughnessFactor = mix(roughnessFactor, jgMatRough, uMatStrength);`
116
116
  vec2 jgMatNormalXy = texture2D(uMatNormal, jgMatUv).rg * 2.0 - 1.0;
117
117
  normal = normalize(normal + vec3(jgMatNormalXy.x, 0.0, jgMatNormalXy.y) * uMatStrength * 0.6);`
118
118
  : "";
119
- const materialAoGlsl = hasTexture
119
+ const materialAoGlsl = hasTexture && textures.ao !== undefined
120
120
  ? `
121
121
  float jgMatAo = texture2D(uMatAo, jgMatUv).r;
122
122
  reflectedLight.indirectDiffuse *= mix(1.0, jgMatAo, uMatStrength);`
@@ -178,6 +178,6 @@ ${materialNormalGlsl}`)
178
178
  .replace("#include <aomap_fragment>", `#include <aomap_fragment>
179
179
  ${materialAoGlsl}`);
180
180
  };
181
- material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}-${hasTexture ? "tex" : "flat"}`;
181
+ material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}-${hasTexture ? "tex" : "flat"}-${hasTexture && textures.roughness !== undefined ? "rough" : "scalar"}-${hasTexture && textures.ao !== undefined ? "ao" : "unoccluded"}`;
182
182
  return { material };
183
183
  }
@@ -82,6 +82,8 @@ export interface FieldGroundOptions {
82
82
  paletteAt?: TerrainPaletteSampler;
83
83
  /** Slope/noise surface shading over the height lerp. Omit for the default terrain look; pass `{ slopeStrength: 0, mottleStrength: 0 }` for a flat gradient. */
84
84
  surface?: TerrainSurfaceColorOptions;
85
+ /** Authored paint overrides descriptor shading at painted vertices. */
86
+ paintColorAt?: (x: number, z: number, out: THREE.Color) => boolean;
85
87
  }
86
88
  /**
87
89
  * Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
@@ -106,6 +106,7 @@ export function createFieldGroundGeometry(field, options = {}) {
106
106
  heightRange: options.heightRange,
107
107
  paletteAt: options.paletteAt,
108
108
  surface: options.surface,
109
+ paintColorAt: options.paintColorAt,
109
110
  });
110
111
  }
111
112
  function buildGroundGeometry(size, segments, sampler, opts) {
@@ -162,6 +163,7 @@ function buildGroundGeometry(size, segments, sampler, opts) {
162
163
  slope01 = grad / (grad + 1);
163
164
  }
164
165
  applyTerrainSurfaceShading(color, x, z, slope01, surface);
166
+ opts.paintColorAt?.(x, z, color);
165
167
  colorValues[colorIndex] = color.r;
166
168
  colorValues[colorIndex + 1] = color.g;
167
169
  colorValues[colorIndex + 2] = color.b;
@@ -0,0 +1,11 @@
1
+ import * as THREE from "three";
2
+ import type { EditableTerrain } from "@jgengine/core/world/terraform";
3
+ /** Live authored paint channels needed to sample a terrain's material stack without its heightfield. */
4
+ export type TerrainPaintSource = Pick<EditableTerrain, "layers" | "weightsAt" | "surfaceAt">;
5
+ /** Writes authored paint into an existing linear color; returns false for unpainted ground. */
6
+ export type TerrainPaintColorSampler = (x: number, z: number, out: THREE.Color) => boolean;
7
+ /**
8
+ * Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
9
+ * @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
10
+ */
11
+ export declare function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler;
@@ -0,0 +1,47 @@
1
+ import * as THREE from "three";
2
+ import { TERRAIN_MATERIAL_COLORS } from "@jgengine/core/editor/terrainMaterials";
3
+ /**
4
+ * Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
5
+ * @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
6
+ */
7
+ export function createTerrainPaintColorSampler(terrain) {
8
+ const layers = terrain.layers;
9
+ const colors = layers.map(layer => new THREE.Color(layer.tint ?? TERRAIN_MATERIAL_COLORS[layer.surface] ?? "#7c7f86"));
10
+ const defaults = new Map(Object.entries(TERRAIN_MATERIAL_COLORS).map(([id, color]) => [id, new THREE.Color(color)]));
11
+ return (x, z, out) => {
12
+ const weights = terrain.weightsAt(x, z);
13
+ if (layers.length > 0 && weights.length === layers.length) {
14
+ let total = 0, r = 0, g = 0, b = 0;
15
+ for (let i = 0; i < layers.length; i++) {
16
+ const weight = weights[i] * (layers[i].opacity ?? 1);
17
+ if (weight <= 0)
18
+ continue;
19
+ const color = colors[i];
20
+ r += color.r * weight;
21
+ g += color.g * weight;
22
+ b += color.b * weight;
23
+ total += weight;
24
+ }
25
+ if (total > 0) {
26
+ out.setRGB(r / total, g / total, b / total);
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ const surface = terrain.surfaceAt(x, z);
32
+ if (surface === null)
33
+ return false;
34
+ const index = layers.findIndex(layer => layer.surface === surface);
35
+ if (index >= 0) {
36
+ if ((layers[index].opacity ?? 1) <= 0)
37
+ return false;
38
+ out.copy(colors[index]);
39
+ return true;
40
+ }
41
+ const color = defaults.get(surface);
42
+ if (color === undefined)
43
+ return false;
44
+ out.copy(color);
45
+ return true;
46
+ };
47
+ }
@@ -548,8 +548,8 @@ function anchorInset(anchor) {
548
548
  }
549
549
  return style;
550
550
  }
551
- function AnchoredSlot({ anchor, children, slotRef, }) {
552
- return (_jsx("div", { className: "pointer-events-none absolute z-40", style: { ...ANCHOR_POSITION[anchor], ...anchorInset(anchor) }, children: _jsx("div", { ref: slotRef, children: children }) }));
551
+ function AnchoredSlot({ anchor, children, slotRef, movement = false, }) {
552
+ return (_jsx("div", { className: "pointer-events-none absolute", style: { zIndex: movement ? 10 : 40, ...ANCHOR_POSITION[anchor], ...anchorInset(anchor) }, children: _jsx("div", { ref: slotRef, children: children }) }));
553
553
  }
554
554
  function bucketByAnchor(buttons) {
555
555
  const map = new Map();
@@ -580,8 +580,8 @@ export function TouchControlsDock({ scheme, sink, style, scale = 1, joystickVari
580
580
  useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
581
581
  const actionsColumn = layout.actions === "left" || layout.actions === "right";
582
582
  const utilityColumn = layout.utility === "left" || layout.utility === "right";
583
- return (_jsxs(ResolvedLayout, { children: [scheme.joystick !== null ? (_jsx(AnchoredSlot, { anchor: layout.movement, slotRef: joystickRef, children: joystickVariant === "floating" ? (_jsx(FloatingJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale })) : (_jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale })) })) : null, primary.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.actions, slotRef: actionsRef, children: ARC_ANCHORS.has(layout.actions) ? (_jsx(ThumbArcCluster, { buttons: primary, sink: sink, tokens: tokens, scale: scale })) : (_jsx(StackCluster, { buttons: primary, utility: [], sink: sink, tokens: tokens, column: actionsColumn, scale: scale })) })) : null, utility.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.utility, slotRef: utilityRef, children: _jsx(StackCluster, { buttons: [], utility: utility, sink: sink, tokens: tokens, column: utilityColumn, scale: scale }) })) : null, [...extras.entries()].map(([anchor, bucket]) => (_jsx(AnchoredSlot, { anchor: anchor, children: _jsx(StackCluster, { buttons: bucket.primary, utility: bucket.utility, sink: sink, tokens: tokens, column: anchor === "left" || anchor === "right", scale: scale }) }, anchor)))] }));
583
+ return (_jsxs(ResolvedLayout, { children: [scheme.joystick !== null ? (_jsx(AnchoredSlot, { anchor: layout.movement, slotRef: joystickRef, movement: true, children: joystickVariant === "floating" ? (_jsx(FloatingJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale })) : (_jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale })) })) : null, primary.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.actions, slotRef: actionsRef, children: ARC_ANCHORS.has(layout.actions) ? (_jsx(ThumbArcCluster, { buttons: primary, sink: sink, tokens: tokens, scale: scale })) : (_jsx(StackCluster, { buttons: primary, utility: [], sink: sink, tokens: tokens, column: actionsColumn, scale: scale })) })) : null, utility.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.utility, slotRef: utilityRef, children: _jsx(StackCluster, { buttons: [], utility: utility, sink: sink, tokens: tokens, column: utilityColumn, scale: scale }) })) : null, [...extras.entries()].map(([anchor, bucket]) => (_jsx(AnchoredSlot, { anchor: anchor, children: _jsx(StackCluster, { buttons: bucket.primary, utility: bucket.utility, sink: sink, tokens: tokens, column: anchor === "left" || anchor === "right", scale: scale }) }, anchor)))] }));
584
584
  }
585
585
  function ResolvedLayout({ children }) {
586
- return _jsx("div", { className: "pointer-events-none absolute inset-0 z-40", children: children });
586
+ return _jsx("div", { className: "pointer-events-none absolute inset-0", children: children });
587
587
  }
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState, useCallback } from "react";
2
2
  import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
3
+ import { bindCommandTransport } from "@jgengine/core/runtime/commandDispatch";
3
4
  import { createTransportSessionId } from "@jgengine/core/runtime/transport";
4
5
  import { attachWorldSync } from "./worldSync.js";
5
6
  /** Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. */
@@ -15,6 +16,9 @@ export function useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef,
15
16
  setFailureReason(null);
16
17
  let disposed = false;
17
18
  const cleanups = [];
19
+ if (isServerAuthoritative(playable.game.multiplayer)) {
20
+ cleanups.push(bindCommandTransport(ctx, { transport: multiplayer.backend.transport, serverId: () => serverIdRef.current }));
21
+ }
18
22
  const sessionId = createTransportSessionId();
19
23
  void multiplayer.backend.transport
20
24
  .joinServer({ gameId: multiplayer.gameId, sessionId })
@@ -1,32 +1,35 @@
1
1
  import { type ReactElement } from "react";
2
2
  import type { ParticleSystem } from "@jgengine/core/vfx/particles";
3
- /** How particle fragments composite: `additive` for fire/sparks/magic glow, `normal` for smoke/dust. */
3
+ import type { ParticleRenderHint } from "@jgengine/core/vfx/particleDirector";
4
+ /** How particle fragments composite on screen. */
4
5
  export type ParticleBlending = "additive" | "normal";
5
- /** Props for {@link ParticleField}. */
6
+ /** @internal Mutable resource/upload counters read through the shell's supported diagnostics probe. */
7
+ export interface ParticleFieldMetrics {
8
+ fieldsMounted: number;
9
+ fieldsUnmounted: number;
10
+ liveFields: number;
11
+ attributeBytes: number;
12
+ totalUploadMs: number;
13
+ maxUploadMs: number;
14
+ uploadSamples: number;
15
+ }
16
+ /** Props for the existing pooled particle renderer. */
6
17
  export interface ParticleFieldProps {
7
- /** The core simulation to render (from `createParticleSystem`). */
8
18
  system: ParticleSystem;
9
- /**
10
- * Advance the simulation each frame with the render delta. Leave `true` for a
11
- * fire-and-forget effect; set `false` when your game loop already ticks the
12
- * system (so it advances exactly once per fixed step).
13
- */
19
+ /** Disable when an external loop already advances this system. Default true. */
14
20
  advance?: boolean;
15
- /** Compositing mode. Default `additive` (glowy). */
16
21
  blending?: ParticleBlending;
17
- /** Pixel-size multiplier applied to every particle's world radius. Default 300. */
22
+ /** Pixel multiplier for point output. Quad outputs use particle sizes in world units. */
18
23
  scale?: number;
19
- /** Write depth (occludes particles behind geometry). Default `false` so a glow layer never self-occludes. */
20
24
  depthWrite?: boolean;
25
+ /** Point, instanced sprite, velocity streak, connected ribbon, or ground ripple output. */
26
+ render?: ParticleRenderHint;
27
+ /** @internal Optional diagnostics; omitted in production. */
28
+ metrics?: ParticleFieldMetrics;
21
29
  }
22
30
  /**
23
- * Renders a core `ParticleSystem` as a GPU point cloud: one draw call, per-particle
24
- * size / color / alpha via a soft-round shader, and a `drawRange` clamped to the live
25
- * count so dead particles cost nothing. The simulation stays engine-side and
26
- * genre-agnostic; this is purely how it reaches the screen. By default it advances
27
- * the sim with the frame delta — pass `advance={false}` to drive it from your own
28
- * fixed loop instead.
29
- *
30
- * @capability particle-field render a core particle system as a soft-point GPU cloud with per-particle size, color, and alpha
31
+ * Render one pooled simulation through a point cloud or instanced quads. Output
32
+ * selection does not replace spawn, forces, collisions, or deterministic state.
33
+ * @capability particle-field render a core particle pool as soft points, velocity streaks, connected ribbons, flakes, flames, smoke sprites, or ground ripples
31
34
  */
32
- export declare function ParticleField({ system, advance, blending, scale, depthWrite, }: ParticleFieldProps): ReactElement;
35
+ export declare function ParticleField({ system, advance, blending, scale, depthWrite, render, metrics }: ParticleFieldProps): ReactElement;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useFrame, useThree } from "@react-three/fiber";
3
- import { useEffect, useMemo, useRef } from "react";
3
+ import { useEffect, useMemo } from "react";
4
4
  import * as THREE from "three";
5
5
  const VERTEX_SHADER = /* glsl */ `
6
6
  attribute float aSize;
@@ -21,71 +21,189 @@ const FRAGMENT_SHADER = /* glsl */ `
21
21
  varying float vAlpha;
22
22
  varying vec3 vColor;
23
23
  void main() {
24
- // Soft round sprite: radial falloff from the point center, discard the corners.
25
- vec2 d = gl_PointCoord - vec2(0.5);
26
- float r = length(d);
24
+ float r = length(gl_PointCoord - vec2(0.5));
27
25
  if (r > 0.5) discard;
28
- float soft = smoothstep(0.5, 0.0, r);
29
- gl_FragColor = vec4(vColor, vAlpha * soft);
26
+ gl_FragColor = vec4(vColor, vAlpha * smoothstep(0.5, 0.0, r));
30
27
  }
31
28
  `;
29
+ const QUAD_VERTEX = /* glsl */ `
30
+ attribute vec3 aCenter;
31
+ attribute vec3 aEnd;
32
+ attribute vec3 aVelocity;
33
+ attribute float aId;
34
+ attribute float aSize;
35
+ attribute float aAlpha;
36
+ attribute vec3 aColor;
37
+ uniform float uShape;
38
+ uniform float uStretch;
39
+ varying vec2 vUv;
40
+ varying float vAlpha;
41
+ varying vec3 vColor;
42
+ void main() {
43
+ vUv = uv;
44
+ vAlpha = aAlpha;
45
+ vColor = aColor;
46
+ vec4 center = modelViewMatrix * vec4(aCenter, 1.0);
47
+ vec4 view = center;
48
+ if (uShape == 1.0 || uShape == 5.0) {
49
+ vec3 end = uShape == 5.0 ? aEnd : aCenter - aVelocity * uStretch;
50
+ vec4 tail = modelViewMatrix * vec4(end, 1.0);
51
+ vec2 delta = center.xy - tail.xy;
52
+ float len = length(delta);
53
+ vec2 side = len > 0.00001 ? vec2(-delta.y, delta.x) / len : vec2(1.0, 0.0);
54
+ view = mix(tail, center, uv.y);
55
+ view.xy += side * position.x * aSize;
56
+ } else if (uShape == 6.0) {
57
+ view = modelViewMatrix * vec4(aCenter + vec3(position.x, 0.002, position.y) * aSize * 2.0, 1.0);
58
+ } else {
59
+ vec2 corner = position.xy * aSize;
60
+ if (uShape == 2.0) {
61
+ float angle = aId * 2.399963;
62
+ corner = mat2(cos(angle), -sin(angle), sin(angle), cos(angle)) * corner;
63
+ }
64
+ if (uShape == 3.0) {
65
+ corner.y *= 2.7;
66
+ corner.x += sin(aId * 1.7 + uv.y * 7.0) * aSize * 0.14 * uv.y;
67
+ }
68
+ if (uShape == 4.0) corner *= 1.65;
69
+ view.xy += corner;
70
+ }
71
+ gl_Position = projectionMatrix * view;
72
+ }
73
+ `;
74
+ const QUAD_FRAGMENT = /* glsl */ `
75
+ uniform float uShape;
76
+ varying vec2 vUv;
77
+ varying float vAlpha;
78
+ varying vec3 vColor;
79
+ void main() {
80
+ vec2 d = vUv - 0.5;
81
+ float alpha;
82
+ if (uShape == 1.0 || uShape == 5.0) {
83
+ alpha = smoothstep(0.5, 0.0, abs(d.x)) * smoothstep(0.0, 0.12, vUv.y) * smoothstep(1.0, 0.7, vUv.y);
84
+ } else if (uShape == 2.0) {
85
+ float angle = atan(d.y, d.x);
86
+ float edge = 0.26 + 0.12 * pow(abs(cos(angle * 3.0)), 4.0);
87
+ alpha = smoothstep(edge, edge * 0.62, length(d));
88
+ } else if (uShape == 3.0) {
89
+ float width = (1.0 - vUv.y) * 0.42 + 0.04;
90
+ alpha = smoothstep(width, 0.0, abs(d.x)) * smoothstep(0.0, 0.18, vUv.y) * smoothstep(1.0, 0.5, vUv.y);
91
+ } else if (uShape == 6.0) {
92
+ float r = length(d);
93
+ alpha = smoothstep(0.07, 0.0, abs(r - 0.38));
94
+ } else {
95
+ float r = length(d);
96
+ alpha = smoothstep(0.5, 0.08, r);
97
+ if (uShape == 4.0) alpha *= 0.65 + 0.35 * sin(vUv.x * 15.0 + sin(vUv.y * 12.0));
98
+ }
99
+ alpha *= vAlpha;
100
+ if (alpha < 0.001) discard;
101
+ gl_FragColor = vec4(vColor, alpha);
102
+ }
103
+ `;
104
+ const SHAPES = { point: 0, streak: 1, flake: 2, flame: 3, smoke: 4, ribbon: 5, ripple: 6 };
32
105
  /**
33
- * Renders a core `ParticleSystem` as a GPU point cloud: one draw call, per-particle
34
- * size / color / alpha via a soft-round shader, and a `drawRange` clamped to the live
35
- * count so dead particles cost nothing. The simulation stays engine-side and
36
- * genre-agnostic; this is purely how it reaches the screen. By default it advances
37
- * the sim with the frame delta — pass `advance={false}` to drive it from your own
38
- * fixed loop instead.
39
- *
40
- * @capability particle-field render a core particle system as a soft-point GPU cloud with per-particle size, color, and alpha
106
+ * Render one pooled simulation through a point cloud or instanced quads. Output
107
+ * selection does not replace spawn, forces, collisions, or deterministic state.
108
+ * @capability particle-field render a core particle pool as soft points, velocity streaks, connected ribbons, flakes, flames, smoke sprites, or ground ripples
41
109
  */
42
- export function ParticleField({ system, advance = true, blending = "additive", scale = 300, depthWrite = false, }) {
43
- const pointsRef = useRef(null);
110
+ export function ParticleField({ system, advance = true, blending = "additive", scale = 300, depthWrite = false, render, metrics }) {
44
111
  const dpr = useThree((state) => state.viewport.dpr);
45
- const { geometry, material } = useMemo(() => {
46
- const initial = system.buffers();
47
- const max = initial.positions.length / 3;
48
- const geo = new THREE.BufferGeometry();
49
- geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
50
- geo.setAttribute("aSize", new THREE.BufferAttribute(new Float32Array(max), 1));
51
- geo.setAttribute("aColor", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
52
- geo.setAttribute("aAlpha", new THREE.BufferAttribute(new Float32Array(max), 1));
53
- geo.setDrawRange(0, 0);
54
- const mat = new THREE.ShaderMaterial({
55
- vertexShader: VERTEX_SHADER,
56
- fragmentShader: FRAGMENT_SHADER,
57
- uniforms: { uScale: { value: scale } },
58
- transparent: true,
59
- depthWrite,
60
- blending: blending === "additive" ? THREE.AdditiveBlending : THREE.NormalBlending,
61
- });
62
- return { geometry: geo, material: mat };
63
- }, [system, scale, depthWrite, blending]);
112
+ const shape = render?.shape ?? "point";
113
+ const quad = shape !== "point";
114
+ const { geometry, material, indexById } = useMemo(() => {
115
+ const max = system.buffers().positions.length / 3;
116
+ const geo = quad ? new THREE.InstancedBufferGeometry() : new THREE.BufferGeometry();
117
+ if (quad) {
118
+ geo.setAttribute("position", new THREE.Float32BufferAttribute([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0], 3));
119
+ geo.setAttribute("uv", new THREE.Float32BufferAttribute([0, 0, 1, 0, 1, 1, 0, 1], 2));
120
+ geo.setIndex([0, 1, 2, 0, 2, 3]);
121
+ geo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(new Float32Array(max * 3), 3));
122
+ geo.setAttribute("aEnd", new THREE.InstancedBufferAttribute(new Float32Array(max * 3), 3));
123
+ geo.setAttribute("aVelocity", new THREE.InstancedBufferAttribute(new Float32Array(max * 3), 3));
124
+ geo.setAttribute("aId", new THREE.InstancedBufferAttribute(new Float32Array(max), 1));
125
+ geo.instanceCount = 0;
126
+ }
127
+ else {
128
+ geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
129
+ geo.setDrawRange(0, 0);
130
+ }
131
+ const attribute = (array, size) => quad ? new THREE.InstancedBufferAttribute(array, size) : new THREE.BufferAttribute(array, size);
132
+ geo.setAttribute("aSize", attribute(new Float32Array(max), 1));
133
+ geo.setAttribute("aColor", attribute(new Float32Array(max * 3), 3));
134
+ geo.setAttribute("aAlpha", attribute(new Float32Array(max), 1));
135
+ const mat = new THREE.ShaderMaterial({ vertexShader: quad ? QUAD_VERTEX : VERTEX_SHADER, fragmentShader: quad ? QUAD_FRAGMENT : FRAGMENT_SHADER,
136
+ uniforms: { uScale: { value: scale }, uShape: { value: SHAPES[shape] }, uStretch: { value: render?.stretch ?? 0.08 } },
137
+ transparent: true, depthWrite, side: THREE.DoubleSide,
138
+ blending: blending === "additive" ? THREE.AdditiveBlending : THREE.NormalBlending });
139
+ return { geometry: geo, material: mat, indexById: new Map() };
140
+ }, [system, quad, shape, scale, depthWrite, blending]);
64
141
  useEffect(() => {
142
+ const bytes = Object.values(geometry.attributes).reduce((total, attribute) => total + attribute.array.byteLength, geometry.index?.array.byteLength ?? 0);
143
+ if (metrics !== undefined) {
144
+ metrics.fieldsMounted++;
145
+ metrics.liveFields++;
146
+ metrics.attributeBytes += bytes;
147
+ }
65
148
  return () => {
66
149
  geometry.dispose();
67
150
  material.dispose();
151
+ if (metrics !== undefined) {
152
+ metrics.fieldsUnmounted++;
153
+ metrics.liveFields--;
154
+ metrics.attributeBytes -= bytes;
155
+ }
68
156
  };
69
- }, [geometry, material]);
157
+ }, [geometry, material, metrics]);
70
158
  useFrame((_, delta) => {
159
+ const started = metrics === undefined ? 0 : performance.now();
71
160
  if (advance)
72
161
  system.update(Math.min(delta, 0.1));
73
162
  const buffers = system.buffers();
74
163
  const count = buffers.count;
75
- const pos = geometry.getAttribute("position");
76
- const size = geometry.getAttribute("aSize");
77
- const color = geometry.getAttribute("aColor");
78
- const alpha = geometry.getAttribute("aAlpha");
79
- pos.array.set(buffers.positions.subarray(0, count * 3));
80
- size.array.set(buffers.sizes.subarray(0, count));
81
- color.array.set(buffers.colors.subarray(0, count * 3));
82
- alpha.array.set(buffers.alphas.subarray(0, count));
83
- pos.needsUpdate = true;
84
- size.needsUpdate = true;
85
- color.needsUpdate = true;
86
- alpha.needsUpdate = true;
87
- geometry.setDrawRange(0, count);
164
+ const upload = (name, values, n) => {
165
+ const attr = geometry.getAttribute(name);
166
+ attr.array.set(values.subarray(0, n));
167
+ attr.needsUpdate = true;
168
+ };
169
+ upload(quad ? "aCenter" : "position", buffers.positions, count * 3);
170
+ upload("aSize", buffers.sizes, count);
171
+ upload("aColor", buffers.colors, count * 3);
172
+ upload("aAlpha", buffers.alphas, count);
173
+ if (quad) {
174
+ upload("aVelocity", buffers.velocities, count * 3);
175
+ const end = geometry.getAttribute("aEnd");
176
+ const ids = geometry.getAttribute("aId");
177
+ if (shape === "ribbon") {
178
+ for (let i = 0; i < count; i++)
179
+ indexById.set(buffers.ids[i], i);
180
+ for (const id of indexById.keys()) {
181
+ const index = indexById.get(id);
182
+ if (index >= count || buffers.ids[index] !== id)
183
+ indexById.delete(id);
184
+ }
185
+ }
186
+ for (let i = 0; i < count; i++) {
187
+ ids.setX(i, buffers.ids[i]);
188
+ const previousIndex = shape === "ribbon" ? indexById.get(buffers.ids[i] - 1) : undefined;
189
+ const source = previousIndex === undefined ? buffers.previousPositions : buffers.positions;
190
+ const offset = (previousIndex ?? i) * 3;
191
+ end.setXYZ(i, source[offset], source[offset + 1], source[offset + 2]);
192
+ }
193
+ end.needsUpdate = true;
194
+ ids.needsUpdate = true;
195
+ geometry.instanceCount = count;
196
+ }
197
+ else
198
+ geometry.setDrawRange(0, count);
88
199
  material.uniforms.uScale.value = scale * dpr;
200
+ material.uniforms.uStretch.value = render?.stretch ?? 0.08;
201
+ if (metrics !== undefined) {
202
+ const elapsed = performance.now() - started;
203
+ metrics.totalUploadMs += elapsed;
204
+ metrics.maxUploadMs = Math.max(metrics.maxUploadMs, elapsed);
205
+ metrics.uploadSamples++;
206
+ }
89
207
  });
90
- return _jsx("points", { ref: pointsRef, geometry: geometry, material: material, frustumCulled: false });
208
+ return quad ? _jsx("mesh", { geometry: geometry, material: material, frustumCulled: false }) : _jsx("points", { geometry: geometry, material: material, frustumCulled: false });
91
209
  }