diff --git a/packages-user/client-base/src/index.ts b/packages-user/client-base/src/index.ts index 1f11563..3904bbe 100644 --- a/packages-user/client-base/src/index.ts +++ b/packages-user/client-base/src/index.ts @@ -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'; diff --git a/packages-user/client-base/src/map/__test__/rendererGate.test.ts b/packages-user/client-base/src/map/__test__/rendererGate.test.ts new file mode 100644 index 0000000..d426b86 --- /dev/null +++ b/packages-user/client-base/src/map/__test__/rendererGate.test.ts @@ -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 { + const init = source.slice( + source.indexOf('private initContext()'), + source.indexOf('destroy(): void') + ); + const result = new Map(); + 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 { + 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'); + 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'); + }); +}); diff --git a/packages-user/client-base/src/map/block.ts b/packages-user/client-base/src/map/chunk.ts similarity index 55% rename from packages-user/client-base/src/map/block.ts rename to packages-user/client-base/src/map/chunk.ts index 629099e..783e7b9 100644 --- a/packages-user/client-base/src/map/block.ts +++ b/packages-user/client-base/src/map/chunk.ts @@ -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 implements IBlockSplitter { - 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> = new Map(); + readonly blockMap: Map> = new Map(); /** 数据宽度配置 */ private splitDataWidth: number = 0; @@ -37,17 +37,17 @@ export class BlockSplitter implements IBlockSplitter { return x >= 0 && y >= 0 && x < this.width && y < this.height; } - getBlockByLoc(x: number, y: number): IBlockData | null { + getChunkByLoc(x: number, y: number): IChunkData | null { if (!this.checkLocRange(x, y)) return null; const index = y * this.width + x; return this.blockMap.get(index) ?? null; } - getBlockByIndex(index: number): IBlockData | null { + getChunkByIndex(index: number): IChunkData | null { return this.blockMap.get(index) ?? null; } - setBlockByLoc(data: T, x: number, y: number): IBlockData | null { + setChunkByLoc(data: T, x: number, y: number): IChunkData | 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 implements IBlockSplitter { return block; } - setBlockByIndex(data: T, index: number): IBlockData | null { + setChunkByIndex(data: T, index: number): IChunkData | null { const block = this.blockMap.get(index); if (!block) return null; block.data = data; return block; } - *iterateBlockByLoc(x: number, y: number): Generator { + *iterateElementsOfChunk( + x: number, + y: number + ): Generator { if (!this.checkLocRange(x, y)) return; const index = y * this.width + x; - yield* this.iterateBlockByIndex(index); + yield* this.iterateElementsOfChunkIndex(index); } - *iterateBlockByIndex(index: number): Generator { + *iterateElementsOfChunkIndex(index: number): Generator { const block = this.blockMap.get(index); if (!block) return; - const startX = block.x * this.blockWidth; - const startY = block.y * this.blockHeight; + const 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 - ): Generator { + ): Generator { for (const index of indices) { - yield* this.iterateBlockByIndex(index); + yield* this.iterateElementsOfChunkIndex(index); } } - iterateBlocks(): Iterable> { + iterateChunks(): Iterable> { return this.blockMap.values(); } - *iterateBlocksOfDataArea( + *iterateChunksOfElementArea( x: number, y: number, width: number, height: number - ): Generator> { + ): Generator> { const r = this.width - 1; const b = this.height - 1; const rx = x + width; const by = y + height; - const left = clamp(Math.floor(x / this.blockWidth), 0, r); - const top = clamp(Math.floor(y / this.blockHeight), 0, b); - const right = clamp(Math.floor(rx / this.blockWidth), 0, r); - const bottom = clamp(Math.floor(by / this.blockHeight), 0, b); + 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 implements IBlockSplitter { } } - 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 implements IBlockSplitter { }; } - getIndicesByLocList(list: Iterable): Iterable { + getChunkIndicesByLocList(list: Iterable): Iterable { 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): Iterable { + getChunkLocByIndices(list: Iterable): Iterable { 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 | null { - const bx = Math.floor(x / this.blockWidth); - const by = Math.floor(y / this.blockHeight); + getChunkByElementLoc(x: number, y: number): IChunkData | 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 | null { - const x = index % this.dataWidth; - const y = Math.floor(index / this.dataWidth); - return this.getBlockByDataLoc(x, y); + getChunkByElementIndex(index: number): IChunkData | null { + const x = index % this.elWidth; + const y = Math.floor(index / this.elWidth); + return this.getChunkByElementLoc(x, y); } getIndicesByDataLocList(list: Iterable): Set { const res = new Set(); for (const { x, y } of list) { - const bx = Math.floor(x / this.blockWidth); - const by = Math.floor(y / this.blockHeight); + 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): Set { + getChunkIndicesByElementIndices(list: Iterable): Set { const res = new Set(); for (const index of list) { - const x = index % this.dataWidth; - const y = Math.floor(index / this.dataWidth); - const bx = Math.floor(x / this.blockWidth); - const by = Math.floor(y / this.blockHeight); + 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): Set> { - const res = new Set>(); + getChunksByElementLocList(list: Iterable): Set> { + const res = new Set>(); for (const { x, y } of list) { - const bx = Math.floor(x / this.blockWidth); - const by = Math.floor(y / this.blockHeight); + 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 implements IBlockSplitter { return res; } - getBlocksByDataIndices(list: Iterable): Set> { - const res = new Set>(); + getChunksByElementIndices(list: Iterable): Set> { + const res = new Set>(); for (const index of list) { - const x = index % this.dataWidth; - const y = Math.floor(index / this.dataWidth); - const bx = Math.floor(x / this.blockWidth); - const by = Math.floor(y / this.blockHeight); + 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 implements IBlockSplitter { 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 implements IBlockSplitter { 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 implements IBlockSplitter { 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 implements IBlockSplitter { 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 implements IBlockSplitter { } } -class SplittedBlockData implements IBlockData, ISearchable8Dir { +class SplittedBlockData implements IChunkData, 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, - 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 | null { - return this.splitter.getBlockByLoc(this.x - 1, this.y); + left(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x - 1, this.y); } - right(): IBlockData | null { - return this.splitter.getBlockByLoc(this.x + 1, this.y); + right(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x + 1, this.y); } - up(): IBlockData | null { - return this.splitter.getBlockByLoc(this.x, this.y - 1); + up(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x, this.y - 1); } - down(): IBlockData | null { - return this.splitter.getBlockByLoc(this.x, this.y + 1); + down(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x, this.y + 1); } - leftUp(): IBlockData | null { - return this.splitter.getBlockByLoc(this.x - 1, this.y - 1); + leftUp(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x - 1, this.y - 1); } - leftDown(): IBlockData | null { - return this.splitter.getBlockByLoc(this.x - 1, this.y + 1); + leftDown(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x - 1, this.y + 1); } - rightUp(): IBlockData | null { - return this.splitter.getBlockByLoc(this.x + 1, this.y - 1); + rightUp(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x + 1, this.y - 1); } - rightDown(): IBlockData | null { - return this.splitter.getBlockByLoc(this.x + 1, this.y + 1); + rightDown(): IChunkData | null { + return this.splitter.getChunkByLoc(this.x + 1, this.y + 1); } - next(): IBlockData | null { - return this.splitter.getBlockByIndex(this.index + 1); + next(): IChunkData | null { + return this.splitter.getChunkByIndex(this.index + 1); } } diff --git a/packages-user/client-base/src/map/extension/text.ts b/packages-user/client-base/src/map/extension/text.ts index cfad261..6e669dd 100644 --- a/packages-user/client-base/src/map/extension/text.ts +++ b/packages-user/client-base/src/map/extension/text.ts @@ -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 | 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 @@ -80,7 +80,7 @@ export class OnMapTextRenderer } getBlockByIndex(index: number): Readonly | null { - const blockData = this.block.getBlockByDataIndex(index); + const blockData = this.block.getChunkByElementIndex(index); if (blockData) { const map = blockData.data.getAttachedData< Map @@ -157,7 +157,7 @@ export class OnMapTextRenderer * @param blockData 分块数据 */ private getAttachedMap( - blockData: IBlockData + blockData: IChunkData ): Map { const map = blockData.data.getAttachedData>( this.attachSymbol @@ -173,7 +173,7 @@ export class OnMapTextRenderer requireBlockArea(x: number, y: number): Readonly | 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>( this.attachSymbol diff --git a/packages-user/client-base/src/map/index.ts b/packages-user/client-base/src/map/index.ts index a19d767..25c1602 100644 --- a/packages-user/client-base/src/map/index.ts +++ b/packages-user/client-base/src/map/index.ts @@ -1,6 +1,6 @@ export * from './extension'; -export * from './block'; +export * from './chunk'; export * from './constant'; export * from './element'; export * from './moving'; diff --git a/packages-user/client-base/src/map/moving.ts b/packages-user/client-base/src/map/moving.ts index a61aa95..fbf5df3 100644 --- a/packages-user/client-base/src/map/moving.ts +++ b/packages-user/client-base/src/map/moving.ts @@ -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 { - 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 { - 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 { - 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 { +// 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 { +// 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 { +// 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); +// } +// } diff --git a/packages-user/client-base/src/map/renderer.ts b/packages-user/client-base/src/map/renderer.ts index e00e95d..7e6637c 100644 --- a/packages-user/client-base/src/map/renderer.ts +++ b/packages-user/client-base/src/map/renderer.ts @@ -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 = new Set(); + private overlayBlocks: Set = new Set(); /** 移动图块对象索引池 */ - private movingIndexPool: number[] = []; + private overlayIndexPool: number[] = []; /** 移动图块对象数量 */ - private movingCount: number = DYNAMIC_RESERVE; + private overlayCount: number = DYNAMIC_RESERVE; /** 移动索引映射 */ - private movingIndexMap: Map = new Map(); + private overlayIndexMap: Map = 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 { - 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 { + 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 ); } diff --git a/packages-user/client-base/src/map/shader/map.frag b/packages-user/client-base/src/map/shader/map.frag index 360bf3e..5d14944 100644 --- a/packages-user/client-base/src/map/shader/map.frag +++ b/packages-user/client-base/src/map/shader/map.frag @@ -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); } diff --git a/packages-user/client-base/src/map/status.ts b/packages-user/client-base/src/map/status.ts index 979abaa..8e21f31 100644 --- a/packages-user/client-base/src/map/status.ts +++ b/packages-user/client-base/src/map/status.ts @@ -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 diff --git a/packages-user/client-base/src/map/types.ts b/packages-user/client-base/src/map/types.ts index cab5924..4f9ad7a 100644 --- a/packages-user/client-base/src/map/types.ts +++ b/packages-user/client-base/src/map/types.ts @@ -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 { + /** + * 绑定要移动的覆盖层图块 + * @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; /** @@ -234,7 +291,11 @@ export interface IMovingBlock extends IBlockStatus { * @param curve 移动曲线,接收完成度作为参数,输出图块应该在的位置 * @param timing 移动的速率曲线,默认为匀速移动 */ - moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise; + moveAs( + curve: ExcitationCurve2D, + time: number, + timing?: ExcitationCurve + ): Promise; /** * 按照指定曲线移动,使用相对模式,即 `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; /** - * 进行一步动画移动效果 - * @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; + ): IOverlayBlock; /** - * 获取所有的移动图块 + * 获取所有的覆盖层图块 */ - getMovingBlock(): Set>; + getOverlayBlocks(): Set; /** - * 根据索引获取移动图块对象 - * @param index 移动图块索引 + * 根据索引获取覆盖层图块对象 + * @param index 覆盖层图块索引 */ - getMovingBlockByIndex(index: number): Readonly | 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 extends IBlockInfo { +export interface IChunkData extends IChunkInfo { /** 这个分块的数据 */ data: T; /** * 获取这个分块左方的分块 */ - left(): IBlockData | null; + left(): IChunkData | null; /** * 获取这个分块右方的分块 */ - right(): IBlockData | null; + right(): IChunkData | null; /** * 获取这个分块上方的分块 */ - up(): IBlockData | null; + up(): IChunkData | null; /** * 获取这个分块下方的分块 */ - down(): IBlockData | null; + down(): IChunkData | null; /** * 获取这个分块左上方的分块 */ - leftUp(): IBlockData | null; + leftUp(): IChunkData | null; /** * 获取这个分块左下方的分块 */ - leftDown(): IBlockData | null; + leftDown(): IChunkData | null; /** * 获取这个分块右上方的分块 */ - rightUp(): IBlockData | null; + rightUp(): IChunkData | null; /** * 获取这个分块右下方的分块 */ - rightDown(): IBlockData | null; + rightDown(): IChunkData | null; /** * 获取下一个索引的分块 */ - next(): IBlockData | null; + next(): IChunkData | 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 extends IBlockSplitterConfig { +export interface IBlockSplitter extends IChunkSplitterConfig { /** 宽度,即横向有多少分块 */ readonly width: number; /** 高度,即纵向有多少分块 */ readonly height: number; /** - * 根据坐标获取分块内容 + * 根据坐标获取分块内容(分块 -> 分块) * @param x 横坐标 * @param y 纵坐标 */ - getBlockByLoc(x: number, y: number): IBlockData | null; + getChunkByLoc(x: number, y: number): IChunkData | null; /** - * 根据分块索引获取分块内容 + * 根据分块索引获取分块内容(分块 -> 分块) * @param index 分块索引 */ - getBlockByIndex(index: number): IBlockData | null; + getChunkByIndex(index: number): IChunkData | null; /** - * 根据分块坐标设置分块内容 + * 根据分块坐标设置分块内容(分块 -> 分块) * @param data 分块数据 * @param x 分块横坐标 * @param y 分块纵坐标 * @returns 分块内容 */ - setBlockByLoc(data: T, x: number, y: number): IBlockData | null; + setChunkByLoc(data: T, x: number, y: number): IChunkData | null; /** - * 根据分块索引设置分块内容 + * 根据分块索引设置分块内容(分块 -> 分块) * @param data 分块数据 * @param index 分块索引 * @returns 分块内容 */ - setBlockByIndex(data: T, index: number): IBlockData | null; + setChunkByIndex(data: T, index: number): IChunkData | null; /** * 根据分块坐标,遍历指定分块中的所有坐标(分块 -> 元素) * @param x 分块横坐标 * @param y 分块纵坐标 */ - iterateBlockByLoc(x: number, y: number): Generator; + iterateElementsOfChunk(x: number, y: number): Generator; /** * 根据分块索引,遍历指定分块中的所有坐标(分块 -> 元素) * @param index 分块索引 */ - iterateBlockByIndex(index: number): Generator; + iterateElementsOfChunkIndex(index: number): Generator; /** * 根据分块索引列表,依次遍历每个分块的所有坐标(分块 -> 元素) * @param indices 分块索引列表 */ - iterateBlockByIndices( + iterateElementsOfChunks( indices: Iterable - ): Generator; + ): Generator; /** * 遍历所有的分块(分块 -> 分块) */ - iterateBlocks(): Iterable>; + iterateChunks(): Iterable>; /** - * 传入指定的数据区域,对其所涉及的分块依次遍历(元素 -> 分块) - * @param x 数据左上角横坐标 - * @param y 数据左上角纵坐标 + * 传入指定的元素区域,对其所涉及的分块依次遍历(元素 -> 分块) + * @param x 元素左上角横坐标 + * @param y 元素左上角纵坐标 * @param width 数据宽度 * @param height 数据高度 */ - iterateBlocksOfDataArea( + iterateChunksOfElementArea( x: number, y: number, width: number, height: number - ): Generator>; + ): Generator>; /** * 根据分块坐标获取分块索引,如果坐标超出范围,返回 -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): Iterable; + getChunkIndicesByLocList(list: Iterable): Iterable; /** * 根据索引列表,获取对应的分块坐标,并组成一个列表(分块 -> 分块) * @param list 索引列表 */ - getLocListByIndices(list: Iterable): Iterable; + getChunkLocByIndices(list: Iterable): Iterable; /** - * 根据数据坐标获取其对应分块的信息(元素 -> 分块) + * 根据元素坐标获取其对应分块的信息(元素 -> 分块) * @param x 数据横坐标 * @param y 数据纵坐标 */ - getBlockByDataLoc(x: number, y: number): IBlockData | null; + getChunkByElementLoc(x: number, y: number): IChunkData | null; /** - * 根据数据索引获取其对应分块的信息(元素 -> 分块) + * 根据元素索引获取其对应分块的信息(元素 -> 分块) * @param index 数据索引 */ - getBlockByDataIndex(index: number): IBlockData | null; + getChunkByElementIndex(index: number): IChunkData | null; /** - * 根据数据的坐标列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块) + * 根据元素的坐标列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块) * @param list 数据坐标列表 */ getIndicesByDataLocList(list: Iterable): Set; /** - * 根据数据的索引列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块) + * 根据元素的索引列表,获取数据所在的分块索引,并组成一个集合(元素 -> 分块) * @param list 数据索引列表 */ - getIndicesByDataIndices(list: Iterable): Set; + getChunkIndicesByElementIndices(list: Iterable): Set; /** * 根据数据的坐标列表,获取数据所在的分块内容,并组成一个集合(元素 -> 分块) * @param list 数据坐标列表 */ - getBlocksByDataLocList(list: Iterable): Set>; + getChunksByElementLocList(list: Iterable): Set>; /** * 根据数据的索引列表,获取数据所在的分块内容,并组成一个集合(元素 -> 分块) * @param list 数据索引列表 */ - getBlocksByDataIndices(list: Iterable): Set>; + getChunksByElementIndices(list: Iterable): Set>; /** * 配置此分块切分器,此行为不会清空分块数据,只有在执行下一次分块时才会生效 * @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>): void; + updateBlockCache(block: Readonly>): 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[]; + readonly blockList: IChunkData[]; } export interface IMapViewportController { @@ -1147,5 +1225,3 @@ export interface IMapViewportController { */ bindTransform(transform: Transform): void; } - -export interface IMapCamera {} diff --git a/packages-user/client-base/src/map/vertex.ts b/packages-user/client-base/src/map/vertex.ts index 68b6cac..7bc1963 100644 --- a/packages-user/client-base/src/map/vertex.ts +++ b/packages-user/client-base/src/map/vertex.ts @@ -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; + readonly block: IChunkData; } const enum VertexUpdate { @@ -132,6 +122,22 @@ export class MapVertexGenerator /** 动态内容偏移数组 */ private dynamicInstancedArray: Float32Array = new Float32Array(); + /** 已提升为覆盖层的静态图块,键为图层对象与地图索引 */ + private readonly promotedBlocks: Map< + IMapLayer, + Map + > = 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>): void { + updateBlockCache(block: Readonly>): 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 { diff --git a/packages-user/client-base/src/map/viewport.ts b/packages-user/client-base/src/map/viewport.ts index 3c8f224..e2a74c5 100644 --- a/packages-user/client-base/src/map/viewport.ts +++ b/packages-user/client-base/src/map/viewport.ts @@ -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, - end: IBlockData + start: IChunkData, + end: IChunkData ) { 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[] = []; + const blockList: IChunkData[] = []; // 内层横向外层纵向的话,索引在换行之前都是连续的,方便整合 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 = blockList[0]; - let updateEnd: IBlockData = blockList[0]; - let renderStart: IBlockData = blockList[0]; - let renderEnd: IBlockData = blockList[0]; + let updateStart: IChunkData = blockList[0]; + let updateEnd: IChunkData = blockList[0]; + let renderStart: IChunkData = blockList[0]; + let renderEnd: IChunkData = blockList[0]; for (let i = 1; i < blockList.length; i++) { const block = blockList[i]; const { renderDirty } = block.data; diff --git a/packages-user/client-base/src/shared.ts b/packages-user/client-base/src/shared.ts index 1c064bc..c2b74bb 100644 --- a/packages-user/client-base/src/shared.ts +++ b/packages-user/client-base/src/shared.ts @@ -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; /** * 移动图块容忍度,不明白含义的话不要动。如果移动图块的数量长期小于当前预留数量,那么将会降低预留数量,提升性能表现。 * 调整此值可以调整频率,值越大,越不容易因为数量小于预留数量而减小预留。 diff --git a/packages-user/data-common/src/store/types.ts b/packages-user/data-common/src/store/types.ts index 82f76b8..b687b32 100644 --- a/packages-user/data-common/src/store/types.ts +++ b/packages-user/data-common/src/store/types.ts @@ -60,6 +60,11 @@ export interface ITileRawData { * 当为 `true` 时,玩家会通过走上去触发图块的触发器。类似于旧样板的 `noPass` */ readonly eventPass: boolean; + /** + * 图块是否包含半透明像素,对于只有完全不透明和完全透明像素的素材,此项为 `false`,否则应为 `true`。 + * 不要将不满足条件的素材设为 `true`,这会大幅拖慢地图渲染速度。 + */ + readonly translucence: boolean; } export interface ITileStore {