From c2a34ea9f1874ef675e8bccebfc250f2ea4273fa Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 3 Sep 2024 02:48:16 +0300 Subject: [PATCH] fix(preflat-worlds): improve mesher performance by 2x by syncing the code from webgpu branch fixes #191 --- prismarine-viewer/examples/shared.ts | 11 ++ prismarine-viewer/viewer/lib/mesher/mesher.ts | 12 +- prismarine-viewer/viewer/lib/mesher/models.ts | 158 +++++++++++------- prismarine-viewer/viewer/lib/mesher/shared.ts | 26 ++- prismarine-viewer/viewer/lib/mesher/world.ts | 2 + .../viewer/lib/worldrendererCommon.ts | 19 ++- 6 files changed, 164 insertions(+), 64 deletions(-) create mode 100644 prismarine-viewer/examples/shared.ts diff --git a/prismarine-viewer/examples/shared.ts b/prismarine-viewer/examples/shared.ts new file mode 100644 index 000000000..4ef9b417a --- /dev/null +++ b/prismarine-viewer/examples/shared.ts @@ -0,0 +1,11 @@ +export type BlockFaceType = { + side: number + textureIndex: number + textureName?: string + tint?: [number, number, number] + isTransparent?: boolean +} + +export type BlockType = { + faces: BlockFaceType[] +} diff --git a/prismarine-viewer/viewer/lib/mesher/mesher.ts b/prismarine-viewer/viewer/lib/mesher/mesher.ts index 118f79c74..4813cfc9c 100644 --- a/prismarine-viewer/viewer/lib/mesher/mesher.ts +++ b/prismarine-viewer/viewer/lib/mesher/mesher.ts @@ -11,6 +11,7 @@ if (module.require) { global.performance = r('perf_hooks').performance } +let workerIndex = 0 let world: World let dirtySections = new Map() let allDataReady = false @@ -85,8 +86,9 @@ const handleMessage = data => { switch (data.type) { case 'mesherData': { - setMesherData(data.blockstatesModels, data.blocksAtlas) + setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu') allDataReady = true + workerIndex = data.workerIndex break } @@ -148,18 +150,22 @@ setInterval(() => { for (const key of dirtySections.keys()) { const [x, y, z] = key.split(',').map(v => parseInt(v, 10)) const chunk = world.getColumn(x, z) + let processTime = 0 if (chunk?.getSection(new Vec3(x, y, z))) { + const start = performance.now() const geometry = getSectionGeometry(x, y, z, world) - const transferable = [geometry.positions.buffer, geometry.normals.buffer, geometry.colors.buffer, geometry.uvs.buffer] + const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean) //@ts-expect-error postMessage({ type: 'geometry', key, geometry }, transferable) + processTime = performance.now() - start } else { // console.info('[mesher] Missing section', x, y, z) } const dirtyTimes = dirtySections.get(key) if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy') for (let i = 0; i < dirtyTimes; i++) { - postMessage({ type: 'sectionFinished', key }) + postMessage({ type: 'sectionFinished', key, workerIndex, processTime }) + processTime = 0 } dirtySections.delete(key) } diff --git a/prismarine-viewer/viewer/lib/mesher/models.ts b/prismarine-viewer/viewer/lib/mesher/models.ts index c4249ac4b..54c879b82 100644 --- a/prismarine-viewer/viewer/lib/mesher/models.ts +++ b/prismarine-viewer/viewer/lib/mesher/models.ts @@ -1,8 +1,10 @@ import { Vec3 } from 'vec3' import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import legacyJson from '../../../../src/preflatMap.json' +import { BlockType } from '../../../examples/shared' import { World, BlockModelPartsResolved, WorldBlock as Block } from './world' import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon' +import { MesherGeometryOutput } from './shared' let blockProvider: WorldBlockProvider @@ -19,6 +21,19 @@ for (const key of Object.keys(tintsData)) { tints[key] = prepareTints(tintsData[key]) } +type TestTileData = { + block: string + faces: Array<{ + face: string + neighbor: string + light?: number + }> +} + +type Tiles = { + [blockPos: string]: BlockType & TestTileData +} + function prepareTints (tints) { const map = new Map() const defaultValue = tintToGl(tints.default) @@ -54,19 +69,25 @@ export function preflatBlockCalculation (block: Block, world: World, position: V ] // set needed props to true: east:'false',north:'false',south:'false',west:'false' const props = {} + let changed = false for (const [i, neighbor] of neighbors.entries()) { const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false if (isConnectedToSolid || neighbor?.name === block.name) { props[['south', 'north', 'east', 'west'][i]] = 'true' + changed = true } } - return props + return changed ? props : undefined } // case 'gate_in_wall': {} case 'block_snowy': { const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow' - return { - snowy: `${aboveIsSnow}` + if (aboveIsSnow) { + return { + snowy: `${aboveIsSnow}` + } + } else { + return } } case 'door': { @@ -139,7 +160,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ if (!neighbor) continue if (neighbor.type === type) continue const isGlass = neighbor.name.includes('glass') - if ((isCube(neighbor) && !isUp) || neighbor.getProperties().waterlogged) continue + if ((isCube(neighbor) && !isUp) || neighbor.material === 'plant' || neighbor.getProperties().waterlogged) continue let tint = [1, 1, 1] if (water) { @@ -151,11 +172,12 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ } if (needTiles) { - attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { + const tiles = attr.tiles as Tiles + tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { block: 'water', faces: [], } - attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ + tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ face, neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, // texture: eFace.texture.name, @@ -183,7 +205,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ let needRecompute = false -function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: Record, globalMatrix: any, globalShift: any, block: Block, biome: string) { +function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: MesherGeometryOutput, globalMatrix: any, globalShift: any, block: Block, biome: string) { const position = cursor // const key = `${position.x},${position.y},${position.z}` // if (!globalThis.allowedBlocks.includes(key)) return @@ -192,7 +214,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: // eslint-disable-next-line guard-for-in for (const face in element.faces) { const eFace = element.faces[face] - const { corners, mask1, mask2 } = elemFaces[face] + const { corners, mask1, mask2, side } = elemFaces[face] const dir = matmul3(globalMatrix, elemFaces[face].dir) if (eFace.cullface) { @@ -214,7 +236,10 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: const maxz = element.to[2] const texture = eFace.texture as any - const { u, v, su, sv } = texture + const { u } = texture + const { v } = texture + const { su } = texture + const { sv } = texture const ndx = Math.floor(attr.positions.length / 3) @@ -246,7 +271,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: let localMatrix = null as any let localShift = null as any - if (element.rotation) { + if (element.rotation && !needTiles) { // todo do we support rescale? localMatrix = buildRotationMatrix( element.rotation.axis, @@ -272,21 +297,23 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: (pos[2] ? maxz : minz) ] - vertex = vecadd3(matmul3(localMatrix, vertex), localShift) - vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) - vertex = vertex.map(v => v / 16) + if (!needTiles) { + vertex = vecadd3(matmul3(localMatrix, vertex), localShift) + vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) + vertex = vertex.map(v => v / 16) - attr.positions.push( - vertex[0] + (cursor.x & 15) - 8, - vertex[1] + (cursor.y & 15) - 8, - vertex[2] + (cursor.z & 15) - 8 - ) + attr.positions.push( + vertex[0] + (cursor.x & 15) - 8, + vertex[1] + (cursor.y & 15) - 8, + vertex[2] + (cursor.z & 15) - 8 + ) - attr.normals.push(...dir) + attr.normals.push(...dir) - const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5 - const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5 - attr.uvs.push(baseu * su + u, basev * sv + v) + const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5 + const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5 + attr.uvs.push(baseu * su + u, basev * sv + v) + } let light = 1 if (doAO) { @@ -322,40 +349,49 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: aos.push(ao) } - attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light) + if (!needTiles) { + attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light) + } } + const lightWithColor = [baseLight * tint[0], baseLight * tint[1], baseLight * tint[2]] as [number, number, number] + if (needTiles) { - attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { + const tiles = attr.tiles as Tiles + tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { block: block.name, faces: [], } - attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ - face, - neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, - light: baseLight - // texture: eFace.texture.name, - }) + const needsOnlyOneFace = false + const isTilesEmpty = tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.length < 1 + if (isTilesEmpty || !needsOnlyOneFace) { + tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ + face, + side, + textureIndex: eFace.texture.tileIndex, + neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, + light: baseLight, + tint: lightWithColor, + //@ts-expect-error debug prop + texture: eFace.texture.debugName || block.name, + } satisfies BlockType['faces'][number] & TestTileData['faces'][number] as any) + } } - if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { - attr.indices.push( - // eslint-disable-next-line @stylistic/function-call-argument-newline - ndx, ndx + 3, ndx + 2, - ndx, ndx + 1, ndx + 3 - ) - } else { - attr.indices.push( - // eslint-disable-next-line @stylistic/function-call-argument-newline - ndx, ndx + 1, ndx + 2, - ndx + 2, ndx + 1, ndx + 3 - ) + if (!needTiles) { + if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { + attr.indices.push( + ndx, ndx + 3, ndx + 2, ndx, ndx + 1, ndx + 3 + ) + } else { + attr.indices.push( + ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3 + ) + } } } } -const makeLooseObj = (obj: Record) => obj - const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier']) const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' @@ -365,7 +401,7 @@ let erroredBlockModel: BlockModelPartsResolved export function getSectionGeometry (sx, sy, sz, world: World) { let delayedRender = [] as Array<() => void> - const attr = makeLooseObj({ + const attr: MesherGeometryOutput = { sx: sx + 8, sy: sy + 8, sz: sz + 8, @@ -381,9 +417,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) { tiles: {}, // todo this can be removed here signs: {}, + isFull: true, highestBlocks: {}, hadErrors: false - } as Record) + } const cursor = new Vec3(0, 0, 0) for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) { @@ -419,19 +456,17 @@ export function getSectionGeometry (sx, sy, sz, world: World) { } const biome = block.biome.name - let preflatRecomputeVariant = !!(block as any)._originalProperties if (world.preflat) { const patchProperties = preflatBlockCalculation(block, world, cursor) if (patchProperties) { - //@ts-expect-error block._originalProperties ??= block._properties - //@ts-expect-error block._properties = { ...block._originalProperties, ...patchProperties } - preflatRecomputeVariant = true + if (block.models && JSON.stringify(block._originalProperties) !== JSON.stringify(block._properties)) { + // recompute models + block.models = undefined + } } else { - //@ts-expect-error block._properties = block._originalProperties ?? block._properties - //@ts-expect-error block._originalProperties = undefined } } @@ -449,7 +484,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { if (block.name !== 'water' && block.name !== 'lava' && !invisibleBlocks.has(block.name)) { // cache let { models } = block - if (block.models === undefined || preflatRecomputeVariant) { + if (block.models === undefined) { try { models = blockProvider.getAllResolvedModels0_1({ name: block.name, @@ -515,7 +550,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { delayedRender = [] let ndx = attr.positions.length / 3 - for (let i = 0; i < attr.t_positions.length / 12; i++) { + for (let i = 0; i < attr.t_positions!.length / 12; i++) { attr.indices.push( ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3, // eslint-disable-next-line @stylistic/function-call-argument-newline @@ -525,10 +560,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) { ndx += 4 } - attr.positions.push(...attr.t_positions) - attr.normals.push(...attr.t_normals) - attr.colors.push(...attr.t_colors) - attr.uvs.push(...attr.t_uvs) + attr.positions.push(...attr.t_positions!) + attr.normals.push(...attr.t_normals!) + attr.colors.push(...attr.t_colors!) + attr.uvs.push(...attr.t_uvs!) delete attr.t_positions delete attr.t_normals @@ -540,6 +575,13 @@ export function getSectionGeometry (sx, sy, sz, world: World) { attr.colors = new Float32Array(attr.colors) as any attr.uvs = new Float32Array(attr.uvs) as any + if (needTiles) { + delete attr.positions + delete attr.normals + delete attr.colors + delete attr.uvs + } + return attr } diff --git a/prismarine-viewer/viewer/lib/mesher/shared.ts b/prismarine-viewer/viewer/lib/mesher/shared.ts index 782a31419..f96c7d4b9 100644 --- a/prismarine-viewer/viewer/lib/mesher/shared.ts +++ b/prismarine-viewer/viewer/lib/mesher/shared.ts @@ -1,11 +1,35 @@ +import { BlockType } from '../../../examples/shared' + export const defaultMesherConfig = { version: '', enableLighting: true, skyLight: 15, smoothLighting: true, - outputFormat: 'threeJs' as 'threeJs' | 'webgl', + outputFormat: 'threeJs' as 'threeJs' | 'webgpu', textureSize: 1024, // for testing debugModelVariant: undefined as undefined | number[] } export type MesherConfig = typeof defaultMesherConfig + +export type MesherGeometryOutput = { + sx: number, + sy: number, + sz: number, + // resulting: float32array + positions: any, + normals: any, + colors: any, + uvs: any, + t_positions?: number[], + t_normals?: number[], + t_colors?: number[], + t_uvs?: number[], + + indices: number[], + tiles: Record, + signs: Record, + isFull: boolean + highestBlocks: Record + hadErrors: boolean +} diff --git a/prismarine-viewer/viewer/lib/mesher/world.ts b/prismarine-viewer/viewer/lib/mesher/world.ts index db940f662..ae65118c9 100644 --- a/prismarine-viewer/viewer/lib/mesher/world.ts +++ b/prismarine-viewer/viewer/lib/mesher/world.ts @@ -26,6 +26,8 @@ export type WorldBlock = Omit & { isCube: boolean /** cache */ models?: BlockModelPartsResolved | null + _originalProperties?: Record + _properties?: Record } diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts index 482907d4e..fe5a3ad2c 100644 --- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -91,8 +91,17 @@ export abstract class WorldRendererCommon items?: CustomTexturesData blocks?: CustomTexturesData } = {} + workersProcessAverageTime = 0 + workersProcessAverageTimeCount = 0 + maxWorkersProcessTime = 0 + edgeChunks = {} as Record + lastAddChunk = null as null | { + timeout: any + x: number + z: number + } - abstract outputFormat: 'threeJs' | 'webgl' + abstract outputFormat: 'threeJs' | 'webgpu' constructor (public config: WorldRendererConfig) { // this.initWorkers(1) // preload script on page load @@ -145,6 +154,11 @@ export abstract class WorldRendererCommon } this.renderUpdateEmitter.emit('update') + if (data.processTime) { + this.workersProcessAverageTimeCount++ + this.workersProcessAverageTime = ((this.workersProcessAverageTime * (this.workersProcessAverageTimeCount - 1)) + data.processTime) / this.workersProcessAverageTimeCount + this.maxWorkersProcessTime = Math.max(this.maxWorkersProcessTime, data.processTime) + } } } worker.onmessage = ({ data }) => { @@ -265,7 +279,7 @@ export abstract class WorldRendererCommon this.currentTextureImage = this.material.map.image this.mesherConfig.textureSize = this.material.map.image.width - for (const worker of this.workers) { + for (const [i, worker] of this.workers.entries()) { const { blockstatesModels } = this if (this.customBlockStates) { // TODO! remove from other versions as well @@ -282,6 +296,7 @@ export abstract class WorldRendererCommon } worker.postMessage({ type: 'mesherData', + workerIndex: i, blocksAtlas: { latest: blocksAtlas },