diff --git a/packages-user/client-base/package.json b/packages-user/client-base/package.json index 4dfed65..44b79b9 100644 --- a/packages-user/client-base/package.json +++ b/packages-user/client-base/package.json @@ -1,6 +1,7 @@ { "name": "@user/client-base", "dependencies": { - "@motajs/render-asset": "workspace:*" + "@motajs/render-asset": "workspace:*", + "@motajs/client-base": "workspace:*" } } diff --git a/packages-user/client-base/src/material/asset.ts b/packages-user/client-base/src/material/asset.ts index f1242be..1bbcf50 100644 --- a/packages-user/client-base/src/material/asset.ts +++ b/packages-user/client-base/src/material/asset.ts @@ -3,7 +3,7 @@ import { IMaterialAsset } from './types'; export class MaterialAsset implements IMaterialAsset { /** 标记列表 */ - private readonly marks: Map = new Map(); + private readonly marks: WeakMap = new WeakMap(); /** 脏标记,所有值小于此标记的都视为需要更新 */ private dirtyFlag: number = 0; @@ -27,4 +27,8 @@ export class MaterialAsset implements IMaterialAsset { const value = this.marks.get(mark) ?? -1; return value < this.dirtyFlag; } + + hasMark(symbol: symbol): boolean { + return this.marks.has(symbol); + } } diff --git a/packages-user/client-base/src/material/autotile.ts b/packages-user/client-base/src/material/autotile.ts index 0de4e3b..f7dbbdd 100644 --- a/packages-user/client-base/src/material/autotile.ts +++ b/packages-user/client-base/src/material/autotile.ts @@ -1,13 +1,18 @@ -import { IRect, ITexture, ITextureRenderable } from '@motajs/render-assets'; import { + IRect, + ITextureRenderable, + SizedCanvasImageSource +} from '@motajs/render-assets'; +import { + AutotileConnection, AutotileType, BlockCls, IAutotileConnection, IAutotileProcessor, - IAutotileRenderable, + IMaterialFramedData, IMaterialManager } from './types'; -import { logger } from '@motajs/common'; +import { isNil } from 'lodash-es'; interface ConnectedAutotile { readonly lt: Readonly; @@ -16,19 +21,23 @@ interface ConnectedAutotile { readonly lb: Readonly; } +export interface IAutotileData { + /** 图像源 */ + readonly source: SizedCanvasImageSource; + /** 自动元件帧数 */ + readonly frames: number; +} + /** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */ const connectionMap3x4 = new Map(); /** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */ const connectionMap2x3 = new Map(); -/** 3x4 自动元件各方向连接矩形映射 */ +/** 3x4 自动元件各方向连接的矩形映射 */ const rectMap3x4 = new Map(); /** 2x3 自动元件各方向连接的矩形映射 */ const rectMap2x3 = new Map(); - -interface AutotileFrameList { - type: AutotileType; - rects: Readonly[]; -} +/** 不重复连接映射,用于平铺自动元件,一共 48 种 */ +const distinctConnectionMap = new Map(); export class AutotileProcessor implements IAutotileProcessor { /** 自动元件父子关系映射,子元件 -> 父元件 */ @@ -46,7 +55,7 @@ export class AutotileProcessor implements IAutotileProcessor { return ensure; } - setParent(autotile: number, parent: number): void { + setConnection(autotile: number, parent: number): void { this.parentMap.set(autotile, parent); const child = this.ensureChildSet(parent); child.add(autotile); @@ -116,8 +125,14 @@ export class AutotileProcessor implements IAutotileProcessor { index: number, width: number ): IAutotileConnection { - let res: number = this.connectEdge(array.length, index, width); const block = array[index]; + if (block === 0) { + return { + connection: 0, + center: 0 + }; + } + let res: number = this.connectEdge(array.length, index, width); const childList = this.childMap.get(block); // 最高位表示左上,低位依次顺时针旋转 @@ -132,7 +147,7 @@ export class AutotileProcessor implements IAutotileProcessor { // Benchmark https://www.measurethat.net/Benchmarks/Show/35271/0/convert-boolean-to-number - if (!childList) { + if (!childList || childList.size === 0) { // 不包含子元件,那么直接跟相同的连接 res |= +(a0 === block) | @@ -161,157 +176,129 @@ export class AutotileProcessor implements IAutotileProcessor { }; } - render( + updateConnectionFor( + connection: number, + center: number, + target: number, + direction: AutotileConnection + ): number { + const childList = this.childMap.get(center); + if (!childList || !childList.has(target)) { + return connection & ~direction; + } else { + return connection | direction; + } + } + + /** + * 检查贴图是否是一个自动元件 + * @param tile 贴图数据 + */ + private checkAutotile(tile: IMaterialFramedData) { + if (tile.cls !== BlockCls.Autotile) return false; + const { texture, frames } = tile; + if (texture.width !== 96 * frames) return false; + if (texture.height === 128 || texture.height === 144) return true; + else return false; + } + + render(autotile: number, connection: number): ITextureRenderable | null { + const tile = this.manager.getTile(autotile); + if (!tile) return null; + if (!this.checkAutotile(tile)) return null; + return this.renderWithoutCheck(tile, connection); + } + + renderWith( + tile: IMaterialFramedData, + connection: number + ): ITextureRenderable | null { + if (!this.checkAutotile(tile)) return null; + return this.renderWithoutCheck(tile, connection); + } + + renderWithoutCheck( + tile: IMaterialFramedData, + connection: number + ): ITextureRenderable | null { + const { texture } = tile; + const size = texture.height === 128 ? 32 : 48; + const index = distinctConnectionMap.get(connection); + if (isNil(index)) return null; + return { + source: texture.source, + rect: { x: 0, y: size * index, w: size, h: size } + }; + } + + *renderAnimated( autotile: number, connection: number - ): Generator | null { - const cls = this.manager.getBlockCls(autotile); - if (cls !== BlockCls.Autotile) return null; - const tile = this.manager.getTile(autotile)!; - return this.fromStaticRenderable(tile.static(), connection); + ): Generator { + const tile = this.manager.getTile(autotile); + if (!tile) return; + yield* this.renderAnimatedWith(tile, connection); } - /** - * 根据静态可渲染对象获取自动元件的帧列表 - * @param renderable 静态可渲染对象 - */ - private getStaticRectList( - renderable: ITextureRenderable - ): AutotileFrameList { - const { x, y, w, h } = renderable.rect; - const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3; - if (w === 96) { - return { - type, - rects: [renderable.rect] - }; - } else { - return { - type, - rects: [ - { x: x + 0, y, w, h }, - { x: x + 96, y, w, h }, - { x: x + 192, y, w, h }, - { x: x + 288, y, w, h } - ] - }; - } - } - - /** - * 对自动元件连接执行偏移操作,偏移至自动元件在图像源中的所在矩形范围 - * @param ox 横向偏移量 - * @param oy 纵向偏移量 - * @param connection 自动元件连接信息 - */ - private getConnectedRect( - ox: number, - oy: number, - connection: ConnectedAutotile - ): ConnectedAutotile { - const { lt, rt, rb, lb } = connection; - - return { - lt: { x: ox + lt.x, y: oy + lt.y, w: lt.w, h: lt.h }, - rt: { x: ox + rt.x, y: oy + rt.y, w: rt.w, h: rt.h }, - rb: { x: ox + rb.x, y: oy + rb.y, w: rb.w, h: rb.h }, - lb: { x: ox + lb.x, y: oy + lb.y, w: lb.w, h: lb.h } - }; - } - - *fromStaticRenderable( - renderable: ITextureRenderable, + *renderAnimatedWith( + tile: IMaterialFramedData, connection: number - ): Generator | null { - const { type, rects } = this.getStaticRectList(renderable); - const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3; - const data = map.get(connection); - if (!data) { - logger.error(27); - return null; - } - if (rects.length === 1) { - const { x, y } = rects[0]; - const connected = this.getConnectedRect(x, y, data); - if (!connected) return null; - const res: IAutotileRenderable = { - source: renderable.source, - lt: connected.lt, - rt: connected.rt, - rb: connected.rb, - lb: connected.lb + ): Generator { + if (!this.checkAutotile(tile)) return; + const { texture, frames } = tile; + const size = texture.height === 128 ? 32 : 48; + const index = distinctConnectionMap.get(connection); + if (isNil(index)) return; + for (let i = 0; i < frames; i++) { + yield { + source: texture.source, + rect: { x: i * size, y: size * index, w: size, h: size } }; - yield res; - } else { - for (const { x, y } of rects) { - const connected = this.getConnectedRect(x, y, data); - if (!connected) return null; - const res: IAutotileRenderable = { - source: renderable.source, - lt: connected.lt, - rt: connected.rt, - rb: connected.rb, - lb: connected.lb - }; - yield res; + } + } + + /** + * 将自动元件图片展平,平铺存储 48 种样式,此时可以只通过一次绘制来绘制出自动元件,不需要四次绘制 + * @param image 原始自动元件图片 + */ + static flatten(image: IAutotileData): SizedCanvasImageSource | null { + const { source, frames } = image; + if (source.width !== frames * 96) return null; + if (source.height !== 128 && source.height !== 144) return null; + const type = + source.height === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3; + const size = type === AutotileType.Big3x4 ? 32 : 48; + const width = frames * size; + const height = 48 * size; + // 画到画布上 + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + const half = size / 2; + const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3; + const used = new Set(); + // 遍历每个组合 + distinctConnectionMap.forEach((index, conn) => { + if (used.has(conn)) return; + used.add(conn); + const { lt, rt, rb, lb } = map.get(conn)!; + const y = index * size; + for (let i = 0; i < frames; i++) { + const x = i * size; + // prettier-ignore + ctx.drawImage(source, lt.x + i * 96, lt.y, lt.w, lt.h, x, y, half, half); + // prettier-ignore + ctx.drawImage(source, rt.x + i * 96, rt.y, rt.w, rt.h, x + half, y, half, half); + // prettier-ignore + ctx.drawImage(source, rb.x + i * 96, rb.y, rb.w, rb.h, x + half, y + half, half, half); + // prettier-ignore + ctx.drawImage(source, lb.x + i * 96, lb.y, lb.w, lb.h, x, y + half, half, half); } - } - } + }); - fromAnimatedRenderable( - renderable: ITextureRenderable, - connection: number - ): IAutotileRenderable | null { - const { x, y, h } = renderable.rect; - const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3; - const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3; - const data = map.get(connection); - if (!data) { - logger.error(27); - return null; - } - const connected = this.getConnectedRect(x, y, data); - if (!connected) return null; - const res: IAutotileRenderable = { - source: renderable.source, - lt: connected.lt, - rt: connected.rt, - rb: connected.rb, - lb: connected.lb - }; - return res; - } - - *fromAnimatedGenerator( - texture: ITexture, - generator: Generator | null, - connection: number - ): Generator | null { - if (!generator) return null; - const h = texture.height; - const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3; - const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3; - const data = map.get(connection); - if (!data) { - logger.error(27); - return null; - } - while (true) { - const value = generator.next(); - if (value.done) break; - const renderable = value.value; - const { x, y } = renderable.rect; - const connected = this.getConnectedRect(x, y, data); - if (!connected) return null; - const res: IAutotileRenderable = { - source: renderable.source, - lt: connected.lt, - rt: connected.rt, - rb: connected.rb, - lb: connected.lb - }; - yield res; - } + return canvas; } } @@ -444,4 +431,23 @@ export function createAutotile() { lb: { x: lbx, y: lby, w: 24, h: 24 } }); }); + const usedRect: [number, number, number, number][] = []; + let flag = 0; + // 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次 + connectionMap2x3.forEach((conn, num) => { + const index = usedRect.findIndex( + used => + used[0] === conn[0] && + used[1] === conn[1] && + used[2] === conn[2] && + used[3] === conn[3] + ); + if (index === -1) { + distinctConnectionMap.set(num, flag); + usedRect.push(conn.slice() as [number, number, number, number]); + flag++; + } else { + distinctConnectionMap.set(num, index); + } + }); } diff --git a/packages-user/client-base/src/material/fallback.ts b/packages-user/client-base/src/material/fallback.ts index 030c4a8..7df806c 100644 --- a/packages-user/client-base/src/material/fallback.ts +++ b/packages-user/client-base/src/material/fallback.ts @@ -31,6 +31,16 @@ function addTileset(set: Set, map?: readonly (readonly number[])[]) { }); } +function addAutotile(set: Set, map?: readonly (readonly number[])[]) { + if (!map) return; + map.forEach(line => { + line.forEach(v => { + const id = core.maps.blocksInfo[v as keyof NumberToId]; + if (id.cls === 'autotile') set.add(v); + }); + }); +} + /** * 兼容旧版加载 */ @@ -57,11 +67,11 @@ export function fallbackLoad() { 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); + materials.addRowAnimate(images.animates, animates, 32); + materials.addRowAnimate(images.enemys, enemys, 32); + materials.addRowAnimate(images.npcs, npcs, 32); + materials.addRowAnimate(images.enemy48, enemy48, 48); + materials.addRowAnimate(images.npc48, npc48, 48); // Autotile for (const key of Object.keys(icons.autotile)) { @@ -91,10 +101,9 @@ export function fallbackLoad() { materials.addImage(img, { index: i, alias: v }); }); - materials.buildAssets(); - // 地图上出现过的 tileset const tilesetSet = new Set(); + const autotileSet = new Set(); core.floorIds.forEach(v => { const floor = core.floors[v]; addTileset(tilesetSet, floor.bgmap); @@ -102,7 +111,14 @@ export function fallbackLoad() { addTileset(tilesetSet, floor.map); addTileset(tilesetSet, floor.fgmap); addTileset(tilesetSet, floor.fg2map); + addAutotile(autotileSet, floor.bgmap); + addAutotile(autotileSet, floor.bg2map); + addAutotile(autotileSet, floor.map); + addAutotile(autotileSet, floor.fgmap); + addAutotile(autotileSet, floor.fg2map); }); - materials.cacheTilesetList(tilesetSet); + materials.cacheTilesetList(tilesetSet.union(autotileSet)); + + materials.buildAssets(); } diff --git a/packages-user/client-base/src/material/index.ts b/packages-user/client-base/src/material/index.ts index 0e8c3b5..aadd600 100644 --- a/packages-user/client-base/src/material/index.ts +++ b/packages-user/client-base/src/material/index.ts @@ -1,7 +1,9 @@ import { loading } from '@user/data-base'; import { fallbackLoad } from './fallback'; +import { createAutotile } from './autotile'; export function createMaterial() { + createAutotile(); loading.once('loaded', () => { fallbackLoad(); }); diff --git a/packages-user/client-base/src/material/manager.ts b/packages-user/client-base/src/material/manager.ts index 30fc2f1..e8ba3ac 100644 --- a/packages-user/client-base/src/material/manager.ts +++ b/packages-user/client-base/src/material/manager.ts @@ -6,7 +6,6 @@ import { ITextureStore, SizedCanvasImageSource, Texture, - TextureColumnAnimater, TextureGridSplitter, TextureRowSplitter, TextureStore @@ -18,15 +17,17 @@ import { IIndexedIdentifier, IMaterialAssetData, BlockCls, - IBigImageData, + IBigImageReturn, IAssetBuilder, - IMaterialAsset + IMaterialAsset, + IMaterialFramedData } from './types'; import { logger } from '@motajs/common'; -import { getClsByString } from './utils'; +import { getClsByString, getTextureFrame } from './utils'; import { isNil } from 'lodash-es'; import { AssetBuilder } from './builder'; import { MaterialAsset } from './asset'; +import { AutotileProcessor } from './autotile'; export class MaterialManager implements IMaterialManager { readonly tileStore: ITextureStore = new TextureStore(); @@ -35,18 +36,26 @@ export class MaterialManager implements IMaterialManager { readonly assetStore: ITextureStore = new TextureStore(); readonly bigImageStore: ITextureStore = new TextureStore(); + /** 自动元件图像源映射 */ + readonly autotileSource: Map = new Map(); + /** 图集信息存储 */ readonly assetDataStore: Map = new Map(); + /** 贴图到图集索引的映射 */ + readonly assetMap: Map = new Map(); /** 大怪物数据 */ - readonly bigImageData: Map = new Map(); + readonly bigImageData: Map = new Map(); /** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */ readonly tilesetOffsetMap: Map = new Map(); /** 图集打包器 */ readonly assetBuilder: IAssetBuilder = new AssetBuilder(); + /** 图块 id 到图块数字的映射 */ readonly idNumMap: Map = new Map(); + /** 图块数字到图块 id 的映射 */ readonly numIdMap: Map = new Map(); + /** 图块数字到图块类型的映射 */ readonly clsMap: Map = new Map(); /** 网格切分器 */ @@ -63,9 +72,6 @@ export class MaterialManager implements IMaterialManager { /** 是否已经构建过素材 */ private built: boolean = false; - /** 标记列表 */ - private readonly markList: symbol[] = []; - constructor() { this.assetBuilder.pipe(this.assetStore); } @@ -125,7 +131,6 @@ export class MaterialManager implements IMaterialManager { addRowAnimate( source: SizedCanvasImageSource, map: ArrayLike, - frames: number, height: number ): Iterable { return this.addMappedSource( @@ -133,18 +138,18 @@ export class MaterialManager implements IMaterialManager { map, this.tileStore, this.rowSplitter, - height, - (tex: ITexture) => { - tex.animated(new TextureColumnAnimater(), frames); - } + height ); } addAutotile( source: SizedCanvasImageSource, identifier: IBlockIdentifier - ): IMaterialData { - const texture = new Texture(source); + ): IMaterialData | null { + const frames = source.width === 96 ? 1 : 4; + const flattened = AutotileProcessor.flatten({ source, frames }); + if (!flattened) return null; + const texture = new Texture(flattened); this.tileStore.addTexture(identifier.num, texture); this.tileStore.alias(identifier.num, identifier.id); this.clsMap.set(identifier.num, BlockCls.Autotile); @@ -177,6 +182,7 @@ export class MaterialManager implements IMaterialManager { logger.warn(78); return null; } + // 一个 tileset 可能不止 10000 个图块,需要计算偏移 const width = Math.floor(source.width / 32); const height = Math.floor(source.height / 32); const count = width * height; @@ -213,11 +219,26 @@ export class MaterialManager implements IMaterialManager { return data; } - getTile(identifier: number): ITexture | null { + getTile(identifier: number): IMaterialFramedData | null { if (identifier < 10000) { - return this.tileStore.getTexture(identifier); + const texture = this.tileStore.getTexture(identifier); + if (!texture) return null; + const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown; + return { + texture, + cls, + offset: 32, + frames: getTextureFrame(cls, texture) + }; } else { - return this.cacheTileset(identifier); + const texture = this.cacheTileset(identifier); + if (!texture) return null; + return { + texture, + cls: BlockCls.Tileset, + offset: 32, + frames: 1 + }; } } @@ -229,11 +250,13 @@ export class MaterialManager implements IMaterialManager { return this.imageStore.getTexture(identifier); } - getTileByAlias(alias: string): ITexture | null { + getTileByAlias(alias: string): IMaterialFramedData | null { if (/X\d{5,}/.test(alias)) { - return this.cacheTileset(parseInt(alias.slice(1))); + return this.getTile(parseInt(alias.slice(1))); } else { - return this.tileStore.fromAlias(alias); + const identifier = this.tileStore.identifierOf(alias); + if (isNil(identifier)) return null; + return this.getTile(identifier); } } @@ -282,11 +305,33 @@ export class MaterialManager implements IMaterialManager { } else { // 如果有新图集,需要添加 const alias = `asset-${data.index}`; + const newAsset = new MaterialAsset(data); + newAsset.dirty(); this.assetStore.alias(data.index, alias); - this.assetDataStore.set(data.index, new MaterialAsset(data)); + this.assetDataStore.set(data.index, newAsset); } } + /** + * 将指定的贴图列表转换至指定的图集数据中 + * @param composedData 组合数据 + * @param textures 贴图列表 + */ + private cacheToAsset( + composedData: ITextureComposedData[], + textures: ITexture[] + ) { + textures.forEach(tex => { + const assetData = composedData.find(v => v.assetMap.has(tex)); + if (!assetData) { + logger.error(38); + return; + } + tex.toAsset(assetData); + }); + composedData.forEach(v => this.checkAssetDirty(v)); + } + cacheTileset(identifier: number): ITexture | null { const newTexture = this.getTilesetOwnTexture(identifier); if (!newTexture) return null; @@ -315,16 +360,59 @@ export class MaterialManager implements IMaterialManager { this.numIdMap.set(v, `X${v}`); }); - const set = new Set(toAdd); + const data = this.assetBuilder.addTextureList(toAdd); + const res = [...data]; + this.cacheToAsset(res, toAdd); + + return toAdd; + } + + /** + * 获取自动元件展开后的图片,如果图片不存在,或是已经展开并存储至了 `tileStore`,那么返回 `null` + * @param identifier 自动元件标识符 + */ + private getFlattenedAutotile( + identifier: number + ): SizedCanvasImageSource | null { + const cls = this.clsMap.get(identifier); + if (cls !== BlockCls.Autotile) return null; + if (this.tileStore.getTexture(identifier)) return null; + const source = this.autotileSource.get(identifier); + if (!source) return null; + const frames = source.width === 96 ? 1 : 4; + const flattened = AutotileProcessor.flatten({ source, frames }); + if (!flattened) return null; + return flattened; + } + + cacheAutotile(identifier: number): ITexture | null { + const flattened = this.getFlattenedAutotile(identifier); + if (!flattened) return null; + const tex = new Texture(flattened); + this.tileStore.addTexture(identifier, tex); + const data = this.assetBuilder.addTexture(tex); + tex.toAsset(data); + this.checkAssetDirty(data); + return tex; + } + + cacheAutotileList( + identifierList: Iterable + ): Iterable { + const arr = [...identifierList]; + const toAdd: ITexture[] = []; + + arr.forEach(v => { + const flattened = this.getFlattenedAutotile(v); + if (!flattened) return; + const tex = new Texture(flattened); + this.tileStore.addTexture(v, tex); + toAdd.push(tex); + }); const data = this.assetBuilder.addTextureList(toAdd); const res = [...data]; - res.forEach(data => { - data.assetMap.keys().forEach(tex => { - if (set.has(tex)) tex.toAsset(data); - }); - this.checkAssetDirty(data); - }); + this.cacheToAsset(res, toAdd); return toAdd; } @@ -380,7 +468,7 @@ export class MaterialManager implements IMaterialManager { if (isNil(cls)) return null; const texture = this.getTextureOf(identifier, cls); if (!texture) return null; - return texture.static(); + return texture.render(); } getRenderableByAlias(alias: string): ITextureRenderable | null { @@ -407,11 +495,22 @@ export class MaterialManager implements IMaterialManager { return this.numIdMap.get(identifier); } - setBigImage(identifier: number, image: ITexture): IBigImageData { + setBigImage( + identifier: number, + image: ITexture, + frames: number + ): IBigImageReturn { const bigImageId = this.bigImageId++; this.bigImageStore.addTexture(bigImageId, image); - this.bigImageData.set(identifier, image); - const data: IBigImageData = { + const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown; + const store: IMaterialFramedData = { + texture: image, + cls, + offset: image.width / 4, + frames + }; + this.bigImageData.set(identifier, store); + const data: IBigImageReturn = { identifier: bigImageId, store: this.bigImageStore }; @@ -422,23 +521,27 @@ export class MaterialManager implements IMaterialManager { return this.bigImageData.has(identifier); } - getBigImage(identifier: number): ITexture | null { + getBigImage(identifier: number): IMaterialFramedData | null { return this.bigImageData.get(identifier) ?? null; } - getBigImageByAlias(alias: string): ITexture | null { + getBigImageByAlias(alias: string): IMaterialFramedData | null { const identifier = this.idNumMap.get(alias); if (isNil(identifier)) return null; return this.bigImageData.get(identifier) ?? null; } - getIfBigImage(identifier: number): ITexture | null { + getIfBigImage(identifier: number): IMaterialFramedData | null { const bigImage = this.bigImageData.get(identifier) ?? null; if (bigImage) return bigImage; - if (identifier < 10000) { - return this.tileStore.getTexture(identifier); - } else { - return this.cacheTileset(identifier); - } + else return this.getTile(identifier); + } + + assetContainsTexture(texture: ITexture): boolean { + return this.assetMap.has(texture); + } + + getTextureAsset(texture: ITexture): number | undefined { + return this.assetMap.get(texture); } } diff --git a/packages-user/client-base/src/material/types.ts b/packages-user/client-base/src/material/types.ts index 7a69d20..1f058e4 100644 --- a/packages-user/client-base/src/material/types.ts +++ b/packages-user/client-base/src/material/types.ts @@ -1,5 +1,5 @@ +import { IDirtyTracker, IDirtyMarker } from '@motajs/common'; import { - IRect, ITexture, ITextureComposedData, ITextureRenderable, @@ -25,6 +25,17 @@ export const enum AutotileType { Big3x4 } +export const enum AutotileConnection { + LeftUp = 0b1000_0000, + Up = 0b0100_0000, + RightUp = 0b0010_0000, + Right = 0b0001_0000, + RightDown = 0b0000_1000, + Down = 0b0000_0100, + LeftDown = 0b0000_0010, + Left = 0b0000_0001 +} + export interface IMaterialData { /** 此素材的贴图对象存入了哪个贴图存储对象 */ readonly store: ITextureStore; @@ -70,53 +81,27 @@ export interface IAutotileConnection { readonly center: number; } -export interface IAutotileRenderable { - /** 自动元件的图像源 */ - readonly source: SizedCanvasImageSource; - /** 左上渲染的矩形范围 */ - readonly lt: Readonly; - /** 右上渲染的矩形范围 */ - readonly rt: Readonly; - /** 右下渲染的矩形范围 */ - readonly rb: Readonly; - /** 左下渲染的矩形范围 */ - readonly lb: Readonly; -} - -export interface IBigImageData { +export interface IBigImageReturn { /** 大怪物贴图在 store 中的标识符 */ readonly identifier: number; /** 存储大怪物贴图的存储对象 */ readonly store: ITextureStore; } -export interface IAssetDirtyMarker { - /** - * 标记为脏,即进行了一次更新 - */ - dirty(): void; +export interface IMaterialFramedData { + /** 贴图对象 */ + readonly texture: ITexture; + /** 图块类型 */ + readonly cls: BlockCls; + /** 贴图总帧数 */ + readonly frames: number; + /** 每帧的横向偏移量 */ + readonly offset: number; } -export interface IAssetDirtyTracker { - /** - * 对图集状态进行标记 - */ - mark(): symbol; - - /** - * 取消指定标记符号 - * @param mark 标记符号 - */ - unmark(mark: symbol): void; - - /** - * 从指定标记符号开始,图集是否发生了变动 - * @param mark 标记符号 - */ - dirtySince(mark: symbol): boolean; -} - -export interface IMaterialAsset extends IAssetDirtyTracker, IAssetDirtyMarker { +export interface IMaterialAsset + extends IDirtyTracker, + IDirtyMarker { /** 图集的贴图数据 */ readonly data: ITextureComposedData; } @@ -126,11 +111,12 @@ export interface IAutotileProcessor { readonly manager: IMaterialManager; /** - * 设置一个自动元件的父元件,一个自动元件可以有多个父元件 + * 设置一个自动元件的特殊连接方式,设置后当前自动元件将会单方面与目标元件连接, + * 一个自动元件可以与多个自动元件有特殊连接 * @param autotile 自动元件 - * @param parent 自动元件的父元件 + * @param target 当前自动元件将会连接至的自动元件 */ - setParent(autotile: number, parent: number): void; + setConnection(autotile: number, target: number): void; /** * 获取自动元件的连接情况 @@ -145,52 +131,71 @@ export interface IAutotileProcessor { ): IAutotileConnection; /** - * 获取指定自动元件经过连接的可渲染对象 + * 检查一个图块与指定方向的连接方式 + * @param connection 当前的连接 + * @param center 中心点的图块数字 + * @param target 连接点的图块数字 + * @param direction 连接点的方向 + * @returns 经过连接后的连接数字 + */ + updateConnectionFor( + connection: number, + center: number, + target: number, + direction: AutotileConnection + ): number; + + /** + * 根据图块数字,获取指定自动元件经过连接的可渲染对象 * @param autotile 自动元件的图块数字 * @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高 + * @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧 + */ + render(autotile: number, connection: number): ITextureRenderable | null; + + /** + * 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象 + * @param tile 自动元件的图块贴图数据 + * @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高 + * @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧 + */ + renderWith( + tile: IMaterialFramedData, + connection: number + ): ITextureRenderable | null; + + /** + * 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象,但是会假设传入的图块就是自动元件,不做不必要的判断 + * @param tile 自动元件的图块贴图数据 + * @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高 + * @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧 + */ + renderWithoutCheck( + tile: IMaterialFramedData, + connection: number + ): ITextureRenderable | null; + + /** + * 根据图块数字,获取指定自动元件经过链接的动态可渲染对象 + * @param autotile 自动元件的图块数字 + * @param connection 自动元件的连接方式 * @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同 */ - render( + renderAnimated( autotile: number, connection: number - ): Generator | null; + ): Generator; /** - * 通过静态可渲染对象(由 {@link ITexture.static} 输出的可渲染对象)输出自动元件经过连接的可渲染对象生成器 - * @param renderable 自动元件的原始可渲染对象 + * 根据图块贴图对象,获取指定自动元件经过链接的动态可渲染对象 + * @param autotile 自动元件的图块数字 * @param connection 自动元件的连接方式 * @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同 */ - fromStaticRenderable( - renderable: ITextureRenderable, + renderAnimatedWith( + tile: IMaterialFramedData, connection: number - ): Generator | null; - - /** - * 通过动画可渲染对象(由 {@link ITexture.dynamic} 或 {@link ITexture.cycled} 输出的单个可渲染对象) - * 输出自动元件经过连接的可渲染对象 - * @param renderable 自动元件的原始可渲染对象 - * @param connection 自动元件的连接方式 - * @returns 这一帧的可渲染对象 - */ - fromAnimatedRenderable( - renderable: ITextureRenderable, - connection: number - ): IAutotileRenderable | null; - - /** - * 通过动画生成器(由 {@link ITexture.dynamic} 或 {@link ITexture.cycled} 输出的生成器) - * 输出自动元件经过连接的可渲染对象生成器 - * @param texture 生成动画的纹理对象 - * @param generator 自动元件的动画生成器 - * @param connection 自动元件的连接方式 - * @returns 生成器,每一个输出代表每一帧的渲染对象 - */ - fromAnimatedGenerator( - texture: ITexture, - generator: Generator | null, - connection: number - ): Generator | null; + ): Generator; } export interface IMaterialGetter { @@ -198,7 +203,7 @@ export interface IMaterialGetter { * 根据图块数字获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存 * @param identifier 图块的图块数字 */ - getTile(identifier: number): ITexture | null; + getTile(identifier: number): IMaterialFramedData | null; /** * 根据图块标识符获取图块类型 @@ -216,14 +221,14 @@ export interface IMaterialGetter { * 根据图块标识符获取一个图块的 `bigImage` 贴图 * @param identifier 图块标识符,即图块数字 */ - getBigImage(identifier: number): ITexture | null; + getBigImage(identifier: number): IMaterialFramedData | null; /** * 根据图块标识符,首先判断是否是 `bigImage` 贴图,如果是,则返回 `bigImage` 贴图, * 否则返回普通贴图。如果图块不存在,则返回 `null` * @param identifier 图块标识符,即图块数字 */ - getIfBigImage(identifier: number): ITexture | null; + getIfBigImage(identifier: number): IMaterialFramedData | null; /** * 根据标识符获取图集信息 @@ -249,7 +254,7 @@ export interface IMaterialAliasGetter { * 根据图块 id 获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存 * @param alias 图块 id */ - getTileByAlias(alias: string): ITexture | null; + getTileByAlias(alias: string): IMaterialFramedData | null; /** * 根据额外素材名称获取额外素材 @@ -279,7 +284,7 @@ export interface IMaterialAliasGetter { * 根据图块别名获取一个图块的 `bigImage` 贴图 * @param alias 图块别名,即图块的 id */ - getBigImageByAlias(alias: string): ITexture | null; + getBigImageByAlias(alias: string): IMaterialFramedData | null; } export interface IMaterialManager @@ -330,11 +335,12 @@ export interface IMaterialManager * 添加自动元件 * @param source 图像源 * @param identifier 自动元件的字符串 id 及图块数字 + * @returns 由于自动元件是懒加载的,因此不会返回任何东西 */ addAutotile( source: SizedCanvasImageSource, identifier: IBlockIdentifier - ): IMaterialData; + ): void; /** * 添加一个 tileset 类型的素材 @@ -357,7 +363,7 @@ export interface IMaterialManager ): IMaterialData; /** - * 缓存某个 tileset + * 缓存某个 tileset,当需要缓存多个时,请使用 {@link cacheTilesetList} 方法 * @param identifier tileset 的标识符,即图块数字 */ cacheTileset(identifier: number): ITexture | null; @@ -370,6 +376,20 @@ export interface IMaterialManager identifierList: Iterable ): Iterable; + /** + * 缓存某个自动元件,当需要缓存多个时,请使用 {@link cacheAutotileList} 方法 + * @param identifier 自动元件标识符,即图块数字 + */ + cacheAutotile(identifier: number): ITexture | null; + + /** + * 缓存一系列自动元件 + * @param identifierList 自动元件标识符列表,即图块数字列表 + */ + cacheAutotileList( + identifierList: Iterable + ): Iterable; + /** * 把常用素材打包成为图集形式供后续使用 */ @@ -403,8 +423,25 @@ export interface IMaterialManager * 设置一个图块的 `bigImage` 贴图,即大怪物贴图,但不止怪物能用 * @param identifier 图块标识符,即图块数字 * @param image `bigImage` 对应的贴图对象 + * @param frames `bigImage` 的帧数,即贴图有多少帧 */ - setBigImage(identifier: number, image: ITexture): IBigImageData; + setBigImage( + identifier: number, + image: ITexture, + frames: number + ): IBigImageReturn; + + /** + * 当前的所有图集中是否包含指定的贴图对象 + * @param texture 贴图对象 + */ + assetContainsTexture(texture: ITexture): boolean; + + /** + * 获取指定贴图对象所属的图集索引 + * @param texture 贴图对象 + */ + getTextureAsset(texture: ITexture): number | undefined; } export interface IAssetBuilder { diff --git a/packages-user/client-base/src/material/utils.ts b/packages-user/client-base/src/material/utils.ts index b6bf0cc..e8bf237 100644 --- a/packages-user/client-base/src/material/utils.ts +++ b/packages-user/client-base/src/material/utils.ts @@ -1,3 +1,4 @@ +import { ITexture } from '@motajs/render-assets'; import { BlockCls } from './types'; export function getClsByString(cls: Cls): BlockCls { @@ -24,3 +25,23 @@ export function getClsByString(cls: Cls): BlockCls { return BlockCls.Unknown; } } + +export function getTextureFrame(cls: BlockCls, texture: ITexture) { + switch (cls) { + case BlockCls.Animates: + case BlockCls.Enemy48: + case BlockCls.Npc48: + return 4; + case BlockCls.Autotile: + return texture.width === 384 ? 4 : 1; + case BlockCls.Enemys: + case BlockCls.Npcs: + return 2; + case BlockCls.Items: + case BlockCls.Terrains: + case BlockCls.Tileset: + return 1; + case BlockCls.Unknown: + return 0; + } +} diff --git a/packages-user/client-modules/src/render/elements/index.ts b/packages-user/client-modules/src/render/elements/index.ts index ab23962..be001be 100644 --- a/packages-user/client-modules/src/render/elements/index.ts +++ b/packages-user/client-modules/src/render/elements/index.ts @@ -6,6 +6,8 @@ import { createViewport } from './viewport'; import { Icon, Winskin } from './misc'; import { Animate } from './animate'; import { createItemDetail } from './itemDetail'; +import { logger } from '@motajs/common'; +import { MapRender } from '../map/element'; export function createElements() { createCache(); @@ -66,6 +68,18 @@ export function createElements() { return new Animate(); }); tagMap.register('icon', standardElementNoCache(Icon)); + tagMap.register('map-render', (_0, _1, props) => { + if (!props) { + logger.error(42); + return new MapRender([]); + } + const { layerList } = props; + if (!layerList) { + logger.error(42); + return new MapRender([]); + } + return new MapRender(layerList); + }); } export * from './animate'; diff --git a/packages-user/client-modules/src/render/elements/props.ts b/packages-user/client-modules/src/render/elements/props.ts index 1525d79..9466d19 100644 --- a/packages-user/client-modules/src/render/elements/props.ts +++ b/packages-user/client-modules/src/render/elements/props.ts @@ -11,6 +11,7 @@ import { import { EAnimateEvent } from './animate'; import { EIconEvent, EWinskinEvent } from './misc'; import { IEnemyCollection } from '@motajs/types'; +import { IRenderLayerData } from '../map/element'; export interface AnimateProps extends BaseProps {} @@ -60,6 +61,10 @@ export interface LayerProps extends BaseProps { ex?: readonly ILayerRenderExtends[]; } +export interface MapRenderProps extends BaseProps { + layerList: IRenderLayerData; +} + declare module 'vue/jsx-runtime' { namespace JSX { export interface IntrinsicElements { diff --git a/packages-user/client-modules/src/render/map/asset.ts b/packages-user/client-modules/src/render/map/asset.ts new file mode 100644 index 0000000..5d754b8 --- /dev/null +++ b/packages-user/client-modules/src/render/map/asset.ts @@ -0,0 +1,31 @@ +import { SizedCanvasImageSource } from '@motajs/render-assets'; +import { + IMaterialGetter, + IMaterialManager, + materials +} from '@user/client-base'; +import { IMapAssetData, IMapAssetManager } from './types'; +import { PrivateListDirtyTracker } from '@motajs/common'; + +export class MapAssetManager implements IMapAssetManager { + materials: IMaterialManager = materials; + + generateAsset(): IMapAssetData { + const data = new MapAssetData(); + + return data; + } +} + +class MapAssetData + extends PrivateListDirtyTracker + implements IMapAssetData +{ + sourceList: ImageBitmap[] = []; + skipRef: Map = new Map(); + materials: IMaterialGetter = materials; + + constructor() { + super(0); + } +} diff --git a/packages-user/client-modules/src/render/map/block.ts b/packages-user/client-modules/src/render/map/block.ts new file mode 100644 index 0000000..47a4b90 --- /dev/null +++ b/packages-user/client-modules/src/render/map/block.ts @@ -0,0 +1,347 @@ +import { clamp } from 'lodash-es'; +import { + IBlockData, + IBlockIndex, + IBlockInfo, + IBlockSplitter, + IBlockSplitterConfig +} from './types'; + +export class BlockSplitter implements IBlockSplitter { + blockWidth: number = 0; + blockHeight: number = 0; + dataWidth: number = 0; + dataHeight: number = 0; + width: number = 0; + height: number = 0; + + /** 分块映射 */ + readonly blockMap: Map> = new Map(); + + /** 数据宽度配置 */ + private splitDataWidth: number = 0; + /** 数据高度配置 */ + private splitDataHeight: number = 0; + /** 单个分块的宽度配置 */ + private splitBlockWidth: number = 0; + /** 单个分块的高度配置 */ + private splitBlockHeight: number = 0; + + /** + * 检查坐标范围 + * @param x 分块横坐标 + * @param y 分块纵坐标 + */ + private checkLocRange(x: number, y: number) { + return x > 0 && y > 0 && x < this.width && y < this.height; + } + + getBlockByLoc(x: number, y: number): IBlockData | null { + if (!this.checkLocRange(x, y)) return null; + const index = y * this.width + x; + return this.blockMap.get(index) ?? null; + } + + getBlockByIndex(index: number): IBlockData | null { + return this.blockMap.get(index) ?? null; + } + + setBlockByLoc(data: T, x: number, y: number): IBlockData | null { + if (!this.checkLocRange(x, y)) return null; + const index = y * this.width + x; + const block = this.blockMap.get(index); + if (!block) return null; + block.data = data; + return block; + } + + setBlockByIndex(data: T, index: number): IBlockData | null { + const block = this.blockMap.get(index); + if (!block) return null; + block.data = data; + return block; + } + + *iterateBlockByLoc(x: number, y: number): Generator { + if (!this.checkLocRange(x, y)) return; + const index = y * this.width + x; + yield* this.iterateBlockByIndex(index); + } + + *iterateBlockByIndex(index: number): Generator { + const block = this.blockMap.get(index); + if (!block) return; + const startX = block.x * this.blockWidth; + const startY = block.y * this.blockHeight; + const endX = startX + block.width; + const endY = startY + block.height; + for (let ny = startY; ny < endY; ny++) { + for (let nx = startX; nx < endX; nx++) { + const index: IBlockIndex = { + x: nx, + y: ny, + dataX: nx * this.blockWidth, + dataY: ny * this.blockHeight, + index: ny * this.dataWidth + nx + }; + yield index; + } + } + } + + *iterateBlockByIndices( + indices: Iterable + ): Generator { + for (const index of indices) { + yield* this.iterateBlockByIndex(index); + } + } + + iterateBlocks(): Iterable> { + return this.blockMap.values(); + } + + *iterateBlocksOfDataArea( + x: number, + y: number, + width: number, + height: number + ): Generator> { + const r = this.width - 1; + const b = this.height - 1; + const rx = x + width; + const by = y + height; + const left = clamp(Math.floor(x / this.blockWidth), 0, r); + const top = clamp(Math.floor(y / this.blockHeight), 0, b); + const right = clamp(Math.floor(rx / this.blockWidth), 0, r); + const bottom = clamp(Math.floor(by / this.blockHeight), 0, b); + for (let ny = left; ny <= right; ny++) { + for (let nx = top; nx <= bottom; nx++) { + const index = ny * this.width + nx; + const block = this.blockMap.get(index); + if (!block) continue; + yield block; + } + } + } + + getIndexByLoc(x: number, y: number): number { + if (!this.checkLocRange(x, y)) return -1; + return y * this.width + x; + } + + getLocByIndex(index: number): Loc | null { + if (index >= this.width * this.height) return null; + return { + x: index % this.width, + y: Math.floor(index / this.width) + }; + } + + getIndicesByLocList(list: Iterable): Iterable { + const res: number[] = []; + for (const { x, y } of list) { + res.push(this.getIndexByLoc(x, y)); + } + return res; + } + + getLocListByIndices(list: Iterable): Iterable { + const res: (Loc | null)[] = []; + for (const index of list) { + res.push(this.getLocByIndex(index)); + } + return res; + } + + getBlockByDataLoc(x: number, y: number): IBlockData | null { + const bx = Math.floor(x / this.blockWidth); + const by = Math.floor(y / this.blockHeight); + if (!this.checkLocRange(bx, by)) return null; + const index = y * this.width + x; + return this.blockMap.get(index) ?? null; + } + + getBlockByDataIndex(index: number): IBlockData | null { + const x = index % this.dataWidth; + const y = Math.floor(index / this.dataWidth); + return this.getBlockByDataLoc(x, y); + } + + getIndicesByDataLocList(list: Iterable): Set { + const res = new Set(); + for (const { x, y } of list) { + const bx = Math.floor(x / this.blockWidth); + const by = Math.floor(y / this.blockHeight); + if (!this.checkLocRange(bx, by)) continue; + res.add(bx + by * this.width); + } + return res; + } + + getIndicesByDataIndices(list: Iterable): Set { + const res = new Set(); + for (const index of list) { + const x = index % this.dataWidth; + const y = Math.floor(index / this.dataWidth); + const bx = Math.floor(x / this.blockWidth); + const by = Math.floor(y / this.blockHeight); + if (!this.checkLocRange(bx, by)) continue; + res.add(bx + by * this.width); + } + return res; + } + + getBlocksByDataLocList(list: Iterable): Set> { + const res = new Set>(); + for (const { x, y } of list) { + const bx = Math.floor(x / this.blockWidth); + const by = Math.floor(y / this.blockHeight); + if (!this.checkLocRange(bx, by)) continue; + const index = bx + by * this.width; + const data = this.blockMap.get(index); + if (data) res.add(data); + } + return res; + } + + getBlocksByDataIndices(list: Iterable): Set> { + const res = new Set>(); + for (const index of list) { + const x = index % this.dataWidth; + const y = Math.floor(index / this.dataWidth); + const bx = Math.floor(x / this.blockWidth); + const by = Math.floor(y / this.blockHeight); + if (!this.checkLocRange(bx, by)) continue; + const blockIndex = bx + by * this.width; + const data = this.blockMap.get(blockIndex); + if (data) res.add(data); + } + return res; + } + + configSplitter(config: IBlockSplitterConfig): void { + this.splitBlockWidth = config.blockWidth; + this.splitBlockHeight = config.blockHeight; + this.splitDataWidth = config.dataWidth; + this.splitBlockHeight = config.dataHeight; + } + + private mapBlock( + x: number, + y: number, + realWidth: number, + width: number, + height: number, + fn: (block: IBlockInfo) => T + ) { + const index = y * realWidth + x; + const block: IBlockInfo = { + index, + x, + y, + dataX: x * this.blockWidth, + dataY: y * this.blockHeight, + width, + height + }; + const data = fn(block); + const blockData = new SplittedBlockData(this, block, data); + this.blockMap.set(index, blockData); + } + + splitBlocks(mapFn: (block: IBlockInfo) => T): void { + this.blockMap.clear(); + const restX = this.splitDataWidth % this.splitBlockWidth; + const restY = this.splitDataHeight % this.splitBlockHeight; + const width = Math.floor(this.splitDataWidth / this.splitBlockWidth); + const height = Math.floor(this.splitDataHeight / this.splitBlockHeight); + const hasXRest = restX > 0; + const hasYRest = restY > 0; + const realWidth = hasXRest ? width + 1 : width; + const bw = this.blockWidth; + const bh = this.blockHeight; + this.width = realWidth; + this.height = hasYRest ? height + 1 : height; + for (let ny = 0; ny < height; ny++) { + for (let nx = 0; nx < width; nx++) { + this.mapBlock(nx, ny, realWidth, bw, bh, mapFn); + } + } + if (hasXRest) { + for (let ny = 0; ny < height; ny++) { + this.mapBlock(width, ny, realWidth, restX, bh, mapFn); + } + } + if (hasYRest) { + for (let nx = 0; nx < width; nx++) { + this.mapBlock(nx, height, realWidth, bw, restY, mapFn); + } + } + if (hasXRest && hasYRest) { + this.mapBlock(width, height, realWidth, restX, restY, mapFn); + } + } +} + +class SplittedBlockData implements IBlockData { + width: number; + height: number; + x: number; + y: number; + dataX: number; + dataY: number; + index: number; + data: T; + + constructor( + readonly splitter: BlockSplitter, + info: IBlockInfo, + data: T + ) { + this.width = info.width; + this.height = info.height; + this.x = info.x; + this.y = info.y; + this.dataX = info.dataX; + this.dataY = info.dataY; + this.index = info.index; + this.data = data; + } + + left(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x - 1, this.y); + } + + right(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x + 1, this.y); + } + + up(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x, this.y - 1); + } + + down(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x, this.y + 1); + } + + leftUp(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x - 1, this.y - 1); + } + + leftDown(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x - 1, this.y + 1); + } + + rightUp(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x + 1, this.y - 1); + } + + rightDown(): IBlockData | null { + return this.splitter.getBlockByLoc(this.x + 1, this.y + 1); + } + + next(): IBlockData | null { + return this.splitter.getBlockByIndex(this.index + 1); + } +} diff --git a/packages-user/client-modules/src/render/map/element.ts b/packages-user/client-modules/src/render/map/element.ts new file mode 100644 index 0000000..21c16e6 --- /dev/null +++ b/packages-user/client-modules/src/render/map/element.ts @@ -0,0 +1,114 @@ +import { + MotaOffscreenCanvas2D, + RenderItem, + Transform +} from '@motajs/render-core'; +import { IMapLayer } from '@user/data-state'; +import { IMapRenderer } from './types'; +import { MapRenderer } from './renderer'; +import { materials } from '@user/client-base'; +import { ElementNamespace, ComponentInternalInstance } from 'vue'; + +export interface IRenderLayerData { + /** 图层对象 */ + readonly layer: IMapLayer; + /** 图层纵深 */ + readonly zIndex: number; + /** 图层别名 */ + readonly alias?: string; +} + +export class MapRender extends RenderItem { + /** 地图渲染器 */ + readonly renderer: IMapRenderer; + /** 地图视角变换矩阵 */ + readonly camera: Transform = new Transform(); + + /** 地图画布 */ + readonly canvas: HTMLCanvasElement; + /** 画布上下文 */ + readonly gl: WebGL2RenderingContext; + + constructor(layerList: Iterable) { + super('static'); + + this.canvas = document.createElement('canvas'); + const gl = this.canvas.getContext('webgl2')!; + this.gl = gl; + + this.renderer = new MapRenderer(materials, this.gl, this.camera); + for (const layer of layerList) { + this.renderer.addLayer(layer.layer, layer.alias); + this.renderer.setZIndex(layer.layer, layer.zIndex); + } + } + + /** + * 更新图层列表 + * @param layerList 图层列表 + */ + updateLayerList(layerList: Iterable) { + this.renderer + .getSortedLayer() + .forEach(v => this.renderer.removeLayer(v)); + for (const layer of layerList) { + this.renderer.addLayer(layer.layer, layer.alias); + this.renderer.setZIndex(layer.layer, layer.zIndex); + } + } + + private sizeGL(width: number, height: number) { + const ratio = this.highResolution ? devicePixelRatio : 1; + const scale = ratio * this.scale; + this.canvas.width = width * scale; + this.canvas.height = height * scale; + this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); + } + + onResize(scale: number): void { + super.onResize(scale); + this.sizeGL(this.width, this.height); + } + + size(width: number, height: number): void { + super.size(width, height); + this.sizeGL(width, height); + } + + updateTransform(transform: Transform): void { + super.updateTransform(transform); + if (transform === this.camera) { + this.update(); + } + } + + protected render(canvas: MotaOffscreenCanvas2D): void { + console.time('map-element-render'); + this.renderer.render(this.gl); + + canvas.ctx.drawImage( + this.canvas, + 0, + 0, + this.canvas.width, + this.canvas.height + ); + console.timeEnd('map-element-render'); + } + + patchProp( + key: string, + prevValue: any, + nextValue: any, + namespace?: ElementNamespace, + parentComponent?: ComponentInternalInstance | null + ): void { + switch (key) { + case 'layerList': { + this.updateLayerList(nextValue); + break; + } + } + super.patchProp(key, prevValue, nextValue, namespace, parentComponent); + } +} diff --git a/packages-user/client-modules/src/render/map/index.ts b/packages-user/client-modules/src/render/map/index.ts new file mode 100644 index 0000000..c0435c5 --- /dev/null +++ b/packages-user/client-modules/src/render/map/index.ts @@ -0,0 +1,3 @@ +export * from './asset'; +export * from './renderer'; +export * from './types'; diff --git a/packages-user/client-modules/src/render/map/moving.ts b/packages-user/client-modules/src/render/map/moving.ts new file mode 100644 index 0000000..06623f2 --- /dev/null +++ b/packages-user/client-modules/src/render/map/moving.ts @@ -0,0 +1,229 @@ +import { linear, TimingFn } from 'mutate-animate'; +import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types'; +import { IMaterialFramedData, IMaterialManager } from '@user/client-base'; +import { logger } from '@motajs/common'; +import { IMapLayer } from '@user/data-state'; + +export interface IMovingRenderer { + /** 素材管理器 */ + readonly manager: IMaterialManager; + /** 顶点数组生成器 */ + readonly vertex: IMapVertexGenerator; + + /** + * 获得当前时间戳 + */ + getTimestamp(): number; + + /** + * 删除指定的移动图块对象 + * @param block 移动图块对象 + */ + deleteMoving(block: IMovingBlock): void; +} + +export class MovingBlock implements IMovingBlock { + readonly texture: IMaterialFramedData; + readonly tile: number; + readonly renderer: IMovingRenderer; + readonly index: number; + readonly layer: IMapLayer; + + x: number = 0; + y: number = 0; + + /** 当前动画开始的时刻 */ + private startTime: number = 0; + + /** 是否是直线动画 */ + private line: boolean = false; + /** 是否是相对模式 */ + private relative: boolean = false; + /** 目标横坐标 */ + private targetX: number = 0; + /** 目标纵坐标 */ + private targetY: number = 0; + /** 直线移动的横坐标增量 */ + private dx: number = 0; + /** 直线移动的纵坐标增量 */ + private dy: number = 0; + /** 动画时长 */ + private time: number = 0; + /** 速率曲线 */ + private timing: TimingFn = () => 0; + /** 移动轨迹曲线 */ + private curve: TimingFn<2> = () => [0, 0]; + + /** 动画开始时横坐标 */ + private startX: number = 0; + /** 动画开始时纵坐标 */ + private startY: number = 0; + /** 当前动画是否已经结束 */ + private end: boolean = false; + + /** 兑现函数 */ + private promiseFunc: () => void = () => {}; + + constructor( + renderer: IMovingRenderer & IMapRenderer, + index: number, + layer: IMapLayer, + block: number | IMaterialFramedData, + x: number, + y: number + ) { + this.renderer = renderer; + this.index = index; + this.layer = layer; + this.x = x; + this.y = y; + if (typeof block === 'number') { + this.texture = renderer.manager.getTile(block)!; + this.tile = block; + } else { + if (!renderer.manager.assetContainsTexture(block.texture)) { + logger.error(34); + } + if (renderer.getOffsetIndex(block.offset) === -1) { + logger.error(41); + } + this.texture = block; + this.tile = -1; + } + } + + lineTo( + x: number, + y: number, + time: number, + timing?: TimingFn + ): Promise { + this.startX = this.x; + this.startY = this.y; + this.targetX = x; + this.targetY = y; + this.dx = x - this.x; + this.dy = y - this.y; + this.time = time; + this.relative = false; + if (time === 0) { + this.x = x; + this.y = y; + this.end = true; + return Promise.resolve(this); + } + this.end = false; + this.timing = timing ?? linear(); + this.line = true; + return new Promise(res => { + this.promiseFunc = () => res(this); + }); + } + + moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise { + this.time = time; + this.line = false; + this.relative = false; + this.startX = this.x; + this.startY = this.y; + if (time === 0) { + const [tx, ty] = curve(1); + this.x = tx; + this.y = ty; + this.end = true; + return Promise.resolve(this); + } + this.end = false; + this.timing = timing ?? linear(); + this.curve = curve; + return new Promise(res => { + this.promiseFunc = () => res(this); + }); + } + + moveRelative( + curve: TimingFn<2>, + time: number, + timing?: TimingFn + ): Promise { + this.time = time; + this.line = false; + this.relative = false; + this.startX = this.x; + this.startY = this.y; + if (time === 0) { + const [tx, ty] = curve(1); + this.x = tx + this.startX; + this.y = ty + this.startY; + this.end = true; + return Promise.resolve(this); + } + this.end = false; + this.timing = timing ?? linear(); + this.curve = curve; + return new Promise(res => { + this.promiseFunc = () => res(this); + }); + } + + stepMoving(timestamp: number): boolean { + if (this.end) return false; + const dt = timestamp - this.startTime; + if (this.line) { + if (dt > this.time) { + this.x = this.targetX; + this.y = this.targetY; + this.end = true; + this.promiseFunc(); + return false; + } else { + const timeProgress = dt / this.time; + const progress = this.timing(timeProgress); + this.x = this.startX + progress * this.dx; + this.y = this.startY + progress * this.dy; + } + } else { + if (dt > this.time) { + const [tx, ty] = this.curve(1); + if (this.relative) { + this.x = tx + this.startX; + this.y = ty + this.startY; + } else { + this.x = tx; + this.y = ty; + } + this.end = true; + this.promiseFunc(); + return false; + } else { + const timeProgress = dt / this.time; + const progress = this.timing(timeProgress); + const [tx, ty] = this.curve(progress); + if (this.relative) { + this.x = tx + this.startX; + this.y = ty + this.startY; + } else { + this.x = tx; + this.y = ty; + } + } + } + return true; + } + + enableFrameAnimate(): void { + this.renderer.vertex.enableDynamicFrameAnimate(this); + } + + disableFrameAnimate(): void { + this.renderer.vertex.disableDynamicFrameAnimate(this); + } + + setAlpha(alpha: number): void { + this.renderer.vertex.setDynamicAlpha(this, alpha); + } + + destroy(): void { + this.renderer.deleteMoving(this); + } +} diff --git a/packages-user/client-modules/src/render/map/renderer.ts b/packages-user/client-modules/src/render/map/renderer.ts new file mode 100644 index 0000000..de63eb4 --- /dev/null +++ b/packages-user/client-modules/src/render/map/renderer.ts @@ -0,0 +1,1488 @@ +import { + ITextureAnimater, + ITextureRenderable, + SizedCanvasImageSource, + TextureColumnAnimater +} from '@motajs/render-assets'; +import { + AutotileProcessor, + BlockCls, + IAutotileProcessor, + IMaterialFramedData, + IMaterialManager +} from '@user/client-base'; +import { + IContextData, + IMapAssetData, + IMapBackgroundConfig, + IMapRenderConfig, + IMapRenderer, + IMapVertexGenerator, + IMapViewportController, + IMovingBlock, + MapBackgroundRepeat, + MapTileAlign, + MapTileBehavior, + MapTileSizeTestMode +} from './types'; +import { + IMapLayer, + IMapLayerData, + IMapLayerExtends, + IMapLayerExtendsController +} from '@user/data-state'; +import { logger } from '@motajs/common'; +import { compileProgramWith } from '@motajs/client-base'; +import { isNil, maxBy } from 'lodash-es'; +import { IMapDataGetter, MapVertexGenerator } from './vertex'; +import mapVert from './shader/map.vert?raw'; +import mapFrag from './shader/map.frag?raw'; +import backVert from './shader/back.vert?raw'; +import backFrag from './shader/back.frag?raw'; +import { IMovingRenderer, MovingBlock } from './moving'; +import { + CELL_HEIGHT, + CELL_WIDTH, + DYNAMIC_RESERVE, + MOVING_TOLERANCE +} from '../shared'; +import { ITransformUpdatable, Transform } from '@motajs/render-core'; +import { MapViewport } from './viewport'; + +const enum BackgroundType { + Static, + Dynamic, + Tile +} + +interface ILayerRenderData { + /** 顶点数组,与总的数组共享 ArrayBuffer */ + readonly vertexArray: Float32Array; + /** 偏移数组,与总的数组共享 ArrayBuffer */ + readonly offsetArray: Int16Array; + + /** 顶点数组在原数组的起始索引值 */ + readonly vertexStart: number; + /** 顶点数组在原数组的终止索引值 */ + readonly vertexEnd: number; + /** 偏移数组在原数组的起始索引值 */ + readonly offsetStart: number; + /** 偏移数组在原数组的终止索引值 */ + readonly offsetEnd: number; +} + +export class MapRenderer + implements + IMapRenderer, + IMovingRenderer, + ITransformUpdatable, + IMapDataGetter +{ + //#region 实例属性 + + /** 自动元件处理器 */ + readonly autotile: IAutotileProcessor; + /** 顶点数组生成器 */ + readonly vertex: IMapVertexGenerator; + /** 地图渲染的视角控制 */ + readonly viewport: IMapViewportController; + + mapWidth: number = 0; + mapHeight: number = 0; + layerCount: number = 0; + renderWidth: number = 0; + renderHeight: number = 0; + cellWidth: number = CELL_WIDTH; + cellHeight: number = CELL_HEIGHT; + + /** 这个渲染器添加的图层 */ + readonly layers: Set = new Set(); + /** 图层的 zIndex 映射 */ + private layerZIndex: Map = new Map(); + /** 图层的别名 */ + private layerAlias: Map = new Map(); + /** 图层到其对应别名的映射 */ + private layerAliasInv: Map = new Map(); + /** 排序后的图层 */ + private sortedLayers: IMapLayer[] = []; + /** 图层到排序索引的映射 */ + private layerIndexMap: Map = new Map(); + + /** 使用的图集数据 */ + private assetData: IMapAssetData | null = null; + + /** 背景图类型 */ + private backgroundType: BackgroundType = BackgroundType.Tile; + /** 静态背景 */ + private staticBack: ITextureRenderable | null = null; + /** 动态背景 */ + private dynamicBack: ITextureRenderable[] | null = null; + /** 图块背景 */ + private tileBack: number = 0; + /** 动态背景每帧持续时长 */ + private backFrameSpeed: number = 300; + /** 当前背景图帧数 */ + private backgroundFrame: number = 0; + /** 背景图总帧数 */ + private backgroundFrameCount: number = 1; + /** 背景图上一帧的时刻 */ + private backLastFrame: number = 0; + /** 背景图横向平铺方式 */ + private backRepeatModeX: MapBackgroundRepeat = MapBackgroundRepeat.Repeat; + /** 背景图纵向平铺方式 */ + private backRepeatModeY: MapBackgroundRepeat = MapBackgroundRepeat.Repeat; + /** 背景图是否使用图片大小作为渲染大小 */ + private backUseImageSize: boolean = true; + /** 背景图的渲染宽度 */ + private backRenderWidth: number = 0; + /** 背景图的渲染高度 */ + private backRenderHeight: number = 0; + /** 背景图是否需要更新 */ + private backgroundDirty: boolean = false; + /** 背景图是否正在更新 */ + private backgroundPending: boolean = false; + /** 背景图宽度 */ + private backgroundWidth: number = 0; + /** 背景图高度 */ + private backgroundHeight: number = 0; + /** 背景顶点数组 */ + private backgroundVertex: Float32Array = new Float32Array(4 * 4); + + /** 图块缩小行为,即当图块比格子大时,应该如何渲染 */ + tileMinifyBehavior: MapTileBehavior = MapTileBehavior.KeepSize; + /** 图块放大行为,即当图块比格子小时,应该如何渲染 */ + tileMagnifyBehavior: MapTileBehavior = MapTileBehavior.FitToSize; + /** 图块水平对齐,仅当图块行为为 `KeepSize` 时有效 */ + tileAlignX: MapTileAlign = MapTileAlign.Center; + /** 图块竖直对齐,仅当图块行为为 `KeepSize` 时有效 */ + tileAlignY: MapTileAlign = MapTileAlign.End; + /** 图块大小与网格大小的判断方式,如果图块大于网格,则执行缩小行为,否则执行放大行为 */ + tileTestMode: MapTileSizeTestMode = MapTileSizeTestMode.WidthOrHeight; + + /** 偏移池 */ + private offsetPool: number[]; + /** 归一化过后的偏移池 */ + private normalizedOffsetPool: number[]; + /** 是否应该更新偏移池 uniform */ + private needUpdateOffsetPool: boolean = true; + + /** 地图渲染数据 */ + private layerData: Map = new Map(); + /** 地图拓展映射 */ + private layerController: Map; + /** 顶点数组的图层列表是否需要更新 */ + private layerDirty: boolean = false; + /** 顶点数组的图层的尺寸是否需要更新 */ + private layerSizeDirty: boolean = false; + + /** 所有正在移动的图块 */ + private movingBlock: Set = new Set(); + /** 移动图块对象索引池 */ + private movingIndexPool: number[] = []; + /** 移动图块对象数量 */ + private movingCount: number = DYNAMIC_RESERVE; + /** 移动索引映射 */ + private movingIndexMap: Map = new Map(); + /** 移动容忍度,如果正在移动的图块数量长期小于预留数量的一半,那么将减少移动数组长度 */ + private lastExpandTime = 0; + + /** 时间戳 */ + private timestamp: number = 0; + /** 上一帧动画的时刻 */ + private lastFrameTime: number = 0; + /** 当前帧数 */ + private frameCounter: number = 0; + /** 帧动画速率 */ + private frameSpeed: number = 300; + + /** 画布上下文数据 */ + private contextData: IContextData; + + /** 图块动画器 */ + private readonly tileAnimater: ITextureAnimater; + + //#endregion + + //#region 初始化 + + /** + * 创建地图渲染器 + * @param manager 素材管理器 + * @param gl 画布 WebGL2 上下文 + * @param transform 视角变换矩阵 + */ + constructor( + readonly manager: IMaterialManager, + readonly gl: WebGL2RenderingContext, + readonly transform: Transform + ) { + // 上下文初始化要依赖于 offsetPool,因此提前调用 + const offsetPool = this.getOffsetPool(); + const data = this.initContext()!; + this.offsetPool = offsetPool; + this.normalizedOffsetPool = offsetPool.map( + v => v / data.tileTextureWidth + ); + this.contextData = data; + this.layerController = new Map(); + this.vertex = new MapVertexGenerator(this, data); + this.autotile = new AutotileProcessor(manager); + this.tick = this.tick.bind(this); + this.transform = new Transform(); + this.transform.bind(this); + this.viewport = new MapViewport(this); + this.viewport.bindTransform(this.transform); + this.tileAnimater = new TextureColumnAnimater(); + // 背景初始化 + const arr = this.backgroundVertex; + // 左下角 + arr[0] = -1; + arr[1] = -1; + // 右下角 + arr[4] = 1; + arr[5] = -1; + // 左上角 + arr[8] = -1; + arr[9] = 1; + // 右上角 + arr[12] = 1; + arr[13] = 1; + gl.bindVertexArray(data.backVAO); + gl.bindBuffer(gl.ARRAY_BUFFER, data.backgroundVertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, arr, gl.DYNAMIC_DRAW); + gl.vertexAttribPointer( + data.backVertexAttribLocation, + 2, + gl.FLOAT, + false, + 4 * 4, + 0 + ); + gl.vertexAttribPointer( + data.backTexCoordAttribLocation, + 2, + gl.FLOAT, + false, + 4 * 4, + 2 * 4 + ); + gl.bindVertexArray(null); + } + + //#endregion + + //#region 图层处理 + + /** + * 图层排序 + */ + private sortLayer() { + this.sortedLayers = [...this.layers].sort((a, b) => { + const za = this.layerZIndex.get(a) ?? -1; + const zb = this.layerZIndex.get(b) ?? -1; + return za - zb; + }); + this.sortedLayers.forEach((v, i) => this.layerIndexMap.set(v, i)); + } + + addLayer(layer: IMapLayer, identifier?: string): void { + this.layers.add(layer); + this.layerZIndex.set(layer, 0); + if (identifier) { + this.layerAlias.set(identifier, layer); + } + const ex = new MapRendererExtends(this); + const controller = layer.addExtends(ex); + controller.load(); + this.layerController.set(layer, controller); + this.sortLayer(); + this.layerDirty = true; + this.layerCount = this.layers.size; + } + + removeLayer(layer: IMapLayer): void { + this.layers.delete(layer); + this.layerData.delete(layer); + this.layerZIndex.delete(layer); + const ex = this.layerController.get(layer); + if (ex) { + ex.unload(); + } + this.layerController.delete(layer); + const alias = this.layerAliasInv.get(layer); + if (!isNil(alias)) { + this.layerAlias.delete(alias); + } + this.layerAliasInv.delete(layer); + this.sortLayer(); + this.layerDirty = true; + this.layerCount = this.layers.size; + } + + getLayer(identifier: string): IMapLayer | null { + return this.layerAlias.get(identifier) ?? null; + } + + hasLayer(layer: IMapLayer): boolean { + return this.layers.has(layer); + } + + getSortedLayer(): IMapLayer[] { + return this.sortedLayers.slice(); + } + + setZIndex(layer: IMapLayer, zIndex: number): void { + this.layerZIndex.set(layer, zIndex); + this.sortLayer(); + this.layerDirty = true; + } + + getZIndex(layer: IMapLayer): number | undefined { + return this.layerZIndex.get(layer); + } + + getLayerIndex(layer: IMapLayer): number { + return this.layerIndexMap.get(layer) ?? -1; + } + + getMapLayerData(layer: IMapLayer): Readonly | null { + const ex = this.layerController.get(layer); + const data = ex?.getMapData(); + return data ?? null; + } + + /** + * 重新适应新的图层大小 + */ + resizeLayer() { + const maxWidth = maxBy(this.sortedLayers, v => v.width)?.width ?? 0; + const maxHeight = maxBy(this.sortedLayers, v => v.height)?.height ?? 0; + if (this.mapWidth !== maxWidth || this.mapHeight !== maxHeight) { + this.layerSizeDirty = true; + } + this.mapWidth = maxWidth; + this.mapHeight = maxHeight; + this.layerDirty = true; + } + + //#endregion + + //#region 背景处理 + + setStaticBackground(renderable: ITextureRenderable): void { + this.backgroundType = BackgroundType.Static; + this.staticBack = renderable; + this.dynamicBack = null; + this.tileBack = 0; + this.backLastFrame = this.timestamp; + this.backgroundFrameCount = 1; + this.checkBackground(this.gl, this.contextData); + } + + setDynamicBackground(renderable: Iterable): void { + const array = [...renderable]; + this.backgroundType = BackgroundType.Dynamic; + this.dynamicBack = array; + this.staticBack = null; + this.tileBack = 0; + this.backLastFrame = this.timestamp; + this.backgroundFrameCount = array.length; + this.checkBackground(this.gl, this.contextData); + } + + setTileBackground(tile: number): void { + this.backgroundType = BackgroundType.Tile; + this.tileBack = tile; + this.staticBack = null; + this.dynamicBack = null; + this.backLastFrame = this.timestamp; + this.checkBackground(this.gl, this.contextData); + } + + configBackground(config: Partial): void { + if (!isNil(config.renderWidth)) { + this.backRenderWidth = config.renderWidth; + } + if (!isNil(config.renderHeight)) { + this.backRenderHeight = config.renderHeight; + } + if (!isNil(config.repeatX)) { + this.backRepeatModeX = config.repeatX; + } + if (!isNil(config.repeatY)) { + this.backRepeatModeY = config.repeatY; + } + if (!isNil(config.useImageSize)) { + this.backUseImageSize = config.useImageSize; + } + if (!isNil(config.frameSpeed)) { + this.backFrameSpeed = config.frameSpeed; + } + this.updateBackgroundVertex( + this.gl, + this.contextData, + this.backgroundWidth, + this.backgroundHeight + ); + } + + getBackgroundConfig(): Readonly { + return { + renderWidth: this.backRenderWidth, + renderHeight: this.backRenderHeight, + repeatX: this.backRepeatModeX, + repeatY: this.backRepeatModeY, + useImageSize: this.backUseImageSize, + frameSpeed: this.backFrameSpeed + }; + } + + //#endregion + + //#region 渲染设置 + + useAsset(asset: IMapAssetData): void { + this.assetData = asset; + this.sortedLayers.forEach(v => { + this.updateLayerArea(v, 0, 0, v.width, v.height); + }); + } + + setRenderSize(width: number, height: number): void { + this.renderWidth = width; + this.renderHeight = height; + this.sortedLayers.forEach(v => { + this.vertex.updateArea(v, 0, 0, this.mapWidth, this.mapHeight); + }); + } + + setCellSize(width: number, height: number): void { + this.cellWidth = width; + this.cellHeight = height; + this.sortedLayers.forEach(v => { + this.vertex.updateArea(v, 0, 0, this.mapWidth, this.mapHeight); + }); + } + + configRendering(config: Partial): void { + let needUpdate = false; + if (!isNil(config.minBehavior)) { + this.tileMinifyBehavior = config.minBehavior; + needUpdate = true; + } + if (!isNil(config.magBehavior)) { + this.tileMagnifyBehavior = config.magBehavior; + needUpdate = true; + } + if (!isNil(config.tileAlignX)) { + this.tileAlignX = config.tileAlignX; + needUpdate = true; + } + if (!isNil(config.tileAlignY)) { + this.tileAlignY = config.tileAlignY; + needUpdate = true; + } + if (!isNil(config.tileTestMode)) { + this.tileTestMode = config.tileTestMode; + needUpdate = true; + } + if (!isNil(config.frameSpeed)) { + this.frameSpeed = config.frameSpeed; + } + if (needUpdate) { + this.sortedLayers.forEach(v => { + this.vertex.updateArea(v, 0, 0, this.mapWidth, this.mapHeight); + }); + } + } + + getRenderingConfig(): Readonly { + return { + minBehavior: this.tileMinifyBehavior, + magBehavior: this.tileMagnifyBehavior, + tileAlignX: this.tileAlignX, + tileAlignY: this.tileAlignY, + tileTestMode: this.tileTestMode, + frameSpeed: this.frameSpeed + }; + } + + getTransform(): Transform { + return this.transform; + } + + private getOffsetPool(): number[] { + const pool = new Set([32]); + // 其他的都是 bigImage 了,直接遍历获取 + for (const identifier of this.manager.bigImageStore.keys()) { + const data = this.manager.getBigImage(identifier); + if (!data) continue; + const offset = data.texture.width / data.frames; + pool.add(offset); + } + if (pool.size > 64 && import.meta.env.DEV) { + logger.warn(82); + } + if (pool.size > this.gl.MAX_VERTEX_UNIFORM_VECTORS) { + logger.error(39, this.gl.MAX_VERTEX_UNIFORM_VECTORS.toString()); + } + return [...pool]; + } + + getAssetSourceIndex(source: SizedCanvasImageSource): number { + if (!this.assetData) return -1; + if (source instanceof ImageBitmap) { + return this.assetData.sourceList.indexOf(source); + } else { + return -1; + } + } + + getOffsetIndex(offset: number): number { + return this.offsetPool.indexOf(offset); + } + + //#endregion + + //#region 画布上下文 + + private initContext(): IContextData | null { + const gl = this.gl; + const vs = mapVert.replace('$1', this.offsetPool.length.toString()); + const tileProgram = compileProgramWith(gl, vs, mapFrag); + const backProgram = compileProgramWith(gl, backVert, backFrag); + if (!tileProgram || !backProgram) { + logger.error(28); + return null; + } + + const poolLocation = gl.getUniformLocation( + tileProgram, + // 数组要写 [0] + 'u_offsetPool[0]' + ); + const frameLocation = gl.getUniformLocation(tileProgram, 'u_nowFrame'); + const tileSampler = gl.getUniformLocation(tileProgram, 'u_sampler'); + const backSampler = gl.getUniformLocation(backProgram, 'u_sampler'); + const tileTrans = gl.getUniformLocation(tileProgram, 'u_transform'); + const backTrans = gl.getUniformLocation(backProgram, 'u_transform'); + const backFrame = gl.getUniformLocation(backProgram, 'u_nowFrame'); + if ( + !poolLocation || + !frameLocation || + !tileSampler || + !backSampler || + !tileTrans || + !backTrans || + !backFrame + ) { + logger.error(29); + return null; + } + + const vertexAttrib = gl.getAttribLocation(tileProgram, 'a_position'); + const texCoordAttrib = gl.getAttribLocation(tileProgram, 'a_texCoord'); + const offsetAttrib = gl.getAttribLocation(tileProgram, 'a_offset'); + const alphaAttrib = gl.getAttribLocation(tileProgram, 'a_alpha'); + const backVertex = gl.getAttribLocation(backProgram, 'a_position'); + const backTexCoord = gl.getAttribLocation(backProgram, 'a_texCoord'); + + const vertexBuffer = gl.createBuffer(); + const offsetBuffer = gl.createBuffer(); + const alphaBuffer = gl.createBuffer(); + const backVertexBuffer = gl.createBuffer(); + + const tileTexture = gl.createTexture(); + const backgroundTexture = gl.createTexture(); + + const tileVAO = gl.createVertexArray(); + const backVAO = gl.createVertexArray(); + + gl.bindTexture(gl.TEXTURE_2D_ARRAY, tileTexture); + gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, 4096, 4096, 1); + + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_MAG_FILTER, + gl.NEAREST + ); + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_MIN_FILTER, + gl.NEAREST + ); + + // 配置清空选项 + gl.clearColor(0, 0, 0, 0); + gl.clearDepth(1); + + const data: IContextData = { + tileProgram: tileProgram.program, + tileVertShader: tileProgram.vertexShader, + tileFragShader: tileProgram.fragmentShader, + backProgram: backProgram.program, + backVertShader: backProgram.vertexShader, + backFragShader: backProgram.fragmentShader, + vertexBuffer, + offsetBuffer, + alphaBuffer, + backgroundVertexBuffer: backVertexBuffer, + offsetPoolLocation: poolLocation, + nowFrameLocation: frameLocation, + tileSamplerLocation: tileSampler, + backSamplerLocation: backSampler, + tileTransformLocation: tileTrans, + backTransformLocation: backTrans, + backNowFrameLocation: backFrame, + vertexAttribLocation: vertexAttrib, + texCoordAttribLocation: texCoordAttrib, + offsetAttribLocation: offsetAttrib, + alphaAttribLocation: alphaAttrib, + backVertexAttribLocation: backVertex, + backTexCoordAttribLocation: backTexCoord, + tileVAO, + backVAO, + tileTexture, + backgroundTexture, + tileTextureWidth: 4096, + tileTextureHeight: 4096, + tileTextureDepth: 1, + backgroundWidth: 0, + backgroundHeight: 0, + backgroundDepth: 0, + tileTextureMark: Symbol(), + vertexMark: Symbol() + }; + + return data; + } + + destroy(): void { + const gl = this.gl; + const data = this.contextData; + if (!data) return; + gl.deleteBuffer(data.offsetBuffer); + gl.deleteBuffer(data.vertexBuffer); + gl.deleteProgram(data.tileProgram); + gl.deleteProgram(data.backProgram); + gl.deleteShader(data.tileVertShader); + gl.deleteShader(data.tileFragShader); + gl.deleteShader(data.backVertShader); + gl.deleteShader(data.backFragShader); + gl.deleteTexture(data.tileTexture); + gl.deleteTexture(data.backgroundTexture); + } + + //#endregion + + //#region 渲染 + + /** + * 检查指定画布的纹理数组尺寸,需要预先绑定 gl.TEXTURE_2D_ARRAY 纹理 + * @param gl WebGL2 上下文 + * @param data 画布上下文数据 + * @param source 图形源列表 + * @returns 最终贴图尺寸是否改变 + */ + private checkTextureArraySize( + gl: WebGL2RenderingContext, + data: IContextData, + source: ImageBitmap[] + ): boolean { + const maxWidth = maxBy(source, v => v.width)?.width ?? 0; + const maxHeight = maxBy(source, v => v.height)?.height ?? 0; + const count = source.length; + if ( + maxWidth !== data.tileTextureWidth || + maxHeight !== data.tileTextureHeight || + count !== data.tileTextureDepth + ) { + gl.texStorage3D( + gl.TEXTURE_2D_ARRAY, + 1, + gl.RGBA8, + maxWidth, + maxHeight, + count + ); + data.tileTextureWidth = maxWidth; + data.tileTextureHeight = maxHeight; + data.tileTextureDepth = count; + this.normalizedOffsetPool = this.offsetPool.map(v => v / maxWidth); + this.needUpdateOffsetPool = true; + return true; + } else { + return false; + } + } + + /** + * 检查指定画布上下文的纹理是否需要更新 + * @param gl WebGL2 上下文 + * @param data 画布上下文数据 + */ + private checkTexture(gl: WebGL2RenderingContext, data: IContextData) { + if (!this.assetData) return; + const tile = data.tileTexture; + const source = this.assetData.sourceList; + if (!this.assetData.hasMark(data.tileTextureMark)) { + // 如果没有标记,那么直接全部重新传递 + gl.bindTexture(gl.TEXTURE_2D_ARRAY, tile); + data.tileTextureMark = this.assetData.mark(); + this.checkTextureArraySize(gl, data, source); + source.forEach((v, i) => { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, + 0, + i, + v.width, + v.height, + 1, + gl.RGBA, + gl.UNSIGNED_BYTE, + v + ); + }); + gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.REPEAT); + } else { + const dirty = this.assetData.dirtySince(data.tileTextureMark); + if (dirty.size === 0) return; + this.assetData.unmark(data.tileTextureMark); + data.tileTextureMark = this.assetData.mark(); + gl.bindTexture(gl.TEXTURE_2D_ARRAY, tile); + const sizeChanged = this.checkTextureArraySize(gl, data, source); + if (sizeChanged) { + // 尺寸变化,需要全部重新传递 + source.forEach((v, i) => { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, + 0, + i, + v.width, + v.height, + 1, + gl.RGBA, + gl.UNSIGNED_BYTE, + v + ); + }); + } else { + // 否则只需要传递标记为脏的图像 + dirty.forEach(v => { + const img = source[v]; + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, + 0, + v, + img.width, + img.height, + 1, + gl.RGBA, + gl.UNSIGNED_BYTE, + img + ); + }); + } + } + } + + /** + * 检查图块顶点数组是否需要更新 + * @param gl WebGL2 上下文 + * @param data 上下文数据 + */ + private checkTileVertexArray( + gl: WebGL2RenderingContext, + data: IContextData + ) { + if (!this.assetData) return; + const dirty = this.vertex.dirtySince(data.vertexMark); + if (!dirty) return; + const array = this.vertex.getVertexArray(); + const { + vertexBuffer, + offsetBuffer, + alphaBuffer, + tileVAO, + vertexAttribLocation: vaLocation, + texCoordAttribLocation: tcaLocation, + offsetAttribLocation: oaLocation, + alphaAttribLocation: aaLocation + } = data; + // 顶点数据不需要实例化,偏移和不透明度需要实例化 + gl.bindVertexArray(tileVAO); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, array.tileVertex, gl.DYNAMIC_DRAW); + gl.vertexAttribPointer(vaLocation, 3, gl.FLOAT, false, 6 * 4, 0); + gl.vertexAttribPointer(tcaLocation, 3, gl.FLOAT, false, 6 * 4, 3 * 4); + gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer); + gl.bufferData(gl.ARRAY_BUFFER, array.tileOffset, gl.DYNAMIC_DRAW); + gl.vertexAttribIPointer(oaLocation, 2, gl.SHORT, 0, 0); + gl.vertexAttribDivisor(oaLocation, 1); + gl.bindBuffer(gl.ARRAY_BUFFER, alphaBuffer); + gl.bufferData(gl.ARRAY_BUFFER, array.tileAlpha, gl.DYNAMIC_DRAW); + gl.vertexAttribPointer(aaLocation, 1, gl.FLOAT, false, 0, 0); + gl.vertexAttribDivisor(aaLocation, 1); + gl.bindVertexArray(null); + } + + /** + * 传递静态背景图 + * @param gl 画布上下文 + * @param data 上下文数据 + * @param source 图像源 + */ + private texStaticBackground( + gl: WebGL2RenderingContext, + data: IContextData, + source: ImageBitmap + ) { + const { width: w, height: h } = source; + if ( + w !== data.backgroundWidth || + h !== data.backgroundHeight || + data.backgroundDepth !== 1 + ) { + gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, w, h, 1); + data.backgroundWidth = w; + data.backgroundHeight = h; + data.backgroundDepth = 1; + } + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, + 0, + 0, + w, + h, + 1, + gl.RGBA, + gl.UNSIGNED_BYTE, + source + ); + } + + /** + * 传递动态背景图 + * @param gl 画布上下文 + * @param data 上下文数据 + * @param width 图像源宽度 + * @param height 图像源高度 + * @param source 图像源列表,要求图像源尺寸一致 + */ + private texDynamicBackground( + gl: WebGL2RenderingContext, + data: IContextData, + width: number, + height: number, + source: ImageBitmap[] + ) { + const w = width; + const h = height; + const depth = source.length; + if ( + w !== data.backgroundWidth || + h !== data.backgroundHeight || + data.backgroundDepth !== depth + ) { + gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, w, h, depth); + data.backgroundWidth = w; + data.backgroundHeight = h; + data.backgroundDepth = depth; + } + source.forEach((v, i) => { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, + 0, + i, + w, + h, + 1, + gl.RGBA, + gl.UNSIGNED_BYTE, + v + ); + }); + } + + /** + * 更新背景图的顶点数组。如果使用了图片尺寸作为渲染尺寸,则使用 `width` `height` 参数, + * 否则使用 `this.backRenderWidth` 和 `this.backRenderHeight` + * @param gl WebGL2 上下文 + * @param data 上下文数据 + * @param width 图片宽度 + * @param height 图片高度 + */ + private updateBackgroundVertex( + gl: WebGL2RenderingContext, + data: IContextData, + width: number, + height: number + ) { + this.backgroundWidth = width; + this.backgroundHeight = height; + const w = this.backUseImageSize ? width : this.backRenderWidth; + const h = this.backUseImageSize ? height : this.backRenderHeight; + if (w === 0 || h === 0) return; + const rw = w / this.renderWidth; + const rh = h / this.renderHeight; + const vx = 1 / rw; + const vy = 1 / rh; + const arr = this.backgroundVertex; + // 左下角 + arr[2] = 0; + arr[3] = vy; + // 右下角 + arr[6] = vx; + arr[7] = vy; + // 左上角 + arr[10] = 0; + arr[11] = 0; + // 右上角 + arr[14] = vx; + arr[15] = 0; + gl.bindBuffer(gl.ARRAY_BUFFER, data.backgroundVertexBuffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, arr); + } + + /** + * 使用静态图片作为背景 + * @param gl WebGL2 上下文 + * @param data 上下文数据 + * @param renderable 可渲染对象 + */ + private async useStaticBackground( + gl: WebGL2RenderingContext, + data: IContextData, + renderable: ITextureRenderable + ) { + const { rect, source } = renderable; + const { x, y, w, h } = rect; + if ( + source.width === w && + source.height === h && + source instanceof ImageBitmap + ) { + // 如果图块的纹理直接就是整个图像源,那么直接传递,就不需要再创建位图了 + this.texStaticBackground(gl, data, source); + } else { + // 否则需要单独创建位图 + const image = await createImageBitmap(source, x, y, w, h); + this.texStaticBackground(gl, data, image); + } + // 更新顶点数组 + this.updateBackgroundVertex(gl, data, w, h); + this.backgroundFrameCount = 1; + } + + /** + * 使用动态图片作为背景 + * @param gl WebGL2 上下文 + * @param data 上下文数据 + * @param renderable 可渲染对象列表 + */ + private async useDynamicBackground( + gl: WebGL2RenderingContext, + data: IContextData, + renderable: ITextureRenderable[] + ) { + if (renderable.length === 0) { + // 纹理不包含动画可渲染数据 + logger.error(36); + return; + } + const { w, h } = renderable[0].rect; + if (renderable.some(v => v.rect.w !== w || v.rect.h !== h)) { + // 如果纹理每帧尺寸不一致 + logger.error(37); + return; + } + const images = await Promise.all( + renderable.map(v => { + const { x, y, w, h } = v.rect; + return createImageBitmap(v.source, x, y, w, h); + }) + ); + this.texDynamicBackground(gl, data, w, h, images); + this.updateBackgroundVertex(gl, data, w, h); + this.backgroundHeight = renderable.length; + } + + /** + * 使用图块作为背景,当绑定过 VAO 与纹理后再调用此方法 + * @param gl 画布上下文 + * @param data 上下文数据 + * @param tile 使用的背景图块 + */ + private useTileBackground( + gl: WebGL2RenderingContext, + data: IContextData, + tile: number + ): Promise { + // 图块背景 + const tex = this.manager.getIfBigImage(tile); + if (!tex) { + // 图块不存在 + logger.error(35); + return Promise.resolve(); + } + this.backgroundFrameCount = tex.frames; + if (tex.frames === 1) { + // 对于一帧图块,只需要传递一个纹理 + if (tex.cls === BlockCls.Autotile) { + const renderable = this.autotile.renderWithoutCheck( + tex, + 0b1111_1111 + )!; + return this.useStaticBackground(gl, data, renderable); + } else { + return this.useStaticBackground(gl, data, tex.texture.render()); + } + } else { + // 多帧图块 + if (tex.cls === BlockCls.Autotile) { + const gen = this.autotile.renderAnimatedWith(tex, 0b1111_1111); + return this.useDynamicBackground(gl, data, [...gen]); + } else { + const gen = this.tileAnimater.once(tex.texture, tex.frames); + return this.useDynamicBackground(gl, data, [...gen]); + } + } + } + + private async checkBackground( + gl: WebGL2RenderingContext, + data: IContextData + ) { + if (!this.backgroundDirty || this.backgroundPending) return; + this.backgroundPending = true; + const { backVAO, backgroundTexture } = data; + gl.bindVertexArray(backVAO); + gl.bindTexture(gl.TEXTURE_2D_ARRAY, backgroundTexture); + // 根据背景类型使用不同贴图 + switch (this.backgroundType) { + case BackgroundType.Tile: { + await this.useTileBackground(gl, data, this.tileBack); + break; + } + case BackgroundType.Static: { + if (!this.staticBack) return; + await this.useStaticBackground(gl, data, this.staticBack); + break; + } + case BackgroundType.Dynamic: { + if (!this.dynamicBack) return; + await this.useDynamicBackground(gl, data, this.dynamicBack); + break; + } + } + // 重复模式 + switch (this.backRepeatModeX) { + case MapBackgroundRepeat.Repeat: { + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_WRAP_S, + gl.REPEAT + ); + break; + } + case MapBackgroundRepeat.RepeatMirror: { + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_WRAP_S, + gl.MIRRORED_REPEAT + ); + break; + } + case MapBackgroundRepeat.ClampToEdge: { + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_WRAP_S, + gl.CLAMP_TO_EDGE + ); + break; + } + } + switch (this.backRepeatModeY) { + case MapBackgroundRepeat.Repeat: { + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_WRAP_T, + gl.REPEAT + ); + break; + } + case MapBackgroundRepeat.RepeatMirror: { + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_WRAP_T, + gl.MIRRORED_REPEAT + ); + break; + } + case MapBackgroundRepeat.ClampToEdge: { + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_WRAP_T, + gl.CLAMP_TO_EDGE + ); + break; + } + } + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_MAG_FILTER, + gl.NEAREST + ); + gl.texParameteri( + gl.TEXTURE_2D_ARRAY, + gl.TEXTURE_MIN_FILTER, + gl.NEAREST + ); + gl.bindVertexArray(null); + this.backgroundPending = false; + } + + /** + * 检查偏移数组是否需要更新 + * @param gl 画布上下文 + * @param data 上下文数据 + */ + private checkOffsetPool(gl: WebGL2RenderingContext, data: IContextData) { + if (!this.needUpdateOffsetPool) return; + gl.uniform1iv(data.offsetPoolLocation, this.normalizedOffsetPool); + } + + render(): void { + const gl = this.gl; + const data = this.contextData; + if (!this.assetData) { + logger.error(31); + return; + } + console.time('render-map'); + + // 图层检查 + if (this.layerDirty) { + this.vertex.updateLayerArray(); + this.layerDirty = false; + } + if (this.layerSizeDirty) { + this.vertex.resizeMap(); + this.layerSizeDirty = false; + } + this.vertex.checkRebuild(); + + // 数据检查 + this.checkTexture(gl, data); + this.checkTileVertexArray(gl, data); + this.checkOffsetPool(gl, data); + + const area = this.viewport.getRenderArea(); + area.blockList.forEach(v => { + this.vertex.updateBlockCache(v); + }); + if (area.dirty.length > 0) { + // 如果需要更新顶点数组... + const { vertexBuffer, offsetBuffer, alphaBuffer } = data; + const array = this.vertex.getVertexArray(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + area.dirty.forEach(v => { + gl.bufferSubData( + gl.ARRAY_BUFFER, + // float32 需要 * 4 + v.startIndex * 6 * 6 * 4, + array.tileVertex, + v.startIndex * 6 * 6, + v.count * 6 * 6 + ); + }); + gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer); + area.dirty.forEach(v => { + gl.bufferSubData( + gl.ARRAY_BUFFER, + // int16 需要 * 2 + v.startIndex * 2 * 2, + array.tileOffset, + v.startIndex * 2, + v.count * 2 + ); + }); + gl.bindBuffer(gl.ARRAY_BUFFER, alphaBuffer); + area.dirty.forEach(v => { + gl.bufferSubData( + gl.ARRAY_BUFFER, + // float32 需要 * 4 + v.startIndex * 4, + array.tileAlpha, + v.startIndex, + v.count + ); + }); + } + + // 背景 + console.time('render-call'); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.useProgram(data.backProgram); + gl.bindVertexArray(data.backVAO); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + // 图块 + gl.useProgram(data.tileProgram); + gl.bindVertexArray(data.tileVAO); + area.render.forEach(v => { + gl.drawArraysInstanced(gl.TRIANGLES, v.startIndex, v.count, 6); + }); + gl.bindVertexArray(null); + console.timeEnd('render-call'); + console.timeEnd('render-map'); + } + + //#endregion + + //#region 地图处理 + + /** + * 更新指定图层的指定区域 + * @param layer 更新的图层 + * @param x 左上角横坐标 + * @param y 左上角纵坐标 + * @param w 区域宽度 + * @param h 区域高度 + */ + updateLayerArea( + layer: IMapLayer, + x: number, + y: number, + w: number, + h: number + ) { + this.vertex.updateArea(layer, x, y, w, h); + } + + /** + * 更新指定图层的指定图块 + * @param layer 更新的图层 + * @param block 更新为的图块 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + updateLayerBlock(layer: IMapLayer, block: number, x: number, y: number) { + this.vertex.updateBlock(layer, block, x, y); + } + + //#endregion + + //#region 移动图块处理 + + /** + * 扩大移动数组 + */ + private expandMoving() { + const start = this.movingCount; + this.movingCount *= 2; + this.vertex.expandMoving(this.movingCount); + this.movingIndexPool.push( + // 翻转数组是因为这样的话内容会优先取到低索引的内容,更容易优化 + ...Array.from({ length: start }, (_, i) => i + start).reverse() + ); + } + + /** + * 减小移动数组 + */ + private reduceMoving() { + const half = Math.round(this.movingCount / 2); + if (half < DYNAMIC_RESERVE) return; + if (this.movingIndexPool.length < half) return; + const needMap: number[] = []; + const restPool: number[] = []; + this.movingIndexPool.forEach(v => { + if (v < half) restPool.push(v); + else needMap.push(v); + }); + // 这个判断理论上不可能成立,但是还是判断下吧 + if (needMap.length > restPool.length) return; + const map = new Map(); + needMap.forEach(v => { + const item = restPool.pop()!; + map.set(v, item); + }); + this.vertex.reduceMoving(half, map); + } + + /** + * 申请移动索引 + * @returns 移动索引 + */ + private requireMovingIndex(): number { + if (this.movingIndexPool.length === 0) { + this.expandMoving(); + } + const half = Math.max( + Math.round(this.movingCount / 2), + DYNAMIC_RESERVE + ); + if (this.movingIndexPool.length < half) { + this.lastExpandTime = this.timestamp; + } + return this.movingIndexPool.pop()!; + } + + /** + * 退回移动索引 + * @param index 退回的索引 + */ + private returnMovingIndex(index: number) { + this.movingIndexPool.push(index); + } + + addMovingBlock( + layer: IMapLayer, + block: number | IMaterialFramedData, + x: number, + y: number + ): IMovingBlock { + const index = this.requireMovingIndex(); + const moving = new MovingBlock(this, index, layer, block, x, y); + this.movingBlock.add(moving); + this.movingIndexMap.set(index, moving); + this.vertex.updateMoving(moving, true); + return moving; + } + + getMovingBlock(): Set { + return this.movingBlock; + } + + getMovingBlockByIndex(index: number): IMovingBlock | null { + return this.movingIndexMap.get(index) ?? null; + } + + deleteMoving(block: IMovingBlock): void { + this.returnMovingIndex(block.index); + this.movingBlock.delete(block); + this.movingIndexMap.delete(block.index); + this.vertex.deleteMoving(block); + } + + hasMoving(moving: IMovingBlock): boolean { + return this.movingBlock.has(moving); + } + + //#endregion + + //#region 图块配置 + + enableTileFrameAnimate(layer: IMapLayer, x: number, y: number): void { + this.vertex.enableStaticFrameAnimate(layer, x, y); + } + + disableTileFrameAnimate(layer: IMapLayer, x: number, y: number): void { + this.vertex.disableStaticFrameAnimate(layer, x, y); + } + + setTileAlpha(layer: IMapLayer, alpha: number, x: number, y: number): void { + this.vertex.setStaticAlpha(layer, alpha, x, y); + } + + //#endregion + + //#region 其他方法 + + getTimestamp(): number { + return this.timestamp; + } + + tick(timestamp: number) { + this.timestamp = timestamp; + const { backNowFrameLocation, nowFrameLocation } = this.contextData; + + // 移动数组 + const expandDT = timestamp - this.lastExpandTime; + if (expandDT > MOVING_TOLERANCE * 1000) { + this.reduceMoving(); + this.lastExpandTime = timestamp; + } + + // 背景 + const backgroundDT = timestamp - this.backLastFrame; + if (backgroundDT > this.backFrameSpeed) { + const last = this.backgroundFrame; + this.backgroundFrame++; + this.backgroundFrame %= this.backgroundFrameCount; + this.backLastFrame = timestamp; + if (last !== this.backgroundFrame) { + this.gl.uniform1f(backNowFrameLocation, this.backgroundFrame); + } + } + + // 地图帧动画 + const frameDT = timestamp - this.lastFrameTime; + if (frameDT > this.frameSpeed) { + this.frameCounter++; + this.gl.uniform1ui(nowFrameLocation, this.frameCounter); + } + + // 图块移动 + if (this.movingBlock.size > 0) { + const toUpdate: IMovingBlock[] = []; + this.movingBlock.forEach(v => { + const move = v.stepMoving(timestamp); + if (move) toUpdate.push(v); + }); + this.vertex.updateMovingList(toUpdate, false); + } + } + + updateTransform(): void { + this.render(); + } + + close(): void { + this.layers.clear(); + this.layerAlias.clear(); + this.layerZIndex.clear(); + } + + //#endregion +} + +class MapRendererExtends implements IMapLayerExtends { + readonly id: string = 'map-render'; + + constructor(readonly renderer: MapRenderer) {} + + onResize(): void { + this.renderer.resizeLayer(); + } + + onUpdateArea( + controller: IMapLayerExtendsController, + x: number, + y: number, + width: number, + height: number + ): void { + this.renderer.updateLayerArea(controller.layer, x, y, width, height); + } + + onUpdateBlock( + controller: IMapLayerExtendsController, + block: number, + x: number, + y: number + ): void { + this.renderer.updateLayerBlock(controller.layer, block, x, y); + } +} diff --git a/packages-user/client-modules/src/render/map/shader/back.frag b/packages-user/client-modules/src/render/map/shader/back.frag new file mode 100644 index 0000000..6aac789 --- /dev/null +++ b/packages-user/client-modules/src/render/map/shader/back.frag @@ -0,0 +1,11 @@ +#version 300 es +precision highp float; + +in vec3 v_texCoord; +out vec4 outColor; + +uniform sampler2DArray u_sampler; + +void main() { + outColor = texture(u_sampler, v_texCoord); +} diff --git a/packages-user/client-modules/src/render/map/shader/back.vert b/packages-user/client-modules/src/render/map/shader/back.vert new file mode 100644 index 0000000..b5e1a85 --- /dev/null +++ b/packages-user/client-modules/src/render/map/shader/back.vert @@ -0,0 +1,18 @@ +#version 300 es +precision highp float; + +in vec2 a_position; +in vec2 a_texCoord; + +out vec3 v_texCoord; + +uniform float u_nowFrame; +uniform mat3 u_transform; + +void main() { + // 背景图永远是全图都画,因此变换矩阵应该作用于纹理坐标 + vec3 texCoord = vec3(a_texCoord, 1.0); + vec3 transformed = u_transform * texCoord; + v_texCoord = vec3(transformed.xy, u_nowFrame); + gl_Position = vec4(a_position, 0.0, 1.0); +} diff --git a/packages-user/client-modules/src/render/map/shader/map.frag b/packages-user/client-modules/src/render/map/shader/map.frag new file mode 100644 index 0000000..e4cc4c3 --- /dev/null +++ b/packages-user/client-modules/src/render/map/shader/map.frag @@ -0,0 +1,14 @@ +#version 300 es +precision highp float; +precision mediump uint; + +in vec4 v_texCoord; + +out vec4 outColor; + +uniform sampler2DArray u_sampler; + +void main() { + vec4 texColor = texture(u_sampler, v_texCoord.xyz); + outColor = vec4(texColor.rgb, texColor.a * v_texCoord.a); +} diff --git a/packages-user/client-modules/src/render/map/shader/map.vert b/packages-user/client-modules/src/render/map/shader/map.vert new file mode 100644 index 0000000..34a6c19 --- /dev/null +++ b/packages-user/client-modules/src/render/map/shader/map.vert @@ -0,0 +1,26 @@ +#version 300 es +precision highp float; +precision mediump uint; + +in vec3 a_position; +in vec3 a_texCoord; +// x: 最大帧数,y: 偏移池索引,实例化绘制传入 +in ivec2 a_offsetData; +// 不透明度,用于前景层虚化 +in float a_alpha; + +// x,y,z: 纹理坐标,w: 不透明度 +out vec4 v_texCoord; + +uniform vec2 u_offsetPool[$1]; +uniform uint u_nowFrame; +uniform mat3 u_transform; + +void main() { + // 偏移量 + uint offset = mod(u_nowFrame, a_offsetData.x); + float fOffset = float(offset); + // 贴图坐标 + v_texCoord = vec4(a_texCoord.xy + u_offsetPool[a_offsetData.y] * fOffset, a_texCoord.z, a_alpha); + gl_Position = vec4(u_transform * a_position, 1.0); +} diff --git a/packages-user/client-modules/src/render/map/types.ts b/packages-user/client-modules/src/render/map/types.ts index 673f03c..a58c5a1 100644 --- a/packages-user/client-modules/src/render/map/types.ts +++ b/packages-user/client-modules/src/render/map/types.ts @@ -1,16 +1,51 @@ +import { IDirtyTracker } from '@motajs/common'; import { ITextureRenderable, SizedCanvasImageSource } from '@motajs/render-assets'; +import { Transform } from '@motajs/render-core'; import { IAutotileProcessor, + IMaterialFramedData, IMaterialGetter, IMaterialManager } from '@user/client-base'; +import { IMapLayer } from '@user/data-state'; +import { TimingFn } from 'mutate-animate'; -export interface IMapAssetData { - /** 缓存对象的标识符 */ - readonly symbol: unique symbol; +export const enum MapBackgroundRepeat { + /** 直接重复 */ + Repeat, + /** 重复,但镜像 */ + RepeatMirror, + /** 超出范围的使用边缘像素 */ + ClampToEdge +} + +export const enum MapTileBehavior { + /** 适应到格子尺寸,宽高会被设置为格子尺寸,如果比例不对会被拉伸 */ + FitToSize, + /** 保持图块尺寸,具体渲染位置会受到对齐属性的影响 */ + KeepSize +} + +export const enum MapTileSizeTestMode { + /** 只要宽度或高度有一个大于格子,那么就视为超出格子大小,会执行图块缩小行为 */ + WidthOrHeight, + /** 当宽度和高度都大于格子宽度和高度时,才视为超出格子大小,执行图块缩小行为 */ + WidthAndHeight +} + +export const enum MapTileAlign { + /** 对于水平对齐,表示与格子左侧对齐;对于竖直对齐,表示与格子上侧对齐 */ + Start, + /** 对于水平对齐,表示与格子左右居中对齐;对于竖直对齐,表示与格子上下居中对齐 */ + Center, + /** 对于水平对齐,表示与格子右侧对齐;对于竖直对齐,表示与格子下侧对齐 */ + End +} + +export interface IMapAssetData extends IDirtyTracker> { /** 图像源列表 */ readonly sourceList: ImageBitmap[]; /** @@ -18,12 +53,40 @@ export interface IMapAssetData { * 因此需要把贴图生成为额外的 `ImageBitmap`,并提供引用跳接映射。值代表在 `sourceList` 中的索引。 */ readonly skipRef: Map; - /** 被标记为脏的图像源,这些图像源经过了更新,需要重新传递给显卡 */ - readonly dirty: Set; /** 贴图数据 */ readonly materials: IMaterialGetter; } +export interface IMapBackgroundConfig { + /** 是否使用图片大小作为背景图渲染大小,如果是 `false`,则使用 `renderWidth` `renderHeight` 作为渲染大小 */ + readonly useImageSize: boolean; + /** 背景图渲染宽度,即要画多宽,单位像素 */ + readonly renderWidth: number; + /** 背景图渲染高度,即要画多高,单位像素 */ + readonly renderHeight: number; + /** 背景图在水平方向上的重复方式 */ + readonly repeatX: MapBackgroundRepeat; + /** 背景图在竖直方向上的重复方式 */ + readonly repeatY: MapBackgroundRepeat; + /** 动态背景图每帧的时长 */ + readonly frameSpeed: number; +} + +export interface IMapRenderConfig { + /** 当图块比格子大时,图块应该如何渲染 */ + readonly minBehavior: MapTileBehavior; + /** 当图块比格子小时,图块应该如何渲染 */ + readonly magBehavior: MapTileBehavior; + /** 当图块与格子大小不匹配时,图块水平对齐方式 */ + readonly tileAlignX: MapTileAlign; + /** 当图块与格子大小不匹配时,图块竖直对齐方式 */ + readonly tileAlignY: MapTileAlign; + /** 图块与网格判断大小的方式 */ + readonly tileTestMode: MapTileSizeTestMode; + /** 帧动画时长 */ + readonly frameSpeed: number; +} + export interface IMapAssetManager { /** 素材管理对象 */ readonly materials: IMaterialManager; @@ -34,142 +97,184 @@ export interface IMapAssetManager { generateAsset(): IMapAssetData; } -export interface IMapVertexData { - /** 顶点坐标数组,包含顶点坐标及对应的纹理坐标 */ - readonly position: Float32Array; - /** 帧偏移池 */ - readonly offsetList: Float32Array; - /** 偏移索引数组 */ - readonly offsetIndices: Uint8Array; +export interface IContextData { + /** 图块程序 */ + readonly tileProgram: WebGLProgram; + /** 背景程序 */ + readonly backProgram: WebGLProgram; + /** 图块顶点着色器 */ + readonly tileVertShader: WebGLShader; + /** 图块片段着色器 */ + readonly tileFragShader: WebGLShader; + /** 背景顶点着色器 */ + readonly backVertShader: WebGLShader; + /** 背景片段着色器 */ + readonly backFragShader: WebGLShader; + /** 偏移池 uniform 地址 */ + readonly offsetPoolLocation: WebGLUniformLocation; + /** 当前帧 uniform 地址 */ + readonly nowFrameLocation: WebGLUniformLocation; + /** 图块采样器 uniform 地址 */ + readonly tileSamplerLocation: WebGLUniformLocation; + /** 图块变换矩阵 uniform 地址 */ + readonly tileTransformLocation: WebGLUniformLocation; + /** 背景采样器 uniform 地址 */ + readonly backSamplerLocation: WebGLUniformLocation; + /** 背景变换矩阵 uniform 地址 */ + readonly backTransformLocation: WebGLUniformLocation; + /** 背景图当前帧数 uniform 地址 */ + readonly backNowFrameLocation: WebGLUniformLocation; + /** 顶点数组输入 */ + readonly vertexAttribLocation: number; + /** 纹理坐标输入输入 */ + readonly texCoordAttribLocation: number; + /** 偏移数组输入 */ + readonly offsetAttribLocation: number; + /** 不透明度数组输入 */ + readonly alphaAttribLocation: number; + /** 背景顶点数组输入 */ + readonly backVertexAttribLocation: number; + /** 背景纹理数组输入 */ + readonly backTexCoordAttribLocation: number; + /** 顶点数组 */ + readonly vertexBuffer: WebGLBuffer; + /** 偏移数组 */ + readonly offsetBuffer: WebGLBuffer; + /** 不透明度数组 */ + readonly alphaBuffer: WebGLBuffer; + /** 背景顶点数组 */ + readonly backgroundVertexBuffer: WebGLBuffer; + /** 图块纹理对象 */ + readonly tileTexture: WebGLTexture; + /** 背景纹理对象 */ + readonly backgroundTexture: WebGLTexture; + /** 图块程序的 VAO */ + readonly tileVAO: WebGLVertexArrayObject; + /** 背景程序的 VAO */ + readonly backVAO: WebGLVertexArrayObject; + + /** 当前画布的图块纹理宽度 */ + tileTextureWidth: number; + /** 当前画布的图块纹理高度 */ + tileTextureHeight: number; + /** 当前画布的图块纹理深度 */ + tileTextureDepth: number; + /** 当前画布的背景纹理宽度 */ + backgroundWidth: number; + /** 当前画布的背景纹理高度 */ + backgroundHeight: number; + /** 当前画布的背景纹理深度 */ + backgroundDepth: number; + + /** 图块纹理的脏标记 */ + tileTextureMark: symbol; + /** 顶点数组的脏标记 */ + vertexMark: symbol; } -export interface IMapLayerHooks { - /** - * 当钩子准备完毕时执行,会自动分析依赖,并把依赖实例作为参数传入,遵循依赖列表的顺序 - * @param dependencies 依赖列表 - */ - awake(...dependencies: IMapLayerHooks[]): void; +export interface IMovingBlock { + /** 移动图块的索引 */ + readonly index: number; + /** 图块数字 */ + readonly tile: number; + /** 图块横坐标 */ + readonly x: number; + /** 图块纵坐标 */ + readonly y: number; + /** 图块使用的纹理 */ + readonly texture: IMaterialFramedData; + /** 该图块所属的图层 */ + readonly layer: IMapLayer; /** - * 当拓展被移除之前执行,可以用来清理相关内容 + * 沿直线移动到目标点 + * @param x 目标横坐标,可以填小数 + * @param y 目标纵坐标,可以填小数 + * @param timing 移动的速率曲线,默认为匀速移动 */ - destroy(): void; - - /** - * 当更新某个区域的图块时执行 - * @param x 更新区域左上角横坐标 - * @param y 更新区域左上角纵坐标 - * @param width 更新区域宽度 - * @param height 更新区域高度 - */ - onUpdateArea(x: number, y: number, width: number, height: number): void; - - /** - * 当更新某个点的图块时执行 - * @param x 更新点横坐标 - * @param y 更新点纵坐标 - */ - onUpdateBlock(x: number, y: number): void; - - /** - * 当顶点数据更新时执行 - * @param data 顶点数据 - */ - onUpdateVertexData(data: IMapVertexData): void; -} - -export interface IMapLayerExtends extends Partial { - /** 这个拓展对象的标识符 */ - readonly id: string; - /** 这个拓展对象的依赖列表 */ - readonly dependencies: string[]; -} - -export interface IMapLayerExtendsController { - /** - * 获取地图数据,是对内部存储的直接引用 - */ - getMapData(): Uint32Array; - - /** - * 获取顶点数据 - */ - getVertexData(): IMapVertexData; - - /** - * 结束此对象的生命周期,释放相关资源 - */ - close(): void; -} - -export interface IMapLayer { - /** 图层的纵深,纵深高的会遮挡纵深低的 */ - readonly zIndex: number; - - /** - * 使用指定图集对象 - * @param asset 要使用的缓存对象 - */ - useAsset(asset: IMapAssetData): void; - - /** - * 设置某一点的图块 - * @param block 图块数字 - * @param x 图块横坐标 - * @param y 图块纵坐标 - */ - setBlock(block: number, x: number, y: number): void; - - /** - * 设置地图图块 - * @param array 地图图块数组 - * @param x 数组第一项代表的横坐标 - * @param y 数组第一项代表的纵坐标 - * @param width 传入数组所表示的矩形范围的宽度 - */ - putMapData(array: Uint32Array, x: number, y: number, width: number): void; - - /** - * 获取整个地图的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容 - */ - getMapData(): Uint32Array; - /** - * 获取地图指定区域的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容 - * @param x 左上角横坐标 - * @param y 左上角纵坐标 - * @param width 获取区域的宽度 - * @param height 获取区域的高度 - */ - getMapData( + lineTo( x: number, y: number, - width: number, - height: number - ): Uint32Array; + time: number, + timing?: TimingFn + ): Promise; /** - * 添加图层拓展,使用一系列钩子与图层本身通讯 - * @param ex 图层拓展对象 - * @returns 图层拓展控制对象,可以通过它来控制拓展的生命周期,也可以用于获取图层内的一些数据 + * 按照指定曲线移动,使用绝对模式,即 `curve` 输出值就是图块当前位置 + * @param curve 移动曲线,接收完成度作为参数,输出图块应该在的位置 + * @param timing 移动的速率曲线,默认为匀速移动 */ - addExtends(ex: IMapLayerExtends): IMapLayerExtendsController; + moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise; /** - * 移除指定的图层拓展对象 - * @param ex 要移除的图层拓展对象,也可以填拓展对象的标识符 + * 按照指定曲线移动,使用相对模式,即 `curve` 输出值与原始坐标相加得到当前位置 + * @param curve 移动曲线,接收完成度作为参数,输出图块应该在的位置 + * @param timing 移动的速率曲线,默认为匀速移动 */ - removeExtends(ex: IMapLayerExtends | string): void; + moveRelative( + curve: TimingFn<2>, + time: number, + timing?: TimingFn + ): Promise; + + /** + * 启用此图块的帧动画 + */ + enableFrameAnimate(): void; + + /** + * 禁用此图块的帧动画 + */ + disableFrameAnimate(): void; + + /** + * 设置此图块的不透明度 + * @param alpha 不透明度 + */ + setAlpha(alpha: number): void; + + /** + * 进行一步动画移动效果 + * @param timestamp 当前时间戳 + * @returns 图块是否发生了移动 + */ + stepMoving(timestamp: number): boolean; + + /** + * 摧毁这个移动图块对象,之后不会再显示到画面上 + */ + destroy(): void; } -/** - * 地图渲染器,本身不包含画布,只能渲染至传入的画布中。可以传入多个 WebGL2 上下文的画布,在不同的画布上渲染。 - * 目前不建议把画布还用于其他渲染,因为状态切换可能会导致地图渲染性能下降。 - */ export interface IMapRenderer { /** 地图渲染器使用的资源管理器 */ readonly manager: IMaterialManager; + /** 画布渲染上下文 */ + readonly gl: WebGL2RenderingContext; + /** 自动元件处理对象 */ readonly autotile: IAutotileProcessor; + /** 地图变换矩阵 */ + readonly transform: Transform; + /** 地图渲染的视角控制 */ + readonly viewport: IMapViewportController; + /** 顶点数组生成器 */ + readonly vertex: IMapVertexGenerator; + + /** 地图宽度 */ + readonly mapWidth: number; + /** 地图高度 */ + readonly mapHeight: number; + /** 图层数量 */ + readonly layerCount: number; + /** 渲染宽度 */ + readonly renderWidth: number; + /** 渲染高度 */ + readonly renderHeight: number; + /** 每个格子的宽度 */ + readonly cellWidth: number; + /** 每个格子的高度 */ + readonly cellHeight: number; /** * 使用指定图集对象 @@ -178,10 +283,9 @@ export interface IMapRenderer { useAsset(asset: IMapAssetData): void; /** - * 初始化指定的 WebGL2 上下文,可以初始化多个上下文 - * @param gl 需要初始化的上下文 + * 摧毁此地图渲染器,表示当前渲染器不会再被使用到 */ - initContext(gl: WebGL2RenderingContext): void; + destroy(): void; /** * 渲染至目标画布 @@ -209,10 +313,41 @@ export interface IMapRenderer { getLayer(identifier: string): IMapLayer | null; /** - * 获取排序后的图层列表 + * 判断当前渲染器是否包含指定图层对象 + * @param layer 图层对象 + */ + hasLayer(layer: IMapLayer): boolean; + + /** + * 获取排序后的图层列表,是内部引用的副本,不是对内部的直接引用,不具有实时性 */ getSortedLayer(): IMapLayer[]; + /** + * 设置当前渲染器中指定图层的纵深 + * @param layer 要设置的图层 + * @param zIndex 目标纵深 + */ + setZIndex(layer: IMapLayer, zIndex: number): void; + + /** + * 获取指定图层的纵深 + * @param layer 要获取的图层 + */ + getZIndex(layer: IMapLayer): number | undefined; + + /** + * 获取指定图层排序后的索引位置 + * @param layer 要获取的图层 + */ + getLayerIndex(layer: IMapLayer): number; + + /** + * 获取指定偏移值在偏移池中的索引 + * @param offset 原始偏移值,非归一化偏移值 + */ + getOffsetIndex(offset: number): number; + /** * 使用静态图片作为地图背景图 * @param renderable 可渲染对象 @@ -231,8 +366,648 @@ export interface IMapRenderer { */ setTileBackground(tile: number): void; + /** + * 配置背景图片的渲染方式,仅对静态与动态背景图有效,对图块背景图无效 + * @param config 背景图片配置 + */ + configBackground(config: Partial): void; + + /** + * 配置渲染器的渲染设置 + * @param config 渲染设置 + */ + configRendering(config: Partial): void; + + /** + * 设置渲染的宽高,单位格子。例如填 13 就表示渲染宽度或高度是 13 个格子。 + * @param width 渲染的格子宽度 + * @param height 渲染的格子高度 + */ + setRenderSize(width: number, height: number): void; + + /** + * 获取背景渲染设置。并不是内部存储的引用,不会实时更新。 + */ + getBackgroundConfig(): IMapBackgroundConfig; + + /** + * 获取地图渲染设置。并不是内部存储的引用,不会实时更新。 + */ + getRenderingConfig(): IMapRenderConfig; + + /** + * 设置每个格子的宽高 + * @param width 每个格子的宽度 + * @param height 每个格子的高度 + */ + setCellSize(width: number, height: number): void; + + /** + * 获取变换矩阵 + */ + getTransform(): Transform; + + /** + * 添加一个移动图块 + * @param layer 图块所属的图层 + * @param block 图块数字或图块的素材对象,要求可渲染对象的图像源必须出现在图集中 + * @param x 图块初始横坐标,可以填小数 + * @param y 图块初始纵坐标,可以填小数 + * @returns 移动图块对象,可以用于控制图块移动 + */ + addMovingBlock( + layer: IMapLayer, + block: number | IMaterialFramedData, + x: number, + y: number + ): Readonly; + + /** + * 获取所有的移动图块 + */ + getMovingBlock(): Set>; + + /** + * 根据索引获取移动图块对象 + * @param index 移动图块索引 + */ + getMovingBlockByIndex(index: number): Readonly | null; + + /** + * 启用指定图块的帧动画效果 + * @param layer 图层 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + enableTileFrameAnimate(layer: IMapLayer, x: number, y: number): void; + + /** + * 禁用指定图块的帧动画效果 + * @param layer 图层 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + disableTileFrameAnimate(layer: IMapLayer, x: number, y: number): void; + + /** + * 设置指定图块的不透明度 + * @param layer 图层 + * @param alpha 图块不透明度 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + setTileAlpha(layer: IMapLayer, alpha: number, x: number, y: number): void; + /** * 结束此渲染器的使用,释放所有相关资源 */ close(): void; } + +export interface IMapVertexArray { + /** + * 地图渲染顶点数组,结构是一个三维张量 `[B, L, T]`,其中 `B` 代表分块,`L` 代表图层,`T` 代表图块,并按照结构顺序平铺存储。 + * 每个顶点包含两个数据,顶点的 `x,y,z` 坐标,顶点的贴图坐标 `x,y,z`。 + * + * 语义解释就是,最内层存储图块,再外面一层存储图层,最外层存储分块。这样的话可以一次性将一个分块的所有图层渲染完毕。 + */ + readonly tileVertex: Float32Array; + + /** 每个图块的偏移数据,使用实例化绘制,第一项表示这个图块的总帧数,第二项表示每帧的偏移量 */ + readonly tileOffset: Int16Array; + /** 每个图块的不透明度,用于实现前景层虚化效果 */ + readonly tileAlpha: Float32Array; + + /** 动态内容顶点起始索引 */ + readonly dynamicStart: number; + /** 动态内容顶点数量 */ + readonly dynamicCount: number; +} + +export interface IBlockIndex { + /** 横坐标 */ + readonly x: number; + /** 纵坐标 */ + readonly y: number; + /** 分块左上角数据的横坐标 */ + readonly dataX: number; + /** 分块左上角数据的纵坐标 */ + readonly dataY: number; + /** 索引,等于 横坐标+纵坐标*宽度 */ + readonly index: number; +} + +export interface IBlockInfo extends IBlockIndex { + /** 分块宽度 */ + readonly width: number; + /** 分块高度 */ + readonly height: number; +} + +export interface IBlockData extends IBlockInfo { + /** 这个分块的数据 */ + data: T; + + /** + * 获取这个分块左方的分块 + */ + left(): IBlockData | null; + + /** + * 获取这个分块右方的分块 + */ + right(): IBlockData | null; + + /** + * 获取这个分块上方的分块 + */ + up(): IBlockData | null; + + /** + * 获取这个分块下方的分块 + */ + down(): IBlockData | null; + + /** + * 获取这个分块左上方的分块 + */ + leftUp(): IBlockData | null; + + /** + * 获取这个分块左下方的分块 + */ + leftDown(): IBlockData | null; + + /** + * 获取这个分块右上方的分块 + */ + rightUp(): IBlockData | null; + + /** + * 获取这个分块右下方的分块 + */ + rightDown(): IBlockData | null; + + /** + * 获取下一个索引的分块 + */ + next(): IBlockData | null; +} + +export interface IBlockSplitterConfig { + /** 分块宽度 */ + readonly blockWidth: number; + /** 分块高度 */ + readonly blockHeight: number; + /** 数据宽度 */ + readonly dataWidth: number; + /** 数据高度 */ + readonly dataHeight: number; +} + +export interface IBlockSplitter extends IBlockSplitterConfig { + /** 宽度,即横向有多少分块 */ + readonly width: number; + /** 高度,即纵向有多少分块 */ + readonly height: number; + + /** + * 根据坐标获取分块内容 + * @param x 横坐标 + * @param y 纵坐标 + */ + getBlockByLoc(x: number, y: number): IBlockData | null; + + /** + * 根据分块索引获取分块内容 + * @param index 分块索引 + */ + getBlockByIndex(index: number): IBlockData | null; + + /** + * 根据分块坐标设置分块内容 + * @param data 分块数据 + * @param x 分块横坐标 + * @param y 分块纵坐标 + * @returns 分块内容 + */ + setBlockByLoc(data: T, x: number, y: number): IBlockData | null; + + /** + * 根据分块索引设置分块内容 + * @param data 分块数据 + * @param index 分块索引 + * @returns 分块内容 + */ + setBlockByIndex(data: T, index: number): IBlockData | null; + + /** + * 根据分块坐标,遍历指定分块中的所有坐标(分块 -> 元素) + * @param x 分块横坐标 + * @param y 分块纵坐标 + */ + iterateBlockByLoc(x: number, y: number): Generator; + + /** + * 根据分块索引,遍历指定分块中的所有坐标(分块 -> 元素) + * @param index 分块索引 + */ + iterateBlockByIndex(index: number): Generator; + + /** + * 根据分块索引列表,依次遍历每个分块的所有坐标(分块 -> 元素) + * @param indices 分块索引列表 + */ + iterateBlockByIndices( + indices: Iterable + ): Generator; + + /** + * 遍历所有的分块(分块 -> 分块) + */ + iterateBlocks(): Iterable>; + + /** + * 传入指定的数据区域,对其所涉及的分块依次遍历(元素 -> 分块) + * @param x 数据左上角横坐标 + * @param y 数据左上角纵坐标 + * @param width 数据宽度 + * @param height 数据高度 + */ + iterateBlocksOfDataArea( + x: number, + y: number, + width: number, + height: number + ): Generator>; + + /** + * 根据分块坐标获取分块索引,如果坐标超出范围,返回 -1(分块 -> 分块) + * @param x 分块横坐标 + * @param y 分块纵坐标 + */ + getIndexByLoc(x: number, y: number): number; + + /** + * 根据分块索引获取分块坐标,如果索引超出范围,返回 `null`(分块 -> 分块) + * @param index 分块索引 + */ + getLocByIndex(index: number): Loc | null; + + /** + * 根据坐标列表,获取对应的分块索引,并组成一个列表(分块 -> 分块) + * @param list 坐标列表 + */ + getIndicesByLocList(list: Iterable): Iterable; + + /** + * 根据索引列表,获取对应的分块坐标,并组成一个列表(分块 -> 分块) + * @param list 索引列表 + */ + getLocListByIndices(list: Iterable): Iterable; + + /** + * 根据数据坐标获取其对应分块的信息(元素 -> 分块) + * @param x 数据横坐标 + * @param y 数据纵坐标 + */ + getBlockByDataLoc(x: number, y: number): IBlockData | null; + + /** + * 根据数据索引获取其对应分块的信息(元素 -> 分块) + * @param index 数据索引 + */ + getBlockByDataIndex(index: number): IBlockData | null; + + /** + * 根据数据的坐标列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块) + * @param list 数据坐标列表 + */ + getIndicesByDataLocList(list: Iterable): Set; + + /** + * 根据数据的索引列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块) + * @param list 数据索引列表 + */ + getIndicesByDataIndices(list: Iterable): Set; + + /** + * 根据数据的坐标列表,获取数据所在的分块内容,并组成一个集合(元素 -> 分块) + * @param list 数据坐标列表 + */ + getBlocksByDataLocList(list: Iterable): Set>; + + /** + * 根据数据的索引列表,获取数据所在的分块内容,并组成一个集合(元素 -> 分块) + * @param list 数据索引列表 + */ + getBlocksByDataIndices(list: Iterable): Set>; + + /** + * 配置此分块切分器,此行为不会清空分块数据,只有在执行下一次分块时才会生效 + * @param config 切分器配置 + */ + configSplitter(config: IBlockSplitterConfig): void; + + /** + * 执行分块操作,对每个分块执行函数,获取分块数据 + * @param mapFn 对每个分块执行的函数 + */ + splitBlocks(mapFn: (block: IBlockInfo) => T): void; +} + +export interface IMapVertexData { + /** 这个分块的顶点数组 */ + readonly vertexArray: Float32Array; + /** 这个分块的偏移数组 */ + readonly offsetArray: Int16Array; + /** 这个分块的不透明度数组 */ + readonly alphaArray: Float32Array; +} + +export interface IIndexedMapVertexData extends IMapVertexData { + /** 这个分块的顶点数组的起始索引 */ + readonly vertexStart: number; + /** 这个分块的偏移数组的起始索引 */ + readonly offsetStart: number; + /** 这个分块的不透明度数组的起始索引 */ + readonly alphaStart: number; +} + +export interface ILayerDirtyData { + /** 是否需要更新顶点数组 */ + dirty: boolean; + /** 脏区域左边缘 */ + dirtyLeft: number; + /** 脏区域上边缘 */ + dirtyTop: number; + /** 脏区域右边缘 */ + dirtyRight: number; + /** 脏区域下边缘 */ + dirtyBottom: number; +} + +export interface IMapVertexBlock extends IMapVertexData { + /** 当前区域是否需要更新 */ + readonly dirty: boolean; + /** 渲染是否需要更新 */ + readonly renderDirty: boolean; + /** 起始索引,即第一个元素索引 */ + readonly startIndex: number; + /** 终止索引,即最后一个元素索引+1 */ + readonly endIndex: number; + /** 元素数量,即终止索引-起始索引 */ + readonly count: number; + + /** + * 取消渲染的脏标记 + */ + render(): void; + + /** + * 标记指定区域为脏,需要更新 + * @param layer 图层对象 + * @param left 标记区域左边缘,相对于分块,即分块左上角为 `0,0`,包含 + * @param top 标记区域上边缘,相对于分块,即分块左上角为 `0,0`,包含 + * @param right 标记区域右边缘,相对于分块,即分块左上角为 `0,0`,不包含 + * @param bottom 标记区域下边缘,相对于分块,即分块左上角为 `0,0`,不包含 + */ + markDirty( + layer: IMapLayer, + left: number, + top: number, + right: number, + bottom: number + ): void; + + /** + * 获取图层需要更新的区域 + * @param layer 图层 + */ + getDirtyArea(layer: IMapLayer): Readonly | null; + + /** + * 获取指定图层的顶点数组,是对内部存储的直接引用 + * @param layer 图层对象 + */ + getLayerVertex(layer: IMapLayer): Float32Array | null; + + /** + * 获取指定图层的偏移数组,是对内部存储的直接引用 + * @param layer 图层对象 + */ + getLayerOffset(layer: IMapLayer): Int16Array | null; + + /** + * 获取指定图层的不透明度数组,是对内部存储的直接引用 + * @param layer 图层对象 + */ + getLayerAlpha(layer: IMapLayer): Float32Array | null; + + /** + * 获取指定图层的所有顶点数组数据,是对内部存储的直接引用 + * @param layer 图层对象 + */ + getLayerData(layer: IMapLayer): IMapVertexData | null; +} + +export interface IMapBlockUpdateObject { + /** 要更新为的图块数字 */ + readonly block: number; + /** 图块横坐标 */ + readonly x: number; + /** 图块纵坐标 */ + readonly y: number; +} + +export interface IMapVertexGenerator extends IDirtyTracker { + /** 地图渲染器 */ + readonly renderer: IMapRenderer; + /** 地图分块 */ + readonly block: IBlockSplitter; + /** 动态部分是否需要更新渲染缓冲区 */ + readonly dynamicRenderDirty: boolean; + + /** + * 取消渲染的脏标记 + */ + renderDynamic(): void; + + /** + * 设置分块大小。一般设置为与画面大小一致,这样在多数情况下性能最优。 + * 不建议主动调用此方法,因为此方法会重建顶点数组,对性能影响较大。 + * @param width 分块宽度 + * @param height 分块高度 + */ + setBlockSize(width: number, height: number): void; + + /** + * 重设地图尺寸 + */ + resizeMap(): void; + + /** + * 扩大移动图块数组尺寸 + * @param targetSize 目标大小 + */ + expandMoving(targetSize: number): void; + + /** + * 缩小移动图块数组尺寸 + * @param targetSize 目标大小 + * @param indexMap 索引映射。由于缩小尺寸后,有可能有一些移动的图块位于较高的索引, + * 因此需要一个映射来指定之前的那些移动图块现在应该在哪个索引。 + */ + reduceMoving(targetSize: number, indexMap: Map): void; + + /** + * 更新图层数组 + */ + updateLayerArray(): void; + + /** + * 检查是否需要重建数组 + */ + checkRebuild(): void; + + /** + * 获取顶点数组,是对内部存储的直接引用,但是内部存储在重新分配内存时引用会丢失 + */ + getVertexArray(): IMapVertexArray; + + /** + * 更新指定区域内的所有图块 + * @param layer 更新的图层 + * @param x 更新区域左上角横坐标 + * @param y 更新区域左上角纵坐标 + * @param w 更新区域宽度 + * @param h 更新区域高度 + */ + updateArea( + layer: IMapLayer, + x: number, + y: number, + w: number, + h: number + ): void; + + /** + * 更新指定图块 + * @param layer 更新的图层 + * @param block 设置为的图块 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + updateBlock(layer: IMapLayer, block: number, x: number, y: number): void; + + /** + * 更新一系列图块,适用于散点图块,如果需要更新某个区域,请换用 {@link updateArea} + * @param layer 更新的图层 + * @param blocks 要更新的图块列表 + */ + updateBlockList(layer: IMapLayer, blocks: IMapBlockUpdateObject[]): void; + + /** + * 更新指定分块的数据,专门用于懒更新 + * @param block 要更新的分块 + */ + updateBlockCache(block: Readonly>): void; + + /** + * 更新指定的移动图块对象,可以用于新增移动图块 + * @param moving 要更新的移动图块对象 + * @param updateTexture 是否更新贴图信息 + */ + updateMoving(moving: IMovingBlock, updateTexture: boolean): void; + + /** + * 更新一系列移动图块,可以用于新增移动图块 + * @param moving 移动图块列表 + * @param updateTexture 是否更新贴图信息 + */ + updateMovingList(moving: IMovingBlock[], updateTexture: boolean): void; + + /** + * 移除指定的移动图块 + * @param moving 移动图块对象 + */ + deleteMoving(moving: IMovingBlock): void; + + /** + * 启用静态内容的帧动画 + * @param layer 图层索引 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + enableStaticFrameAnimate(layer: IMapLayer, x: number, y: number): void; + + /** + * 禁用静态内容的帧动画 + * @param layer 图层索引 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + disableStaticFrameAnimate(layer: IMapLayer, x: number, y: number): void; + + /** + * 设置静态内容的不透明度 + * @param layer 图层索引 + * @param alpha 图块不透明度 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + setStaticAlpha(layer: IMapLayer, alpha: number, x: number, y: number): void; + + /** + * 启用动态内容的帧动画 + * @param block 移动图块对象 + */ + enableDynamicFrameAnimate(block: IMovingBlock): void; + + /** + * 禁用动态内容的帧动画 + * @param block 移动图块对象 + */ + disableDynamicFrameAnimate(block: IMovingBlock): void; + + /** + * 设置动态内容的不透明度 + * @param block 移动图块对象 + * @param alpha 图块的不透明度 + */ + setDynamicAlpha(block: IMovingBlock, alpha: number): void; +} + +export interface IMapRenderArea { + /** 顶点起始索引,从哪个顶点开始处理 */ + readonly startIndex: number; + /** 顶点终止索引,处理到哪个顶点 */ + readonly endIndex: number; + /** 顶点数量,即终止索引减去起始索引 */ + readonly count: number; +} + +export interface IMapRenderData { + /** 需要渲染的区域 */ + readonly render: IMapRenderArea[]; + /** 需要更新顶点数组的区域 */ + readonly dirty: IMapRenderArea[]; + /** 需要更新的分块列表 */ + readonly blockList: IBlockData[]; +} + +export interface IMapViewportController { + /** 变换矩阵 */ + readonly transform: Transform; + + /** + * 获取渲染区域,即哪些分块在画面内 + */ + getRenderArea(): IMapRenderData; + + /** + * 绑定变换矩阵 + * @param transform 变换矩阵 + */ + bindTransform(transform: Transform): void; +} diff --git a/packages-user/client-modules/src/render/map/vertex.ts b/packages-user/client-modules/src/render/map/vertex.ts new file mode 100644 index 0000000..bd394d7 --- /dev/null +++ b/packages-user/client-modules/src/render/map/vertex.ts @@ -0,0 +1,1243 @@ +import { IMapLayer, IMapLayerData } from '@user/data-state'; +import { + IBlockData, + IBlockSplitter, + IContextData, + IIndexedMapVertexData, + ILayerDirtyData, + IMapBlockUpdateObject, + IMapRenderer, + IMapVertexArray, + IMapVertexBlock, + IMapVertexData, + IMapVertexGenerator, + IMovingBlock, + MapTileAlign, + MapTileBehavior, + MapTileSizeTestMode +} from './types'; +import { logger, PrivateBooleanDirtyTracker } from '@motajs/common'; +import { DYNAMIC_RESERVE, MAP_BLOCK_HEIGHT, MAP_BLOCK_WIDTH } from '../shared'; +import { BlockSplitter } from './block'; +import { clamp, isNil } from 'lodash-es'; +import { BlockCls, IMaterialFramedData } from '@user/client-base'; +import { IRect, SizedCanvasImageSource } from '@motajs/render-assets'; + +// todo: 潜在优化点:顶点数组的 z 坐标以及纹理的 z 坐标可以换为实例化绘制 + +export interface IMapDataGetter { + /** 图块缩小行为,即图块比格子大时应该如何处理 */ + readonly tileMinifyBehavior: MapTileBehavior; + /** 图块放大行为,即图块比格子小时应该如何处理 */ + readonly tileMagnifyBehavior: MapTileBehavior; + /** 图块水平对齐,仅当图块行为为 `KeepSize` 时有效 */ + readonly tileAlignX: MapTileAlign; + /** 图块竖直对齐,仅当图块行为为 `KeepSize` 时有效 */ + readonly tileAlignY: MapTileAlign; + /** 图块大小与格子大小判断方式 */ + readonly tileTestMode: MapTileSizeTestMode; + + /** + * 获取指定图层的图块信息,是对内部存储的直接引用 + * @param layer 地图图层 + */ + getMapLayerData(layer: IMapLayer): Readonly | null; + + /** + * 根据图集的图像源获取其索引 + * @param source 图像源 + */ + getAssetSourceIndex(source: SizedCanvasImageSource): number; + + /** + * 渲染器是否包含指定的移动图块对象 + * @param moving 移动图块对象 + */ + hasMoving(moving: IMovingBlock): boolean; +} + +interface BlockMapPos { + /** 地图中的横坐标 */ + readonly mapX: number; + /** 地图中的纵坐标 */ + readonly mapY: number; +} + +interface IndexedBlockMapPos extends BlockMapPos { + /** 图块所在的图层 */ + readonly layer: IMapLayer; + /** 图块在分块中的索引 */ + readonly blockIndex: number; +} + +interface BlockIndex extends IndexedBlockMapPos { + /** 图块在分块中的横坐标 */ + readonly blockX: number; + /** 图块在分块中的纵坐标 */ + readonly blockY: number; + /** 地图中的索引 */ + readonly mapIndex: number; +} + +interface TilePosition { + /** 左边缘位置 */ + readonly left: number; + /** 上边缘位置 */ + readonly top: number; + /** 右边缘位置 */ + readonly right: number; + /** 下边缘位置 */ + readonly bottom: number; +} + +const enum VertexUpdate { + /** 更新顶点信息 */ + Vertex = 0b01, + /** 更新贴图信息 */ + Texture = 0b10, + /** 全部更新 */ + All = 0b11 +} + +/** + * 构建地图顶点数组,当且仅当数组长度发生变化时才会标记为脏,需要完全重新分配内存。 + */ +export class MapVertexGenerator + extends PrivateBooleanDirtyTracker + implements IMapVertexGenerator +{ + //#region 属性声明 + + dynamicRenderDirty: boolean = true; + + /** 空顶点数组,因为空顶点很常用,所以直接定义一个全局常量 */ + private static readonly EMPTY_VETREX: Float32Array = new Float32Array( + 6 * 6 + ); + + readonly block: IBlockSplitter; + + /** 顶点数组 */ + private vertexArray: Float32Array = new Float32Array(); + /** 偏移数组 */ + private offsetArray: Int16Array = new Int16Array(); + /** 不透明度数组 */ + private alphaArray: Float32Array = new Float32Array(); + /** 动态内容顶点数组 */ + private dynamicVertexArray: Float32Array = new Float32Array(); + /** 动态内容偏移数组 */ + private dynamicOffsetArray: Int16Array = new Int16Array(); + /** 动态内容不透明度数组 */ + private dynamicAlphaArray: Float32Array = new Float32Array(); + + /** 图层列表 */ + private layers: IMapLayer[] = []; + + /** 对应分块的顶点数组 */ + private blockList: Map = new Map(); + /** 分块宽度 */ + private blockWidth: number = MAP_BLOCK_WIDTH; + /** 分块高度 */ + private blockHeight: number = MAP_BLOCK_HEIGHT; + + /** 地图宽度 */ + private mapWidth: number = 0; + /** 地图高度 */ + private mapHeight: number = 0; + + /** 是否需要重建数组 */ + private needRebuild: boolean = false; + + /** 静态内容数组顶点数量 */ + private staticLength: number = 0; + /** 动态内容数组顶点数量。动态内容的数量无法预测,因此使用预留数量+动态扩充的方式 */ + private dynamicLength: number = DYNAMIC_RESERVE; + + /** 更新图块性能检查防抖起始时刻 */ + private updateCallDebounceTime: number = 0; + /** 更新图块性能检查的调用次数 */ + private updateCallDebounceCount: number = 0; + + constructor( + readonly renderer: IMapRenderer & IMapDataGetter, + readonly data: IContextData + ) { + super(); + this.block = new BlockSplitter(); + } + + //#endregion + + //#region 分块操作 + + private mallocVertexArray() { + // 顶点数组尺寸等于 地图大小 * 每个图块的顶点数量 * 每个顶点的数据量 + const area = this.renderer.mapWidth * this.renderer.mapHeight; + const staticCount = area * this.renderer.layerCount; + const count = staticCount + this.dynamicLength; + const vertexSize = count * 6 * 6; + const offsetSize = count * 2; + const alphaSize = count; + this.vertexArray = new Float32Array(vertexSize); + this.offsetArray = new Int16Array(offsetSize); + this.alphaArray = new Float32Array(alphaSize); + this.alphaArray.fill(1); + this.staticLength = staticCount; + this.dynamicVertexArray = this.vertexArray.subarray( + staticCount * 6 * 6, + count * 6 * 6 + ); + this.dynamicOffsetArray = this.offsetArray.subarray( + staticCount * 2, + count * 2 + ); + this.dynamicAlphaArray = this.alphaArray.subarray(staticCount, count); + } + + private splitBlock() { + this.block.configSplitter({ + dataWidth: this.mapWidth, + dataHeight: this.mapHeight, + blockWidth: this.blockWidth, + blockHeight: this.blockHeight + }); + const blockCount = this.blockWidth * this.blockHeight; + const lineCount = this.mapWidth * this.blockHeight; + const lastCount = (this.mapHeight % this.blockHeight) * this.blockWidth; + const bh = Math.floor(this.mapHeight / this.blockHeight); + const lastStart = bh * lineCount; + const layerCount = this.renderer.layerCount; + this.block.splitBlocks(block => { + // 最后一行的算法与其他行不同 + const startIndex = + block.height < this.blockHeight + ? lastStart + lastCount * block.x + : lineCount * block.y + blockCount * block.x; + const count = block.width * block.height * layerCount; + + const origin: IMapVertexData = { + vertexArray: this.vertexArray, + offsetArray: this.offsetArray, + alphaArray: this.alphaArray + }; + const data = new MapVertexBlock( + this.renderer, + origin, + startIndex, + count, + block.width, + block.height + ); + return data; + }); + } + + setBlockSize(width: number, height: number): void { + this.blockWidth = width; + this.blockHeight = height; + this.mallocVertexArray(); + this.splitBlock(); + } + + resizeMap(): void { + if ( + this.mapWidth !== this.renderer.mapWidth || + this.mapHeight !== this.renderer.mapHeight + ) { + this.needRebuild = true; + } + } + + expandMoving(targetSize: number): void { + const beforeVertex = this.vertexArray; + const beforeOffset = this.offsetArray; + const beforeAlpha = this.alphaArray; + this.dynamicLength = targetSize; + this.mallocVertexArray(); + this.vertexArray.set(beforeVertex); + this.offsetArray.set(beforeOffset); + this.alphaArray.set(beforeAlpha); + const array: IMapVertexData = { + vertexArray: this.vertexArray, + offsetArray: this.offsetArray, + alphaArray: this.alphaArray + }; + // 重建一下对应分块就行了,不需要重新分块 + for (const block of this.block.iterateBlocks()) { + block.data.rebuild(array); + } + } + + reduceMoving(targetSize: number, indexMap: Map): void { + const beforeVertexLength = this.vertexArray.length; + const beforeOffsetLength = this.offsetArray.length; + const beforeAlphaLength = this.alphaArray.length; + const deltaLength = this.dynamicLength - targetSize; + this.dynamicLength = targetSize; + this.vertexArray = this.vertexArray.subarray( + 0, + beforeVertexLength - deltaLength * 6 * 6 + ); + this.offsetArray = this.offsetArray.subarray( + 0, + beforeOffsetLength - deltaLength * 2 + ); + this.alphaArray = this.alphaArray.subarray( + 0, + beforeAlphaLength - deltaLength + ); + indexMap.forEach((target, from) => { + const next = from + 1; + this.dynamicVertexArray.copyWithin( + target * 6 * 6, + from * 6 * 6, + next * 6 * 6 + ); + this.dynamicOffsetArray.copyWithin(target * 2, from * 2, next * 2); + this.dynamicAlphaArray[target] = this.dynamicAlphaArray[from]; + }); + this.dynamicVertexArray = this.dynamicVertexArray.subarray( + 0, + targetSize * 6 * 6 + ); + this.dynamicOffsetArray = this.dynamicOffsetArray.subarray( + 0, + targetSize * 2 + ); + this.dynamicAlphaArray = this.dynamicAlphaArray.subarray(0, targetSize); + // 这个不需要重新分配内存,依然共用同一个 ArrayBuffer,因此不需要重新分块 + } + + updateLayerArray(): void { + const layers = this.renderer.getSortedLayer(); + if ( + layers.length !== this.layers.length || + this.layers.some((v, i) => layers[i] !== v) + ) { + this.needRebuild = true; + } + } + + checkRebuild() { + if (!this.needRebuild) return; + this.needRebuild = false; + this.mallocVertexArray(); + this.splitBlock(); + } + + //#endregion + + //#region 顶点数组更新 + + /** + * 获取图块经过对齐与缩放后的位置 + * @param pos 图块位置信息 + * @param width 图块的贴图宽度 + * @param height 图块的贴图高度 + */ + private getTilePosition( + pos: BlockMapPos, + width: number, + height: number + ): TilePosition { + const { + renderWidth, + renderHeight, + cellWidth, + cellHeight, + tileMinifyBehavior, + tileMagnifyBehavior, + tileAlignX, + tileAlignY, + tileTestMode + } = this.renderer; + const larger = + tileTestMode === MapTileSizeTestMode.WidthOrHeight + ? width > cellWidth || height > cellHeight + : width > cellWidth && height > cellHeight; + // 放大行为多数是适应到格子大小,因此把尺寸相等也归为放大行为,性能表现会更好 + const mode = larger ? tileMinifyBehavior : tileMagnifyBehavior; + const cwu = cellWidth / renderWidth; // normalized cell width in range [0, 1] + const chu = cellHeight / renderHeight; // normalized cell width in range [0, 1] + const cw = cwu * 2; // normalized cell width in range [-1, 1] + const ch = chu * 2; // normalized cell height in range [-1, 1] + const cl = pos.mapX * cw - 1; // cell left + const ct = pos.mapY * ch - 1; // cell top + if (mode === MapTileBehavior.FitToSize) { + // 适应到格子大小 + return { + left: cl, + top: ct, + right: cl + cw, + bottom: ct + ch + }; + } else { + // 维持大小,需要判断对齐 + // 下面这些计算是经过推导后的最简表达式,因此和语义可能不同 + // twu, thu, cwu, chu 的准确含义应该是“归一化尺寸的一半”,这样就好理解了 + + const twu = width / renderWidth; // normalized texture width in range [0, 1] + const thu = height / renderHeight; // normalized texture width in range [0, 1] + const tw = twu * 2; // normalized texture width in range [-1, 1] + const th = thu * 2; // normalized texture height in range [-1, 1] + let left = 0; + let top = 0; + let right = 0; + let bottom = 0; + switch (tileAlignX) { + case MapTileAlign.Start: { + // 左对齐 + left = cl; + right = cl + tw; + break; + } + case MapTileAlign.Center: { + // 左右居中对齐 + const center = cl + cwu; + left = center - twu; + right = center + twu; + break; + } + case MapTileAlign.End: { + // 右对齐 + right = cl + cw; + left = right - tw; + break; + } + } + switch (tileAlignY) { + case MapTileAlign.Start: { + // 上对齐 + top = ct; + bottom = ct + th; + break; + } + case MapTileAlign.Center: { + // 上下居中对齐 + const center = ct + chu; + top = center - thu; + bottom = center + thu; + break; + } + case MapTileAlign.End: { + // 下对齐 + bottom = ct + ch; + top = bottom - th; + } + } + return { left, top, right, bottom }; + } + } + + /** + * 更新指定图块的顶点数组信息 + * @param vertex 顶点数组对象 + * @param rect 可渲染对象的矩形区域 + * @param index 图块索引对象 + * @param assetIndex 贴图所在的图集索引 + * @param offsetIndex 贴图偏移值所在偏移池的索引 + * @param frames 贴图总帧数 + * @param update 顶点坐标更新方式 + */ + private updateTileVertex( + vertex: IMapVertexData, + rect: Readonly, + index: IndexedBlockMapPos, + assetIndex: number, + offsetIndex: number, + frames: number, + update: VertexUpdate + ) { + const { offsetArray, vertexArray } = vertex; + // 顶点数组 + const { renderWidth, renderHeight, layerCount } = this.renderer; + const { x, y, w, h } = rect; + const vertexStart = index.blockIndex * 6 * 6; + if (update & VertexUpdate.Texture) { + const texLeft = (x / renderWidth) * 2 - 1; + const texTop = (y / renderHeight) * 2 - 1; + const texRight = ((x + w) / renderWidth) * 2 - 1; + const texBottom = ((y + h) / renderHeight) * 2 - 1; + // 六个顶点分别是 左下,右下,左上,左上,右下,右上 + vertexArray[vertexStart + 3] = texLeft; + vertexArray[vertexStart + 4] = texBottom; + vertexArray[vertexStart + 5] = assetIndex; + vertexArray[vertexStart + 9] = texRight; + vertexArray[vertexStart + 10] = texBottom; + vertexArray[vertexStart + 11] = assetIndex; + vertexArray[vertexStart + 15] = texLeft; + vertexArray[vertexStart + 16] = texTop; + vertexArray[vertexStart + 17] = assetIndex; + vertexArray[vertexStart + 21] = texLeft; + vertexArray[vertexStart + 22] = texTop; + vertexArray[vertexStart + 23] = assetIndex; + vertexArray[vertexStart + 27] = texRight; + vertexArray[vertexStart + 28] = texBottom; + vertexArray[vertexStart + 29] = assetIndex; + vertexArray[vertexStart + 33] = texRight; + vertexArray[vertexStart + 34] = texTop; + vertexArray[vertexStart + 35] = assetIndex; + } + if (update & VertexUpdate.Vertex) { + // 如果需要更新顶点坐标 + const layerIndex = this.renderer.getLayerIndex(index.layer); + const layerStart = (layerIndex / layerCount) * 2 - 1; + const zIndex = layerStart + index.mapY / this.mapHeight; + const { left, top, right, bottom } = this.getTilePosition( + index, + w, + h + ); + vertexArray[vertexStart] = left; + vertexArray[vertexStart + 1] = bottom; + vertexArray[vertexStart + 2] = zIndex; + vertexArray[vertexStart + 6] = right; + vertexArray[vertexStart + 7] = bottom; + vertexArray[vertexStart + 8] = zIndex; + vertexArray[vertexStart + 12] = left; + vertexArray[vertexStart + 13] = top; + vertexArray[vertexStart + 14] = zIndex; + vertexArray[vertexStart + 18] = left; + vertexArray[vertexStart + 19] = top; + vertexArray[vertexStart + 20] = zIndex; + vertexArray[vertexStart + 24] = right; + vertexArray[vertexStart + 25] = bottom; + vertexArray[vertexStart + 26] = zIndex; + vertexArray[vertexStart + 30] = right; + vertexArray[vertexStart + 31] = top; + vertexArray[vertexStart + 32] = zIndex; + } + // 偏移数组 + const offsetStart = index.blockIndex * 2; + offsetArray[offsetStart] = frames; + offsetArray[offsetStart + 1] = offsetIndex; + } + + /** + * 更新指定点的自动元件,不会检查中心点是不是自动元件 + * @param mapArray 地图数组 + * @param vertex 顶点数组对象 + * @param index 中心图块索引 + * @param tile 图块的素材对象 + * @param update 顶点数组更新方式 + */ + private updateAutotile( + mapArray: Uint32Array, + vertex: IMapVertexData, + index: BlockIndex, + tile: IMaterialFramedData, + update: VertexUpdate + ) { + const autotile = this.renderer.autotile; + const { connection, center } = autotile.connect( + mapArray, + index.mapIndex, + this.mapWidth + ); + // 使用不带检查的版本可以减少分支数量,提升性能 + const renderable = autotile.renderWithoutCheck(tile, connection); + if (!renderable) return; + const assetIndex = this.renderer.getAssetSourceIndex(renderable.source); + const offsetIndex = this.renderer.getOffsetIndex(tile.offset); + if (assetIndex === -1 || offsetIndex === -1) { + logger.error(40, center.toString()); + return; + } + this.updateTileVertex( + vertex, + renderable.rect, + index, + assetIndex, + offsetIndex, + tile.frames, + update + ); + } + + /** + * 处理一个自动元件周围一圈的自动元件连接 + * @param mapArray 地图图块数组 + * @param vertex 顶点数组对象 + * @param index 原始索引 + * @param dx 横坐标偏移 + * @param dy 纵坐标偏移 + */ + private checkAutotileConnectionAround( + layer: IMapLayer, + mapArray: Uint32Array, + index: BlockIndex, + dx: number, + dy: number + ) { + const mx = index.mapX + dx; + const my = index.mapY + dy; + const block = this.block.getBlockByDataLoc(mx, my); + if (!block) return; + const vertex = block.data.getLayerData(layer); + if (!vertex) return; + const newIndex: BlockIndex = { + layer, + mapX: mx, + mapY: my, + mapIndex: my * this.mapWidth + mx, + blockX: block.x, + blockY: block.y, + blockIndex: block.y * block.width + block.x + }; + const tile = this.renderer.manager.getTile(mapArray[index.mapIndex]); + if (!tile) return; + this.updateAutotile( + mapArray, + vertex, + newIndex, + tile, + VertexUpdate.Texture + ); + block.data.markRenderDirty(); + } + + /** + * 更新指定的顶点数组 + * @param mapArray 地图图块数组,用于自动元件判定 + * @param vertex 顶点数组对象 + * @param index 图块索引对象 + * @param num 图块数字 + */ + private updateVertexArray( + mapArray: Uint32Array, + vertex: IMapVertexData, + index: BlockIndex, + num: number + ) { + // 此处仅更新当前图块,不更新周围一圈的自动元件 + // 周围一圈的自动元件需要在更新某个图块或者某个区域时处理,不在这里处理 + const tile = this.renderer.manager.getIfBigImage(num); + + if (!tile) { + // 不存在可渲染对象,认为是空图块 + vertex.vertexArray.set( + MapVertexGenerator.EMPTY_VETREX, + index.blockIndex * 6 * 6 + ); + const offsetStart = index.blockIndex * 2; + vertex.offsetArray[offsetStart] = 0; + vertex.offsetArray[offsetStart + 1] = 0; + return; + } + + if (tile.cls === BlockCls.Autotile) { + // 如果图块是自动元件 + this.updateAutotile( + mapArray, + vertex, + index, + tile, + VertexUpdate.All + ); + } else { + // 正常图块 + const renderable = tile.texture.render(); + // 宽度要除以帧数,因为我们假设所有素材都是横向平铺的 + const rect: IRect = { + x: renderable.rect.x, + y: renderable.rect.y, + w: renderable.rect.w / tile.frames, + h: renderable.rect.h + }; + const assetIndex = this.renderer.getAssetSourceIndex( + renderable.source + ); + const offsetIndex = this.renderer.getOffsetIndex(tile.offset); + if (assetIndex === -1 || offsetIndex === -1) { + logger.error(40, num.toString()); + return; + } + this.updateTileVertex( + vertex, + rect, + index, + assetIndex, + offsetIndex, + tile.frames, + VertexUpdate.All + ); + } + } + + /** + * 更新指定图块,但是不包含调用性能检查 + * @param layer 更新的图层 + * @param block 设置为的图块 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + private updateBlockVertex( + layer: IMapLayer, + num: number, + x: number, + y: number + ) { + const block = this.block.getBlockByDataLoc(x, y); + if (!block) return; + const vertex = block.data.getLayerData(layer); + const data = this.renderer.getMapLayerData(layer); + if (!vertex || !data) return; + const { array } = data; + const dx = x - block.dataX; + const dy = y - block.dataY; + const dIndex = dy * block.width + dx; + const index: BlockIndex = { + layer, + mapX: x, + mapY: y, + mapIndex: y * this.mapWidth + x, + blockX: block.x, + blockY: block.y, + blockIndex: dIndex + }; + // 需要检查周围一圈的自动元件 + this.checkAutotileConnectionAround(layer, array, index, -1, -1); + this.checkAutotileConnectionAround(layer, array, index, 0, -1); + this.checkAutotileConnectionAround(layer, array, index, 1, -1); + this.checkAutotileConnectionAround(layer, array, index, 1, 0); + this.checkAutotileConnectionAround(layer, array, index, 1, 1); + this.checkAutotileConnectionAround(layer, array, index, 0, 1); + this.checkAutotileConnectionAround(layer, array, index, -1, 1); + this.checkAutotileConnectionAround(layer, array, index, -1, 0); + // 再更新当前图块 + this.updateVertexArray(array, vertex, index, num); + block.data.markRenderDirty(); + } + + //#endregion + + //#region 更新接口 + + /** + * 性能监测,如果频繁调用 `updateArea` `updateBlock` `updateBlockList` 则抛出警告 + */ + private checkUpdateCallPerformance(method: string) { + const now = performance.now(); + if (now - this.updateCallDebounceTime <= 10) { + this.updateCallDebounceCount++; + } else { + this.updateCallDebounceCount = 0; + this.updateCallDebounceTime = now; + } + if (this.updateCallDebounceCount >= 50) { + logger.warn(83, method); + this.updateCallDebounceCount = 0; + this.updateCallDebounceTime = now; + } + } + + updateArea( + layer: IMapLayer, + x: number, + y: number, + w: number, + h: number + ): void { + // 这里多一圈是因为要更新这一圈的自动元件 + const ax = x - 1; + const ay = y - 1; + const areaRight = x + w + 1; + const areaBottom = y + h + 1; + const blocks = this.block.iterateBlocksOfDataArea(ax, ay, w + 2, h + 2); + for (const block of blocks) { + const left = ax - block.dataX; + const top = ay - block.dataY; + const right = Math.min(areaRight - block.dataX, left + block.width); + const bottom = Math.min( + areaBottom - block.dataY, + top + block.height + ); + block.data.markDirty(layer, left, top, right, bottom); + block.data.markRenderDirty(); + } + } + + updateBlock(layer: IMapLayer, num: number, x: number, y: number): void { + if (import.meta.env.DEV) { + this.checkUpdateCallPerformance('updateBlock'); + } + this.updateBlockVertex(layer, num, x, y); + } + + updateBlockList(layer: IMapLayer, blocks: IMapBlockUpdateObject[]): void { + if (!this.renderer.hasLayer(layer)) return; + if (import.meta.env.DEV) { + this.checkUpdateCallPerformance('updateBlockList'); + } + if (blocks.length > 50) { + // 对于超出50个的更新操作使用懒更新 + blocks.forEach(v => { + const block = this.block.getBlockByDataLoc(v.x, v.y); + if (!block) return; + const bx = v.x - block.dataX; + const by = v.y - block.dataY; + block.data.markDirty(layer, bx - 1, by - 1, bx + 2, by + 2); + block.data.markRenderDirty(); + const left = bx === 0; + const top = by === 0; + const right = bx === block.width - 1; + const bottom = by === block.height - 1; + // 需要更一圈的自动元件 + if (left) { + // 左侧的分块需要更新 + const nextBlock = block.left(); + if (nextBlock) { + const { width: w, data } = nextBlock; + data.markDirty(layer, w - 1, by - 1, w, by + 1); + data.markRenderDirty(); + } + if (top) { + // 左上侧的分块需要更新 + const nextBlock = block.leftUp(); + if (nextBlock) { + const { width: w, height: h, data } = nextBlock; + data.markDirty(layer, w - 1, h - 1, w, h); + data.markRenderDirty(); + } + } + if (bottom) { + // 左下侧的分块需要更新 + const nextBlock = block.leftDown(); + if (nextBlock) { + const { width: w, data } = nextBlock; + data.markDirty(layer, w - 1, 0, w, 1); + data.markRenderDirty(); + } + } + } + if (top) { + // 上侧的分块需要更新 + const nextBlock = block.up(); + if (nextBlock) { + const { height: h, data } = nextBlock; + data.markDirty(layer, bx - 1, h - 1, bx + 1, h); + data.markRenderDirty(); + } + } + if (right) { + // 右侧的分块需要更新 + const nextBlock = block.right(); + if (nextBlock) { + const { data } = nextBlock; + data.markDirty(layer, 0, by - 1, 1, by + 1); + data.markRenderDirty(); + } + if (top) { + // 右上侧的分块需要更新 + const nextBlock = block.rightUp(); + if (nextBlock) { + const { height: h, data } = nextBlock; + data.markDirty(layer, 0, h - 1, 1, h); + data.markRenderDirty(); + } + } + if (bottom) { + // 右下侧的分块需要更新 + const nextBlock = block.rightDown(); + if (nextBlock) { + const { data } = nextBlock; + data.markDirty(layer, 0, 0, 1, 1); + data.markRenderDirty(); + } + } + } + if (bottom) { + // 下侧的分块需要更新 + const nextBlock = block.down(); + if (nextBlock) { + const { data } = nextBlock; + data.markDirty(layer, bx - 1, 0, bx + 1, 1); + data.markRenderDirty(); + } + } + }); + } else { + blocks.forEach(({ block: num, x, y }) => { + this.updateBlockVertex(layer, num, x, y); + }); + } + } + + updateBlockCache(block: Readonly>): void { + console.time('update-block-cache'); + const layers = this.renderer.getSortedLayer(); + layers.forEach(layer => { + const dirty = block.data.getDirtyArea(layer); + if (!dirty || !dirty.dirty) return; + const vertex = block.data.getLayerData(layer); + const mapData = this.renderer.getMapLayerData(layer); + if (!vertex || !mapData) return; + const { array } = mapData; + const { dirtyLeft, dirtyTop, dirtyRight, dirtyBottom } = dirty; + for (let nx = dirtyLeft; nx < dirtyRight; nx++) { + for (let ny = dirtyTop; ny < dirtyBottom; ny++) { + const mapX = nx + block.dataX; + const mapY = ny + block.dataY; + const mapIndex = mapY * this.mapWidth + mapX; + const index: BlockIndex = { + layer, + blockX: nx, + blockY: ny, + blockIndex: ny * block.width + nx, + mapX, + mapY, + mapIndex + }; + this.updateVertexArray( + array, + vertex, + index, + array[mapIndex] + ); + } + } + }); + console.timeEnd('update-block-cache'); + } + + //#endregion + + //#region 图块配置 + + enableStaticFrameAnimate(layer: IMapLayer, x: number, y: number): void { + const data = this.renderer.getMapLayerData(layer); + const block = this.block.getBlockByDataLoc(x, y); + if (!data || !block) return; + const vertexArray = block.data.getLayerOffset(layer); + if (!vertexArray) return; + const mapIndex = y * this.mapWidth + x; + const num = data.array[mapIndex]; + const tile = this.renderer.manager.getIfBigImage(num); + if (!tile) return; + const bx = x - block.dataX; + const by = y - block.dataY; + const bIndex = by * block.width + bx; + vertexArray[bIndex * 2] = tile.frames; + block.data.markRenderDirty(); + } + + disableStaticFrameAnimate(layer: IMapLayer, x: number, y: number): void { + const block = this.block.getBlockByDataLoc(x, y); + if (!block) return; + const vertexArray = block.data.getLayerOffset(layer); + if (!vertexArray) return; + const bx = x - block.dataX; + const by = y - block.dataY; + const bIndex = by * block.width + bx; + vertexArray[bIndex * 2] = 1; + block.data.markRenderDirty(); + } + + setStaticAlpha( + layer: IMapLayer, + alpha: number, + x: number, + y: number + ): void { + const block = this.block.getBlockByDataLoc(x, y); + if (!block) return; + const vertexArray = block.data.getLayerAlpha(layer); + if (!vertexArray) return; + const bx = x - block.dataX; + const by = y - block.dataY; + const bIndex = by * block.width + bx; + vertexArray[bIndex] = alpha; + block.data.markRenderDirty(); + } + + //#endregion + + //#region 动态图块 + + updateMoving(block: IMovingBlock, updateTexture: boolean): void { + if (!this.renderer.hasMoving(block)) return; + const { cls, frames, offset, texture } = block.texture; + const vertex: IMapVertexData = { + vertexArray: this.dynamicVertexArray, + offsetArray: this.dynamicOffsetArray, + alphaArray: this.dynamicAlphaArray + }; + const index: IndexedBlockMapPos = { + layer: block.layer, + mapX: block.x, + mapY: block.y, + blockIndex: block.index + }; + const assetIndex = this.renderer.getAssetSourceIndex(texture.source); + const offsetIndex = this.renderer.getOffsetIndex(offset); + if (assetIndex === -1 || offsetIndex === -1) { + logger.error(40, block.tile.toString()); + return; + } + const update = updateTexture ? VertexUpdate.All : VertexUpdate.Vertex; + if (cls === BlockCls.Autotile) { + // 自动元件使用全部不连接 + const renderable = this.renderer.autotile.renderWithoutCheck( + block.texture, + 0b0000_0000 + ); + if (!renderable) return; + + this.updateTileVertex( + vertex, + renderable.rect, + index, + assetIndex, + offset, + frames, + update + ); + } else { + // 正常图块 + const renderable = texture.render(); + // 宽度要除以帧数,因为我们假设所有素材都是横向平铺的 + const rect: IRect = { + x: renderable.rect.x, + y: renderable.rect.y, + w: renderable.rect.w / frames, + h: renderable.rect.h + }; + this.updateTileVertex( + vertex, + rect, + index, + assetIndex, + offset, + frames, + update + ); + } + + this.dynamicRenderDirty = true; + } + + updateMovingList(moving: IMovingBlock[], updateTexture: boolean): void { + console.time('update-moving'); + moving.forEach(v => { + this.updateMoving(v, updateTexture); + }); + console.timeEnd('update-moving'); + } + + deleteMoving(moving: IMovingBlock): void { + this.dynamicVertexArray.set( + MapVertexGenerator.EMPTY_VETREX, + moving.index * 6 * 6 + ); + const offsetStart = moving.index * 2; + this.dynamicOffsetArray[offsetStart] = 0; + this.dynamicOffsetArray[offsetStart + 1] = 0; + this.dynamicAlphaArray[moving.index] = 0; + this.dynamicRenderDirty = true; + } + + enableDynamicFrameAnimate(block: IMovingBlock): void { + if (!this.renderer.hasMoving(block)) return; + this.dynamicOffsetArray[block.index * 2] = 1; + this.dynamicRenderDirty = true; + } + + disableDynamicFrameAnimate(block: IMovingBlock): void { + if (!this.renderer.hasMoving(block)) return; + this.dynamicOffsetArray[block.index * 2] = block.texture.frames; + this.dynamicRenderDirty = true; + } + + setDynamicAlpha(block: IMovingBlock, alpha: number): void { + if (!this.renderer.hasMoving(block)) return; + this.dynamicAlphaArray[block.index] = alpha; + this.dynamicRenderDirty = true; + } + + //#endregion + + //#region 其他接口 + + renderDynamic(): void { + // todo: vertex, offset, alpha 的脏标记分开 + this.dynamicRenderDirty = false; + } + + getVertexArray(): IMapVertexArray { + this.checkRebuild(); + return { + dynamicStart: this.staticLength * 6, + dynamicCount: this.dynamicLength * 6, + tileVertex: this.vertexArray, + tileOffset: this.offsetArray, + tileAlpha: this.alphaArray + }; + } + + //#endregion +} + +//#region 分块对象 + +class MapVertexBlock implements IMapVertexBlock { + vertexArray!: Float32Array; + offsetArray!: Int16Array; + alphaArray!: Float32Array; + + dirty: boolean = false; + renderDirty: boolean = true; + + private readonly layerDirty: Map = new Map(); + + readonly startIndex: number; + readonly endIndex: number; + readonly count: number; + + readonly vertexStart: number; + readonly offsetStart: number; + readonly alphaStart: number; + + private readonly indexMap: Map = new Map(); + private readonly vertexMap: Map = new Map(); + private readonly offsetMap: Map = new Map(); + private readonly alphaMap: Map = new Map(); + + /** + * 创建分块的顶点数组对象,此对象不能动态扩展,如果地图变化,需要全部重建 + * @param renderer 渲染器对象 + * @param originArray 原始顶点数组 + * @param startIndex 起始网格索引 + * @param count 分块数量 + */ + constructor( + readonly renderer: IMapRenderer, + originArray: IMapVertexData, + startIndex: number, + count: number, + private readonly blockWidth: number, + private readonly blockHeight: number + ) { + this.startIndex = startIndex; + this.endIndex = startIndex + count; + this.count = count; + const layerCount = renderer.layerCount; + const vertexStart = startIndex * layerCount * 6 * 6; + const offsetStart = startIndex * layerCount * 2; + const alphaStart = startIndex * layerCount; + this.vertexStart = vertexStart; + this.offsetStart = offsetStart; + this.alphaStart = alphaStart; + + this.rebuild(originArray); + } + + render(): void { + this.renderDirty = false; + } + + markRenderDirty() { + // todo: 潜在优化点:vertex, offset, alpha 的脏标记分开 + this.renderDirty = true; + } + + markDirty( + layer: IMapLayer, + left: number, + top: number, + right: number, + bottom: number + ): void { + // todo: 更细致的脏标记是否会更好? + const data = this.layerDirty.get(layer); + if (!data) return; + const dl = clamp(left, 0, this.blockWidth); + const dt = clamp(top, 0, this.blockHeight); + const dr = clamp(right, left, this.blockWidth); + const db = clamp(bottom, top, this.blockHeight); + if (!data.dirty) { + data.dirtyLeft = dl; + data.dirtyTop = dt; + data.dirtyRight = dr; + data.dirtyBottom = db; + } else { + data.dirtyLeft = Math.min(dl, data.dirtyLeft); + data.dirtyTop = Math.min(dt, data.dirtyTop); + data.dirtyRight = Math.max(dr, data.dirtyRight); + data.dirtyBottom = Math.max(db, data.dirtyBottom); + } + this.dirty = true; + } + + getDirtyArea(layer: IMapLayer): Readonly | null { + return this.layerDirty.get(layer) ?? null; + } + + rebuild(originArray: IMapVertexData) { + const vertexStart = this.vertexStart; + const offsetStart = this.offsetStart; + const alphaStart = this.alphaStart; + const count = this.count; + this.vertexArray = originArray.vertexArray.subarray( + vertexStart, + vertexStart + count * 6 * 6 + ); + this.offsetArray = originArray.offsetArray.subarray( + offsetStart, + offsetStart + count * 2 + ); + this.alphaArray = originArray.alphaArray.subarray( + alphaStart, + alphaStart + count + ); + + this.renderer.getSortedLayer().forEach((v, i) => { + const vs = vertexStart + i * count * 6 * 6; + const os = offsetStart + i * count * 2; + const as = alphaStart + i * count; + const va = this.vertexArray.subarray(vs, vs + count * 6 * 6); + const oa = this.offsetArray.subarray(os, os + count * 2); + const aa = this.alphaArray.subarray(as, as + count); + this.vertexMap.set(v, va); + this.offsetMap.set(v, oa); + this.alphaMap.set(v, aa); + this.indexMap.set(v, i); + this.layerDirty.set(v, { + dirty: true, + dirtyLeft: 0, + dirtyTop: 0, + dirtyRight: this.blockWidth, + dirtyBottom: this.blockHeight + }); + }); + } + + getLayerVertex(layer: IMapLayer): Float32Array | null { + return this.vertexMap.get(layer) ?? null; + } + + getLayerOffset(layer: IMapLayer): Int16Array | null { + return this.offsetMap.get(layer) ?? null; + } + + getLayerAlpha(layer: IMapLayer): Float32Array | null { + return this.alphaMap.get(layer) ?? null; + } + + getLayerData(layer: IMapLayer): IIndexedMapVertexData | null { + const vertex = this.vertexMap.get(layer); + const offset = this.offsetMap.get(layer); + const alpha = this.alphaMap.get(layer); + const index = this.indexMap.get(layer); + if (!vertex || !offset || !alpha || isNil(index)) return null; + return { + vertexArray: vertex, + offsetArray: offset, + alphaArray: alpha, + vertexStart: this.vertexStart + index * this.count * 6 * 6, + offsetStart: this.offsetStart + index * this.count * 2, + alphaStart: this.alphaStart + index * this.count + }; + } +} + +//#endregion diff --git a/packages-user/client-modules/src/render/map/viewport.ts b/packages-user/client-modules/src/render/map/viewport.ts new file mode 100644 index 0000000..8835bb2 --- /dev/null +++ b/packages-user/client-modules/src/render/map/viewport.ts @@ -0,0 +1,105 @@ +import { Transform } from '@motajs/render-core'; +import { + IBlockData, + IMapRenderArea, + IMapRenderData, + IMapRenderer, + IMapVertexBlock, + IMapVertexGenerator, + IMapViewportController +} from './types'; +import { clamp } from 'lodash-es'; + +export class MapViewport implements IMapViewportController { + transform: Transform = new Transform(); + /** 顶点生成器 */ + readonly vertex: IMapVertexGenerator; + + constructor(readonly renderer: IMapRenderer) { + this.vertex = renderer.vertex; + } + + getRenderArea(): IMapRenderData { + const { cellWidth, cellHeight, renderWidth, renderHeight } = + this.renderer; + const { blockWidth, blockHeight, width, height } = this.vertex.block; + // 减一是因为第一个像素是 0,所以最后一个像素就是宽度减一 + const r = renderWidth - 1; + const b = renderHeight - 1; + // 其实只需要算左上角和右下角就行了 + const [left, top] = this.transform.transformed(0, 0); + const [right, bottom] = this.transform.transformed(r, b); + const cl = left / cellWidth; + const ct = top / cellHeight; + const cr = right / cellWidth; + const cb = bottom / cellHeight; + const blockLeft = clamp(Math.floor(cl / blockWidth), 0, width - 1); + const blockRight = clamp(Math.floor(cr / blockWidth), 0, width - 1); + const blockTop = clamp(Math.floor(ct / blockHeight), 0, height - 1); + const blockBottom = clamp(Math.floor(cb / blockHeight), 0, height - 1); + + const renderArea: IMapRenderArea[] = []; + const updateArea: IMapRenderArea[] = []; + const blockList: IBlockData[] = []; + + // 使用这种方式的话,索引在换行之前都是连续的,方便整合 + for (let ny = blockTop; ny <= blockBottom; ny++) { + const first = this.vertex.block.getBlockByLoc(blockLeft, ny)!; + const last = this.vertex.block.getBlockByLoc(blockRight, ny)!; + if (first.data.dirty) { + blockList.push(first); + } + if (last.data.dirty) { + blockList.push(last); + } + renderArea.push({ + startIndex: first.data.startIndex, + endIndex: last.data.endIndex, + count: last.data.endIndex - first.data.startIndex + }); + for (let nx = blockLeft + 1; nx < blockRight; nx++) { + const block = this.vertex.block.getBlockByLoc(nx, ny)!; + if (block.data.dirty) { + blockList.push(block); + } + } + } + + if (blockList.length > 0) { + if (blockList.length === 1) { + const block = blockList[0]; + updateArea.push(block.data); + } else { + let continuousStart: IBlockData = blockList[0]; + let continuousLast: IBlockData = blockList[0]; + for (let i = 1; i < blockList.length; i++) { + const block = blockList[i]; + if (block.index === continuousLast.index + 1) { + // 连续则合并 + continuousLast = block; + } else { + const start = continuousStart.data.startIndex; + const end = continuousLast.data.endIndex; + updateArea.push({ + startIndex: start, + endIndex: end, + count: end - start + }); + continuousStart = block; + continuousLast = block; + } + } + } + } + + return { + render: renderArea, + dirty: updateArea, + blockList + }; + } + + bindTransform(transform: Transform): void { + this.transform = transform; + } +} diff --git a/packages-user/client-modules/src/render/shared.ts b/packages-user/client-modules/src/render/shared.ts index 11b5516..d8bc30f 100644 --- a/packages-user/client-modules/src/render/shared.ts +++ b/packages-user/client-modules/src/render/shared.ts @@ -19,6 +19,20 @@ export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT; export const HALF_MAP_WIDTH = MAP_WIDTH / 2; /** 地图高度的一半 */ export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2; +/** + * 动态内容预留,不明白含义的话不要动。地图上所有正在移动的图块称为动态内容,这些内容的数量无法预测,因此需要预留数组大小, + * 如果不够再临时扩充。如果你的塔中有大量的移动操作,可以适当提高此值,避免频繁的内存扩充行为,可以一定程度上提高性能表现。 + */ +export const DYNAMIC_RESERVE = 16; +/** + * 移动图块容忍度,不明白含义的话不要动。如果移动图块的数量长期小于当前预留数量,那么将会降低预留数量,提升性能表现。 + * 调整此值可以调整频率,值越大,越不容易因为数量小于预留数量而减小预留。 + */ +export const MOVING_TOLERANCE = 60; +/** 每个格子的默认宽度,现阶段用处不大 */ +export const CELL_WIDTH = 32; +/** 每个格子的默认高度,现阶段用处不大 */ +export const CELL_HEIGHT = 32; //#region 状态栏 diff --git a/packages-user/client-modules/src/render/ui/main.tsx b/packages-user/client-modules/src/render/ui/main.tsx index fcd5ac9..cfc68b1 100644 --- a/packages-user/client-modules/src/render/ui/main.tsx +++ b/packages-user/client-modules/src/render/ui/main.tsx @@ -56,6 +56,7 @@ import { LayerGroup } from '../elements'; import { isNil } from 'lodash-es'; +import { materials } from '@user/client-base'; const MainScene = defineComponent(() => { //#region 基本定义 @@ -267,8 +268,25 @@ const MainScene = defineComponent(() => { core.doRegisteredAction('onmove', bx, by, ev.offsetX, ev.offsetY); }; + const testRender = (canvas: MotaOffscreenCanvas2D) => { + const tileset = materials.getAsset(0); + if (!tileset) return; + canvas.ctx.drawImage( + tileset.data.texture.source, + 0, + 0, + canvas.width, + canvas.height + ); + }; + return () => ( + = new Set(); + /** 添加的图层拓展 */ + private addedExtends: Map = new Map(); + + /** 地图图块数组 */ + private mapArray: Uint32Array; + /** 地图数据引用 */ + private mapData: IMapLayerData; + + constructor(array: Uint32Array, width: number, height: number) { + this.width = width; + this.height = height; + const area = width * height; + this.mapArray = new Uint32Array(area); + // 超出的裁剪,不足的补零 + this.mapArray.set(array); + this.mapData = { + expired: false, + array: this.mapArray + }; + } + + resize(width: number, height: number): void { + if (this.width === width && this.height === height) { + this.loadedExtends.forEach(v => { + v.ex.onResize?.(v.controller, width, height); + }); + return; + } + this.mapData.expired = true; + const before = this.mapArray; + const beforeWidth = this.width; + const beforeHeight = this.height; + const beforeArea = beforeWidth * beforeHeight; + this.width = width; + this.height = height; + const area = width * height; + const newArray = new Uint32Array(area); + this.mapArray = newArray; + // 将原来的地图数组赋值给现在的 + if (beforeArea > area) { + // 如果地图变小了,那么直接设置,不需要补零 + for (let ny = 0; ny < height; ny++) { + const begin = ny * beforeWidth; + newArray.set(before.subarray(begin, begin + width), ny * width); + } + } else { + // 如果地图变大了,那么需要补零。因为新数组本来就是用 0 填充的,实际上只要赋值就可以了 + for (let ny = 0; ny < beforeHeight; ny++) { + const begin = ny * beforeWidth; + newArray.set( + before.subarray(begin, begin + beforeWidth), + ny * width + ); + } + } + this.mapData = { + expired: false, + array: this.mapArray + }; + this.loadedExtends.forEach(v => { + v.ex.onResize?.(v.controller, width, height); + }); + } + + resize2(width: number, height: number): void { + if (this.width === width && this.height === height) { + this.mapArray.fill(0); + this.loadedExtends.forEach(v => { + v.ex.onResize?.(v.controller, width, height); + }); + return; + } + this.mapData.expired = true; + this.width = width; + this.height = height; + this.mapArray = new Uint32Array(width * height); + this.mapData = { + expired: false, + array: this.mapArray + }; + this.loadedExtends.forEach(v => { + v.ex.onResize?.(v.controller, width, height); + }); + this.empty = true; + } + + setBlock(block: number, x: number, y: number): void { + const index = y * this.width + x; + this.mapArray[index] = block; + this.loadedExtends.forEach(v => { + v.ex.onUpdateBlock?.(v.controller, block, x, y); + }); + if (block !== 0) { + this.empty = false; + } + } + + getBlock(x: number, y: number): number { + if (x < 0 || y < 0 || x >= this.width || y >= this.height) { + // 不在地图内,返回 -1 + return -1; + } + return this.mapArray[y * this.width + x]; + } + + putMapData(array: Uint32Array, x: number, y: number, width: number): void { + if (array.length % width !== 0) { + logger.warn(8); + } + const height = Math.ceil(array.length / width); + if (width === this.width && height === this.height) { + this.mapArray.set(array); + return; + } + const w = this.width; + const r = x + width; + const b = y + height; + if (x < 0 || y < 0 || r > w || b > this.height) { + logger.warn(9); + } + const nl = Math.max(x, 0); + const nt = Math.max(y, 0); + const nr = Math.min(r, w); + const nb = Math.min(b, this.height); + const nw = nr - nl; + const nh = nb - nt; + let empty = true; + for (let ny = 0; ny < nh; ny++) { + const start = ny * nw; + const offset = (ny + nt) * w + nl; + const sub = array.subarray(start, start + nw); + if (empty && sub.some(v => v !== 0)) { + // 空地图判断 + empty = false; + } + this.mapArray.set(array.subarray(start, start + nw), offset); + } + this.loadedExtends.forEach(v => { + v.ex.onUpdateArea?.(v.controller, x, y, width, height); + }); + this.empty &&= empty; + } + + getMapData(): Uint32Array; + getMapData( + x: number, + y: number, + width: number, + height: number + ): Uint32Array; + getMapData( + x?: number, + y?: number, + width?: number, + height?: number + ): Uint32Array { + if (isNil(x)) { + return new Uint32Array(this.mapArray); + } + if (isNil(y) || isNil(width) || isNil(height)) { + logger.warn(80); + return new Uint32Array(); + } + const w = this.width; + const h = this.height; + const r = x + width; + const b = y + height; + if (x < 0 || y < 0 || r > w || b > h) { + logger.warn(81); + } + const res = new Uint32Array(width * height); + const arr = this.mapArray; + const nr = Math.min(r, w); + const nb = Math.min(b, h); + for (let nx = x; nx < nr; nx++) { + for (let ny = y; ny < nb; ny++) { + const origin = ny * w + nx; + const target = (ny - y) * width + (nx - x); + res[target] = arr[origin]; + } + } + return res; + } + + /** + * 获取地图数据的内部存储直接引用 + */ + getMapRef(): IMapLayerData { + return this.mapData; + } + + loadExtends(ex: IMapLayerExtends): boolean { + if (!this.addedExtends.has(ex.id)) return false; + ex.awake?.(); + const data = this.addedExtends.get(ex.id)!; + this.loadedExtends.add(data); + return true; + } + + addExtends(ex: IMapLayerExtends): IMapLayerExtendsController { + const controller = new MapLayerExtendsController(this, ex); + this.addedExtends.set(ex.id, { + ex, + controller + }); + return controller; + } + + removeExtends(ex: IMapLayerExtends | string): void { + const id = typeof ex === 'string' ? ex : ex.id; + const data = this.addedExtends.get(id); + if (!data) return; + data.ex.destroy?.(); + this.addedExtends.delete(id); + this.loadedExtends.delete(data); + } +} + +class MapLayerExtendsController implements IMapLayerExtendsController { + loaded: boolean = false; + + constructor( + readonly layer: MapLayer, + readonly ex: IMapLayerExtends + ) {} + + load(): void { + this.loaded = this.layer.loadExtends(this.ex); + } + + getMapData(): Readonly { + return this.layer.getMapRef(); + } + + unload(): void { + this.layer.removeExtends(this.ex); + this.loaded = false; + } +} diff --git a/packages-user/data-state/src/map/types.ts b/packages-user/data-state/src/map/types.ts new file mode 100644 index 0000000..0714a86 --- /dev/null +++ b/packages-user/data-state/src/map/types.ts @@ -0,0 +1,167 @@ +export interface IMapLayerData { + /** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */ + expired: boolean; + /** 地图图块数组,是对内部存储的直接引用 */ + array: Uint32Array; +} + +export interface IMapLayerHooks { + /** + * 当钩子准备完毕时执行,会自动分析依赖,并把依赖实例作为参数传入,遵循依赖列表的顺序 + * @param dependencies 依赖列表 + */ + awake(): void; + + /** + * 当拓展被移除之前执行,可以用来清理相关内容 + */ + destroy(): void; + + /** + * 当地图大小发生变化时执行 + * @param controller 拓展控制器 + * @param width 地图宽度 + * @param height 地图高度 + */ + onResize( + controller: IMapLayerExtendsController, + width: number, + height: number + ): void; + + /** + * 当更新某个区域的图块时执行 + * @param controller 拓展控制器 + * @param x 更新区域左上角横坐标 + * @param y 更新区域左上角纵坐标 + * @param width 更新区域宽度 + * @param height 更新区域高度 + */ + onUpdateArea( + controller: IMapLayerExtendsController, + x: number, + y: number, + width: number, + height: number + ): void; + + /** + * 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法 + * @param controller 拓展控制器 + * @param block 更新为的图块数字 + * @param x 更新点横坐标 + * @param y 更新点纵坐标 + */ + onUpdateBlock( + controller: IMapLayerExtendsController, + block: number, + x: number, + y: number + ): void; +} + +export interface IMapLayerExtends extends Partial { + /** 这个拓展对象的标识符 */ + readonly id: string; +} + +export interface IMapLayerExtendsController { + /** 当前图层拓展是否已经被加载 */ + readonly loaded: boolean; + /** 拓展所属的图层对象 */ + readonly layer: IMapLayer; + + /** + * 加载此图层拓展,如果拓展依赖了其他拓展并且已经添加,将会自动加载其他拓展 + */ + load(): void; + + /** + * 获取地图数据,是对内部存储的直接引用 + */ + getMapData(): Readonly; + + /** + * 结束此拓展的生命周期,释放相关资源 + */ + unload(): void; +} + +export interface IMapLayer { + /** 地图宽度 */ + readonly width: number; + /** 地图高度 */ + readonly height: number; + /** 地图是否全部空白,此值具有保守性,即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白 */ + readonly empty: boolean; + + /** + * 调整地图尺寸,如果尺寸变大,那么会补零,如果尺寸变小,那么会将当前数组裁剪 + * @param width 地图宽度 + * @param height 地图高度 + */ + resize(width: number, height: number): void; + + /** + * 调整地图尺寸,但是将地图全部重置为零,不保留原地图数据 + * @param width 地图宽度 + * @param height 地图高度 + */ + resize2(width: number, height: number): void; + + /** + * 设置某一点的图块 + * @param block 图块数字 + * @param x 图块横坐标 + * @param y 图块纵坐标 + */ + setBlock(block: number, x: number, y: number): void; + + /** + * 获取指定点的图块 + * @param x 图块横坐标 + * @param y 图块纵坐标 + * @returns 指定点的图块,如果没有图块,返回 0,如果不在地图上,返回 -1 + */ + getBlock(x: number, y: number): number; + + /** + * 设置地图图块 + * @param array 地图图块数组 + * @param x 数组第一项代表的横坐标 + * @param y 数组第一项代表的纵坐标 + * @param width 传入数组所表示的矩形范围的宽度 + */ + putMapData(array: Uint32Array, x: number, y: number, width: number): void; + + /** + * 获取整个地图的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容 + */ + getMapData(): Uint32Array; + /** + * 获取地图指定区域的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容 + * @param x 左上角横坐标 + * @param y 左上角纵坐标 + * @param width 获取区域的宽度 + * @param height 获取区域的高度 + */ + getMapData( + x: number, + y: number, + width: number, + height: number + ): Uint32Array; + + /** + * 添加图层拓展,使用一系列钩子与图层本身通讯。不同图层拓展没有顺序关系。 + * @param ex 图层拓展对象 + * @returns 图层拓展控制对象,可以通过它来控制拓展的生命周期,也可以用于获取图层内的一些数据 + */ + addExtends(ex: IMapLayerExtends): IMapLayerExtendsController; + + /** + * 移除指定的图层拓展对象 + * @param ex 要移除的图层拓展对象,也可以填拓展对象的标识符 + */ + removeExtends(ex: IMapLayerExtends | string): void; +} diff --git a/packages/client-base/src/glUtils.ts b/packages/client-base/src/glUtils.ts index 6e216c2..d02414a 100644 --- a/packages/client-base/src/glUtils.ts +++ b/packages/client-base/src/glUtils.ts @@ -1,5 +1,14 @@ import { logger } from '@motajs/common'; +export interface ICompiledProgram { + /** 着色器程序 */ + readonly program: WebGLProgram; + /** 顶点着色器对象 */ + readonly vertexShader: WebGLShader; + /** 片段着色器对象 */ + readonly fragmentShader: WebGLShader; +} + /** * 编译着色器 * @param gl WebGL2 上下文 @@ -61,11 +70,17 @@ export function compileProgramWith( gl: WebGL2RenderingContext, vs: string, fs: string -): WebGLProgram | null { +): ICompiledProgram | null { const vsShader = compileShader(gl, gl.VERTEX_SHADER, vs); const fsShader = compileShader(gl, gl.FRAGMENT_SHADER, fs); if (!vsShader || !fsShader) return null; + const program = compileProgram(gl, vsShader, fsShader); + if (!program) return null; - return compileProgram(gl, vsShader, fsShader); + return { + program, + vertexShader: vsShader, + fragmentShader: fsShader + }; } diff --git a/packages/client-base/src/index.ts b/packages/client-base/src/index.ts index 192a965..a832b94 100644 --- a/packages/client-base/src/index.ts +++ b/packages/client-base/src/index.ts @@ -1,2 +1,3 @@ +export * from './glUtils'; export * from './keyCodes'; export * from './types'; diff --git a/packages/common/src/dirtyTracker.ts b/packages/common/src/dirtyTracker.ts new file mode 100644 index 0000000..f79d471 --- /dev/null +++ b/packages/common/src/dirtyTracker.ts @@ -0,0 +1,133 @@ +import { isNil } from 'lodash-es'; +import { IDirtyTracker } from './types'; + +/** + * 布尔类型的脏标记追踪器。当传入 `dirtySince` 的标记不属于当前的追踪器时,会返回 `true` + */ +export class PrivateBooleanDirtyTracker implements IDirtyTracker { + /** 标记映射 */ + private markMap: WeakMap = new WeakMap(); + /** 脏标记 */ + private dirtyFlag: number = 0; + + mark(): symbol { + const symbol = Symbol(); + this.markMap.set(symbol, this.dirtyFlag); + return symbol; + } + + unmark(mark: symbol): void { + this.markMap.delete(mark); + } + + dirtySince(mark: symbol): boolean { + const num = this.markMap.get(mark); + if (isNil(num)) return true; + return num < this.dirtyFlag; + } + + hasMark(symbol: symbol): boolean { + return this.markMap.has(symbol); + } + + /** + * 将数据标记为脏 + */ + protected dirty() { + this.dirtyFlag++; + } +} + +/** + * 列表的脏标记追踪器。当传入 `dirtySince` 的标记不属于当前的追踪器时,会返回空集合 + */ +export class PrivateListDirtyTracker + implements IDirtyTracker> +{ + /** 标记映射,键表示在索引,值表示其对应的标记数字 */ + private readonly markMap: Map = new Map(); + /** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */ + private readonly symbolMap: WeakMap = new WeakMap(); + + /** 脏标记数字 */ + private dirtyFlag: number = 0; + + constructor(protected length: number) {} + + mark(): symbol { + const symbol = Symbol(); + this.symbolMap.set(symbol, this.dirtyFlag); + return symbol; + } + + unmark(mark: symbol): void { + this.symbolMap.delete(mark); + } + + dirtySince(mark: symbol): Set { + const num = this.symbolMap.get(mark); + const res = new Set(); + if (isNil(num)) return res; + this.markMap.forEach((v, k) => { + if (v > num) res.add(k); + }); + return res; + } + + hasMark(symbol: symbol): boolean { + return this.symbolMap.has(symbol); + } + + protected dirty(data: T): void { + if (data >= this.length) return; + this.dirtyFlag++; + this.markMap.set(data, this.dirtyFlag); + } + + protected updateLength(length: number) { + this.length = length; + } +} + +export class PrivateMapDirtyTracker + implements IDirtyTracker> +{ + /** 标记映射,键表示名称,值表示其对应的标记数字 */ + private readonly markMap: Map = new Map(); + /** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */ + private readonly symbolMap: WeakMap = new WeakMap(); + + /** 脏标记数字 */ + private dirtyFlag: number = 0; + + constructor(protected length: number) {} + + mark(): symbol { + const symbol = Symbol(); + this.symbolMap.set(symbol, this.dirtyFlag); + return symbol; + } + + unmark(mark: symbol): void { + this.symbolMap.delete(mark); + } + + dirtySince(mark: symbol): Record { + const num = this.symbolMap.get(mark) ?? 0; + const obj: Partial> = {}; + this.markMap.forEach((v, k) => { + if (v > num) obj[k] = true; + else obj[k] = false; + }); + return obj as Record; + } + + hasMark(symbol: symbol): boolean { + return this.symbolMap.has(symbol); + } + + protected dirty(data: T): void { + this.dirtyFlag++; + this.markMap.set(data, this.dirtyFlag); + } +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 8ff45f4..7133d2a 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,2 +1,4 @@ +export * from './dirtyTracker'; export * from './logger'; export * from './utils'; +export * from './types'; diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index a6c7b34..c64762e 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -27,6 +27,21 @@ "25": "Unknown audio type. Header: '$1'", "26": "Uncaught error when fetching stream data from '$1'. Error info: $2.", "27": "No autotile connection data, please ensure you have created autotile connection map.", + "28": "Cannot compile map render shader.", + "29": "Cannot get uniform location of map render shader.", + "30": "", + "31": "No asset data is specified when rending map.", + "32": "Every layer added to map renderer must share the same size. Different layer: $1.", + "33": "Map layer transfered to vertex generator must belong to the renderer that the generator use.", + "34": "The texture of moving block must be a part of built asset.", + "35": "Tile background transfered to map renderer does not exists.", + "36": "Tile background transfered to map renderer has no frame data.", + "37": "Frame of tile background transfered to map renderer must share the same size.", + "38": "Cached texture cannot be convert to asset. This is likely an internal bug, please contact us.", + "39": "Offset pool size exceeds WebGL2 limitation, ensure size type of your big image is less than $1.", + "40": "Material used by map block $1 is not found in built asset. Please ensure you have pack it into asset.", + "41": "You are trying to use a texture on moving block whose offset is not in the offset pool, please build it into asset after loading.", + "42": "The layerList property of map-render element is required.", "1101": "Shadow extension needs 'floor-hero' extension as dependency.", "1201": "Floor-damage extension needs 'floor-binder' extension as dependency.", "1301": "Portal extension need 'floor-binder' extension as dependency.", @@ -112,6 +127,10 @@ "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.", "79": "Assets can only be built once.", + "80": "Parameter count of MapLayer.getMapData must be 0 or 4.", + "81": "Map data to get is partially (or totally) out of range. Overflowed area will be filled with zero.", + "82": "Big image offset size is larger than 64. Considier reduce big image offset bucket.", + "83": "It seems that you call 'updateBlock' too frequently. This will extremely affect game's performance, so considering integrate them into one 'updateArea' or 'updateBlockList' call.", "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." } diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts new file mode 100644 index 0000000..767a4e0 --- /dev/null +++ b/packages/common/src/types.ts @@ -0,0 +1,32 @@ +export interface IDirtyMarker { + /** + * 标记为脏,即进行了一次更新 + * @param data 传递给追踪器的数据 + */ + dirty(data: T): void; +} + +export interface IDirtyTracker { + /** + * 对状态进行标记 + */ + mark(): symbol; + + /** + * 取消指定标记符号 + * @param mark 标记符号 + */ + unmark(mark: symbol): void; + + /** + * 从指定标记符号开始,数据是否发生了变动 + * @param mark 标记符号 + */ + dirtySince(mark: symbol): T; + + /** + * 当前追踪器是否包含指定标记符号 + * @param symbol 标记符号 + */ + hasMark(symbol: symbol): boolean; +} diff --git a/packages/render-assets/src/animater.ts b/packages/render-assets/src/animater.ts index 7dfef8f..95228b3 100644 --- a/packages/render-assets/src/animater.ts +++ b/packages/render-assets/src/animater.ts @@ -1,123 +1,82 @@ -import { logger } from '@motajs/common'; import { ITexture, ITextureAnimater, ITextureRenderable } from './types'; /** - * 基于帧的动画控制器,创建时传入的参数代表帧数,生成动画时传入的参数自定义 + * 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,动画传入的参数代表帧数 */ -export abstract class FrameBasedAnimater - implements ITextureAnimater -{ - texture: ITexture | null = null; - - /** 动画的总帧数 */ - protected frames: number = 0; - - create(texture: ITexture, data: number): void { - if (this.texture) { - logger.warn(70); - return; - } - this.texture = texture; - this.frames = data; - } - - /** - * 判断当前动画控制器是否已经初始化完毕并开始生成动画 - */ - protected check(): boolean { - if (!this.texture || this.frames === 0) { - logger.warn(71); - return false; - } - if (this.texture.height % this.frames !== 0) { - logger.warn(72); - return false; - } - return true; - } - - abstract open(init: T): Generator | null; - - abstract cycled(init: T): Generator | null; -} - -/** - * 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,创建时传入的参数代表帧数 - */ -export class TextureRowAnimater extends FrameBasedAnimater { - *open(): Generator | null { - if (!this.check()) return null; - const renderable = this.texture!.static(); +export class TextureRowAnimater implements ITextureAnimater { + *once(texture: ITexture, frames: number): Generator { + if (frames <= 0) return; + const renderable = texture.render(); const { x: ox, y: oy } = renderable.rect; - const { width: w, height } = this.texture!; - const h = height / this.frames; - for (let i = 0; i < this.frames; i++) { + const { width: w, height } = texture!; + const h = height / frames; + for (let i = 0; i < frames; i++) { const renderable: ITextureRenderable = { - source: this.texture!.source, - rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h }) + source: texture.source, + rect: texture.clampRect({ x: i * w + ox, y: oy, w, h }) }; yield renderable; } } - *cycled(): Generator | null { - if (!this.check()) return null; - const renderable = this.texture!.static(); + *cycled(texture: ITexture, frames: number): Generator { + if (frames <= 0) return; + const renderable = texture.render(); const { x: ox, y: oy } = renderable.rect; - const { width: w, height } = this.texture!; - const h = height / this.frames; + const { width: w, height } = texture; + const h = height / frames; let i = 0; while (true) { const renderable: ITextureRenderable = { - source: this.texture!.source, - rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h }) + source: texture.source, + rect: texture.clampRect({ x: i * w + ox, y: oy, w, h }) }; yield renderable; i++; - if (i === this.frames) i = 0; + if (i === frames) i = 0; } } } /** - * 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,创建时传入的参数代表帧数 + * 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,动画传入的参数代表帧数 */ -export class TextureColumnAnimater extends FrameBasedAnimater { - *open(): Generator | null { - if (!this.check()) return null; - const renderable = this.texture!.static(); +export class TextureColumnAnimater implements ITextureAnimater { + *once(texture: ITexture, frames: number): Generator { + if (frames <= 0) return; + const renderable = texture.render(); const { x: ox, y: oy } = renderable.rect; - const { width, height: h } = this.texture!; - const w = width / this.frames; - for (let i = 0; i < this.frames; i++) { + const { width, height: h } = texture!; + const w = width / frames; + for (let i = 0; i < frames; i++) { const renderable: ITextureRenderable = { - source: this.texture!.source, - rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h }) + source: texture.source, + rect: texture.clampRect({ x: i * w + ox, y: oy, w, h }) }; yield renderable; } } - *cycled(): Generator | null { - if (!this.check()) return null; - const renderable = this.texture!.static(); + *cycled(texture: ITexture, frames: number): Generator { + if (frames <= 0) return null; + const renderable = texture.render(); const { x: ox, y: oy } = renderable.rect; - const { width, height: h } = this.texture!; - const w = width / this.frames; + const { width, height: h } = texture; + const w = width / frames; let i = 0; while (true) { const renderable: ITextureRenderable = { - source: this.texture!.source, - rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h }) + source: texture.source, + rect: texture.clampRect({ x: i * w + ox, y: oy, w, h }) }; yield renderable; i++; - if (i === this.frames) i = 0; + if (i === frames) i = 0; } } } -export interface IScanAnimaterCreate { +export interface IScanAnimaterData { /** 每帧的宽度 */ readonly width: number; /** 每帧的高度 */ @@ -130,82 +89,47 @@ export interface IScanAnimaterCreate { * 扫描动画控制器,会按照先从左到右,再从上到下的顺序依次输出,可以用于动画精灵图等 */ export class TextureScanAnimater - implements ITextureAnimater + implements ITextureAnimater { - texture: ITexture | null = null; + *once( + texture: ITexture, + data: IScanAnimaterData + ): Generator { + const w = texture.width; + const h = texture.height; - private width: number = 0; - private height: number = 0; - - private frames: number = 0; - private frameX: number = 0; - private frameY: number = 0; - - create(texture: ITexture, data: IScanAnimaterCreate): void { - if (this.texture) { - logger.warn(70); - return; - } - this.texture = texture; - - this.width = data.width; - this.height = data.height; - this.frames = data.frames; - - // 如果尺寸不匹配 - if ( - texture.width % data.width !== 0 || - texture.height % data.height !== 0 - ) { - logger.warn(74); - } - - const frameX = Math.floor(texture.width / data.width); - const frameY = Math.floor(texture.height / data.height); - const possibleFrames = frameX * frameY; - - // 如果传入的帧数超出了可能的帧数上限 - if (this.frames > possibleFrames) { - this.frames = possibleFrames; - } - } - - *open(): Generator | null { - const texture = this.texture; - if (!texture) return null; - - const w = this.width; - const h = this.height; - - for (let y = 0; y < this.frameY; y++) { - for (let x = 0; x < this.frameX; x++) { - const data: ITextureRenderable = { + let frame = 0; + for (let y = 0; y < data.width; y++) { + for (let x = 0; x < data.height; x++) { + const renderable: ITextureRenderable = { source: texture.source, rect: texture.clampRect({ x: x * w, y: y * h, w, h }) }; - yield data; + yield renderable; + frame++; + if (frame === data.frames) break; } } } - *cycled(): Generator | null { - const texture = this.texture; - if (!texture) return null; - - const w = this.width; - const h = this.height; + *cycled( + texture: ITexture, + data: IScanAnimaterData + ): Generator { + const w = texture.width; + const h = texture.height; let index = 0; while (true) { - const x = index % this.frameX; - const y = Math.floor(index / this.frameX); - const data: ITextureRenderable = { + const x = index % data.width; + const y = Math.floor(index / data.height); + const renderable: ITextureRenderable = { source: texture.source, rect: texture.clampRect({ x: x * w, y: y * h, w, h }) }; - yield data; + yield renderable; index++; - if (index === this.frames) index = 0; + if (index === data.frames) index = 0; } } } diff --git a/packages/render-assets/src/composer.ts b/packages/render-assets/src/composer.ts index 93ef2e0..0debb72 100644 --- a/packages/render-assets/src/composer.ts +++ b/packages/render-assets/src/composer.ts @@ -16,7 +16,7 @@ import vert from './shader/pack.vert?raw'; import frag from './shader/pack.frag?raw'; import { logger } from '@motajs/common'; import { isNil } from 'lodash-es'; -import { compileProgramWith } from 'packages/client-base/src/glUtils'; +import { compileProgramWith } from '@motajs/client-base'; interface IndexMarkedComposedData { /** 组合数据 */ @@ -68,7 +68,7 @@ export class TextureGridComposer const dx = x * data.width; const dy = y * data.height; const texture = tex[i + start]; - const renderable = texture.static(); + const renderable = texture.render(); const { x: sx, y: sy, w: sw, h: sh } = renderable.rect; ctx.drawImage(renderable.source, sx, sy, sw, sh, dx, dy, sw, sh); map.set(texture, { x: dx, y: dy, w: sw, h: sh }); @@ -146,7 +146,7 @@ export class TextureMaxRectsComposer ); const arr = [...input]; const rects = arr.map(v => { - const rect = v.static().rect; + const rect = v.render().rect; const toPack = new Rectangle(rect.w, rect.h); toPack.data = v; return toPack; @@ -164,7 +164,7 @@ export class TextureMaxRectsComposer bin.rects.forEach(v => { const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height }; map.set(v.data, rect); - const renderable = v.data.static(); + const renderable = v.data.render(); const { x, y, w, h } = renderable.rect; const source = renderable.source; ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height); @@ -229,7 +229,7 @@ export class TextureMaxRectsWebGL2Composer this.canvas.width = maxWidth; this.canvas.height = maxHeight; this.gl = this.canvas.getContext('webgl2')!; - const program = compileProgramWith(this.gl, vert, frag)!; + const { program } = compileProgramWith(this.gl, vert, frag)!; this.program = program; // 初始化画布数据 @@ -342,7 +342,7 @@ export class TextureMaxRectsWebGL2Composer rects.forEach((v, i) => { const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height }; map.set(v.data, rect); - const renderable = v.data.static(); + const renderable = v.data.render(); const { width: tw, height: th } = v.data.source; const { x, y, w, h } = renderable.rect; // 画到目标画布上的位置 @@ -422,7 +422,7 @@ export class TextureMaxRectsWebGL2Composer ); const arr = [...input]; const rects = arr.map(v => { - const rect = v.static().rect; + const rect = v.render().rect; const toPack = new Rectangle(rect.w, rect.h); toPack.data = v; return toPack; diff --git a/packages/render-assets/src/store.ts b/packages/render-assets/src/store.ts index c5c54fe..396a4a3 100644 --- a/packages/render-assets/src/store.ts +++ b/packages/render-assets/src/store.ts @@ -1,19 +1,20 @@ import { isNil } from 'lodash-es'; -import { Texture } from './texture'; -import { ITexture, ITextureStore, SizedCanvasImageSource } from './types'; +import { ITexture, ITextureStore } from './types'; import { logger } from '@motajs/common'; -export class TextureStore implements ITextureStore { - private readonly texMap: Map = new Map(); - private readonly invMap: Map = new Map(); +export class TextureStore + implements ITextureStore +{ + private readonly texMap: Map = new Map(); + private readonly invMap: Map = new Map(); private readonly aliasMap: Map = new Map(); private readonly aliasInvMap: Map = new Map(); - [Symbol.iterator](): Iterator<[key: number, tex: ITexture]> { + [Symbol.iterator](): Iterator<[key: number, tex: T]> { return this.texMap.entries(); } - entries(): Iterable<[key: number, tex: ITexture]> { + entries(): Iterable<[key: number, tex: T]> { return this.texMap.entries(); } @@ -21,15 +22,11 @@ export class TextureStore implements ITextureStore { return this.texMap.keys(); } - values(): Iterable { + values(): Iterable { return this.texMap.values(); } - createTexture(source: SizedCanvasImageSource): ITexture { - return new Texture(source); - } - - addTexture(identifier: number, texture: ITexture): void { + addTexture(identifier: number, texture: T): void { if (this.texMap.has(identifier)) { logger.warn(66, identifier.toString()); return; @@ -38,7 +35,7 @@ export class TextureStore implements ITextureStore { this.invMap.set(texture, identifier); } - private removeBy(id: number, tex: ITexture, alias?: string) { + private removeBy(id: number, tex: T, alias?: string) { this.texMap.delete(id); this.invMap.delete(tex); if (alias) { @@ -47,7 +44,7 @@ export class TextureStore implements ITextureStore { } } - removeTexture(identifier: number | string | ITexture): void { + removeTexture(identifier: number | string | T): void { if (typeof identifier === 'string') { const id = this.aliasMap.get(identifier); if (isNil(id)) return; @@ -67,7 +64,7 @@ export class TextureStore implements ITextureStore { } } - getTexture(identifier: number): ITexture | null { + getTexture(identifier: number): T | null { return this.texMap.get(identifier) ?? null; } @@ -92,7 +89,7 @@ export class TextureStore implements ITextureStore { return this.texMap.get(id) ?? null; } - idOf(texture: ITexture): number | undefined { + idOf(texture: T): number | undefined { return this.invMap.get(texture); } diff --git a/packages/render-assets/src/streamComposer.ts b/packages/render-assets/src/streamComposer.ts index 3f489b3..d8f6e7a 100644 --- a/packages/render-assets/src/streamComposer.ts +++ b/packages/render-assets/src/streamComposer.ts @@ -18,7 +18,7 @@ export class TextureGridStreamComposer implements ITextureStreamComposer { readonly cols: number; private nowIndex: number = 0; - private outputIndex: number = 0; + private outputIndex: number = -1; private nowTexture: ITexture; private nowCanvas: HTMLCanvasElement; @@ -65,7 +65,7 @@ export class TextureGridStreamComposer implements ITextureStreamComposer { const nowRow = Math.floor(index / this.cols); const nowCol = index % this.cols; - const { source, rect } = tex.static(); + const { source, rect } = tex.render(); const { x: cx, y: cy, w: cw, h: ch } = rect; const x = nowRow * this.width; const y = nowCol * this.height; @@ -107,7 +107,7 @@ export class TextureMaxRectsStreamComposer /** Max Rects 打包器 */ readonly packer: MaxRectsPacker; - private outputIndex: number = 0; + private outputIndex: number = -1; private nowTexture!: ITexture; private nowCanvas!: HTMLCanvasElement; @@ -151,7 +151,7 @@ export class TextureMaxRectsStreamComposer *add(textures: Iterable): Generator { const arr = [...textures]; const rects = arr.map(v => { - const rect = v.static().rect; + const rect = v.render().rect; const toPack = new Rectangle(rect.w, rect.h); toPack.data = v; return toPack; @@ -176,7 +176,7 @@ export class TextureMaxRectsStreamComposer h: v.height }; this.nowMap.set(v.data, target); - const { source, rect } = v.data.static(); + const { source, rect } = v.data.render(); const { x: cx, y: cy, w: cw, h: ch } = rect; this.nowCtx.drawImage(source, cx, cy, cw, ch, v.x, v.y, cw, ch); }); diff --git a/packages/render-assets/src/texture.ts b/packages/render-assets/src/texture.ts index bcddde2..dd9bb6f 100644 --- a/packages/render-assets/src/texture.ts +++ b/packages/render-assets/src/texture.ts @@ -2,16 +2,14 @@ import { logger } from '@motajs/common'; import { IRect, ITexture, - ITextureAnimater, ITextureComposedData, ITextureRenderable, ITextureSplitter, SizedCanvasImageSource } from './types'; -export class Texture implements ITexture { +export class Texture implements ITexture { source: SizedCanvasImageSource; - animater: ITextureAnimater | null = null; width: number; height: number; isBitmap: boolean = false; @@ -85,12 +83,7 @@ export class Texture implements ITexture { return splitter.split(this, data); } - animated(animater: ITextureAnimater, data: T): void { - this.animater = animater; - animater.create(this, data); - } - - static(): ITextureRenderable { + render(): ITextureRenderable { return { source: this.source, rect: { x: this.cl, y: this.ct, w: this.width, h: this.height } @@ -112,21 +105,10 @@ export class Texture implements ITexture { }; } - dynamic(data: A): Generator | null { - if (!this.animater) return null; - return this.animater.open(data); - } - - cycled(data: A): Generator | null { - if (!this.animater) return null; - return this.animater.cycled(data); - } - dispose(): void { if (this.source instanceof ImageBitmap) { this.source.close(); } - this.animater = null; } toAsset(asset: ITextureComposedData): boolean { @@ -192,7 +174,7 @@ export class Texture implements ITexture { fy: number ): Generator | null { if (!animate) return null; - const { x: ox, y: oy } = origin.static().rect; + const { x: ox, y: oy } = origin.render().rect; while (true) { const next = animate.next(); diff --git a/packages/render-assets/src/types.ts b/packages/render-assets/src/types.ts index 375460e..258d13a 100644 --- a/packages/render-assets/src/types.ts +++ b/packages/render-assets/src/types.ts @@ -66,40 +66,29 @@ export interface ITextureSplitter { split(texture: ITexture, data: T): Generator; } -export interface ITextureAnimater { - /** 此动画控制器所控制的贴图 */ - readonly texture: ITexture | null; - - /** - * 对一个贴图对象创建动画控制器 - * @param texture 要绑定的贴图对象 - * @param data 传递给动画控制器的参数 - */ - create(texture: ITexture, data: T): void; - +export interface ITextureAnimater { /** * 开始动画序列 - * @param init 动画初始化参数 + * @param texture 贴图对象 + * @param data 动画初始化参数 */ - open(init: I): Generator | null; + once(texture: ITexture, data: T): Generator; /** * 开始循环动画序列 - * @param init 动画初始化参数 + * @param texture 贴图对象 + * @param data 动画初始化参数 */ - cycled(init: I): Generator | null; + cycled(texture: ITexture, data: T): Generator; } -export interface ITexture { +export interface ITexture { /** 贴图的图像源 */ readonly source: SizedCanvasImageSource; - /** 此贴图使用的动画控制器 */ - readonly animater: ITextureAnimater | null; /** 贴图宽度 */ readonly width: number; /** 贴图高度 */ readonly height: number; - /** 当前贴图是否是完整 bitmap 图像 */ readonly isBitmap: boolean; @@ -116,17 +105,10 @@ export interface ITexture { */ split(splitter: ITextureSplitter, data: T): Generator; - /** - * 将此贴图标记为可动画贴图,使用传入的动画控制器描述动画。每个贴图只能绑定一个动画控制器,反之同理 - * @param animater 动画控制器 - * @param data 传递给动画控制器的参数 - */ - animated(animater: ITextureAnimater, data: T): void; - /** * 获取整张图的可渲染对象 */ - static(): ITextureRenderable; + render(): ITextureRenderable; /** * 限制矩形范围至当前贴图对象范围 @@ -135,23 +117,11 @@ export interface ITexture { clampRect(rect: Readonly): Readonly; /** - * 获取贴图经过矩形裁剪后的可渲染对象,并不是简单地对图像源裁剪,还会处理其他情况 + * 获取贴图经过指定矩形裁剪后的可渲染对象,并不是简单地对图像源裁剪,还会处理其他情况 * @param rect 裁剪矩形 */ clipped(rect: Readonly): ITextureRenderable; - /** - * 获取一系列动画可渲染对象,不循环,按帧数依次排列 - * @param data 传递给动画控制器的初始化参数 - */ - dynamic(data: A): Generator | null; - - /** - * 获取无限循环的动画可渲染对象 - * @param data 传递给动画控制器的初始化参数 - */ - cycled(data: A): Generator | null; - /** * 释放此贴图的资源,将不能再被使用 */ @@ -165,13 +135,20 @@ export interface ITexture { toAsset(asset: ITextureComposedData): boolean; } -export interface ITextureStore { - [Symbol.iterator](): Iterator<[key: number, tex: ITexture]>; +export const enum TextureOffsetDirection { + LeftToRight, + RightToLeft, + TopToBottom, + BottomToTop +} + +export interface ITextureStore { + [Symbol.iterator](): Iterator<[key: number, tex: T]>; /** * 获取纹理对象键值对的可迭代对象 */ - entries(): Iterable<[key: number, tex: ITexture]>; + entries(): Iterable<[key: number, tex: T]>; /** * 获取纹理对象的键的可迭代对象 @@ -181,35 +158,29 @@ export interface ITextureStore { /** * 获取纹理对象的值的可迭代对象 */ - values(): Iterable; - - /** - * 通过图像源创建贴图对象 - * @param source 贴图使用的图像源 - */ - createTexture(source: SizedCanvasImageSource): ITexture; + values(): Iterable; /** * 添加一个贴图 * @param identifier 贴图 id * @param texture 贴图对象 */ - addTexture(identifier: number, texture: ITexture): void; + addTexture(identifier: number, texture: T): void; /** * 移除一个贴图 * @param identifier 要移除的贴图对象 id 或 别名 或 贴图对象 */ - removeTexture(identifier: number | string | ITexture): void; + removeTexture(identifier: number | string | T): void; /** * 根据贴图对象 id 获取贴图 * @param identifier 贴图对象 id */ - getTexture(identifier: number): ITexture | null; + getTexture(identifier: number): T | null; /** - * 给贴图对象命名一个别名 + * 给贴图对象命名一个别名,如果贴图对象不存在也可以设置 * @param identifier 贴图对象 id * @param alias 要命名的别名 */ diff --git a/packages/render-core/src/item.ts b/packages/render-core/src/item.ts index 539c04a..ad6fb82 100644 --- a/packages/render-core/src/item.ts +++ b/packages/render-core/src/item.ts @@ -225,7 +225,7 @@ export abstract class RenderItem IRenderTickerSupport, IRenderChildable, IRenderVueSupport, - ITransformUpdatable, + ITransformUpdatable, IRenderEvent { /** 渲染的全局ticker */ @@ -671,10 +671,12 @@ export abstract class RenderItem } } - updateTransform() { + updateTransform(transform: Transform) { // 更新变换矩阵时,不需要更新自身的缓存,直接调用父元素的更新即可 - this._parent?.update(); - this.emit('transform', this, this._transform); + if (transform === this.transform) { + this._parent?.update(); + this.emit('transform', this, this._transform); + } } //#endregion diff --git a/packages/render-core/src/transform.ts b/packages/render-core/src/transform.ts index 97b0d02..d7d13e7 100644 --- a/packages/render-core/src/transform.ts +++ b/packages/render-core/src/transform.ts @@ -1,7 +1,7 @@ import { mat3, mat4, ReadonlyMat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix'; -export interface ITransformUpdatable { - updateTransform?(): void; +export interface ITransformUpdatable { + updateTransform?(transform: T): void; } export class Transform { @@ -17,13 +17,13 @@ export class Transform { private modified: boolean = false; /** 绑定的可更新元素 */ - bindedObject?: ITransformUpdatable; + bindedObject?: ITransformUpdatable; /** * 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数 * @param obj 要绑定的对象 */ - bind(obj?: ITransformUpdatable) { + bind(obj?: ITransformUpdatable) { this.bindedObject = obj; } @@ -38,7 +38,7 @@ export class Transform { this.scaleY = 1; this.rad = 0; this.modified = false; - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); } /** @@ -49,7 +49,7 @@ export class Transform { this.scaleX *= x; this.scaleY *= y; this.modified = true; - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -61,7 +61,7 @@ export class Transform { this.x += x; this.y += y; this.modified = true; - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -76,7 +76,7 @@ export class Transform { this.rad -= n * Math.PI * 2; } this.modified = true; - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -88,7 +88,7 @@ export class Transform { this.scaleX = x; this.scaleY = y; this.modified = true; - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -100,7 +100,7 @@ export class Transform { this.x = x; this.y = y; this.modified = true; - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -111,7 +111,7 @@ export class Transform { mat3.rotate(this.mat, this.mat, rad - this.rad); this.rad = rad; this.modified = true; - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -138,7 +138,7 @@ export class Transform { mat3.fromValues(a, b, 0, c, d, 0, e, f, 1) ); this.calAttributes(); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -161,7 +161,7 @@ export class Transform { ): this { mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1); this.calAttributes(); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -285,13 +285,13 @@ export class Transform3D { mat: mat4 = mat4.create(); /** 绑定的可更新元素 */ - bindedObject?: ITransformUpdatable; + bindedObject?: ITransformUpdatable; /** * 绑定可更新对象 * @param obj 要绑定的对象 */ - bind(obj?: ITransformUpdatable) { + bind(obj?: ITransformUpdatable) { this.bindedObject = obj; } @@ -300,7 +300,7 @@ export class Transform3D { */ reset(): this { mat4.identity(this.mat); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -312,7 +312,7 @@ export class Transform3D { */ scale(x: number, y: number, z: number): this { mat4.scale(this.mat, this.mat, [x, y, z]); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -324,7 +324,7 @@ export class Transform3D { */ translate(x: number, y: number, z: number): this { mat4.translate(this.mat, this.mat, [x, y, z]); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -335,7 +335,7 @@ export class Transform3D { */ rotate(rad: number, axis: vec3): this { mat4.rotate(this.mat, this.mat, rad, axis); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -371,7 +371,7 @@ export class Transform3D { */ lookAt(eye: vec3, center: vec3, up: vec3): this { mat4.lookAt(this.mat, eye, center, up); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -384,7 +384,7 @@ export class Transform3D { */ perspective(fovy: number, aspect: number, near: number, far: number): this { mat4.perspective(this.mat, fovy, aspect, near, far); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } @@ -406,7 +406,7 @@ export class Transform3D { far: number ): this { mat4.ortho(this.mat, left, right, bottom, top, near, far); - this.bindedObject?.updateTransform?.(); + this.bindedObject?.updateTransform?.(this); return this; } diff --git a/script/lines.ts b/script/lines.ts index 27af861..1d3963a 100644 --- a/script/lines.ts +++ b/script/lines.ts @@ -25,9 +25,16 @@ import { formatSize } from './utils.js'; '.vue', '.less', '.css', - '.html' + '.html', + '.vert', + '.frag' ]; + const mapExt = (ext: string) => { + if (ext === '.vert' || ext === '.frag') return '.shader'; + else return ext; + }; + const check = async (dir: string) => { if (ignoreDir.some(v => dir.includes(v))) return; const d = await fs.readdir(dir); @@ -38,7 +45,7 @@ import { formatSize } from './utils.js'; if (exts.some(v => one.endsWith(v))) { const file = await fs.readFile(resolve(dir, one), 'utf-8'); const lines = file.split('\n').length; - const ext = extname(one); + const ext = mapExt(extname(one)); list[ext] ??= [0, 0, 0]; list[ext][0]++; list[ext][1] += lines; @@ -59,7 +66,7 @@ import { formatSize } from './utils.js'; }); for (const [ext, [file, lines, size]] of sorted) { console.log( - `${ext.slice(1).padEnd(7, ' ')}files: ${file + `${ext.slice(1).padEnd(9, ' ')}files: ${file .toString() .padEnd(6, ' ')}lines: ${lines .toString() @@ -67,7 +74,7 @@ import { formatSize } from './utils.js'; ); } console.log( - `\x1b[33mtotal files: ${totalFiles + `\x1b[33mtotal files: ${totalFiles .toString() .padEnd(6, ' ')}lines: ${totalLines .toString() diff --git a/vite.config.ts b/vite.config.ts index d79c981..85d5e89 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,7 +8,7 @@ import * as glob from 'glob'; const custom = [ 'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom', 'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin', - 'container-custom' + 'container-custom', 'map-render' ]; const aliases = glob.sync('packages/*/src').map((srcPath) => {