mirror of
https://github.com/motajs/template.git
synced 2026-11-16 00:44:43 +08:00
feat: 画家算法排序半透明渲染
This commit is contained in:
1 parent
3c5a997302
commit
67fcf15ea5
14 files changed
+1902
-712
No files matched your search
@@ -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');
|
||||
});
|
||||
});
|
||||
+105
-102
@@ -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,6 +1,6 @@
|
||||
export * from './extension';
|
||||
|
||||
export * from './block';
|
||||
export * from './chunk';
|
||||
export * from './constant';
|
||||
export * from './element';
|
||||
export * from './moving';
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 {}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
Reference in new issue
Block a user