@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
|
@@ -1,91 +1,183 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
4
5
|
import { createParticleSystem } from "@jgengine/core/vfx/particles";
|
|
5
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
7
|
import { ParticleField } from "../vfx/ParticleField.js";
|
|
7
8
|
const QUALITY_POOL_CAP = { low: 128, medium: 256, high: 512, ultra: 1024 };
|
|
8
|
-
/**
|
|
9
|
-
* The per-effect particle pool a graphics tier allows: the requested `max`
|
|
10
|
-
* clamped to the tier cap, so lower tiers degrade density instead of dropping
|
|
11
|
-
* effects entirely.
|
|
12
|
-
*/
|
|
9
|
+
/** The requested visual pool clamped to the graphics tier. */
|
|
13
10
|
export function resolveParticleBudget(quality, requested, particleCap) {
|
|
14
|
-
const
|
|
15
|
-
|
|
11
|
+
const rawCap = particleCap ?? QUALITY_POOL_CAP[quality];
|
|
12
|
+
const cap = Number.isFinite(rawCap) ? Math.max(1, Math.floor(rawCap)) : QUALITY_POOL_CAP[quality];
|
|
13
|
+
return Number.isFinite(requested) ? Math.max(1, Math.floor(Math.min(requested, cap))) : cap;
|
|
16
14
|
}
|
|
17
15
|
const MAX_CONCURRENT_BURSTS = 32;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* follow binding) and therefore needs a React re-render — config-only retunes are
|
|
21
|
-
* applied imperatively to the live system and must stay render-free.
|
|
22
|
-
*
|
|
23
|
-
* @internal WorldParticles render-boundary helper, exported for its regression test.
|
|
24
|
-
*/
|
|
16
|
+
const ZERO = [0, 0, 0];
|
|
17
|
+
/** @internal Config retunes stay imperative; output changes require React rendering. */
|
|
25
18
|
export function specNeedsRender(prev, next) {
|
|
26
|
-
if (prev.blending !== next.blending || prev.follow !== next.follow)
|
|
19
|
+
if (prev.blending !== next.blending || prev.follow !== next.follow || prev.space !== next.space)
|
|
27
20
|
return true;
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
if (prev.render?.shape !== next.render?.shape || prev.render?.stretch !== next.render?.stretch)
|
|
22
|
+
return true;
|
|
23
|
+
const a = prev.offset, b = next.offset;
|
|
30
24
|
if (a === b)
|
|
31
25
|
return false;
|
|
32
26
|
if (a === undefined || b === undefined)
|
|
33
27
|
return true;
|
|
34
28
|
return a[0] !== b[0] || a[1] !== b[1] || a[2] !== b[2];
|
|
35
29
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
/** @internal Transform a simulation vector through a followed yaw and origin. */
|
|
31
|
+
export function transformParticleVector(point, origin, yaw, inverse = false, out = [0, 0, 0]) {
|
|
32
|
+
const c = Math.cos(yaw), s = Math.sin(yaw);
|
|
33
|
+
const x = inverse ? point[0] - origin[0] : point[0];
|
|
34
|
+
const y = inverse ? point[1] - origin[1] : point[1];
|
|
35
|
+
const z = inverse ? point[2] - origin[2] : point[2];
|
|
36
|
+
out[0] = inverse ? c * x - s * z : origin[0] + c * x + s * z;
|
|
37
|
+
out[1] = inverse ? y : origin[1] + y;
|
|
38
|
+
out[2] = inverse ? s * x + c * z : origin[2] - s * x + c * z;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** @internal Cosmetic catch-up follows game time, with bounded work after suspension. */
|
|
42
|
+
export function resolveParticleDelta(previousTime, gameTime) {
|
|
43
|
+
return Number.isFinite(previousTime) && Number.isFinite(gameTime) ? Math.max(0, Math.min(0.1, gameTime - previousTime)) : 0;
|
|
44
|
+
}
|
|
45
|
+
/** Renders the existing director and pooled simulator; named bursts reuse the standing pool. @internal */
|
|
46
|
+
export function WorldParticles({ quality, particleCap, diagnostics = false }) {
|
|
47
47
|
const ctx = useGameContext();
|
|
48
48
|
const emittersRef = useRef(new Map());
|
|
49
49
|
const burstsRef = useRef([]);
|
|
50
|
+
const previousTime = useRef(ctx.time.now());
|
|
51
|
+
const timing = useRef({ poolsCreated: 0, poolsRetired: 0, updateSamples: 0, lastUpdateMs: 0, totalUpdateMs: 0, maxUpdateMs: 0, droppedCatchupSeconds: 0 });
|
|
52
|
+
const resources = useRef({ fieldsMounted: 0, fieldsUnmounted: 0, liveFields: 0, attributeBytes: 0, totalUploadMs: 0, maxUploadMs: 0, uploadSamples: 0 });
|
|
50
53
|
const [, setGeneration] = useState(0);
|
|
54
|
+
function pose(spec) {
|
|
55
|
+
const offset = spec.offset ?? [0, 0, 0];
|
|
56
|
+
const entity = spec.follow === undefined ? undefined : ctx.scene.entity.get(spec.follow);
|
|
57
|
+
if (entity === undefined || entity === null)
|
|
58
|
+
return { position: offset, yaw: 0, found: spec.follow === undefined };
|
|
59
|
+
if (spec.space === "local")
|
|
60
|
+
return { position: transformParticleVector(offset, entity.position, entity.rotationY), yaw: entity.rotationY, found: true };
|
|
61
|
+
return { position: [entity.position[0] + offset[0], entity.position[1] + offset[1], entity.position[2] + offset[2]], yaw: 0, found: true };
|
|
62
|
+
}
|
|
63
|
+
function syncPose(instance) {
|
|
64
|
+
const resolved = pose(instance.spec);
|
|
65
|
+
instance.pose = resolved;
|
|
66
|
+
if (instance.spec.space === "local" && resolved.found) {
|
|
67
|
+
instance.group?.position.set(...resolved.position);
|
|
68
|
+
if (instance.group !== null)
|
|
69
|
+
instance.group.rotation.y = resolved.yaw;
|
|
70
|
+
}
|
|
71
|
+
else if (instance.spec.space !== "local") {
|
|
72
|
+
const base = instance.spec.follow === undefined ? instance.spec.config.position ?? [0, 0, 0] : [0, 0, 0];
|
|
73
|
+
instance.system.configure({ position: [resolved.position[0] + base[0], resolved.position[1] + base[1], resolved.position[2] + base[2]] });
|
|
74
|
+
}
|
|
75
|
+
if (resolved.found && instance.spec.active !== false)
|
|
76
|
+
instance.system.start();
|
|
77
|
+
else
|
|
78
|
+
instance.system.stop();
|
|
79
|
+
}
|
|
80
|
+
function collisionFor(spec, frozenPose) {
|
|
81
|
+
return (from, to) => {
|
|
82
|
+
const current = spec();
|
|
83
|
+
const local = current.space === "local";
|
|
84
|
+
const resolved = frozenPose ?? emittersRef.current.get(current.id)?.pose ?? pose(current);
|
|
85
|
+
const start = local ? transformParticleVector(from, resolved.position, resolved.yaw) : from;
|
|
86
|
+
const end = local ? transformParticleVector(to, resolved.position, resolved.yaw) : to;
|
|
87
|
+
const direction = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
|
|
88
|
+
const maxDistance = Math.hypot(...direction);
|
|
89
|
+
if (maxDistance === 0)
|
|
90
|
+
return null;
|
|
91
|
+
const hit = ctx.scene.raycast({ origin: start, direction, maxDistance,
|
|
92
|
+
filter: { entities: false, objects: true, terrain: true, walls: true } });
|
|
93
|
+
if (hit === null)
|
|
94
|
+
return null;
|
|
95
|
+
return { position: local ? transformParticleVector(hit.point, resolved.position, resolved.yaw, true) : hit.point,
|
|
96
|
+
normal: local ? transformParticleVector(hit.normal, [0, 0, 0], resolved.yaw, true) : hit.normal };
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function accelerationFor(spec, frozenPose) {
|
|
100
|
+
const worldScratch = [0, 0, 0];
|
|
101
|
+
const forceScratch = [0, 0, 0];
|
|
102
|
+
return (position, _velocity, out) => {
|
|
103
|
+
const current = spec();
|
|
104
|
+
const influence = current.environment;
|
|
105
|
+
if (influence === undefined) {
|
|
106
|
+
out[0] = out[1] = out[2] = 0;
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
const resolved = frozenPose ?? emittersRef.current.get(current.id)?.pose ?? pose(current);
|
|
110
|
+
const local = current.space === "local";
|
|
111
|
+
const world = local ? transformParticleVector(position, resolved.position, resolved.yaw, false, worldScratch) : position;
|
|
112
|
+
const force = ctx.environment.accelerationAt(world, ctx.time.now(), influence.windResponse ?? 0, influence.maxAcceleration ?? 100, influence.mask ?? 0xffffffff, forceScratch);
|
|
113
|
+
const result = local ? transformParticleVector(force, ZERO, resolved.yaw, true, forceScratch) : force;
|
|
114
|
+
out[0] = result[0];
|
|
115
|
+
out[1] = result[1];
|
|
116
|
+
out[2] = result[2];
|
|
117
|
+
return out;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
51
120
|
useEffect(() => {
|
|
52
121
|
const emitters = emittersRef.current;
|
|
53
122
|
function sync() {
|
|
54
123
|
let changed = false;
|
|
55
|
-
const specs = ctx.particles.emitters();
|
|
56
124
|
const liveIds = new Set();
|
|
57
|
-
for (const spec of
|
|
125
|
+
for (const spec of ctx.particles.emitters()) {
|
|
58
126
|
liveIds.add(spec.id);
|
|
59
127
|
const poolMax = resolveParticleBudget(quality, spec.config.max, particleCap);
|
|
60
128
|
const current = emitters.get(spec.id);
|
|
61
129
|
if (current === undefined || current.poolMax !== poolMax) {
|
|
62
|
-
|
|
63
|
-
spec,
|
|
64
|
-
|
|
65
|
-
system
|
|
66
|
-
|
|
130
|
+
const next = { spec, poolMax, group: current?.group ?? null, pose: pose(spec),
|
|
131
|
+
system: createParticleSystem({ ...spec.config, max: poolMax, seed: spec.config.seed ?? spec.id }, { collision: collisionFor(() => next.spec), acceleration: accelerationFor(() => next.spec) }) };
|
|
132
|
+
if (current !== undefined) {
|
|
133
|
+
next.system.restore(current.system.snapshot());
|
|
134
|
+
next.system.configure({ ...spec.config, max: poolMax, seed: spec.config.seed ?? spec.id }, true);
|
|
135
|
+
}
|
|
136
|
+
timing.current.poolsCreated++;
|
|
137
|
+
if (current !== undefined)
|
|
138
|
+
timing.current.poolsRetired++;
|
|
139
|
+
emitters.set(spec.id, next);
|
|
140
|
+
syncPose(next);
|
|
67
141
|
changed = true;
|
|
68
142
|
}
|
|
69
143
|
else if (current.spec !== spec) {
|
|
70
144
|
if (specNeedsRender(current.spec, spec))
|
|
71
145
|
changed = true;
|
|
72
|
-
current.system.configure({ ...spec.config, max: poolMax });
|
|
146
|
+
current.system.configure({ ...spec.config, max: poolMax, seed: spec.config.seed ?? spec.id }, true);
|
|
73
147
|
current.spec = spec;
|
|
148
|
+
syncPose(current);
|
|
74
149
|
}
|
|
75
150
|
}
|
|
76
151
|
for (const id of emitters.keys()) {
|
|
77
152
|
if (liveIds.has(id))
|
|
78
153
|
continue;
|
|
79
154
|
emitters.delete(id);
|
|
155
|
+
timing.current.poolsRetired++;
|
|
80
156
|
changed = true;
|
|
81
157
|
}
|
|
82
158
|
for (const burst of ctx.particles.drainBursts()) {
|
|
159
|
+
const named = burst.emitterId === undefined ? undefined : emitters.get(burst.emitterId);
|
|
160
|
+
if (named !== undefined) {
|
|
161
|
+
syncPose(named);
|
|
162
|
+
named.system.emit(burst.count);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
83
165
|
const poolMax = resolveParticleBudget(quality, Math.min(burst.count, burst.config.max ?? burst.count), particleCap);
|
|
84
|
-
const
|
|
166
|
+
const spec = { id: `burst:${burst.seq}`, config: burst.config, ...burst.options };
|
|
167
|
+
const resolved = pose(spec);
|
|
168
|
+
const system = createParticleSystem({ ...burst.config, rate: 0, max: poolMax, seed: burst.config.seed ?? burst.seq }, { collision: collisionFor(() => spec, resolved), acceleration: accelerationFor(() => spec, resolved) });
|
|
169
|
+
if (spec.space !== "local") {
|
|
170
|
+
const base = spec.follow === undefined ? burst.config.position ?? [0, 0, 0] : [0, 0, 0];
|
|
171
|
+
system.configure({ position: [resolved.position[0] + base[0], resolved.position[1] + base[1], resolved.position[2] + base[2]] });
|
|
172
|
+
}
|
|
85
173
|
system.emit(burst.count);
|
|
86
|
-
|
|
87
|
-
|
|
174
|
+
timing.current.poolsCreated++;
|
|
175
|
+
burstsRef.current.push({ seq: burst.seq, system, blending: burst.blending ?? "additive", render: spec.render, collisionEffect: spec.collisionEffect,
|
|
176
|
+
position: spec.space === "local" ? resolved.position : [0, 0, 0], rotationY: spec.space === "local" ? resolved.yaw : 0 });
|
|
177
|
+
if (burstsRef.current.length > MAX_CONCURRENT_BURSTS) {
|
|
88
178
|
burstsRef.current.shift();
|
|
179
|
+
timing.current.poolsRetired++;
|
|
180
|
+
}
|
|
89
181
|
changed = true;
|
|
90
182
|
}
|
|
91
183
|
if (changed)
|
|
@@ -93,36 +185,92 @@ export function WorldParticles({ quality, particleCap }) {
|
|
|
93
185
|
}
|
|
94
186
|
sync();
|
|
95
187
|
const off = ctx.particles.subscribe(sync);
|
|
188
|
+
return off;
|
|
189
|
+
}, [ctx, quality, particleCap]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!diagnostics)
|
|
192
|
+
return;
|
|
193
|
+
return devtools.probes.register("particles", () => {
|
|
194
|
+
let count = 0, capacity = 0, renderBufferBytes = 0;
|
|
195
|
+
const read = (system) => {
|
|
196
|
+
const buffers = system.buffers();
|
|
197
|
+
count += system.count();
|
|
198
|
+
capacity += buffers.positions.length / 3;
|
|
199
|
+
renderBufferBytes += buffers.positions.byteLength + buffers.previousPositions.byteLength + buffers.velocities.byteLength + buffers.sizes.byteLength + buffers.colors.byteLength + buffers.alphas.byteLength + buffers.ids.byteLength;
|
|
200
|
+
};
|
|
201
|
+
for (const instance of emittersRef.current.values())
|
|
202
|
+
read(instance.system);
|
|
203
|
+
for (const burst of burstsRef.current)
|
|
204
|
+
read(burst.system);
|
|
205
|
+
const sample = timing.current, fields = resources.current;
|
|
206
|
+
return { scope: "WorldParticles", emitters: emittersRef.current.size, bursts: burstsRef.current.length, count, capacity, renderBufferBytes,
|
|
207
|
+
poolsCreated: sample.poolsCreated, poolsRetired: sample.poolsRetired,
|
|
208
|
+
updateSamples: sample.updateSamples, lastUpdateMs: sample.lastUpdateMs, maxUpdateMs: sample.maxUpdateMs,
|
|
209
|
+
avgUpdateMs: sample.updateSamples === 0 ? 0 : sample.totalUpdateMs / sample.updateSamples,
|
|
210
|
+
droppedCatchupSeconds: sample.droppedCatchupSeconds,
|
|
211
|
+
resources: { ...fields, avgUploadMsPerField: fields.uploadSamples === 0 ? 0 : fields.totalUploadMs / fields.uploadSamples } };
|
|
212
|
+
});
|
|
213
|
+
}, [ctx, diagnostics]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
previousTime.current = ctx.time.now();
|
|
96
216
|
return () => {
|
|
97
|
-
|
|
98
|
-
|
|
217
|
+
timing.current.poolsRetired += emittersRef.current.size + burstsRef.current.length;
|
|
218
|
+
emittersRef.current.clear();
|
|
99
219
|
burstsRef.current = [];
|
|
100
220
|
};
|
|
101
|
-
}, [ctx
|
|
221
|
+
}, [ctx]);
|
|
102
222
|
useFrame(() => {
|
|
223
|
+
const started = diagnostics ? performance.now() : 0;
|
|
224
|
+
const gameTime = ctx.time.now();
|
|
225
|
+
const dt = resolveParticleDelta(previousTime.current, gameTime);
|
|
226
|
+
if (diagnostics)
|
|
227
|
+
timing.current.droppedCatchupSeconds += Math.max(0, gameTime - previousTime.current - dt);
|
|
228
|
+
previousTime.current = gameTime;
|
|
229
|
+
const impacts = [];
|
|
230
|
+
const collect = (id, system, effect, origin, yaw = 0) => {
|
|
231
|
+
const events = system.drainEvents();
|
|
232
|
+
for (const event of events) {
|
|
233
|
+
if (origin !== undefined) {
|
|
234
|
+
event.position = transformParticleVector(event.position, origin, yaw);
|
|
235
|
+
event.velocity = transformParticleVector(event.velocity, [0, 0, 0], yaw);
|
|
236
|
+
if (event.normal !== undefined)
|
|
237
|
+
event.normal = transformParticleVector(event.normal, [0, 0, 0], yaw);
|
|
238
|
+
}
|
|
239
|
+
if (effect !== undefined && event.type === "collision" && impacts.length < 16)
|
|
240
|
+
impacts.push({ effect, event });
|
|
241
|
+
}
|
|
242
|
+
ctx.particles.reportEvents(id, events);
|
|
243
|
+
};
|
|
103
244
|
for (const instance of emittersRef.current.values()) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
245
|
+
syncPose(instance);
|
|
246
|
+
instance.system.update(dt);
|
|
247
|
+
const resolved = instance.spec.space === "local" ? instance.pose : undefined;
|
|
248
|
+
collect(instance.spec.id, instance.system, instance.spec.collisionEffect, resolved?.position, resolved?.yaw);
|
|
249
|
+
}
|
|
250
|
+
let finished = false;
|
|
251
|
+
for (const burst of burstsRef.current) {
|
|
252
|
+
burst.system.update(dt);
|
|
253
|
+
collect(`burst:${burst.seq}`, burst.system, burst.collisionEffect, burst.position, burst.rotationY);
|
|
254
|
+
if (burst.system.count() === 0) {
|
|
255
|
+
finished = true;
|
|
256
|
+
timing.current.poolsRetired++;
|
|
257
|
+
}
|
|
118
258
|
}
|
|
119
|
-
|
|
120
|
-
if (finished.length > 0) {
|
|
259
|
+
if (finished) {
|
|
121
260
|
burstsRef.current = burstsRef.current.filter((burst) => burst.system.count() > 0);
|
|
122
261
|
setGeneration((n) => n + 1);
|
|
123
262
|
}
|
|
263
|
+
for (const { effect, event } of impacts) {
|
|
264
|
+
const { collision: _, ...config } = effect.config;
|
|
265
|
+
ctx.particles.burst({ ...config, position: event.position, rate: 0, deathEvents: false }, effect.count, effect.blending, { render: effect.render });
|
|
266
|
+
}
|
|
267
|
+
if (diagnostics) {
|
|
268
|
+
const elapsed = performance.now() - started, sample = timing.current;
|
|
269
|
+
sample.lastUpdateMs = elapsed;
|
|
270
|
+
sample.totalUpdateMs += elapsed;
|
|
271
|
+
sample.maxUpdateMs = Math.max(sample.maxUpdateMs, elapsed);
|
|
272
|
+
sample.updateSamples++;
|
|
273
|
+
}
|
|
124
274
|
});
|
|
125
|
-
return
|
|
126
|
-
? instance.spec.offset
|
|
127
|
-
: [0, 0, 0], children: _jsx(ParticleField, { system: instance.system, blending: instance.spec.blending ?? "normal" }) }, instance.spec.id))), burstsRef.current.map((burst) => (_jsx(ParticleField, { system: burst.system, blending: burst.blending }, burst.seq)))] }));
|
|
275
|
+
return _jsxs(_Fragment, { children: [Array.from(emittersRef.current.values()).map((instance) => _jsx("group", { ref: (group) => { instance.group = group; }, children: _jsx(ParticleField, { system: instance.system, advance: false, blending: instance.spec.blending ?? "normal", render: instance.spec.render, metrics: diagnostics ? resources.current : undefined }) }, instance.spec.id)), burstsRef.current.map((burst) => _jsx("group", { position: burst.position, rotation: [0, burst.rotationY, 0], children: _jsx(ParticleField, { system: burst.system, advance: false, blending: burst.blending, render: burst.render, metrics: diagnostics ? resources.current : undefined }) }, burst.seq))] });
|
|
128
276
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import { type EditorDocument } from "@jgengine/core/editor/index";
|
|
2
3
|
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
3
4
|
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
4
5
|
import { type SceneObject } from "@jgengine/core/scene/objectStore";
|
|
5
6
|
import type { EntitySpriteConfig, ModelConfig, ObjectStyle } from "@jgengine/core/game/playableGame";
|
|
6
7
|
import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
|
|
7
|
-
export declare function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }: {
|
|
8
|
+
export declare function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }: {
|
|
8
9
|
entitySprites: Record<string, EntitySpriteConfig> | undefined;
|
|
9
10
|
entityModels: Record<string, string | ModelConfig> | undefined;
|
|
10
11
|
objectModels: Record<string, string | ModelConfig> | undefined;
|
|
@@ -15,6 +16,7 @@ export declare function WorldView({ entitySprites, entityModels, objectModels, o
|
|
|
15
16
|
renderObject: ((object: SceneObject) => ReactNode) | undefined;
|
|
16
17
|
selectedIds: ReadonlySet<string>;
|
|
17
18
|
hideLocalActor: boolean;
|
|
19
|
+
editorLayers?: EditorDocument | undefined;
|
|
18
20
|
}): import("react").JSX.Element;
|
|
19
21
|
export declare function RemotePlayers({ rows, presenceInterpolationMs }: {
|
|
20
22
|
rows: PresencePoseRow[];
|
package/dist/world/WorldScene.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { modelWithAuthoredMaterials } from "@jgengine/core/editor/materialAuthoring";
|
|
3
|
+
import { modelWithAuthoredNodeVisibility } from "@jgengine/core/editor/modelNodeVisibility";
|
|
2
4
|
import { useFrame } from "@react-three/fiber";
|
|
3
5
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
6
|
import * as THREE from "three";
|
|
7
|
+
import { createEmptyEditorDocument } from "@jgengine/core/editor/index";
|
|
8
|
+
import { resolveAuthoredObjects } from "@jgengine/core/world/authoredObjects";
|
|
9
|
+
import { useLiveEditorDocument } from "../scene/AuthoredScene.js";
|
|
10
|
+
import { StaticObjectModels } from "./StaticObjectModels.js";
|
|
11
|
+
import { groupStaticObjects } from "./staticObjectBatches.js";
|
|
5
12
|
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
6
13
|
import { WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
7
14
|
import { beginFallbackSeam, endFallbackPass, reportFallbackSeam, } from "@jgengine/core/devtools/fallbackSeams";
|
|
@@ -12,11 +19,12 @@ import { DefaultSurface, detailMaps } from "../render/defaultSurface.js";
|
|
|
12
19
|
import { useDisposable } from "../render/useDisposable.js";
|
|
13
20
|
import { MeasuredBoundsGroup } from "../render/measureBounds.js";
|
|
14
21
|
import { EntitySprite, IsolatedEntityModel } from "../render/SceneModels.js";
|
|
15
|
-
import {
|
|
22
|
+
import { resolveEntityModel, resolveObjectModel } from "../render/resolveModel.js";
|
|
16
23
|
import { useRenderVisibility } from "../visibility/CullingProvider.js";
|
|
17
24
|
import { writeEntityPose, writeRenderPose } from "./entityPose.js";
|
|
18
25
|
import { createSnapshotBuffer } from "@jgengine/core/runtime/snapshotBuffer";
|
|
19
26
|
import { POINTER_ENTITY_KEY, POINTER_OBJECT_KEY } from "../pointer/pointerService.js";
|
|
27
|
+
const EMPTY_DOCUMENT = createEmptyEditorDocument();
|
|
20
28
|
const GROUND_SIZE = 160;
|
|
21
29
|
const GROUND_SEGMENTS = 80;
|
|
22
30
|
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
|
|
@@ -130,10 +138,15 @@ function WorldEnvironment({ environment: Environment }) {
|
|
|
130
138
|
reportFallbackSeam("ground", "noScene");
|
|
131
139
|
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx(RockField, {})] }));
|
|
132
140
|
}
|
|
133
|
-
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
141
|
+
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }) {
|
|
134
142
|
const ctx = useGameContext();
|
|
135
143
|
const entityIds = useSceneEntityIds();
|
|
136
144
|
const objectIds = useSceneObjectIds();
|
|
145
|
+
const document = useLiveEditorDocument(editorLayers ?? EMPTY_DOCUMENT, editorLayers !== undefined);
|
|
146
|
+
const materialMarkers = useMemo(() => new Map(document.markers.map((marker) => [marker.id, marker])), [document]);
|
|
147
|
+
const authoredIds = useMemo(() => new Set(resolveAuthoredObjects(document).map((object) => object.instanceId)), [document]);
|
|
148
|
+
const batchCache = useRef(new Map());
|
|
149
|
+
const modelCache = useMemo(() => new Map(), [objectModels, assets]);
|
|
137
150
|
const player = usePlayer();
|
|
138
151
|
const targetId = useTarget(player.userId);
|
|
139
152
|
const controlledId = useGameStore((c) => c.player.possession.active(player.userId));
|
|
@@ -148,6 +161,29 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
148
161
|
useEffect(() => {
|
|
149
162
|
endFallbackPass();
|
|
150
163
|
});
|
|
164
|
+
const candidates = objectIds.flatMap((instanceId) => {
|
|
165
|
+
const object = ctx.scene.object.get(instanceId);
|
|
166
|
+
if (object === null)
|
|
167
|
+
return [];
|
|
168
|
+
const custom = renderObject?.(object);
|
|
169
|
+
const model = modelWithAuthoredNodeVisibility(modelWithAuthoredMaterials(resolveObjectModel(object, objectModels, assets, modelCache), { materialAssets: document.materialAssets, markers: materialMarkers.has(object.instanceId) ? [materialMarkers.get(object.instanceId)] : [] }, object.instanceId), materialMarkers.get(object.instanceId));
|
|
170
|
+
const style = objectStyles?.[object.catalogId];
|
|
171
|
+
// Reaching the primitive box means no model resolved. A present-but-unresolved objectModels
|
|
172
|
+
// key implies its asset pack is not pulled; an absent key is an omitted (often intended) mapping.
|
|
173
|
+
if ((custom === undefined || custom === null) && model === undefined && style?.hidden !== true) {
|
|
174
|
+
reportFallbackSeam("object", objectModels?.[object.catalogId] === undefined ? "omittedMapping" : "unpulledPack");
|
|
175
|
+
}
|
|
176
|
+
return [{ object, model, custom, style, authored: authoredIds.has(instanceId), catalog: ctx.scene.object.catalog(instanceId) }];
|
|
177
|
+
});
|
|
178
|
+
const grouped = groupStaticObjects(candidates.map((candidate) => ({ ...candidate, custom: candidate.custom !== undefined && candidate.custom !== null })), 24, batchCache.current);
|
|
179
|
+
batchCache.current = new Map(grouped.batches.map((batch) => [batch.key, batch]));
|
|
180
|
+
const candidatesById = new Map(candidates.map((candidate) => [candidate.object.instanceId, candidate]));
|
|
181
|
+
const renderSingle = (object) => {
|
|
182
|
+
object = ctx.scene.object.get(object.instanceId) ?? object;
|
|
183
|
+
const candidate = candidatesById.get(object.instanceId);
|
|
184
|
+
const model = modelWithAuthoredNodeVisibility(modelWithAuthoredMaterials(resolveObjectModel(object, objectModels, assets, modelCache), { materialAssets: document.materialAssets, markers: materialMarkers.has(object.instanceId) ? [materialMarkers.get(object.instanceId)] : [] }, object.instanceId), materialMarkers.get(object.instanceId));
|
|
185
|
+
return _jsx(ObjectMarker, { object: object, custom: object === candidate.object ? candidate.custom : renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId);
|
|
186
|
+
};
|
|
151
187
|
return (_jsxs(_Fragment, { children: [entityIds.map((entityId) => {
|
|
152
188
|
const entity = ctx.scene.entity.get(entityId);
|
|
153
189
|
if (entity === null || entity.name === WORLD_ITEM_ENTITY_NAME)
|
|
@@ -163,31 +199,10 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
163
199
|
reportFallbackSeam("entity", "omittedMapping");
|
|
164
200
|
}
|
|
165
201
|
return (_jsx(EntityMarker, { entity: entity, custom: custom, model: model, sprite: sprite, isLocal: entityId === controlledId, targeted: entityId === targetId, selected: selectedIds.has(entityId), onSelect: handleSelect }, entityId));
|
|
166
|
-
}),
|
|
167
|
-
const object = ctx.scene.object.get(instanceId);
|
|
168
|
-
if (object === null)
|
|
169
|
-
return null;
|
|
170
|
-
const custom = renderObject?.(object);
|
|
171
|
-
const resolved = resolveModel(objectModels?.[object.catalogId], assets, {
|
|
172
|
-
seam: "objectModels",
|
|
173
|
-
key: object.catalogId,
|
|
174
|
-
}) ?? tryResolveCatalogModel(object.catalogId, assets);
|
|
175
|
-
// An authored per-placement animation override (marker.meta.animation → SceneObject.animation,
|
|
176
|
-
// #1276) wins over catalog resolution's default "auto"; absent, the resolved config is untouched.
|
|
177
|
-
const model = resolved !== undefined && object.animation !== undefined
|
|
178
|
-
? { ...resolved, animation: object.animation }
|
|
179
|
-
: resolved;
|
|
180
|
-
const style = objectStyles?.[object.catalogId];
|
|
181
|
-
// Reaching the primitive box means no model resolved. A present-but-unresolved objectModels
|
|
182
|
-
// key implies its asset pack is not pulled; an absent key is an omitted (often intended) mapping.
|
|
183
|
-
if ((custom === undefined || custom === null) && model === undefined && style?.hidden !== true) {
|
|
184
|
-
reportFallbackSeam("object", objectModels?.[object.catalogId] === undefined ? "omittedMapping" : "unpulledPack");
|
|
185
|
-
}
|
|
186
|
-
return (_jsx(ObjectMarker, { object: object, custom: custom, model: model, style: style }, instanceId));
|
|
187
|
-
})] }));
|
|
202
|
+
}), grouped.singles.map((candidate) => renderSingle(candidate.object)), grouped.batches.map((batch) => _jsx(StaticObjectModels, { batch: batch, single: renderSingle }, batch.key))] }));
|
|
188
203
|
}
|
|
189
|
-
export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
190
|
-
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor })] }));
|
|
204
|
+
export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }) {
|
|
205
|
+
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor, editorLayers: editorLayers })] }));
|
|
191
206
|
}
|
|
192
207
|
export function RemotePlayers({ rows, presenceInterpolationMs = 100 }) {
|
|
193
208
|
const groups = useRef(new Map());
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { ModelConfig, ObjectStyle } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
4
|
+
import type { GameContextObjectEntry } from "@jgengine/core/runtime/gameContext";
|
|
5
|
+
import type { ScatterModelSource } from "../scatter/scatterModels.js";
|
|
6
|
+
/** @internal */
|
|
7
|
+
export interface StaticObjectCandidate {
|
|
8
|
+
object: SceneObject;
|
|
9
|
+
model?: ModelConfig;
|
|
10
|
+
authored: boolean;
|
|
11
|
+
custom: boolean;
|
|
12
|
+
catalog: GameContextObjectEntry | null;
|
|
13
|
+
style?: ObjectStyle | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** @internal */
|
|
16
|
+
export interface StaticObjectBatch {
|
|
17
|
+
key: string;
|
|
18
|
+
model: ModelConfig;
|
|
19
|
+
objects: SceneObject[];
|
|
20
|
+
}
|
|
21
|
+
/** The same compatibility gate is used when live placements change after grouping. @internal */
|
|
22
|
+
export declare function canBatchStaticObject(candidate: StaticObjectCandidate): boolean;
|
|
23
|
+
/** Checks a live placement without constructing a grouping candidate. @internal */
|
|
24
|
+
export declare function isCompatibleStaticObject(object: SceneObject, model: ModelConfig | undefined, catalog: GameContextObjectEntry | null): boolean;
|
|
25
|
+
/** Stable exclusion snapshot; only compatibility transitions allocate. @internal */
|
|
26
|
+
export declare function incompatibleStaticObjectIds(batch: StaticObjectBatch, get: (id: string) => SceneObject | null, catalog: (id: string) => GameContextObjectEntry | null, previous: readonly string[], fallbackObjects?: Map<string, SceneObject>): readonly string[];
|
|
27
|
+
/** Groups compatible authored props above eight placements per spatial cell. @internal */
|
|
28
|
+
export declare function groupStaticObjects(candidates: readonly StaticObjectCandidate[], chunkSize?: number, previous?: ReadonlyMap<string, StaticObjectBatch>): {
|
|
29
|
+
batches: StaticObjectBatch[];
|
|
30
|
+
singles: StaticObjectCandidate[];
|
|
31
|
+
};
|
|
32
|
+
/** Rejects loaded scenes whose behavior cannot be represented by static instanced draws. @internal */
|
|
33
|
+
export declare function canInstanceStaticScene(scene: THREE.Object3D, clips: readonly THREE.AnimationClip[]): boolean;
|
|
34
|
+
/** Compact visible placements and update picking identities without allocating per instance. @internal */
|
|
35
|
+
export declare function syncStaticObjectSource(mesh: THREE.InstancedMesh, source: ScatterModelSource, objects: readonly SceneObject[], get: (id: string) => SceneObject | null, visible: (id: string) => boolean, scratch: THREE.Object3D): void;
|