#18 动态图集用于优化drawcall
This commit is contained in:
133
source/src/Utils/RuntimeSpriteSheet/AssetPacker.ts
Normal file
133
source/src/Utils/RuntimeSpriteSheet/AssetPacker.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
module es {
|
||||
import Bitmap = egret.Bitmap;
|
||||
|
||||
export class AssetPacker {
|
||||
protected itemsToRaster: TextureToPack[] = [];
|
||||
public onProcessCompleted: Function;
|
||||
public useCache: boolean = false;
|
||||
public cacheName: string = "";
|
||||
|
||||
protected _sprites: Map<string, egret.Texture> = new Map<string, egret.Texture>();
|
||||
protected allow4096Textures = false;
|
||||
|
||||
public addTextureToPack(texture: egret.Texture, customID: string){
|
||||
this.itemsToRaster.push(new TextureToPack(texture, customID));
|
||||
}
|
||||
|
||||
public async process(allow4096Textures: boolean = false){
|
||||
this.allow4096Textures = allow4096Textures;
|
||||
if (this.useCache){
|
||||
if (this.cacheName == "") {
|
||||
console.error("未指定缓存名称");
|
||||
return;
|
||||
}
|
||||
|
||||
let cacheExist = await RES.getResByUrl(this.cacheName);
|
||||
|
||||
if (!cacheExist)
|
||||
this.createPack();
|
||||
else
|
||||
this.loadPack();
|
||||
}else{
|
||||
this.createPack();
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadPack() {
|
||||
let loaderTexture = await RES.getResByUrl(this.cacheName);
|
||||
if (this.onProcessCompleted) this.onProcessCompleted();
|
||||
return loaderTexture;
|
||||
}
|
||||
|
||||
protected createPack(){
|
||||
let textures: egret.Bitmap[] = [];
|
||||
let images = [];
|
||||
|
||||
for (let itemToRaster of this.itemsToRaster){
|
||||
textures.push(new Bitmap(itemToRaster.texture));
|
||||
images.push(itemToRaster.id);
|
||||
}
|
||||
|
||||
let textureSize = this.allow4096Textures ? 4096 : 2048;
|
||||
|
||||
let rectangles = [];
|
||||
for (let i = 0; i < textures.length; i ++){
|
||||
if (textures[i].width > textureSize || textures[i].height > textureSize){
|
||||
throw new Error("一个纹理的大小比图集的大小大");
|
||||
}else{
|
||||
rectangles.push(new Rectangle(0, 0, textures[i].width, textures[i].height));
|
||||
}
|
||||
}
|
||||
|
||||
const padding = 1;
|
||||
let numSpriteSheet = 0;
|
||||
while (rectangles.length > 0){
|
||||
let texture = new egret.RenderTexture();
|
||||
|
||||
let packer: RectanglePacker = new RectanglePacker(textureSize, textureSize, padding);
|
||||
for (let i = 0; i < rectangles.length; i ++)
|
||||
packer.insertRectangle(Math.floor(rectangles[i].width), Math.floor(rectangles[i].height), i);
|
||||
|
||||
packer.packRectangles();
|
||||
|
||||
if (packer.rectangleCount > 0){
|
||||
let rect = new IntegerRectangle();
|
||||
let textureAssets: TextureAsset[] = [];
|
||||
let garbageRect: Rectangle[] = [];
|
||||
let garabeTextures: egret.Texture[] = [];
|
||||
let garbageImages: string[] = [];
|
||||
|
||||
for (let j = 0; j < packer.rectangleCount; j ++){
|
||||
rect = packer.getRectangle(j, rect);
|
||||
let index = packer.getRectangleId(j);
|
||||
texture.drawToTexture(textures[index], new Rectangle(rect.x, rect.y, rect.width, rect.height));
|
||||
|
||||
let textureAsset: TextureAsset = new TextureAsset();
|
||||
textureAsset.x = rect.x;
|
||||
textureAsset.y = rect.y;
|
||||
textureAsset.width = rect.width;
|
||||
textureAsset.height = rect.height;
|
||||
textureAsset.name = images[index];
|
||||
|
||||
textureAssets.push(textureAsset);
|
||||
|
||||
garbageRect.push(rectangles[index]);
|
||||
garabeTextures.push(textures[index].texture);
|
||||
garbageImages.push(images[index]);
|
||||
}
|
||||
|
||||
for (let garbage of garbageRect)
|
||||
rectangles.remove(garbage);
|
||||
|
||||
for (let garbage of garabeTextures)
|
||||
textures.removeAll(a => {return a.texture.hashCode == garbage.hashCode});
|
||||
|
||||
for (let garbage of garbageImages)
|
||||
images.remove(garbage);
|
||||
|
||||
if (this.cacheName != ""){
|
||||
texture.saveToFile("image/png", this.cacheName);
|
||||
++ numSpriteSheet;
|
||||
}
|
||||
|
||||
for (let textureAsset of textureAssets)
|
||||
this._sprites.set(textureAsset.name, texture);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.onProcessCompleted) this.onProcessCompleted();
|
||||
}
|
||||
|
||||
public dispose(){
|
||||
this._sprites.forEach((asset, name) => {
|
||||
asset.dispose();
|
||||
RES.destroyRes(name);
|
||||
});
|
||||
this._sprites.clear();
|
||||
}
|
||||
|
||||
public getTexture(id: string){
|
||||
return this._sprites.get(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
source/src/Utils/RuntimeSpriteSheet/IntegerRectangle.ts
Normal file
9
source/src/Utils/RuntimeSpriteSheet/IntegerRectangle.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
module es {
|
||||
/**
|
||||
* 类中用于存储矩形值的矩形封装器
|
||||
* ID参数需要连接矩形和原来插入的矩形
|
||||
*/
|
||||
export class IntegerRectangle extends Rectangle{
|
||||
public id: number;
|
||||
}
|
||||
}
|
||||
278
source/src/Utils/RuntimeSpriteSheet/RectanglePacker.ts
Normal file
278
source/src/Utils/RuntimeSpriteSheet/RectanglePacker.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
module es {
|
||||
/**
|
||||
* 类用于在容器矩形内包装矩形,并具有接近最优解。
|
||||
*/
|
||||
export class RectanglePacker {
|
||||
private _width: number = 0;
|
||||
private _height: number = 0;
|
||||
private _padding: number = 8;
|
||||
|
||||
private _packedWidth = 0;
|
||||
private _packedHeight = 0;
|
||||
|
||||
private _insertList: SortableSize[] = [];
|
||||
private _insertedRectangles: IntegerRectangle[] = [];
|
||||
private _freeAreas: IntegerRectangle[] = [];
|
||||
private _newFreeAreas: IntegerRectangle[] = [];
|
||||
|
||||
private _outsideRectangle: IntegerRectangle;
|
||||
|
||||
private _sortableSizeStack: SortableSize[] = [];
|
||||
private _rectangleStack: IntegerRectangle[] = [];
|
||||
|
||||
public get rectangleCount() {
|
||||
return this._insertedRectangles.length;
|
||||
}
|
||||
|
||||
public get packedWidth() {
|
||||
return this._packedWidth;
|
||||
}
|
||||
|
||||
public get packedHeight() {
|
||||
return this._packedHeight;
|
||||
}
|
||||
|
||||
public get padding() {
|
||||
return this._padding;
|
||||
}
|
||||
|
||||
constructor(width: number, height: number, padding: number = 0) {
|
||||
this._outsideRectangle = new IntegerRectangle(width + 1, height + 1, 0, 0);
|
||||
this.reset(width, height, padding);
|
||||
}
|
||||
|
||||
public reset(width: number, height: number, padding: number = 0) {
|
||||
while (this._insertedRectangles.length > 0)
|
||||
this.freeRectangle(this._insertedRectangles.pop());
|
||||
|
||||
while (this._freeAreas.length > 0)
|
||||
this.freeRectangle(this._freeAreas.pop());
|
||||
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
|
||||
this._packedWidth = 0;
|
||||
this._packedHeight = 0;
|
||||
|
||||
this._freeAreas.push(this.allocateRectangle(0, 0, this._width, this._height));
|
||||
|
||||
while (this._insertedRectangles.length > 0)
|
||||
this.freeSize(this._insertList.pop());
|
||||
|
||||
this._padding = padding;
|
||||
}
|
||||
|
||||
public insertRectangle(width: number, height: number, id: number) {
|
||||
let sortableSize = this.allocateSize(width, height, id);
|
||||
this._insertList.push(sortableSize);
|
||||
}
|
||||
|
||||
public packRectangles(sort: boolean = true) {
|
||||
if (sort)
|
||||
this._insertList.sort((emp1, emp2) => {
|
||||
return emp1.width - emp2.width;
|
||||
});
|
||||
|
||||
while (this._insertList.length > 0) {
|
||||
let sortableSize = this._insertList.pop();
|
||||
let width = sortableSize.width;
|
||||
let height = sortableSize.height;
|
||||
|
||||
let index = this.getFreeAreaIndex(width, height);
|
||||
if (index >= 0) {
|
||||
let freeArea = this._freeAreas[index];
|
||||
let target = this.allocateRectangle(freeArea.x, freeArea.y, width, height);
|
||||
target.id = sortableSize.id;
|
||||
|
||||
this.generateNewFreeAreas(target, this._freeAreas, this._newFreeAreas);
|
||||
|
||||
while (this._newFreeAreas.length > 0)
|
||||
this._freeAreas.push(this._newFreeAreas.pop());
|
||||
|
||||
this._insertedRectangles.push(target);
|
||||
|
||||
if (target.right > this._packedWidth)
|
||||
this._packedWidth = target.right;
|
||||
|
||||
if (target.bottom > this._packedHeight)
|
||||
this._packedHeight = target.bottom;
|
||||
}
|
||||
|
||||
this.freeSize(sortableSize);
|
||||
}
|
||||
|
||||
return this.rectangleCount;
|
||||
}
|
||||
|
||||
public getRectangle(index: number, rectangle: IntegerRectangle){
|
||||
let inserted = this._insertedRectangles[index];
|
||||
rectangle.x = inserted.x;
|
||||
rectangle.y = inserted.y;
|
||||
rectangle.width = inserted.width;
|
||||
rectangle.height = inserted.height;
|
||||
|
||||
return rectangle;
|
||||
}
|
||||
|
||||
public getRectangleId(index: number){
|
||||
let inserted = this._insertedRectangles[index];
|
||||
return inserted.id;
|
||||
}
|
||||
|
||||
private generateNewFreeAreas(target: IntegerRectangle, areas: IntegerRectangle[], results: IntegerRectangle[]) {
|
||||
let x = target.x;
|
||||
let y = target.y;
|
||||
let right = target.right + 1 + this._padding;
|
||||
let bottom = target.bottom + 1 + this._padding;
|
||||
|
||||
let targetWithPadding: IntegerRectangle = null;
|
||||
if (this._padding == 0)
|
||||
targetWithPadding = target;
|
||||
|
||||
for (let i = areas.length - 1; i >= 0; i --){
|
||||
let area = areas[i];
|
||||
if (!(x >= area.right || right <= area.x || y >= area.bottom || bottom <= area.y)){
|
||||
if (targetWithPadding == null)
|
||||
targetWithPadding = this.allocateRectangle(target.x, target.y, target.width + this._padding, target.height + this._padding);
|
||||
|
||||
this.generateDividedAreas(targetWithPadding, area, results);
|
||||
let topOfStack = areas.pop();
|
||||
if (i < areas.length){
|
||||
areas[i] = topOfStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetWithPadding != null && targetWithPadding != target)
|
||||
this.freeRectangle(targetWithPadding);
|
||||
|
||||
this.filterSelfSubAreas(results);
|
||||
}
|
||||
|
||||
private filterSelfSubAreas(areas: IntegerRectangle[]){
|
||||
for (let i = areas.length - 1; i >= 0; i --){
|
||||
let filtered = areas[i];
|
||||
for (let j = areas.length - 1; j >= 0; j --){
|
||||
if (i != j){
|
||||
let area = areas[j];
|
||||
if (filtered.x >= area.x && filtered.y >= area.y && filtered.right <= area.right && filtered.bottom <= area.bottom){
|
||||
this.freeRectangle(filtered);
|
||||
let topOfStack = areas.pop();
|
||||
if (i < areas.length){
|
||||
areas[i] = topOfStack;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private generateDividedAreas(divider: IntegerRectangle, area: IntegerRectangle, results: IntegerRectangle[]){
|
||||
let count = 0;
|
||||
|
||||
let rightDelta = area.right - divider.right;
|
||||
if (rightDelta > 0){
|
||||
results.push(this.allocateRectangle(divider.right, area.y, rightDelta, area.height));
|
||||
count ++;
|
||||
}
|
||||
|
||||
let leftDelta = divider.x - area.x;
|
||||
if (leftDelta > 0){
|
||||
results.push(this.allocateRectangle(area.x, area.y, leftDelta, area.height));
|
||||
count ++;
|
||||
}
|
||||
|
||||
let bottomDelta = area.bottom - divider.bottom;
|
||||
if (bottomDelta > 0){
|
||||
results.push(this.allocateRectangle(area.x, divider.bottom, area.width, bottomDelta));
|
||||
count ++;
|
||||
}
|
||||
|
||||
let topDelta = divider.y - area.y;
|
||||
if (topDelta > 0){
|
||||
results.push(this.allocateRectangle(area.x, area.y, area.width, topDelta));
|
||||
count ++;
|
||||
}
|
||||
|
||||
if (count == 0 && (divider.width < area.width || divider.height < area.height)){
|
||||
results.push(area);
|
||||
}else{
|
||||
this.freeRectangle(area);
|
||||
}
|
||||
}
|
||||
|
||||
private getFreeAreaIndex(width: number, height: number) {
|
||||
let best = this._outsideRectangle;
|
||||
let index = -1;
|
||||
|
||||
let paddedWidth = width + this._padding;
|
||||
let paddedHeight = height + this._padding;
|
||||
|
||||
let count = this._freeAreas.length;
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
let free = this._freeAreas[i];
|
||||
if (free.x < this._packedWidth || free.y < this.packedHeight) {
|
||||
if (free.x < best.x && paddedWidth <= free.width && paddedHeight <= free.height) {
|
||||
index = i;
|
||||
if ((paddedWidth == free.width && free.width <= free.height && free.right < this._width) ||
|
||||
(paddedHeight == free.height && free.height <= free.width)) {
|
||||
break;
|
||||
}
|
||||
|
||||
best = free;
|
||||
}
|
||||
} else {
|
||||
if (free.x < best.x && width <= free.width && height <= free.height) {
|
||||
index = i;
|
||||
if ((width == free.width && free.width <= free.height && free.right < this._width) ||
|
||||
(height == free.height && free.height <= free.width)) {
|
||||
break;
|
||||
}
|
||||
|
||||
best = free;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private allocateSize(width: number, height: number, id: number): SortableSize {
|
||||
if (this._sortableSizeStack.length > 0) {
|
||||
let size: SortableSize = this._sortableSizeStack.pop();
|
||||
size.width = width;
|
||||
size.height = height;
|
||||
size.id = id;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
return new SortableSize(width, height, id);
|
||||
}
|
||||
|
||||
private freeSize(size: SortableSize) {
|
||||
this._sortableSizeStack.push(size);
|
||||
}
|
||||
|
||||
private allocateRectangle(x: number, y: number, width: number, height: number) {
|
||||
if (this._rectangleStack.length > 0) {
|
||||
let rectangle = this._rectangleStack.pop();
|
||||
rectangle.x = x;
|
||||
rectangle.y = y;
|
||||
rectangle.width = width;
|
||||
rectangle.height = height;
|
||||
rectangle.right = x + width;
|
||||
rectangle.bottom = y + height;
|
||||
|
||||
return rectangle;
|
||||
}
|
||||
|
||||
return new IntegerRectangle(x, y, width, height);
|
||||
}
|
||||
|
||||
private freeRectangle(rectangle: IntegerRectangle) {
|
||||
this._rectangleStack.push(rectangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
source/src/Utils/RuntimeSpriteSheet/SortableSize.ts
Normal file
16
source/src/Utils/RuntimeSpriteSheet/SortableSize.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
module es {
|
||||
/**
|
||||
* 用于根据维度对插入的矩形进行排序
|
||||
*/
|
||||
export class SortableSize {
|
||||
public width: number;
|
||||
public height: number;
|
||||
public id: number;
|
||||
|
||||
constructor(width: number, height: number, id: number){
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
source/src/Utils/RuntimeSpriteSheet/TextureAssets.ts
Normal file
16
source/src/Utils/RuntimeSpriteSheet/TextureAssets.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
module es {
|
||||
export class TextureAssets {
|
||||
public assets: TextureAsset[];
|
||||
constructor(assets: TextureAsset[]){
|
||||
this.assets = assets;
|
||||
}
|
||||
}
|
||||
|
||||
export class TextureAsset {
|
||||
public x: number;
|
||||
public y: number;
|
||||
public width: number;
|
||||
public height: number;
|
||||
public name: string;
|
||||
}
|
||||
}
|
||||
11
source/src/Utils/RuntimeSpriteSheet/TextureToPack.ts
Normal file
11
source/src/Utils/RuntimeSpriteSheet/TextureToPack.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
module es {
|
||||
export class TextureToPack {
|
||||
public texture: egret.Texture;
|
||||
public id: string;
|
||||
|
||||
constructor(texture: egret.Texture, id: string){
|
||||
this.texture = texture;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user