UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

582 lines (441 loc) • 15.5 kB
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(); } }