@waica/engine 0.23.0 → 0.25.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 +43 -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.d.ts +6 -0
- package/dist/components/animated-sprite.js +16 -2
- package/dist/components/light.d.ts +68 -0
- package/dist/components/light.js +59 -0
- package/dist/components/particle-emitter.d.ts +6 -0
- package/dist/components/particle-emitter.js +13 -0
- package/dist/components/sprite.d.ts +6 -0
- package/dist/components/sprite.js +16 -2
- package/dist/components/tilemap.js +9 -1
- package/dist/entity.d.ts +1 -1
- package/dist/entity.js +1 -1
- package/dist/frame-quad.d.ts +21 -0
- package/dist/frame-quad.js +50 -0
- package/dist/game.d.ts +28 -3
- package/dist/game.js +79 -52
- package/dist/index.d.ts +14 -3
- package/dist/index.js +9 -1
- package/dist/light-field.d.ts +78 -0
- package/dist/light-field.js +145 -0
- package/dist/light-map-material.d.ts +40 -0
- package/dist/light-map-material.js +203 -0
- package/dist/light-map.d.ts +46 -0
- package/dist/light-map.js +132 -0
- package/dist/lit-frame.d.ts +48 -0
- package/dist/lit-frame.js +167 -0
- package/dist/occluder-grid.d.ts +19 -0
- package/dist/occluder-grid.js +50 -0
- package/dist/particle-batch.d.ts +1 -1
- package/dist/particle-batch.js +7 -3
- 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/post-effects.d.ts +33 -0
- package/dist/post-effects.js +48 -0
- package/dist/post-pass.d.ts +23 -0
- package/dist/post-pass.js +93 -0
- package/dist/render-layers.d.ts +18 -0
- package/dist/render-layers.js +19 -0
- package/dist/render-output.d.ts +34 -0
- package/dist/render-output.js +60 -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/runtime-inspection.d.ts +5 -0
- package/dist/runtime-inspection.js +5 -1
- package/dist/runtime-lighting-snapshot.d.ts +41 -0
- package/dist/runtime-lighting-snapshot.js +46 -0
- package/dist/scene-lighting.d.ts +52 -0
- package/dist/scene-lighting.js +107 -0
- package/dist/scene-render-options.d.ts +51 -0
- package/dist/scene-render-options.js +126 -0
- package/dist/scene.d.ts +29 -0
- package/dist/sprite-batch.d.ts +3 -3
- package/dist/sprite-batch.js +45 -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/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';
|
|
@@ -12,12 +12,18 @@ import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedT
|
|
|
12
12
|
import { advanceGameTime, GameTime } from './game-time.js';
|
|
13
13
|
import { Input } from './input.js';
|
|
14
14
|
import { Pointer } from './pointer.js';
|
|
15
|
+
import { GamePost } from './post-effects.js';
|
|
15
16
|
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
16
17
|
import { RuntimeInspector } from './runtime-inspection.js';
|
|
18
|
+
import { FrameComposer } from './lit-frame.js';
|
|
19
|
+
import { EMISSIVE_LAYER } from './render-layers.js';
|
|
17
20
|
import { projectIsometric, unprojectIsometric } from './projection.js';
|
|
18
|
-
import {
|
|
21
|
+
import { canvasBackground, drawStraightToCanvas, mapUvPerVertex } from './render-output.js';
|
|
22
|
+
import { RenderReadiness } from './render-readiness.js';
|
|
23
|
+
import { applyYSort } from './render-sort.js';
|
|
19
24
|
import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
|
|
20
25
|
import { sceneDrainsOf } from './scene-drains.js';
|
|
26
|
+
import { GameLighting } from './scene-lighting.js';
|
|
21
27
|
import { createSpriteBatches } from './sprite-batches.js';
|
|
22
28
|
import { createSpatialQuery } from './spatial-query.js';
|
|
23
29
|
import { Stats } from './stats.js';
|
|
@@ -62,6 +68,10 @@ export class Game {
|
|
|
62
68
|
cameraEffects;
|
|
63
69
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
64
70
|
time = new GameTime();
|
|
71
|
+
/** The live scene's Ambient Light and Lights (ADR 0026); it dies with its scene. */
|
|
72
|
+
lighting = new GameLighting();
|
|
73
|
+
/** The live scene's Post Effects (vignette, color grade); they die with their scene. */
|
|
74
|
+
post = new GamePost();
|
|
65
75
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
66
76
|
registry = null;
|
|
67
77
|
paramOverrides = {};
|
|
@@ -70,11 +80,14 @@ export class Game {
|
|
|
70
80
|
* or collisions — the editor's edit mode.
|
|
71
81
|
*/
|
|
72
82
|
simulate = true;
|
|
73
|
-
|
|
83
|
+
/** WebGPU when the browser offers it, else three's WebGL2 fallback (ADR 0025). */
|
|
74
84
|
renderer;
|
|
85
|
+
readiness;
|
|
75
86
|
resizeObserver;
|
|
76
87
|
/** Scene-scoped Sprite Batches (ADR 0024); `render.batch: false` opts a scene out. */
|
|
77
88
|
spriteBatches;
|
|
89
|
+
/** Off, lit or through the Post Effects: how each frame is drawn (ADR 0026). */
|
|
90
|
+
frame;
|
|
78
91
|
updateFns = new Set();
|
|
79
92
|
invalidUpdateCompositions = new WeakMap();
|
|
80
93
|
/**
|
|
@@ -105,6 +118,8 @@ export class Game {
|
|
|
105
118
|
liveSceneName = null;
|
|
106
119
|
/** True for the whole extent of a runFrame() call, incl. its tail. */
|
|
107
120
|
insideFrame = false;
|
|
121
|
+
/** Set by dispose(): a disposed Game never starts again nor reports its renderer (review #3). */
|
|
122
|
+
disposed = false;
|
|
108
123
|
/** A loadSceneByName() enqueued while insideFrame; applied at the next runFrame's start. */
|
|
109
124
|
pendingSceneLoad = null;
|
|
110
125
|
constructor(options) {
|
|
@@ -142,11 +157,25 @@ export class Game {
|
|
|
142
157
|
// One screen pixel in world units: the shake snaps to it (never the base).
|
|
143
158
|
pixel: () => (this.resolution ? this.viewHeight / this.resolution.height : null),
|
|
144
159
|
});
|
|
145
|
-
this.renderer = new THREE.
|
|
160
|
+
this.renderer = new THREE.WebGPURenderer({ canvas, antialias: true });
|
|
146
161
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
147
|
-
|
|
162
|
+
// The model view on the CPU, as three's WebGL renderer computed it: a Sprite
|
|
163
|
+
// Batch instance carries that same matrix (ADR 0024), so batched and
|
|
164
|
+
// per-sprite draws stay pixel-identical.
|
|
165
|
+
this.renderer.highPrecision = true;
|
|
166
|
+
drawStraightToCanvas(this.renderer);
|
|
167
|
+
mapUvPerVertex(this.renderer);
|
|
168
|
+
this.readiness = new RenderReadiness(this.renderer, (error) => {
|
|
169
|
+
if (!this.disposed)
|
|
170
|
+
activeRuntimeBridgeHook()?.fail?.({ code: 'render-backend-failed', message: error.message });
|
|
171
|
+
});
|
|
172
|
+
this.scene.background = canvasBackground(background);
|
|
148
173
|
this.camera = new THREE.OrthographicCamera();
|
|
149
174
|
this.camera.position.z = 10;
|
|
175
|
+
// Emissive drawables sit on their own layer; an unlit frame draws it in place.
|
|
176
|
+
this.camera.layers.enable(EMISSIVE_LAYER);
|
|
177
|
+
const { renderer, spriteBatches, resolution } = this;
|
|
178
|
+
this.frame = new FrameComposer({ game: this, renderer, spriteBatches, resolution });
|
|
150
179
|
this.pointer = new Pointer(canvas, {
|
|
151
180
|
camera: this.camera,
|
|
152
181
|
resolution: this.resolution,
|
|
@@ -157,6 +186,19 @@ export class Game {
|
|
|
157
186
|
this.resizeObserver = new ResizeObserver(() => this.resize());
|
|
158
187
|
this.resizeObserver.observe(canvas);
|
|
159
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolves once the renderer can draw; rejects, naming both Render
|
|
191
|
+
* Backends, when neither initializes. Every call returns the same promise.
|
|
192
|
+
* Frames before it simulate but draw nothing, so a host awaits it before
|
|
193
|
+
* its first frame (ADR 0025).
|
|
194
|
+
*/
|
|
195
|
+
ready() {
|
|
196
|
+
return this.readiness.promise;
|
|
197
|
+
}
|
|
198
|
+
/** The Render Backend the renderer settled on; null until ready() resolved. */
|
|
199
|
+
get backend() {
|
|
200
|
+
return this.readiness.backend;
|
|
201
|
+
}
|
|
160
202
|
/** Creates a live entity in the scene. */
|
|
161
203
|
spawn(name) {
|
|
162
204
|
const entity = new Entity(this, name);
|
|
@@ -200,6 +242,8 @@ export class Game {
|
|
|
200
242
|
// destroyed below, so an owner's own destroy() cancellation is a no-op.
|
|
201
243
|
this.time.cancelSceneScoped();
|
|
202
244
|
this.cameraEffects.unloadScene();
|
|
245
|
+
this.lighting.unloadScene();
|
|
246
|
+
this.post.unloadScene();
|
|
203
247
|
// An explicit unload means "no scene": a swap queued earlier this frame
|
|
204
248
|
// would otherwise flush next frame and resurrect one.
|
|
205
249
|
this.pendingSceneLoad = null;
|
|
@@ -306,6 +350,8 @@ export class Game {
|
|
|
306
350
|
setSceneRender(json) {
|
|
307
351
|
this.renderSort = json?.sort === 'y' ? 'y' : null;
|
|
308
352
|
this.spriteBatches.enabled = json?.batch !== false;
|
|
353
|
+
this.lighting.loadScene(json);
|
|
354
|
+
this.post.loadScene(json);
|
|
309
355
|
const projection = json?.projection === 'isometric' ? 'isometric' : null;
|
|
310
356
|
if (projection === this.sceneProjection)
|
|
311
357
|
return;
|
|
@@ -338,6 +384,8 @@ export class Game {
|
|
|
338
384
|
this.setViewHeight(this.sceneCamera.zoom);
|
|
339
385
|
}
|
|
340
386
|
start() {
|
|
387
|
+
if (this.disposed)
|
|
388
|
+
return;
|
|
341
389
|
const activation = activeRuntimeBridgeHook();
|
|
342
390
|
if (activation) {
|
|
343
391
|
if (!this.runtimeBridge) {
|
|
@@ -357,19 +405,27 @@ export class Game {
|
|
|
357
405
|
loadScene: (name) => this.loadSceneByName(name),
|
|
358
406
|
availableScenes: () => this.availableScenes,
|
|
359
407
|
assets: () => this.assets.status,
|
|
408
|
+
backend: () => this.backend,
|
|
360
409
|
});
|
|
361
410
|
activation.register(this.runtimeBridge);
|
|
362
411
|
this.audio.setSilenced(true);
|
|
363
412
|
window.addEventListener('pagehide', this.unregisterRuntimeBridge);
|
|
364
413
|
}
|
|
365
414
|
this.renderSurface();
|
|
415
|
+
// Started before ready(): the paused frame draws once the renderer can, frame unchanged.
|
|
416
|
+
if (!this.readiness.isReady) {
|
|
417
|
+
this.readiness.whenReady(() => {
|
|
418
|
+
if (this.runtimeBridge)
|
|
419
|
+
this.renderSurface();
|
|
420
|
+
});
|
|
421
|
+
}
|
|
366
422
|
return;
|
|
367
423
|
}
|
|
368
424
|
this.resetClock();
|
|
369
|
-
this.
|
|
425
|
+
this.setAnimationLoop((time) => this.tick(time));
|
|
370
426
|
}
|
|
371
427
|
stop() {
|
|
372
|
-
this.
|
|
428
|
+
this.setAnimationLoop(null);
|
|
373
429
|
}
|
|
374
430
|
/** Internal: called by Entity.destroy(). Its Anchored Pieces go (or freeze) with it. */
|
|
375
431
|
removeEntity(entity) {
|
|
@@ -392,6 +448,7 @@ export class Game {
|
|
|
392
448
|
}
|
|
393
449
|
/** Shuts the game down completely (loop, input, GPU). */
|
|
394
450
|
dispose() {
|
|
451
|
+
this.disposed = true;
|
|
395
452
|
this.stop();
|
|
396
453
|
this.unregisterRuntimeBridge();
|
|
397
454
|
this.input.dispose();
|
|
@@ -410,7 +467,15 @@ export class Game {
|
|
|
410
467
|
// After the entities: their clones go with the cascade above, the
|
|
411
468
|
// cached bases go here, exactly once (ADR 0019).
|
|
412
469
|
this.assets.dispose();
|
|
413
|
-
this.
|
|
470
|
+
this.frame.dispose();
|
|
471
|
+
this.readiness.disposeRenderer();
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* three installs the loop once its init settled; a failed init is
|
|
475
|
+
* reported by ready(), not by every loop change made before it.
|
|
476
|
+
*/
|
|
477
|
+
setAnimationLoop(loop) {
|
|
478
|
+
this.renderer.setAnimationLoop(loop).catch(() => { });
|
|
414
479
|
}
|
|
415
480
|
/**
|
|
416
481
|
* Real-time playback for the Runtime Bridge: the same clock-driven loop
|
|
@@ -421,7 +486,7 @@ export class Game {
|
|
|
421
486
|
*/
|
|
422
487
|
resumeRuntime(onStep) {
|
|
423
488
|
this.resetClock();
|
|
424
|
-
this.
|
|
489
|
+
this.setAnimationLoop((time) => this.tick(time, onStep));
|
|
425
490
|
}
|
|
426
491
|
/** Forgets the clock and any partial step, so the next frame runs no burst. */
|
|
427
492
|
resetClock() {
|
|
@@ -549,40 +614,6 @@ export class Game {
|
|
|
549
614
|
this.runtimeBridge = null;
|
|
550
615
|
this.audio.setSilenced(false);
|
|
551
616
|
};
|
|
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
617
|
renderSurface() {
|
|
587
618
|
if (this.sceneProjection === 'isometric') {
|
|
588
619
|
for (const entity of this.entities) {
|
|
@@ -592,7 +623,7 @@ export class Game {
|
|
|
592
623
|
}
|
|
593
624
|
}
|
|
594
625
|
if (this.renderSort === 'y')
|
|
595
|
-
this.
|
|
626
|
+
applyYSort(this.entities, sceneDrainsOf(this).participants());
|
|
596
627
|
this.ui.setActive(this.simulate);
|
|
597
628
|
// The shake offset exists only while drawing (ADR 0020): Anchored Pieces
|
|
598
629
|
// and the render see the drawn center, then the base comes back exactly,
|
|
@@ -603,14 +634,10 @@ export class Game {
|
|
|
603
634
|
this.camera.position.y = y + shake.y;
|
|
604
635
|
try {
|
|
605
636
|
anchoredPiecesOf(this.ui).place();
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
this.renderer.clear(true, false, false);
|
|
611
|
-
this.renderer.setScissorTest(true);
|
|
612
|
-
}
|
|
613
|
-
this.spriteBatches.drawFrame(this.scene, this.camera, () => this.renderer.render(this.scene, this.camera));
|
|
637
|
+
// Before ready() the frame simulated, but three cannot draw yet.
|
|
638
|
+
if (!this.readiness.isReady)
|
|
639
|
+
return;
|
|
640
|
+
this.frame.draw();
|
|
614
641
|
}
|
|
615
642
|
finally {
|
|
616
643
|
this.camera.position.x = x;
|
package/dist/index.d.ts
CHANGED
|
@@ -37,8 +37,10 @@ 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';
|
|
43
|
+
export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
|
|
42
44
|
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
43
45
|
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
|
|
44
46
|
export { Stats } from './stats.js';
|
|
@@ -46,6 +48,15 @@ export type { StatValue } from './stats.js';
|
|
|
46
48
|
export { GameUi } from './ui.js';
|
|
47
49
|
export type { AnchoredPieceHandle, AttachOptions } from './anchored-pieces.js';
|
|
48
50
|
export { Sprite } from './components/sprite.js';
|
|
51
|
+
export { Light } from './components/light.js';
|
|
52
|
+
export { GameLighting } from './scene-lighting.js';
|
|
53
|
+
export type { AmbientLightInput } from './scene-lighting.js';
|
|
54
|
+
export { GamePost } from './post-effects.js';
|
|
55
|
+
export { EMISSIVE_LAYER, setEmissive } from './render-layers.js';
|
|
56
|
+
export { lightFalloff, lightMapValue, lightVisibility } from './light-field.js';
|
|
57
|
+
export type { LightField, OccluderGrid, Rgb } from './light-field.js';
|
|
58
|
+
export { sceneRenderIssues } from './scene-render-options.js';
|
|
59
|
+
export type { AmbientLight, ColorGradeEffect, PostEffectsState, SceneRenderIssue, VignetteEffect, } from './scene-render-options.js';
|
|
49
60
|
export { ParticleEmitter } from './components/particle-emitter.js';
|
|
50
61
|
export type { ParticleBlend, ParticleDestroyMode, ParticleOverflow, ParticleSpace, ParticleVector, } from './components/particle-emitter.js';
|
|
51
62
|
export { Solid } from './components/solid.js';
|
|
@@ -65,7 +76,7 @@ export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, }
|
|
|
65
76
|
export type { EntityWith, NearestQueryContext, NearestSpatialQueryFilter, RayHit, SpatialQuery, SpatialQueryFilter, } from './spatial-query.js';
|
|
66
77
|
export { Emitter } from './events.js';
|
|
67
78
|
export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
|
|
68
|
-
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, PrefabJson, } from './scene.js';
|
|
79
|
+
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, SceneLightingJson, ScenePostJson, PrefabJson, } from './scene.js';
|
|
69
80
|
export { ClipPlayer } from './animation/clip-player.js';
|
|
70
81
|
export type { ClipDef } from './animation/clip-player.js';
|
|
71
82
|
export { sheetCell, sheetFrameCount, locateFrame } from './animation/sheet.js';
|
|
@@ -76,4 +87,4 @@ export { StateMachine, evaluateTrigger, nextTransition } from './state/state-mac
|
|
|
76
87
|
export type { StateJson, StateTransitionJson, TriggerEnv } from './state/state-machine.js';
|
|
77
88
|
export { defineRole, defineStates, installArchetype, logicSet, registeredLogicSets, registeredRoles, resetRegistries, roleDefinition, } from './state/hooks.js';
|
|
78
89
|
export type { ArchetypeBundle, RoleDefinition, RoleGraph, StateContext, StateHooks, StateLogic, } from './state/hooks.js';
|
|
79
|
-
export * as THREE from 'three';
|
|
90
|
+
export * as THREE from 'three/webgpu';
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,12 @@ export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
|
22
22
|
export { Stats } from './stats.js';
|
|
23
23
|
export { GameUi } from './ui.js';
|
|
24
24
|
export { Sprite } from './components/sprite.js';
|
|
25
|
+
export { Light } from './components/light.js';
|
|
26
|
+
export { GameLighting } from './scene-lighting.js';
|
|
27
|
+
export { GamePost } from './post-effects.js';
|
|
28
|
+
export { EMISSIVE_LAYER, setEmissive } from './render-layers.js';
|
|
29
|
+
export { lightFalloff, lightMapValue, lightVisibility } from './light-field.js';
|
|
30
|
+
export { sceneRenderIssues } from './scene-render-options.js';
|
|
25
31
|
export { ParticleEmitter } from './components/particle-emitter.js';
|
|
26
32
|
export { Solid } from './components/solid.js';
|
|
27
33
|
export { Hitbox } from './components/hitbox.js';
|
|
@@ -42,4 +48,6 @@ export { StateMachine, evaluateTrigger, nextTransition } from './state/state-mac
|
|
|
42
48
|
export { defineRole, defineStates, installArchetype, logicSet, registeredLogicSets, registeredRoles, resetRegistries, roleDefinition, } from './state/hooks.js';
|
|
43
49
|
// Explicit escape hatch while our own API grows: a single source of three
|
|
44
50
|
// for the whole workspace. The thesis is that three stays an implementation detail.
|
|
45
|
-
|
|
51
|
+
// It is the WebGPU build the engine draws with (ADR 0025): no classic WebGL renderer,
|
|
52
|
+
// and node materials (TSL) instead of GLSL shaders.
|
|
53
|
+
export * as THREE from 'three/webgpu';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CPU reference of a light-map texel (issue #78 CA-5..CA-7): what the
|
|
3
|
+
* light shader computes per texel, written once in plain TypeScript so it
|
|
4
|
+
* can be tested. The shader in `light-map-material.ts` follows it step by
|
|
5
|
+
* step; this file is the specification both are read against.
|
|
6
|
+
*/
|
|
7
|
+
/** A light-map value or a light color: three multipliers in 0..1 over the sRGB frame. */
|
|
8
|
+
export type Rgb = [number, number, number];
|
|
9
|
+
/** One Light as the light-map sees it: logical position and radius, its look, its shadow. */
|
|
10
|
+
export interface LightField {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
/** Logical units: the same distance in every grid direction, also on an isometric screen. */
|
|
14
|
+
radius: number;
|
|
15
|
+
color: Rgb;
|
|
16
|
+
intensity: number;
|
|
17
|
+
/** 0 = smooth falloff; N > 0 = N equal steps. */
|
|
18
|
+
bands: number;
|
|
19
|
+
/** 0 = hard shadow edge; up to 1 = the widest penumbra. */
|
|
20
|
+
softness: number;
|
|
21
|
+
castShadows: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every Tilemap's solid tiles rasterized onto one logical grid: `solid` holds
|
|
25
|
+
* one byte per cell, row-major, row 0 at `originY` (rows grow with y).
|
|
26
|
+
*/
|
|
27
|
+
export interface OccluderGrid {
|
|
28
|
+
originX: number;
|
|
29
|
+
originY: number;
|
|
30
|
+
cellSize: number;
|
|
31
|
+
columns: number;
|
|
32
|
+
rows: number;
|
|
33
|
+
solid: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
/** A logical point. */
|
|
36
|
+
export interface LogicalPoint {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
}
|
|
40
|
+
/** What every light-map texel of a frame is computed from. */
|
|
41
|
+
export interface LightScene {
|
|
42
|
+
ambient: Rgb;
|
|
43
|
+
lights: readonly LightField[];
|
|
44
|
+
grid: OccluderGrid | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Brightness at `t` = distance / radius: 1 at the centre, a smooth ease to 0
|
|
48
|
+
* at the radius and nothing beyond. With `bands` > 0 every lit point takes
|
|
49
|
+
* the top of its step, so the outermost band still reaches the radius.
|
|
50
|
+
*/
|
|
51
|
+
export declare function lightFalloff(t: number, bands: number): number;
|
|
52
|
+
/** The render-space box a light covers: a circle's square, or the 2:1 ellipse's box on an isometric screen. */
|
|
53
|
+
export declare function lightFootprint(radius: number, projection: 'isometric' | null): {
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Whether the segment from a texel to a light crosses no solid cell other
|
|
59
|
+
* than the texel's own (the lit face of a wall, CA-7). A grid walk
|
|
60
|
+
* (Amanatides–Woo) over every cell the segment enters, the light's included.
|
|
61
|
+
*/
|
|
62
|
+
export declare function segmentClear(grid: OccluderGrid, from: LogicalPoint, to: LogicalPoint): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* How far the segment from a texel to a light travels inside solid cells,
|
|
65
|
+
* in logical units, never counting the texel's own cell (its lit face): the
|
|
66
|
+
* same grid walk as `segmentClear`, measuring instead of stopping.
|
|
67
|
+
*/
|
|
68
|
+
export declare function segmentPenetration(grid: OccluderGrid, from: LogicalPoint, to: LogicalPoint): number;
|
|
69
|
+
/**
|
|
70
|
+
* How much of a light reaches a texel. Hard (softness 0): 1 or 0. Soft: one
|
|
71
|
+
* walk measures how deep the ray runs through solid cells, and the light
|
|
72
|
+
* fades over the first `softness` cells of it — a ray grazing a wall's
|
|
73
|
+
* corner keeps most of its light, one through the wall none — so the edge
|
|
74
|
+
* is a single transition that widens away from the occluder (PR #150 review).
|
|
75
|
+
*/
|
|
76
|
+
export declare function lightVisibility(grid: OccluderGrid | null, light: LightField, point: LogicalPoint): number;
|
|
77
|
+
/** A light-map texel at a logical point: the Ambient Light plus every light's term, clamped per channel. */
|
|
78
|
+
export declare function lightMapValue({ ambient, lights, grid }: LightScene, point: LogicalPoint): Rgb;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CPU reference of a light-map texel (issue #78 CA-5..CA-7): what the
|
|
3
|
+
* light shader computes per texel, written once in plain TypeScript so it
|
|
4
|
+
* can be tested. The shader in `light-map-material.ts` follows it step by
|
|
5
|
+
* step; this file is the specification both are read against.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Brightness at `t` = distance / radius: 1 at the centre, a smooth ease to 0
|
|
9
|
+
* at the radius and nothing beyond. With `bands` > 0 every lit point takes
|
|
10
|
+
* the top of its step, so the outermost band still reaches the radius.
|
|
11
|
+
*/
|
|
12
|
+
export function lightFalloff(t, bands) {
|
|
13
|
+
if (!(t < 1))
|
|
14
|
+
return 0;
|
|
15
|
+
const clamped = Math.max(0, t);
|
|
16
|
+
const smooth = 1 - clamped * clamped * (3 - 2 * clamped);
|
|
17
|
+
if (bands <= 0)
|
|
18
|
+
return smooth;
|
|
19
|
+
return Math.ceil(smooth * bands) / bands;
|
|
20
|
+
}
|
|
21
|
+
/** The render-space box a light covers: a circle's square, or the 2:1 ellipse's box on an isometric screen. */
|
|
22
|
+
export function lightFootprint(radius, projection) {
|
|
23
|
+
if (projection === 'isometric')
|
|
24
|
+
return { width: 2 * Math.SQRT2 * radius, height: Math.SQRT2 * radius };
|
|
25
|
+
return { width: 2 * radius, height: 2 * radius };
|
|
26
|
+
}
|
|
27
|
+
function solidAt(grid, column, row) {
|
|
28
|
+
if (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows)
|
|
29
|
+
return false;
|
|
30
|
+
return grid.solid[row * grid.columns + column] === 1;
|
|
31
|
+
}
|
|
32
|
+
/** The ray parameter of the first cell boundary on one axis, and how much it grows per cell. */
|
|
33
|
+
function axisCrossing(start, delta, cell) {
|
|
34
|
+
if (delta === 0)
|
|
35
|
+
return { next: Number.POSITIVE_INFINITY, step: Number.POSITIVE_INFINITY };
|
|
36
|
+
const step = Math.abs(1 / delta);
|
|
37
|
+
const toBoundary = delta > 0 ? cell + 1 - start : start - cell;
|
|
38
|
+
return { next: toBoundary * step, step };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether the segment from a texel to a light crosses no solid cell other
|
|
42
|
+
* than the texel's own (the lit face of a wall, CA-7). A grid walk
|
|
43
|
+
* (Amanatides–Woo) over every cell the segment enters, the light's included.
|
|
44
|
+
*/
|
|
45
|
+
export function segmentClear(grid, from, to) {
|
|
46
|
+
const ax = (from.x - grid.originX) / grid.cellSize;
|
|
47
|
+
const ay = (from.y - grid.originY) / grid.cellSize;
|
|
48
|
+
const bx = (to.x - grid.originX) / grid.cellSize;
|
|
49
|
+
const by = (to.y - grid.originY) / grid.cellSize;
|
|
50
|
+
const endColumn = Math.floor(bx);
|
|
51
|
+
const endRow = Math.floor(by);
|
|
52
|
+
const walk = { column: Math.floor(ax), row: Math.floor(ay), nextX: 0, nextY: 0 };
|
|
53
|
+
const x = axisCrossing(ax, bx - ax, walk.column);
|
|
54
|
+
const y = axisCrossing(ay, by - ay, walk.row);
|
|
55
|
+
walk.nextX = x.next;
|
|
56
|
+
walk.nextY = y.next;
|
|
57
|
+
const stepColumn = Math.sign(bx - ax);
|
|
58
|
+
const stepRow = Math.sign(by - ay);
|
|
59
|
+
const cells = Math.abs(endColumn - walk.column) + Math.abs(endRow - walk.row);
|
|
60
|
+
for (let index = 0; index < cells; index += 1) {
|
|
61
|
+
if (walk.nextX < walk.nextY) {
|
|
62
|
+
walk.column += stepColumn;
|
|
63
|
+
walk.nextX += x.step;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
walk.row += stepRow;
|
|
67
|
+
walk.nextY += y.step;
|
|
68
|
+
}
|
|
69
|
+
if (solidAt(grid, walk.column, walk.row))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How far the segment from a texel to a light travels inside solid cells,
|
|
76
|
+
* in logical units, never counting the texel's own cell (its lit face): the
|
|
77
|
+
* same grid walk as `segmentClear`, measuring instead of stopping.
|
|
78
|
+
*/
|
|
79
|
+
export function segmentPenetration(grid, from, to) {
|
|
80
|
+
const ax = (from.x - grid.originX) / grid.cellSize;
|
|
81
|
+
const ay = (from.y - grid.originY) / grid.cellSize;
|
|
82
|
+
const bx = (to.x - grid.originX) / grid.cellSize;
|
|
83
|
+
const by = (to.y - grid.originY) / grid.cellSize;
|
|
84
|
+
const walk = { column: Math.floor(ax), row: Math.floor(ay), nextX: 0, nextY: 0 };
|
|
85
|
+
const x = axisCrossing(ax, bx - ax, walk.column);
|
|
86
|
+
const y = axisCrossing(ay, by - ay, walk.row);
|
|
87
|
+
walk.nextX = x.next;
|
|
88
|
+
walk.nextY = y.next;
|
|
89
|
+
const cells = Math.abs(Math.floor(bx) - walk.column) + Math.abs(Math.floor(by) - walk.row);
|
|
90
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
91
|
+
let inside = 0;
|
|
92
|
+
for (let index = 0; index < cells; index += 1) {
|
|
93
|
+
let entry;
|
|
94
|
+
if (walk.nextX < walk.nextY) {
|
|
95
|
+
entry = walk.nextX;
|
|
96
|
+
walk.column += Math.sign(bx - ax);
|
|
97
|
+
walk.nextX += x.step;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
entry = walk.nextY;
|
|
101
|
+
walk.row += Math.sign(by - ay);
|
|
102
|
+
walk.nextY += y.step;
|
|
103
|
+
}
|
|
104
|
+
if (solidAt(grid, walk.column, walk.row))
|
|
105
|
+
inside += (Math.min(walk.nextX, walk.nextY, 1) - entry) * length;
|
|
106
|
+
}
|
|
107
|
+
return inside;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* How much of a light reaches a texel. Hard (softness 0): 1 or 0. Soft: one
|
|
111
|
+
* walk measures how deep the ray runs through solid cells, and the light
|
|
112
|
+
* fades over the first `softness` cells of it — a ray grazing a wall's
|
|
113
|
+
* corner keeps most of its light, one through the wall none — so the edge
|
|
114
|
+
* is a single transition that widens away from the occluder (PR #150 review).
|
|
115
|
+
*/
|
|
116
|
+
export function lightVisibility(grid, light, point) {
|
|
117
|
+
if (!grid || !light.castShadows)
|
|
118
|
+
return 1;
|
|
119
|
+
if (light.softness <= 0)
|
|
120
|
+
return segmentClear(grid, point, light) ? 1 : 0;
|
|
121
|
+
const depth = Math.min(1, segmentPenetration(grid, point, light) / (light.softness * grid.cellSize));
|
|
122
|
+
return 1 - depth * depth * (3 - 2 * depth);
|
|
123
|
+
}
|
|
124
|
+
/** One light's own term at a logical point: color × intensity × falloff × visibility, never negative. */
|
|
125
|
+
function lightTerm(light, grid, point) {
|
|
126
|
+
const strength = Math.max(0, light.intensity);
|
|
127
|
+
const distance = Math.hypot(point.x - light.x, point.y - light.y);
|
|
128
|
+
const reach = light.radius > 0 ? lightFalloff(distance / light.radius, light.bands) : 0;
|
|
129
|
+
if (strength === 0 || reach === 0)
|
|
130
|
+
return [0, 0, 0];
|
|
131
|
+
const amount = strength * reach * lightVisibility(grid, light, point);
|
|
132
|
+
return [Math.max(0, light.color[0]) * amount, Math.max(0, light.color[1]) * amount, Math.max(0, light.color[2]) * amount];
|
|
133
|
+
}
|
|
134
|
+
const clampUnit = (value) => Math.min(1, Math.max(0, value));
|
|
135
|
+
/** A light-map texel at a logical point: the Ambient Light plus every light's term, clamped per channel. */
|
|
136
|
+
export function lightMapValue({ ambient, lights, grid }, point) {
|
|
137
|
+
const total = [ambient[0], ambient[1], ambient[2]];
|
|
138
|
+
for (const light of lights) {
|
|
139
|
+
const [r, g, b] = lightTerm(light, grid, point);
|
|
140
|
+
total[0] += r;
|
|
141
|
+
total[1] += g;
|
|
142
|
+
total[2] += b;
|
|
143
|
+
}
|
|
144
|
+
return [clampUnit(total[0]), clampUnit(total[1]), clampUnit(total[2])];
|
|
145
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as THREE from 'three/webgpu';
|
|
2
|
+
/** What one light-map mesh draws: its light, in logical units, with its look. */
|
|
3
|
+
export interface LightDraw {
|
|
4
|
+
readonly position: THREE.Vector2;
|
|
5
|
+
/** color × intensity, the term the light adds at its centre. */
|
|
6
|
+
readonly color: THREE.Vector3;
|
|
7
|
+
radius: number;
|
|
8
|
+
bands: number;
|
|
9
|
+
softness: number;
|
|
10
|
+
/** 1 casts shadows, 0 does not. */
|
|
11
|
+
castShadows: number;
|
|
12
|
+
}
|
|
13
|
+
/** Registers what a light-map mesh draws; the shared material reads it per object. */
|
|
14
|
+
export declare function setLightDraw(mesh: THREE.Object3D, draw: LightDraw): void;
|
|
15
|
+
/** The light-map's per-frame inputs every light reads: the projection and the occluder grid. */
|
|
16
|
+
export interface LightMapUniforms {
|
|
17
|
+
/** 1 for an isometric scene: render space is unprojected to logical space per texel. */
|
|
18
|
+
readonly isometric: THREE.UniformNode<'float', number>;
|
|
19
|
+
/** originX, originY, cellSize and 1 when a grid exists (0 casts no shadow at all). */
|
|
20
|
+
readonly grid: THREE.UniformNode<'vec4', THREE.Vector4>;
|
|
21
|
+
/** The grid's columns and rows. */
|
|
22
|
+
readonly gridSize: THREE.UniformNode<'vec2', THREE.Vector2>;
|
|
23
|
+
/** One byte per cell, row 0 first: 255 solid, 0 open. */
|
|
24
|
+
readonly occluders: THREE.TextureNode;
|
|
25
|
+
}
|
|
26
|
+
/** The light-map's shared uniforms, with an empty occluder grid. */
|
|
27
|
+
export declare function createLightMapUniforms(empty: THREE.Texture): LightMapUniforms;
|
|
28
|
+
/**
|
|
29
|
+
* The material every light-map mesh shares (issue #78 CA-5..CA-7): per texel,
|
|
30
|
+
* `color × intensity × falloff(d / radius) × visibility` with `d` measured in
|
|
31
|
+
* logical space (ADR 0009), added onto the Ambient Light the target was
|
|
32
|
+
* cleared to. light-field.ts is its specification.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createLightMaterial(uniforms: LightMapUniforms): THREE.MeshBasicNodeMaterial;
|
|
35
|
+
/**
|
|
36
|
+
* The quad that multiplies the light-map over the frame already drawn
|
|
37
|
+
* (ADR 0026): `frame × light`, alpha kept, on the sRGB values as stored —
|
|
38
|
+
* on the canvas, or in a Post Effect's target, which stores them alike.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createMultiplyMaterial(light: THREE.Texture): THREE.MeshBasicNodeMaterial;
|