@waica/engine 0.22.0 → 0.23.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.
@@ -0,0 +1,93 @@
1
+ import * as THREE from 'three';
2
+ import type { TextureOutcome } from './assets/asset-loader.js';
3
+ export type SpriteBatchShape = 'rectangle' | 'circle';
4
+ /**
5
+ * What decides whether two sprites can share one draw: the art, its
6
+ * filtering and the quad's shape. `texture` null is the untextured entry of
7
+ * a shape; '' is an AnimatedSprite sheet with no url yet (a bare texture).
8
+ */
9
+ export interface SpriteBatchKey {
10
+ texture: string | null;
11
+ pixelArt: boolean;
12
+ shape: SpriteBatchShape;
13
+ }
14
+ /** The first capacity of a batch's instance buffers; it doubles when full. */
15
+ export declare const INITIAL_SPRITE_BATCH_CAPACITY = 16;
16
+ /** Where a batch gets its key's texture: `game.assets` (ADR 0019). */
17
+ export interface SpriteBatchTextures {
18
+ texture(url: string): {
19
+ texture: THREE.Texture;
20
+ settled: Promise<TextureOutcome>;
21
+ };
22
+ }
23
+ /** The key's canonical string: equal keys share one batch. */
24
+ export declare function spriteBatchKeyId(key: SpriteBatchKey): string;
25
+ /** The unit quad every sprite of a shape draws, exactly as the per-sprite path builds it. */
26
+ export declare function spriteGeometry(shape: SpriteBatchShape): THREE.BufferGeometry;
27
+ /** What a run writes per sprite. */
28
+ export interface SpriteBatchInstance {
29
+ /** View × world of the sprite's quad. */
30
+ readonly modelView: THREE.Matrix4;
31
+ /** Linear RGB tint. */
32
+ readonly color: THREE.Color;
33
+ /** The map's repeat and offset (identity for an untransformed map). */
34
+ readonly uvRepeat: THREE.Vector2;
35
+ readonly uvOffset: THREE.Vector2;
36
+ }
37
+ /**
38
+ * One pooled draw of a run: an instanced quad and its three typed instance
39
+ * attributes (view × world, tint, map UV transform), sized by the longest
40
+ * run it has drawn — 16 instances at first, doubling when a longer run
41
+ * needs it, never shrinking during the scene.
42
+ */
43
+ export declare class SpriteRunMesh {
44
+ readonly mesh: THREE.InstancedMesh;
45
+ private readonly colors;
46
+ private readonly uvs;
47
+ constructor(base: THREE.BufferGeometry, material: THREE.MeshBasicMaterial, capacity: number);
48
+ get capacity(): number;
49
+ /** Writes the sprite drawn `index`-th in this run. */
50
+ write(index: number, instance: SpriteBatchInstance): void;
51
+ /** Draws the first `count` written instances and uploads only those. */
52
+ commit(count: number): void;
53
+ dispose(): void;
54
+ }
55
+ /**
56
+ * A Sprite Batch's resources for one key (CONTEXT.md, ADR 0024): the shared
57
+ * material, a slot per member, and the pooled run meshes its runs draw
58
+ * through — one per run of the key in a frame, each sized by its own run, so
59
+ * instance memory stays proportional to the sprites drawn. Released at
60
+ * unload (ADR 0011).
61
+ */
62
+ export declare class SpriteBatch {
63
+ private readonly geometry;
64
+ readonly material: THREE.MeshBasicMaterial;
65
+ /** When the key's texture settled; null for untextured and bare entries. */
66
+ readonly settled: Promise<TextureOutcome> | null;
67
+ private readonly slots;
68
+ private readonly freeSlots;
69
+ private _capacity;
70
+ private readonly runs;
71
+ private runsDrawn;
72
+ constructor(key: SpriteBatchKey, textures: SpriteBatchTextures, geometry: THREE.BufferGeometry);
73
+ /** The most members the key has held at once, rounded up to 16 × 2^n: no run is ever longer. */
74
+ get capacity(): number;
75
+ /** Live members. */
76
+ get size(): number;
77
+ /** Takes a freed slot first, growing the capacity geometrically only when none is left. */
78
+ join(): number;
79
+ /** Frees a member's slot for the next join, the same step. */
80
+ leave(slot: number): void;
81
+ /** Starts a frame: no run of the key drawn yet. */
82
+ beginFrame(): void;
83
+ /**
84
+ * The pooled mesh for the key's next run this frame, holding at least
85
+ * `length` instances. A pooled mesh is rebuilt only when this run is longer
86
+ * than any it drew before, doubling from its size (16 at first).
87
+ */
88
+ nextRun(length: number): SpriteRunMesh;
89
+ /** Takes the pooled meshes no run used this frame out of the scene. */
90
+ detachUndrawn(): void;
91
+ /** Releases every GPU-side object this batch created; its texture clone included. */
92
+ dispose(): void;
93
+ }
@@ -0,0 +1,211 @@
1
+ import * as THREE from 'three';
2
+ /** The first capacity of a batch's instance buffers; it doubles when full. */
3
+ export const INITIAL_SPRITE_BATCH_CAPACITY = 16;
4
+ /** The key's canonical string: equal keys share one batch. */
5
+ export function spriteBatchKeyId(key) {
6
+ const texture = key.texture === null ? 'none' : `url:${key.texture}`;
7
+ return `${texture}|${key.texture === null ? false : key.pixelArt}|${key.shape}`;
8
+ }
9
+ /** The unit quad every sprite of a shape draws, exactly as the per-sprite path builds it. */
10
+ export function spriteGeometry(shape) {
11
+ return shape === 'circle' ? new THREE.CircleGeometry(0.5, 32) : new THREE.PlaneGeometry(1, 1);
12
+ }
13
+ /** The per-instance UV transform a run carries: repeat (x, y) and offset (z, w). */
14
+ const INSTANCE_UV = 'instanceUv';
15
+ /**
16
+ * Shader hook shared by every batch material. The instance matrix already
17
+ * holds view × world (three's own `modelViewMatrix` for that sprite, computed
18
+ * the same way on the CPU), so the vertex shader skips `modelViewMatrix`; the
19
+ * map's UV transform comes from the instance instead of `mapTransform`. Every
20
+ * rewrite sits behind USE_INSTANCING: the hidden anchors share this material,
21
+ * and if one were ever drawn as a plain mesh it compiles three's own chunks.
22
+ */
23
+ function useInstanceTransforms(shader) {
24
+ shader.vertexShader = shader.vertexShader
25
+ .replace('#include <common>', ['#include <common>', '#ifdef USE_INSTANCING', ` attribute vec4 ${INSTANCE_UV};`, '#endif'].join('\n'))
26
+ .replace('#include <uv_vertex>', [
27
+ '#ifdef USE_INSTANCING',
28
+ ' #ifdef USE_MAP',
29
+ ` vMapUv = ( mat3( ${INSTANCE_UV}.x, 0.0, 0.0, 0.0, ${INSTANCE_UV}.y, 0.0, ${INSTANCE_UV}.z, ${INSTANCE_UV}.w, 1.0 ) * vec3( MAP_UV, 1 ) ).xy;`,
30
+ ' #endif',
31
+ '#else',
32
+ ' #include <uv_vertex>',
33
+ '#endif',
34
+ ].join('\n'))
35
+ .replace('#include <project_vertex>', [
36
+ '#ifdef USE_INSTANCING',
37
+ ' vec4 mvPosition = instanceMatrix * vec4( transformed, 1.0 );',
38
+ ' gl_Position = projectionMatrix * mvPosition;',
39
+ '#else',
40
+ ' #include <project_vertex>',
41
+ '#endif',
42
+ ].join('\n'));
43
+ }
44
+ function createMaterial(key, textures) {
45
+ // Double-sided: a flipped sprite mirrors through a negative x scale in its
46
+ // matrix, exactly like its own mesh; per instance there is no winding flip.
47
+ const material = new THREE.MeshBasicMaterial({ transparent: true, side: THREE.DoubleSide });
48
+ material.forceSinglePass = true;
49
+ material.onBeforeCompile = useInstanceTransforms;
50
+ if (key.texture === null)
51
+ return { material, settled: null };
52
+ const { texture, settled } = key.texture
53
+ ? textures.texture(key.texture)
54
+ : { texture: new THREE.Texture(), settled: null };
55
+ // One clone per key holds the filters, so pixelArt never leaks across keys.
56
+ if (key.pixelArt) {
57
+ texture.magFilter = THREE.NearestFilter;
58
+ texture.minFilter = THREE.NearestFilter;
59
+ }
60
+ material.map = texture;
61
+ return { material, settled };
62
+ }
63
+ /** Marks the first `count` items of an instance attribute for upload. */
64
+ function upload(attribute, count) {
65
+ attribute.clearUpdateRanges();
66
+ attribute.addUpdateRange(0, count * attribute.itemSize);
67
+ attribute.needsUpdate = true;
68
+ }
69
+ /**
70
+ * One pooled draw of a run: an instanced quad and its three typed instance
71
+ * attributes (view × world, tint, map UV transform), sized by the longest
72
+ * run it has drawn — 16 instances at first, doubling when a longer run
73
+ * needs it, never shrinking during the scene.
74
+ */
75
+ export class SpriteRunMesh {
76
+ mesh;
77
+ colors;
78
+ uvs;
79
+ constructor(base, material, capacity) {
80
+ // Its own copy of the quad's attributes, so disposing one run never frees
81
+ // buffers another geometry still binds.
82
+ const geometry = base.clone();
83
+ this.uvs = new THREE.InstancedBufferAttribute(new Float32Array(capacity * 4), 4).setUsage(THREE.DynamicDrawUsage);
84
+ geometry.setAttribute(INSTANCE_UV, this.uvs);
85
+ this.mesh = new THREE.InstancedMesh(geometry, material, capacity);
86
+ this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
87
+ this.colors = new THREE.InstancedBufferAttribute(new Float32Array(capacity * 3), 3).setUsage(THREE.DynamicDrawUsage);
88
+ this.mesh.instanceColor = this.colors;
89
+ // Drawn in an explicit renderOrder; its instances are already in view space.
90
+ this.mesh.frustumCulled = false;
91
+ this.mesh.matrixAutoUpdate = false;
92
+ this.mesh.boundingSphere = new THREE.Sphere();
93
+ }
94
+ get capacity() {
95
+ return this.mesh.instanceMatrix.count;
96
+ }
97
+ /** Writes the sprite drawn `index`-th in this run. */
98
+ write(index, instance) {
99
+ instance.modelView.toArray(this.mesh.instanceMatrix.array, index * 16);
100
+ instance.color.toArray(this.colors.array, index * 3);
101
+ const uv = this.uvs.array;
102
+ uv[index * 4] = instance.uvRepeat.x;
103
+ uv[index * 4 + 1] = instance.uvRepeat.y;
104
+ uv[index * 4 + 2] = instance.uvOffset.x;
105
+ uv[index * 4 + 3] = instance.uvOffset.y;
106
+ }
107
+ /** Draws the first `count` written instances and uploads only those. */
108
+ commit(count) {
109
+ this.mesh.count = count;
110
+ for (const attribute of [this.mesh.instanceMatrix, this.colors, this.uvs])
111
+ upload(attribute, count);
112
+ }
113
+ dispose() {
114
+ this.mesh.removeFromParent();
115
+ this.mesh.geometry.dispose();
116
+ this.mesh.dispose();
117
+ }
118
+ }
119
+ /**
120
+ * A Sprite Batch's resources for one key (CONTEXT.md, ADR 0024): the shared
121
+ * material, a slot per member, and the pooled run meshes its runs draw
122
+ * through — one per run of the key in a frame, each sized by its own run, so
123
+ * instance memory stays proportional to the sprites drawn. Released at
124
+ * unload (ADR 0011).
125
+ */
126
+ export class SpriteBatch {
127
+ geometry;
128
+ material;
129
+ /** When the key's texture settled; null for untextured and bare entries. */
130
+ settled;
131
+ // A slot reserves room in the key's capacity; it is not a position in any
132
+ // instance buffer, which a frame rewrites in draw order (CA-4 deviation).
133
+ slots = [];
134
+ freeSlots = [];
135
+ _capacity = INITIAL_SPRITE_BATCH_CAPACITY;
136
+ runs = [];
137
+ runsDrawn = 0;
138
+ constructor(key, textures, geometry) {
139
+ this.geometry = geometry;
140
+ const { material, settled } = createMaterial(key, textures);
141
+ this.material = material;
142
+ this.settled = settled;
143
+ }
144
+ /** The most members the key has held at once, rounded up to 16 × 2^n: no run is ever longer. */
145
+ get capacity() {
146
+ return this._capacity;
147
+ }
148
+ /** Live members. */
149
+ get size() {
150
+ return this.slots.length - this.freeSlots.length;
151
+ }
152
+ /** Takes a freed slot first, growing the capacity geometrically only when none is left. */
153
+ join() {
154
+ const reused = this.freeSlots.pop();
155
+ if (reused !== undefined) {
156
+ this.slots[reused] = true;
157
+ return reused;
158
+ }
159
+ this.slots.push(true);
160
+ while (this.slots.length > this._capacity)
161
+ this._capacity *= 2;
162
+ return this.slots.length - 1;
163
+ }
164
+ /** Frees a member's slot for the next join, the same step. */
165
+ leave(slot) {
166
+ if (this.slots[slot] !== true)
167
+ return;
168
+ this.slots[slot] = false;
169
+ this.freeSlots.push(slot);
170
+ }
171
+ /** Starts a frame: no run of the key drawn yet. */
172
+ beginFrame() {
173
+ this.runsDrawn = 0;
174
+ }
175
+ /**
176
+ * The pooled mesh for the key's next run this frame, holding at least
177
+ * `length` instances. A pooled mesh is rebuilt only when this run is longer
178
+ * than any it drew before, doubling from its size (16 at first).
179
+ */
180
+ nextRun(length) {
181
+ const index = this.runsDrawn;
182
+ this.runsDrawn += 1;
183
+ const pooled = this.runs[index];
184
+ if (pooled && pooled.capacity >= length)
185
+ return pooled;
186
+ let capacity = pooled?.capacity ?? INITIAL_SPRITE_BATCH_CAPACITY;
187
+ while (capacity < length)
188
+ capacity *= 2;
189
+ // Never past the key's reservation: a run cannot outnumber the members.
190
+ const run = new SpriteRunMesh(this.geometry, this.material, Math.min(capacity, Math.max(this._capacity, length)));
191
+ if (pooled) {
192
+ pooled.mesh.parent?.add(run.mesh);
193
+ pooled.dispose();
194
+ }
195
+ this.runs[index] = run;
196
+ return run;
197
+ }
198
+ /** Takes the pooled meshes no run used this frame out of the scene. */
199
+ detachUndrawn() {
200
+ for (let index = this.runsDrawn; index < this.runs.length; index += 1)
201
+ this.runs[index]?.mesh.removeFromParent();
202
+ }
203
+ /** Releases every GPU-side object this batch created; its texture clone included. */
204
+ dispose() {
205
+ for (const run of this.runs)
206
+ run.dispose();
207
+ this.runs.length = 0;
208
+ this.material.map?.dispose();
209
+ this.material.dispose();
210
+ }
211
+ }
@@ -0,0 +1,107 @@
1
+ import * as THREE from 'three';
2
+ import type { TextureOutcome } from './assets/asset-loader.js';
3
+ import type { Game } from './game.js';
4
+ import { SpriteBatch, type SpriteBatchInstance, type SpriteBatchKey, type SpriteBatchTextures } from './sprite-batch.js';
5
+ /** Where an instance reads its map's UV transform: an AnimatedSprite's sheet clone. */
6
+ export interface SpriteUvSource {
7
+ readonly offset: THREE.Vector2;
8
+ readonly repeat: THREE.Vector2;
9
+ }
10
+ export type SpriteAnchor = THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>;
11
+ /** Where an instance sits: its key, that key's batch and its slot there. */
12
+ export interface SpriteBatchSeat {
13
+ key: SpriteBatchKey;
14
+ batch: SpriteBatch;
15
+ slot: number;
16
+ }
17
+ /**
18
+ * One sprite inside a Sprite Batch. Its `anchor` is a hidden mesh placed
19
+ * exactly where the sprite's own mesh would be — same parent, transform,
20
+ * shape geometry and creation order — so draw order, y-sort and the world
21
+ * matrix come out as before; the anchor itself is never drawn.
22
+ */
23
+ export declare class SpriteInstance implements SpriteBatchInstance {
24
+ readonly anchor: SpriteAnchor;
25
+ private readonly owner;
26
+ seat: SpriteBatchSeat;
27
+ /** Linear RGB tint, what the per-sprite path writes into its material color. */
28
+ readonly color: THREE.Color;
29
+ /** Identity until an AnimatedSprite points it at its current sheet's clone. */
30
+ uvSource: SpriteUvSource;
31
+ readonly modelView: THREE.Matrix4;
32
+ /**
33
+ * For an AnimatedSprite: the sheet its key was last chosen for and whether
34
+ * that sheet had failed, so a frame on the same sheet skips the move.
35
+ */
36
+ shownSheet: number;
37
+ shownSheetFailed: boolean;
38
+ constructor(anchor: SpriteAnchor, owner: SpriteBatches, seat: SpriteBatchSeat);
39
+ get key(): SpriteBatchKey;
40
+ get uvRepeat(): THREE.Vector2;
41
+ get uvOffset(): THREE.Vector2;
42
+ /** Moves to the batch of `key` (a shape change, a failed texture, another sheet); a no-op for the same key. */
43
+ moveTo(key: SpriteBatchKey): void;
44
+ /** Frees the slot and removes the anchor: the sprite was destroyed. */
45
+ release(): void;
46
+ }
47
+ /** The batch instance whose hidden anchor this is; undefined for an ordinary mesh. */
48
+ export declare function spriteInstanceOf(object: THREE.Object3D | undefined): SpriteInstance | undefined;
49
+ /**
50
+ * A Game's Sprite Batches (ADR 0024): one SpriteBatch per key, the hidden
51
+ * anchors of every batched sprite, and the per-frame pass that orders the
52
+ * frame like three would, groups runs and draws each through one instanced
53
+ * mesh. Scene-scoped: unload() releases everything (ADR 0011).
54
+ */
55
+ export declare class SpriteBatches {
56
+ private readonly textures;
57
+ /** Whether sprites readied from now on batch: the scene's `render.batch !== false`. */
58
+ enabled: boolean;
59
+ private readonly batches;
60
+ private readonly geometries;
61
+ private readonly live;
62
+ private readonly root;
63
+ /** Under root, one Group per non-zero group order, so a run keeps its sprites' Group renderOrder. */
64
+ private readonly groups;
65
+ private readonly collector;
66
+ private camera;
67
+ private nextOrder;
68
+ private readonly pinned;
69
+ private readonly pinnedOrders;
70
+ private pinnedScene;
71
+ private sceneAutoUpdate;
72
+ private readonly visitor;
73
+ constructor(textures: SpriteBatchTextures);
74
+ /** A new batched sprite in the key's batch; the caller parents its hidden anchor. */
75
+ attach(key: SpriteBatchKey): SpriteInstance;
76
+ /** When the key's texture settled; null for an untextured or bare entry. */
77
+ settled(key: SpriteBatchKey): Promise<TextureOutcome> | null;
78
+ move(instance: SpriteInstance, key: SpriteBatchKey): void;
79
+ detach(instance: SpriteInstance): void;
80
+ /**
81
+ * The frame pass, right before `renderer.render`: orders every transparent
82
+ * renderable as three would, draws each run of same-key sprites through one
83
+ * instanced mesh, and pins that order with renderOrder. Returns what undoes
84
+ * the temporary renderOrder of the non-sprite renderables after the render.
85
+ */
86
+ prepareFrame(scene: THREE.Scene, camera: THREE.Camera): () => void;
87
+ /** Runs `render` with this frame's runs drawn and the draw order pinned, then undoes the pin. */
88
+ drawFrame(scene: THREE.Scene, camera: THREE.Camera, render: () => void): void;
89
+ /** Releases every batch, geometry and run mesh; batching is on again for the next scene. */
90
+ unload(): void;
91
+ private batchFor;
92
+ private geometryFor;
93
+ /** Draws the frame's run `start`..`end` of `batch` through its next pooled run mesh, in its Group order. */
94
+ private drawRun;
95
+ /** Pins a non-sprite renderable's renderOrder to its place in the frame, until unpin. */
96
+ private pin;
97
+ /** After the render: every pinned renderOrder back, and the scene's own matrix updates. */
98
+ private readonly unpin;
99
+ /** The Group a run of `groupOrder` hangs from: three reads its renderOrder as the run's group order. */
100
+ private groupFor;
101
+ /** The instance with its view × world computed exactly as three computes `modelViewMatrix`. */
102
+ private placed;
103
+ }
104
+ /** Internal: the Game creates its batches here at construction, findable by its sprites. */
105
+ export declare function createSpriteBatches(game: Game, textures: SpriteBatchTextures): SpriteBatches;
106
+ /** A Game's Sprite Batches; undefined for anything that is not a constructed Game (a test stub). */
107
+ export declare function spriteBatchesOf(game: Game): SpriteBatches | undefined;
@@ -0,0 +1,260 @@
1
+ import * as THREE from 'three';
2
+ import { SpriteBatch, spriteBatchKeyId, spriteGeometry, } from './sprite-batch.js';
3
+ import { TransparentDrawCollector } from './sprite-draw-collector.js';
4
+ import { compareTransparentDraws, forEachSpriteRun } from './sprite-runs.js';
5
+ /**
6
+ * One sprite inside a Sprite Batch. Its `anchor` is a hidden mesh placed
7
+ * exactly where the sprite's own mesh would be — same parent, transform,
8
+ * shape geometry and creation order — so draw order, y-sort and the world
9
+ * matrix come out as before; the anchor itself is never drawn.
10
+ */
11
+ export class SpriteInstance {
12
+ anchor;
13
+ owner;
14
+ seat;
15
+ /** Linear RGB tint, what the per-sprite path writes into its material color. */
16
+ color = new THREE.Color(1, 1, 1);
17
+ /** Identity until an AnimatedSprite points it at its current sheet's clone. */
18
+ uvSource = { offset: new THREE.Vector2(0, 0), repeat: new THREE.Vector2(1, 1) };
19
+ modelView = new THREE.Matrix4();
20
+ /**
21
+ * For an AnimatedSprite: the sheet its key was last chosen for and whether
22
+ * that sheet had failed, so a frame on the same sheet skips the move.
23
+ */
24
+ shownSheet = -1;
25
+ shownSheetFailed = false;
26
+ constructor(anchor, owner, seat) {
27
+ this.anchor = anchor;
28
+ this.owner = owner;
29
+ this.seat = seat;
30
+ }
31
+ get key() {
32
+ return this.seat.key;
33
+ }
34
+ get uvRepeat() {
35
+ return this.uvSource.repeat;
36
+ }
37
+ get uvOffset() {
38
+ return this.uvSource.offset;
39
+ }
40
+ /** Moves to the batch of `key` (a shape change, a failed texture, another sheet); a no-op for the same key. */
41
+ moveTo(key) {
42
+ this.owner.move(this, key);
43
+ }
44
+ /** Frees the slot and removes the anchor: the sprite was destroyed. */
45
+ release() {
46
+ this.owner.detach(this);
47
+ }
48
+ }
49
+ const anchors = new WeakMap();
50
+ /** The batch instance whose hidden anchor this is; undefined for an ordinary mesh. */
51
+ export function spriteInstanceOf(object) {
52
+ return object ? anchors.get(object) : undefined;
53
+ }
54
+ /**
55
+ * A Game's Sprite Batches (ADR 0024): one SpriteBatch per key, the hidden
56
+ * anchors of every batched sprite, and the per-frame pass that orders the
57
+ * frame like three would, groups runs and draws each through one instanced
58
+ * mesh. Scene-scoped: unload() releases everything (ADR 0011).
59
+ */
60
+ export class SpriteBatches {
61
+ textures;
62
+ /** Whether sprites readied from now on batch: the scene's `render.batch !== false`. */
63
+ enabled = true;
64
+ batches = new Map();
65
+ geometries = new Map();
66
+ live = new Set();
67
+ root = new THREE.Group();
68
+ /** Under root, one Group per non-zero group order, so a run keeps its sprites' Group renderOrder. */
69
+ groups = new Map();
70
+ collector;
71
+ // Per-frame state, reused every frame: the camera, the next renderOrder to
72
+ // pin, and the non-sprite renderables whose renderOrder is pinned.
73
+ camera = new THREE.Camera();
74
+ nextOrder = 0;
75
+ pinned = [];
76
+ pinnedOrders = [];
77
+ pinnedScene = null;
78
+ sceneAutoUpdate = true;
79
+ visitor = {
80
+ run: (batch, start, end) => this.drawRun(batch, start, end),
81
+ other: (item) => this.pin(item.object),
82
+ };
83
+ constructor(textures) {
84
+ this.textures = textures;
85
+ this.root.name = 'waica:sprite-batches';
86
+ this.collector = new TransparentDrawCollector({
87
+ instanceOf: (object) => {
88
+ const instance = anchors.get(object);
89
+ return instance && this.live.has(instance) ? instance : undefined;
90
+ },
91
+ keyOf: (instance) => instance.seat.batch,
92
+ }, this.root);
93
+ }
94
+ /** A new batched sprite in the key's batch; the caller parents its hidden anchor. */
95
+ attach(key) {
96
+ const batch = this.batchFor(key);
97
+ const anchor = new THREE.Mesh(this.geometryFor(key.shape), batch.material);
98
+ // Never drawn and never read for visibility: a batched sprite shows or
99
+ // hides through its entity's node (README "Sprite Batches").
100
+ anchor.visible = false;
101
+ const instance = new SpriteInstance(anchor, this, { key, batch, slot: batch.join() });
102
+ anchors.set(anchor, instance);
103
+ this.live.add(instance);
104
+ return instance;
105
+ }
106
+ /** When the key's texture settled; null for an untextured or bare entry. */
107
+ settled(key) {
108
+ return this.batchFor(key).settled;
109
+ }
110
+ move(instance, key) {
111
+ if (spriteBatchKeyId(key) === spriteBatchKeyId(instance.key))
112
+ return;
113
+ instance.seat.batch.leave(instance.seat.slot);
114
+ const batch = this.batchFor(key);
115
+ instance.seat = { key, batch, slot: batch.join() };
116
+ instance.anchor.material = batch.material;
117
+ instance.anchor.geometry = this.geometryFor(key.shape);
118
+ }
119
+ detach(instance) {
120
+ if (!this.live.delete(instance))
121
+ return;
122
+ instance.seat.batch.leave(instance.seat.slot);
123
+ instance.anchor.removeFromParent();
124
+ anchors.delete(instance.anchor);
125
+ }
126
+ /**
127
+ * The frame pass, right before `renderer.render`: orders every transparent
128
+ * renderable as three would, draws each run of same-key sprites through one
129
+ * instanced mesh, and pins that order with renderOrder. Returns what undoes
130
+ * the temporary renderOrder of the non-sprite renderables after the render.
131
+ */
132
+ prepareFrame(scene, camera) {
133
+ if (this.live.size === 0) {
134
+ this.root.removeFromParent();
135
+ return this.unpin;
136
+ }
137
+ scene.updateMatrixWorld();
138
+ camera.updateMatrixWorld();
139
+ this.collector.collect(scene, camera);
140
+ const drawables = this.collector.drawables.sort(compareTransparentDraws);
141
+ this.camera = camera;
142
+ this.nextOrder = 0;
143
+ for (const batch of this.batches.values())
144
+ batch.beginFrame();
145
+ forEachSpriteRun(drawables, this.visitor);
146
+ for (const batch of this.batches.values())
147
+ batch.detachUndrawn();
148
+ if (this.root.parent !== scene)
149
+ scene.add(this.root);
150
+ // Matrices are current: three's own update would recompute the same.
151
+ this.pinnedScene = scene;
152
+ this.sceneAutoUpdate = scene.matrixWorldAutoUpdate;
153
+ scene.matrixWorldAutoUpdate = false;
154
+ return this.unpin;
155
+ }
156
+ /** Runs `render` with this frame's runs drawn and the draw order pinned, then undoes the pin. */
157
+ drawFrame(scene, camera, render) {
158
+ const restore = this.prepareFrame(scene, camera);
159
+ try {
160
+ render();
161
+ }
162
+ finally {
163
+ restore();
164
+ }
165
+ }
166
+ /** Releases every batch, geometry and run mesh; batching is on again for the next scene. */
167
+ unload() {
168
+ for (const instance of [...this.live])
169
+ instance.release();
170
+ for (const batch of this.batches.values())
171
+ batch.dispose();
172
+ this.batches.clear();
173
+ for (const geometry of this.geometries.values())
174
+ geometry.dispose();
175
+ this.geometries.clear();
176
+ for (const group of this.groups.values())
177
+ group.removeFromParent();
178
+ this.groups.clear();
179
+ this.root.removeFromParent();
180
+ this.enabled = true;
181
+ }
182
+ batchFor(key) {
183
+ const id = spriteBatchKeyId(key);
184
+ const existing = this.batches.get(id);
185
+ if (existing)
186
+ return existing;
187
+ const created = new SpriteBatch(key, this.textures, this.geometryFor(key.shape));
188
+ this.batches.set(id, created);
189
+ return created;
190
+ }
191
+ geometryFor(shape) {
192
+ const existing = this.geometries.get(shape);
193
+ if (existing)
194
+ return existing;
195
+ const created = spriteGeometry(shape);
196
+ this.geometries.set(shape, created);
197
+ return created;
198
+ }
199
+ /** Draws the frame's run `start`..`end` of `batch` through its next pooled run mesh, in its Group order. */
200
+ drawRun(batch, start, end) {
201
+ const drawables = this.collector.drawables;
202
+ const run = batch.nextRun(end - start);
203
+ for (let index = start; index < end; index += 1)
204
+ run.write(index - start, this.placed(drawables[index]?.instance));
205
+ run.commit(end - start);
206
+ run.mesh.renderOrder = this.nextOrder;
207
+ this.nextOrder += 1;
208
+ const group = this.groupFor(drawables[start]?.groupOrder ?? 0);
209
+ if (run.mesh.parent !== group)
210
+ group.add(run.mesh);
211
+ }
212
+ /** Pins a non-sprite renderable's renderOrder to its place in the frame, until unpin. */
213
+ pin(object) {
214
+ this.pinned.push(object);
215
+ this.pinnedOrders.push(object.renderOrder);
216
+ object.renderOrder = this.nextOrder;
217
+ this.nextOrder += 1;
218
+ }
219
+ /** After the render: every pinned renderOrder back, and the scene's own matrix updates. */
220
+ unpin = () => {
221
+ for (const [index, object] of this.pinned.entries())
222
+ object.renderOrder = this.pinnedOrders[index] ?? object.renderOrder;
223
+ this.pinned.length = 0;
224
+ this.pinnedOrders.length = 0;
225
+ if (this.pinnedScene)
226
+ this.pinnedScene.matrixWorldAutoUpdate = this.sceneAutoUpdate;
227
+ this.pinnedScene = null;
228
+ };
229
+ /** The Group a run of `groupOrder` hangs from: three reads its renderOrder as the run's group order. */
230
+ groupFor(groupOrder) {
231
+ if (groupOrder === 0)
232
+ return this.root;
233
+ const existing = this.groups.get(groupOrder);
234
+ if (existing)
235
+ return existing;
236
+ const group = new THREE.Group();
237
+ group.renderOrder = groupOrder;
238
+ this.root.add(group);
239
+ this.groups.set(groupOrder, group);
240
+ return group;
241
+ }
242
+ /** The instance with its view × world computed exactly as three computes `modelViewMatrix`. */
243
+ placed(instance) {
244
+ if (!instance)
245
+ throw new Error('a Sprite Batch run holds only sprites');
246
+ instance.modelView.multiplyMatrices(this.camera.matrixWorldInverse, instance.anchor.matrixWorld);
247
+ return instance;
248
+ }
249
+ }
250
+ const registry = new WeakMap();
251
+ /** Internal: the Game creates its batches here at construction, findable by its sprites. */
252
+ export function createSpriteBatches(game, textures) {
253
+ const batches = new SpriteBatches(textures);
254
+ registry.set(game, batches);
255
+ return batches;
256
+ }
257
+ /** A Game's Sprite Batches; undefined for anything that is not a constructed Game (a test stub). */
258
+ export function spriteBatchesOf(game) {
259
+ return registry.get(game);
260
+ }