pelican-engine 0.0.0-stage → 0.3.1
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/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import * as THREE from "three/webgpu";
|
|
2
|
+
import { createUIRenderer } from "./ui.js";
|
|
3
|
+
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
|
|
4
|
+
import { createGameServices } from "../runtime/game-services.js";
|
|
5
|
+
import { createPhysicsFactory } from "../physics/index.js";
|
|
6
|
+
import { createNavigationPlugin } from "../navigation/index.js";
|
|
7
|
+
import { createModelLibrary } from "./models.js";
|
|
8
|
+
import { resolveEntityDefinition } from "../runtime/prefabs.js";
|
|
9
|
+
/** Private presentation ownership. Games receive only SDK services, never browser/library objects. */
|
|
10
|
+
export async function prepareGameplay(ir, baseURL, renderer, materials = new Map()) {
|
|
11
|
+
const assetURL = (id) => {
|
|
12
|
+
const asset = ir.assets.find((entry) => entry.id === id);
|
|
13
|
+
if (!asset)
|
|
14
|
+
throw new Error("TC_ASSET_MISSING: " + id);
|
|
15
|
+
return new URL(asset.path, baseURL).href;
|
|
16
|
+
};
|
|
17
|
+
const models = createModelLibrary(ir.assets
|
|
18
|
+
.filter((a) => a.type === "model")
|
|
19
|
+
.map((a) => ({ id: a.id, url: assetURL(a.id) })), { material: id => materials.get(id) });
|
|
20
|
+
const textures = new Map();
|
|
21
|
+
let physics;
|
|
22
|
+
let environmentTarget;
|
|
23
|
+
const diagnostics = [];
|
|
24
|
+
let audio;
|
|
25
|
+
if ((ir.sounds?.length ?? 0) > 0) {
|
|
26
|
+
try {
|
|
27
|
+
if (typeof AudioContext !== "undefined")
|
|
28
|
+
audio = new AudioContext();
|
|
29
|
+
else
|
|
30
|
+
diagnostics.push("TC_AUDIO_UNAVAILABLE: AudioContext is unavailable.");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
diagnostics.push("TC_AUDIO_UNAVAILABLE: " + String(error));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const buffers = new Map();
|
|
37
|
+
let masterGain;
|
|
38
|
+
const busGains = new Map();
|
|
39
|
+
const busFadeKeys = new Map();
|
|
40
|
+
const voices = new Map();
|
|
41
|
+
let ui;
|
|
42
|
+
let uiInput;
|
|
43
|
+
let uiEmit;
|
|
44
|
+
let services;
|
|
45
|
+
let closed = false;
|
|
46
|
+
let audioRevision = -1;
|
|
47
|
+
const stopVoice = (id) => {
|
|
48
|
+
const voice = voices.get(id);
|
|
49
|
+
if (!voice)
|
|
50
|
+
return;
|
|
51
|
+
voice.source.onended = null;
|
|
52
|
+
try {
|
|
53
|
+
voice.source.stop();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* A completed buffer source is already stopped. */
|
|
57
|
+
}
|
|
58
|
+
voice.source.disconnect();
|
|
59
|
+
voice.gain.disconnect();
|
|
60
|
+
voice.panner?.disconnect();
|
|
61
|
+
voices.delete(id);
|
|
62
|
+
};
|
|
63
|
+
const dispose = async () => {
|
|
64
|
+
if (closed)
|
|
65
|
+
return;
|
|
66
|
+
closed = true;
|
|
67
|
+
ui?.dispose();
|
|
68
|
+
for (const id of [...voices.keys()])
|
|
69
|
+
stopVoice(id);
|
|
70
|
+
for (const gain of busGains.values())
|
|
71
|
+
gain.disconnect();
|
|
72
|
+
busGains.clear();
|
|
73
|
+
busFadeKeys.clear();
|
|
74
|
+
masterGain?.disconnect();
|
|
75
|
+
environmentTarget?.dispose();
|
|
76
|
+
for (const texture of textures.values())
|
|
77
|
+
texture.dispose();
|
|
78
|
+
textures.clear();
|
|
79
|
+
buffers.clear();
|
|
80
|
+
await Promise.all([models.dispose(), audio?.close()]);
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
await models.prepare();
|
|
84
|
+
if (ir.scene.environment.lighting === "studio") {
|
|
85
|
+
const room = new RoomEnvironment(), pmrem = new THREE.PMREMGenerator(renderer);
|
|
86
|
+
try {
|
|
87
|
+
environmentTarget = pmrem.fromScene(room, 0.04);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
pmrem.dispose();
|
|
91
|
+
room.dispose();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const asset of ir.assets.filter((a) => a.type === "texture")) {
|
|
95
|
+
const texture = await new THREE.TextureLoader().loadAsync(assetURL(asset.id));
|
|
96
|
+
texture.colorSpace = asset.colorSpace === "linear" ? THREE.LinearSRGBColorSpace : THREE.SRGBColorSpace;
|
|
97
|
+
textures.set(asset.id, texture);
|
|
98
|
+
}
|
|
99
|
+
if (audio)
|
|
100
|
+
for (const sound of ir.sounds ?? []) {
|
|
101
|
+
if (buffers.has(sound.asset))
|
|
102
|
+
continue;
|
|
103
|
+
const response = await fetch(assetURL(sound.asset));
|
|
104
|
+
if (!response.ok)
|
|
105
|
+
throw new Error("TC_AUDIO_LOAD: " + sound.asset + " HTTP " + response.status);
|
|
106
|
+
const buffer = await audio.decodeAudioData(await response.arrayBuffer());
|
|
107
|
+
buffers.set(sound.asset, buffer);
|
|
108
|
+
}
|
|
109
|
+
services = createGameServices(ir, models.animation);
|
|
110
|
+
const modelBindings = (records) => records.filter((record) => resolveEntityDefinition(ir, record)?.model);
|
|
111
|
+
const modelDefinitions = (records) => modelBindings(records).map((record) => ({
|
|
112
|
+
entity: record.id,
|
|
113
|
+
definition: resolveEntityDefinition(ir, record).model,
|
|
114
|
+
}));
|
|
115
|
+
const validateModels = (value) => {
|
|
116
|
+
const saved = structuredClone(value);
|
|
117
|
+
if (!saved ||
|
|
118
|
+
saved.version !== 1 ||
|
|
119
|
+
!Array.isArray(saved.bindings) ||
|
|
120
|
+
saved.bindings.some((record) => !record ||
|
|
121
|
+
typeof record.id !== "string" ||
|
|
122
|
+
(record.prefab !== undefined && typeof record.prefab !== "string")) ||
|
|
123
|
+
new Set(saved.bindings.map((record) => record.id)).size !==
|
|
124
|
+
saved.bindings.length)
|
|
125
|
+
throw new Error("TC_MODEL_BINDINGS_SNAPSHOT: Invalid model content identity.");
|
|
126
|
+
if (modelBindings(saved.bindings).length !== saved.bindings.length)
|
|
127
|
+
throw new Error("TC_MODEL_BINDINGS_SNAPSHOT: Unknown model content.");
|
|
128
|
+
const definitions = modelDefinitions(saved.bindings);
|
|
129
|
+
const animation = models.validateSnapshot(saved.animation, definitions);
|
|
130
|
+
if (JSON.stringify(animation.animations.map((item) => item.entity).sort()) !== JSON.stringify(saved.bindings.map((record) => record.id).sort()))
|
|
131
|
+
throw new Error("TC_MODEL_BINDINGS_SNAPSHOT: Active model bindings differ from animation state.");
|
|
132
|
+
return { animation, definitions };
|
|
133
|
+
};
|
|
134
|
+
const plugins = [
|
|
135
|
+
services.factory,
|
|
136
|
+
createNavigationPlugin,
|
|
137
|
+
(context) => ({
|
|
138
|
+
id: "$engine.models",
|
|
139
|
+
version: "2",
|
|
140
|
+
snapshot: () => {
|
|
141
|
+
const bindings = modelBindings(context.world.records()).map(({ id, prefab }) => ({
|
|
142
|
+
id,
|
|
143
|
+
...(prefab === undefined ? {} : { prefab }),
|
|
144
|
+
}));
|
|
145
|
+
models.reconcileBindings(modelDefinitions(bindings));
|
|
146
|
+
return { version: 1, bindings, animation: models.snapshot() };
|
|
147
|
+
},
|
|
148
|
+
validateSnapshot: (value) => {
|
|
149
|
+
validateModels(value);
|
|
150
|
+
},
|
|
151
|
+
validateWorld: (value, world) => {
|
|
152
|
+
const bindings = value.bindings;
|
|
153
|
+
const expected = modelBindings(world.entities)
|
|
154
|
+
.map(({ id, prefab }) => JSON.stringify({
|
|
155
|
+
id,
|
|
156
|
+
...(prefab === undefined ? {} : { prefab }),
|
|
157
|
+
}))
|
|
158
|
+
.sort();
|
|
159
|
+
if (JSON.stringify(expected) !==
|
|
160
|
+
JSON.stringify(bindings
|
|
161
|
+
.map(({ id, prefab }) => JSON.stringify({
|
|
162
|
+
id,
|
|
163
|
+
...(prefab === undefined ? {} : { prefab }),
|
|
164
|
+
}))
|
|
165
|
+
.sort()))
|
|
166
|
+
throw new Error("TC_MODEL_BINDINGS_WORLD: Model identities must match the restored World.");
|
|
167
|
+
},
|
|
168
|
+
restore: (value) => {
|
|
169
|
+
const { animation, definitions } = validateModels(value);
|
|
170
|
+
models.restore(animation, definitions);
|
|
171
|
+
},
|
|
172
|
+
dispose() {
|
|
173
|
+
/* Content owns async GPU/asset disposal. */
|
|
174
|
+
},
|
|
175
|
+
inspect: () => models.inspect(),
|
|
176
|
+
systems: [
|
|
177
|
+
{
|
|
178
|
+
id: "$engine.models",
|
|
179
|
+
phase: "presentation",
|
|
180
|
+
run: (dt) => {
|
|
181
|
+
const records = context.world.records();
|
|
182
|
+
models.reconcileBindings(modelDefinitions(records));
|
|
183
|
+
models.tick(dt);
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
];
|
|
189
|
+
if ([...ir.scene.entities, ...ir.prefabs].some((entity) => entity.rigidBody || entity.character)) {
|
|
190
|
+
const factory = await createPhysicsFactory();
|
|
191
|
+
plugins.push((context) => {
|
|
192
|
+
const plugin = factory(context);
|
|
193
|
+
physics = plugin.services?.physics;
|
|
194
|
+
return plugin;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
ui = createUIRenderer(ir.ui ?? [], {
|
|
198
|
+
input: (key, pressed) => {
|
|
199
|
+
uiInput?.(key, pressed);
|
|
200
|
+
void audio
|
|
201
|
+
?.resume()
|
|
202
|
+
.catch((error) => diagnostics.push("TC_AUDIO_RESUME: " + String(error)));
|
|
203
|
+
},
|
|
204
|
+
emit: (type, payload) => {
|
|
205
|
+
if (type === "ui.focus" &&
|
|
206
|
+
payload &&
|
|
207
|
+
typeof payload === "object" &&
|
|
208
|
+
ir.ui?.some((element) => element.id === payload.id &&
|
|
209
|
+
element.kind === "input"))
|
|
210
|
+
for (const key of new Set(Object.values(ir.input ?? {}).flat()))
|
|
211
|
+
uiInput?.(key, false);
|
|
212
|
+
uiEmit?.(type, payload);
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
models,
|
|
217
|
+
async rebuildGpuResources(next) {
|
|
218
|
+
if (closed)
|
|
219
|
+
throw new Error("TC_CONTENT_DISPOSED");
|
|
220
|
+
environmentTarget?.dispose();
|
|
221
|
+
environmentTarget = undefined;
|
|
222
|
+
if (ir.scene.environment.lighting === "studio") {
|
|
223
|
+
const room = new RoomEnvironment(), generator = new THREE.PMREMGenerator(next);
|
|
224
|
+
try {
|
|
225
|
+
environmentTarget = generator.fromScene(room, 0.04);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
generator.dispose();
|
|
229
|
+
room.dispose();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return environmentTarget?.texture;
|
|
233
|
+
},
|
|
234
|
+
environment: environmentTarget?.texture,
|
|
235
|
+
textures,
|
|
236
|
+
plugins,
|
|
237
|
+
services,
|
|
238
|
+
dispose,
|
|
239
|
+
mount(input, emit) {
|
|
240
|
+
uiInput = input;
|
|
241
|
+
uiEmit = emit;
|
|
242
|
+
ui.mount();
|
|
243
|
+
},
|
|
244
|
+
consumesKeyboard(event) {
|
|
245
|
+
return ui?.consumesKeyboard(event) ?? false;
|
|
246
|
+
},
|
|
247
|
+
unlockAudio() {
|
|
248
|
+
void audio
|
|
249
|
+
?.resume()
|
|
250
|
+
.catch((error) => diagnostics.push("TC_AUDIO_RESUME: " + String(error)));
|
|
251
|
+
},
|
|
252
|
+
present(camera, objects, dt, tick) {
|
|
253
|
+
const state = services.inspect();
|
|
254
|
+
if (state.camera) {
|
|
255
|
+
const follow = state.camera, object = objects.get(follow.entity);
|
|
256
|
+
if (object) {
|
|
257
|
+
const at = object.getWorldPosition(new THREE.Vector3());
|
|
258
|
+
const target = at
|
|
259
|
+
.clone()
|
|
260
|
+
.add(new THREE.Vector3(0, follow.lookHeight, 0));
|
|
261
|
+
const position = at.add(new THREE.Vector3(Math.sin(follow.yaw) * follow.distance, follow.height, Math.cos(follow.yaw) * follow.distance));
|
|
262
|
+
if (physics) {
|
|
263
|
+
const direction = position.clone().sub(target), distance = direction.length();
|
|
264
|
+
const hit = physics.raycast(target.toArray(), direction.toArray(), distance, { exclude: [follow.entity], includeTriggers: false });
|
|
265
|
+
if (hit && hit.distance < distance)
|
|
266
|
+
position
|
|
267
|
+
.copy(target)
|
|
268
|
+
.addScaledVector(direction.normalize(), Math.max(0.2, hit.distance - 0.25));
|
|
269
|
+
}
|
|
270
|
+
camera.position.lerp(position, dt > 0 && follow.smoothing > 0
|
|
271
|
+
? 1 - Math.pow(1 - follow.smoothing, dt * 60)
|
|
272
|
+
: 1);
|
|
273
|
+
if (physics) {
|
|
274
|
+
const actual = camera.position.clone().sub(target), length = actual.length();
|
|
275
|
+
if (length > 0) {
|
|
276
|
+
const hit = physics.raycast(target.toArray(), actual.toArray(), length, { exclude: [follow.entity] });
|
|
277
|
+
if (hit && hit.distance < length)
|
|
278
|
+
camera.position
|
|
279
|
+
.copy(target)
|
|
280
|
+
.addScaledVector(actual.normalize(), Math.max(0.2, hit.distance - 0.25));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
camera.lookAt(target);
|
|
284
|
+
camera.updateMatrixWorld();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const size = renderer.getSize(new THREE.Vector2());
|
|
288
|
+
const positions = new Map();
|
|
289
|
+
for (const definition of state.ui ?? []) {
|
|
290
|
+
if (!definition.world)
|
|
291
|
+
continue;
|
|
292
|
+
const object = objects.get(definition.world.entity);
|
|
293
|
+
if (!object) {
|
|
294
|
+
positions.set(definition.id, {
|
|
295
|
+
x: 0,
|
|
296
|
+
y: 0,
|
|
297
|
+
visible: false,
|
|
298
|
+
occluded: false,
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
object.updateWorldMatrix(true, false);
|
|
303
|
+
const point = object
|
|
304
|
+
.getWorldPosition(new THREE.Vector3())
|
|
305
|
+
.add(new THREE.Vector3().fromArray(definition.world.offset));
|
|
306
|
+
const delta = point.clone().sub(camera.position), distance = delta.length(), projected = point.clone().project(camera), forward = new THREE.Vector3();
|
|
307
|
+
camera.getWorldDirection(forward);
|
|
308
|
+
let occluded = false;
|
|
309
|
+
if (definition.world.occlude && distance > 0) {
|
|
310
|
+
const raycaster = new THREE.Raycaster(camera.position, delta.clone().normalize(), 0.01, distance - 0.01);
|
|
311
|
+
occluded = raycaster
|
|
312
|
+
.intersectObjects([...objects.values()], true)
|
|
313
|
+
.some((hit) => {
|
|
314
|
+
for (let parent = hit.object; parent; parent = parent.parent)
|
|
315
|
+
if (parent === object)
|
|
316
|
+
return false;
|
|
317
|
+
return hit.object.visible;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
positions.set(definition.id, {
|
|
321
|
+
x: ((projected.x + 1) * size.x) / 2,
|
|
322
|
+
y: ((1 - projected.y) * size.y) / 2,
|
|
323
|
+
visible: forward.dot(delta) > 0 &&
|
|
324
|
+
projected.z >= -1 &&
|
|
325
|
+
projected.z <= 1 &&
|
|
326
|
+
Math.abs(projected.x) <= 1 &&
|
|
327
|
+
Math.abs(projected.y) <= 1,
|
|
328
|
+
occluded,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
ui?.present(state.ui ?? [], positions);
|
|
332
|
+
const clock = services.audioClock();
|
|
333
|
+
if (clock.revision !== audioRevision) {
|
|
334
|
+
for (const id of [...voices.keys()])
|
|
335
|
+
stopVoice(id);
|
|
336
|
+
busFadeKeys.clear();
|
|
337
|
+
for (const gain of busGains.values())
|
|
338
|
+
gain.gain.cancelScheduledValues(audio?.currentTime ?? 0);
|
|
339
|
+
audioRevision = clock.revision;
|
|
340
|
+
}
|
|
341
|
+
const wanted = new Set(state.audio.voices.filter((voice) => voice.pausedAt == null).map((voice) => voice.id));
|
|
342
|
+
for (const id of [...voices.keys()])
|
|
343
|
+
if (!wanted.has(id))
|
|
344
|
+
stopVoice(id);
|
|
345
|
+
if (audio) {
|
|
346
|
+
if (!masterGain) {
|
|
347
|
+
const master = audio.createGain();
|
|
348
|
+
const created = new Map();
|
|
349
|
+
try {
|
|
350
|
+
master.connect(audio.destination);
|
|
351
|
+
for (const bus of ir.audioBuses ?? []) {
|
|
352
|
+
const gain = audio.createGain();
|
|
353
|
+
created.set(bus.id, gain);
|
|
354
|
+
gain.connect(master);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
for (const gain of created.values())
|
|
359
|
+
gain.disconnect();
|
|
360
|
+
master.disconnect();
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
masterGain = master;
|
|
364
|
+
for (const [id, gain] of created)
|
|
365
|
+
busGains.set(id, gain);
|
|
366
|
+
}
|
|
367
|
+
masterGain.gain.value = state.audio.volume;
|
|
368
|
+
for (const bus of state.audio.busVolumes ?? []) {
|
|
369
|
+
const gain = busGains.get(bus.id).gain;
|
|
370
|
+
const fade = bus.fade;
|
|
371
|
+
const key = fade && `${clock.revision}:${fade.startedAt}:${fade.from}:${fade.target}:${fade.duration}`;
|
|
372
|
+
if (key && clock.fixedDt !== null) {
|
|
373
|
+
if (busFadeKeys.get(bus.id) !== key) {
|
|
374
|
+
gain.cancelScheduledValues(audio.currentTime);
|
|
375
|
+
gain.setValueAtTime(bus.volume, audio.currentTime);
|
|
376
|
+
gain.linearRampToValueAtTime(fade.target, audio.currentTime + Math.max(0, fade.duration - (clock.tick - fade.startedAt) * clock.fixedDt));
|
|
377
|
+
busFadeKeys.set(bus.id, key);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
if (busFadeKeys.has(bus.id))
|
|
382
|
+
gain.cancelScheduledValues(audio.currentTime);
|
|
383
|
+
busFadeKeys.delete(bus.id);
|
|
384
|
+
gain.value = bus.volume;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
audio.listener.positionX.value = camera.position.x;
|
|
388
|
+
audio.listener.positionY.value = camera.position.y;
|
|
389
|
+
audio.listener.positionZ.value = camera.position.z;
|
|
390
|
+
const forward = new THREE.Vector3();
|
|
391
|
+
camera.getWorldDirection(forward);
|
|
392
|
+
audio.listener.forwardX.value = forward.x;
|
|
393
|
+
audio.listener.forwardY.value = forward.y;
|
|
394
|
+
audio.listener.forwardZ.value = forward.z;
|
|
395
|
+
for (const voice of state.audio.voices) {
|
|
396
|
+
if (voice.pausedAt != null)
|
|
397
|
+
continue;
|
|
398
|
+
let active = voices.get(voice.id);
|
|
399
|
+
if (active && active.version !== clock.versions[voice.id]) {
|
|
400
|
+
stopVoice(voice.id);
|
|
401
|
+
active = undefined;
|
|
402
|
+
}
|
|
403
|
+
if (!active && audio.state === "running") {
|
|
404
|
+
const definition = ir.sounds.find((sound) => sound.id === voice.sound);
|
|
405
|
+
const buffer = buffers.get(definition.asset);
|
|
406
|
+
const age = Math.max(0, (clock.tick - voice.startedAt - (voice.pausedTicks ?? 0)) * (clock.fixedDt ?? 0));
|
|
407
|
+
if (buffer.duration <= 0 || (!voice.loop && age >= buffer.duration))
|
|
408
|
+
continue;
|
|
409
|
+
const source = audio.createBufferSource(), gain = audio.createGain();
|
|
410
|
+
source.buffer = buffer;
|
|
411
|
+
source.loop = voice.loop;
|
|
412
|
+
let panner;
|
|
413
|
+
if (voice.entity) {
|
|
414
|
+
panner = audio.createPanner();
|
|
415
|
+
panner.panningModel = "HRTF";
|
|
416
|
+
panner.distanceModel = "inverse";
|
|
417
|
+
source.connect(panner);
|
|
418
|
+
panner.connect(gain);
|
|
419
|
+
}
|
|
420
|
+
else
|
|
421
|
+
source.connect(gain);
|
|
422
|
+
gain.connect(voice.bus === undefined ? masterGain : busGains.get(voice.bus));
|
|
423
|
+
active = { source, gain, panner, version: clock.versions[voice.id] };
|
|
424
|
+
voices.set(voice.id, active);
|
|
425
|
+
source.onended = () => {
|
|
426
|
+
source.disconnect();
|
|
427
|
+
gain.disconnect();
|
|
428
|
+
panner?.disconnect();
|
|
429
|
+
};
|
|
430
|
+
source.start(0, age % buffer.duration);
|
|
431
|
+
}
|
|
432
|
+
if (active) {
|
|
433
|
+
active.gain.gain.value = voice.volume;
|
|
434
|
+
const position = voice.entity
|
|
435
|
+
? objects
|
|
436
|
+
.get(voice.entity)
|
|
437
|
+
?.getWorldPosition(new THREE.Vector3())
|
|
438
|
+
: undefined;
|
|
439
|
+
if (active.panner && position) {
|
|
440
|
+
active.panner.positionX.value = position.x;
|
|
441
|
+
active.panner.positionY.value = position.y;
|
|
442
|
+
active.panner.positionZ.value = position.z;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
inspect() {
|
|
449
|
+
return {
|
|
450
|
+
...services.inspect(),
|
|
451
|
+
models: models.inspect(),
|
|
452
|
+
audioDevice: audio?.state ?? ((ir.sounds?.length ?? 0) > 0 ? "unavailable" : "unused"),
|
|
453
|
+
decodedSounds: buffers.size,
|
|
454
|
+
uiPlatform: ui?.inspect(),
|
|
455
|
+
diagnostics: [...diagnostics],
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
await dispose();
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as THREE from "three/webgpu";
|
|
2
|
+
/** Private render adapter. Never exported by the game SDK. */
|
|
3
|
+
export function extractTriangles(scene) {
|
|
4
|
+
scene.updateMatrixWorld(true);
|
|
5
|
+
const triangles = [];
|
|
6
|
+
scene.traverseVisible((object) => {
|
|
7
|
+
if (!(object instanceof THREE.Mesh) ||
|
|
8
|
+
!object.visible ||
|
|
9
|
+
object.userData.noTrace)
|
|
10
|
+
return;
|
|
11
|
+
if (object instanceof THREE.SkinnedMesh)
|
|
12
|
+
object.skeleton.update();
|
|
13
|
+
const material = (Array.isArray(object.material) ? object.material[0] : object.material);
|
|
14
|
+
const geometry = object.geometry;
|
|
15
|
+
const count = geometry.index?.count ?? geometry.getAttribute("position").count;
|
|
16
|
+
const points = [
|
|
17
|
+
new THREE.Vector3(),
|
|
18
|
+
new THREE.Vector3(),
|
|
19
|
+
new THREE.Vector3(),
|
|
20
|
+
];
|
|
21
|
+
for (let i = 0; i < count; i += 3) {
|
|
22
|
+
for (let k = 0; k < 3; k++)
|
|
23
|
+
object
|
|
24
|
+
.getVertexPosition(geometry.index?.getX(i + k) ?? i + k, points[k])
|
|
25
|
+
.applyMatrix4(object.matrixWorld);
|
|
26
|
+
const color = material.color ?? new THREE.Color(0.6, 0.6, 0.6);
|
|
27
|
+
triangles.push({
|
|
28
|
+
a: points[0].toArray(),
|
|
29
|
+
b: points[1].toArray(),
|
|
30
|
+
c: points[2].toArray(),
|
|
31
|
+
color: color.toArray(),
|
|
32
|
+
emission: material.emissiveIntensity && material.emissive?.getHex()
|
|
33
|
+
? material.emissiveIntensity
|
|
34
|
+
: 0,
|
|
35
|
+
roughness: material.roughness ?? 0.8,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return triangles;
|
|
40
|
+
}
|
|
41
|
+
/** Median-split CPU BVH prototype. Each upload is explicitly measured, not marketed as GPU refit. */
|
|
42
|
+
export function buildBVH(input) {
|
|
43
|
+
const ordered = [];
|
|
44
|
+
const nodes = [];
|
|
45
|
+
const boundsOf = (triangles) => {
|
|
46
|
+
const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];
|
|
47
|
+
for (const triangle of triangles)
|
|
48
|
+
for (const p of [triangle.a, triangle.b, triangle.c])
|
|
49
|
+
for (let i = 0; i < 3; i++) {
|
|
50
|
+
low[i] = Math.min(low[i], p[i]);
|
|
51
|
+
high[i] = Math.max(high[i], p[i]);
|
|
52
|
+
}
|
|
53
|
+
return { low, high };
|
|
54
|
+
};
|
|
55
|
+
const visit = (triangles) => {
|
|
56
|
+
const index = nodes.length;
|
|
57
|
+
nodes.push([]);
|
|
58
|
+
const { low, high } = boundsOf(triangles);
|
|
59
|
+
if (triangles.length <= 4) {
|
|
60
|
+
const offset = ordered.length;
|
|
61
|
+
ordered.push(...triangles);
|
|
62
|
+
nodes[index] = [...low, offset, ...high, triangles.length];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const axis = high
|
|
66
|
+
.map((v, i) => v - low[i])
|
|
67
|
+
.indexOf(Math.max(...high.map((v, i) => v - low[i])));
|
|
68
|
+
triangles.sort((x, y) => x.a[axis] +
|
|
69
|
+
x.b[axis] +
|
|
70
|
+
x.c[axis] -
|
|
71
|
+
(y.a[axis] + y.b[axis] + y.c[axis]));
|
|
72
|
+
const middle = triangles.length >> 1;
|
|
73
|
+
const left = visit(triangles.slice(0, middle)), right = visit(triangles.slice(middle));
|
|
74
|
+
nodes[index] = [...low, left, ...high, -right];
|
|
75
|
+
}
|
|
76
|
+
return index;
|
|
77
|
+
};
|
|
78
|
+
if (!input.length)
|
|
79
|
+
throw new Error("TC_RENDER_EMPTY_GEOMETRY");
|
|
80
|
+
visit([...input]);
|
|
81
|
+
const packed = new Float32Array(ordered.length * 20);
|
|
82
|
+
ordered.forEach((t, i) => packed.set([
|
|
83
|
+
...t.a,
|
|
84
|
+
t.emission,
|
|
85
|
+
...t.b,
|
|
86
|
+
t.roughness,
|
|
87
|
+
...t.c,
|
|
88
|
+
0,
|
|
89
|
+
...t.color,
|
|
90
|
+
0,
|
|
91
|
+
0,
|
|
92
|
+
0,
|
|
93
|
+
0,
|
|
94
|
+
0,
|
|
95
|
+
], i * 20));
|
|
96
|
+
return {
|
|
97
|
+
triangles: packed,
|
|
98
|
+
nodes: new Float32Array(nodes.flat()),
|
|
99
|
+
triangleCount: ordered.length,
|
|
100
|
+
nodeCount: nodes.length,
|
|
101
|
+
bounds: [...nodes[0].slice(0, 3), ...nodes[0].slice(4, 7)],
|
|
102
|
+
};
|
|
103
|
+
}
|