@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.
- package/CHANGELOG.md +346 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +12 -5
- package/dist/defineGame.js +24 -9
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +3 -0
- package/dist/render/modelLoad.js +9 -0
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +21 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +209 -61
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +22 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +204 -67
- package/skills/jgengine-ui/capabilities.md +115 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +200 -57
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +152 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
3
|
+
import type { ParticleRenderHint } from "@jgengine/core/vfx/particleDirector";
|
|
4
|
+
/** How particle fragments composite on screen. */
|
|
4
5
|
export type ParticleBlending = "additive" | "normal";
|
|
5
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
geo
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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("
|
|
208
|
+
return quad ? _jsx("mesh", { geometry: geometry, material: material, frustumCulled: false }) : _jsx("points", { geometry: geometry, material: material, frustumCulled: false });
|
|
91
209
|
}
|
|
@@ -3,7 +3,7 @@ import { createContext, useContext, useMemo, useRef } from "react";
|
|
|
3
3
|
import { useThree, useFrame } from "@react-three/fiber";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
-
import {
|
|
6
|
+
import { buildCullingDriver } from "./cullingDriver.js";
|
|
7
7
|
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
|
|
8
8
|
const ALWAYS_VISIBLE = () => true;
|
|
9
9
|
const alwaysVisibleRef = { current: ALWAYS_VISIBLE };
|
|
@@ -16,66 +16,6 @@ const CullingContext = createContext(null);
|
|
|
16
16
|
export function useRenderVisibility() {
|
|
17
17
|
return useContext(CullingContext) ?? alwaysVisibleRef;
|
|
18
18
|
}
|
|
19
|
-
const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
|
|
20
|
-
function objectRadius(scale) {
|
|
21
|
-
if (scale === undefined)
|
|
22
|
-
return 1;
|
|
23
|
-
const s = typeof scale === "number" ? scale : Math.max(scale[0], scale[1], scale[2]);
|
|
24
|
-
return Math.max(0.75, s);
|
|
25
|
-
}
|
|
26
|
-
function buildSystem(ctx, config) {
|
|
27
|
-
const versions = new Map();
|
|
28
|
-
let currentView = null;
|
|
29
|
-
const scratch = { id: "", position: [0, 0, 0], version: 0 };
|
|
30
|
-
const objectSpec = { kind: "sphere", radius: 1, offset: [0, 0.5, 0] };
|
|
31
|
-
function bump(id, position, rot) {
|
|
32
|
-
const prev = versions.get(id);
|
|
33
|
-
if (prev !== undefined && prev.x === position[0] && prev.y === position[1] && prev.z === position[2] && prev.rot === rot) {
|
|
34
|
-
return prev.version;
|
|
35
|
-
}
|
|
36
|
-
const version = (prev?.version ?? 0) + 1;
|
|
37
|
-
versions.set(id, { x: position[0], y: position[1], z: position[2], rot, version });
|
|
38
|
-
return version;
|
|
39
|
-
}
|
|
40
|
-
function* renderables() {
|
|
41
|
-
const entityOverrides = config?.entities;
|
|
42
|
-
for (const entity of ctx.scene.entity.list()) {
|
|
43
|
-
scratch.id = entity.id;
|
|
44
|
-
scratch.position = entity.position;
|
|
45
|
-
scratch.version = bump(entity.id, entity.position, entity.rotationY);
|
|
46
|
-
scratch.bounds = ENTITY_BOUNDS;
|
|
47
|
-
scratch.overrides = entityOverrides?.[entity.name];
|
|
48
|
-
scratch.layer = "entity";
|
|
49
|
-
yield scratch;
|
|
50
|
-
}
|
|
51
|
-
const objectOverrides = config?.objects;
|
|
52
|
-
for (const object of ctx.scene.object.list()) {
|
|
53
|
-
scratch.id = object.instanceId;
|
|
54
|
-
scratch.position = object.position;
|
|
55
|
-
scratch.version = bump(object.instanceId, object.position, object.rotationY);
|
|
56
|
-
objectSpec.radius = objectRadius(object.visual?.scale);
|
|
57
|
-
scratch.bounds = objectSpec;
|
|
58
|
-
scratch.overrides = objectOverrides?.[object.catalogId];
|
|
59
|
-
scratch.layer = "object";
|
|
60
|
-
yield scratch;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const cameras = () => (currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }]);
|
|
64
|
-
const options = {
|
|
65
|
-
renderables,
|
|
66
|
-
cameras,
|
|
67
|
-
...(config?.culling !== undefined ? { settings: config.culling } : {}),
|
|
68
|
-
...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
|
|
69
|
-
};
|
|
70
|
-
const system = createVisibilitySystem(options);
|
|
71
|
-
return {
|
|
72
|
-
system,
|
|
73
|
-
setView(view) {
|
|
74
|
-
currentView = view;
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const EMPTY_CAMERAS = [];
|
|
79
19
|
const tmpDir = new THREE.Vector3();
|
|
80
20
|
function viewFromCamera(camera) {
|
|
81
21
|
camera.getWorldDirection(tmpDir);
|
|
@@ -120,7 +60,7 @@ export function CullingProvider({ config, drawDistance, children }) {
|
|
|
120
60
|
return config;
|
|
121
61
|
return { ...config, culling: { ...config?.culling, defaultMaxRenderDistance: config?.culling?.defaultMaxRenderDistance ?? drawDistance } };
|
|
122
62
|
}, [config, drawDistance]);
|
|
123
|
-
const driver = useMemo(() => (enabled ?
|
|
63
|
+
const driver = useMemo(() => (enabled ? buildCullingDriver(ctx, resolvedConfig) : null), [ctx, resolvedConfig, enabled]);
|
|
124
64
|
useFrame(() => {
|
|
125
65
|
if (driver === null) {
|
|
126
66
|
predicateRef.current = ALWAYS_VISIBLE;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { CameraView } from "@jgengine/core/visibility/frustum";
|
|
3
|
+
import type { VisibilityConfig } from "@jgengine/core/visibility/config";
|
|
4
|
+
/** Builds the shell's live scene adapter for headless visibility. @internal */
|
|
5
|
+
export declare function buildCullingDriver(ctx: GameContext, config: VisibilityConfig | undefined): {
|
|
6
|
+
system: import("@jgengine/core/visibility/visibilitySystem").VisibilitySystem;
|
|
7
|
+
setView(view: CameraView | null): void;
|
|
8
|
+
trackedCount(): number;
|
|
9
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
|
|
2
|
+
const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
|
|
3
|
+
const EMPTY_CAMERAS = [];
|
|
4
|
+
function objectRadius(scale) {
|
|
5
|
+
if (scale === undefined)
|
|
6
|
+
return 1;
|
|
7
|
+
const size = typeof scale === "number" ? Math.abs(scale) : Math.max(Math.abs(scale[0]), Math.abs(scale[1]), Math.abs(scale[2]));
|
|
8
|
+
return Math.max(0.75, size);
|
|
9
|
+
}
|
|
10
|
+
/** Builds the shell's live scene adapter for headless visibility. @internal */
|
|
11
|
+
export function buildCullingDriver(ctx, config) {
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
let frame = 0;
|
|
14
|
+
let currentView = null;
|
|
15
|
+
function update(id, position, rotation, radius, overrides, layer) {
|
|
16
|
+
let entry = entries.get(id);
|
|
17
|
+
if (entry === undefined) {
|
|
18
|
+
entry = { renderable: { id, position, version: 0 }, bounds: { kind: "sphere", radius, offset: [0, 0.5, 0] }, x: NaN, y: NaN, z: NaN, rotation: NaN, radius: NaN, seen: frame };
|
|
19
|
+
entries.set(id, entry);
|
|
20
|
+
}
|
|
21
|
+
const previous = entry.renderable;
|
|
22
|
+
const changed = entry.x !== position[0] || entry.y !== position[1] || entry.z !== position[2] || entry.rotation !== rotation || entry.radius !== radius || previous.overrides !== overrides || previous.layer !== layer;
|
|
23
|
+
if (changed) {
|
|
24
|
+
// A retained renderable must belong to one row: the core may keep it for always-visible processing.
|
|
25
|
+
entry.bounds.radius = radius;
|
|
26
|
+
previous.position = position;
|
|
27
|
+
previous.version += 1;
|
|
28
|
+
previous.bounds = layer === "entity" ? ENTITY_BOUNDS : entry.bounds;
|
|
29
|
+
previous.overrides = overrides;
|
|
30
|
+
previous.layer = layer;
|
|
31
|
+
entry.x = position[0];
|
|
32
|
+
entry.y = position[1];
|
|
33
|
+
entry.z = position[2];
|
|
34
|
+
entry.rotation = rotation;
|
|
35
|
+
entry.radius = radius;
|
|
36
|
+
}
|
|
37
|
+
entry.seen = frame;
|
|
38
|
+
return entry.renderable;
|
|
39
|
+
}
|
|
40
|
+
function* renderables() {
|
|
41
|
+
frame += 1;
|
|
42
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
43
|
+
yield update(entity.id, entity.position, entity.rotationY, 2, config?.entities?.[entity.name], "entity");
|
|
44
|
+
}
|
|
45
|
+
for (const object of ctx.scene.object.list()) {
|
|
46
|
+
yield update(object.instanceId, object.position, object.rotationY, objectRadius(object.visual?.scale), config?.objects?.[object.catalogId], "object");
|
|
47
|
+
}
|
|
48
|
+
for (const [id, entry] of entries) {
|
|
49
|
+
if (entry.seen !== frame)
|
|
50
|
+
entries.delete(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const system = createVisibilitySystem({
|
|
54
|
+
renderables,
|
|
55
|
+
cameras: () => currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }],
|
|
56
|
+
...(config?.culling !== undefined ? { settings: config.culling } : {}),
|
|
57
|
+
...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
system,
|
|
61
|
+
setView(view) {
|
|
62
|
+
currentView = view;
|
|
63
|
+
},
|
|
64
|
+
trackedCount() {
|
|
65
|
+
return entries.size;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EditorDocument } from "@jgengine/core/editor/types";
|
|
2
|
+
import { type WeatherLayerProps } from "./WeatherLayer.js";
|
|
3
|
+
/** Authored weather document and optional precipitation appearance and ground sampling. */
|
|
4
|
+
export interface AuthoredWeatherLayerProps {
|
|
5
|
+
document: EditorDocument;
|
|
6
|
+
rain?: WeatherLayerProps["rain"];
|
|
7
|
+
snow?: WeatherLayerProps["snow"];
|
|
8
|
+
dust?: WeatherLayerProps["dust"];
|
|
9
|
+
heightAt?: (x: number, z: number) => number;
|
|
10
|
+
/** Register actual procedural weather pool and impact query counters. */
|
|
11
|
+
diagnostics?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Render pooled precipitation from the same authored document and clock as gameplay.
|
|
14
|
+
* @capability authored-weather-rendering Render authored weather from shared gameplay samples, simulation time and shelter volumes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function AuthoredWeatherLayer({ document, rain, snow, dust, heightAt, diagnostics }: AuthoredWeatherLayerProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
4
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
5
|
+
import { warnOnce } from "@jgengine/core/devtools/warnOnce";
|
|
6
|
+
import { WEATHER_SHELTER_BUDGET } from "./weatherShelter.js";
|
|
7
|
+
import { WeatherLayer } from "./WeatherLayer.js";
|
|
8
|
+
/** Render pooled precipitation from the same authored document and clock as gameplay.
|
|
9
|
+
* @capability authored-weather-rendering Render authored weather from shared gameplay samples, simulation time and shelter volumes.
|
|
10
|
+
*/
|
|
11
|
+
export function AuthoredWeatherLayer({ document, rain, snow, dust, heightAt, diagnostics = false }) {
|
|
12
|
+
const ctx = useGameContext();
|
|
13
|
+
const config = document.simulation?.weather;
|
|
14
|
+
const metrics = useRef({ rain: { count: 0, capacity: 0 }, snow: { count: 0, capacity: 0 }, dust: { count: 0, capacity: 0 }, impacts: { count: 0, capacity: 0, heightQueries: 0, exposureQueries: 0, queryBudget: 0 } });
|
|
15
|
+
const mounted = config !== undefined;
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!diagnostics || !mounted)
|
|
18
|
+
return;
|
|
19
|
+
return devtools.probes.register("weatherParticles", () => ({ scope: "procedural-weather", ...structuredClone(metrics.current) }));
|
|
20
|
+
}, [diagnostics, mounted]);
|
|
21
|
+
const shelters = useMemo(() => document.volumes.filter((volume) => volume.kind === "shelter"), [document.volumes]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (shelters.length > WEATHER_SHELTER_BUDGET)
|
|
24
|
+
warnOnce("weather-shelter-budget", `[jgengine] Weather renders the closest ${WEATHER_SHELTER_BUDGET} of ${shelters.length} shelters; gameplay uses all authored shelters.`);
|
|
25
|
+
}, [shelters.length]);
|
|
26
|
+
const sample = useCallback((x, z) => ctx.environment.sample(x, z), [ctx.environment]);
|
|
27
|
+
const exposureAt = useCallback((x, y, z) => ctx.environment.exposureAt(x, y, z), [ctx.environment]);
|
|
28
|
+
if (config === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
return _jsx(WeatherLayer, { sample: sample, metrics: diagnostics ? metrics.current : undefined, shelters: shelters, rain: rain, snow: snow, dust: dust, impacts: heightAt === undefined ? false : { heightAt, exposureAt } });
|
|
31
|
+
}
|
|
@@ -26,6 +26,7 @@ export interface DustFieldProps {
|
|
|
26
26
|
*/
|
|
27
27
|
groundBias?: number;
|
|
28
28
|
timeScale?: number;
|
|
29
|
+
timeSeconds?: number | (() => number);
|
|
29
30
|
seed?: number;
|
|
30
31
|
renderOrder?: number;
|
|
31
32
|
frustumCulled?: boolean;
|
|
@@ -38,4 +39,4 @@ export interface DustFieldProps {
|
|
|
38
39
|
*
|
|
39
40
|
* @capability weather-dust airborne particulate that turns still air into visible wind
|
|
40
41
|
*/
|
|
41
|
-
export declare function DustField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, groundBias, timeScale, seed, renderOrder, frustumCulled, }: DustFieldProps): import("react").JSX.Element;
|
|
42
|
+
export declare function DustField({ count, density, budget, volume, wind, origin, followCamera, speed, size, sway, opacity, color, groundBias, timeScale, timeSeconds, seed, renderOrder, frustumCulled, }: DustFieldProps): import("react").JSX.Element;
|