@waica/engine 0.21.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.
- package/README.md +14 -0
- package/dist/collision-category.js +5 -2
- package/dist/collision-dispatch.d.ts +7 -1
- package/dist/collision-dispatch.js +18 -11
- package/dist/components/animated-sprite.d.ts +2 -0
- package/dist/components/animated-sprite.js +41 -6
- package/dist/components/sprite.d.ts +6 -0
- package/dist/components/sprite.js +52 -4
- package/dist/game.d.ts +7 -3
- package/dist/game.js +15 -5
- package/dist/gamepad.d.ts +72 -0
- package/dist/gamepad.js +137 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/input.d.ts +61 -11
- package/dist/input.js +130 -24
- package/dist/runtime-bridge.d.ts +15 -3
- package/dist/runtime-bridge.js +14 -2
- package/dist/scene.d.ts +10 -0
- package/dist/spatial-broadphase.d.ts +7 -1
- package/dist/spatial-broadphase.js +16 -45
- package/dist/spatial-pairs.d.ts +17 -0
- package/dist/spatial-pairs.js +79 -0
- package/dist/sprite-batch.d.ts +93 -0
- package/dist/sprite-batch.js +211 -0
- package/dist/sprite-batches.d.ts +107 -0
- package/dist/sprite-batches.js +260 -0
- package/dist/sprite-draw-collector.d.ts +42 -0
- package/dist/sprite-draw-collector.js +107 -0
- package/dist/sprite-runs.d.ts +40 -0
- package/dist/sprite-runs.js +49 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { Drawable } from './sprite-runs.js';
|
|
3
|
+
/** A frame drawable: what three sorts it by, the object, and the sprite instance it stands for (null otherwise). */
|
|
4
|
+
export interface FrameDrawable<K, I> extends Drawable<K> {
|
|
5
|
+
object: THREE.Object3D;
|
|
6
|
+
instance: I | null;
|
|
7
|
+
}
|
|
8
|
+
/** How the collector recognises a batched sprite's hidden anchor and its batch key. */
|
|
9
|
+
export interface CollectedSprites<K, I> {
|
|
10
|
+
/** The live sprite instance this object anchors, or undefined for any other object. */
|
|
11
|
+
instanceOf(object: THREE.Object3D): I | undefined;
|
|
12
|
+
keyOf(instance: I): K;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One frame's transparent drawables, collected the way WebGLRenderer's
|
|
16
|
+
* projectObject walks the scene: visibility, camera layers, Group render
|
|
17
|
+
* order, frustum culling, and the clip-space z it sorts by. Sprite anchors
|
|
18
|
+
* are hidden yet collected when in view; `skip` (the batches' own run
|
|
19
|
+
* meshes) is never walked. The drawables are pooled across frames.
|
|
20
|
+
*/
|
|
21
|
+
export declare class TransparentDrawCollector<K, I> {
|
|
22
|
+
private readonly sprites;
|
|
23
|
+
private readonly skip;
|
|
24
|
+
/** This frame's drawables, in walk order; the same array every frame. */
|
|
25
|
+
readonly drawables: FrameDrawable<K, I>[];
|
|
26
|
+
private readonly pool;
|
|
27
|
+
private readonly projScreen;
|
|
28
|
+
private readonly frustum;
|
|
29
|
+
private readonly point;
|
|
30
|
+
private layers;
|
|
31
|
+
constructor(sprites: CollectedSprites<K, I>, skip: THREE.Object3D);
|
|
32
|
+
/** Walks `scene` for `camera`, whose matrices must be current. */
|
|
33
|
+
collect(scene: THREE.Object3D, camera: THREE.Camera): void;
|
|
34
|
+
private visit;
|
|
35
|
+
/** Files the object itself when three would draw it transparent; returns the group order for its children. */
|
|
36
|
+
private visitOwn;
|
|
37
|
+
/** three's per-object culling: `!frustumCulled || intersectsFrustum`. */
|
|
38
|
+
private inView;
|
|
39
|
+
private push;
|
|
40
|
+
/** The clip-space z three sorts the object by. */
|
|
41
|
+
private sortDepth;
|
|
42
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
function isRenderable(object) {
|
|
3
|
+
const flags = object;
|
|
4
|
+
return flags.isMesh === true || flags.isLine === true || flags.isPoints === true || flags.isSprite === true;
|
|
5
|
+
}
|
|
6
|
+
/** Whether three files any of the object's materials in its transparent list. */
|
|
7
|
+
function drawsTransparent(object) {
|
|
8
|
+
const list = Array.isArray(object.material) ? object.material : [object.material];
|
|
9
|
+
return list.some((material) => {
|
|
10
|
+
const transmission = material.transmission ?? 0;
|
|
11
|
+
return material.visible && material.transparent && transmission <= 0;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** The bounding-sphere center three sorts a mesh, line or points by: the object's own sphere if it has one. */
|
|
15
|
+
function sortCenter(object) {
|
|
16
|
+
const own = object;
|
|
17
|
+
if (own.boundingSphere === undefined) {
|
|
18
|
+
const geometry = object.geometry;
|
|
19
|
+
if (geometry.boundingSphere === null)
|
|
20
|
+
geometry.computeBoundingSphere();
|
|
21
|
+
return geometry.boundingSphere?.center ?? new THREE.Vector3();
|
|
22
|
+
}
|
|
23
|
+
if (own.boundingSphere === null)
|
|
24
|
+
own.computeBoundingSphere?.();
|
|
25
|
+
return own.boundingSphere?.center ?? new THREE.Vector3();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One frame's transparent drawables, collected the way WebGLRenderer's
|
|
29
|
+
* projectObject walks the scene: visibility, camera layers, Group render
|
|
30
|
+
* order, frustum culling, and the clip-space z it sorts by. Sprite anchors
|
|
31
|
+
* are hidden yet collected when in view; `skip` (the batches' own run
|
|
32
|
+
* meshes) is never walked. The drawables are pooled across frames.
|
|
33
|
+
*/
|
|
34
|
+
export class TransparentDrawCollector {
|
|
35
|
+
sprites;
|
|
36
|
+
skip;
|
|
37
|
+
/** This frame's drawables, in walk order; the same array every frame. */
|
|
38
|
+
drawables = [];
|
|
39
|
+
pool = [];
|
|
40
|
+
projScreen = new THREE.Matrix4();
|
|
41
|
+
frustum = new THREE.Frustum();
|
|
42
|
+
point = new THREE.Vector4();
|
|
43
|
+
layers = new THREE.Layers();
|
|
44
|
+
constructor(sprites, skip) {
|
|
45
|
+
this.sprites = sprites;
|
|
46
|
+
this.skip = skip;
|
|
47
|
+
}
|
|
48
|
+
/** Walks `scene` for `camera`, whose matrices must be current. */
|
|
49
|
+
collect(scene, camera) {
|
|
50
|
+
this.layers = camera.layers;
|
|
51
|
+
this.projScreen.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
52
|
+
this.frustum.setFromProjectionMatrix(this.projScreen, THREE.WebGLCoordinateSystem, camera.reversedDepth);
|
|
53
|
+
this.drawables.length = 0;
|
|
54
|
+
this.visit(scene, 0);
|
|
55
|
+
}
|
|
56
|
+
visit(object, groupOrder) {
|
|
57
|
+
if (object === this.skip)
|
|
58
|
+
return;
|
|
59
|
+
const instance = this.sprites.instanceOf(object);
|
|
60
|
+
if (instance !== undefined) {
|
|
61
|
+
if (object.layers.test(this.layers) && this.inView(object))
|
|
62
|
+
this.push(object, groupOrder, instance);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!object.visible)
|
|
66
|
+
return;
|
|
67
|
+
const order = this.visitOwn(object, groupOrder);
|
|
68
|
+
for (const child of object.children)
|
|
69
|
+
this.visit(child, order);
|
|
70
|
+
}
|
|
71
|
+
/** Files the object itself when three would draw it transparent; returns the group order for its children. */
|
|
72
|
+
visitOwn(object, groupOrder) {
|
|
73
|
+
if (!object.layers.test(this.layers))
|
|
74
|
+
return groupOrder;
|
|
75
|
+
if (object.isGroup === true)
|
|
76
|
+
return object.renderOrder;
|
|
77
|
+
if (isRenderable(object) && drawsTransparent(object) && this.inView(object))
|
|
78
|
+
this.push(object, groupOrder, null);
|
|
79
|
+
return groupOrder;
|
|
80
|
+
}
|
|
81
|
+
/** three's per-object culling: `!frustumCulled || intersectsFrustum`. */
|
|
82
|
+
inView(object) {
|
|
83
|
+
return !object.frustumCulled || object.intersectsFrustum(this.frustum) === true;
|
|
84
|
+
}
|
|
85
|
+
push(object, groupOrder, instance) {
|
|
86
|
+
const index = this.drawables.length;
|
|
87
|
+
const drawable = this.pool[index] ?? { key: null, groupOrder: 0, renderOrder: 0, z: 0, id: 0, object, instance: null };
|
|
88
|
+
this.pool[index] = drawable;
|
|
89
|
+
drawable.key = instance === null ? null : this.sprites.keyOf(instance);
|
|
90
|
+
drawable.groupOrder = groupOrder;
|
|
91
|
+
drawable.renderOrder = object.renderOrder;
|
|
92
|
+
drawable.z = this.sortDepth(object);
|
|
93
|
+
drawable.id = object.id;
|
|
94
|
+
drawable.object = object;
|
|
95
|
+
drawable.instance = instance;
|
|
96
|
+
this.drawables.push(drawable);
|
|
97
|
+
}
|
|
98
|
+
/** The clip-space z three sorts the object by. */
|
|
99
|
+
sortDepth(object) {
|
|
100
|
+
if (object.isSprite === true) {
|
|
101
|
+
return this.point.setFromMatrixPosition(object.matrixWorld).applyMatrix4(this.projScreen).z;
|
|
102
|
+
}
|
|
103
|
+
const center = sortCenter(object);
|
|
104
|
+
// Vector4.copy of a Vector3, as three does it: w becomes 1.
|
|
105
|
+
return this.point.set(center.x, center.y, center.z, 1).applyMatrix4(object.matrixWorld).applyMatrix4(this.projScreen).z;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One transparent renderable of a frame, with the fields three sorts it by. */
|
|
2
|
+
export interface Drawable<K> {
|
|
3
|
+
/** The sprite's batch key; null for any other renderable (particles, tilemaps, meshes). */
|
|
4
|
+
key: K | null;
|
|
5
|
+
/** The renderOrder of the nearest ancestor Group (three's `groupOrder`). */
|
|
6
|
+
groupOrder: number;
|
|
7
|
+
renderOrder: number;
|
|
8
|
+
/** Clip-space z of the sort point, as three computes it: higher is farther. */
|
|
9
|
+
z: number;
|
|
10
|
+
/** Object3D id: three's last tie-break, so creation order. */
|
|
11
|
+
id: number;
|
|
12
|
+
}
|
|
13
|
+
/** A Sprite Batch run, or a renderable drawn on its own between runs. */
|
|
14
|
+
export type DrawStep<K, T extends Drawable<K>> = {
|
|
15
|
+
kind: 'run';
|
|
16
|
+
key: K;
|
|
17
|
+
groupOrder: number;
|
|
18
|
+
items: T[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'other';
|
|
21
|
+
item: T;
|
|
22
|
+
};
|
|
23
|
+
/** three's `reversePainterSortStable`: the order it draws transparent objects in. */
|
|
24
|
+
export declare function compareTransparentDraws<K>(a: Drawable<K>, b: Drawable<K>): number;
|
|
25
|
+
/** What forEachSpriteRun reports, in draw order. */
|
|
26
|
+
export interface SpriteRunVisitor<K, T extends Drawable<K>> {
|
|
27
|
+
/** A run: the drawables `start` (inclusive) to `end` (exclusive) share `key` and one group order. */
|
|
28
|
+
run(key: K, start: number, end: number): void;
|
|
29
|
+
other(item: T): void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walks drawables already in draw order and reports each maximal sequence of
|
|
33
|
+
* consecutive same-key sprites under one group order as a run. A sprite of
|
|
34
|
+
* another key, any other renderable, or a change of group order ends the
|
|
35
|
+
* run — so batching never changes what is drawn in front of what (ADR 0024).
|
|
36
|
+
* Allocates nothing: the frame pass calls it every rendered frame.
|
|
37
|
+
*/
|
|
38
|
+
export declare function forEachSpriteRun<K, T extends Drawable<K>>(ordered: readonly T[], visitor: SpriteRunVisitor<K, T>): void;
|
|
39
|
+
/** Orders a frame's drawables exactly as three would draw them, then groups them into runs. */
|
|
40
|
+
export declare function buildSpriteRuns<K, T extends Drawable<K>>(drawables: readonly T[]): DrawStep<K, T>[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** three's `reversePainterSortStable`: the order it draws transparent objects in. */
|
|
2
|
+
export function compareTransparentDraws(a, b) {
|
|
3
|
+
if (a.groupOrder !== b.groupOrder)
|
|
4
|
+
return a.groupOrder - b.groupOrder;
|
|
5
|
+
if (a.renderOrder !== b.renderOrder)
|
|
6
|
+
return a.renderOrder - b.renderOrder;
|
|
7
|
+
if (a.z !== b.z)
|
|
8
|
+
return b.z - a.z;
|
|
9
|
+
return a.id - b.id;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Walks drawables already in draw order and reports each maximal sequence of
|
|
13
|
+
* consecutive same-key sprites under one group order as a run. A sprite of
|
|
14
|
+
* another key, any other renderable, or a change of group order ends the
|
|
15
|
+
* run — so batching never changes what is drawn in front of what (ADR 0024).
|
|
16
|
+
* Allocates nothing: the frame pass calls it every rendered frame.
|
|
17
|
+
*/
|
|
18
|
+
export function forEachSpriteRun(ordered, visitor) {
|
|
19
|
+
let start = 0;
|
|
20
|
+
while (start < ordered.length) {
|
|
21
|
+
const first = ordered[start];
|
|
22
|
+
if (!first)
|
|
23
|
+
return;
|
|
24
|
+
const key = first.key;
|
|
25
|
+
if (key === null) {
|
|
26
|
+
visitor.other(first);
|
|
27
|
+
start += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
let end = start + 1;
|
|
31
|
+
while (end < ordered.length && ordered[end]?.key === key && ordered[end]?.groupOrder === first.groupOrder)
|
|
32
|
+
end += 1;
|
|
33
|
+
visitor.run(key, start, end);
|
|
34
|
+
start = end;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Orders a frame's drawables exactly as three would draw them, then groups them into runs. */
|
|
38
|
+
export function buildSpriteRuns(drawables) {
|
|
39
|
+
const ordered = [...drawables].sort(compareTransparentDraws);
|
|
40
|
+
const steps = [];
|
|
41
|
+
forEachSpriteRun(ordered, {
|
|
42
|
+
run: (key, start, end) => {
|
|
43
|
+
const items = ordered.slice(start, end);
|
|
44
|
+
steps.push({ kind: 'run', key, groupOrder: items[0]?.groupOrder ?? 0, items });
|
|
45
|
+
},
|
|
46
|
+
other: (item) => steps.push({ kind: 'other', item }),
|
|
47
|
+
});
|
|
48
|
+
return steps;
|
|
49
|
+
}
|