@waica/engine 0.23.0 → 0.24.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/README.md +19 -0
- package/dist/assets/asset-loader.d.ts +1 -1
- package/dist/assets/asset-loader.js +1 -1
- package/dist/assets/texture-backend.d.ts +1 -1
- package/dist/assets/texture-backend.js +1 -1
- package/dist/camera.js +1 -1
- package/dist/components/animated-sprite.js +1 -1
- package/dist/components/sprite.js +1 -1
- package/dist/components/tilemap.js +3 -1
- package/dist/entity.d.ts +1 -1
- package/dist/entity.js +1 -1
- package/dist/game.d.ts +20 -3
- package/dist/game.js +59 -44
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -1
- package/dist/particle-batch.d.ts +1 -1
- package/dist/particle-batch.js +4 -1
- package/dist/particle-sort-bounds.d.ts +1 -1
- package/dist/particle-sort-bounds.js +1 -1
- package/dist/pointer.js +1 -1
- package/dist/render-output.d.ts +27 -0
- package/dist/render-output.js +44 -0
- package/dist/render-readiness.d.ts +37 -0
- package/dist/render-readiness.js +61 -0
- package/dist/render-sort.d.ts +14 -0
- package/dist/render-sort.js +37 -0
- package/dist/runtime-bridge.d.ts +18 -2
- package/dist/runtime-bridge.js +7 -1
- package/dist/sprite-batch.d.ts +3 -3
- package/dist/sprite-batch.js +41 -29
- package/dist/sprite-batches.d.ts +2 -2
- package/dist/sprite-batches.js +1 -1
- package/dist/sprite-draw-collector.d.ts +3 -3
- package/dist/sprite-draw-collector.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,25 @@ Waica's public engine core: entities and components, the game loop, scene and pr
|
|
|
6
6
|
import { Component, Game, loadScene } from '@waica/engine'
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
## Migrating to 0.24.0: WebGPURenderer and `game.ready()`
|
|
10
|
+
|
|
11
|
+
A `Game` draws through three's `WebGPURenderer` (ADR 0025): WebGPU when the browser offers it, otherwise the renderer's own WebGL2 fallback. The browser decides — there is no option to force either one — and `game.backend` reports which one it got (`'webgpu'` or `'webgl2'`; `null` until the renderer is ready).
|
|
12
|
+
|
|
13
|
+
- **Await `game.ready()` before the first frame.** `new Game(...)` stays synchronous, but the renderer initializes asynchronously. Frames before it finishes still simulate (steps, `onUpdate`) but draw nothing, so a host waits for it before `game.start()`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const game = new Game({ canvas })
|
|
17
|
+
loadScene(game, scene, registry)
|
|
18
|
+
await game.assets.ready()
|
|
19
|
+
await game.ready()
|
|
20
|
+
game.start()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every call returns the same promise. It rejects with an error naming both `webgpu` and `webgl2` when neither initializes; nothing is ever drawn then. A `Game` disposed while `ready()` is pending never starts, and its renderer is released once initialization settles.
|
|
24
|
+
- **`THREE` is the `three/webgpu` build.** `import { THREE } from '@waica/engine'` now re-exports `three/webgpu`, the same copy of three the engine draws with: `THREE.WebGPURenderer` exists and `THREE.WebGLRenderer` does not. Custom GLSL does not render under `WebGPURenderer` — `ShaderMaterial`, `RawShaderMaterial` and `material.onBeforeCompile` are not supported there — so write custom shading as node materials with TSL (`three/tsl`). Built-in materials such as `MeshBasicMaterial` keep working unchanged.
|
|
25
|
+
- **Tests under happy-dom.** happy-dom has no GPU: a project test that builds a `Game` replaces `WebGPURenderer` from `three/webgpu` with a fake (it previously replaced `WebGLRenderer` from `three`), whose `init()` resolves.
|
|
26
|
+
- **Run Sessions.** A Run Session waits for the Render Backend before it reports ready, steps or screenshots, and its snapshots carry `backend`. A renderer that cannot initialize ends `start_project` with a runtime error that names the failure.
|
|
27
|
+
|
|
9
28
|
## Hitbox Collision Layers and Masks
|
|
10
29
|
|
|
11
30
|
Every `Hitbox` belongs to one named Collision Layer and declares the other layers in which it is interested through a Collision Mask:
|
package/dist/camera.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import { sheetCell } from '../animation/sheet.js';
|
|
3
3
|
import { Component } from '../component.js';
|
|
4
4
|
import { projectIsometric } from '../projection.js';
|
|
@@ -283,6 +283,8 @@ export class Tilemap extends Component {
|
|
|
283
283
|
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(buffers.indices), 1));
|
|
284
284
|
mesh.geometry.dispose();
|
|
285
285
|
mesh.geometry = geometry;
|
|
286
|
+
// WebGPURenderer issues a zero-count draw (WebGLRenderer skipped it): no tile, no draw.
|
|
287
|
+
mesh.visible = buffers.indices.length > 0;
|
|
286
288
|
}
|
|
287
289
|
/** The loaded sheet image's size, or one pixel per sheet cell until it arrives. */
|
|
288
290
|
sheetSize() {
|
package/dist/entity.d.ts
CHANGED
package/dist/entity.js
CHANGED
package/dist/game.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import { AssetLoader } from './assets/asset-loader.js';
|
|
3
3
|
import type { TextureBackend } from './assets/texture-backend.js';
|
|
4
4
|
import type { AudioBackend } from './audio/backend.js';
|
|
@@ -11,6 +11,7 @@ import { Emitter } from './events.js';
|
|
|
11
11
|
import { GameTime } from './game-time.js';
|
|
12
12
|
import { Input, type InputBindings } from './input.js';
|
|
13
13
|
import { Pointer } from './pointer.js';
|
|
14
|
+
import { type RenderBackend } from './render-readiness.js';
|
|
14
15
|
import { type SceneJson, type SceneRegistry, type SceneRenderJson } from './scene.js';
|
|
15
16
|
import { type SpatialQuery } from './spatial-query.js';
|
|
16
17
|
import { Stats, type StatValue } from './stats.js';
|
|
@@ -101,7 +102,9 @@ export declare class Game {
|
|
|
101
102
|
* or collisions — the editor's edit mode.
|
|
102
103
|
*/
|
|
103
104
|
simulate: boolean;
|
|
105
|
+
/** WebGPU when the browser offers it, else three's WebGL2 fallback (ADR 0025). */
|
|
104
106
|
private readonly renderer;
|
|
107
|
+
private readonly readiness;
|
|
105
108
|
private readonly resizeObserver;
|
|
106
109
|
/** Scene-scoped Sprite Batches (ADR 0024); `render.batch: false` opts a scene out. */
|
|
107
110
|
private readonly spriteBatches;
|
|
@@ -135,9 +138,20 @@ export declare class Game {
|
|
|
135
138
|
private liveSceneName;
|
|
136
139
|
/** True for the whole extent of a runFrame() call, incl. its tail. */
|
|
137
140
|
private insideFrame;
|
|
141
|
+
/** Set by dispose(): a disposed Game never starts again nor reports its renderer (review #3). */
|
|
142
|
+
private disposed;
|
|
138
143
|
/** A loadSceneByName() enqueued while insideFrame; applied at the next runFrame's start. */
|
|
139
144
|
private pendingSceneLoad;
|
|
140
145
|
constructor(options: GameOptions);
|
|
146
|
+
/**
|
|
147
|
+
* Resolves once the renderer can draw; rejects, naming both Render
|
|
148
|
+
* Backends, when neither initializes. Every call returns the same promise.
|
|
149
|
+
* Frames before it simulate but draw nothing, so a host awaits it before
|
|
150
|
+
* its first frame (ADR 0025).
|
|
151
|
+
*/
|
|
152
|
+
ready(): Promise<void>;
|
|
153
|
+
/** The Render Backend the renderer settled on; null until ready() resolved. */
|
|
154
|
+
get backend(): RenderBackend | null;
|
|
141
155
|
/** Creates a live entity in the scene. */
|
|
142
156
|
spawn(name: string): Entity;
|
|
143
157
|
/** Instantiates a registered prefab after a scene has supplied the registry. */
|
|
@@ -205,6 +219,11 @@ export declare class Game {
|
|
|
205
219
|
setViewHeight(value: number): void;
|
|
206
220
|
/** Shuts the game down completely (loop, input, GPU). */
|
|
207
221
|
dispose(): void;
|
|
222
|
+
/**
|
|
223
|
+
* three installs the loop once its init settled; a failed init is
|
|
224
|
+
* reported by ready(), not by every loop change made before it.
|
|
225
|
+
*/
|
|
226
|
+
private setAnimationLoop;
|
|
208
227
|
/**
|
|
209
228
|
* Real-time playback for the Runtime Bridge: the same clock-driven loop
|
|
210
229
|
* as start(), sharing the accumulator, with `onStep` told after every
|
|
@@ -248,8 +267,6 @@ export declare class Game {
|
|
|
248
267
|
private runHostUpdates;
|
|
249
268
|
private flushPendingSceneLoad;
|
|
250
269
|
private unregisterRuntimeBridge;
|
|
251
|
-
/** Under y-sort, re-derives every participant's z from layer band + entity Y. */
|
|
252
|
-
private applyYSort;
|
|
253
270
|
private renderSurface;
|
|
254
271
|
private componentUpdateSchedule;
|
|
255
272
|
private updateSceneCamera;
|
package/dist/game.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import { gameViewport } from './anchored-pieces.js';
|
|
3
3
|
import { AssetLoader } from './assets/asset-loader.js';
|
|
4
4
|
import { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
@@ -15,7 +15,9 @@ import { Pointer } from './pointer.js';
|
|
|
15
15
|
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
16
16
|
import { RuntimeInspector } from './runtime-inspection.js';
|
|
17
17
|
import { projectIsometric, unprojectIsometric } from './projection.js';
|
|
18
|
-
import {
|
|
18
|
+
import { canvasBackground, drawStraightToCanvas, mapUvPerVertex } from './render-output.js';
|
|
19
|
+
import { RenderReadiness } from './render-readiness.js';
|
|
20
|
+
import { applyYSort } from './render-sort.js';
|
|
19
21
|
import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
|
|
20
22
|
import { sceneDrainsOf } from './scene-drains.js';
|
|
21
23
|
import { createSpriteBatches } from './sprite-batches.js';
|
|
@@ -70,8 +72,9 @@ export class Game {
|
|
|
70
72
|
* or collisions — the editor's edit mode.
|
|
71
73
|
*/
|
|
72
74
|
simulate = true;
|
|
73
|
-
|
|
75
|
+
/** WebGPU when the browser offers it, else three's WebGL2 fallback (ADR 0025). */
|
|
74
76
|
renderer;
|
|
77
|
+
readiness;
|
|
75
78
|
resizeObserver;
|
|
76
79
|
/** Scene-scoped Sprite Batches (ADR 0024); `render.batch: false` opts a scene out. */
|
|
77
80
|
spriteBatches;
|
|
@@ -105,6 +108,8 @@ export class Game {
|
|
|
105
108
|
liveSceneName = null;
|
|
106
109
|
/** True for the whole extent of a runFrame() call, incl. its tail. */
|
|
107
110
|
insideFrame = false;
|
|
111
|
+
/** Set by dispose(): a disposed Game never starts again nor reports its renderer (review #3). */
|
|
112
|
+
disposed = false;
|
|
108
113
|
/** A loadSceneByName() enqueued while insideFrame; applied at the next runFrame's start. */
|
|
109
114
|
pendingSceneLoad = null;
|
|
110
115
|
constructor(options) {
|
|
@@ -142,9 +147,19 @@ export class Game {
|
|
|
142
147
|
// One screen pixel in world units: the shake snaps to it (never the base).
|
|
143
148
|
pixel: () => (this.resolution ? this.viewHeight / this.resolution.height : null),
|
|
144
149
|
});
|
|
145
|
-
this.renderer = new THREE.
|
|
150
|
+
this.renderer = new THREE.WebGPURenderer({ canvas, antialias: true });
|
|
146
151
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
147
|
-
|
|
152
|
+
// The model view on the CPU, as three's WebGL renderer computed it: a Sprite
|
|
153
|
+
// Batch instance carries that same matrix (ADR 0024), so batched and
|
|
154
|
+
// per-sprite draws stay pixel-identical.
|
|
155
|
+
this.renderer.highPrecision = true;
|
|
156
|
+
drawStraightToCanvas(this.renderer);
|
|
157
|
+
mapUvPerVertex(this.renderer);
|
|
158
|
+
this.readiness = new RenderReadiness(this.renderer, (error) => {
|
|
159
|
+
if (!this.disposed)
|
|
160
|
+
activeRuntimeBridgeHook()?.fail?.({ code: 'render-backend-failed', message: error.message });
|
|
161
|
+
});
|
|
162
|
+
this.scene.background = canvasBackground(background);
|
|
148
163
|
this.camera = new THREE.OrthographicCamera();
|
|
149
164
|
this.camera.position.z = 10;
|
|
150
165
|
this.pointer = new Pointer(canvas, {
|
|
@@ -157,6 +172,19 @@ export class Game {
|
|
|
157
172
|
this.resizeObserver = new ResizeObserver(() => this.resize());
|
|
158
173
|
this.resizeObserver.observe(canvas);
|
|
159
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolves once the renderer can draw; rejects, naming both Render
|
|
177
|
+
* Backends, when neither initializes. Every call returns the same promise.
|
|
178
|
+
* Frames before it simulate but draw nothing, so a host awaits it before
|
|
179
|
+
* its first frame (ADR 0025).
|
|
180
|
+
*/
|
|
181
|
+
ready() {
|
|
182
|
+
return this.readiness.promise;
|
|
183
|
+
}
|
|
184
|
+
/** The Render Backend the renderer settled on; null until ready() resolved. */
|
|
185
|
+
get backend() {
|
|
186
|
+
return this.readiness.backend;
|
|
187
|
+
}
|
|
160
188
|
/** Creates a live entity in the scene. */
|
|
161
189
|
spawn(name) {
|
|
162
190
|
const entity = new Entity(this, name);
|
|
@@ -338,6 +366,8 @@ export class Game {
|
|
|
338
366
|
this.setViewHeight(this.sceneCamera.zoom);
|
|
339
367
|
}
|
|
340
368
|
start() {
|
|
369
|
+
if (this.disposed)
|
|
370
|
+
return;
|
|
341
371
|
const activation = activeRuntimeBridgeHook();
|
|
342
372
|
if (activation) {
|
|
343
373
|
if (!this.runtimeBridge) {
|
|
@@ -357,19 +387,27 @@ export class Game {
|
|
|
357
387
|
loadScene: (name) => this.loadSceneByName(name),
|
|
358
388
|
availableScenes: () => this.availableScenes,
|
|
359
389
|
assets: () => this.assets.status,
|
|
390
|
+
backend: () => this.backend,
|
|
360
391
|
});
|
|
361
392
|
activation.register(this.runtimeBridge);
|
|
362
393
|
this.audio.setSilenced(true);
|
|
363
394
|
window.addEventListener('pagehide', this.unregisterRuntimeBridge);
|
|
364
395
|
}
|
|
365
396
|
this.renderSurface();
|
|
397
|
+
// Started before ready(): the paused frame draws once the renderer can, frame unchanged.
|
|
398
|
+
if (!this.readiness.isReady) {
|
|
399
|
+
this.readiness.whenReady(() => {
|
|
400
|
+
if (this.runtimeBridge)
|
|
401
|
+
this.renderSurface();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
366
404
|
return;
|
|
367
405
|
}
|
|
368
406
|
this.resetClock();
|
|
369
|
-
this.
|
|
407
|
+
this.setAnimationLoop((time) => this.tick(time));
|
|
370
408
|
}
|
|
371
409
|
stop() {
|
|
372
|
-
this.
|
|
410
|
+
this.setAnimationLoop(null);
|
|
373
411
|
}
|
|
374
412
|
/** Internal: called by Entity.destroy(). Its Anchored Pieces go (or freeze) with it. */
|
|
375
413
|
removeEntity(entity) {
|
|
@@ -392,6 +430,7 @@ export class Game {
|
|
|
392
430
|
}
|
|
393
431
|
/** Shuts the game down completely (loop, input, GPU). */
|
|
394
432
|
dispose() {
|
|
433
|
+
this.disposed = true;
|
|
395
434
|
this.stop();
|
|
396
435
|
this.unregisterRuntimeBridge();
|
|
397
436
|
this.input.dispose();
|
|
@@ -410,7 +449,14 @@ export class Game {
|
|
|
410
449
|
// After the entities: their clones go with the cascade above, the
|
|
411
450
|
// cached bases go here, exactly once (ADR 0019).
|
|
412
451
|
this.assets.dispose();
|
|
413
|
-
this.
|
|
452
|
+
this.readiness.disposeRenderer();
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* three installs the loop once its init settled; a failed init is
|
|
456
|
+
* reported by ready(), not by every loop change made before it.
|
|
457
|
+
*/
|
|
458
|
+
setAnimationLoop(loop) {
|
|
459
|
+
this.renderer.setAnimationLoop(loop).catch(() => { });
|
|
414
460
|
}
|
|
415
461
|
/**
|
|
416
462
|
* Real-time playback for the Runtime Bridge: the same clock-driven loop
|
|
@@ -421,7 +467,7 @@ export class Game {
|
|
|
421
467
|
*/
|
|
422
468
|
resumeRuntime(onStep) {
|
|
423
469
|
this.resetClock();
|
|
424
|
-
this.
|
|
470
|
+
this.setAnimationLoop((time) => this.tick(time, onStep));
|
|
425
471
|
}
|
|
426
472
|
/** Forgets the clock and any partial step, so the next frame runs no burst. */
|
|
427
473
|
resetClock() {
|
|
@@ -549,40 +595,6 @@ export class Game {
|
|
|
549
595
|
this.runtimeBridge = null;
|
|
550
596
|
this.audio.setSilenced(false);
|
|
551
597
|
};
|
|
552
|
-
/** Under y-sort, re-derives every participant's z from layer band + entity Y. */
|
|
553
|
-
applyYSort() {
|
|
554
|
-
const singles = [];
|
|
555
|
-
const batches = [];
|
|
556
|
-
const entries = [];
|
|
557
|
-
for (const entity of this.entities) {
|
|
558
|
-
for (const component of entity.components) {
|
|
559
|
-
if (isYSortBatchParticipant(component)) {
|
|
560
|
-
const batchEntries = component.ySortEntries();
|
|
561
|
-
batches.push({ participant: component, start: entries.length, count: batchEntries.length });
|
|
562
|
-
entries.push(...batchEntries);
|
|
563
|
-
}
|
|
564
|
-
else if (isYSortParticipant(component)) {
|
|
565
|
-
singles.push({ participant: component, index: entries.length });
|
|
566
|
-
entries.push({ layer: component.layer, y: entity.node.position.y });
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
for (const participant of sceneDrainsOf(this).participants()) {
|
|
571
|
-
const batchEntries = participant.ySortEntries();
|
|
572
|
-
batches.push({ participant, start: entries.length, count: batchEntries.length });
|
|
573
|
-
entries.push(...batchEntries);
|
|
574
|
-
}
|
|
575
|
-
const z = ySortZ(entries);
|
|
576
|
-
for (const { participant, index } of singles) {
|
|
577
|
-
const sortZ = z[index];
|
|
578
|
-
if (sortZ === undefined)
|
|
579
|
-
throw new Error(`ySortZ returned no z for y-sort participant ${index}`);
|
|
580
|
-
participant.setSortZ(sortZ);
|
|
581
|
-
}
|
|
582
|
-
for (const { participant, start, count } of batches) {
|
|
583
|
-
participant.setSortZs(z.slice(start, start + count));
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
598
|
renderSurface() {
|
|
587
599
|
if (this.sceneProjection === 'isometric') {
|
|
588
600
|
for (const entity of this.entities) {
|
|
@@ -592,7 +604,7 @@ export class Game {
|
|
|
592
604
|
}
|
|
593
605
|
}
|
|
594
606
|
if (this.renderSort === 'y')
|
|
595
|
-
this.
|
|
607
|
+
applyYSort(this.entities, sceneDrainsOf(this).participants());
|
|
596
608
|
this.ui.setActive(this.simulate);
|
|
597
609
|
// The shake offset exists only while drawing (ADR 0020): Anchored Pieces
|
|
598
610
|
// and the render see the drawn center, then the base comes back exactly,
|
|
@@ -603,6 +615,9 @@ export class Game {
|
|
|
603
615
|
this.camera.position.y = y + shake.y;
|
|
604
616
|
try {
|
|
605
617
|
anchoredPiecesOf(this.ui).place();
|
|
618
|
+
// Before ready() the frame simulated, but three cannot draw yet.
|
|
619
|
+
if (!this.readiness.isReady)
|
|
620
|
+
return;
|
|
606
621
|
if (this.resolution) {
|
|
607
622
|
// Letterbox bars: clear the whole canvas, then render inside the scissor.
|
|
608
623
|
this.renderer.setScissorTest(false);
|
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,8 @@ export type { GamepadControl } from './gamepad.js';
|
|
|
37
37
|
export { Pointer } from './pointer.js';
|
|
38
38
|
export type { PointerCamera, PointerDeps, PointerPick, PointerResolution } from './pointer.js';
|
|
39
39
|
export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
40
|
-
export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
|
|
40
|
+
export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeBridgeFailure, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
|
|
41
|
+
export type { RenderBackend } from './render-readiness.js';
|
|
41
42
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
42
43
|
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
43
44
|
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
|
|
@@ -76,4 +77,4 @@ export { StateMachine, evaluateTrigger, nextTransition } from './state/state-mac
|
|
|
76
77
|
export type { StateJson, StateTransitionJson, TriggerEnv } from './state/state-machine.js';
|
|
77
78
|
export { defineRole, defineStates, installArchetype, logicSet, registeredLogicSets, registeredRoles, resetRegistries, roleDefinition, } from './state/hooks.js';
|
|
78
79
|
export type { ArchetypeBundle, RoleDefinition, RoleGraph, StateContext, StateHooks, StateLogic, } from './state/hooks.js';
|
|
79
|
-
export * as THREE from 'three';
|
|
80
|
+
export * as THREE from 'three/webgpu';
|
package/dist/index.js
CHANGED
|
@@ -42,4 +42,6 @@ export { StateMachine, evaluateTrigger, nextTransition } from './state/state-mac
|
|
|
42
42
|
export { defineRole, defineStates, installArchetype, logicSet, registeredLogicSets, registeredRoles, resetRegistries, roleDefinition, } from './state/hooks.js';
|
|
43
43
|
// Explicit escape hatch while our own API grows: a single source of three
|
|
44
44
|
// for the whole workspace. The thesis is that three stays an implementation detail.
|
|
45
|
-
|
|
45
|
+
// It is the WebGPU build the engine draws with (ADR 0025): no classic WebGL renderer,
|
|
46
|
+
// and node materials (TSL) instead of GLSL shaders.
|
|
47
|
+
export * as THREE from 'three/webgpu';
|
package/dist/particle-batch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import type { AssetLoader } from './assets/asset-loader.js';
|
|
3
3
|
import type { ParticleBlend, ParticleOverflow } from './components/particle-emitter.js';
|
|
4
4
|
import { type ParticleSpawn } from './particle-pool.js';
|
package/dist/particle-batch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import { reportRejection } from './report-rejection.js';
|
|
3
3
|
import { ParticlePool, particleValueAt, } from './particle-pool.js';
|
|
4
4
|
import { projectIsometric } from './projection.js';
|
|
@@ -116,6 +116,9 @@ export class ParticleBatch {
|
|
|
116
116
|
colors.needsUpdate = true;
|
|
117
117
|
this.sortIndices();
|
|
118
118
|
this.mesh.geometry.setDrawRange(0, this.active * 6);
|
|
119
|
+
// WebGPURenderer issues a zero-count draw (WebGLRenderer skipped it):
|
|
120
|
+
// an empty batch is not drawn at all.
|
|
121
|
+
this.mesh.visible = this.active > 0;
|
|
119
122
|
}
|
|
120
123
|
ySortEntries() {
|
|
121
124
|
this.sortEntrySlots();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
/** Keeps Three's transparent-object sort key on live vertices, never unused capacity. */
|
|
3
3
|
export declare function refreshParticleSortBounds(geometry: THREE.BufferGeometry, active: number, emptyZ: number): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
/** Keeps Three's transparent-object sort key on live vertices, never unused capacity. */
|
|
3
3
|
export function refreshParticleSortBounds(geometry, active, emptyZ) {
|
|
4
4
|
const positions = geometry.getAttribute('position');
|
package/dist/pointer.js
CHANGED
|
@@ -140,7 +140,7 @@ export class Pointer {
|
|
|
140
140
|
renderX <= centerX + halfW &&
|
|
141
141
|
renderY >= centerY - halfH &&
|
|
142
142
|
renderY <= centerY + halfH) {
|
|
143
|
-
// Matches
|
|
143
|
+
// Matches applyYSort (render-sort.ts)'s key exactly: the entity's node
|
|
144
144
|
// position, which for an isometric scene is the projected Y (the
|
|
145
145
|
// same `anchor.y` just used to place this box), not the logical one.
|
|
146
146
|
hits.push({ entity, layer: box.layer, y: anchor.y });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
/**
|
|
3
|
+
* Makes WebGPURenderer draw the way WebGLRenderer did (ADR 0025): straight
|
|
4
|
+
* into the 8-bit canvas, each material encoding its color to sRGB in its own
|
|
5
|
+
* shader, so alpha blending happens on sRGB values. By default three renders
|
|
6
|
+
* into a linear half-float target and converts it in an extra full-screen
|
|
7
|
+
* pass: blending in linear space, which changes every translucent edge, and
|
|
8
|
+
* one more draw per frame. A render target keeps the linear working space.
|
|
9
|
+
*/
|
|
10
|
+
export declare function drawStraightToCanvas(renderer: THREE.WebGPURenderer): void;
|
|
11
|
+
/**
|
|
12
|
+
* The scene background for a canvas drawn straight: three clears with a
|
|
13
|
+
* color's working-space (linear) components, and the canvas stores sRGB, so
|
|
14
|
+
* the clear carries the color's sRGB components — what WebGLRenderer wrote.
|
|
15
|
+
*/
|
|
16
|
+
export declare function canvasBackground(color: THREE.ColorRepresentation): THREE.Color;
|
|
17
|
+
/**
|
|
18
|
+
* Applies a material map's UV transform (offset, repeat) per vertex, as
|
|
19
|
+
* WebGLRenderer's `vMapUv` did, instead of per fragment as three's node
|
|
20
|
+
* materials do by default. At a quad's corners the UV is 0 or 1, so the
|
|
21
|
+
* transformed corner UV is the same float however a shader compiler fuses
|
|
22
|
+
* the multiply and add — per fragment it is not, and a sprite whose sheet
|
|
23
|
+
* offset is not 0 could then sample a texel row apart in two shaders of the
|
|
24
|
+
* same picture (a Sprite Batch and its per-sprite fallback, ADR 0024).
|
|
25
|
+
* Textures with their own UV node, or sampled outside a mesh, stay three's.
|
|
26
|
+
*/
|
|
27
|
+
export declare function mapUvPerVertex(renderer: THREE.WebGPURenderer): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { uv, vertexStage, workingToColorSpace } from 'three/tsl';
|
|
2
|
+
import * as THREE from 'three/webgpu';
|
|
3
|
+
/**
|
|
4
|
+
* Makes WebGPURenderer draw the way WebGLRenderer did (ADR 0025): straight
|
|
5
|
+
* into the 8-bit canvas, each material encoding its color to sRGB in its own
|
|
6
|
+
* shader, so alpha blending happens on sRGB values. By default three renders
|
|
7
|
+
* into a linear half-float target and converts it in an extra full-screen
|
|
8
|
+
* pass: blending in linear space, which changes every translucent edge, and
|
|
9
|
+
* one more draw per frame. A render target keeps the linear working space.
|
|
10
|
+
*/
|
|
11
|
+
export function drawStraightToCanvas(renderer) {
|
|
12
|
+
// The working space as output: three then needs no intermediate target.
|
|
13
|
+
renderer.outputColorSpace = THREE.ColorManagement.workingColorSpace;
|
|
14
|
+
const context = renderer.contextNode.value;
|
|
15
|
+
context.getOutput = (output, builder) => builder.renderer.getRenderTarget() === null ? workingToColorSpace(output, THREE.SRGBColorSpace) : output;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The scene background for a canvas drawn straight: three clears with a
|
|
19
|
+
* color's working-space (linear) components, and the canvas stores sRGB, so
|
|
20
|
+
* the clear carries the color's sRGB components — what WebGLRenderer wrote.
|
|
21
|
+
*/
|
|
22
|
+
export function canvasBackground(color) {
|
|
23
|
+
const { r, g, b } = new THREE.Color(color).getRGB({ r: 0, g: 0, b: 0 }, THREE.SRGBColorSpace);
|
|
24
|
+
return new THREE.Color().setRGB(r, g, b, THREE.LinearSRGBColorSpace);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Applies a material map's UV transform (offset, repeat) per vertex, as
|
|
28
|
+
* WebGLRenderer's `vMapUv` did, instead of per fragment as three's node
|
|
29
|
+
* materials do by default. At a quad's corners the UV is 0 or 1, so the
|
|
30
|
+
* transformed corner UV is the same float however a shader compiler fuses
|
|
31
|
+
* the multiply and add — per fragment it is not, and a sprite whose sheet
|
|
32
|
+
* offset is not 0 could then sample a texel row apart in two shaders of the
|
|
33
|
+
* same picture (a Sprite Batch and its per-sprite fallback, ADR 0024).
|
|
34
|
+
* Textures with their own UV node, or sampled outside a mesh, stay three's.
|
|
35
|
+
*/
|
|
36
|
+
export function mapUvPerVertex(renderer) {
|
|
37
|
+
const context = renderer.contextNode.value;
|
|
38
|
+
context.getUV = (texture, builder) => {
|
|
39
|
+
if (texture.uvNode !== null || !builder.object.isMesh)
|
|
40
|
+
return null;
|
|
41
|
+
texture.setUpdateMatrix(false);
|
|
42
|
+
return vertexStage(texture.getTransformedUV(uv(texture.value.channel)));
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GPU API a Game draws through (CONTEXT.md): `webgpu` when the browser
|
|
3
|
+
* offers it, otherwise three's own WebGL2 fallback. The browser decides; no
|
|
4
|
+
* option forces either one (ADR 0025).
|
|
5
|
+
*/
|
|
6
|
+
export type RenderBackend = 'webgpu' | 'webgl2';
|
|
7
|
+
/** The part of three's WebGPURenderer readiness depends on. */
|
|
8
|
+
export interface InitializingRenderer {
|
|
9
|
+
init(): Promise<unknown>;
|
|
10
|
+
dispose(): Promise<void>;
|
|
11
|
+
/** After init(), the backend it settled on: the WebGPU one, or the WebGL2 fallback. */
|
|
12
|
+
readonly backend: object;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A renderer's asynchronous initialization, owned by its Game: started at
|
|
16
|
+
* construction, settled once, and the only gate between a frame and a draw.
|
|
17
|
+
* `promise` is what `game.ready()` hands out; a failure rejects it with an
|
|
18
|
+
* error naming both Render Backends and is also reported to `onFailure`.
|
|
19
|
+
*/
|
|
20
|
+
export declare class RenderReadiness {
|
|
21
|
+
private readonly renderer;
|
|
22
|
+
readonly promise: Promise<void>;
|
|
23
|
+
private state;
|
|
24
|
+
constructor(renderer: InitializingRenderer, onFailure: (error: Error) => void);
|
|
25
|
+
/** Runs `then` once the renderer is ready; never after a failed init. */
|
|
26
|
+
whenReady(then: () => void): void;
|
|
27
|
+
/** True once the renderer can draw. */
|
|
28
|
+
get isReady(): boolean;
|
|
29
|
+
/** The Render Backend after a successful init; null before it and after a failure. */
|
|
30
|
+
get backend(): RenderBackend | null;
|
|
31
|
+
/**
|
|
32
|
+
* Frees the renderer: right away once it is ready, after init settles
|
|
33
|
+
* while it is pending (three cannot dispose a renderer mid-init), never
|
|
34
|
+
* after a failed init (nothing was allocated).
|
|
35
|
+
*/
|
|
36
|
+
disposeRenderer(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** three flags its WebGPU backend with `isWebGPUBackend`; its WebGL2 fallback has no such flag. */
|
|
2
|
+
function backendOf(backend) {
|
|
3
|
+
return 'isWebGPUBackend' in backend && backend.isWebGPUBackend === true ? 'webgpu' : 'webgl2';
|
|
4
|
+
}
|
|
5
|
+
function describeCause(cause) {
|
|
6
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A renderer's asynchronous initialization, owned by its Game: started at
|
|
10
|
+
* construction, settled once, and the only gate between a frame and a draw.
|
|
11
|
+
* `promise` is what `game.ready()` hands out; a failure rejects it with an
|
|
12
|
+
* error naming both Render Backends and is also reported to `onFailure`.
|
|
13
|
+
*/
|
|
14
|
+
export class RenderReadiness {
|
|
15
|
+
renderer;
|
|
16
|
+
promise;
|
|
17
|
+
state = { status: 'pending' };
|
|
18
|
+
constructor(renderer, onFailure) {
|
|
19
|
+
this.renderer = renderer;
|
|
20
|
+
this.promise = renderer.init().then(() => {
|
|
21
|
+
this.state = { status: 'ready', backend: backendOf(renderer.backend) };
|
|
22
|
+
}, (cause) => {
|
|
23
|
+
this.state = { status: 'failed' };
|
|
24
|
+
const error = new Error(`No Render Backend could initialize: neither webgpu nor webgl2 is available (${describeCause(cause)}).`, { cause });
|
|
25
|
+
onFailure(error);
|
|
26
|
+
throw error;
|
|
27
|
+
});
|
|
28
|
+
// The rejection belongs to whoever awaits game.ready(); a Game nobody
|
|
29
|
+
// asked must not surface it as an unhandled rejection on top of that.
|
|
30
|
+
this.promise.catch(() => { });
|
|
31
|
+
}
|
|
32
|
+
/** Runs `then` once the renderer is ready; never after a failed init. */
|
|
33
|
+
whenReady(then) {
|
|
34
|
+
this.promise.then(then, () => { });
|
|
35
|
+
}
|
|
36
|
+
/** True once the renderer can draw. */
|
|
37
|
+
get isReady() {
|
|
38
|
+
return this.state.status === 'ready';
|
|
39
|
+
}
|
|
40
|
+
/** The Render Backend after a successful init; null before it and after a failure. */
|
|
41
|
+
get backend() {
|
|
42
|
+
return this.state.status === 'ready' ? this.state.backend : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Frees the renderer: right away once it is ready, after init settles
|
|
46
|
+
* while it is pending (three cannot dispose a renderer mid-init), never
|
|
47
|
+
* after a failed init (nothing was allocated).
|
|
48
|
+
*/
|
|
49
|
+
disposeRenderer() {
|
|
50
|
+
const dispose = () => {
|
|
51
|
+
this.renderer.dispose().catch((error) => {
|
|
52
|
+
console.error('[waica] could not dispose the renderer', error);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
if (this.isReady) {
|
|
56
|
+
dispose();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.promise.then(dispose, () => { });
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/render-sort.d.ts
CHANGED
|
@@ -35,3 +35,17 @@ export declare function isYSortBatchParticipant(value: unknown): value is YSortB
|
|
|
35
35
|
* apart, so their band is exactly the old fixed 0.01.
|
|
36
36
|
*/
|
|
37
37
|
export declare function ySortZ(entries: readonly YSortEntry[]): number[];
|
|
38
|
+
/** What applyYSort reads of an entity: its components and the Y its node draws at. */
|
|
39
|
+
export interface YSortEntity {
|
|
40
|
+
readonly components: readonly unknown[];
|
|
41
|
+
readonly node: {
|
|
42
|
+
readonly position: {
|
|
43
|
+
readonly y: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Under y-sort, re-derives every participant's z from layer band + entity Y:
|
|
49
|
+
* the scene's entities, then the batch participants its drains hold.
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyYSort(entities: readonly YSortEntity[], drains: readonly YSortBatchParticipant[]): void;
|
package/dist/render-sort.js
CHANGED
|
@@ -48,3 +48,40 @@ export function ySortZ(entries) {
|
|
|
48
48
|
}
|
|
49
49
|
return z;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Under y-sort, re-derives every participant's z from layer band + entity Y:
|
|
53
|
+
* the scene's entities, then the batch participants its drains hold.
|
|
54
|
+
*/
|
|
55
|
+
export function applyYSort(entities, drains) {
|
|
56
|
+
const singles = [];
|
|
57
|
+
const batches = [];
|
|
58
|
+
const entries = [];
|
|
59
|
+
for (const entity of entities) {
|
|
60
|
+
for (const component of entity.components) {
|
|
61
|
+
if (isYSortBatchParticipant(component)) {
|
|
62
|
+
const batchEntries = component.ySortEntries();
|
|
63
|
+
batches.push({ participant: component, start: entries.length, count: batchEntries.length });
|
|
64
|
+
entries.push(...batchEntries);
|
|
65
|
+
}
|
|
66
|
+
else if (isYSortParticipant(component)) {
|
|
67
|
+
singles.push({ participant: component, index: entries.length });
|
|
68
|
+
entries.push({ layer: component.layer, y: entity.node.position.y });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const participant of drains) {
|
|
73
|
+
const batchEntries = participant.ySortEntries();
|
|
74
|
+
batches.push({ participant, start: entries.length, count: batchEntries.length });
|
|
75
|
+
entries.push(...batchEntries);
|
|
76
|
+
}
|
|
77
|
+
const z = ySortZ(entries);
|
|
78
|
+
for (const { participant, index } of singles) {
|
|
79
|
+
const sortZ = z[index];
|
|
80
|
+
if (sortZ === undefined)
|
|
81
|
+
throw new Error(`ySortZ returned no z for y-sort participant ${index}`);
|
|
82
|
+
participant.setSortZ(sortZ);
|
|
83
|
+
}
|
|
84
|
+
for (const { participant, start, count } of batches) {
|
|
85
|
+
participant.setSortZs(z.slice(start, start + count));
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/runtime-bridge.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssetStatus } from './assets/asset-loader.js';
|
|
2
|
+
import type { RenderBackend } from './render-readiness.js';
|
|
2
3
|
import type { RuntimeSnapshot, RuntimeSnapshotFilters } from './runtime-inspection.js';
|
|
3
4
|
export declare const RUNTIME_BRIDGE_PROTOCOL_VERSION: 1;
|
|
4
5
|
export declare const RUNTIME_BRIDGE_SYMBOL: unique symbol;
|
|
@@ -20,9 +21,13 @@ export type RuntimeMode = 'paused' | 'real-time';
|
|
|
20
21
|
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
21
22
|
* `analog-actions` (issue #75, ADR 0023) announces that `hold` takes an
|
|
22
23
|
* optional `value` in (0, 1] and that every control result carries
|
|
23
|
-
* `actionValues` beside `heldActions`.
|
|
24
|
+
* `actionValues` beside `heldActions`. `render-backend` (issue #140, ADR
|
|
25
|
+
* 0025) announces that every metadata carries `backend`: the Render Backend
|
|
26
|
+
* once the renderer is ready, null before — a Run Session waits for it — and
|
|
27
|
+
* that a renderer that fails to initialize reports through the activation's
|
|
28
|
+
* `fail`.
|
|
24
29
|
*/
|
|
25
|
-
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects', 'analog-actions'];
|
|
30
|
+
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects', 'analog-actions', 'render-backend'];
|
|
26
31
|
export interface RuntimeMetadata {
|
|
27
32
|
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
28
33
|
engineVersion: string;
|
|
@@ -32,6 +37,8 @@ export interface RuntimeMetadata {
|
|
|
32
37
|
capabilities: readonly string[];
|
|
33
38
|
/** `game.assets.status` at the moment of the read — reading it never advances `frame`. */
|
|
34
39
|
assets: AssetStatus;
|
|
40
|
+
/** The Render Backend once `game.ready()` resolved; null until then (and for good after a failed init). */
|
|
41
|
+
backend: RenderBackend | null;
|
|
35
42
|
}
|
|
36
43
|
export type RuntimeControlRequest = {
|
|
37
44
|
operation: 'press' | 'release';
|
|
@@ -76,11 +83,18 @@ export interface RuntimeBridge {
|
|
|
76
83
|
inspect(filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
77
84
|
control(request: RuntimeControlRequest): RuntimeControlResult;
|
|
78
85
|
}
|
|
86
|
+
/** Why a Game can never become operational; reported through the activation. */
|
|
87
|
+
export interface RuntimeBridgeFailure {
|
|
88
|
+
code: 'render-backend-failed';
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
79
91
|
/** Ephemeral pre-page hook installed by the owner of a browser context. */
|
|
80
92
|
export interface RuntimeBridgeActivation {
|
|
81
93
|
readonly protocolVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
82
94
|
register(bridge: RuntimeBridge): void;
|
|
83
95
|
unregister(bridge: RuntimeBridge): void;
|
|
96
|
+
/** Optional: told when the Game's renderer fails to initialize, so the owner stops waiting. */
|
|
97
|
+
fail?(failure: RuntimeBridgeFailure): void;
|
|
84
98
|
}
|
|
85
99
|
export declare function activeRuntimeBridgeHook(): RuntimeBridgeActivation | null;
|
|
86
100
|
export interface RuntimeBridgeHost {
|
|
@@ -104,6 +118,8 @@ export interface RuntimeBridgeHost {
|
|
|
104
118
|
availableScenes(): string[];
|
|
105
119
|
/** A fresh `game.assets.status`. */
|
|
106
120
|
assets(): AssetStatus;
|
|
121
|
+
/** The Render Backend, null until the renderer is ready. */
|
|
122
|
+
backend(): RenderBackend | null;
|
|
107
123
|
}
|
|
108
124
|
export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
109
125
|
readonly surface: HTMLCanvasElement;
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -20,7 +20,11 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
|
20
20
|
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
21
21
|
* `analog-actions` (issue #75, ADR 0023) announces that `hold` takes an
|
|
22
22
|
* optional `value` in (0, 1] and that every control result carries
|
|
23
|
-
* `actionValues` beside `heldActions`.
|
|
23
|
+
* `actionValues` beside `heldActions`. `render-backend` (issue #140, ADR
|
|
24
|
+
* 0025) announces that every metadata carries `backend`: the Render Backend
|
|
25
|
+
* once the renderer is ready, null before — a Run Session waits for it — and
|
|
26
|
+
* that a renderer that fails to initialize reports through the activation's
|
|
27
|
+
* `fail`.
|
|
24
28
|
*/
|
|
25
29
|
export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
26
30
|
'click',
|
|
@@ -29,6 +33,7 @@ export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
|
29
33
|
'assets',
|
|
30
34
|
'camera-effects',
|
|
31
35
|
'analog-actions',
|
|
36
|
+
'render-backend',
|
|
32
37
|
];
|
|
33
38
|
export class RuntimeBridgeOperationError extends Error {
|
|
34
39
|
code;
|
|
@@ -79,6 +84,7 @@ export class EngineRuntimeBridge {
|
|
|
79
84
|
simulationTime: this.frame * SIMULATION_STEP,
|
|
80
85
|
capabilities: RUNTIME_BRIDGE_CAPABILITIES,
|
|
81
86
|
assets: this.host.assets(),
|
|
87
|
+
backend: this.host.backend(),
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
inspect(filters = {}) {
|
package/dist/sprite-batch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import type { TextureOutcome } from './assets/asset-loader.js';
|
|
3
3
|
export type SpriteBatchShape = 'rectangle' | 'circle';
|
|
4
4
|
/**
|
|
@@ -44,7 +44,7 @@ export declare class SpriteRunMesh {
|
|
|
44
44
|
readonly mesh: THREE.InstancedMesh;
|
|
45
45
|
private readonly colors;
|
|
46
46
|
private readonly uvs;
|
|
47
|
-
constructor(base: THREE.BufferGeometry, material: THREE.
|
|
47
|
+
constructor(base: THREE.BufferGeometry, material: THREE.MeshBasicNodeMaterial, capacity: number);
|
|
48
48
|
get capacity(): number;
|
|
49
49
|
/** Writes the sprite drawn `index`-th in this run. */
|
|
50
50
|
write(index: number, instance: SpriteBatchInstance): void;
|
|
@@ -61,7 +61,7 @@ export declare class SpriteRunMesh {
|
|
|
61
61
|
*/
|
|
62
62
|
export declare class SpriteBatch {
|
|
63
63
|
private readonly geometry;
|
|
64
|
-
readonly material: THREE.
|
|
64
|
+
readonly material: THREE.MeshBasicNodeMaterial;
|
|
65
65
|
/** When the key's texture settled; null for untextured and bare entries. */
|
|
66
66
|
readonly settled: Promise<TextureOutcome> | null;
|
|
67
67
|
private readonly slots;
|
package/dist/sprite-batch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { attribute, context, mat3, positionLocal, uv, vec3, vertexStage } from 'three/tsl';
|
|
2
|
+
import * as THREE from 'three/webgpu';
|
|
2
3
|
/** The first capacity of a batch's instance buffers; it doubles when full. */
|
|
3
4
|
export const INITIAL_SPRITE_BATCH_CAPACITY = 16;
|
|
4
5
|
/** The key's canonical string: equal keys share one batch. */
|
|
@@ -12,41 +13,52 @@ export function spriteGeometry(shape) {
|
|
|
12
13
|
}
|
|
13
14
|
/** The per-instance UV transform a run carries: repeat (x, y) and offset (z, w). */
|
|
14
15
|
const INSTANCE_UV = 'instanceUv';
|
|
16
|
+
/** Whether three is building the material for one of the batch's instanced run meshes. */
|
|
17
|
+
function drawsInstances(builder) {
|
|
18
|
+
return builder.object.isInstancedMesh === true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The map UV of one instance: its repeat and offset as the same mat3 a
|
|
22
|
+
* texture's own matrix is, applied to the quad's UV per vertex — as every
|
|
23
|
+
* other map is (render-output.ts `mapUvPerVertex`) — so the corner UVs are
|
|
24
|
+
* the very floats the per-sprite draw interpolates.
|
|
25
|
+
*/
|
|
26
|
+
function instanceMapUv() {
|
|
27
|
+
const transform = attribute(INSTANCE_UV, 'vec4');
|
|
28
|
+
return vertexStage(mat3(transform.x, 0, 0, 0, transform.y, 0, transform.z, transform.w, 1).mul(vec3(uv(), 1)).xy);
|
|
29
|
+
}
|
|
15
30
|
/**
|
|
16
|
-
*
|
|
17
|
-
* holds view × world (three's own `modelViewMatrix` for that
|
|
18
|
-
* the same way on the CPU), so
|
|
19
|
-
* map's UV transform comes from the instance instead
|
|
20
|
-
*
|
|
21
|
-
* and if one were ever drawn as a plain mesh it
|
|
31
|
+
* Every batch's material, as a node material (ADR 0025). The instance
|
|
32
|
+
* matrix already holds view × world (three's own `modelViewMatrix` for that
|
|
33
|
+
* sprite, computed the same way on the CPU), so an instanced draw skips
|
|
34
|
+
* `modelViewMatrix`; the map's UV transform comes from the instance instead
|
|
35
|
+
* of the texture. Both apply to instanced draws only: the hidden anchors
|
|
36
|
+
* share this material, and if one were ever drawn as a plain mesh it gets
|
|
37
|
+
* three's own transforms.
|
|
22
38
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
' #include <project_vertex>',
|
|
41
|
-
'#endif',
|
|
42
|
-
].join('\n'));
|
|
39
|
+
class SpriteBatchMaterial extends THREE.MeshBasicNodeMaterial {
|
|
40
|
+
constructor(parameters) {
|
|
41
|
+
super(parameters);
|
|
42
|
+
this.contextNode = context({
|
|
43
|
+
getUV: (texture, builder) => {
|
|
44
|
+
if (!drawsInstances(builder))
|
|
45
|
+
return null;
|
|
46
|
+
// The instance transform replaces the map's own matrix.
|
|
47
|
+
texture.setUpdateMatrix(false);
|
|
48
|
+
return instanceMapUv();
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
setupPositionView(builder) {
|
|
53
|
+
// three's instancing already multiplied positionLocal by the instance matrix.
|
|
54
|
+
return drawsInstances(builder) ? positionLocal : super.setupPositionView(builder);
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
57
|
function createMaterial(key, textures) {
|
|
45
58
|
// Double-sided: a flipped sprite mirrors through a negative x scale in its
|
|
46
59
|
// matrix, exactly like its own mesh; per instance there is no winding flip.
|
|
47
|
-
const material = new
|
|
60
|
+
const material = new SpriteBatchMaterial({ transparent: true, side: THREE.DoubleSide });
|
|
48
61
|
material.forceSinglePass = true;
|
|
49
|
-
material.onBeforeCompile = useInstanceTransforms;
|
|
50
62
|
if (key.texture === null)
|
|
51
63
|
return { material, settled: null };
|
|
52
64
|
const { texture, settled } = key.texture
|
package/dist/sprite-batches.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import type { TextureOutcome } from './assets/asset-loader.js';
|
|
3
3
|
import type { Game } from './game.js';
|
|
4
4
|
import { SpriteBatch, type SpriteBatchInstance, type SpriteBatchKey, type SpriteBatchTextures } from './sprite-batch.js';
|
|
@@ -7,7 +7,7 @@ export interface SpriteUvSource {
|
|
|
7
7
|
readonly offset: THREE.Vector2;
|
|
8
8
|
readonly repeat: THREE.Vector2;
|
|
9
9
|
}
|
|
10
|
-
export type SpriteAnchor = THREE.Mesh<THREE.BufferGeometry, THREE.
|
|
10
|
+
export type SpriteAnchor = THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicNodeMaterial>;
|
|
11
11
|
/** Where an instance sits: its key, that key's batch and its slot there. */
|
|
12
12
|
export interface SpriteBatchSeat {
|
|
13
13
|
key: SpriteBatchKey;
|
package/dist/sprite-batches.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import { SpriteBatch, spriteBatchKeyId, spriteGeometry, } from './sprite-batch.js';
|
|
3
3
|
import { TransparentDrawCollector } from './sprite-draw-collector.js';
|
|
4
4
|
import { compareTransparentDraws, forEachSpriteRun } from './sprite-runs.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
import type { Drawable } from './sprite-runs.js';
|
|
3
3
|
/** A frame drawable: what three sorts it by, the object, and the sprite instance it stands for (null otherwise). */
|
|
4
4
|
export interface FrameDrawable<K, I> extends Drawable<K> {
|
|
@@ -12,8 +12,8 @@ export interface CollectedSprites<K, I> {
|
|
|
12
12
|
keyOf(instance: I): K;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* One frame's transparent drawables, collected the way
|
|
16
|
-
*
|
|
15
|
+
* One frame's transparent drawables, collected the way three's renderer
|
|
16
|
+
* (`_projectObject`) walks the scene: visibility, camera layers, Group render
|
|
17
17
|
* order, frustum culling, and the clip-space z it sorts by. Sprite anchors
|
|
18
18
|
* are hidden yet collected when in view; `skip` (the batches' own run
|
|
19
19
|
* meshes) is never walked. The drawables are pooled across frames.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
2
|
function isRenderable(object) {
|
|
3
3
|
const flags = object;
|
|
4
4
|
return flags.isMesh === true || flags.isLine === true || flags.isPoints === true || flags.isSprite === true;
|
|
@@ -25,8 +25,8 @@ function sortCenter(object) {
|
|
|
25
25
|
return own.boundingSphere?.center ?? new THREE.Vector3();
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* One frame's transparent drawables, collected the way
|
|
29
|
-
*
|
|
28
|
+
* One frame's transparent drawables, collected the way three's renderer
|
|
29
|
+
* (`_projectObject`) walks the scene: visibility, camera layers, Group render
|
|
30
30
|
* order, frustum culling, and the clip-space z it sorts by. Sprite anchors
|
|
31
31
|
* are hidden yet collected when in view; `skip` (the batches' own run
|
|
32
32
|
* meshes) is never walked. The drawables are pooled across frames.
|