feat: 图块打包进图集

This commit is contained in:
unanmed 2025-10-26 16:21:50 +08:00
parent eea66f001b
commit b3ed39f800
7 changed files with 272 additions and 54 deletions

View File

@ -27,6 +27,7 @@ export class AssetBuilder implements IAssetBuilder {
this.started = true;
const res = [...this.composer.add([texture])];
const data = res[0];
if (this.output) {
if (!this.output.getTexture(data.index)) {
this.output.addTexture(data.index, data.texture);

View File

@ -0,0 +1,108 @@
import { materials } from './manager';
import { IBlockIdentifier, IIndexedIdentifier } from './types';
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
cls: C,
map: Record<string, number>,
icons: Record<string, number>
): IBlockIdentifier[] {
const max = Math.max(...Object.values(icons));
const arr = Array(max).fill(0);
for (const [key, value] of Object.entries(icons)) {
if (!(key in map)) continue;
const id = key as AllIdsOf<C>;
const num = map[id] as keyof NumberToId;
const identifier: IBlockIdentifier = {
id: id as string,
cls,
num
};
arr[value] = identifier;
}
return arr;
}
function addTileset(set: Set<number>, map?: readonly (readonly number[])[]) {
if (!map) return;
map.forEach(line => {
line.forEach(v => {
if (v >= 10000) set.add(v);
});
});
}
/**
*
*/
export function fallbackLoad() {
// 基本素材
const icons = core.icons.icons;
const images = core.material.images;
const idNumMap: Record<string, number> = {};
for (const [key, value] of Object.entries(core.maps.blocksInfo)) {
idNumMap[value.id] = Number(key);
}
const terrains = extractClsBlocks('terrains', idNumMap, icons.terrains);
const animates = extractClsBlocks('animates', idNumMap, icons.animates);
const items = extractClsBlocks('items', idNumMap, icons.items);
const enemys = extractClsBlocks('enemys', idNumMap, icons.enemys);
const npcs = extractClsBlocks('npcs', idNumMap, icons.npcs);
const enemy48 = extractClsBlocks('enemy48', idNumMap, icons.enemy48);
const npc48 = extractClsBlocks('npc48', idNumMap, icons.npc48);
// Grid
materials.addGrid(images.terrains, terrains);
materials.addGrid(images.items, items);
// Row Animates
materials.addRowAnimate(images.animates, animates, 4, 32);
materials.addRowAnimate(images.enemys, enemys, 2, 32);
materials.addRowAnimate(images.npcs, npcs, 2, 32);
materials.addRowAnimate(images.enemy48, enemy48, 4, 48);
materials.addRowAnimate(images.npc48, npc48, 4, 48);
// Autotile
for (const key of Object.keys(icons.autotile)) {
const id = key as AllIdsOf<'autotile'>;
const img = images.autotile[id];
const identifier: IBlockIdentifier = {
id,
num: idNumMap[id],
cls: 'autotile'
};
materials.addAutotile(img, identifier);
}
// Tilesets
core.tilesets.forEach((v, i) => {
const img = images.tilesets[v];
const identifier: IIndexedIdentifier = {
index: i,
alias: v
};
materials.addTileset(img, identifier);
});
// Images
core.images.forEach((v, i) => {
const img = core.material.images.images[v];
materials.addImage(img, { index: i, alias: v });
});
materials.buildAssets();
// 地图上出现过的 tileset
const tilesetSet = new Set<number>();
core.floorIds.forEach(v => {
const floor = core.floors[v];
addTileset(tilesetSet, floor.bgmap);
addTileset(tilesetSet, floor.bg2map);
addTileset(tilesetSet, floor.map);
addTileset(tilesetSet, floor.fgmap);
addTileset(tilesetSet, floor.fg2map);
});
materials.cacheTilesetList(tilesetSet);
}

View File

@ -1,3 +1,10 @@
export function createMaterial() {}
import { loading } from '@user/data-base';
import { fallbackLoad } from './fallback';
export function createMaterial() {
loading.once('loaded', () => {
fallbackLoad();
});
}
export * from './manager';

View File

@ -53,6 +53,14 @@ export class MaterialManager implements IMaterialManager {
/** 大怪物贴图的标识符 */
private bigImageId: number = 0;
/** 当前 tileset 索引 */
private nowTilesetIndex: number = -1;
/** 当前 tileset 偏移 */
private nowTilesetOffset: number = 0;
constructor() {
this.assetBuilder.pipe(this.assetStore);
}
/**
*
@ -109,14 +117,15 @@ export class MaterialManager implements IMaterialManager {
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number
frames: number,
height: number
): Iterable<IMaterialData> {
return this.addMappedSource(
source,
map,
this.tileStore,
this.rowSplitter,
32,
height,
(tex: ITexture<number>) => {
tex.animated(new TextureColumnAnimater(), frames);
}
@ -143,10 +152,34 @@ export class MaterialManager implements IMaterialManager {
addTileset(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData {
): IMaterialData | null {
const tex = new Texture(source);
this.tilesetStore.addTexture(identifier.index, tex);
this.tilesetStore.alias(identifier.index, identifier.alias);
const width = Math.floor(source.width / 32);
const height = Math.floor(source.height / 32);
const count = width * height;
const offset = Math.ceil(count / 10000);
if (identifier.index === 0) {
this.tilesetOffsetMap.set(0, 0);
this.nowTilesetIndex = 0;
this.nowTilesetOffset = offset;
} else {
if (identifier.index - 1 !== this.nowTilesetIndex) {
logger.warn(78);
return null;
}
const width = Math.floor(source.width / 32);
const height = Math.floor(source.height / 32);
const count = width * height;
const offset = Math.ceil(count / 10000);
const end = this.nowTilesetOffset + offset;
for (let i = this.nowTilesetOffset; i < end; i++) {
this.tilesetOffsetMap.set(i, identifier.index);
}
this.nowTilesetOffset = end;
this.nowTilesetIndex = identifier.index;
}
const data: IMaterialData = {
store: this.tilesetStore,
texture: tex,
@ -173,7 +206,11 @@ export class MaterialManager implements IMaterialManager {
}
getTile(identifier: number): ITexture | null {
return this.tileStore.getTexture(identifier);
if (identifier < 10000) {
return this.tileStore.getTexture(identifier);
} else {
return this.cacheTileset(identifier);
}
}
getTileset(identifier: number): ITexture | null {
@ -185,7 +222,11 @@ export class MaterialManager implements IMaterialManager {
}
getTileByAlias(alias: string): ITexture | null {
return this.tileStore.fromAlias(alias);
if (/X\d{5,}/.test(alias)) {
return this.cacheTileset(parseInt(alias.slice(1)));
} else {
return this.tileStore.fromAlias(alias);
}
}
getTilesetByAlias(alias: string): ITexture | null {
@ -196,6 +237,71 @@ export class MaterialManager implements IMaterialManager {
return this.imageStore.fromAlias(alias);
}
private getTilesetOwnTexture(identifier: number) {
const texture = this.tileStore.getTexture(identifier);
if (texture) return texture;
// 如果 tileset 不存在,那么执行缓存操作
const offset = Math.floor(identifier / 10000);
const index = this.tilesetOffsetMap.get(offset - 1);
if (isNil(index)) return null;
// 获取对应的 tileset 贴图
const tileset = this.tilesetStore.getTexture(index);
if (!tileset) return null;
// 计算图块位置
const rest = identifier - offset * 10000;
const { width, height } = tileset;
const tileWidth = Math.floor(width / 32);
const tileHeight = Math.floor(height / 32);
// 如果图块位置超出了贴图范围
if (rest > tileWidth * tileHeight) return null;
// 裁剪 tileset生成贴图
const x = rest % tileWidth;
const y = Math.floor(rest / tileWidth);
const newTexture = new Texture(tileset.source);
newTexture.clip(x * 32, y * 32, 32, 32);
return newTexture;
}
cacheTileset(identifier: number): ITexture | null {
const newTexture = this.getTilesetOwnTexture(identifier);
if (!newTexture) return null;
// 缓存贴图
this.tileStore.addTexture(identifier, newTexture);
this.idNumMap.set(`X${identifier}`, identifier);
this.numIdMap.set(identifier, `X${identifier}`);
const data = this.assetBuilder.addTexture(newTexture);
newTexture.toAsset(data);
return newTexture;
}
cacheTilesetList(
identifierList: Iterable<number>
): Iterable<ITexture | null> {
const arr = [...identifierList];
const toAdd: ITexture[] = [];
arr.forEach(v => {
const newTexture = this.getTilesetOwnTexture(v);
if (!newTexture) return;
toAdd.push(newTexture);
this.tileStore.addTexture(v, newTexture);
this.idNumMap.set(`X${v}`, v);
this.numIdMap.set(v, `X${v}`);
});
const set = new Set(toAdd);
const data = this.assetBuilder.addTextureList(toAdd);
const res = [...data];
res.forEach(v => {
v.assetMap.keys().forEach(tex => {
if (set.has(tex)) tex.toAsset(v);
});
});
return toAdd;
}
buildAssets(): Iterable<IMaterialAssetData> {
this.assetBuilder.pipe(this.assetStore);
const data = this.assetBuilder.addTextureList(this.tileStore.values());
@ -204,6 +310,7 @@ export class MaterialManager implements IMaterialManager {
arr.forEach((v, i) => {
const alias = `asset-${i}`;
this.assetStore.alias(i, alias);
this.assetDataStore.set(i, v);
const data: IMaterialAssetData = {
data: v,
identifier: i,
@ -234,34 +341,7 @@ export class MaterialManager implements IMaterialManager {
return this.tileStore.getTexture(identifier);
}
if (identifier < 10000) return null;
const texture = this.tileStore.getTexture(identifier);
if (texture) return texture;
// 如果 tileset 不存在,那么执行缓存操作
const offset = Math.floor(identifier / 10000);
const index = this.tilesetOffsetMap.get(offset);
if (isNil(index)) return null;
// 获取对应的 tileset 贴图
const tileset = this.tilesetStore.getTexture(index);
if (!tileset) return null;
// 计算图块位置
const rest = identifier - offset * 10000;
const { width, height } = tileset;
const tileWidth = Math.floor(width / 32);
const tileHeight = Math.floor(height / 32);
// 如果图块位置超出了贴图范围
if (rest > tileWidth * tileHeight) return null;
// 裁剪 tileset生成贴图
const x = rest % tileWidth;
const y = Math.floor(rest / tileWidth);
const newTexture = new Texture(tileset.source);
newTexture.clip(x * 32, y * 32, 32, 32);
// 缓存贴图
this.tileStore.addTexture(identifier, newTexture);
this.idNumMap.set(`X${identifier}`, identifier);
this.numIdMap.set(identifier, `X${identifier}`);
const data = this.assetBuilder.addTexture(newTexture);
newTexture.toAsset(data);
return newTexture;
return this.cacheTileset(identifier);
}
getRenderable(identifier: number): ITextureRenderable | null {

View File

@ -150,11 +150,13 @@ export interface IMaterialManager {
* @param source
* @param map id
* @param frames
* @param height
*/
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number
frames: number,
height: number
): Iterable<IMaterialData>;
/**
@ -175,7 +177,7 @@ export interface IMaterialManager {
addTileset(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData;
): IMaterialData | null;
/**
*
@ -223,6 +225,20 @@ export interface IMaterialManager {
*/
getImageByAlias(alias: string): ITexture | null;
/**
* tileset
* @param identifier tileset
*/
cacheTileset(identifier: number): ITexture | null;
/**
* tileset
* @param identifierList
*/
cacheTilesetList(
identifierList: Iterable<number>
): Iterable<ITexture | null>;
/**
* 使
*/

View File

@ -109,6 +109,7 @@
"75": "Material count splitted by grid does not match alias count, some material may have no alias or some alias may not map to a texture. Splitted: $1, alias: $2.",
"76": "Cannot pipe texture store when asset builder is started.",
"77": "Texture is clipped to an area of 0. Ensure that the texture contains your clip rect and clip rect's area is not zero.",
"78": "Adding tileset source must follow index order.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
}

View File

@ -72,17 +72,23 @@ export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
this.nowCtx.drawImage(source, cx, cy, cw, ch, x, y, cw, ch);
this.nowMap.set(tex, { x, y, w: cw, h: ch });
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
if (++index === max) {
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
this.nextCanvas();
}
}
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
}
close(): void {
@ -102,12 +108,12 @@ export class TextureMaxRectsStreamComposer
readonly packer: MaxRectsPacker<MaxRectsRectangle>;
private outputIndex: number = 0;
private nowTexture: ITexture;
private nowTexture!: ITexture;
private nowCanvas: HTMLCanvasElement;
private nowCtx: CanvasRenderingContext2D;
private nowMap: Map<ITexture, Readonly<IRect>>;
private nowBin: number;
private nowCanvas!: HTMLCanvasElement;
private nowCtx!: CanvasRenderingContext2D;
private nowMap!: Map<ITexture, Readonly<IRect>>;
private nowBin: number = 0;
/**
* 使 Max Rects
@ -129,16 +135,14 @@ export class TextureMaxRectsStreamComposer
options
);
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
this.nowBin = 0;
this.nowTexture = new Texture(this.nowCanvas);
this.nextCanvas();
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowCanvas.width = this.maxWidth;
this.nowCanvas.height = this.maxHeight;
this.nowMap = new Map();
this.outputIndex++;
this.nowTexture = new Texture(this.nowCanvas);
@ -176,6 +180,7 @@ export class TextureMaxRectsStreamComposer
const { x: cx, y: cy, w: cw, h: ch } = rect;
this.nowCtx.drawImage(source, cx, cy, cw, ch, v.x, v.y, cw, ch);
});
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
@ -187,7 +192,7 @@ export class TextureMaxRectsStreamComposer
}
}
this.nowBin = bins.length;
this.nowBin = bins.length - 1;
}
close(): void {