feat: 画家算法排序半透明渲染

This commit is contained in:
AncTe committed 2026-11-10 16:01:46 +08:00
1 parent 3c5a997302
commit 67fcf15ea5
14 files changed
+1902 -712

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ export * from './layout';
export * from './map';
export * from './material';
export * from './save';
export * from './setting';
export * from './use';
export * from './shared';
@@ -0,0 +1,524 @@
import { describe, expect, it } from 'vitest';
import rendererSource from '../renderer.ts?raw';
import vertexSource from '../vertex.ts?raw';
import typesSource from '../types.ts?raw';
import movingSource from '../moving.ts?raw';
import statusSource from '../status.ts?raw';
import viewportSource from '../viewport.ts?raw';
import fragSource from '../shader/map.frag?raw';
// 提取指定方法的源码片段,从签名开始到该方法的四空格闭合括号为止
function getMethodBody(source: string, signature: string): string {
const start = source.indexOf(signature);
if (start < 0) return '';
const end = source.indexOf('\n }', start);
return end < 0 ? source.slice(start) : source.slice(start, end);
}
// 依据每个颜色附着的最近一次帧缓冲绑定,得到帧缓冲到颜色纹理的配对
function getColorAttachments(source: string): Map<string, string> {
const init = source.slice(
source.indexOf('private initContext()'),
source.indexOf('destroy(): void')
);
const result = new Map<string, string>();
const re =
/framebufferTexture2D\(\s*gl\.FRAMEBUFFER\s*,\s*gl\.COLOR_ATTACHMENT0\s*,\s*gl\.TEXTURE_2D\s*,\s*(\w+)/g;
let match: RegExpExecArray | null = re.exec(init);
while (match) {
const before = init.slice(0, match.index);
const bind = before.lastIndexOf('bindFramebuffer(gl.FRAMEBUFFER, ');
const fb =
bind < 0
? null
: /bindFramebuffer\(gl\.FRAMEBUFFER, (\w+)\)/.exec(
before.slice(bind)
);
result.set(fb ? fb[1] : '?', match[1]);
match = re.exec(init);
}
return result;
}
/**
* 去掉整行注释与空行,用于判断一个文件是否只剩注释
* @param source 源码文本
*/
function getUncommented(source: string): string {
return source
.split(/\r?\n/)
.filter(line => line.trim().length > 0 && !line.trim().startsWith('//'))
.join('\n');
}
describe('渲染源码闸门', () => {
// 覆盖层不再依赖独立模块,也不再有字符串身份键的构造器
it('keeps the overlay implementation inside the vertex generator', () => {
expect(vertexSource).not.toContain("from './overlay'");
expect(vertexSource).not.toContain('MapOverlayOrder');
expect(vertexSource).not.toContain('IMapOverlayOrder');
expect(vertexSource).not.toContain('getStaticOverlayKey');
expect(vertexSource).not.toContain('getDynamicOverlayKey');
});
// 覆盖层身份只由图层对象与数值索引组成,不再拼接任何字符串键
it('uses no string identity keys for the overlay', () => {
expect(/static:\$\{|dynamic:\$\{/.test(vertexSource)).toBe(false);
expect(vertexSource).not.toContain('Map<string');
});
// 覆盖块接口新增实例索引、写回静态格与不降级的不透明度契约
it('declares a single overlay block contract', () => {
const start = typesSource.indexOf('export interface IOverlayBlock');
expect(start).toBeGreaterThan(-1);
const end = typesSource.indexOf('\n}', start);
const body = typesSource.slice(start, end);
expect(body).toContain('readonly layer: IMapLayer;');
expect(body).toContain('readonly priority: number;');
expect(body).toContain('x: number;');
expect(body).toContain('y: number;');
expect(body).toContain('readonly index: number;');
expect(body).toContain('toStatic(');
expect(body).toContain('setAlpha(');
expect(body).toContain('readonly texture: IMaterialFramedData;');
expect(body).toContain('readonly renderer: IMapRenderer;');
expect(typesSource).not.toContain('IOverlayStaticBlock');
expect(typesSource).not.toContain('OverlayBlockKind');
expect(typesSource).not.toContain('kind');
});
// 移动算法退化为注释,接口仍保留但没有任何实现
it('keeps the mover interface without an implementation', () => {
expect(typesSource).toContain('export interface IRenderBlockMover');
expect(getUncommented(movingSource).trim()).toBe('');
});
// 渲染器提供五个覆盖层方法与一个内部工厂,不再保留移动图块接口
it('declares the renderer overlay api', () => {
const names = [
'addOverlayBlock(',
'getOverlayBlocks(',
'getOverlayBlockByIndex(',
'deleteOverlayBlock(',
'deleteOverlayBlockByIndex(',
'createOverlayBlock('
];
names.forEach(name => expect(typesSource).toContain(name));
expect(typesSource).not.toContain('addMovingBlock');
expect(typesSource).not.toContain('deleteMoving');
expect(typesSource).not.toContain('hasMoving');
});
// 覆盖层只剩一个实现了覆盖块接口的具体类
it('declares one concrete overlay block', () => {
expect(statusSource).toContain('export class OverlayBlock');
expect(statusSource).toContain('implements IOverlayBlock');
expect(statusSource).toContain('readonly index: number;');
expect(statusSource).toContain('readonly tile: number;');
expect(statusSource).toContain('readonly renderer: IMapRenderer;');
expect(statusSource).toContain('readonly priority: number;');
expect(statusSource).toContain('x: number = 0;');
expect(statusSource).toContain('y: number = 0;');
expect(statusSource).not.toContain('DynamicBlockStatus');
expect(statusSource).not.toContain('StaticOverlayBlock');
expect(statusSource).not.toContain('IOverlayStaticBlock');
});
// 覆盖层的不透明度与帧数统一走动态槽位接口
it('routes overlay state through the dynamic slot api', () => {
const body = statusSource.slice(
statusSource.indexOf('export class OverlayBlock')
);
expect(body).toContain('setDynamicAlpha(');
expect(body).toContain('setDynamicFrame(');
expect(body).toContain('getDynamicAlpha(');
expect(body).toContain('getDynamicFrame(');
expect(body).not.toContain('setStaticAlpha(');
expect(body).not.toContain('setStaticFrame(');
});
// 覆盖块的销毁与写回都委托给渲染器
it('delegates destroy and toStatic to the renderer', () => {
const body = statusSource.slice(
statusSource.indexOf('export class OverlayBlock')
);
expect(body).toContain('deleteOverlayBlock(this)');
expect(body).toContain('vertex.toStatic(this');
});
// 静态图块状态对象原样保留
it('keeps the static block status', () => {
expect(statusSource).toContain('export class StaticBlockStatus');
expect(statusSource).toContain('implements IBlockStatus');
});
// 移动算法的五个方法只以注释形式保留
it('keeps the moving algorithm as comments only', () => {
expect(getUncommented(movingSource).trim()).toBe('');
const methods = [
'lineTo(',
'moveAs(',
'moveRelative(',
'endMoving(',
'stepMoving('
];
methods.forEach(name => expect(movingSource).toContain(name));
});
// 渲染器用集合、索引映射与索引池管理覆盖层
it('manages overlays through a set map and pool', () => {
expect(rendererSource).toContain('overlayBlocks: Set<IOverlayBlock>');
expect(rendererSource).toContain('overlayIndexMap');
expect(rendererSource).toContain('overlayIndexPool');
expect(rendererSource).toContain('requireOverlayIndex(');
expect(rendererSource).toContain('returnOverlayIndex(');
});
// 新增覆盖块与 API 覆盖块共用同一个工厂
it('creates overlay blocks through one factory', () => {
expect(rendererSource).toContain('createOverlayBlock(');
const body = getMethodBody(rendererSource, 'addOverlayBlock(');
expect(body).toContain('createOverlayBlock(');
expect(body).toContain('updateOverlay(overlay, true)');
});
// 渲染器不再包含任何移动图块的公开面
it('removes the moving renderer surface', () => {
expect(rendererSource).not.toContain('IMovingRenderer');
expect(rendererSource).not.toContain('MovingBlock');
expect(rendererSource).not.toContain('IMovingBlock');
expect(rendererSource).not.toContain('addMovingBlock');
expect(rendererSource).not.toContain('deleteMoving');
expect(rendererSource).not.toContain('hasMoving');
});
// tick 只保留背景帧、地图帧与 ticker,不再步进任何图块
it('keeps the tick free of moving steps', () => {
const body = getMethodBody(rendererSource, '\n tick(timestamp');
expect(body).toContain('tickers.forEach');
expect(body).toContain('backgroundFrame');
expect(body).not.toContain('stepMoving');
expect(body).not.toContain('updateOverlayList');
expect(body).not.toContain('movingBlock');
});
// 排序键依次为图层、纵坐标、横坐标与绘制优先级
it('sorts overlays by layer y x and priority', () => {
const body = getMethodBody(vertexSource, 'private sortOverlay(');
expect(body).toContain('getOverlayBlocks()');
expect(body).toContain('getLayerIndex(a.layer)');
expect(body).toContain('getLayerIndex(b.layer)');
expect(body).toContain('a.y - b.y');
expect(body).toContain('a.x - b.x');
expect(body).toContain('a.priority - b.priority');
expect(body).toContain('overlayValueDirty = true');
expect(body).not.toContain('StaticOverlayBlock');
expect(body).not.toContain('instanceof');
});
// 组装时每块只按自身索引从动态数组拷贝 16 个浮点
it('assembles every overlay from its slot', () => {
const body = getMethodBody(vertexSource, 'private assembleOverlay(');
expect(body).toContain('block.index * INSTANCED_COUNT');
expect(body).toContain('dynamicInstancedArray');
expect(body).toContain('overlayCount');
expect(body).not.toContain('instanceof');
expect(body).not.toContain('rectX');
});
// 静态图块 alpha 小于 1 时自动提升为覆盖层
it('promotes static blocks below full alpha', () => {
const body = getMethodBody(vertexSource, 'setStaticAlpha(');
expect(body).toContain('alpha < 1');
expect(body).toContain('promoteStaticBlock(');
expect(body).toContain('getPromotedBlock(');
expect(body).toContain('writeStaticAlpha(');
const promote = getMethodBody(
vertexSource,
'private promoteStaticBlock('
);
expect(promote).toContain('createOverlayBlock(');
expect(promote).toContain('subarray(');
expect(promote).toContain('INSTANCED_COUNT');
expect(promote).toContain('setPromotedBlock(');
expect(promote).toContain('markRenderDirty(');
expect(vertexSource).not.toContain('alphaMap');
expect(vertexSource).not.toContain('flushPendingAlpha');
expect(vertexSource).not.toContain('pendingAlpha');
expect(vertexSource).not.toContain('overlayStatic');
});
// 读取已提升图块的 alpha 时转读覆盖层槽位
it('reads promoted alpha from the overlay slot', () => {
const getAlpha = getMethodBody(vertexSource, 'getStaticAlpha(');
expect(getAlpha).toContain('getPromotedBlock(');
expect(getAlpha).toContain('getDynamicAlpha(');
const setAlpha = getMethodBody(vertexSource, 'setStaticAlpha(');
expect(setAlpha).toContain('alpha >= 1');
expect(setAlpha).toContain('writeStaticAlpha(');
const write = getMethodBody(vertexSource, 'private writeStaticAlpha(');
expect(write).toContain('INSTANCED_COUNT');
expect(write).toContain('markRenderDirty(');
});
// 静态格更新时按掩码刷新提升槽位,源格变空时删除提升块
it('refreshes promoted slots and drops empty cells', () => {
const tile = getMethodBody(vertexSource, 'private updateTileVertex(');
expect(tile).toContain('getPromotedBlock(');
expect(tile).toContain('dynamicInstancedArray[');
expect(tile).toContain('instancedArray[startIndex] = 0');
expect(tile).toContain('VertexUpdate.Texture');
expect(tile).toContain('VertexUpdate.Frame');
const empty = getMethodBody(vertexSource, 'private updateVertexArray(');
expect(empty).toContain('getPromotedBlock(');
expect(empty).toContain('deleteOverlayBlock(');
});
// 覆盖层 alpha 恰好为 1 时不删除提升块,也没有自动降级
it('never demotes promoted overlays', () => {
const body = getMethodBody(vertexSource, 'setStaticAlpha(');
expect(body).toContain('getPromotedBlock(');
expect(body).toContain('.setAlpha(');
expect(body).not.toContain('deletePromotedBlock');
expect(body).not.toContain('deleteOverlayBlock');
expect(body).not.toContain('promotedBlocks.clear');
});
// toStatic 先写回静态格并按目标格重算位置与纵深,最后释放覆盖块
it('writes overlay data back to a static cell', () => {
const body = getMethodBody(vertexSource, 'toStatic(');
expect(body).toContain('getIndexInBlock(');
expect(body).toContain('INSTANCED_COUNT');
expect(body).toContain('layerStart');
expect(body).toContain('perBlockZ');
expect(body).toContain('cellWidth');
expect(body).toContain('renderWidth');
expect(body).toContain('markRenderDirty(');
expect(body).toContain('deleteOverlayBlock(');
});
// 删除覆盖块时顺序脏与值脏必须同时置位
it('keeps both dirty flags when deleting an overlay', () => {
const body = getMethodBody(vertexSource, 'deleteOverlay(');
expect(body).toContain('overlayOrderDirty = true');
expect(body).toContain('overlayValueDirty = true');
});
// 帧数读写先查提升表,命中时转调动态槽位
it('forwards frames through the promotion table', () => {
const setFrame = getMethodBody(vertexSource, 'setStaticFrame(');
expect(setFrame).toContain('getPromotedBlock(');
expect(setFrame).toContain('setDynamicFrame(');
const getFrame = getMethodBody(vertexSource, 'getStaticFrame(');
expect(getFrame).toContain('getPromotedBlock(');
expect(getFrame).toContain('getDynamicFrame(');
});
// 新增的私有覆盖层方法都在签名前带有 jsDoc
it('documents every new private overlay method', () => {
const signatures = [
'getPromotedBlock',
'setPromotedBlock',
'deletePromotedBlock',
'deletePromotedBlockByValue',
'promoteStaticBlock',
'writeStaticAlpha',
'sortOverlay',
'assembleOverlay'
];
let found = 0;
for (const name of signatures) {
const index = vertexSource.indexOf('private ' + name + '(');
if (index < 0) continue;
found++;
expect(
vertexSource.slice(Math.max(0, index - 400), index)
).toContain('/**');
}
expect(found).toBe(signatures.length);
});
// 取覆盖层数组的顺序固定为先排序再组装
it('takes the overlay array in sort then assemble order', () => {
const body = getMethodBody(vertexSource, 'getOverlayArray(');
expect(body).not.toBe('');
const sort = body.indexOf('sortOverlay(');
const assemble = body.indexOf('assembleOverlay(');
expect(sort).toBeGreaterThan(-1);
expect(assemble).toBeGreaterThan(sort);
expect(body).not.toContain('flushPendingAlpha');
});
// 覆盖层脏标记只由顺序脏与值脏组成
it('covers order and value in the overlay dirty flag', () => {
const body = getMethodBody(vertexSource, 'isOverlayDirty(');
expect(body).toContain('overlayOrderDirty');
expect(body).toContain('overlayValueDirty');
expect(body).not.toContain('alphaMap');
});
// 四个源码文件都不再引用任何被删除的覆盖层符号
it('references no removed overlay symbols', () => {
const sources = [
typesSource,
statusSource,
vertexSource,
rendererSource
];
const removed = [
'DynamicBlockStatus',
'StaticOverlayBlock',
'IOverlayStaticBlock',
'IMovingBlock',
'IMovingRenderer',
'MovingBlock',
'pendingAlpha',
'flushPendingAlpha',
'alphaMap',
'overlayStatic'
];
sources.forEach(source =>
removed.forEach(name => expect(source).not.toContain(name))
);
});
// 不透明遍与覆盖层遍分别显式设置深度掩码,并在覆盖层之后恢复
it('sets the depth mask explicitly for both passes', () => {
const off = rendererSource.indexOf('gl.depthMask(false)');
const on = rendererSource.lastIndexOf('gl.depthMask(true)');
expect(rendererSource).toContain('gl.depthMask(true)');
expect(off).toBeGreaterThan(-1);
expect(on).toBeGreaterThan(off);
});
// 使用分离式混合函数,保证 alpha 通道正确累积
it('uses separate blend factors', () => {
expect(rendererSource).toContain('blendFuncSeparate(');
expect(rendererSource).not.toContain(
'gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)'
);
});
// 后处理帧缓冲使用深度附件,并在缩放与销毁时完整处理
it('attaches and disposes depth renderbuffers', () => {
expect(rendererSource).toContain('DEPTH_COMPONENT24');
expect(rendererSource).toContain('framebufferRenderbuffer');
expect(rendererSource).toContain('renderbufferStorage');
expect(rendererSource).toContain('deleteRenderbuffer');
expect(rendererSource).toContain('deleteFramebuffer');
expect(rendererSource).toContain('deleteTexture');
expect(
/setCanvasSize\([\s\S]{0,2000}renderbufferStorage/.test(
rendererSource
)
).toBe(true);
});
// ping 帧缓冲绑定 ping 颜色纹理,pong 帧缓冲绑定 pong 颜色纹理
it('pairs each framebuffer with its own color texture', () => {
const attach = getColorAttachments(rendererSource);
expect(attach.get('pingFramebuffer')).toBe('pingTexture2D');
expect(attach.get('pongFramebuffer')).toBe('pongTexture2D');
});
// 最后一个后处理效果输出到屏幕,链路不再使用倒数第二个切换
it('renders the last post effect to the screen', () => {
const start = rendererSource.indexOf('postEffects.forEach(');
const end = rendererSource.indexOf('});', start);
const loop = rendererSource.slice(start, end < 0 ? start + 1600 : end);
expect(loop).toContain('.length - 1');
expect(loop).toContain('null');
expect(rendererSource).not.toContain('.length - 2');
expect(rendererSource).not.toContain('postEffects.length > 1');
});
// 第一个效果读取 ping 的颜色纹理,零个效果时直接绘制到默认帧缓冲
it('feeds the scene texture into the first post effect', () => {
const chain = rendererSource.slice(
rendererSource.indexOf('const postEffects'),
rendererSource.indexOf('postEffects.forEach(')
);
expect(chain).toContain('pingTexture2D');
});
// 覆盖层使用独立缓冲区与实例化绘制,脏分支先取数组再取数量
it('draws the overlay from its own instanced buffer', () => {
expect(rendererSource).toContain('overlayBuffer');
expect(rendererSource).toContain('drawArraysInstanced');
expect(rendererSource).not.toContain('drawElementsInstanced');
const start = rendererSource.indexOf('isOverlayDirty()');
expect(start).toBeGreaterThan(-1);
const seg = rendererSource.slice(start, start + 900);
const array = seg.indexOf('getOverlayArray()');
const count = seg.indexOf('getOverlayCount()');
expect(array).toBeGreaterThan(-1);
expect(count).toBeGreaterThan(-1);
expect(array).toBeLessThan(count);
});
// 不透明遍按动态起始索引裁掉动态尾部,避免与覆盖层重复绘制
it('clips the dynamic tail from the opaque pass', () => {
expect(rendererSource).toContain('Math.min(v.endIndex, dynamicStart)');
});
// 覆盖层绘制区间保留了解释性注释,说明深度、顺序与上传策略
it('documents the overlay draw block', () => {
const start = rendererSource.indexOf('// 图块');
const end = rendererSource.indexOf('// Post effects', start);
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
const comments = (
rendererSource.slice(start, end).match(/(^|\r?\n)\s*\/\//g) || []
).length;
expect(comments).toBeGreaterThanOrEqual(6);
});
// 分块张量布局保持不变,且静态分块数组没有被排序
it('keeps the chunk layout and never sorts the chunk array', () => {
expect(vertexSource).toContain('this.dynamicStart = staticCount;');
expect(vertexSource).toContain('instancedStart');
expect(vertexSource).toContain('i * count * INSTANCED_COUNT');
expect(
/(instancedArray|tileInstanced)\s*\.sort\(/.test(vertexSource)
).toBe(false);
});
// 视口仍然把动态尾部合并进渲染区域,由不透明遍负责裁剪
it('keeps merging the dynamic tail into the render area', () => {
expect(viewportSource).toContain(
'this.checkDynamic(renderArea, dynamicStart, dynamicCount);'
);
expect(viewportSource).toContain('last.count += dynamicCount;');
});
// 片元着色器的丢弃阈值已下调,旧阈值不再残留
it('lowers the fragment discard threshold', () => {
expect(fragSource).toContain('0.004');
expect(fragSource).not.toContain('alpha < 0.1');
});
// 渲染路径不使用半精度深度编码或顺序剥离机制
it('avoids half precision depth encoding and order peeling', () => {
const pattern = /OIT|depthPeel|gl_FragDepth|halfFloat|half_float/i;
expect(pattern.test(rendererSource)).toBe(false);
expect(pattern.test(vertexSource)).toBe(false);
});
// 块内偏移使用分块左上角坐标,跨分块地图才能定位到正确的实例
it('locates instances with data coordinates inside a chunk', () => {
const body = getMethodBody(vertexSource, 'private getIndexInBlock(');
expect(body).not.toBe('');
expect(body).toContain('x - block.elX');
expect(body).toContain('y - block.elY');
expect(body).not.toContain('x - block.x');
});
// getOverlayCount 的契约说明要求在脏时先调用 getOverlayArray 完成组装
it('documents the getOverlayCount contract', () => {
const index = typesSource.indexOf('getOverlayCount():');
expect(index).toBeGreaterThan(-1);
const docStart = typesSource.lastIndexOf('/**', index);
expect(docStart).toBeGreaterThan(-1);
expect(typesSource.slice(docStart, index)).toContain('getOverlayArray');
});
});
@@ -1,23 +1,23 @@
import { clamp } from 'lodash-es';
import {
IBlockData,
IBlockIndex,
IBlockInfo,
IChunkData,
IChunkIndex,
IChunkInfo,
IBlockSplitter,
IBlockSplitterConfig
IChunkSplitterConfig
} from './types';
import { ISearchable8Dir } from '@motajs/common';
export class BlockSplitter<T> implements IBlockSplitter<T> {
blockWidth: number = 0;
blockHeight: number = 0;
dataWidth: number = 0;
dataHeight: number = 0;
chunkWidth: number = 0;
chunkHeight: number = 0;
elWidth: number = 0;
elHeight: number = 0;
width: number = 1;
height: number = 1;
/** 分块映射 */
readonly blockMap: Map<number, IBlockData<T>> = new Map();
readonly blockMap: Map<number, IChunkData<T>> = new Map();
/** 数据宽度配置 */
private splitDataWidth: number = 0;
@@ -37,17 +37,17 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
return x >= 0 && y >= 0 && x < this.width && y < this.height;
}
getBlockByLoc(x: number, y: number): IBlockData<T> | null {
getChunkByLoc(x: number, y: number): IChunkData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
return this.blockMap.get(index) ?? null;
}
getBlockByIndex(index: number): IBlockData<T> | null {
getChunkByIndex(index: number): IChunkData<T> | null {
return this.blockMap.get(index) ?? null;
}
setBlockByLoc(data: T, x: number, y: number): IBlockData<T> | null {
setChunkByLoc(data: T, x: number, y: number): IChunkData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
const block = this.blockMap.get(index);
@@ -56,66 +56,69 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
return block;
}
setBlockByIndex(data: T, index: number): IBlockData<T> | null {
setChunkByIndex(data: T, index: number): IChunkData<T> | null {
const block = this.blockMap.get(index);
if (!block) return null;
block.data = data;
return block;
}
*iterateBlockByLoc(x: number, y: number): Generator<IBlockIndex, void> {
*iterateElementsOfChunk(
x: number,
y: number
): Generator<IChunkIndex, void> {
if (!this.checkLocRange(x, y)) return;
const index = y * this.width + x;
yield* this.iterateBlockByIndex(index);
yield* this.iterateElementsOfChunkIndex(index);
}
*iterateBlockByIndex(index: number): Generator<IBlockIndex, void> {
*iterateElementsOfChunkIndex(index: number): Generator<IChunkIndex, void> {
const block = this.blockMap.get(index);
if (!block) return;
const startX = block.x * this.blockWidth;
const startY = block.y * this.blockHeight;
const startX = block.x * this.chunkWidth;
const startY = block.y * this.chunkHeight;
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 = {
const index: IChunkIndex = {
x: nx,
y: ny,
dataX: nx * this.blockWidth,
dataY: ny * this.blockHeight,
index: ny * this.dataWidth + nx
elX: nx * this.chunkWidth,
elY: ny * this.chunkHeight,
index: ny * this.elWidth + nx
};
yield index;
}
}
}
*iterateBlockByIndices(
*iterateElementsOfChunks(
indices: Iterable<number>
): Generator<IBlockIndex, void> {
): Generator<IChunkIndex, void> {
for (const index of indices) {
yield* this.iterateBlockByIndex(index);
yield* this.iterateElementsOfChunkIndex(index);
}
}
iterateBlocks(): Iterable<IBlockData<T>> {
iterateChunks(): Iterable<IChunkData<T>> {
return this.blockMap.values();
}
*iterateBlocksOfDataArea(
*iterateChunksOfElementArea(
x: number,
y: number,
width: number,
height: number
): Generator<IBlockData<T>> {
): Generator<IChunkData<T>> {
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);
const left = clamp(Math.floor(x / this.chunkWidth), 0, r);
const top = clamp(Math.floor(y / this.chunkHeight), 0, b);
const right = clamp(Math.floor(rx / this.chunkWidth), 0, r);
const bottom = clamp(Math.floor(by / this.chunkHeight), 0, b);
for (let ny = left; ny <= right; ny++) {
for (let nx = top; nx <= bottom; nx++) {
const index = ny * this.width + nx;
@@ -126,12 +129,12 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
}
}
getIndexByLoc(x: number, y: number): number {
getChunkIndexByLoc(x: number, y: number): number {
if (!this.checkLocRange(x, y)) return -1;
return y * this.width + x;
}
getLocByIndex(index: number): Loc | null {
getChunkLocByIndex(index: number): Loc | null {
if (index >= this.width * this.height) return null;
return {
x: index % this.width,
@@ -139,65 +142,65 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
};
}
getIndicesByLocList(list: Iterable<Loc>): Iterable<number> {
getChunkIndicesByLocList(list: Iterable<Loc>): Iterable<number> {
const res: number[] = [];
for (const { x, y } of list) {
res.push(this.getIndexByLoc(x, y));
res.push(this.getChunkIndexByLoc(x, y));
}
return res;
}
getLocListByIndices(list: Iterable<number>): Iterable<Loc | null> {
getChunkLocByIndices(list: Iterable<number>): Iterable<Loc | null> {
const res: (Loc | null)[] = [];
for (const index of list) {
res.push(this.getLocByIndex(index));
res.push(this.getChunkLocByIndex(index));
}
return res;
}
getBlockByDataLoc(x: number, y: number): IBlockData<T> | null {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
getChunkByElementLoc(x: number, y: number): IChunkData<T> | null {
const bx = Math.floor(x / this.chunkWidth);
const by = Math.floor(y / this.chunkHeight);
if (!this.checkLocRange(bx, by)) return null;
const index = by * this.width + bx;
return this.blockMap.get(index) ?? null;
}
getBlockByDataIndex(index: number): IBlockData<T> | null {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
return this.getBlockByDataLoc(x, y);
getChunkByElementIndex(index: number): IChunkData<T> | null {
const x = index % this.elWidth;
const y = Math.floor(index / this.elWidth);
return this.getChunkByElementLoc(x, y);
}
getIndicesByDataLocList(list: Iterable<Loc>): Set<number> {
const res = new Set<number>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
const bx = Math.floor(x / this.chunkWidth);
const by = Math.floor(y / this.chunkHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getIndicesByDataIndices(list: Iterable<number>): Set<number> {
getChunkIndicesByElementIndices(list: Iterable<number>): Set<number> {
const res = new Set<number>();
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);
const x = index % this.elWidth;
const y = Math.floor(index / this.elWidth);
const bx = Math.floor(x / this.chunkWidth);
const by = Math.floor(y / this.chunkHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getBlocksByDataLocList(list: Iterable<Loc>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
getChunksByElementLocList(list: Iterable<Loc>): Set<IChunkData<T>> {
const res = new Set<IChunkData<T>>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
const bx = Math.floor(x / this.chunkWidth);
const by = Math.floor(y / this.chunkHeight);
if (!this.checkLocRange(bx, by)) continue;
const index = bx + by * this.width;
const data = this.blockMap.get(index);
@@ -206,13 +209,13 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
return res;
}
getBlocksByDataIndices(list: Iterable<number>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
getChunksByElementIndices(list: Iterable<number>): Set<IChunkData<T>> {
const res = new Set<IChunkData<T>>();
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);
const x = index % this.elWidth;
const y = Math.floor(index / this.elWidth);
const bx = Math.floor(x / this.chunkWidth);
const by = Math.floor(y / this.chunkHeight);
if (!this.checkLocRange(bx, by)) continue;
const blockIndex = bx + by * this.width;
const data = this.blockMap.get(blockIndex);
@@ -221,11 +224,11 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
return res;
}
configSplitter(config: IBlockSplitterConfig): void {
this.splitDataWidth = config.dataWidth;
this.splitDataHeight = config.dataHeight;
this.splitBlockWidth = config.blockWidth;
this.splitBlockHeight = config.blockHeight;
configSplitter(config: IChunkSplitterConfig): void {
this.splitDataWidth = config.elWidth;
this.splitDataHeight = config.elHeight;
this.splitBlockWidth = config.chunkWidth;
this.splitBlockHeight = config.chunkHeight;
}
private mapBlock(
@@ -234,15 +237,15 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
realWidth: number,
width: number,
height: number,
fn: (block: IBlockInfo) => T
fn: (block: IChunkInfo) => T
) {
const index = y * realWidth + x;
const block: IBlockInfo = {
const block: IChunkInfo = {
index,
x,
y,
dataX: x * this.blockWidth,
dataY: y * this.blockHeight,
elX: x * this.chunkWidth,
elY: y * this.chunkHeight,
width,
height
};
@@ -251,12 +254,12 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
this.blockMap.set(index, blockData);
}
splitBlocks(mapFn: (block: IBlockInfo) => T): void {
splitChunks(mapFn: (block: IChunkInfo) => T): void {
this.blockMap.clear();
this.blockWidth = this.splitBlockWidth;
this.blockHeight = this.splitBlockHeight;
this.dataWidth = this.splitDataWidth;
this.dataHeight = this.splitDataHeight;
this.chunkWidth = this.splitBlockWidth;
this.chunkHeight = this.splitBlockHeight;
this.elWidth = this.splitDataWidth;
this.elHeight = this.splitDataHeight;
const restX = this.splitDataWidth % this.splitBlockWidth;
const restY = this.splitDataHeight % this.splitBlockHeight;
const width = Math.floor(this.splitDataWidth / this.splitBlockWidth);
@@ -264,8 +267,8 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
const hasXRest = restX > 0;
const hasYRest = restY > 0;
const realWidth = hasXRest ? width + 1 : width;
const bw = this.blockWidth;
const bh = this.blockHeight;
const bw = this.chunkWidth;
const bh = this.chunkHeight;
this.width = realWidth;
this.height = hasYRest ? height + 1 : height;
for (let ny = 0; ny < height; ny++) {
@@ -289,64 +292,64 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
}
}
class SplittedBlockData<T> implements IBlockData<T>, ISearchable8Dir {
class SplittedBlockData<T> implements IChunkData<T>, ISearchable8Dir {
width: number;
height: number;
x: number;
y: number;
dataX: number;
dataY: number;
elX: number;
elY: number;
index: number;
data: T;
constructor(
readonly splitter: BlockSplitter<T>,
info: IBlockInfo,
info: IChunkInfo,
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.elX = info.elX;
this.elY = info.elY;
this.index = info.index;
this.data = data;
}
left(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y);
left(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x - 1, this.y);
}
right(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y);
right(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x + 1, this.y);
}
up(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y - 1);
up(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x, this.y - 1);
}
down(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y + 1);
down(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x, this.y + 1);
}
leftUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y - 1);
leftUp(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x - 1, this.y - 1);
}
leftDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y + 1);
leftDown(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x - 1, this.y + 1);
}
rightUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y - 1);
rightUp(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x + 1, this.y - 1);
}
rightDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y + 1);
rightDown(): IChunkData<T> | null {
return this.splitter.getChunkByLoc(this.x + 1, this.y + 1);
}
next(): IBlockData<T> | null {
return this.splitter.getBlockByIndex(this.index + 1);
next(): IChunkData<T> | null {
return this.splitter.getChunkByIndex(this.index + 1);
}
}
@@ -1,6 +1,6 @@
import { logger } from '@motajs/common';
import {
IBlockData,
IChunkData,
IBlockSplitter,
IMapRenderer,
IMapRenderResult,
@@ -69,7 +69,7 @@ export class OnMapTextRenderer
getBlockByLoc(x: number, y: number): Readonly<IMapTextArea> | null {
const index = y * this.renderer.mapWidth + x;
// 首先尝试使用分块系统获取对应分块并从分块附着数据读取
const blockData = this.block.getBlockByDataIndex(index);
const blockData = this.block.getChunkByElementIndex(index);
if (blockData) {
const map = blockData.data.getAttachedData<
Map<number, MapTextArea>
@@ -80,7 +80,7 @@ export class OnMapTextRenderer
}
getBlockByIndex(index: number): Readonly<IMapTextArea> | null {
const blockData = this.block.getBlockByDataIndex(index);
const blockData = this.block.getChunkByElementIndex(index);
if (blockData) {
const map = blockData.data.getAttachedData<
Map<number, MapTextArea>
@@ -157,7 +157,7 @@ export class OnMapTextRenderer
* @param blockData 分块数据
*/
private getAttachedMap(
blockData: IBlockData<IMapVertexBlock>
blockData: IChunkData<IMapVertexBlock>
): Map<number, MapTextArea> {
const map = blockData.data.getAttachedData<Map<number, MapTextArea>>(
this.attachSymbol
@@ -173,7 +173,7 @@ export class OnMapTextRenderer
requireBlockArea(x: number, y: number): Readonly<IMapTextArea> | null {
const index = y * this.renderer.mapWidth + x;
// try to find corresponding block by data index
const blockData = this.block.getBlockByDataIndex(index);
const blockData = this.block.getChunkByElementIndex(index);
if (blockData) {
const map = this.getAttachedMap(blockData);
const exist = map.get(index);
@@ -194,7 +194,7 @@ export class OnMapTextRenderer
clear(): void {
// 清理所有附着在分块上的文本区域
for (const b of this.block.iterateBlocks()) {
for (const b of this.block.iterateChunks()) {
const blk = b.data;
const map = blk.getAttachedData<Map<number, MapTextArea>>(
this.attachSymbol
+1 -1
View File
@@ -1,6 +1,6 @@
export * from './extension';
export * from './block';
export * from './chunk';
export * from './constant';
export * from './element';
export * from './moving';
+271 -268
View File
@@ -1,268 +1,271 @@
import { linear, TimingFn } from 'mutate-animate';
import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
import { IMaterialFramedData, ITextureManager } from '@user/client-base';
import { logger } from '@motajs/common';
import { IMapLayer } from '@user/data-base';
import { DynamicBlockStatus } from './status';
export interface IMovingRenderer {
/** 素材管理器 */
readonly manager: ITextureManager;
/** 顶点数组生成器 */
readonly vertex: IMapVertexGenerator;
/**
* 获得当前时间戳
*/
getTimestamp(): number;
/**
* 删除指定的移动图块对象
* @param block 移动图块对象
*/
deleteMoving(block: IMovingBlock): void;
}
export class MovingBlock extends DynamicBlockStatus implements IMovingBlock {
readonly tile: number;
readonly renderer: IMovingRenderer;
readonly layer: IMapLayer;
texture: IMaterialFramedData;
index: number;
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 = true;
/** 是否通过 `setPos` 设置了位置 */
private posUpdated: boolean = false;
/** 兑现函数 */
private promiseFunc: () => void = () => {};
constructor(
renderer: IMovingRenderer & IMapRenderer,
index: number,
layer: IMapLayer,
block: number | IMaterialFramedData
) {
super(layer, renderer.vertex, index);
this.renderer = renderer;
this.index = index;
this.layer = layer;
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;
}
}
setPos(x: number, y: number): void {
if (!this.end) return;
this.x = x;
this.y = y;
this.posUpdated = true;
}
setTexture(texture: IMaterialFramedData): void {
if (texture === this.texture) return;
this.texture = texture;
this.renderer.vertex.updateMoving(this, true);
}
lineTo(
x: number,
y: number,
time: number,
timing?: TimingFn
): Promise<this> {
if (!this.end) return Promise.resolve(this);
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;
this.startTime = this.renderer.getTimestamp();
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> {
if (!this.end) return Promise.resolve(this);
this.time = time;
this.line = false;
this.relative = false;
this.startX = this.x;
this.startY = this.y;
this.startTime = this.renderer.getTimestamp();
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> {
if (!this.end) return Promise.resolve(this);
this.time = time;
this.line = false;
this.relative = true;
this.startX = this.x;
this.startY = this.y;
this.startTime = this.renderer.getTimestamp();
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) {
if (this.posUpdated) {
this.posUpdated = false;
return true;
}
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 true;
} 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 true;
} 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;
}
endMoving(): void {
this.end = true;
if (this.line) {
this.x = this.targetX;
this.y = this.targetY;
} else {
const [x, y] = this.curve(1);
if (this.relative) {
this.x = x + this.startX;
this.y = y + this.startY;
} else {
this.x = x;
this.y = y;
}
}
this.promiseFunc();
this.posUpdated = true;
}
useDefaultFrame(): void {
if (!this.renderer.manager.getTile(this.tile)) return;
const defaultFrame = this.renderer.manager.getDefaultFrame(this.tile);
this.useSpecifiedFrame(defaultFrame);
}
destroy(): void {
this.renderer.deleteMoving(this);
}
}
// 本阶段只保留静态层与覆盖层,mover 的创建与驱动不在范围内。
// 以下移动算法原样保留,供未来 `IRenderBlockMover` 实现参考。
//
// import { linear, ExcitationCurve, ExcitationCurve2D } from '@motajs/animate';
// import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
// import { IMaterialFramedData, ITextureManager } from '@user/client-base';
// import { logger } from '@motajs/common';
// import { IMapLayer } from '@user/data-base';
// import { DynamicBlockStatus } from './status';
//
// export interface IMovingRenderer {
// /** 素材管理器 */
// readonly manager: ITextureManager;
// /** 顶点数组生成器 */
// readonly vertex: IMapVertexGenerator;
//
// /**
// * 获得当前时间戳
// */
// getTimestamp(): number;
//
// /**
// * 删除指定的移动图块对象
// * @param block 移动图块对象
// */
// deleteMoving(block: IMovingBlock): void;
// }
//
// export class MovingBlock extends DynamicBlockStatus implements IMovingBlock {
// readonly tile: number;
// readonly renderer: IMovingRenderer;
//
// texture: IMaterialFramedData;
// index: number;
//
// /** 当前动画开始的时刻 */
// 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: ExcitationCurve = () => 0;
// /** 移动轨迹曲线 */
// private curve: ExcitationCurve2D = () => [0, 0];
//
// /** 动画开始时横坐标 */
// private startX: number = 0;
// /** 动画开始时纵坐标 */
// private startY: number = 0;
// /** 当前动画是否已经结束 */
// private end: boolean = true;
//
// /** 是否通过 `setPos` 设置了位置 */
// private posUpdated: boolean = false;
//
// /** 兑现函数 */
// private promiseFunc: () => void = () => {};
//
// constructor(
// renderer: IMovingRenderer & IMapRenderer,
// index: number,
// layer: IMapLayer,
// block: number | IMaterialFramedData
// ) {
// super(layer, renderer.vertex, index);
// this.renderer = renderer;
// this.index = index;
// 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;
// }
// }
//
// setPos(x: number, y: number): void {
// if (!this.end) return;
// this.x = x;
// this.y = y;
// this.posUpdated = true;
// }
//
// setTexture(texture: IMaterialFramedData): void {
// if (texture === this.texture) return;
// this.texture = texture;
// this.renderer.vertex.updateOverlay(this, true);
// }
//
// lineTo(
// x: number,
// y: number,
// time: number,
// timing?: ExcitationCurve
// ): Promise<this> {
// if (!this.end) return Promise.resolve(this);
// 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;
// this.startTime = this.renderer.getTimestamp();
// 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: ExcitationCurve2D,
// time: number,
// timing?: ExcitationCurve
// ): Promise<this> {
// if (!this.end) return Promise.resolve(this);
// this.time = time;
// this.line = false;
// this.relative = false;
// this.startX = this.x;
// this.startY = this.y;
// this.startTime = this.renderer.getTimestamp();
// 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: ExcitationCurve2D,
// time: number,
// timing?: ExcitationCurve
// ): Promise<this> {
// if (!this.end) return Promise.resolve(this);
// this.time = time;
// this.line = false;
// this.relative = true;
// this.startX = this.x;
// this.startY = this.y;
// this.startTime = this.renderer.getTimestamp();
// 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) {
// if (this.posUpdated) {
// this.posUpdated = false;
// return true;
// }
// 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 true;
// } 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 true;
// } 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;
// }
//
// endMoving(): void {
// this.end = true;
// if (this.line) {
// this.x = this.targetX;
// this.y = this.targetY;
// } else {
// const [x, y] = this.curve(1);
// if (this.relative) {
// this.x = x + this.startX;
// this.y = y + this.startY;
// } else {
// this.x = x;
// this.y = y;
// }
// }
// this.promiseFunc();
// this.posUpdated = true;
// }
//
// useDefaultFrame(): void {
// if (!this.renderer.manager.getTile(this.tile)) return;
// const defaultFrame = this.renderer.manager.getDefaultFrame(this.tile);
// this.useSpecifiedFrame(defaultFrame);
// }
//
// destroy(): void {
// this.renderer.deleteMoving(this);
// }
// }
+248 -98
View File
@@ -22,7 +22,7 @@ import {
IMapRenderResult,
IMapVertexGenerator,
IMapViewportController,
IMovingBlock,
IOverlayBlock,
MapBackgroundRepeat,
MapTileAlign,
MapTileBehavior,
@@ -37,14 +37,13 @@ import {
} from '@user/data-base';
import { TileType } from '@user/data-common';
import { IHookController, logger } from '@motajs/common';
import { compileProgramWith } from '@motajs/client-base';
import { compileProgramWith } from '@motajs/render';
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,
@@ -53,7 +52,7 @@ import {
} from '../shared';
import { MapViewport } from './viewport';
import { INSTANCED_COUNT } from './constant';
import { StaticBlockStatus } from './status';
import { OverlayBlock, StaticBlockStatus } from './status';
const enum BackgroundType {
Static,
@@ -61,9 +60,7 @@ const enum BackgroundType {
Tile
}
export class MapRenderer
implements IMapRenderer, IMovingRenderer, IMapDataGetter
{
export class MapRenderer implements IMapRenderer, IMapDataGetter {
//#region 实例属性
/** 自动元件处理器 */
@@ -151,13 +148,13 @@ export class MapRenderer
private needUpdateOffsetPool: boolean = true;
/** 所有正在移动的图块 */
private movingBlock: Set<IMovingBlock> = new Set();
private overlayBlocks: Set<IOverlayBlock> = new Set();
/** 移动图块对象索引池 */
private movingIndexPool: number[] = [];
private overlayIndexPool: number[] = [];
/** 移动图块对象数量 */
private movingCount: number = DYNAMIC_RESERVE;
private overlayCount: number = DYNAMIC_RESERVE;
/** 移动索引映射 */
private movingIndexMap: Map<number, IMovingBlock> = new Map();
private overlayIndexMap: Map<number, IOverlayBlock> = new Map();
/** 移动容忍度,如果正在移动的图块数量长期小于预留数量的一半,那么将减少移动数组长度 */
private lastExpandTime = 0;
@@ -214,8 +211,8 @@ export class MapRenderer
* @param manager 素材管理器
*/
constructor(readonly manager: ITextureManager) {
this.movingIndexPool.push(
...Array.from({ length: this.movingCount }, (_, i) => i).reverse()
this.overlayIndexPool.push(
...Array.from({ length: this.overlayCount }, (_, i) => i).reverse()
);
this.canvas = document.createElement('canvas');
this.gl = this.canvas.getContext('webgl2')!;
@@ -359,7 +356,12 @@ export class MapRenderer
this.canvas.height = height;
// 更新 FBO 的纹理尺寸信息
const gl = this.gl;
const { pingTexture2D, pongTexture2D } = this.contextData;
const {
pingTexture2D,
pongTexture2D,
pingDepthBuffer,
pongDepthBuffer
} = this.contextData;
gl.bindTexture(gl.TEXTURE_2D, pingTexture2D);
gl.texImage2D(
gl.TEXTURE_2D,
@@ -385,6 +387,22 @@ export class MapRenderer
null
);
gl.bindTexture(gl.TEXTURE_2D, null);
// 深度缓冲要和颜色纹理保持同样的尺寸
gl.bindRenderbuffer(gl.RENDERBUFFER, pingDepthBuffer);
gl.renderbufferStorage(
gl.RENDERBUFFER,
gl.DEPTH_COMPONENT24,
width,
height
);
gl.bindRenderbuffer(gl.RENDERBUFFER, pongDepthBuffer);
gl.renderbufferStorage(
gl.RENDERBUFFER,
gl.DEPTH_COMPONENT24,
width,
height
);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
this.updateRequired = true;
}
@@ -722,6 +740,7 @@ export class MapRenderer
const vertexBuffer = gl.createBuffer();
const instancedBuffer = gl.createBuffer();
const backVertexBuffer = gl.createBuffer();
const overlayBuffer = gl.createBuffer();
const tileTexture = gl.createTexture();
const backgroundTexture = gl.createTexture();
@@ -734,28 +753,11 @@ export class MapRenderer
const pongFramebuffer = gl.createFramebuffer();
const pingTexture2D = gl.createTexture();
const pongTexture2D = gl.createTexture();
const pingDepthBuffer = gl.createRenderbuffer();
const pongDepthBuffer = gl.createRenderbuffer();
// 初始化 Post effect FBO 和 Texture
gl.bindTexture(gl.TEXTURE_2D, pongTexture2D);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
this.canvas.width,
this.canvas.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null
);
gl.bindFramebuffer(gl.FRAMEBUFFER, pingFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
pongTexture2D,
0
);
// ping 与 pong 各自持有颜色纹理,后处理链交替读写以避免反馈环
gl.bindTexture(gl.TEXTURE_2D, pingTexture2D);
gl.texImage2D(
gl.TEXTURE_2D,
@@ -768,7 +770,47 @@ export class MapRenderer
gl.UNSIGNED_BYTE,
null
);
gl.bindFramebuffer(gl.FRAMEBUFFER, pongFramebuffer);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// 非 2 的幂尺寸下必须夹取到边缘,否则采样会越界变成黑色
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, pongTexture2D);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
this.canvas.width,
this.canvas.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null);
// 深度附件让场景在离屏渲染时依然保留深度测试
gl.bindRenderbuffer(gl.RENDERBUFFER, pingDepthBuffer);
gl.renderbufferStorage(
gl.RENDERBUFFER,
gl.DEPTH_COMPONENT24,
this.canvas.width,
this.canvas.height
);
gl.bindRenderbuffer(gl.RENDERBUFFER, pongDepthBuffer);
gl.renderbufferStorage(
gl.RENDERBUFFER,
gl.DEPTH_COMPONENT24,
this.canvas.width,
this.canvas.height
);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, pingFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
@@ -776,6 +818,26 @@ export class MapRenderer
pingTexture2D,
0
);
gl.framebufferRenderbuffer(
gl.FRAMEBUFFER,
gl.DEPTH_ATTACHMENT,
gl.RENDERBUFFER,
pingDepthBuffer
);
gl.bindFramebuffer(gl.FRAMEBUFFER, pongFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
pongTexture2D,
0
);
gl.framebufferRenderbuffer(
gl.FRAMEBUFFER,
gl.DEPTH_ATTACHMENT,
gl.RENDERBUFFER,
pongDepthBuffer
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
@@ -793,7 +855,14 @@ export class MapRenderer
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.depthFunc(gl.LESS);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// 分离式混合让 alpha 通道按 ONE / ONE_MINUS_SRC_ALPHA 累积,
// 避免多次叠加后 alpha 被重复乘衰
gl.blendFuncSeparate(
gl.SRC_ALPHA,
gl.ONE_MINUS_SRC_ALPHA,
gl.ONE,
gl.ONE_MINUS_SRC_ALPHA
);
const data: IContextData = {
tileProgram: tileProgram.program,
@@ -805,6 +874,7 @@ export class MapRenderer
vertexBuffer,
instancedBuffer,
backgroundVertexBuffer: backVertexBuffer,
overlayBuffer,
offsetPoolLocation: poolLocation,
nowFrameLocation: frameLocation,
tileSamplerLocation: tileSampler,
@@ -828,6 +898,8 @@ export class MapRenderer
pongFramebuffer,
pingTexture2D,
pongTexture2D,
pingDepthBuffer,
pongDepthBuffer,
tileTextureWidth: 4096,
tileTextureHeight: 4096,
@@ -849,6 +921,7 @@ export class MapRenderer
gl.deleteBuffer(data.vertexBuffer);
gl.deleteBuffer(data.instancedBuffer);
gl.deleteBuffer(data.backgroundVertexBuffer);
gl.deleteBuffer(data.overlayBuffer);
gl.deleteProgram(data.tileProgram);
gl.deleteProgram(data.backProgram);
gl.deleteShader(data.tileVertShader);
@@ -857,6 +930,12 @@ export class MapRenderer
gl.deleteShader(data.backFragShader);
gl.deleteTexture(data.tileTexture);
gl.deleteTexture(data.backgroundTexture);
gl.deleteTexture(data.pingTexture2D);
gl.deleteTexture(data.pongTexture2D);
gl.deleteFramebuffer(data.pingFramebuffer);
gl.deleteFramebuffer(data.pongFramebuffer);
gl.deleteRenderbuffer(data.pingDepthBuffer);
gl.deleteRenderbuffer(data.pongDepthBuffer);
gl.deleteVertexArray(data.tileVAO);
gl.deleteVertexArray(data.backVAO);
}
@@ -1378,6 +1457,7 @@ export class MapRenderer
tileProgram,
tileTexture,
instancedBuffer,
overlayBuffer,
offsetPoolLocation,
nowFrameLocation,
tileTransformLocation,
@@ -1433,9 +1513,13 @@ export class MapRenderer
this.viewportHeight
);
// 不透明遍必须写深度,覆盖层的遮挡完全依赖这一次写入
gl.depthMask(true);
// 有启用的效果时先把场景画进 ping,效果需要读取整幅画面才能处理
const postEffects = this.postEffects.filter(v => v.enabled);
if (postEffects.length > 1) {
if (postEffects.length > 0) {
gl.bindFramebuffer(gl.FRAMEBUFFER, pingFramebuffer);
} else {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
@@ -1479,8 +1563,13 @@ export class MapRenderer
// 由于 WebGL2 没有 glDrawArraysInstancedBaseInstance,只能每次渲染的时候临时修改 VBO 读取方式
const stride = INSTANCED_COUNT * 4;
const dynamicStart = this.vertex.dynamicStart;
gl.bindBuffer(gl.ARRAY_BUFFER, instancedBuffer);
area.render.forEach(v => {
// 动态图块改由覆盖层绘制,这里把动态尾部裁掉避免重复绘制
const end = Math.min(v.endIndex, dynamicStart);
const count = end - v.startIndex;
if (count <= 0) return;
const s = v.startIndex * INSTANCED_COUNT;
const o1 = s + 0;
const o2 = o1 + 4 * 4;
@@ -1490,30 +1579,63 @@ export class MapRenderer
gl.vertexAttribPointer(texCoord, 4, gl.FLOAT, false, stride, o2);
gl.vertexAttribPointer(tileData, 4, gl.FLOAT, false, stride, o3);
gl.vertexAttribPointer(texData, 4, gl.FLOAT, false, stride, o4);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, v.count);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
});
// 覆盖层最后画,它依赖不透明遍已经写入的深度做遮挡
let overlayCount = this.vertex.getOverlayCount();
if (this.vertex.isOverlayDirty()) {
// 顶点数组已在上传前 flush,这里只把结果排序并组装成暂存数组
const overlayArray = this.vertex.getOverlayArray();
overlayCount = this.vertex.getOverlayCount();
if (overlayCount > 0) {
// 只上传已组装的实例数量,暂存数组尾部可能还留着上一次的残留
gl.bindBuffer(gl.ARRAY_BUFFER, overlayBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
overlayArray.subarray(0, overlayCount * INSTANCED_COUNT),
gl.DYNAMIC_DRAW
);
}
}
// 没有覆盖层条目时跳过整段绘制,避免无意义的缓冲区绑定
if (overlayCount > 0) {
// 关闭深度写入:覆盖层彼此靠提交顺序混合,写深度会裁掉后提交的同深度面
gl.depthMask(false);
gl.bindBuffer(gl.ARRAY_BUFFER, overlayBuffer);
// WebGL2 没有 baseInstance,每次绘制都要重设四个实例属性指针
gl.vertexAttribPointer(tilePos, 4, gl.FLOAT, false, stride, 0);
gl.vertexAttribPointer(texCoord, 4, gl.FLOAT, false, stride, 4 * 4);
gl.vertexAttribPointer(tileData, 4, gl.FLOAT, false, stride, 8 * 4);
gl.vertexAttribPointer(texData, 4, gl.FLOAT, false, stride, 12 * 4);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, overlayCount);
// 恢复深度写入,避免污染下一帧的不透明遍与后处理链
gl.depthMask(true);
}
gl.bindVertexArray(null);
gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
// Post effects
let inputTextrue = pongTexture2D;
let outputFBO: WebGLFramebuffer | null = pingFramebuffer;
// 场景已经渲染进 ping 帧缓冲,因此第一个效果读取 ping 的颜色纹理
let inputTextrue = pingTexture2D;
let outputFBO: WebGLFramebuffer | null = pongFramebuffer;
postEffects.forEach((v, i, a) => {
if (i === a.length - 1) {
// 最后一个效果直接写默认帧缓冲上屏,不再多一次拷贝
v.render(gl, inputTextrue, null, data);
return;
}
// 读写必须落在不同帧缓冲上,否则会形成读自己正在写的纹理的反馈环
v.render(gl, inputTextrue, outputFBO, data);
if (inputTextrue === pongTexture2D) {
if (outputFBO === pingFramebuffer) {
inputTextrue = pingTexture2D;
outputFBO = pongFramebuffer;
} else {
inputTextrue = pongTexture2D;
}
if (i === a.length - 2) {
outputFBO = null;
} else {
if (outputFBO === pingFramebuffer) {
outputFBO = pongFramebuffer;
} else {
outputFBO = pingFramebuffer;
}
outputFBO = pingFramebuffer;
}
});
@@ -1581,11 +1703,11 @@ export class MapRenderer
/**
* 扩大移动数组
*/
private expandMoving() {
const start = this.movingCount;
this.movingCount *= 2;
this.vertex.expandMoving(this.movingCount);
this.movingIndexPool.push(
private expandOverlay() {
const start = this.overlayCount;
this.overlayCount *= 2;
this.vertex.expandOverlay(this.overlayCount);
this.overlayIndexPool.push(
// 翻转数组是因为这样的话内容会优先取到低索引的内容,更容易优化
...Array.from({ length: start }, (_, i) => i + start).reverse()
);
@@ -1594,73 +1716,110 @@ export class MapRenderer
/**
* 减小移动数组
*/
private reduceMoving() {
const half = Math.round(this.movingCount / 2);
private reduceOverlay() {
const half = Math.round(this.overlayCount / 2);
if (half < DYNAMIC_RESERVE) return;
for (const moving of this.movingBlock) {
for (const moving of this.overlayBlocks) {
if (moving.index >= half) return;
}
this.vertex.reduceMoving(half);
this.vertex.reduceOverlay(half);
}
/**
* 申请移动索引
* @returns 移动索引
*/
private requireMovingIndex(): number {
if (this.movingIndexPool.length === 0) {
this.expandMoving();
private requireOverlayIndex(): number {
if (this.overlayIndexPool.length === 0) {
this.expandOverlay();
}
const half = Math.max(
Math.round(this.movingCount / 2),
Math.round(this.overlayCount / 2),
DYNAMIC_RESERVE
);
if (this.movingIndexPool.length < half) {
if (this.overlayIndexPool.length < half) {
this.lastExpandTime = this.timestamp;
}
return this.movingIndexPool.pop()!;
return this.overlayIndexPool.pop()!;
}
/**
* 退回移动索引
* @param index 退回的索引
*/
private returnMovingIndex(index: number) {
this.movingIndexPool.push(index);
private returnOverlayIndex(index: number) {
this.overlayIndexPool.push(index);
}
addMovingBlock(
addOverlayBlock(
layer: IMapLayer,
block: number | IMaterialFramedData,
x: number,
y: number
): IMovingBlock {
const index = this.requireMovingIndex();
const moving = new MovingBlock(this, index, layer, block);
moving.setPos(x, y);
this.movingBlock.add(moving);
this.movingIndexMap.set(index, moving);
this.vertex.updateMoving(moving, true);
return moving;
): IOverlayBlock {
let texture: IMaterialFramedData;
let tile: number;
if (typeof block === 'number') {
texture = this.manager.getTile(block)!;
tile = block;
} else {
if (!this.manager.assetContainsTexture(block.texture)) {
logger.error(34);
}
if (this.getOffsetIndex(block.offset) === -1) {
logger.error(41);
}
texture = block;
tile = -1;
}
const overlay = this.createOverlayBlock(layer, 1, texture, tile);
overlay.setPos(x, y);
this.vertex.updateOverlay(overlay, true);
this.requestUpdate();
return overlay;
}
getMovingBlock(): Set<IMovingBlock> {
return this.movingBlock;
createOverlayBlock(
layer: IMapLayer,
priority: number,
texture: IMaterialFramedData,
tile: number
): IOverlayBlock {
const index = this.requireOverlayIndex();
const block = new OverlayBlock(
layer,
priority,
index,
texture,
tile,
this
);
this.overlayBlocks.add(block);
this.overlayIndexMap.set(index, block);
return block;
}
getMovingBlockByIndex(index: number): IMovingBlock | null {
return this.movingIndexMap.get(index) ?? null;
getOverlayBlocks(): Set<IOverlayBlock> {
return this.overlayBlocks;
}
deleteMoving(block: IMovingBlock): void {
this.returnMovingIndex(block.index);
this.movingBlock.delete(block);
this.movingIndexMap.delete(block.index);
this.vertex.deleteMoving(block);
getOverlayBlockByIndex(index: number): IOverlayBlock | null {
return this.overlayIndexMap.get(index) ?? null;
}
hasMoving(moving: IMovingBlock): boolean {
return this.movingBlock.has(moving);
deleteOverlayBlock(block: IOverlayBlock): void {
if (!this.overlayBlocks.has(block)) return;
this.returnOverlayIndex(block.index);
this.overlayBlocks.delete(block);
this.overlayIndexMap.delete(block.index);
this.vertex.deleteOverlay(block);
this.requestUpdate();
}
deleteOverlayBlockByIndex(index: number): void {
const block = this.overlayIndexMap.get(index);
if (!block) return;
this.deleteOverlayBlock(block);
}
//#endregion
@@ -1677,7 +1836,7 @@ export class MapRenderer
// 移动数组
const expandDT = timestamp - this.lastExpandTime;
if (expandDT > MOVING_TOLERANCE * 1000) {
this.reduceMoving();
this.reduceOverlay();
this.lastExpandTime = timestamp;
}
@@ -1699,16 +1858,6 @@ export class MapRenderer
}
this.tickers.forEach(v => void v.fn(timestamp));
// 图块移动
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);
}
}
requestTicker(fn: (timestamp: number) => void): IMapRendererTicker {
@@ -1736,6 +1885,7 @@ export class MapRenderer
this.needUpdateBackgroundFrame ||
this.needUpdateTransform ||
this.vertex.dynamicRenderDirty ||
this.vertex.isOverlayDirty() ||
this.needUpdateOffsetPool
);
}
@@ -11,7 +11,8 @@ uniform sampler2DArray u_sampler;
void main() {
vec4 texColor = texture(u_sampler, v_texCoord.xyz);
float alpha = texColor.a * v_texCoord.a;
// todo: 透明像素应该如何解决??
if (alpha < 0.1) discard;
// 低于阈值的透明像素直接 discard 来避免写深度区
// 这里主要是为了剔除非透明素材的透明像素,如果素材有半透明像素需要单独声明为半透明素材
if (alpha < 0.004) discard;
outColor = vec4(texColor.rgb, alpha);
}
+72 -17
View File
@@ -1,5 +1,13 @@
import { IMapLayer } from '@user/data-base';
import { IBlockStatus, IMapVertexStatus } from './types';
import {
IBlockStatus,
IMapRenderer,
IMapVertexStatus,
IOverlayBlock
} from './types';
import { IMaterialFramedData } from '../material';
//#region 图块状态
export class StaticBlockStatus implements IBlockStatus {
/**
@@ -16,7 +24,7 @@ export class StaticBlockStatus implements IBlockStatus {
) {}
setAlpha(alpha: number): void {
this.vertex.setStaticAlpha(this.layer, alpha, this.x, this.y);
this.vertex.setStaticAlpha(this.layer, this.x, this.y, alpha);
}
getAlpha(): number {
@@ -36,35 +44,82 @@ export class StaticBlockStatus implements IBlockStatus {
}
}
export class DynamicBlockStatus implements IBlockStatus {
/**
* @param layer 图层对象
* @param vertex 顶点数组生成器对象
* @param index 图块索引
*/
//#endregion
//#region 覆盖层图块
export class OverlayBlock implements IOverlayBlock {
readonly layer: IMapLayer;
readonly priority: number;
readonly index: number;
readonly renderer: IMapRenderer;
texture: IMaterialFramedData;
readonly tile: number;
x: number = 0;
y: number = 0;
constructor(
readonly layer: IMapLayer,
readonly vertex: IMapVertexStatus,
readonly index: number
) {}
layer: IMapLayer,
priority: number,
index: number,
texture: IMaterialFramedData,
tile: number,
renderer: IMapRenderer
) {
this.layer = layer;
this.priority = priority;
this.index = index;
this.texture = texture;
this.tile = tile;
this.renderer = renderer;
}
setPos(x: number, y: number): void {
this.x = x;
this.y = y;
// TODO: 通知更新
this.renderer.vertex.updateOverlay(this, false);
}
setTexture(texture: IMaterialFramedData): void {
if (texture === this.texture) return;
this.texture = texture;
this.renderer.vertex.updateOverlay(this, true);
}
setAlpha(alpha: number): void {
this.vertex.setDynamicAlpha(this.index, alpha);
this.renderer.vertex.setDynamicAlpha(this.index, alpha);
}
getAlpha(): number {
return this.vertex.getDynamicAlpha(this.index);
return this.renderer.vertex.getDynamicAlpha(this.index);
}
useGlobalFrame(): void {
this.vertex.setDynamicFrame(this.index, -1);
this.renderer.vertex.setDynamicFrame(this.index, -1);
}
useSpecifiedFrame(frame: number): void {
this.vertex.setDynamicFrame(this.index, frame);
this.renderer.vertex.setDynamicFrame(this.index, frame);
}
getFrame(): number {
return this.vertex.getDynamicFrame(this.index);
return this.renderer.vertex.getDynamicFrame(this.index);
}
useDefaultFrame(): void {
if (!this.renderer.manager.getTile(this.tile)) return;
const defaultFrame = this.renderer.manager.getDefaultFrame(this.tile);
this.useSpecifiedFrame(defaultFrame);
}
destroy(): void {
this.renderer.deleteOverlayBlock(this);
}
toStatic(layer: IMapLayer, x: number, y: number): void {
this.renderer.vertex.toStatic(this, layer, x, y);
}
}
//#endregion
+201 -125
View File
@@ -1,4 +1,4 @@
import { IDirtyMark, IDirtyTracker } from '@motajs/common';
import { IDirtyMark, IDirtyTracker, ITileLocator } from '@motajs/common';
import {
ITextureRenderable,
SizedCanvasImageSource,
@@ -11,7 +11,13 @@ import {
ITrackedAssetData
} from '@user/client-base';
import { IGameMap, IMapLayer } from '@user/data-base';
import { TimingFn } from 'mutate-animate';
import {
ExcitationCurve,
ExcitationCurve2D,
IExcitable
} from '@motajs/animate';
//#region 枚举值
export const enum MapBackgroundRepeat {
/** 直接重复 */
@@ -45,6 +51,10 @@ export const enum MapTileAlign {
End
}
//#endregion
//#region 渲染配置
export interface IMapBackgroundConfig {
/** 是否使用图片大小作为背景图渲染大小,如果是 `false`,则使用 `renderWidth` `renderHeight` 作为渲染大小 */
readonly useImageSize: boolean;
@@ -122,6 +132,8 @@ export interface IContextData {
readonly instancedBuffer: WebGLBuffer;
/** 背景顶点数组 */
readonly backgroundVertexBuffer: WebGLBuffer;
/** 覆盖层实例化数据数组 */
readonly overlayBuffer: WebGLBuffer;
/** 图块纹理对象 */
readonly tileTexture: WebGLTexture;
/** 背景纹理对象 */
@@ -139,6 +151,10 @@ export interface IContextData {
readonly pingTexture2D: WebGLTexture;
/** 第二个 texture2D */
readonly pongTexture2D: WebGLTexture;
/** 第一个 framebuffer 的深度缓冲 */
readonly pingDepthBuffer: WebGLRenderbuffer;
/** 第二个 framebuffer 的深度缓冲 */
readonly pongDepthBuffer: WebGLRenderbuffer;
/** 当前画布的图块纹理宽度 */
tileTextureWidth: number;
@@ -159,12 +175,15 @@ export interface IContextData {
vertexMark: IDirtyMark;
}
//#endregion
export interface IBlockStatus {
/** 图块所属图层 */
readonly layer: IMapLayer;
/**
* 设置图块的不透明度
* 设置图块的不透明度。静态图块设置小于 1 的不透明度会自动提升为覆盖层图块,
* 覆盖层图块设置为 1 不会降级回静态层。
* @param alpha 图块不透明度
*/
setAlpha(alpha: number): void;
@@ -191,20 +210,31 @@ export interface IBlockStatus {
getFrame(): number;
}
export interface IMovingBlock extends IBlockStatus {
/** 移动图块的索引 */
export interface IOverlayBlock extends IBlockStatus {
/** 所属图层 */
readonly layer: IMapLayer;
/** 同一排序位置时的绘制优先级,数值小的先绘制,显示在优先级更高者的下面 */
readonly priority: number;
/** 覆盖层实例数组索引,由渲染器分配并保证唯一 */
readonly index: number;
/** 图块数字 */
readonly tile: number;
/** 图块横坐标 */
/** 横坐标,可为小数 */
readonly x: number;
/** 图块纵坐标 */
/** 纵坐标,可为小数 */
readonly y: number;
/** 图块使用的纹理 */
readonly texture: IMaterialFramedData;
/** 所属的地图渲染器 */
readonly renderer: IMapRenderer;
/**
* 直接设置图块的位置,动画中设置无效
* 设置图块的不透明度。覆盖层图块设置为 1 时仍留在覆盖层,
* 不会自动降级回静态层。
* @param alpha 目标不透明度
*/
setAlpha(alpha: number): void;
/**
* 直接设置图块的位置,可以填写小数
* @param x 目标横坐标
* @param y 目标纵坐标
*/
@@ -216,6 +246,33 @@ export interface IMovingBlock extends IBlockStatus {
*/
setTexture(texture: IMaterialFramedData): void;
/**
* 使用图块默认帧数(如果图块存在的话)
*/
useDefaultFrame(): void;
/**
* 摧毁这个移动图块对象,之后不会再显示到画面上
*/
destroy(): void;
/**
* 把覆盖层实例数据写回目标静态图块,重算静态纵坐标与位置,
* 删除当前覆盖层图块并释放索引,图块数据由调用方维护。
* @param layer 目标图块所属的图层
* @param x 目标图块横坐标
* @param y 目标图块纵坐标
*/
toStatic(layer: IMapLayer, x: number, y: number): void;
}
export interface IRenderBlockMover extends IExcitable<number> {
/**
* 绑定要移动的覆盖层图块
* @param block 要移动的覆盖层图块
*/
bind(block: IOverlayBlock): void;
/**
* 沿直线移动到目标点
* @param x 目标横坐标,可以填小数
@@ -226,7 +283,7 @@ export interface IMovingBlock extends IBlockStatus {
x: number,
y: number,
time: number,
timing?: TimingFn
timing?: ExcitationCurve
): Promise<this>;
/**
@@ -234,7 +291,11 @@ export interface IMovingBlock extends IBlockStatus {
* @param curve 移动曲线,接收完成度作为参数,输出图块应该在的位置
* @param timing 移动的速率曲线,默认为匀速移动
*/
moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise<this>;
moveAs(
curve: ExcitationCurve2D,
time: number,
timing?: ExcitationCurve
): Promise<this>;
/**
* 按照指定曲线移动,使用相对模式,即 `curve` 输出值与原始坐标相加得到当前位置
@@ -242,42 +303,16 @@ export interface IMovingBlock extends IBlockStatus {
* @param timing 移动的速率曲线,默认为匀速移动
*/
moveRelative(
curve: TimingFn<2>,
curve: ExcitationCurve2D,
time: number,
timing?: TimingFn
timing?: ExcitationCurve
): Promise<this>;
/**
* 进行一步动画移动效果
* @param timestamp 当前时间戳
* @returns 图块是否发生了移动
* 立刻结束当前移动
* @param keep 图块保持在当前移动的位置(true)还是瞬移到目标位置(false),默认为后者
*/
stepMoving(timestamp: number): boolean;
/**
* 立刻停止移动
*/
endMoving(): void;
/**
* 使用图块默认帧数(如果图块存在的话)
*/
useDefaultFrame(): void;
/**
* 摧毁这个移动图块对象,之后不会再显示到画面上
*/
destroy(): void;
}
export interface IMapRendererTicker {
/** 当前的时间戳 */
readonly timestamp: number;
/**
* 移除这个帧函数
*/
remove(): void;
endMoving(keep?: boolean): void;
}
export interface IMapRendererPostEffect {
@@ -524,36 +559,55 @@ export interface IMapRenderer {
setCellSize(width: number, height: number): void;
/**
* 添加一个移动图块
* 添加一个覆盖层图块
* @param layer 图块所属的图层
* @param block 图块数字或图块的素材对象,要求可渲染对象的图像源必须出现在图集中
* @param x 图块初始横坐标,可以填小数
* @param y 图块初始纵坐标,可以填小数
* @returns 移动图块对象,可以用于控制图块移动
*/
addMovingBlock(
addOverlayBlock(
layer: IMapLayer,
block: number | IMaterialFramedData,
x: number,
y: number
): Readonly<IMovingBlock>;
): IOverlayBlock;
/**
* 获取所有的移动图块
* 获取所有的覆盖层图块
*/
getMovingBlock(): Set<Readonly<IMovingBlock>>;
getOverlayBlocks(): Set<IOverlayBlock>;
/**
* 根据索引获取移动图块对象
* @param index 移动图块索引
* 根据索引获取覆盖层图块对象
* @param index 覆盖层图块索引
*/
getMovingBlockByIndex(index: number): Readonly<IMovingBlock> | null;
getOverlayBlockByIndex(index: number): IOverlayBlock | null;
/**
* 进行一帧更新
* @param timestamp 时间戳
* 删除指定的覆盖层图块
* @param block 覆盖层图块对象
*/
tick(timestamp: number): void;
deleteOverlayBlock(block: IOverlayBlock): void;
/**
* 根据索引删除覆盖层图块
* @param index 覆盖层图块索引
*/
deleteOverlayBlockByIndex(index: number): void;
/**
* 分配覆盖层索引、创建并登记覆盖层图块,不写实例数据
* @param layer 图块所属的图层
* @param priority 绘制优先级,数值小的先绘制
* @param texture 图块使用的纹理
* @param tile 图块数字,-1 表示不是由图块数字创建
*/
createOverlayBlock(
layer: IMapLayer,
priority: number,
texture: IMaterialFramedData,
tile: number
): IOverlayBlock;
/**
* 获取指定图层的指定图块的状态信息,可以设置与获取图块状态。多次调用的返回值不同引用。
@@ -567,12 +621,6 @@ export interface IMapRenderer {
* 当前地图状态是否发生改变,需要更新
*/
needUpdate(): boolean;
/**
* 添加一个每帧执行的函数
* @param fn 每帧执行的函数
*/
requestTicker(fn: (timestamp: number) => void): IMapRendererTicker;
}
export interface IMapVertexArray {
@@ -591,236 +639,236 @@ export interface IMapVertexArray {
readonly dynamicCount: number;
}
export interface IBlockIndex {
export interface IChunkIndex {
/** 横坐标 */
readonly x: number;
/** 纵坐标 */
readonly y: number;
/** 分块左上角数据的横坐标 */
readonly dataX: number;
readonly elX: number;
/** 分块左上角数据的纵坐标 */
readonly dataY: number;
readonly elY: number;
/** 索引,等于 横坐标+纵坐标*宽度 */
readonly index: number;
}
export interface IBlockInfo extends IBlockIndex {
export interface IChunkInfo extends IChunkIndex {
/** 分块宽度 */
readonly width: number;
/** 分块高度 */
readonly height: number;
}
export interface IBlockData<T> extends IBlockInfo {
export interface IChunkData<T> extends IChunkInfo {
/** 这个分块的数据 */
data: T;
/**
* 获取这个分块左方的分块
*/
left(): IBlockData<T> | null;
left(): IChunkData<T> | null;
/**
* 获取这个分块右方的分块
*/
right(): IBlockData<T> | null;
right(): IChunkData<T> | null;
/**
* 获取这个分块上方的分块
*/
up(): IBlockData<T> | null;
up(): IChunkData<T> | null;
/**
* 获取这个分块下方的分块
*/
down(): IBlockData<T> | null;
down(): IChunkData<T> | null;
/**
* 获取这个分块左上方的分块
*/
leftUp(): IBlockData<T> | null;
leftUp(): IChunkData<T> | null;
/**
* 获取这个分块左下方的分块
*/
leftDown(): IBlockData<T> | null;
leftDown(): IChunkData<T> | null;
/**
* 获取这个分块右上方的分块
*/
rightUp(): IBlockData<T> | null;
rightUp(): IChunkData<T> | null;
/**
* 获取这个分块右下方的分块
*/
rightDown(): IBlockData<T> | null;
rightDown(): IChunkData<T> | null;
/**
* 获取下一个索引的分块
*/
next(): IBlockData<T> | null;
next(): IChunkData<T> | null;
}
export interface IBlockSplitterConfig {
export interface IChunkSplitterConfig {
/** 分块宽度 */
readonly blockWidth: number;
readonly chunkWidth: number;
/** 分块高度 */
readonly blockHeight: number;
/** 数据宽度 */
readonly dataWidth: number;
/** 数据高度 */
readonly dataHeight: number;
readonly chunkHeight: number;
/** 数据的总宽度 */
readonly elWidth: number;
/** 数据的总高度 */
readonly elHeight: number;
}
export interface IBlockSplitter<T> extends IBlockSplitterConfig {
export interface IBlockSplitter<T> extends IChunkSplitterConfig {
/** 宽度,即横向有多少分块 */
readonly width: number;
/** 高度,即纵向有多少分块 */
readonly height: number;
/**
* 根据坐标获取分块内容
* 根据坐标获取分块内容(分块 -> 分块)
* @param x 横坐标
* @param y 纵坐标
*/
getBlockByLoc(x: number, y: number): IBlockData<T> | null;
getChunkByLoc(x: number, y: number): IChunkData<T> | null;
/**
* 根据分块索引获取分块内容
* 根据分块索引获取分块内容(分块 -> 分块)
* @param index 分块索引
*/
getBlockByIndex(index: number): IBlockData<T> | null;
getChunkByIndex(index: number): IChunkData<T> | null;
/**
* 根据分块坐标设置分块内容
* 根据分块坐标设置分块内容(分块 -> 分块)
* @param data 分块数据
* @param x 分块横坐标
* @param y 分块纵坐标
* @returns 分块内容
*/
setBlockByLoc(data: T, x: number, y: number): IBlockData<T> | null;
setChunkByLoc(data: T, x: number, y: number): IChunkData<T> | null;
/**
* 根据分块索引设置分块内容
* 根据分块索引设置分块内容(分块 -> 分块)
* @param data 分块数据
* @param index 分块索引
* @returns 分块内容
*/
setBlockByIndex(data: T, index: number): IBlockData<T> | null;
setChunkByIndex(data: T, index: number): IChunkData<T> | null;
/**
* 根据分块坐标,遍历指定分块中的所有坐标(分块 -> 元素)
* @param x 分块横坐标
* @param y 分块纵坐标
*/
iterateBlockByLoc(x: number, y: number): Generator<IBlockIndex, void>;
iterateElementsOfChunk(x: number, y: number): Generator<IChunkIndex, void>;
/**
* 根据分块索引,遍历指定分块中的所有坐标(分块 -> 元素)
* @param index 分块索引
*/
iterateBlockByIndex(index: number): Generator<IBlockIndex, void>;
iterateElementsOfChunkIndex(index: number): Generator<IChunkIndex, void>;
/**
* 根据分块索引列表,依次遍历每个分块的所有坐标(分块 -> 元素)
* @param indices 分块索引列表
*/
iterateBlockByIndices(
iterateElementsOfChunks(
indices: Iterable<number>
): Generator<IBlockIndex, void>;
): Generator<IChunkIndex, void>;
/**
* 遍历所有的分块(分块 -> 分块)
*/
iterateBlocks(): Iterable<IBlockData<T>>;
iterateChunks(): Iterable<IChunkData<T>>;
/**
* 传入指定的数据区域,对其所涉及的分块依次遍历(元素 -> 分块)
* @param x 数据左上角横坐标
* @param y 数据左上角纵坐标
* 传入指定的元素区域,对其所涉及的分块依次遍历(元素 -> 分块)
* @param x 元素左上角横坐标
* @param y 元素左上角纵坐标
* @param width 数据宽度
* @param height 数据高度
*/
iterateBlocksOfDataArea(
iterateChunksOfElementArea(
x: number,
y: number,
width: number,
height: number
): Generator<IBlockData<T>>;
): Generator<IChunkData<T>>;
/**
* 根据分块坐标获取分块索引,如果坐标超出范围,返回 -1(分块 -> 分块)
* @param x 分块横坐标
* @param y 分块纵坐标
*/
getIndexByLoc(x: number, y: number): number;
getChunkIndexByLoc(x: number, y: number): number;
/**
* 根据分块索引获取分块坐标,如果索引超出范围,返回 `null`(分块 -> 分块)
* @param index 分块索引
*/
getLocByIndex(index: number): Loc | null;
getChunkLocByIndex(index: number): ITileLocator | null;
/**
* 根据坐标列表,获取对应的分块索引,并组成一个列表(分块 -> 分块)
* @param list 坐标列表
*/
getIndicesByLocList(list: Iterable<Loc>): Iterable<number>;
getChunkIndicesByLocList(list: Iterable<ITileLocator>): Iterable<number>;
/**
* 根据索引列表,获取对应的分块坐标,并组成一个列表(分块 -> 分块)
* @param list 索引列表
*/
getLocListByIndices(list: Iterable<number>): Iterable<Loc | null>;
getChunkLocByIndices(list: Iterable<number>): Iterable<ITileLocator | null>;
/**
* 根据数据坐标获取其对应分块的信息(元素 -> 分块)
* 根据元素坐标获取其对应分块的信息(元素 -> 分块)
* @param x 数据横坐标
* @param y 数据纵坐标
*/
getBlockByDataLoc(x: number, y: number): IBlockData<T> | null;
getChunkByElementLoc(x: number, y: number): IChunkData<T> | null;
/**
* 根据数据索引获取其对应分块的信息(元素 -> 分块)
* 根据元素索引获取其对应分块的信息(元素 -> 分块)
* @param index 数据索引
*/
getBlockByDataIndex(index: number): IBlockData<T> | null;
getChunkByElementIndex(index: number): IChunkData<T> | null;
/**
* 根据数据的坐标列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块)
* 根据元素的坐标列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块)
* @param list 数据坐标列表
*/
getIndicesByDataLocList(list: Iterable<Loc>): Set<number>;
/**
* 根据数据的索引列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块)
* 根据元素的索引列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块)
* @param list 数据索引列表
*/
getIndicesByDataIndices(list: Iterable<number>): Set<number>;
getChunkIndicesByElementIndices(list: Iterable<number>): Set<number>;
/**
* 根据数据的坐标列表,获取数据所在的分块内容,并组成一个集合(元素 -> 分块)
* @param list 数据坐标列表
*/
getBlocksByDataLocList(list: Iterable<Loc>): Set<IBlockData<T>>;
getChunksByElementLocList(list: Iterable<Loc>): Set<IChunkData<T>>;
/**
* 根据数据的索引列表,获取数据所在的分块内容,并组成一个集合(元素 -> 分块)
* @param list 数据索引列表
*/
getBlocksByDataIndices(list: Iterable<number>): Set<IBlockData<T>>;
getChunksByElementIndices(list: Iterable<number>): Set<IChunkData<T>>;
/**
* 配置此分块切分器,此行为不会清空分块数据,只有在执行下一次分块时才会生效
* @param config 切分器配置
*/
configSplitter(config: IBlockSplitterConfig): void;
configSplitter(config: IChunkSplitterConfig): void;
/**
* 执行分块操作,对每个分块执行函数,获取分块数据
* @param mapFn 对每个分块执行的函数
*/
splitBlocks(mapFn: (block: IBlockInfo) => T): void;
splitChunks(mapFn: (block: IChunkInfo) => T): void;
}
export interface IMapVertexData {
@@ -1032,13 +1080,13 @@ export interface IMapVertexGenerator
* 扩大移动图块数组尺寸
* @param targetSize 目标大小
*/
expandMoving(targetSize: number): void;
expandOverlay(targetSize: number): void;
/**
* 缩小移动图块数组尺寸
* @param targetSize 目标大小
*/
reduceMoving(targetSize: number): void;
reduceOverlay(targetSize: number): void;
/**
* 更新图层数组
@@ -1055,6 +1103,22 @@ export interface IMapVertexGenerator
*/
getVertexArray(): IMapVertexArray;
/**
* 覆盖层是否需要重新排序或重新组装实例数据
*/
isOverlayDirty(): boolean;
/**
* 获取覆盖层的实例数据数组,是对内部存储的直接引用,仅在脏时重新组装
*/
getOverlayArray(): Float32Array;
/**
* 获取覆盖层当前的实例数量,顺序或值脏时需先调用 `getOverlayArray()` 完成组装,
* 否则返回的是上一次组装的数量
*/
getOverlayCount(): number;
/**
* 更新指定区域内的所有图块
* @param layer 更新的图层
@@ -1091,27 +1155,41 @@ export interface IMapVertexGenerator
* 更新指定分块的数据,专门用于懒更新
* @param block 要更新的分块
*/
updateBlockCache(block: Readonly<IBlockData<IMapVertexBlock>>): void;
updateBlockCache(block: Readonly<IChunkData<IMapVertexBlock>>): void;
/**
* 更新指定的移动图块对象,可以用于新增移动图块
* @param moving 要更新的移动图块对象
* @param updateTexture 是否更新贴图信息
*/
updateMoving(moving: IMovingBlock, updateTexture: boolean): void;
updateOverlay(moving: IOverlayBlock, updateTexture: boolean): void;
/**
* 更新一系列移动图块,可以用于新增移动图块
* @param moving 移动图块列表
* @param updateTexture 是否更新贴图信息
*/
updateMovingList(moving: IMovingBlock[], updateTexture: boolean): void;
updateOverlayList(moving: IOverlayBlock[], updateTexture: boolean): void;
/**
* 移除指定的移动图块
* @param moving 移动图块对象
*/
deleteMoving(moving: IMovingBlock): void;
deleteOverlay(moving: IOverlayBlock): void;
/**
* 低层写回助手,把覆盖层槽位写入目标静态实例,重算 z 与位置并释放覆盖块
* @param block 要写回的覆盖层图块
* @param layer 目标图块所属的图层
* @param x 目标图块横坐标
* @param y 目标图块纵坐标
*/
toStatic(
block: IOverlayBlock,
layer: IMapLayer,
x: number,
y: number
): void;
}
export interface IMapRenderArea {
@@ -1129,7 +1207,7 @@ export interface IMapRenderData {
/** 需要更新顶点数组的区域 */
readonly dirty: IMapRenderArea[];
/** 需要更新的分块列表 */
readonly blockList: IBlockData<IMapVertexBlock>[];
readonly blockList: IChunkData<IMapVertexBlock>[];
}
export interface IMapViewportController {
@@ -1147,5 +1225,3 @@ export interface IMapViewportController {
*/
bindTransform(transform: Transform): void;
}
export interface IMapCamera {}
+449 -80
View File
@@ -1,7 +1,7 @@
import { IMapLayer } from '@user/data-base';
import { TileType } from '@user/data-common';
import {
IBlockData,
IChunkData,
IBlockSplitter,
IContextData,
IIndexedMapVertexData,
@@ -12,18 +12,14 @@ import {
IMapVertexBlock,
IMapVertexData,
IMapVertexGenerator,
IMovingBlock,
IOverlayBlock,
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 { DYNAMIC_RESERVE, MAP_BLOCK_HEIGHT, MAP_BLOCK_WIDTH } from '../shared';
import { BlockSplitter } from './chunk';
import { clamp, isNil } from 'lodash-es';
import { IMaterialFramedData } from '@user/client-base';
import { IRect } from '@motajs/render';
@@ -41,12 +37,6 @@ export interface IMapDataGetter {
/** 图块大小与格子大小判断方式 */
readonly tileTestMode: MapTileSizeTestMode;
/**
* 渲染器是否包含指定的移动图块对象
* @param moving 移动图块对象
*/
hasMoving(moving: IMovingBlock): boolean;
/**
* 申请更新渲染
*/
@@ -84,7 +74,7 @@ interface VertexArrayOfBlock {
/** 分块顶点数组 */
readonly array: Float32Array;
/** 分块数据 */
readonly block: IBlockData<MapVertexBlock>;
readonly block: IChunkData<MapVertexBlock>;
}
const enum VertexUpdate {
@@ -132,6 +122,22 @@ export class MapVertexGenerator
/** 动态内容偏移数组 */
private dynamicInstancedArray: Float32Array = new Float32Array();
/** 已提升为覆盖层的静态图块,键为图层对象与地图索引 */
private readonly promotedBlocks: Map<
IMapLayer,
Map<number, IOverlayBlock>
> = new Map();
/** 覆盖层条目的成员或排序键是否变化,为真时需要重新排序 */
private overlayOrderDirty: boolean = false;
/** 覆盖层实例数据是否失效,为真时需要重新组装 */
private overlayValueDirty: boolean = false;
/** 覆盖层条目的排序缓存,每次排序时整体重建 */
private overlayOrdered: IOverlayBlock[] = [];
/** 覆盖层实例数据的暂存数组 */
private overlayInstancedArray: Float32Array = new Float32Array();
/** 覆盖层当前的实例数量 */
private overlayCount: number = 0;
/** 分块宽度 */
private blockWidth: number = MAP_BLOCK_WIDTH;
/** 分块高度 */
@@ -185,17 +191,17 @@ export class MapVertexGenerator
private splitBlock() {
this.block.configSplitter({
dataWidth: this.mapWidth,
dataHeight: this.mapHeight,
blockWidth: this.blockWidth,
blockHeight: this.blockHeight
elWidth: this.mapWidth,
elHeight: this.mapHeight,
chunkWidth: this.blockWidth,
chunkHeight: 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 => {
this.block.splitChunks(block => {
// 最后一行的算法与其他行不同
const startIndex =
block.height < this.blockHeight
@@ -223,6 +229,11 @@ export class MapVertexGenerator
this.blockHeight = height;
this.mallocVertexArray();
this.splitBlock();
this.promotedBlocks.clear();
this.overlayOrdered.length = 0;
this.overlayCount = 0;
this.overlayOrderDirty = true;
this.overlayValueDirty = true;
}
resizeMap(): void {
@@ -236,7 +247,7 @@ export class MapVertexGenerator
}
}
expandMoving(targetSize: number): void {
expandOverlay(targetSize: number): void {
const beforeOffset = this.instancedArray;
this.dynamicCount = targetSize;
this.mallocVertexArray();
@@ -245,12 +256,12 @@ export class MapVertexGenerator
instancedArray: this.instancedArray
};
// 重建一下对应分块就行了,不需要重新分块
for (const block of this.block.iterateBlocks()) {
for (const block of this.block.iterateChunks()) {
block.data.rebuild(array);
}
}
reduceMoving(targetSize: number): void {
reduceOverlay(targetSize: number): void {
const beforeOffsetLength = this.instancedArray.length;
const deltaLength = this.dynamicCount - targetSize;
this.dynamicCount = targetSize;
@@ -274,9 +285,198 @@ export class MapVertexGenerator
this.needRebuild = false;
this.mallocVertexArray();
this.splitBlock();
// 重建会重新分配实例数组并把不透明度复位为 1,旧的覆盖层成员全部失效
this.promotedBlocks.clear();
this.overlayOrdered.length = 0;
this.overlayCount = 0;
this.overlayOrderDirty = true;
this.overlayValueDirty = true;
this.dirty();
}
/**
* 获取指定图层指定坐标的图块对应的分块信息
* @param layer 图层对象
* @param x 图块横坐标
* @param y 图块纵坐标
*/
private getIndexInBlock(
layer: IMapLayer,
x: number,
y: number
): VertexArrayOfBlock | null {
const block = this.block.getChunkByElementLoc(x, y);
if (!block) return null;
const data = block.data.getLayerInstanced(layer);
if (!data) return null;
const dx = x - block.elX;
const dy = y - block.elY;
const dIndex = dy * block.width + dx;
return { array: data, index: dIndex, block };
}
//#endregion
//#region 覆盖层
/**
* 获取指定图层指定地图索引的提升块,未登记时返回 `null`
* @param layer 图块所在的图层
* @param mapIndex 图块在地图中的索引
*/
private getPromotedBlock(
layer: IMapLayer,
mapIndex: number
): IOverlayBlock | null {
const blocks = this.promotedBlocks.get(layer);
if (!blocks) return null;
return blocks.get(mapIndex) ?? null;
}
/**
* 登记指定图层指定地图索引的提升块
* @param layer 图块所在的图层
* @param mapIndex 图块在地图中的索引
* @param block 提升后的覆盖层图块
*/
private setPromotedBlock(
layer: IMapLayer,
mapIndex: number,
block: IOverlayBlock
): void {
const blocks = this.promotedBlocks.getOrInsertComputed(
layer,
() => new Map()
);
blocks.set(mapIndex, block);
}
/**
* 删除指定图层指定地图索引的提升登记,内层清空后一并移除
* @param layer 图块所在的图层
* @param mapIndex 图块在地图中的索引
*/
private deletePromotedBlock(layer: IMapLayer, mapIndex: number): void {
const blocks = this.promotedBlocks.get(layer);
if (!blocks) return;
blocks.delete(mapIndex);
if (blocks.size === 0) this.promotedBlocks.delete(layer);
}
/**
* 按引用查找并删除提升登记,找不到时不做任何事
* @param block 要删除的覆盖层图块
*/
private deletePromotedBlockByValue(block: IOverlayBlock): void {
for (const [layer, blocks] of this.promotedBlocks) {
for (const [mapIndex, value] of blocks) {
if (value === block) {
blocks.delete(mapIndex);
if (blocks.size === 0) this.promotedBlocks.delete(layer);
return;
}
}
}
}
/**
* 按 `(layerIndex, y, x, kind)` 重建覆盖层的绘制顺序。
* 动态成员每次都从活动移动图块现取,不保留登记表,因此已销毁的块不会残留,
* 图层顺序变化后也不会继续使用旧的 `layerIndex`。
*/
private sortOverlay(): void {
const ordered = [...this.renderer.getOverlayBlocks()];
// 静态与动态的坐标可能完全相同,最后用种类保证静态先于动态,
// x 只是 CPU 侧的稳定次级键,绝不参与 z / 深度的计算
ordered.sort(
(a, b) =>
this.renderer.getLayerIndex(a.layer) -
this.renderer.getLayerIndex(b.layer) ||
a.y - b.y ||
a.x - b.x ||
a.priority - b.priority
);
this.overlayOrdered = ordered;
this.overlayOrderDirty = false;
// 顺序或成员变化意味着暂存数组与数量都已过期,必须让下一次取数组重新组装
this.overlayValueDirty = true;
}
/**
* 按覆盖层的缓存顺序组装实例数据,组装完成后清除脏标记。
* 静态条目只从分块图层数组拷贝那 16 个浮点并覆盖前四个浮点(进入覆盖层前的矩形),
* 从不修改分块张量本身。
*/
private assembleOverlay(): void {
const ordered = this.overlayOrdered;
const required = ordered.length * INSTANCED_COUNT;
if (this.overlayInstancedArray.length < required) {
this.overlayInstancedArray = new Float32Array(
Math.max(this.overlayInstancedArray.length * 2, required)
);
}
const target = this.overlayInstancedArray;
let count = 0;
ordered.forEach(block => {
const offset = count * INSTANCED_COUNT;
const source = block.index * INSTANCED_COUNT;
target.set(
this.dynamicInstancedArray.subarray(
source,
source + INSTANCED_COUNT
),
offset
);
count++;
});
this.overlayCount = count;
this.overlayValueDirty = false;
this.overlayOrderDirty = false;
}
/**
* 把覆盖层槽位写回目标静态实例,按目标格重算位置与纵深,
* 最后删除覆盖层图块以释放索引。
* @param block 要写回的覆盖层图块
* @param layer 目标图块所属的图层
* @param x 目标图块横坐标
* @param y 目标图块纵坐标
*/
toStatic(
block: IOverlayBlock,
layer: IMapLayer,
x: number,
y: number
): void {
const index = this.getIndexInBlock(layer, x, y);
if (!index) return;
const source = block.index * INSTANCED_COUNT;
const target = index.index * INSTANCED_COUNT;
index.array.set(
this.dynamicInstancedArray.subarray(
source,
source + INSTANCED_COUNT
),
target
);
// 目标格与覆盖块原坐标的差换算成归一化位移,横坐标加、纵坐标减
const dx =
((x - block.x) * this.renderer.cellWidth * 2) /
this.renderer.renderWidth;
const dy =
((y - block.y) * this.renderer.cellHeight * 2) /
this.renderer.renderHeight;
index.array[target] += dx;
index.array[target + 1] -= dy;
const layerIndex = this.renderer.getLayerIndex(layer);
const layerStart = (layerIndex / this.renderer.layerCount) * 1.8 - 0.9;
const perBlockZ = 1 / this.mapHeight / this.renderer.layerCount;
const blockZ = y * perBlockZ;
index.array[target + 8] = -layerStart - blockZ;
index.block.data.markRenderDirty();
this.renderer.deleteOverlayBlock(block);
}
//#endregion
//#region 顶点数组更新
@@ -380,6 +580,7 @@ export class MapVertexGenerator
* @param offsetIndex 贴图偏移值所在偏移池的索引
* @param frames 贴图总帧数
* @param update 顶点坐标更新方式
* @param dynamic 是否是动态图块
*/
private updateTileVertex(
vertex: IMapVertexData,
@@ -396,22 +597,50 @@ export class MapVertexGenerator
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 perBlockZ = 1 / this.mapHeight / layerCount;
const blockZ = index.mapY * perBlockZ;
const zIndex = -layerStart - blockZ - (dynamic ? perBlockZ : 0);
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 (dynamic) {
// 动态图块一定写
const layerIndex = this.renderer.getLayerIndex(index.layer);
// 避免 z 坐标是 1 的时候被裁剪,因此范围选择 [-0.9, 0.9]
const layerStart = (layerIndex / layerCount) * 1.8 - 0.9;
const perBlockZ = 1 / this.mapHeight / layerCount;
const blockZ = index.mapY * perBlockZ;
const zIndex = -layerStart - blockZ - (dynamic ? perBlockZ : 0);
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;
} else {
// 如果不在,那么写数据
const layerIndex = this.renderer.getLayerIndex(index.layer);
// 避免 z 坐标是 1 的时候被裁剪,因此范围选择 [-0.9, 0.9]
const layerStart = (layerIndex / layerCount) * 1.8 - 0.9;
const perBlockZ = 1 / this.mapHeight / layerCount;
const blockZ = index.mapY * perBlockZ;
const zIndex = -layerStart - blockZ - (dynamic ? perBlockZ : 0);
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;
@@ -434,6 +663,44 @@ export class MapVertexGenerator
);
instancedArray[startIndex + 12] = defaultFrame;
}
if (!dynamic) {
const mapIndex = index.mapY * this.mapWidth + index.mapX;
const promoted = this.getPromotedBlock(index.layer, mapIndex);
if (promoted) {
const dy = this.dynamicInstancedArray;
const offset = promoted.index * INSTANCED_COUNT;
if (update & VertexUpdate.Position) {
dy[offset] = instancedArray[startIndex];
dy[offset + 1] = instancedArray[startIndex + 1];
dy[offset + 2] = instancedArray[startIndex + 2];
dy[offset + 3] = instancedArray[startIndex + 3];
dy[offset + 8] = instancedArray[startIndex + 8];
instancedArray[startIndex] = 0;
instancedArray[startIndex + 1] = 0;
instancedArray[startIndex + 2] = 0;
instancedArray[startIndex + 3] = 0;
}
if (update & VertexUpdate.Texture) {
dy[offset + 4] = instancedArray[startIndex + 4];
dy[offset + 5] = instancedArray[startIndex + 5];
dy[offset + 6] = instancedArray[startIndex + 6];
dy[offset + 7] = instancedArray[startIndex + 7];
dy[offset + 13] = instancedArray[startIndex + 13];
dy[offset + 14] = instancedArray[startIndex + 14];
dy[offset + 15] = instancedArray[startIndex + 15];
}
if (update & VertexUpdate.Frame) {
dy[offset + 12] = instancedArray[startIndex + 12];
}
const chunk = this.block.getChunkByElementLoc(
index.mapX,
index.mapY
);
if (chunk) chunk.data.markRenderDirty();
this.overlayValueDirty = true;
this.dynamicRenderDirty = true;
}
}
}
/**
@@ -496,12 +763,12 @@ export class MapVertexGenerator
) {
const mx = index.mapX + dx;
const my = index.mapY + dy;
const block = this.block.getBlockByDataLoc(mx, my);
const block = this.block.getChunkByElementLoc(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 bx = mx - block.elX;
const by = my - block.elY;
const mapIndex = my * this.mapWidth + mx;
const num = mapArray[mapIndex];
const newIndex: BlockIndex = {
@@ -534,6 +801,7 @@ export class MapVertexGenerator
* @param vertex 顶点数组对象
* @param index 图块索引对象
* @param num 图块数字
* @param dynamic 是否是动态图块
*/
private updateVertexArray(
mapArray: Uint32Array,
@@ -555,6 +823,15 @@ export class MapVertexGenerator
instancedArray[instancedStart + 1] = 0;
instancedArray[instancedStart + 2] = 0;
instancedArray[instancedStart + 3] = 0;
if (!dynamic) {
const promoted = this.getPromotedBlock(
index.layer,
index.mapIndex
);
if (promoted) {
this.renderer.deleteOverlayBlock(promoted);
}
}
return;
}
@@ -615,14 +892,14 @@ export class MapVertexGenerator
x: number,
y: number
) {
const block = this.block.getBlockByDataLoc(x, y);
const block = this.block.getChunkByElementLoc(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 dx = x - block.elX;
const dy = y - block.elY;
const dIndex = dy * block.width + dx;
const index: BlockIndex = {
layer,
@@ -685,16 +962,18 @@ export class MapVertexGenerator
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);
const blocks = this.block.iterateChunksOfElementArea(
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
);
const left = ax - block.elX;
const top = ay - block.elY;
const right = Math.min(areaRight - block.elX, left + block.width);
const bottom = Math.min(areaBottom - block.elY, top + block.height);
block.data.markDirty(layer, left, top, right, bottom);
block.data.markRenderDirty();
}
@@ -724,10 +1003,10 @@ export class MapVertexGenerator
// 对于超出50个的更新操作使用懒更新
blocks.forEach(v => {
const block = this.block.getBlockByDataLoc(v.x, v.y);
const block = this.block.getChunkByElementLoc(v.x, v.y);
if (!block) return;
const bx = v.x - block.dataX;
const by = v.y - block.dataY;
const bx = v.x - block.elX;
const by = v.y - block.elY;
block.data.markDirty(layer, bx - 1, by - 1, bx + 2, by + 2);
block.data.markRenderDirty();
const left = bx === 0;
@@ -810,7 +1089,7 @@ export class MapVertexGenerator
});
}
updateBlockCache(block: Readonly<IBlockData<IMapVertexBlock>>): void {
updateBlockCache(block: Readonly<IChunkData<IMapVertexBlock>>): void {
if (!block.data.dirty) return;
const layers = this.renderer.getSortedLayer();
layers.forEach(layer => {
@@ -824,8 +1103,8 @@ export class MapVertexGenerator
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 mapX = nx + block.elX;
const mapY = ny + block.elY;
const mapIndex = mapY * this.mapWidth + mapX;
const num = array[mapIndex];
const index: BlockIndex = {
@@ -848,15 +1127,15 @@ export class MapVertexGenerator
//#region 动态图块
updateMoving(block: IMovingBlock, updateTexture: boolean): void {
if (!this.renderer.hasMoving(block)) return;
updateOverlay(block: IOverlayBlock, updateTexture: boolean): void {
if (!this.renderer.getOverlayBlocks().has(block)) return;
const { tileType, frames, offset, texture } = block.texture;
const vertex: IMapVertexData = {
instancedArray: this.dynamicInstancedArray
};
const index: IndexedBlockMapPos = {
layer: block.layer,
num: block.tile,
num: -1,
mapX: block.x,
mapY: block.y,
blockIndex: block.index
@@ -864,7 +1143,7 @@ export class MapVertexGenerator
const assetIndex = this.renderer.getAssetSourceIndex(texture.source);
const offsetIndex = this.renderer.getOffsetIndex(offset);
if (assetIndex === -1 || offsetIndex === -1) {
logger.error(40, block.tile.toString());
logger.error(40, block.index.toString());
return;
}
const update = updateTexture
@@ -911,21 +1190,29 @@ export class MapVertexGenerator
}
this.dynamicRenderDirty = true;
// 位置与图层都是排序键,动态值也可能变化,因此两种脏都要标记
this.overlayOrderDirty = true;
this.overlayValueDirty = true;
}
updateMovingList(moving: IMovingBlock[], updateTexture: boolean): void {
updateOverlayList(moving: IOverlayBlock[], updateTexture: boolean): void {
moving.forEach(v => {
this.updateMoving(v, updateTexture);
this.updateOverlay(v, updateTexture);
});
}
deleteMoving(moving: IMovingBlock): void {
const instancedStart = moving.index * INSTANCED_COUNT;
deleteOverlay(block: IOverlayBlock): void {
// 删除改变了成员集合,只置顺序脏会在排序时连同顺序脏一起清掉而跳过组装,
// 暂存数组里就会留下已删除动态块的残影,因此两个脏标记都要置
this.overlayOrderDirty = true;
this.overlayValueDirty = true;
const instancedStart = block.index * INSTANCED_COUNT;
// 这个需要全部清空了,因为可能会复用
this.dynamicInstancedArray.set(
MapVertexGenerator.EMPTY_VETREX,
instancedStart
);
this.deletePromotedBlockByValue(block);
this.dynamicRenderDirty = true;
}
@@ -934,27 +1221,55 @@ export class MapVertexGenerator
//#region 图块状态
/**
* 获取指定图层指定坐标的图块对应的分块信息
* @param layer 图层对象
* 为静态图块申请覆盖层索引,把静态实例的数据整体拷入覆盖层槽位,
* 清零静态矩形并登记提升关系。
* @param layer 图块所属的图层
* @param x 图块横坐标
* @param y 图块纵坐标
* @param mapIndex 图块在地图中的索引
* @param alpha 覆盖层的不透明度
*/
private getIndexInBlock(
private promoteStaticBlock(
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 };
y: number,
mapIndex: number,
alpha: number
): void {
const index = this.getIndexInBlock(layer, x, y);
if (!index) return;
const num = layer.getMapRef().array[mapIndex];
const tile = this.renderer.manager.getTile(num);
if (!tile) return;
const block = this.renderer.createOverlayBlock(layer, 0, tile, num);
block.setPos(x, y);
const source = index.index * INSTANCED_COUNT;
const target = block.index * INSTANCED_COUNT;
this.dynamicInstancedArray.set(
index.array.subarray(source, source + INSTANCED_COUNT),
target
);
this.dynamicInstancedArray[target + 9] = alpha;
index.array[source] = 0;
index.array[source + 1] = 0;
index.array[source + 2] = 0;
index.array[source + 3] = 0;
index.block.data.markRenderDirty();
this.setPromotedBlock(layer, mapIndex, block);
this.dynamicRenderDirty = true;
this.overlayOrderDirty = true;
this.overlayValueDirty = true;
this.renderer.requestUpdate();
}
setStaticAlpha(
/**
* 把不透明度写入静态实例并标记该分块需要重传渲染数据
* @param layer 图块所属的图层
* @param x 图块横坐标
* @param y 图块纵坐标
* @param alpha 目标不透明度
*/
private writeStaticAlpha(
layer: IMapLayer,
x: number,
y: number,
@@ -966,25 +1281,61 @@ export class MapVertexGenerator
index.block.data.markRenderDirty();
}
setStaticAlpha(
layer: IMapLayer,
x: number,
y: number,
alpha: number
): void {
const mapIndex = y * this.mapWidth + x;
const promoted = this.getPromotedBlock(layer, mapIndex);
if (promoted) {
promoted.setAlpha(alpha);
return;
}
if (alpha < 1) {
this.promoteStaticBlock(layer, x, y, mapIndex, alpha);
} else if (alpha >= 1) {
this.writeStaticAlpha(layer, x, y, alpha);
}
}
setStaticFrame(
layer: IMapLayer,
x: number,
y: number,
frame: number
): void {
const mapIndex = y * this.mapWidth + x;
const promoted = this.getPromotedBlock(layer, mapIndex);
if (promoted) {
this.setDynamicFrame(promoted.index, frame);
return;
}
const index = this.getIndexInBlock(layer, x, y);
if (!index) return;
index.array[index.index * INSTANCED_COUNT + 12] = frame;
this.overlayValueDirty = true;
index.block.data.markRenderDirty();
}
getStaticAlpha(layer: IMapLayer, x: number, y: number): number {
const mapIndex = y * this.mapWidth + x;
const promoted = this.getPromotedBlock(layer, mapIndex);
if (promoted) {
return this.getDynamicAlpha(promoted.index);
}
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 mapIndex = y * this.mapWidth + x;
const promoted = this.getPromotedBlock(layer, mapIndex);
if (promoted) {
return this.getDynamicFrame(promoted.index);
}
const index = this.getIndexInBlock(layer, x, y);
if (!index) return -1;
return index.array[index.index * INSTANCED_COUNT + 12];
@@ -992,11 +1343,13 @@ export class MapVertexGenerator
setDynamicAlpha(index: number, alpha: number): void {
this.dynamicInstancedArray[index * INSTANCED_COUNT + 9] = alpha;
this.overlayValueDirty = true;
this.dynamicRenderDirty = true;
}
setDynamicFrame(index: number, frame: number): void {
this.dynamicInstancedArray[index * INSTANCED_COUNT + 12] = frame;
this.overlayValueDirty = true;
this.dynamicRenderDirty = true;
}
@@ -1018,6 +1371,22 @@ export class MapVertexGenerator
this.dynamicRenderDirty = false;
}
isOverlayDirty(): boolean {
return this.overlayOrderDirty || this.overlayValueDirty;
}
getOverlayArray(): Float32Array {
if (this.overlayOrderDirty) this.sortOverlay();
if (this.overlayValueDirty || this.overlayOrderDirty) {
this.assembleOverlay();
}
return this.overlayInstancedArray;
}
getOverlayCount(): number {
return this.overlayCount;
}
getVertexArray(): IMapVertexArray {
this.checkRebuild();
return {
+15 -10
View File
@@ -1,6 +1,6 @@
import { Transform } from '@motajs/render';
import {
IBlockData,
IChunkData,
IMapRenderArea,
IMapRenderData,
IMapRenderer,
@@ -21,8 +21,8 @@ export class MapViewport implements IMapViewportController {
private pushBlock(
list: IMapRenderArea[],
start: IBlockData<IMapVertexBlock>,
end: IBlockData<IMapVertexBlock>
start: IChunkData<IMapVertexBlock>,
end: IChunkData<IMapVertexBlock>
) {
const startIndex = start.data.startIndex;
const endIndex = end.data.endIndex;
@@ -54,7 +54,12 @@ export class MapViewport implements IMapViewportController {
getRenderArea(): IMapRenderData {
const { cellWidth, cellHeight, renderWidth, renderHeight } =
this.renderer;
const { blockWidth, blockHeight, width, height } = this.vertex.block;
const {
chunkWidth: blockWidth,
chunkHeight: blockHeight,
width,
height
} = this.vertex.block;
// 其实只需要算左上角和右下角就行了
const [left, top] = this.transform.untransformed(-1, -1);
const [right, bottom] = this.transform.untransformed(1, 1);
@@ -69,12 +74,12 @@ export class MapViewport implements IMapViewportController {
const renderArea: IMapRenderArea[] = [];
const updateArea: IMapRenderArea[] = [];
const blockList: IBlockData<IMapVertexBlock>[] = [];
const blockList: IChunkData<IMapVertexBlock>[] = [];
// 内层横向外层纵向的话,索引在换行之前都是连续的,方便整合
for (let ny = blockTop; ny <= blockBottom; ny++) {
for (let nx = blockLeft; nx <= blockRight; nx++) {
const block = this.vertex.block.getBlockByLoc(nx, ny)!;
const block = this.vertex.block.getChunkByLoc(nx, ny)!;
blockList.push(block);
}
}
@@ -88,10 +93,10 @@ export class MapViewport implements IMapViewportController {
this.pushBlock(renderArea, block, block);
} else {
// 更新区域
let updateStart: IBlockData<IMapVertexBlock> = blockList[0];
let updateEnd: IBlockData<IMapVertexBlock> = blockList[0];
let renderStart: IBlockData<IMapVertexBlock> = blockList[0];
let renderEnd: IBlockData<IMapVertexBlock> = blockList[0];
let updateStart: IChunkData<IMapVertexBlock> = blockList[0];
let updateEnd: IChunkData<IMapVertexBlock> = blockList[0];
let renderStart: IChunkData<IMapVertexBlock> = blockList[0];
let renderEnd: IChunkData<IMapVertexBlock> = blockList[0];
for (let i = 1; i < blockList.length; i++) {
const block = blockList[i];
const { renderDirty } = block.data;
+1 -3
View File
@@ -1,5 +1,3 @@
import { ElementLocator, Font } from '@motajs/render';
//#region 地图
/** 每个格子的默认宽度,现阶段用处不大 */
@@ -20,7 +18,7 @@ export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT;
* 动态内容预留,不明白含义的话不要动。地图上所有正在移动的图块称为动态内容,这些内容的数量无法预测,因此需要预留数组大小,
* 如果不够再临时扩充。如果你的塔中有大量的移动操作,可以适当提高此值,避免频繁的内存扩充行为,可以一定程度上提高性能表现。
*/
export const DYNAMIC_RESERVE = 16;
export const DYNAMIC_RESERVE = 64;
/**
* 移动图块容忍度,不明白含义的话不要动。如果移动图块的数量长期小于当前预留数量,那么将会降低预留数量,提升性能表现。
* 调整此值可以调整频率,值越大,越不容易因为数量小于预留数量而减小预留。
@@ -60,6 +60,11 @@ export interface ITileRawData {
* 当为 `true` 时,玩家会通过走上去触发图块的触发器。类似于旧样板的 `noPass`
*/
readonly eventPass: boolean;
/**
* 图块是否包含半透明像素,对于只有完全不透明和完全透明像素的素材,此项为 `false`,否则应为 `true`。
* 不要将不满足条件的素材设为 `true`,这会大幅拖慢地图渲染速度。
*/
readonly translucence: boolean;
}
export interface ITileStore {