@jgengine/core 0.8.0 → 0.9.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 +16 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +21 -0
- package/dist/runtime/gameContext.js +184 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +198 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +2585 -1704
- package/package.json +12 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const DEFAULT_HUD_DESIGN_SIZE = { width: 1600, height: 900 };
|
|
2
|
+
export const DEFAULT_HUD_MIN_SCALE = 0.4;
|
|
3
|
+
export const DEFAULT_HUD_MAX_SCALE = 1;
|
|
4
|
+
export function resolveHudFit(config, mobile) {
|
|
5
|
+
const base = {
|
|
6
|
+
designSize: config?.designSize ?? DEFAULT_HUD_DESIGN_SIZE,
|
|
7
|
+
minScale: config?.minScale ?? DEFAULT_HUD_MIN_SCALE,
|
|
8
|
+
maxScale: config?.maxScale ?? DEFAULT_HUD_MAX_SCALE,
|
|
9
|
+
};
|
|
10
|
+
if (!mobile || config?.mobile === undefined)
|
|
11
|
+
return base;
|
|
12
|
+
return {
|
|
13
|
+
designSize: config.mobile.designSize ?? base.designSize,
|
|
14
|
+
minScale: config.mobile.minScale ?? base.minScale,
|
|
15
|
+
maxScale: config.mobile.maxScale ?? base.maxScale,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one scaling rule for every display: the ratio of the live viewport to
|
|
20
|
+
* the authored design size along the limiting axis, clamped. 1 on a viewport
|
|
21
|
+
* at or above design size; smoothly below 1 down to `minScale` on phones.
|
|
22
|
+
*/
|
|
23
|
+
export function hudScaleForViewport(fit, viewport) {
|
|
24
|
+
if (!Number.isFinite(viewport.width) ||
|
|
25
|
+
!Number.isFinite(viewport.height) ||
|
|
26
|
+
viewport.width <= 0 ||
|
|
27
|
+
viewport.height <= 0 ||
|
|
28
|
+
fit.designSize.width <= 0 ||
|
|
29
|
+
fit.designSize.height <= 0) {
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
const ratio = Math.min(viewport.width / fit.designSize.width, viewport.height / fit.designSize.height);
|
|
33
|
+
return Math.min(fit.maxScale, Math.max(fit.minScale, ratio));
|
|
34
|
+
}
|
|
35
|
+
/** Non-null when any edge of `rect` falls outside `viewport` by more than `tolerance` px; each field is the overflow distance past that edge (0 when inside). */
|
|
36
|
+
export function rectOverflow(rect, viewport, tolerance = 1.5) {
|
|
37
|
+
const left = Math.max(0, -rect.x);
|
|
38
|
+
const top = Math.max(0, -rect.y);
|
|
39
|
+
const right = Math.max(0, rect.x + rect.width - viewport.width);
|
|
40
|
+
const bottom = Math.max(0, rect.y + rect.height - viewport.height);
|
|
41
|
+
if (left <= tolerance && top <= tolerance && right <= tolerance && bottom <= tolerance)
|
|
42
|
+
return null;
|
|
43
|
+
const round = (n) => Math.round(n * 10) / 10;
|
|
44
|
+
return { left: round(left), top: round(top), right: round(right), bottom: round(bottom) };
|
|
45
|
+
}
|
|
46
|
+
/** Every panel rect that escapes the viewport — the data behind the HUD overflow gate. */
|
|
47
|
+
export function overflowingPanels(panels, viewport, tolerance = 1.5) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const panel of panels) {
|
|
50
|
+
const overflow = rectOverflow(panel.rect, viewport, tolerance);
|
|
51
|
+
if (overflow !== null)
|
|
52
|
+
out.push({ id: panel.id, ...overflow });
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StreamingSettings } from "./settings.js";
|
|
2
|
+
import type { VisibilityStats } from "./diagnostics.js";
|
|
3
|
+
export type AssetLoadState = "queued" | "loading" | "loaded" | "unloaded" | "error";
|
|
4
|
+
export interface AssetLoadResult {
|
|
5
|
+
/** Approximate resident size, used for the memory budget and small-asset resident policy. */
|
|
6
|
+
readonly bytes?: number;
|
|
7
|
+
/** Opaque handle to the loaded resource (texture, mesh, buffer). */
|
|
8
|
+
readonly value?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface CancelSignal {
|
|
11
|
+
readonly cancelled: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface AssetStreamingOptions {
|
|
14
|
+
/** Async loader. Must honor `signal.cancelled` and avoid committing work when it flips true. */
|
|
15
|
+
readonly load: (assetId: string, signal: CancelSignal) => Promise<AssetLoadResult>;
|
|
16
|
+
/** Release GPU/CPU resources for an asset. */
|
|
17
|
+
readonly unload?: (assetId: string) => void;
|
|
18
|
+
readonly settings?: Partial<StreamingSettings>;
|
|
19
|
+
readonly now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
export interface AssetRecord {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
state: AssetLoadState;
|
|
24
|
+
bytes: number;
|
|
25
|
+
priority: number;
|
|
26
|
+
refCount: number;
|
|
27
|
+
pinned: boolean;
|
|
28
|
+
lastActiveMs: number;
|
|
29
|
+
value: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface StreamingStats {
|
|
32
|
+
queued: number;
|
|
33
|
+
loading: number;
|
|
34
|
+
loaded: number;
|
|
35
|
+
unloaded: number;
|
|
36
|
+
errored: number;
|
|
37
|
+
inFlight: number;
|
|
38
|
+
cancelled: number;
|
|
39
|
+
bytes: number;
|
|
40
|
+
}
|
|
41
|
+
export interface AssetStreamingSystem {
|
|
42
|
+
/** Demand an asset (near/in view). Deduplicated — an already loaded/loading/queued asset is never re-fetched concurrently. */
|
|
43
|
+
request(assetId: string, priority?: number): void;
|
|
44
|
+
/** Shared ownership by an active object. A retained asset (refCount > 0) is never auto-unloaded. */
|
|
45
|
+
retain(assetId: string): void;
|
|
46
|
+
release(assetId: string): void;
|
|
47
|
+
/** Pin resident regardless of distance/refcount/scene. */
|
|
48
|
+
pin(assetId: string): void;
|
|
49
|
+
unpin(assetId: string): void;
|
|
50
|
+
/** Refresh the "last needed" timestamp used by the grace-period unload. */
|
|
51
|
+
markActive(assetId: string): void;
|
|
52
|
+
/** Advance one frame: start up to the load budget, evict past-grace assets up to the unload budget. */
|
|
53
|
+
tick(dt: number): void;
|
|
54
|
+
/** Cancel an in-flight or queued request that is no longer needed. */
|
|
55
|
+
cancel(assetId: string): void;
|
|
56
|
+
stateOf(assetId: string): AssetLoadState | undefined;
|
|
57
|
+
isLoaded(assetId: string): boolean;
|
|
58
|
+
record(assetId: string): AssetRecord | undefined;
|
|
59
|
+
stats(): StreamingStats;
|
|
60
|
+
/** Merge asset counters into a per-frame VisibilityStats. */
|
|
61
|
+
applyTo(stats: VisibilityStats): void;
|
|
62
|
+
/** Resolve once every in-flight load settles — for deterministic tests. */
|
|
63
|
+
settle(): Promise<void>;
|
|
64
|
+
clear(): void;
|
|
65
|
+
}
|
|
66
|
+
export declare function createAssetStreamingSystem(options: AssetStreamingOptions): AssetStreamingSystem;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { DEFAULT_STREAMING_SETTINGS, mergeStreamingSettings } from "./settings.js";
|
|
2
|
+
import { nowMs } from "./diagnostics.js";
|
|
3
|
+
export function createAssetStreamingSystem(options) {
|
|
4
|
+
const settings = mergeStreamingSettings(DEFAULT_STREAMING_SETTINGS, options.settings ?? {});
|
|
5
|
+
const clock = options.now ?? nowMs;
|
|
6
|
+
const records = new Map();
|
|
7
|
+
const queued = new Set();
|
|
8
|
+
const signals = new Map();
|
|
9
|
+
const inFlight = new Set();
|
|
10
|
+
let unloadedTotal = 0;
|
|
11
|
+
let cancelledTotal = 0;
|
|
12
|
+
function ensure(id) {
|
|
13
|
+
let record = records.get(id);
|
|
14
|
+
if (record === undefined) {
|
|
15
|
+
record = { id, state: "unloaded", bytes: 0, priority: 0, refCount: 0, pinned: false, lastActiveMs: clock(), value: undefined };
|
|
16
|
+
records.set(id, record);
|
|
17
|
+
}
|
|
18
|
+
return record;
|
|
19
|
+
}
|
|
20
|
+
function enqueue(record, priority) {
|
|
21
|
+
if (record.state === "loaded" || record.state === "loading") {
|
|
22
|
+
record.priority = Math.max(record.priority, priority);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
record.state = "queued";
|
|
26
|
+
record.priority = Math.max(record.priority, priority);
|
|
27
|
+
queued.add(record.id);
|
|
28
|
+
}
|
|
29
|
+
function startLoad(record) {
|
|
30
|
+
queued.delete(record.id);
|
|
31
|
+
record.state = "loading";
|
|
32
|
+
const signal = { cancelled: false };
|
|
33
|
+
signals.set(record.id, signal);
|
|
34
|
+
const promise = options
|
|
35
|
+
.load(record.id, signal)
|
|
36
|
+
.then((result) => {
|
|
37
|
+
signals.delete(record.id);
|
|
38
|
+
if (signal.cancelled || !records.has(record.id)) {
|
|
39
|
+
record.state = "unloaded";
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
record.state = "loaded";
|
|
43
|
+
record.bytes = result.bytes ?? 0;
|
|
44
|
+
record.value = result.value;
|
|
45
|
+
record.lastActiveMs = clock();
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
signals.delete(record.id);
|
|
49
|
+
if (!signal.cancelled)
|
|
50
|
+
record.state = "error";
|
|
51
|
+
})
|
|
52
|
+
.finally(() => {
|
|
53
|
+
inFlight.delete(promise);
|
|
54
|
+
});
|
|
55
|
+
inFlight.add(promise);
|
|
56
|
+
}
|
|
57
|
+
function evictable(record, now) {
|
|
58
|
+
if (record.state !== "loaded")
|
|
59
|
+
return false;
|
|
60
|
+
if (record.pinned || record.refCount > 0)
|
|
61
|
+
return false;
|
|
62
|
+
if (record.bytes > 0 && record.bytes <= settings.keepResidentBytes)
|
|
63
|
+
return false;
|
|
64
|
+
return now - record.lastActiveMs >= settings.unloadGraceSeconds * 1000;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
request(assetId, priority = 0) {
|
|
68
|
+
const record = ensure(assetId);
|
|
69
|
+
record.lastActiveMs = clock();
|
|
70
|
+
enqueue(record, priority);
|
|
71
|
+
},
|
|
72
|
+
retain(assetId) {
|
|
73
|
+
ensure(assetId).refCount += 1;
|
|
74
|
+
},
|
|
75
|
+
release(assetId) {
|
|
76
|
+
const record = records.get(assetId);
|
|
77
|
+
if (record !== undefined && record.refCount > 0)
|
|
78
|
+
record.refCount -= 1;
|
|
79
|
+
},
|
|
80
|
+
pin(assetId) {
|
|
81
|
+
ensure(assetId).pinned = true;
|
|
82
|
+
},
|
|
83
|
+
unpin(assetId) {
|
|
84
|
+
const record = records.get(assetId);
|
|
85
|
+
if (record !== undefined)
|
|
86
|
+
record.pinned = false;
|
|
87
|
+
},
|
|
88
|
+
markActive(assetId) {
|
|
89
|
+
const record = records.get(assetId);
|
|
90
|
+
if (record !== undefined)
|
|
91
|
+
record.lastActiveMs = clock();
|
|
92
|
+
},
|
|
93
|
+
tick() {
|
|
94
|
+
const now = clock();
|
|
95
|
+
if (queued.size > 0) {
|
|
96
|
+
const pending = [];
|
|
97
|
+
for (const id of queued) {
|
|
98
|
+
const record = records.get(id);
|
|
99
|
+
if (record !== undefined)
|
|
100
|
+
pending.push(record);
|
|
101
|
+
}
|
|
102
|
+
pending.sort((a, b) => b.priority - a.priority);
|
|
103
|
+
const budget = Math.min(settings.maxLoadsPerFrame, pending.length);
|
|
104
|
+
for (let i = 0; i < budget; i += 1)
|
|
105
|
+
startLoad(pending[i]);
|
|
106
|
+
}
|
|
107
|
+
let unloads = 0;
|
|
108
|
+
for (const record of records.values()) {
|
|
109
|
+
if (unloads >= settings.maxUnloadsPerFrame)
|
|
110
|
+
break;
|
|
111
|
+
if (!evictable(record, now))
|
|
112
|
+
continue;
|
|
113
|
+
options.unload?.(record.id);
|
|
114
|
+
record.state = "unloaded";
|
|
115
|
+
record.bytes = 0;
|
|
116
|
+
record.value = undefined;
|
|
117
|
+
unloadedTotal += 1;
|
|
118
|
+
unloads += 1;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
cancel(assetId) {
|
|
122
|
+
if (queued.delete(assetId)) {
|
|
123
|
+
const record = records.get(assetId);
|
|
124
|
+
if (record !== undefined)
|
|
125
|
+
record.state = "unloaded";
|
|
126
|
+
cancelledTotal += 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const signal = signals.get(assetId);
|
|
130
|
+
if (signal !== undefined) {
|
|
131
|
+
signal.cancelled = true;
|
|
132
|
+
const record = records.get(assetId);
|
|
133
|
+
if (record !== undefined)
|
|
134
|
+
record.state = "unloaded";
|
|
135
|
+
cancelledTotal += 1;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
stateOf(assetId) {
|
|
139
|
+
return records.get(assetId)?.state;
|
|
140
|
+
},
|
|
141
|
+
isLoaded(assetId) {
|
|
142
|
+
return records.get(assetId)?.state === "loaded";
|
|
143
|
+
},
|
|
144
|
+
record(assetId) {
|
|
145
|
+
return records.get(assetId);
|
|
146
|
+
},
|
|
147
|
+
stats() {
|
|
148
|
+
let queuedCount = 0, loading = 0, loaded = 0, errored = 0, bytes = 0;
|
|
149
|
+
for (const record of records.values()) {
|
|
150
|
+
switch (record.state) {
|
|
151
|
+
case "queued":
|
|
152
|
+
queuedCount += 1;
|
|
153
|
+
break;
|
|
154
|
+
case "loading":
|
|
155
|
+
loading += 1;
|
|
156
|
+
break;
|
|
157
|
+
case "loaded":
|
|
158
|
+
loaded += 1;
|
|
159
|
+
bytes += record.bytes;
|
|
160
|
+
break;
|
|
161
|
+
case "error":
|
|
162
|
+
errored += 1;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
queued: queuedCount,
|
|
168
|
+
loading,
|
|
169
|
+
loaded,
|
|
170
|
+
unloaded: unloadedTotal,
|
|
171
|
+
errored,
|
|
172
|
+
inFlight: inFlight.size,
|
|
173
|
+
cancelled: cancelledTotal,
|
|
174
|
+
bytes,
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
applyTo(stats) {
|
|
178
|
+
const s = this.stats();
|
|
179
|
+
stats.assetsQueued = s.queued;
|
|
180
|
+
stats.assetsLoading = s.loading;
|
|
181
|
+
stats.assetsLoaded = s.loaded;
|
|
182
|
+
stats.assetsUnloaded = s.unloaded;
|
|
183
|
+
stats.streamedBytes = s.bytes;
|
|
184
|
+
},
|
|
185
|
+
async settle() {
|
|
186
|
+
while (inFlight.size > 0) {
|
|
187
|
+
await Promise.all(Array.from(inFlight));
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
clear() {
|
|
191
|
+
for (const signal of signals.values())
|
|
192
|
+
signal.cancelled = true;
|
|
193
|
+
records.clear();
|
|
194
|
+
queued.clear();
|
|
195
|
+
signals.clear();
|
|
196
|
+
inFlight.clear();
|
|
197
|
+
unloadedTotal = 0;
|
|
198
|
+
cancelledTotal = 0;
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export type Vec3 = EntityPosition;
|
|
3
|
+
/**
|
|
4
|
+
* World-space bounds a culler tests against. Kept as flat scalars (not tuples) so the
|
|
5
|
+
* hot culling path reads them without allocating. Carries both a bounding sphere (cheap
|
|
6
|
+
* broad reject) and a tight AABB (precise reject) — the standard "engine bounds" the
|
|
7
|
+
* frustum, distance, and spatial-index tests all consume.
|
|
8
|
+
*/
|
|
9
|
+
export interface RenderBounds {
|
|
10
|
+
centerX: number;
|
|
11
|
+
centerY: number;
|
|
12
|
+
centerZ: number;
|
|
13
|
+
radius: number;
|
|
14
|
+
minX: number;
|
|
15
|
+
minY: number;
|
|
16
|
+
minZ: number;
|
|
17
|
+
maxX: number;
|
|
18
|
+
maxY: number;
|
|
19
|
+
maxZ: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* How a renderable declares its extent. AABB, bounding sphere, and 2D rectangle cover the
|
|
23
|
+
* common cases; `point` is the degenerate zero-size default for objects that never override.
|
|
24
|
+
* `offset` shifts the volume from the object origin (e.g. a tall model whose pivot is at its feet).
|
|
25
|
+
*/
|
|
26
|
+
export type BoundsSpec = {
|
|
27
|
+
readonly kind: "sphere";
|
|
28
|
+
readonly radius: number;
|
|
29
|
+
readonly offset?: Vec3;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: "aabb";
|
|
32
|
+
readonly half: Vec3;
|
|
33
|
+
readonly offset?: Vec3;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "rect";
|
|
36
|
+
readonly halfWidth: number;
|
|
37
|
+
readonly halfDepth: number;
|
|
38
|
+
readonly halfHeight?: number;
|
|
39
|
+
readonly offset?: Vec3;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "point";
|
|
42
|
+
};
|
|
43
|
+
/** Conservative default: a unit sphere. Objects without explicit bounds cull as a small volume, never a point. */
|
|
44
|
+
export declare const DEFAULT_BOUNDS: BoundsSpec;
|
|
45
|
+
export declare function createRenderBounds(): RenderBounds;
|
|
46
|
+
/** Resolve a spec + world position into concrete bounds. Writes into `out` when supplied (no allocation). */
|
|
47
|
+
export declare function resolveBounds(spec: BoundsSpec, position: Vec3, out?: RenderBounds): RenderBounds;
|
|
48
|
+
export declare function expandRenderBounds(bounds: RenderBounds, margin: number, out?: RenderBounds): RenderBounds;
|
|
49
|
+
export declare function aabbIntersects(aMinX: number, aMinY: number, aMinZ: number, aMaxX: number, aMaxY: number, aMaxZ: number, bMinX: number, bMinY: number, bMinZ: number, bMaxX: number, bMaxY: number, bMaxZ: number): boolean;
|
|
50
|
+
export declare function boundsIntersect(a: RenderBounds, b: RenderBounds): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* BoundsSystem: caches resolved bounds per object and recomputes only when the caller's
|
|
53
|
+
* version advances — i.e. when a transform, geometry, or the bounds spec itself changed.
|
|
54
|
+
* The cached `RenderBounds` object is reused across recomputes, so a steady-state frame
|
|
55
|
+
* that touches thousands of objects performs zero bounds allocations.
|
|
56
|
+
*/
|
|
57
|
+
export interface BoundsCache {
|
|
58
|
+
get(id: string, version: number, spec: BoundsSpec, position: Vec3): RenderBounds;
|
|
59
|
+
peek(id: string): RenderBounds | undefined;
|
|
60
|
+
invalidate(id: string): void;
|
|
61
|
+
delete(id: string): void;
|
|
62
|
+
clear(): void;
|
|
63
|
+
size(): number;
|
|
64
|
+
recomputes(): number;
|
|
65
|
+
}
|
|
66
|
+
export declare function createBoundsCache(): BoundsCache;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Conservative default: a unit sphere. Objects without explicit bounds cull as a small volume, never a point. */
|
|
2
|
+
export const DEFAULT_BOUNDS = { kind: "sphere", radius: 1 };
|
|
3
|
+
export function createRenderBounds() {
|
|
4
|
+
return {
|
|
5
|
+
centerX: 0,
|
|
6
|
+
centerY: 0,
|
|
7
|
+
centerZ: 0,
|
|
8
|
+
radius: 0,
|
|
9
|
+
minX: 0,
|
|
10
|
+
minY: 0,
|
|
11
|
+
minZ: 0,
|
|
12
|
+
maxX: 0,
|
|
13
|
+
maxY: 0,
|
|
14
|
+
maxZ: 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function fillBounds(out, cx, cy, cz, hx, hy, hz) {
|
|
18
|
+
out.centerX = cx;
|
|
19
|
+
out.centerY = cy;
|
|
20
|
+
out.centerZ = cz;
|
|
21
|
+
out.minX = cx - hx;
|
|
22
|
+
out.minY = cy - hy;
|
|
23
|
+
out.minZ = cz - hz;
|
|
24
|
+
out.maxX = cx + hx;
|
|
25
|
+
out.maxY = cy + hy;
|
|
26
|
+
out.maxZ = cz + hz;
|
|
27
|
+
out.radius = Math.sqrt(hx * hx + hy * hy + hz * hz);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/** Resolve a spec + world position into concrete bounds. Writes into `out` when supplied (no allocation). */
|
|
31
|
+
export function resolveBounds(spec, position, out = createRenderBounds()) {
|
|
32
|
+
if (spec.kind === "point") {
|
|
33
|
+
return fillBounds(out, position[0], position[1], position[2], 0, 0, 0);
|
|
34
|
+
}
|
|
35
|
+
const offset = spec.offset;
|
|
36
|
+
const cx = position[0] + (offset?.[0] ?? 0);
|
|
37
|
+
const cy = position[1] + (offset?.[1] ?? 0);
|
|
38
|
+
const cz = position[2] + (offset?.[2] ?? 0);
|
|
39
|
+
switch (spec.kind) {
|
|
40
|
+
case "sphere": {
|
|
41
|
+
const r = spec.radius;
|
|
42
|
+
fillBounds(out, cx, cy, cz, r, r, r);
|
|
43
|
+
out.radius = r;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
case "aabb":
|
|
47
|
+
return fillBounds(out, cx, cy, cz, spec.half[0], spec.half[1], spec.half[2]);
|
|
48
|
+
case "rect":
|
|
49
|
+
return fillBounds(out, cx, cy, cz, spec.halfWidth, spec.halfHeight ?? 0, spec.halfDepth);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function expandRenderBounds(bounds, margin, out = createRenderBounds()) {
|
|
53
|
+
out.centerX = bounds.centerX;
|
|
54
|
+
out.centerY = bounds.centerY;
|
|
55
|
+
out.centerZ = bounds.centerZ;
|
|
56
|
+
out.radius = bounds.radius + margin;
|
|
57
|
+
out.minX = bounds.minX - margin;
|
|
58
|
+
out.minY = bounds.minY - margin;
|
|
59
|
+
out.minZ = bounds.minZ - margin;
|
|
60
|
+
out.maxX = bounds.maxX + margin;
|
|
61
|
+
out.maxY = bounds.maxY + margin;
|
|
62
|
+
out.maxZ = bounds.maxZ + margin;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export function aabbIntersects(aMinX, aMinY, aMinZ, aMaxX, aMaxY, aMaxZ, bMinX, bMinY, bMinZ, bMaxX, bMaxY, bMaxZ) {
|
|
66
|
+
return (aMinX <= bMaxX && aMaxX >= bMinX &&
|
|
67
|
+
aMinY <= bMaxY && aMaxY >= bMinY &&
|
|
68
|
+
aMinZ <= bMaxZ && aMaxZ >= bMinZ);
|
|
69
|
+
}
|
|
70
|
+
export function boundsIntersect(a, b) {
|
|
71
|
+
return aabbIntersects(a.minX, a.minY, a.minZ, a.maxX, a.maxY, a.maxZ, b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ);
|
|
72
|
+
}
|
|
73
|
+
export function createBoundsCache() {
|
|
74
|
+
const entries = new Map();
|
|
75
|
+
let recomputes = 0;
|
|
76
|
+
return {
|
|
77
|
+
get(id, version, spec, position) {
|
|
78
|
+
const existing = entries.get(id);
|
|
79
|
+
if (existing !== undefined && existing.version === version)
|
|
80
|
+
return existing.bounds;
|
|
81
|
+
if (existing === undefined) {
|
|
82
|
+
const bounds = resolveBounds(spec, position);
|
|
83
|
+
entries.set(id, { version, bounds });
|
|
84
|
+
recomputes += 1;
|
|
85
|
+
return bounds;
|
|
86
|
+
}
|
|
87
|
+
resolveBounds(spec, position, existing.bounds);
|
|
88
|
+
existing.version = version;
|
|
89
|
+
recomputes += 1;
|
|
90
|
+
return existing.bounds;
|
|
91
|
+
},
|
|
92
|
+
peek(id) {
|
|
93
|
+
return entries.get(id)?.bounds;
|
|
94
|
+
},
|
|
95
|
+
invalidate(id) {
|
|
96
|
+
const existing = entries.get(id);
|
|
97
|
+
if (existing !== undefined)
|
|
98
|
+
existing.version = Number.NaN;
|
|
99
|
+
},
|
|
100
|
+
delete(id) {
|
|
101
|
+
entries.delete(id);
|
|
102
|
+
},
|
|
103
|
+
clear() {
|
|
104
|
+
entries.clear();
|
|
105
|
+
},
|
|
106
|
+
size() {
|
|
107
|
+
return entries.size;
|
|
108
|
+
},
|
|
109
|
+
recomputes() {
|
|
110
|
+
return recomputes;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CameraView } from "./frustum.js";
|
|
2
|
+
export type CameraKind = "main" | "splitScreen" | "renderToTexture" | "editor" | "minimap";
|
|
3
|
+
/**
|
|
4
|
+
* A camera's contribution to visibility. The VisibilitySystem unions results across every
|
|
5
|
+
* active context: an object stays renderable/loaded if *any* relevant camera needs it.
|
|
6
|
+
* A camera can opt out of driving asset streaming (e.g. a minimap that only needs positions,
|
|
7
|
+
* not loaded meshes) via `influencesStreaming: false`.
|
|
8
|
+
*/
|
|
9
|
+
export interface CameraVisibilityContext {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly view: CameraView;
|
|
12
|
+
/** Default "main". Editor/minimap cameras are still honored but tagged for diagnostics. */
|
|
13
|
+
readonly kind?: CameraKind;
|
|
14
|
+
/** Default true. When false this camera keeps objects renderable but does not preload their assets. */
|
|
15
|
+
readonly influencesStreaming?: boolean;
|
|
16
|
+
/** When true this camera renders everything (no frustum/distance reject). Default false. */
|
|
17
|
+
readonly cullingDisabled?: boolean;
|
|
18
|
+
/** Overrides the global render distance for this camera. */
|
|
19
|
+
readonly maxRenderDistance?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function cameraInfluencesStreaming(context: CameraVisibilityContext): boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CullingSettings, StreamingSettings, ObjectVisibilityOverrides } from "./settings.js";
|
|
2
|
+
/**
|
|
3
|
+
* Per-game visibility configuration, surfaced on `PlayableGame.visibility`. Everything is
|
|
4
|
+
* optional: an existing game that sets nothing gets the conservative engine defaults
|
|
5
|
+
* automatically. This is the scene-level and per-kind override seam (requirement: per-object,
|
|
6
|
+
* per-layer, per-scene, and global controls).
|
|
7
|
+
*/
|
|
8
|
+
export interface VisibilityConfig {
|
|
9
|
+
/** Master switch for render culling in the shell. Default true. */
|
|
10
|
+
readonly enabled?: boolean;
|
|
11
|
+
/** Overrides for the global culling defaults (margins, distances, hysteresis, occlusion flag). */
|
|
12
|
+
readonly culling?: Partial<CullingSettings>;
|
|
13
|
+
/** Overrides for the asset-streaming defaults. */
|
|
14
|
+
readonly streaming?: Partial<StreamingSettings>;
|
|
15
|
+
/** Scene-wide overrides applied to every object. */
|
|
16
|
+
readonly scene?: ObjectVisibilityOverrides;
|
|
17
|
+
/** Overrides keyed by entity kind name. */
|
|
18
|
+
readonly entities?: Record<string, ObjectVisibilityOverrides>;
|
|
19
|
+
/** Overrides keyed by placed-object catalog id. */
|
|
20
|
+
readonly objects?: Record<string, ObjectVisibilityOverrides>;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-frame visibility + streaming counters. The VisibilitySystem fills the culling and
|
|
3
|
+
* timing fields; the AssetStreamingSystem fills the asset fields. Everything is a plain
|
|
4
|
+
* number so a HUD or the devtools hub can read it without adapters.
|
|
5
|
+
*/
|
|
6
|
+
export interface VisibilityStats {
|
|
7
|
+
totalObjects: number;
|
|
8
|
+
consideredForRender: number;
|
|
9
|
+
rejectedByFrustum: number;
|
|
10
|
+
rejectedByDistance: number;
|
|
11
|
+
rejectedByOcclusion: number;
|
|
12
|
+
drawCallsAvoided: number;
|
|
13
|
+
visible: number;
|
|
14
|
+
preloaded: number;
|
|
15
|
+
assetsQueued: number;
|
|
16
|
+
assetsLoading: number;
|
|
17
|
+
assetsLoaded: number;
|
|
18
|
+
assetsUnloaded: number;
|
|
19
|
+
streamedBytes: number;
|
|
20
|
+
spatialQueryMs: number;
|
|
21
|
+
cullingMs: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function createVisibilityStats(): VisibilityStats;
|
|
24
|
+
export declare function resetCullingStats(stats: VisibilityStats): void;
|
|
25
|
+
/** High-resolution clock that degrades to a monotonic-ish fallback when `performance` is absent. */
|
|
26
|
+
export declare function nowMs(): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function createVisibilityStats() {
|
|
2
|
+
return {
|
|
3
|
+
totalObjects: 0,
|
|
4
|
+
consideredForRender: 0,
|
|
5
|
+
rejectedByFrustum: 0,
|
|
6
|
+
rejectedByDistance: 0,
|
|
7
|
+
rejectedByOcclusion: 0,
|
|
8
|
+
drawCallsAvoided: 0,
|
|
9
|
+
visible: 0,
|
|
10
|
+
preloaded: 0,
|
|
11
|
+
assetsQueued: 0,
|
|
12
|
+
assetsLoading: 0,
|
|
13
|
+
assetsLoaded: 0,
|
|
14
|
+
assetsUnloaded: 0,
|
|
15
|
+
streamedBytes: 0,
|
|
16
|
+
spatialQueryMs: 0,
|
|
17
|
+
cullingMs: 0,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function resetCullingStats(stats) {
|
|
21
|
+
stats.totalObjects = 0;
|
|
22
|
+
stats.consideredForRender = 0;
|
|
23
|
+
stats.rejectedByFrustum = 0;
|
|
24
|
+
stats.rejectedByDistance = 0;
|
|
25
|
+
stats.rejectedByOcclusion = 0;
|
|
26
|
+
stats.drawCallsAvoided = 0;
|
|
27
|
+
stats.visible = 0;
|
|
28
|
+
stats.preloaded = 0;
|
|
29
|
+
stats.spatialQueryMs = 0;
|
|
30
|
+
stats.cullingMs = 0;
|
|
31
|
+
}
|
|
32
|
+
/** High-resolution clock that degrades to a monotonic-ish fallback when `performance` is absent. */
|
|
33
|
+
export function nowMs() {
|
|
34
|
+
const perf = globalThis.performance;
|
|
35
|
+
return perf !== undefined ? perf.now() : 0;
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Vec3 } from "./bounds.js";
|
|
2
|
+
export declare function distanceSquared(ax: number, ay: number, az: number, bx: number, by: number, bz: number): number;
|
|
3
|
+
/**
|
|
4
|
+
* Whether an object at `objectCenter` should be distance-culled from `cameraPos`.
|
|
5
|
+
* Compares squared distances against squared thresholds — no per-object square root.
|
|
6
|
+
* `nearestSurface` subtracts the object radius so a large object is not culled by the
|
|
7
|
+
* distance to its far side. `hysteresis` widens the max band by a fixed world-unit
|
|
8
|
+
* band for objects that were visible last check, so they don't flicker at the boundary.
|
|
9
|
+
*/
|
|
10
|
+
export declare function culledByDistance(cameraX: number, cameraY: number, cameraZ: number, objectX: number, objectY: number, objectZ: number, minDistance: number, maxDistance: number, radius?: number, hysteresis?: number): boolean;
|
|
11
|
+
export declare function distance(a: Vec3, b: Vec3): number;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function distanceSquared(ax, ay, az, bx, by, bz) {
|
|
2
|
+
const dx = bx - ax;
|
|
3
|
+
const dy = by - ay;
|
|
4
|
+
const dz = bz - az;
|
|
5
|
+
return dx * dx + dy * dy + dz * dz;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether an object at `objectCenter` should be distance-culled from `cameraPos`.
|
|
9
|
+
* Compares squared distances against squared thresholds — no per-object square root.
|
|
10
|
+
* `nearestSurface` subtracts the object radius so a large object is not culled by the
|
|
11
|
+
* distance to its far side. `hysteresis` widens the max band by a fixed world-unit
|
|
12
|
+
* band for objects that were visible last check, so they don't flicker at the boundary.
|
|
13
|
+
*/
|
|
14
|
+
export function culledByDistance(cameraX, cameraY, cameraZ, objectX, objectY, objectZ, minDistance, maxDistance, radius = 0, hysteresis = 0) {
|
|
15
|
+
const distSq = distanceSquared(cameraX, cameraY, cameraZ, objectX, objectY, objectZ);
|
|
16
|
+
if (minDistance > 0) {
|
|
17
|
+
const nearFar = minDistance + radius;
|
|
18
|
+
if (distSq < nearFar * nearFar)
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (maxDistance !== Infinity && maxDistance !== Number.POSITIVE_INFINITY) {
|
|
22
|
+
const farNear = maxDistance + radius + hysteresis;
|
|
23
|
+
if (distSq > farNear * farNear)
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export function distance(a, b) {
|
|
29
|
+
return Math.sqrt(distanceSquared(a[0], a[1], a[2], b[0], b[1], b[2]));
|
|
30
|
+
}
|