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,7 +18,8 @@
"@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;