feat: 素材管理器

This commit is contained in:
unanmed 2025-10-25 12:21:03 +08:00
parent ce7be406cb
commit 618827b419
9 changed files with 761 additions and 1 deletions

View File

@ -0,0 +1,6 @@
{
"name": "@user/client-base",
"dependencies": {
"@motajs/render-asset": "workspace:*"
}
}

View File

@ -0,0 +1,7 @@
import { createMaterial } from './material';
export function create() {
createMaterial();
}
export * from './material';

View File

@ -0,0 +1,56 @@
import {
ITextureStore,
ITexture,
ITextureComposedData,
ITextureStreamComposer,
TextureMaxRectsStreamComposer
} from '@motajs/render-assets';
import { IAssetBuilder } from './types';
import { logger } from '@motajs/common';
export class AssetBuilder implements IAssetBuilder {
readonly composer: ITextureStreamComposer<void> =
new TextureMaxRectsStreamComposer(4096, 4096, 0);
private output: ITextureStore | null = null;
private started: boolean = false;
pipe(store: ITextureStore): void {
if (this.started) {
logger.warn(76);
return;
}
this.output = store;
}
addTexture(texture: ITexture): ITextureComposedData {
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);
}
}
return data;
}
addTextureList(
texture: Iterable<ITexture>
): Iterable<ITextureComposedData> {
this.started = true;
const res = [...this.composer.add(texture)];
if (this.output) {
res.forEach(v => {
if (!this.output!.getTexture(v.index)) {
this.output!.addTexture(v.index, v.texture);
}
});
}
return res;
}
close(): void {
this.composer.close();
}
}

View File

@ -0,0 +1,3 @@
export function createMaterial() {}
export * from './manager';

View File

@ -0,0 +1,325 @@
import {
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureSplitter,
ITextureStore,
SizedCanvasImageSource,
Texture,
TextureColumnAnimater,
TextureGridSplitter,
TextureRowSplitter,
TextureStore
} from '@motajs/render-assets';
import {
IBlockIdentifier,
IMaterialData,
IMaterialManager,
IIndexedIdentifier,
IMaterialAssetData,
BlockCls,
IBigImageData,
IAssetBuilder
} from './types';
import { logger } from '@motajs/common';
import { getClsByString } from './utils';
import { isNil } from 'lodash-es';
import { AssetBuilder } from './builder';
export class MaterialManager implements IMaterialManager {
readonly tileStore: ITextureStore = new TextureStore();
readonly tilesetStore: ITextureStore = new TextureStore();
readonly imageStore: ITextureStore = new TextureStore();
readonly assetStore: ITextureStore = new TextureStore();
readonly bigImageStore: ITextureStore = new TextureStore();
/** 图集信息存储 */
readonly assetDataStore: Map<number, ITextureComposedData> = new Map();
/** 大怪物数据 */
readonly bigImageData: Map<number, ITexture> = new Map();
/** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */
readonly tilesetOffsetMap: Map<number, number> = new Map();
/** 图集打包器 */
readonly assetBuilder: IAssetBuilder = new AssetBuilder();
readonly idNumMap: Map<string, number> = new Map();
readonly numIdMap: Map<number, string> = new Map();
readonly clsMap: Map<number, BlockCls> = new Map();
/** 网格切分器 */
readonly gridSplitter: TextureGridSplitter = new TextureGridSplitter();
/** 行切分器 */
readonly rowSplitter: TextureRowSplitter = new TextureRowSplitter();
/** 大怪物贴图的标识符 */
private bigImageId: number = 0;
/**
*
* @param source
* @param map id
* @param store
* @param splitter 使
* @param splitterData
* @param processTexture
*/
private addMappedSource<T>(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
store: ITextureStore,
splitter: ITextureSplitter<T>,
splitterData: T,
processTexture?: (tex: ITexture) => void
): Iterable<IMaterialData> {
const tex = new Texture(source);
const textures = [...splitter.split(tex, splitterData)];
if (textures.length !== map.length) {
logger.warn(75, textures.length.toString(), map.length.toString());
}
const res: IMaterialData[] = textures.map((v, i) => {
const { id, num, cls } = map[i];
store.addTexture(num, v);
store.alias(num, id);
this.clsMap.set(num, getClsByString(cls));
processTexture?.(v);
const data: IMaterialData = {
store,
texture: v,
identifier: num,
alias: id
};
return data;
});
return res;
}
addGrid(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>
): Iterable<IMaterialData> {
return this.addMappedSource(
source,
map,
this.tileStore,
this.gridSplitter,
[32, 32]
);
}
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number
): Iterable<IMaterialData> {
return this.addMappedSource(
source,
map,
this.tileStore,
this.rowSplitter,
32,
(tex: ITexture<number>) => {
tex.animated(new TextureColumnAnimater(), frames);
}
);
}
addAutotile(
source: SizedCanvasImageSource,
identifier: IBlockIdentifier
): IMaterialData {
const texture = new Texture(source);
this.tileStore.addTexture(identifier.num, texture);
this.tileStore.alias(identifier.num, identifier.id);
this.clsMap.set(identifier.num, BlockCls.Autotile);
const data: IMaterialData = {
store: this.tileStore,
texture,
identifier: identifier.num,
alias: identifier.id
};
return data;
}
addTileset(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData {
const tex = new Texture(source);
this.tilesetStore.addTexture(identifier.index, tex);
this.tilesetStore.alias(identifier.index, identifier.alias);
const data: IMaterialData = {
store: this.tilesetStore,
texture: tex,
identifier: identifier.index,
alias: identifier.alias
};
return data;
}
addImage(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData {
const texture = new Texture(source);
this.imageStore.addTexture(identifier.index, texture);
this.imageStore.alias(identifier.index, identifier.alias);
const data: IMaterialData = {
store: this.imageStore,
texture,
identifier: identifier.index,
alias: identifier.alias
};
return data;
}
getTile(identifier: number): ITexture | null {
return this.tileStore.getTexture(identifier);
}
getTileset(identifier: number): ITexture | null {
return this.tilesetStore.getTexture(identifier);
}
getImage(identifier: number): ITexture | null {
return this.imageStore.getTexture(identifier);
}
getTileByAlias(alias: string): ITexture | null {
return this.tileStore.fromAlias(alias);
}
getTilesetByAlias(alias: string): ITexture | null {
return this.tilesetStore.fromAlias(alias);
}
getImageByAlias(alias: string): ITexture | null {
return this.imageStore.fromAlias(alias);
}
buildAssets(): Iterable<IMaterialAssetData> {
this.assetBuilder.pipe(this.assetStore);
const data = this.assetBuilder.addTextureList(this.tileStore.values());
const arr = [...data];
const res: IMaterialAssetData[] = [];
arr.forEach((v, i) => {
const alias = `asset-${i}`;
this.assetStore.alias(i, alias);
const data: IMaterialAssetData = {
data: v,
identifier: i,
alias,
store: this.assetStore
};
for (const tex of v.assetMap.keys()) {
tex.toAsset(v);
}
res.push(data);
});
return res;
}
getAsset(identifier: number): ITextureComposedData | null {
return this.assetDataStore.get(identifier) ?? null;
}
getAssetByAlias(alias: string): ITextureComposedData | null {
const id = this.assetStore.identifierOf(alias);
if (isNil(id)) return null;
return this.assetDataStore.get(id) ?? null;
}
private getTextureOf(identifier: number, cls: BlockCls): ITexture | null {
if (cls === BlockCls.Unknown) return null;
if (cls !== BlockCls.Tileset) {
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;
}
getRenderable(identifier: number): ITextureRenderable | null {
const cls = this.clsMap.get(identifier);
if (isNil(cls)) return null;
const texture = this.getTextureOf(identifier, cls);
if (!texture) return null;
return texture.static();
}
getRenderableByAlias(alias: string): ITextureRenderable | null {
const identifier = this.idNumMap.get(alias);
if (isNil(identifier)) return null;
return this.getRenderable(identifier);
}
getBlockCls(identifier: number): BlockCls {
return this.clsMap.get(identifier) ?? BlockCls.Unknown;
}
getBlockClsByAlias(alias: string): BlockCls {
const id = this.idNumMap.get(alias);
if (isNil(id)) return BlockCls.Unknown;
return this.clsMap.get(id) ?? BlockCls.Unknown;
}
getIdentifierByAlias(alias: string): number | undefined {
return this.idNumMap.get(alias);
}
getAliasByIdentifier(identifier: number): string | undefined {
return this.numIdMap.get(identifier);
}
setBigImage(identifier: number, image: ITexture): IBigImageData {
const bigImageId = this.bigImageId++;
this.bigImageStore.addTexture(bigImageId, image);
this.bigImageData.set(identifier, image);
const data: IBigImageData = {
identifier: bigImageId,
store: this.bigImageStore
};
return data;
}
isBigImage(identifier: number): boolean {
return this.bigImageData.has(identifier);
}
getBigImage(identifier: number): ITexture | null {
return this.bigImageData.get(identifier) ?? null;
}
getBigImageByAlias(alias: string): ITexture | null {
const identifier = this.idNumMap.get(alias);
if (isNil(identifier)) return null;
return this.bigImageData.get(identifier) ?? null;
}
}
export const materials = new MaterialManager();

View File

@ -0,0 +1,330 @@
import {
IRect,
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureStore,
SizedCanvasImageSource
} from '@motajs/render-assets';
export const enum BlockCls {
Unknown,
Terrains,
Animates,
Enemys,
Npcs,
Items,
Enemy48,
Npc48,
Tileset,
Autotile
}
export interface IMaterialData {
/** 此素材的贴图对象存入了哪个贴图存储对象 */
readonly store: ITextureStore;
/** 贴图对象 */
readonly texture: ITexture;
/** 此素材的贴图对象的数字 id一般对应到图块数字 */
readonly identifier: number;
/** 此素材的贴图对象的字符串别名,一般对应到图块 id */
readonly alias?: string;
}
export interface IBlockIdentifier {
/** 图块 id */
readonly id: string;
/** 图块数字 */
readonly num: number;
/** 图块类型 */
readonly cls: Cls;
}
export interface IIndexedIdentifier {
/** 标识符索引 */
readonly index: number;
/** 标识符别名 */
readonly alias: string;
}
export interface IMaterialAssetData {
/** 图集数据 */
readonly data: ITextureComposedData;
/** 贴图的标识符 */
readonly identifier: number;
/** 贴图的别名 */
readonly alias: string;
/** 贴图所属的存储对象 */
readonly store: ITextureStore;
}
export interface IAutotileConnection {
/** 连接方式,上方连接是第一位,顺时针旋转位次依次升高 */
readonly connection: number;
/** 中心自动元件对应的图块数字 */
readonly center: number;
}
export interface IAutotileRenderable {
/** 自动元件的图像源 */
readonly source: SizedCanvasImageSource;
/** 渲染的矩形范围 */
readonly rects: Readonly<IRect>[];
}
export interface IBigImageData {
/** 大怪物贴图在 store 中的标识符 */
readonly identifier: number;
/** 存储大怪物贴图的存储对象 */
readonly store: ITextureStore;
}
export interface IAutotileProcessor {
/** 该自动元件处理器使用的素材管理器 */
readonly manager: IMaterialManager;
/**
*
* @param autotile
* @param parent
*/
setParent(autotile: number, parent: number): void;
/**
*
* @param array
* @param index
* @param edge
*/
connect(
array: Float32Array,
index: number,
edge: number
): IAutotileConnection;
/**
*
* @param autotile
* @param connection
*/
render(autotile: number, connection: number): IAutotileRenderable;
/**
*
* @param renderable
* @param connection
*/
fromRenderable(
renderable: ITextureRenderable,
connection: number
): IAutotileRenderable;
}
export interface IMaterialManager {
/** 贴图存储,把 terrains 等内容单独分开存储 */
readonly tileStore: ITextureStore;
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
readonly tilesetStore: ITextureStore;
/** 存储注册的图像的存储对象 */
readonly imageStore: ITextureStore;
/** 图集存储,将常用贴图存入其中 */
readonly assetStore: ITextureStore;
/** bigImage 存储,存储大怪物数据 */
readonly bigImageStore: ITextureStore;
/** 图块类型映射 */
readonly clsMap: Map<number, BlockCls>;
/**
* terrains items
* @param source
* @param map id
*/
addGrid(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>
): Iterable<IMaterialData>;
/**
* animates enemys npcs enemy48 npc48
* @param source
* @param map id
* @param frames
*/
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number
): Iterable<IMaterialData>;
/**
*
* @param source
* @param identifier id
*/
addAutotile(
source: SizedCanvasImageSource,
identifier: IBlockIdentifier
): IMaterialData;
/**
* tileset
* @param source
* @param alias tileset tilesets
*/
addTileset(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData;
/**
*
* @param source
* @param identifier images
*/
addImage(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData;
/**
*
* @param identifier
*/
getTile(identifier: number): ITexture | null;
/**
*
* @param identifier
*/
getTileset(identifier: number): ITexture | null;
/**
*
* @param identifier
*/
getImage(identifier: number): ITexture | null;
/**
* id
* @param alias id
*/
getTileByAlias(alias: string): ITexture | null;
/**
*
* @param alias
*/
getTilesetByAlias(alias: string): ITexture | null;
/**
*
* @param alias
*/
getImageByAlias(alias: string): ITexture | null;
/**
* 使
*/
buildAssets(): Iterable<IMaterialAssetData>;
/**
*
* @param identifier
*/
getAsset(identifier: number): ITextureComposedData | null;
/**
*
* @param alias
*/
getAssetByAlias(alias: string): ITextureComposedData | null;
/**
*
* @param identifier
*/
getRenderable(identifier: number): ITextureRenderable | null;
/**
*
* @param alias id
*/
getRenderableByAlias(alias: string): ITextureRenderable | null;
/**
*
* @param identifier
*/
getBlockCls(identifier: number): BlockCls;
/**
*
* @param alias id
*/
getBlockClsByAlias(alias: string): BlockCls;
/**
*
* @param alias id
*/
getIdentifierByAlias(alias: string): number | undefined;
/**
* id
* @param identifier
*/
getAliasByIdentifier(identifier: number): string | undefined;
/**
* `bigImage`
* @param identifier
* @param image `bigImage`
*/
setBigImage(identifier: number, image: ITexture): IBigImageData;
/**
* `bigImage`
* @param identifier
*/
isBigImage(identifier: number): boolean;
/**
* `bigImage`
* @param identifier
*/
getBigImage(identifier: number): ITexture | null;
/**
* `bigImage`
* @param alias id
*/
getBigImageByAlias(alias: string): ITexture | null;
}
export interface IAssetBuilder {
/**
*
* @param store
*/
pipe(store: ITextureStore): void;
/**
*
* @param texture
* @returns
*/
addTexture(texture: ITexture): ITextureComposedData;
/**
*
* @param texture
* @returns 使
*/
addTextureList(texture: Iterable<ITexture>): Iterable<ITextureComposedData>;
/**
*
*/
close(): void;
}

View File

@ -0,0 +1,26 @@
import { BlockCls } from './types';
export function getClsByString(cls: Cls): BlockCls {
switch (cls) {
case 'terrains':
return BlockCls.Terrains;
case 'animates':
return BlockCls.Animates;
case 'autotile':
return BlockCls.Autotile;
case 'enemys':
return BlockCls.Enemys;
case 'items':
return BlockCls.Items;
case 'npcs':
return BlockCls.Npcs;
case 'npc48':
return BlockCls.Npc48;
case 'enemy48':
return BlockCls.Enemy48;
case 'tileset':
return BlockCls.Tileset;
default:
return BlockCls.Unknown;
}
}

View File

@ -5,6 +5,7 @@
"@motajs/client-base": "workspace:*",
"@motajs/common": "workspace:*",
"@motajs/render": "workspace:*",
"@motajs/render-assets": "workspace:*",
"@motajs/render-core": "workspace:*",
"@motajs/render-elements": "workspace:*",
"@motajs/render-style": "workspace:*",
@ -17,6 +18,7 @@
"@motajs/legacy-data": "workspace:*",
"@motajs/legacy-ui": "workspace:*",
"@motajs/legacy-system": "workspace:*",
"@user/client-base": "workspace:*",
"@user/client-modules": "workspace:*",
"@user/legacy-plugin-client": "workspace:*"
}

View File

@ -5,6 +5,7 @@ import * as LegacyClient from '@motajs/legacy-client';
import * as LegacySystem from '@motajs/legacy-system';
import * as LegacyUI from '@motajs/legacy-ui';
import * as Render from '@motajs/render';
import * as RenderAssets from '@motajs/render-assets';
import * as RenderCore from '@motajs/render-core';
import * as RenderElements from '@motajs/render-elements';
import * as RenderStyle from '@motajs/render-style';
@ -12,6 +13,7 @@ import * as RenderVue from '@motajs/render-vue';
import * as System from '@motajs/system';
import * as SystemAction from '@motajs/system-action';
import * as SystemUI from '@motajs/system-ui';
import * as UserClientBase from '@user/client-base';
import * as ClientModules from '@user/client-modules';
import * as LegacyPluginClient from '@user/legacy-plugin-client';
import * as MutateAnimate from 'mutate-animate';
@ -28,6 +30,7 @@ export function create() {
Mota.register('@motajs/legacy-system', LegacySystem);
Mota.register('@motajs/legacy-ui', LegacyUI);
Mota.register('@motajs/render', Render);
Mota.register('@motajs/render-assets', RenderAssets);
Mota.register('@motajs/render-core', RenderCore);
Mota.register('@motajs/render-elements', RenderElements);
Mota.register('@motajs/render-style', RenderStyle);
@ -35,6 +38,7 @@ export function create() {
Mota.register('@motajs/system', System);
Mota.register('@motajs/system-action', SystemAction);
Mota.register('@motajs/system-ui', SystemUI);
Mota.register('@user/client-base', UserClientBase);
Mota.register('@user/client-modules', ClientModules);
Mota.register('@user/legacy-plugin-client', LegacyPluginClient);
Mota.register('MutateAnimate', MutateAnimate);
@ -47,6 +51,7 @@ export function create() {
async function createModule() {
LegacyUI.create();
ClientModules.create();
UserClientBase.create();
await import('ant-design-vue/dist/antd.dark.css');
main.renderLoaded = true;