0.2.6-dev: impostor slots sized from every clip a unit plays and clipped to themselves (GitHub #542 by @ImPQlc: in a crowd 野鬃's idle flag, 浊心斯卡蒂's S3 flight and 隐现's pistol were cut, offset or left stray pixels) — spine.js measures each skeleton's clips once within a 2.5 ms frame budget, units.js impostorBox covers the setup pose and the played clips (a clip past 4× / 2× of that core is drawn directly while it plays, the slot kept), impostor.js draws each slot's skeleton under a scissor rect of the slot; golden: unchanged
This commit is contained in:
1 parent
e825254850
commit
c45d86f70c
8 files changed
+1153
-22
No files matched your search
@@ -6,14 +6,22 @@
|
||||
// load/store on tiled GPUs. Instead of one RenderTexture per unit (one framebuffer switch each), every due
|
||||
// impostor of a frame is drawn into slots of a few shared atlas pages in ONE render call per page, before the
|
||||
// main pass starts:
|
||||
// page = RenderTexture (2048² physical px) + a batch container [erasers…, skeletons…]
|
||||
// slot = shelf-packed rect (16 px grid) owned by one unit; erased (ERASE blend quad) then redrawn when due
|
||||
// page = RenderTexture (2048² physical px) + a batch container [erasers…, slot boxes…]
|
||||
// slot = shelf-packed rect (16 px grid) owned by one unit; erased (ERASE blend quad over exactly the slot) then
|
||||
// redrawn when due. The skeleton is drawn inside the slot's own box — a container masked by the slot
|
||||
// rectangle, which PIXI turns into a scissor rect (an axis-aligned rectangle mask: no stencil, no render-pass
|
||||
// break) — so nothing a unit draws can land outside its slot: a pose wider than the unit's box (render/units.js
|
||||
// _impBox) is cut at the slot's edge instead of painting into the neighbouring slots or the free space
|
||||
// around them, where no eraser ever cleared it again (GitHub #542: 野鬃's flag, 浊心斯卡蒂's S3 flight).
|
||||
// Every redraw therefore starts from an empty slot: its eraser clears all the slot can hold.
|
||||
// Pages are created on demand (≤ MAX_PAGES); when every page is full `alloc` returns null and the unit renders
|
||||
// directly (still correct, just not batched). A shelf whose last slot is freed is emptied for reuse, and empty shelves
|
||||
// at the top of a page are given back (a page whose slots are all freed — every battle / prep rebuild — starts over):
|
||||
// without that, zooming a crowd in and out (the enemy pen camera) left pages full of shelves of stale sizes and the
|
||||
// next battle's units fell back to one private render target each. Clipped skeletons get slots on small separate "clip" pages: their
|
||||
// stencil clears break the page's render pass, and the cost of a break scales with the page size.
|
||||
// stencil clears break the page's render pass, and the cost of a break scales with the page size. The scissor of the
|
||||
// slot box also bounds those stencil clears to the slot. Cost of the clipping: one batch flush per drawn slot (each
|
||||
// skeleton is its own draw call or calls; skeletons sharing an atlas texture no longer batch across slots).
|
||||
//
|
||||
// const imp = new ImpostorAtlas(renderer)
|
||||
// const slot = imp.alloc(w, h) // CSS px; null when full
|
||||
@@ -49,9 +57,9 @@ export class ImpostorAtlas {
|
||||
try { rt.framebuffer.enableStencil(); } catch { /* masks fall back to scissor */ }
|
||||
const batch = new P.Container();
|
||||
const erasers = new P.Container();
|
||||
const bodies = new P.Container();
|
||||
batch.addChild(erasers, bodies);
|
||||
const page = { kind, size, rt, batch, erasers, bodies, shelves: [], nextY: 0, queued: 0, cleared: false, index: this.pages.length };
|
||||
const boxes = new P.Container();
|
||||
batch.addChild(erasers, boxes);
|
||||
const page = { kind, size, rt, batch, erasers, boxes, shelves: [], nextY: 0, queued: 0, cleared: false, index: this.pages.length };
|
||||
this.pages.push(page);
|
||||
this.stats.pages = this.pages.length;
|
||||
return page;
|
||||
@@ -97,9 +105,25 @@ export class ImpostorAtlas {
|
||||
eraser.width = w; eraser.height = h;
|
||||
eraser.visible = false;
|
||||
page.erasers.addChild(eraser);
|
||||
// the slot's own pass: its skeleton is drawn in `box`, clipped to the slot rectangle (a scissor rect)
|
||||
const mask = new P.Graphics();
|
||||
mask.beginFill(0xffffff, 1);
|
||||
mask.drawRect(x, y, w, h);
|
||||
mask.endFill();
|
||||
const box = new P.Container();
|
||||
box.addChild(mask);
|
||||
box.mask = mask;
|
||||
box.visible = false;
|
||||
page.boxes.addChild(box);
|
||||
const tex = new P.Texture(page.rt.baseTexture, new P.Rectangle(x, y, w, h));
|
||||
sh.live = (sh.live || 0) + 1;
|
||||
return { page, shelf: sh, clip: page.kind === 'clip', x, y, w, h, eraser, tex, freed: false };
|
||||
return { page, shelf: sh, clip: page.kind === 'clip', x, y, w, h, eraser, box, mask, tex, freed: false };
|
||||
}
|
||||
|
||||
/** Back to the parking lot: every skeleton queued in a slot's box (never destroyed here — the actors own them). */
|
||||
_unqueue(slot) {
|
||||
for (const b of [...slot.box.children]) if (b !== slot.mask) this.park(b);
|
||||
slot.box.visible = false;
|
||||
}
|
||||
|
||||
free(slot) {
|
||||
@@ -107,6 +131,11 @@ export class ImpostorAtlas {
|
||||
slot.freed = true;
|
||||
this.stats.slots = Math.max(0, this.stats.slots - 1);
|
||||
try { slot.eraser.destroy(); } catch { /* ignore */ }
|
||||
try {
|
||||
this._unqueue(slot); // a skeleton queued this frame (its unit left impostor mode) is not destroyed with the box
|
||||
slot.box.mask = null;
|
||||
slot.box.destroy({ children: true });
|
||||
} catch { /* ignore */ }
|
||||
try { slot.tex.destroy(false); } catch { /* ignore */ }
|
||||
const page = slot.page;
|
||||
const sh = slot.shelf && page.shelves.includes(slot.shelf) ? slot.shelf : page.shelves.find((s) => s.y === slot.y);
|
||||
@@ -135,7 +164,8 @@ export class ImpostorAtlas {
|
||||
obj.position.set(slot.x + m.tx, slot.y + m.ty);
|
||||
obj.scale.set(m.a, m.d);
|
||||
obj.visible = true;
|
||||
if (obj.parent !== page.bodies) page.bodies.addChild(obj);
|
||||
if (obj.parent !== slot.box) slot.box.addChild(obj);
|
||||
slot.box.visible = true;
|
||||
page.queued++;
|
||||
}
|
||||
|
||||
@@ -152,14 +182,19 @@ export class ImpostorAtlas {
|
||||
page.queued = 0;
|
||||
for (const e of page.erasers.children) e.visible = false;
|
||||
// back to the parking lot until due again
|
||||
for (const b of [...page.bodies.children]) this.park(b);
|
||||
for (const box of page.boxes.children) {
|
||||
if (!box.visible) continue;
|
||||
box.visible = false;
|
||||
for (const b of [...box.children]) if (b !== box.mask) this.park(b);
|
||||
}
|
||||
}
|
||||
this.stats.drawn = drawn;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const page of this.pages) {
|
||||
try { page.batch.destroy({ children: false }); } catch { /* ignore */ }
|
||||
for (const box of page.boxes.children) for (const b of [...box.children]) if (b !== box.mask) box.removeChild(b);
|
||||
try { page.batch.destroy({ children: true }); } catch { /* ignore */ }
|
||||
try { page.rt.destroy(true); } catch { /* ignore */ }
|
||||
}
|
||||
this.pages = [];
|
||||
|
||||
Reference in new issue
Block a user