@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
582 lines (441 loc) • 15.5 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import { ceilPowerOfTwo } from "../../../../core/binary/operations/ceilPowerOfTwo.js";
import { BinaryDataType } from "../../../../core/binary/type/BinaryDataType.js";
import {
DataType2TypedArrayConstructorMapping
} from "../../../../core/binary/type/DataType2TypedArrayConstructorMapping.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { MaxRectanglesPacker } from "../../../../core/geom/packing/max-rect/MaxRectanglesPacker.js";
import Vector2 from "../../../../core/geom/Vector2.js";
import IdPool from "../../../../core/IdPool.js";
import { max2 } from "../../../../core/math/max2.js";
import { invokeObjectClone } from "../../../../core/model/object/invokeObjectClone.js";
import { Sampler2D } from "../sampler/Sampler2D.js";
import { sampler2d_sub_copy_same_item_size } from "../sampler/sampler2d_sub_copy_same_item_size.js";
import { AbstractTextureAtlas } from "./AbstractTextureAtlas.js";
import { AtlasPatch } from "./AtlasPatch.js";
import { AtlasPatchFlag } from "./AtlasPatchFlag.js";
export class TextureAtlas extends AbstractTextureAtlas {
/**
*
* @param {number} [size]
* @param {BinaryDataType} [data_type]
* @param {number} [channel_count]
* @constructor
*/
constructor(size = 16, data_type = BinaryDataType.Uint8, channel_count = 4) {
super();
assert.isNonNegativeInteger(size, 'size');
assert.enum(data_type, BinaryDataType, 'data_type');
assert.isNonNegativeInteger(channel_count, 'channel_count');
/**
*
* @type {IdPool}
* @private
*/
this.idPool = new IdPool();
/**
* @private
* @type {AtlasPatch[]}
*/
this.patches = [];
/**
* @readonly
* @type {Vector2}
*/
this.size = new Vector2(size, size);
const TypeArrayConstructor = DataType2TypedArrayConstructorMapping[data_type];
/**
* @private
* @readonly
* @type {Sampler2D}
*/
this.__sampler = new Sampler2D(new TypeArrayConstructor(size * size * channel_count), channel_count, size, size);
/**
* @private
* @type {MaxRectanglesPacker}
*/
this.packer = new MaxRectanglesPacker(size, size);
this.on = {
painted: new Signal(),
/**
* @type {Signal<AtlasPatch, Sampler2D>}
*/
removed: new Signal()
};
/**
*
* @type {boolean}
* @private
*/
this.__needsUpdate = false;
}
/**
*
* @return {Sampler2D}
*/
get sampler() {
return this.__sampler;
}
/**
*
* @param {Sampler2D} sampler
* @returns {AtlasPatch|undefined}
*/
getPatch(sampler) {
const patches = this.patches;
const patch_count = patches.length;
for (let i = 0; i < patch_count; i++) {
const patch = patches[i];
if (patch.sampler === sampler) {
return patch;
}
}
}
/**
* Whether or not current state of atlas requires calling {#update}
* @returns {boolean}
*/
needsUpdate() {
return this.__needsUpdate;
}
/**
*
* @param {AtlasPatch} patch
* @returns {boolean}
*/
contains(patch) {
assert.notNull(patch, 'patch');
assert.defined(patch, 'patch');
assert.ok(patch.isAtlasPatch, 'patch argument is not an AtlasPatch');
return this.patches.includes(patch);
}
/**
* Clear canvas and update patch flags
*/
erase() {
//clear all painted patches
const patches = this.patches;
const l = patches.length;
for (let i = 0; i < l; i++) {
const atlasPatch = patches[i];
atlasPatch.clearFlag(AtlasPatchFlag.Painted);
}
//erase data
this.__sampler.data.fill(0);
this.__needsUpdate = true;
}
/**
*
* @param {number} x
* @param {number} y
* @returns {boolean} true if resizing successful, false otherwise
*/
resize(x, y) {
assert.isNonNegativeInteger(x, 'x');
assert.isNonNegativeInteger(y, 'y');
//check if any patches would be cut
const patches = this.patches;
const numPatches = patches.length;
//update patch UVs
for (let i = 0; i < numPatches; i++) {
const patch = patches[i];
if (patch.getFlag(AtlasPatchFlag.Packed)) {
//only care about packed patches
const x1 = patch.packing.x1;
const y1 = patch.packing.y1;
if (x1 > x || y1 > y) {
// patch bounds would violate the desired atlas extends
// Resizing atlas would result in some patches not fitting
return false;
}
}
}
//update patch UVs
for (let i = 0; i < numPatches; i++) {
const patch = patches[i];
if (patch.getFlag(AtlasPatchFlag.Packed)) {
patch.updateUV(x, y);
}
}
this.size.set(x, y);
this.packer.resize(x, y);
this.__sampler.resize(x, y);
return true;
}
/**
*
* @param {AtlasPatch} patch
* @private
*/
paintPatch(patch) {
// console.time('TextureAtlas.paintPatch');
const target = this.__sampler;
const source = patch.sampler;
const patch_position = patch.position;
const patch_size = patch.size;
sampler2d_sub_copy_same_item_size(
target,
source,
0, 0,
patch_position.x, patch_position.y, patch_size.x, patch_size.y
);
patch.setFlag(AtlasPatchFlag.Painted);
patch.last_painted_version = source.version; // record version
// console.timeEnd('TextureAtlas.paintPatch');
}
/**
*
* @param {AtlasPatch} patch
* @private
*/
erasePatch(patch) {
//erase the patch
const packing = patch.packing;
const x0 = packing.x0;
const y0 = packing.y0;
const x1 = packing.x1;
const y1 = packing.y1;
this.eraseArea(x0, y0, x1, y1);
// mark as non-painted
patch.clearFlag(AtlasPatchFlag.Painted);
}
/**
* @private
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
*/
eraseArea(x0, y0, x1, y1) {
const sampler = this.__sampler;
sampler.zeroFill(x0, y0, x1 - x0, y1 - y0);
}
/**
*
* @param {Sampler2D} sampler
* @param {number} [padding]
* @return {AtlasPatch}
*/
add(sampler, padding = 4) {
assert.notNull(sampler, 'sampler');
assert.defined(sampler, 'sampler');
assert.equal(sampler.isSampler2D, true, 'sampler.isSampler2D !== true');
assert.isNonNegativeInteger(padding, 'padding');
// console.log('#add');
const patch = new AtlasPatch();
patch.id = this.idPool.get();
patch.sampler = sampler;
patch.size.set(sampler.width, sampler.height);
patch.padding = padding;
// mark patch as belonging to an atlas
patch.setFlag(AtlasPatchFlag.Attached);
const padding2 = padding * 2;
patch.packing.set(0, 0, sampler.width + padding2, sampler.height + padding2);
this.patches.push(patch);
this.__needsUpdate = true;
return patch;
}
/**
* Whether a patch of certain size could be packed without triggering repack or growing the atlas
* @param {number} w
* @param {number} h
* @returns {boolean}
*/
can_pack(w, h) {
return this.packer.canAdd(w, h);
}
/**
*
* @param {AtlasPatch} patch
* @return {boolean}
*/
remove(patch) {
// console.log('#remove');
const patchIndex = this.patches.indexOf(patch);
if (patchIndex === -1) {
//not on the atlas, do nothing
return false;
}
this.patches.splice(patchIndex, 1);
this.idPool.release(patch.id);
if (patch.getFlag(AtlasPatchFlag.Painted)) {
//erase the patch
this.erasePatch(patch);
}
if (patch.getFlag(AtlasPatchFlag.Packed)) {
this.packer.remove(patch.packing);
}
// clear flags
patch.clearFlag(
AtlasPatchFlag.Attached
| AtlasPatchFlag.Packed
| AtlasPatchFlag.Painted
);
this.on.removed.send2(patch, patch.sampler);
return true;
}
/**
* Re-packs all patches, this is useful for reclaiming fragmented space after extended usage
* @returns {boolean}
*/
repack() {
const patches = this.patches;
const numPatches = patches.length;
for (let i = 0; i < numPatches; i++) {
const patch = patches[i];
if (patch.getFlag(AtlasPatchFlag.Packed)) {
patch.clearFlag(AtlasPatchFlag.Packed | AtlasPatchFlag.Packed);
this.packer.remove(patch.packing);
}
}
this.pack();
this.__needsUpdate = true;
return true;
}
/**
* Pack any patches that are not packed yet
* @private
* @returns {boolean}
*/
pack() {
const patches = this.patches;
const numPatches = patches.length;
let i, l;
const additions = [];
for (i = 0; i < numPatches; i++) {
const patch = patches[i];
if (!patch.getFlag(AtlasPatchFlag.Packed)) {
additions.push(patch.packing);
}
}
//perform additions
if (additions.length <= 0) {
//nothing to pack
return true;
}
//add all at once, this allows packer to optimize order internally to achieve better results
if (this.packer.addMany(additions)) {
//all packed, we're done
} else {
//packing failed, lets try a fresh re-pack
const repacker = new MaxRectanglesPacker(this.size.x, this.size.y);
// Clone existing placements to make sure our attempt can be reverted
const originalPlacements = this.packer.boxes.map(invokeObjectClone);
// Add packed boxes
Array.prototype.push.apply(additions, this.packer.boxes);
const repackSuccessful = repacker.addMany(additions);
if (!repackSuccessful) {
// repack failed
for (i = 0, l = originalPlacements.length; i < l; i++) {
const originalPlacement = originalPlacements[i];
const box = this.packer.boxes[i];
//restore packing
box.copy(originalPlacement);
}
return false;
}
//repack succeeded, actualize placements
for (i = 0, l = originalPlacements.length; i < l; i++) {
const source = this.packer.boxes[i];
const originalPlacement = originalPlacements[i];
if (source.equals(originalPlacement)) {
//same position retained
// console.log("patch retained");
} else {
//find patch
const atlasPatch = this.patches.find(p => p.packing === source);
assert.defined(atlasPatch, "atlasPatch");
//clear our original patch area
this.eraseArea(originalPlacement.x0, originalPlacement.y0, originalPlacement.x1, originalPlacement.y1);
//mark patch for re-paint
atlasPatch.clearFlag(AtlasPatchFlag.Painted);
// console.log("patch erased");
}
//replace packing
}
//mark all patches for a repaint
// this.erase();
//replace packer with the new one
this.packer = repacker;
}
for (i = 0; i < numPatches; i++) {
const patch = patches[i];
//mark patch as packed
patch.setFlag(AtlasPatchFlag.Packed);
patch.updatePositionFromPacking(this.size.x, this.size.y);
}
return true;
}
/**
* @private
*/
paint() {
// console.time('TextureAtlas.paint');
const patches = this.patches;
const l = patches.length;
let paintCount = 0;
for (let i = 0; i < l; i++) {
const patch = patches[i];
if (
!patch.getFlag(AtlasPatchFlag.Painted)
|| patch.last_painted_version !== patch.sampler.version
) {
this.paintPatch(patch);
paintCount++;
}
}
// console.timeEnd('TextureAtlas.paint');
if (paintCount > 0) {
//notify that atlas was painted
this.on.painted.send0();
}
}
/**
*
*/
update() {
if (!this.__needsUpdate) {
//no update required
return;
}
// console.group("TextureAtlas.update");
// console.time('TextureAtlas.update');
const maxPower = 14;
const initial_p_x = max2(0, Math.log2(ceilPowerOfTwo(this.size.x)));
const initial_p_y = max2(0, Math.log2(ceilPowerOfTwo(this.size.y)));
let power_x = initial_p_x;
let power_y = initial_p_y;
while (!this.pack()) {
//packing failed, grow canvas
for (; ;) {
if (power_x < power_y) {
power_x++;
} else {
power_y++;
}
if (power_x > maxPower || power_y > maxPower) {
throw new Error(`Packing failed, could not pack ${this.patches.length} into ${Math.pow(2, maxPower)} resolution texture. Initial powers: ${initial_p_x}, ${initial_p_y}`);
}
const size_x = Math.pow(2, power_x);
const size_y = Math.pow(2, power_y);
if (this.resize(size_x, size_y)) {
break;
}
}
}
this.paint();
this.__needsUpdate = false;
// console.timeEnd('TextureAtlas.update');
// console.groupEnd("TextureAtlas.update");
}
/**
* Drops all the data
*/
reset() {
this.__sampler.data.fill(0);
this.patches = [];
this.packer.clear();
this.idPool.reset();
}
}