HumanBreak/packages-user/client-modules/src/render/map/vertex.ts

1126 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { IMapLayer } 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';
import { INSTANCED_COUNT } from './constant';
export interface IMapDataGetter {
/** 图块缩小行为,即图块比格子大时应该如何处理 */
readonly tileMinifyBehavior: MapTileBehavior;
/** 图块放大行为,即图块比格子小时应该如何处理 */
readonly tileMagnifyBehavior: MapTileBehavior;
/** 图块水平对齐,仅当图块行为为 `KeepSize` 时有效 */
readonly tileAlignX: MapTileAlign;
/** 图块竖直对齐,仅当图块行为为 `KeepSize` 时有效 */
readonly tileAlignY: MapTileAlign;
/** 图块大小与格子大小判断方式 */
readonly tileTestMode: MapTileSizeTestMode;
/**
* 根据图集的图像源获取其索引
* @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 VertexArrayOfBlock {
/** 图块在数组中的起始索引 */
readonly index: number;
/** 分块顶点数组 */
readonly array: Float32Array;
/** 分块数据 */
readonly block: IBlockData<MapVertexBlock>;
}
const enum VertexUpdate {
/** 更新顶点位置信息 */
Position = 0b01,
/** 更新贴图信息 */
Texture = 0b10,
/** 全部更新 */
All = 0b11
}
/**
* 构建地图顶点数组,当且仅当数组长度发生变化时才会标记为脏,需要完全重新分配内存。
*/
export class MapVertexGenerator
extends PrivateBooleanDirtyTracker
implements IMapVertexGenerator
{
//#region 属性声明
dynamicRenderDirty: boolean = true;
dynamicStart: number = 0;
dynamicCount: number = DYNAMIC_RESERVE;
/** 空顶点数组,因为空顶点很常用,所以直接定义一个全局常量 */
private static readonly EMPTY_VETREX: Float32Array = new Float32Array(
INSTANCED_COUNT
);
readonly block: IBlockSplitter<MapVertexBlock>;
/** 偏移数组 */
private instancedArray: Float32Array = new Float32Array();
/** 动态内容偏移数组 */
private dynamicInstancedArray: Float32Array = new Float32Array();
/** 分块宽度 */
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 updateCallDebounceTime: number = 0;
/** 更新图块性能检查的调用次数 */
private updateCallDebounceCount: number = 0;
constructor(
readonly renderer: IMapRenderer & IMapDataGetter,
readonly data: IContextData
) {
super();
this.resizeMap();
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.dynamicCount;
const offsetSize = count * INSTANCED_COUNT;
this.instancedArray = new Float32Array(offsetSize);
this.dynamicStart = staticCount;
this.dynamicInstancedArray = this.instancedArray.subarray(
staticCount * INSTANCED_COUNT,
count * INSTANCED_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;
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;
const origin: IMapVertexData = {
instancedArray: this.instancedArray
};
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;
this.mapWidth = this.renderer.mapWidth;
this.mapHeight = this.renderer.mapHeight;
}
}
expandMoving(targetSize: number): void {
const beforeOffset = this.instancedArray;
this.dynamicCount = targetSize;
this.mallocVertexArray();
this.instancedArray.set(beforeOffset);
const array: IMapVertexData = {
instancedArray: this.instancedArray
};
// 重建一下对应分块就行了,不需要重新分块
for (const block of this.block.iterateBlocks()) {
block.data.rebuild(array);
}
}
reduceMoving(targetSize: number): void {
const beforeOffsetLength = this.instancedArray.length;
const deltaLength = this.dynamicCount - targetSize;
this.dynamicCount = targetSize;
this.instancedArray = this.instancedArray.subarray(
0,
beforeOffsetLength - deltaLength * INSTANCED_COUNT
);
this.dynamicInstancedArray = this.dynamicInstancedArray.subarray(
0,
targetSize * INSTANCED_COUNT
);
// 这个不需要重新分配内存,依然共用同一个 ArrayBuffer因此不需要重新分块
}
updateLayerArray(): void {
this.needRebuild = true;
}
checkRebuild() {
if (!this.needRebuild) return;
this.needRebuild = false;
this.mallocVertexArray();
this.splitBlock();
this.dirty();
}
//#endregion
//#region 顶点数组更新
/**
* 获取图块经过对齐与缩放后的位置
* @param pos 图块位置信息
* @param width 图块的贴图宽度
* @param height 图块的贴图高度
*/
private getTilePosition(
pos: BlockMapPos,
width: number,
height: number
): Readonly<IRect> {
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 = 1 - pos.mapY * ch; // cell top
if (mode === MapTileBehavior.FitToSize) {
// 适应到格子大小
return {
x: cl,
y: ct,
w: cw,
h: 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;
switch (tileAlignX) {
case MapTileAlign.Start: {
// 左对齐
left = cl;
break;
}
case MapTileAlign.Center: {
// 左右居中对齐
left = cl + cwu - twu;
break;
}
case MapTileAlign.End: {
// 右对齐
left = cl + cw - tw;
break;
}
}
switch (tileAlignY) {
case MapTileAlign.Start: {
// 上对齐
top = ct;
break;
}
case MapTileAlign.Center: {
// 上下居中对齐
top = ct + chu - thu;
break;
}
case MapTileAlign.End: {
// 下对齐
top = ct + ch - th;
}
}
return { x: left, y: top, w: tw, h: th };
}
}
/**
* 更新指定图块的顶点数组信息
* @param vertex 顶点数组对象
* @param rect 可渲染对象的矩形区域
* @param index 图块索引对象
* @param assetIndex 贴图所在的图集索引
* @param offsetIndex 贴图偏移值所在偏移池的索引
* @param frames 贴图总帧数
* @param update 顶点坐标更新方式
*/
private updateTileVertex(
vertex: IMapVertexData,
rect: Readonly<IRect>,
index: IndexedBlockMapPos,
assetIndex: number,
offsetIndex: number,
frames: number,
update: VertexUpdate
) {
const { instancedArray } = vertex;
// 顶点数组
const { layerCount, assetWidth, assetHeight } = this.renderer;
const { x, y, w: width, h: height } = rect;
const startIndex = index.blockIndex * INSTANCED_COUNT;
if (update & VertexUpdate.Position) {
// 如果需要更新顶点坐标
const layerIndex = this.renderer.getLayerIndex(index.layer);
// 避免 z 坐标是 1 的时候被裁剪,因此范围选择 [-0.9, 0.9]
const layerStart = (layerIndex / layerCount) * 1.8 - 0.9;
const zIndex = -layerStart - index.mapY / this.mapHeight;
const { x, y, w, h } = this.getTilePosition(index, width, height);
// 图块位置
instancedArray[startIndex] = x;
instancedArray[startIndex + 1] = y;
instancedArray[startIndex + 2] = w;
instancedArray[startIndex + 3] = h;
// 图块纵深
instancedArray[startIndex + 8] = zIndex;
}
if (update & VertexUpdate.Texture) {
const texX = x / assetWidth;
const texY = y / assetHeight;
const texWidth = width / assetWidth;
const texHeight = height / assetHeight;
// 纹理坐标
instancedArray[startIndex + 4] = texX;
instancedArray[startIndex + 5] = texY;
instancedArray[startIndex + 6] = texWidth;
instancedArray[startIndex + 7] = texHeight;
// 不透明度
instancedArray[startIndex + 9] = 1;
// 帧数、偏移、纹理索引
instancedArray[startIndex + 12] = -1;
instancedArray[startIndex + 13] = frames;
instancedArray[startIndex + 14] = offsetIndex;
instancedArray[startIndex + 15] = assetIndex;
}
}
/**
* 更新指定点的自动元件,不会检查中心点是不是自动元件
* @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 bx = mx - block.dataX;
const by = my - block.dataY;
const newIndex: BlockIndex = {
layer,
mapX: mx,
mapY: my,
mapIndex: my * this.mapWidth + mx,
blockX: bx,
blockY: by,
blockIndex: by * block.width + bx
};
const tile = this.renderer.manager.getTile(mapArray[newIndex.mapIndex]);
if (!tile || tile.cls !== BlockCls.Autotile) 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) {
// 不存在可渲染对象,认为是空图块
const { instancedArray } = vertex;
const instancedStart = index.blockIndex * INSTANCED_COUNT;
// 只把坐标改成 0 就可以了,其他的保留
instancedArray[instancedStart] = 0;
instancedArray[instancedStart + 1] = 0;
instancedArray[instancedStart + 2] = 0;
instancedArray[instancedStart + 3] = 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 = layer.getMapRef();
if (!vertex) 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 {
if (!this.renderer.hasLayer(layer)) return;
this.checkRebuild();
// 这里多一圈是因为要更新这一圈的自动元件
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.checkRebuild();
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');
}
this.checkRebuild();
if (blocks.length <= 50) {
blocks.forEach(({ block: num, x, y }) => {
this.updateBlockVertex(layer, num, x, y);
});
return;
}
// 对于超出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();
}
}
});
}
updateBlockCache(block: Readonly<IBlockData<IMapVertexBlock>>): void {
if (!block.data.dirty) return;
const layers = this.renderer.getSortedLayer();
layers.forEach(layer => {
const dirty = block.data.getDirtyArea(layer);
if (!dirty || !dirty.dirty) return;
block.data.updated();
const vertex = block.data.getLayerData(layer);
const mapData = layer.getMapRef();
if (!vertex) 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]
);
}
}
});
}
//#endregion
//#region 动态图块
updateMoving(block: IMovingBlock, updateTexture: boolean): void {
if (!this.renderer.hasMoving(block)) return;
const { cls, frames, offset, texture } = block.texture;
const vertex: IMapVertexData = {
instancedArray: this.dynamicInstancedArray
};
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.Position;
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 {
moving.forEach(v => {
this.updateMoving(v, updateTexture);
});
}
deleteMoving(moving: IMovingBlock): void {
const instancedStart = moving.index * INSTANCED_COUNT;
// 这个需要全部清空了,因为可能会复用
this.dynamicInstancedArray.set(
MapVertexGenerator.EMPTY_VETREX,
instancedStart
);
this.dynamicRenderDirty = true;
}
//#endregion
//#region 图块状态
/**
* 获取指定图层指定坐标的图块对应的分块信息
* @param layer 图层对象
* @param x 图块横坐标
* @param y 图块纵坐标
*/
private getIndexInBlock(
layer: IMapLayer,
x: number,
y: number
): VertexArrayOfBlock | null {
const block = this.block.getBlockByDataLoc(x, y);
if (!block) return null;
const data = block?.data.getLayerInstanced(layer);
if (!data) return null;
const dx = x - block.x;
const dy = y - block.y;
const dIndex = dy * block.width + dx;
return { array: data, index: dIndex, block };
}
setStaticAlpha(
layer: IMapLayer,
x: number,
y: number,
alpha: number
): void {
const index = this.getIndexInBlock(layer, x, y);
if (!index) return;
index.array[index.index * INSTANCED_COUNT + 9] = alpha;
index.block.data.markRenderDirty();
}
setStaticFrame(
layer: IMapLayer,
x: number,
y: number,
frame: number
): void {
const index = this.getIndexInBlock(layer, x, y);
if (!index) return;
index.array[index.index * INSTANCED_COUNT + 12] = frame;
index.block.data.markRenderDirty();
}
getStaticAlpha(layer: IMapLayer, x: number, y: number): number {
const index = this.getIndexInBlock(layer, x, y);
if (!index) return 0;
return index.array[index.index * INSTANCED_COUNT + 9];
}
getStaticFrame(layer: IMapLayer, x: number, y: number): number {
const index = this.getIndexInBlock(layer, x, y);
if (!index) return -1;
return index.array[index.index * INSTANCED_COUNT + 12];
}
setDynamicAlpha(index: number, alpha: number): void {
this.dynamicInstancedArray[index * INSTANCED_COUNT + 9] = alpha;
this.dynamicRenderDirty = true;
}
setDynamicFrame(index: number, frame: number): void {
this.dynamicInstancedArray[index * INSTANCED_COUNT + 12] = frame;
this.dynamicRenderDirty = true;
}
getDynamicAlpha(index: number): number {
if (index > this.dynamicCount) return 0;
return this.dynamicInstancedArray[index * INSTANCED_COUNT + 9];
}
getDynamicFrame(index: number): number {
if (index > this.dynamicCount) return -1;
return this.dynamicInstancedArray[index * INSTANCED_COUNT + 12];
}
//#endregion
//#region 其他接口
renderDynamic(): void {
this.dynamicRenderDirty = false;
}
getVertexArray(): IMapVertexArray {
this.checkRebuild();
return {
dynamicStart: this.dynamicStart,
dynamicCount: this.dynamicCount,
tileInstanced: this.instancedArray
};
}
//#endregion
}
//#region 分块对象
class MapVertexBlock implements IMapVertexBlock {
instancedArray!: Float32Array;
dirty: boolean = true;
renderDirty: boolean = true;
private readonly layerDirty: Map<IMapLayer, ILayerDirtyData> = new Map();
readonly startIndex: number;
readonly endIndex: number;
readonly count: number;
readonly layerCount: number;
readonly instancedStart: number;
private readonly indexMap: Map<IMapLayer, number> = new Map();
private readonly instancedMap: Map<IMapLayer, Float32Array> = new Map();
/**
* 创建分块的顶点数组对象,此对象不能动态扩展,如果地图变化,需要全部重建
* @param renderer 渲染器对象
* @param originArray 原始顶点数组
* @param startIndex 起始网格索引
* @param count 单个图层的图块数量
* @param blockWidth 分块宽度
* @param blockHeight 分块高度
*/
constructor(
readonly renderer: IMapRenderer,
originArray: IMapVertexData,
startIndex: number,
count: number,
private readonly blockWidth: number,
private readonly blockHeight: number
) {
const layerCount = renderer.layerCount;
this.startIndex = startIndex * layerCount;
this.endIndex = (startIndex + count) * layerCount;
this.count = count;
const offsetStart = startIndex * layerCount * INSTANCED_COUNT;
this.instancedStart = offsetStart;
this.layerCount = layerCount;
this.rebuild(originArray);
}
render(): void {
this.renderDirty = false;
}
updated(): void {
this.dirty = false;
}
/**
* 标记为需要更新渲染缓冲区
*/
markRenderDirty() {
this.renderDirty = true;
}
markDirty(
layer: IMapLayer,
left: number,
top: number,
right: number,
bottom: number
): void {
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<ILayerDirtyData> | null {
return this.layerDirty.get(layer) ?? null;
}
rebuild(originArray: IMapVertexData) {
const offsetStart = this.instancedStart;
const count = this.count;
this.instancedArray = originArray.instancedArray.subarray(
offsetStart,
offsetStart + count * INSTANCED_COUNT * this.layerCount
);
this.renderer.getSortedLayer().forEach((v, i) => {
const os = i * count * INSTANCED_COUNT;
const oa = this.instancedArray.subarray(
os,
os + count * INSTANCED_COUNT
);
this.instancedMap.set(v, oa);
this.indexMap.set(v, i);
this.layerDirty.set(v, {
dirty: true,
dirtyLeft: 0,
dirtyTop: 0,
dirtyRight: this.blockWidth,
dirtyBottom: this.blockHeight
});
});
this.dirty = true;
}
getLayerInstanced(layer: IMapLayer): Float32Array | null {
return this.instancedMap.get(layer) ?? null;
}
getLayerData(layer: IMapLayer): IIndexedMapVertexData | null {
const offset = this.instancedMap.get(layer);
const index = this.indexMap.get(layer);
if (!offset || isNil(index)) return null;
return {
instancedArray: offset,
instancedStart:
this.instancedStart + index * this.count * INSTANCED_COUNT
};
}
}
//#endregion