@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
package/dist/multiplayer.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { adapterOf, isServerAuthoritative } from "@jgengine/core/runtime/adapter";
|
|
2
|
+
import { createHostedWorldSession, createHostedWorldSessionAsync } from "@jgengine/core/runtime/hostedWorldSession";
|
|
3
|
+
import { createWorldGameHost } from "@jgengine/ws/worldHost";
|
|
2
4
|
import { createWsBackend } from "@jgengine/ws/createWsBackend";
|
|
3
5
|
import { announcePeerHost, broadcastChannelSignaling, createPeerGuest, createPeerHost, joinPeerSession, } from "@jgengine/ws/peer";
|
|
4
6
|
export const DEFAULT_FEED_ACTIONS = ["entity.died"];
|
|
@@ -47,35 +49,116 @@ export function resolveShellMultiplayer(args) {
|
|
|
47
49
|
`so the game shipped single-player. Wire up the ${adapter.kind} transport or switch to ws()/lan().`);
|
|
48
50
|
return null;
|
|
49
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Opens a peer session. With `playable`, one host runs the game loop and guests mirror its shared world.
|
|
54
|
+
* Await `close()` to finish authoritative persistence. Same-origin rooms use BroadcastChannel signaling.
|
|
55
|
+
* @capability multiplayer-p2p bootstrap a playable authoritative world for browser peer co-op
|
|
56
|
+
*/
|
|
50
57
|
export async function resolvePeerShellMultiplayer(args) {
|
|
58
|
+
if (args.playable !== undefined && !isServerAuthoritative(args.playable.game.multiplayer)) {
|
|
59
|
+
throw new Error("playable peer worlds require authority: server");
|
|
60
|
+
}
|
|
61
|
+
const tickMs = args.tickMs ?? 1000 / 30;
|
|
62
|
+
if (!Number.isFinite(tickMs) || tickMs <= 0 || tickMs > 1000)
|
|
63
|
+
throw new Error("tickMs must be greater than zero and at most 1000");
|
|
64
|
+
const saveIntervalMs = args.saveIntervalMs ?? 1000;
|
|
65
|
+
if (!Number.isFinite(saveIntervalMs) || saveIntervalMs < 0)
|
|
66
|
+
throw new Error("saveIntervalMs must be nonnegative and finite");
|
|
51
67
|
const userId = args.userId ?? randomPlayerId();
|
|
52
68
|
const feedActions = args.feedActions ?? DEFAULT_FEED_ACTIONS;
|
|
53
|
-
const signaling = broadcastChannelSignaling(args.room ?? `jg-p2p-${args.gameId}`);
|
|
69
|
+
const signaling = args.signaling ?? broadcastChannelSignaling(args.room ?? `jg-p2p-${args.gameId}`);
|
|
70
|
+
const peers = args.peers ?? { host: createPeerHost, guest: createPeerGuest };
|
|
54
71
|
if (args.role === "host") {
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
let closed = false;
|
|
73
|
+
let activeSession = null;
|
|
74
|
+
let host;
|
|
75
|
+
let cancelTicks = () => { };
|
|
76
|
+
let stopAnnouncing = () => { };
|
|
77
|
+
let peerHost;
|
|
78
|
+
let closing = null;
|
|
79
|
+
try {
|
|
80
|
+
if (args.playable !== undefined) {
|
|
81
|
+
const playable = args.playable;
|
|
82
|
+
let loading = null;
|
|
83
|
+
host = createWorldGameHost({
|
|
84
|
+
slotsPerServer: args.slotsPerServer,
|
|
85
|
+
session({ gameId, serverId }) {
|
|
86
|
+
if (closed || gameId !== args.gameId || serverId !== args.gameId)
|
|
87
|
+
return null;
|
|
88
|
+
if (loading === null) {
|
|
89
|
+
const attempt = args.store === undefined
|
|
90
|
+
? Promise.resolve(createHostedWorldSession({ definition: playable.game, content: playable.content, host: { userId, isNew: true }, saveIntervalMs }))
|
|
91
|
+
: createHostedWorldSessionAsync({ definition: playable.game, content: playable.content, host: { userId, isNew: true }, store: args.store, saveIntervalMs });
|
|
92
|
+
loading = attempt;
|
|
93
|
+
void attempt.catch(() => { if (loading === attempt)
|
|
94
|
+
loading = null; });
|
|
95
|
+
}
|
|
96
|
+
return loading.then(session => { activeSession = session; return session; });
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
peerHost = peers.host({ userId, ...(host === undefined ? {} : { host }) });
|
|
101
|
+
stopAnnouncing = announcePeerHost(peerHost, signaling);
|
|
102
|
+
if (host !== undefined) {
|
|
103
|
+
const worldHost = host;
|
|
104
|
+
const tick = () => { if (!closed && activeSession !== null && activeSession.members().length > 0)
|
|
105
|
+
worldHost.tick(tickMs / 1000); };
|
|
106
|
+
cancelTicks = args.scheduleTicks?.(tick, tickMs) ?? (() => {
|
|
107
|
+
const timer = setInterval(tick, tickMs);
|
|
108
|
+
return () => clearInterval(timer);
|
|
109
|
+
})();
|
|
110
|
+
}
|
|
111
|
+
const resolvedPeerHost = peerHost;
|
|
112
|
+
return {
|
|
113
|
+
gameId: args.gameId, userId, backend: resolvedPeerHost.backend, feedActions,
|
|
114
|
+
close() {
|
|
115
|
+
if (closing !== null)
|
|
116
|
+
return closing;
|
|
117
|
+
closed = true;
|
|
118
|
+
cancelTicks();
|
|
119
|
+
stopAnnouncing();
|
|
120
|
+
signaling.close();
|
|
121
|
+
resolvedPeerHost.close();
|
|
122
|
+
closing = resolvedPeerHost.router.drain().then(async () => {
|
|
123
|
+
if (host !== undefined)
|
|
124
|
+
await host.stop();
|
|
125
|
+
});
|
|
126
|
+
return closing;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
closed = true;
|
|
132
|
+
cancelTicks();
|
|
133
|
+
stopAnnouncing();
|
|
134
|
+
signaling.close();
|
|
135
|
+
peerHost?.close();
|
|
136
|
+
await peerHost?.router.drain();
|
|
137
|
+
if (host !== undefined)
|
|
138
|
+
await host.stop();
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let guest;
|
|
143
|
+
try {
|
|
144
|
+
guest = peers.guest({ userId });
|
|
145
|
+
const backend = await joinPeerSession(guest, signaling);
|
|
146
|
+
const resolvedGuest = guest;
|
|
147
|
+
let closed = false;
|
|
57
148
|
return {
|
|
58
|
-
gameId: args.gameId,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
stopAnnouncing();
|
|
149
|
+
gameId: args.gameId, userId, backend, feedActions,
|
|
150
|
+
async close() {
|
|
151
|
+
if (closed)
|
|
152
|
+
return;
|
|
153
|
+
closed = true;
|
|
64
154
|
signaling.close();
|
|
65
|
-
|
|
155
|
+
resolvedGuest.close();
|
|
66
156
|
},
|
|
67
157
|
};
|
|
68
158
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
backend,
|
|
75
|
-
feedActions,
|
|
76
|
-
close: () => {
|
|
77
|
-
signaling.close();
|
|
78
|
-
guest.close();
|
|
79
|
-
},
|
|
80
|
-
};
|
|
159
|
+
catch (error) {
|
|
160
|
+
signaling.close();
|
|
161
|
+
guest?.close();
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
81
164
|
}
|
|
@@ -2,6 +2,8 @@ import * as THREE from "three";
|
|
|
2
2
|
import type { PointerHit, PointerVec3 } from "@jgengine/core/input/pointer";
|
|
3
3
|
export declare const POINTER_ENTITY_KEY = "jgEntityId";
|
|
4
4
|
export declare const POINTER_OBJECT_KEY = "jgObjectId";
|
|
5
|
+
/** Object-store identities indexed by the current compacted GPU instance slot. @internal */
|
|
6
|
+
export declare const POINTER_OBJECT_INSTANCES_KEY = "jgObjectInstances";
|
|
5
7
|
export type PointerHitFilter = (object: THREE.Object3D) => boolean;
|
|
6
8
|
interface PointerDeps {
|
|
7
9
|
camera: THREE.Camera;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
export const POINTER_ENTITY_KEY = "jgEntityId";
|
|
3
3
|
export const POINTER_OBJECT_KEY = "jgObjectId";
|
|
4
|
+
/** Object-store identities indexed by the current compacted GPU instance slot. @internal */
|
|
5
|
+
export const POINTER_OBJECT_INSTANCES_KEY = "jgObjectInstances";
|
|
4
6
|
function tagOf(object, key) {
|
|
5
7
|
let cursor = object;
|
|
6
8
|
while (cursor !== null) {
|
|
@@ -24,6 +26,8 @@ export function createPointerService() {
|
|
|
24
26
|
const centerNdc = new THREE.Vector2(0, 0);
|
|
25
27
|
const scratch = new THREE.Vector3();
|
|
26
28
|
const normalMatrix = new THREE.Matrix3();
|
|
29
|
+
const instanceMatrix = new THREE.Matrix4();
|
|
30
|
+
const hitMatrix = new THREE.Matrix4();
|
|
27
31
|
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
28
32
|
let deps = null;
|
|
29
33
|
let cursorPresent = false;
|
|
@@ -45,7 +49,12 @@ export function createPointerService() {
|
|
|
45
49
|
const point = [hit.point.x, hit.point.y, hit.point.z];
|
|
46
50
|
let normal = [0, 1, 0];
|
|
47
51
|
if (hit.face !== null && hit.face !== undefined) {
|
|
48
|
-
|
|
52
|
+
hitMatrix.copy(hit.object.matrixWorld);
|
|
53
|
+
if (mesh.isInstancedMesh && hit.instanceId !== undefined) {
|
|
54
|
+
mesh.getMatrixAt(hit.instanceId, instanceMatrix);
|
|
55
|
+
hitMatrix.multiply(instanceMatrix);
|
|
56
|
+
}
|
|
57
|
+
normalMatrix.getNormalMatrix(hitMatrix);
|
|
49
58
|
scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
|
|
50
59
|
normal = [scratch.x, scratch.y, scratch.z];
|
|
51
60
|
}
|
|
@@ -54,7 +63,8 @@ export function createPointerService() {
|
|
|
54
63
|
point,
|
|
55
64
|
normal,
|
|
56
65
|
entity: tagOf(hit.object, POINTER_ENTITY_KEY),
|
|
57
|
-
object: tagOf(hit.object, POINTER_OBJECT_KEY)
|
|
66
|
+
object: hit.instanceId === undefined ? tagOf(hit.object, POINTER_OBJECT_KEY) :
|
|
67
|
+
hit.object.userData[POINTER_OBJECT_INSTANCES_KEY]?.[hit.instanceId] ?? tagOf(hit.object, POINTER_OBJECT_KEY),
|
|
58
68
|
...(uv !== undefined ? { uv } : {}),
|
|
59
69
|
material: standardMaterialSample(mesh.material),
|
|
60
70
|
...(hit.instanceId !== undefined ? { instanceId: hit.instanceId } : {}),
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import { EffectComposer, type ShaderPass } from "three-stdlib";
|
|
1
2
|
import type { PostProcessingConfig } from "@jgengine/core/render/postProcessing";
|
|
2
3
|
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
4
|
+
interface BuiltGraph {
|
|
5
|
+
composer: EffectComposer;
|
|
6
|
+
grade: ShaderPass | null;
|
|
7
|
+
}
|
|
8
|
+
/** Release each pass and the composer's owned targets. @internal */
|
|
9
|
+
export declare function disposeGraph(built: BuiltGraph): void;
|
|
10
|
+
/** Resize the composer and its passes at the renderer's pixel ratio. @internal */
|
|
11
|
+
export declare function syncSize(built: BuiltGraph, width: number, height: number, pixelRatio: number): void;
|
|
3
12
|
/**
|
|
4
13
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
5
14
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
@@ -20,3 +29,4 @@ export declare function PostProcessing({ config, quality, stages }: {
|
|
|
20
29
|
smaa: boolean;
|
|
21
30
|
};
|
|
22
31
|
}): null;
|
|
32
|
+
export {};
|
|
@@ -8,7 +8,7 @@ import { BokehPass } from "three/examples/jsm/postprocessing/BokehPass.js";
|
|
|
8
8
|
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
|
|
9
9
|
import { createGradePass } from "./gradeShader.js";
|
|
10
10
|
import { StylizePass } from "./stylizePass.js";
|
|
11
|
-
import {
|
|
11
|
+
import { renderShadowlessPrepass } from "./shadowPrepass.js";
|
|
12
12
|
/**
|
|
13
13
|
* GTAO renders the scene with an `overrideMaterial` normal/depth prepass, which stamps
|
|
14
14
|
* transparent overlay quads (additive VFX sprites, telegraphs, debug gizmos) into the AO
|
|
@@ -19,18 +19,14 @@ import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
|
|
|
19
19
|
class OverlayAwareGTAOPass extends GTAOPass {
|
|
20
20
|
hiddenOverlays = [];
|
|
21
21
|
render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
|
|
22
|
-
|
|
23
|
-
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive);
|
|
24
|
-
restorePostfxOverlays(this.hiddenOverlays);
|
|
22
|
+
renderShadowlessPrepass(renderer, this.scene, this.hiddenOverlays, () => super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive));
|
|
25
23
|
}
|
|
26
24
|
}
|
|
27
25
|
/** DOF's depth prepass has the same `overrideMaterial` overlay-stamping problem as GTAO. */
|
|
28
26
|
class OverlayAwareBokehPass extends BokehPass {
|
|
29
27
|
hiddenOverlays = [];
|
|
30
28
|
render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) {
|
|
31
|
-
|
|
32
|
-
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive);
|
|
33
|
-
restorePostfxOverlays(this.hiddenOverlays);
|
|
29
|
+
renderShadowlessPrepass(renderer, this.scene, this.hiddenOverlays, () => super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive));
|
|
34
30
|
}
|
|
35
31
|
}
|
|
36
32
|
const TONE_MAPPING = {
|
|
@@ -41,22 +37,16 @@ const TONE_MAPPING = {
|
|
|
41
37
|
linear: THREE.LinearToneMapping,
|
|
42
38
|
none: THREE.NoToneMapping,
|
|
43
39
|
};
|
|
44
|
-
|
|
40
|
+
/** Release each pass and the composer's owned targets. @internal */
|
|
41
|
+
export function disposeGraph(built) {
|
|
45
42
|
for (const pass of built.composer.passes)
|
|
46
43
|
pass.dispose();
|
|
47
|
-
if (built.dof !== null) {
|
|
48
|
-
built.dof.renderTargetDepth.dispose();
|
|
49
|
-
built.dof.materialDepth.dispose();
|
|
50
|
-
built.dof.materialBokeh.dispose();
|
|
51
|
-
built.dof.fsQuad.dispose();
|
|
52
|
-
}
|
|
53
44
|
built.composer.dispose();
|
|
54
45
|
}
|
|
55
|
-
|
|
46
|
+
/** Resize the composer and its passes at the renderer's pixel ratio. @internal */
|
|
47
|
+
export function syncSize(built, width, height, pixelRatio) {
|
|
56
48
|
built.composer.setPixelRatio(pixelRatio);
|
|
57
49
|
built.composer.setSize(width, height);
|
|
58
|
-
if (built.dof !== null)
|
|
59
|
-
built.dof.renderTargetDepth.setSize(width * pixelRatio, height * pixelRatio);
|
|
60
50
|
}
|
|
61
51
|
/**
|
|
62
52
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
@@ -102,10 +92,9 @@ export function PostProcessing({ config, quality = "high", stages }) {
|
|
|
102
92
|
const b = config.bloom ?? {};
|
|
103
93
|
composer.addPass(new UnrealBloomPass(new THREE.Vector2(width, height), b.strength ?? 0.32, b.radius ?? 0.55, b.threshold ?? 0.85));
|
|
104
94
|
}
|
|
105
|
-
let dof = null;
|
|
106
95
|
if (resolvedStages.dof && config.dof !== undefined && config.dof !== false) {
|
|
107
96
|
const d = config.dof;
|
|
108
|
-
dof = new OverlayAwareBokehPass(scene, camera, {
|
|
97
|
+
const dof = new OverlayAwareBokehPass(scene, camera, {
|
|
109
98
|
focus: d.focus ?? 18,
|
|
110
99
|
aperture: d.aperture ?? (quality === "high" ? 0.00025 : 0.00012),
|
|
111
100
|
maxblur: d.maxBlur ?? 0.01,
|
|
@@ -125,7 +114,7 @@ export function PostProcessing({ config, quality = "high", stages }) {
|
|
|
125
114
|
grade = createGradePass(config.grade ?? {});
|
|
126
115
|
composer.addPass(grade);
|
|
127
116
|
}
|
|
128
|
-
const graph = { composer, grade
|
|
117
|
+
const graph = { composer, grade };
|
|
129
118
|
syncSize(graph, width, height, gl.getPixelRatio());
|
|
130
119
|
builtRef.current = graph;
|
|
131
120
|
return () => {
|
|
@@ -133,27 +122,6 @@ export function PostProcessing({ config, quality = "high", stages }) {
|
|
|
133
122
|
builtRef.current = null;
|
|
134
123
|
};
|
|
135
124
|
}, [gl, scene, camera, config, quality, stages]);
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
const lut = config.grade !== undefined && config.grade !== false ? config.grade.lut : undefined;
|
|
138
|
-
const grade = builtRef.current?.grade;
|
|
139
|
-
if (lut === undefined || grade === null || grade === undefined)
|
|
140
|
-
return;
|
|
141
|
-
let cancelled = false;
|
|
142
|
-
const loader = new THREE.TextureLoader();
|
|
143
|
-
loader.loadAsync(lut.url).then((texture) => {
|
|
144
|
-
if (cancelled) {
|
|
145
|
-
texture.dispose();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
texture.colorSpace = THREE.SRGBColorSpace;
|
|
149
|
-
texture.minFilter = THREE.NearestFilter;
|
|
150
|
-
texture.magFilter = THREE.NearestFilter;
|
|
151
|
-
texture.generateMipmaps = false;
|
|
152
|
-
grade.uniforms.uLut.value = texture;
|
|
153
|
-
grade.uniforms.uLutSize.value = lut.size ?? 32;
|
|
154
|
-
}).catch(() => undefined);
|
|
155
|
-
return () => { cancelled = true; };
|
|
156
|
-
}, [config.grade]);
|
|
157
125
|
useEffect(() => {
|
|
158
126
|
const prevTone = gl.toneMapping;
|
|
159
127
|
const prevExposure = gl.toneMappingExposure;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { ShaderPass } from "three-stdlib";
|
|
2
2
|
import type { GradeConfig } from "@jgengine/core/render/postProcessing";
|
|
3
|
-
/** Build the display-space colour-grade pass
|
|
3
|
+
/** Build the display-space colour-grade pass and load its optional LUT. Dispose the pass to release its LUT; advance `uniforms.uTime.value` each frame to animate grain. */
|
|
4
4
|
export declare function createGradePass(config?: GradeConfig): ShaderPass;
|
|
@@ -61,11 +61,11 @@ const vertexShader = /* glsl */ `
|
|
|
61
61
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
62
62
|
}
|
|
63
63
|
`;
|
|
64
|
-
/** Build the display-space colour-grade pass
|
|
64
|
+
/** Build the display-space colour-grade pass and load its optional LUT. Dispose the pass to release its LUT; advance `uniforms.uTime.value` each frame to animate grain. */
|
|
65
65
|
export function createGradePass(config = {}) {
|
|
66
66
|
const lift = config.lift ?? DEFAULT_LIFT;
|
|
67
67
|
const gain = config.gain ?? DEFAULT_GAIN;
|
|
68
|
-
|
|
68
|
+
const pass = new ShaderPass({
|
|
69
69
|
uniforms: {
|
|
70
70
|
tDiffuse: { value: null },
|
|
71
71
|
uLift: { value: new THREE.Vector3(lift[0], lift[1], lift[2]) },
|
|
@@ -82,4 +82,32 @@ export function createGradePass(config = {}) {
|
|
|
82
82
|
vertexShader,
|
|
83
83
|
fragmentShader,
|
|
84
84
|
});
|
|
85
|
+
let disposed = false;
|
|
86
|
+
let lutTexture = null;
|
|
87
|
+
const dispose = pass.dispose.bind(pass);
|
|
88
|
+
pass.dispose = () => {
|
|
89
|
+
if (disposed)
|
|
90
|
+
return;
|
|
91
|
+
disposed = true;
|
|
92
|
+
lutTexture?.dispose();
|
|
93
|
+
lutTexture = null;
|
|
94
|
+
dispose();
|
|
95
|
+
};
|
|
96
|
+
if (config.lut !== undefined) {
|
|
97
|
+
const lut = config.lut;
|
|
98
|
+
void new THREE.TextureLoader().loadAsync(lut.url).then((texture) => {
|
|
99
|
+
if (disposed) {
|
|
100
|
+
texture.dispose();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
lutTexture = texture;
|
|
104
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
105
|
+
texture.minFilter = THREE.NearestFilter;
|
|
106
|
+
texture.magFilter = THREE.NearestFilter;
|
|
107
|
+
texture.generateMipmaps = false;
|
|
108
|
+
pass.uniforms.uLut.value = texture;
|
|
109
|
+
pass.uniforms.uLutSize.value = lut.size ?? 32;
|
|
110
|
+
}).catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
return pass;
|
|
85
113
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
/** Reuse the beauty pass's shadow maps while drawing a scene prepass. @internal */
|
|
3
|
+
export declare function renderShadowlessPrepass<T>(renderer: Pick<THREE.WebGLRenderer, "shadowMap">, scene: THREE.Object3D, hiddenOverlays: THREE.Object3D[], render: () => T): T;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
|
|
2
|
+
/** Reuse the beauty pass's shadow maps while drawing a scene prepass. @internal */
|
|
3
|
+
export function renderShadowlessPrepass(renderer, scene, hiddenOverlays, render) {
|
|
4
|
+
const shadowMap = renderer.shadowMap;
|
|
5
|
+
const autoUpdate = shadowMap.autoUpdate;
|
|
6
|
+
const needsUpdate = shadowMap.needsUpdate;
|
|
7
|
+
hidePostfxOverlays(scene, hiddenOverlays);
|
|
8
|
+
shadowMap.autoUpdate = false;
|
|
9
|
+
shadowMap.needsUpdate = false;
|
|
10
|
+
try {
|
|
11
|
+
return render();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
shadowMap.autoUpdate = autoUpdate;
|
|
15
|
+
shadowMap.needsUpdate = needsUpdate;
|
|
16
|
+
restorePostfxOverlays(hiddenOverlays);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
/** Marks only a rejected presentation import for page reload; browser module failures are cached. @internal */
|
|
4
|
+
export declare function markPresentationCodeLoadFailure(error: unknown): Error;
|
|
5
|
+
/** @internal Distinguishes module loading from recoverable assets without guessing from error text. */
|
|
6
|
+
export declare function presentationRecoveryAction(error: unknown): "reload" | "retry";
|
|
7
|
+
/** @internal Classifies a reported React error after its owning boundary commits. */
|
|
8
|
+
export declare function deferUnhandledPresentationError(error: unknown, report: () => void): void;
|
|
9
|
+
/** @internal Exact pending texture groups owned by one presentation, including uncommitted Suspense. */
|
|
10
|
+
export declare function createPresentationRetryRegistry(defaultClear?: (inputs: string[]) => void): {
|
|
11
|
+
register(inputs: readonly string[], clear?: ((inputs: string[]) => void) | undefined): () => void;
|
|
12
|
+
retry(): void;
|
|
13
|
+
};
|
|
14
|
+
type RetryRegistry = ReturnType<typeof createPresentationRetryRegistry>;
|
|
15
|
+
type PresentationRetryScope = {
|
|
16
|
+
registry: RetryRegistry;
|
|
17
|
+
fail: (error: unknown, stack?: string) => void;
|
|
18
|
+
recoveredDraw: () => void;
|
|
19
|
+
};
|
|
20
|
+
/** Renderer-only loader adapters share the DOM owner without importing their libraries here. @internal */
|
|
21
|
+
export declare function usePresentationRetryScope(): PresentationRetryScope | null;
|
|
22
|
+
/** @internal Forwards renderer-local loader errors to the owning DOM presentation. */
|
|
23
|
+
export declare function PresentationAssetBoundary({ children }: {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}): ReactNode;
|
|
26
|
+
/** @internal Retires only diagnostics belonging to this exact handled presentation error. */
|
|
27
|
+
export declare function createPresentationDiagnosticOwnership(): {
|
|
28
|
+
register(error: unknown, id: number, phase: string): void;
|
|
29
|
+
recovered(error: unknown): ReadonlySet<number>;
|
|
30
|
+
};
|
|
31
|
+
/** Restores an owned recovered surface only after Suspense has attached and revealed it. @internal */
|
|
32
|
+
export declare function createPlaySurfaceFocusOwnership<Owner>(): {
|
|
33
|
+
request(owner: Owner): void;
|
|
34
|
+
reveal(owner: Owner, surface: HTMLElement | null, active: Element | null, body: HTMLElement): boolean;
|
|
35
|
+
};
|
|
36
|
+
/** Reports the owning presentation attempt and recovery lifecycle. @internal */
|
|
37
|
+
export type PresentationRecoveryEvent = {
|
|
38
|
+
phase: string;
|
|
39
|
+
attempt: number;
|
|
40
|
+
scope: number;
|
|
41
|
+
sameOwner: boolean;
|
|
42
|
+
pending: boolean;
|
|
43
|
+
failed: boolean;
|
|
44
|
+
};
|
|
45
|
+
type RecoveryProps = {
|
|
46
|
+
ctx: GameContext;
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
onRuntimeError: (error: unknown, phase: string, componentStack?: string) => void;
|
|
49
|
+
onRetryCommitted?: () => void;
|
|
50
|
+
onRecoveredDraw?: (error: unknown) => void;
|
|
51
|
+
onRecoveryEvent?: (event: PresentationRecoveryEvent) => void;
|
|
52
|
+
};
|
|
53
|
+
type RecoveryState = {
|
|
54
|
+
owner: GameContext;
|
|
55
|
+
failed: boolean;
|
|
56
|
+
error: unknown;
|
|
57
|
+
registry: RetryRegistry;
|
|
58
|
+
};
|
|
59
|
+
/** @internal Retains the live context for asset Retry; failed browser modules require page reload. */
|
|
60
|
+
export declare class PresentationRecovery extends Component<RecoveryProps, RecoveryState> {
|
|
61
|
+
state: RecoveryState;
|
|
62
|
+
static getDerivedStateFromProps(props: RecoveryProps, state: RecoveryState): Partial<RecoveryState> | null;
|
|
63
|
+
static getDerivedStateFromError(error: unknown): Partial<RecoveryState>;
|
|
64
|
+
componentDidCatch(error: unknown, info: ErrorInfo): void;
|
|
65
|
+
componentDidUpdate(previousProps: RecoveryProps, previousState: RecoveryState): void;
|
|
66
|
+
componentWillUnmount(): void;
|
|
67
|
+
private attempt;
|
|
68
|
+
private scopeId;
|
|
69
|
+
private trace;
|
|
70
|
+
private scope;
|
|
71
|
+
private pendingRecovery;
|
|
72
|
+
private retry;
|
|
73
|
+
render(): ReactNode;
|
|
74
|
+
}
|
|
75
|
+
export {};
|