From 2fbfc18d2e1645a45c8fbe704af261c8d80af43e Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 30 Oct 2024 11:33:26 +0300 Subject: [PATCH] feat: optimize slabs render performance by rendering less not visible tiles Improve performance in Greenfield by 6% --- prismarine-viewer/examples/baseScene.ts | 11 ++- prismarine-viewer/examples/playground.ts | 2 +- prismarine-viewer/examples/scenes/index.ts | 1 + .../examples/scenes/slabsOptimization.ts | 15 ++++ prismarine-viewer/viewer/lib/mesher/models.ts | 88 ++++++++++--------- .../viewer/lib/mesher/test/playground.ts | 9 +- prismarine-viewer/viewer/lib/mesher/world.ts | 42 +++++++-- scripts/makeOptimizedMcData.mjs | 2 +- 8 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 prismarine-viewer/examples/scenes/slabsOptimization.ts diff --git a/prismarine-viewer/examples/baseScene.ts b/prismarine-viewer/examples/baseScene.ts index 52b9e781..b192baa8 100644 --- a/prismarine-viewer/examples/baseScene.ts +++ b/prismarine-viewer/examples/baseScene.ts @@ -281,9 +281,14 @@ export class BasePlaygroundScene { addKeyboardShortcuts () { document.addEventListener('keydown', (e) => { - if (e.code === 'KeyR' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { - this.controls?.reset() - this.resetCamera() + if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { + if (e.code === 'KeyR') { + this.controls?.reset() + this.resetCamera() + } + if (e.code === 'KeyE') { + worldView?.setBlockStateId(this.targetPos, this.world.getBlockStateId(this.targetPos)) + } } }) document.addEventListener('visibilitychange', () => { diff --git a/prismarine-viewer/examples/playground.ts b/prismarine-viewer/examples/playground.ts index c6d216e6..cd0fa219 100644 --- a/prismarine-viewer/examples/playground.ts +++ b/prismarine-viewer/examples/playground.ts @@ -4,7 +4,7 @@ import * as scenes from './scenes' const qsScene = new URLSearchParams(window.location.search).get('scene') const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main -playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates'] +playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization'] playgroundGlobalUiState.selected = qsScene ?? 'main' const scene = new Scene() diff --git a/prismarine-viewer/examples/scenes/index.ts b/prismarine-viewer/examples/scenes/index.ts index a526686e..81657a71 100644 --- a/prismarine-viewer/examples/scenes/index.ts +++ b/prismarine-viewer/examples/scenes/index.ts @@ -7,3 +7,4 @@ export { default as transparencyIssue } from './transparencyIssue' export { default as rotationIssue } from './rotationIssue' export { default as entities } from './entities' export { default as frequentUpdates } from './frequentUpdates' +export { default as slabsOptimization } from './slabsOptimization' diff --git a/prismarine-viewer/examples/scenes/slabsOptimization.ts b/prismarine-viewer/examples/scenes/slabsOptimization.ts new file mode 100644 index 00000000..9035a777 --- /dev/null +++ b/prismarine-viewer/examples/scenes/slabsOptimization.ts @@ -0,0 +1,15 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class extends BasePlaygroundScene { + expectedNumberOfFaces = 30 + + setupWorld () { + this.addWorldBlock(0, 1, 0, 'stone_slab') + this.addWorldBlock(0, 0, 0, 'stone') + this.addWorldBlock(0, -1, 0, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(0, -1, -1, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(0, -1, 1, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(-1, -1, 0, 'stone_slab', { type: 'top', waterlogged: false }) + this.addWorldBlock(1, -1, 0, 'stone_slab', { type: 'top', waterlogged: false }) + } +} diff --git a/prismarine-viewer/viewer/lib/mesher/models.ts b/prismarine-viewer/viewer/lib/mesher/models.ts index 92f4b0b5..9518ff2e 100644 --- a/prismarine-viewer/viewer/lib/mesher/models.ts +++ b/prismarine-viewer/viewer/lib/mesher/models.ts @@ -196,13 +196,53 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ } } -let needRecompute = false +const identicalCull = (currentElement: BlockElement, neighbor: Block, direction: Vec3) => { + const dirStr = `${direction.x},${direction.y},${direction.z}` + const lookForOppositeSide = { + '0,1,0': 'down', + '0,-1,0': 'up', + '1,0,0': 'east', + '-1,0,0': 'west', + '0,0,1': 'south', + '0,0,-1': 'north', + }[dirStr]! + const elemCompareForm = { + '0,1,0': (e: BlockElement) => `${e.from[0]},${e.from[2]}:${e.to[0]},${e.to[2]}`, + '0,-1,0': (e: BlockElement) => `${e.to[0]},${e.to[2]}:${e.from[0]},${e.from[2]}`, + '1,0,0': (e: BlockElement) => `${e.from[2]},${e.from[1]}:${e.to[2]},${e.to[1]}`, + '-1,0,0': (e: BlockElement) => `${e.to[2]},${e.to[1]}:${e.from[2]},${e.from[1]}`, + '0,0,1': (e: BlockElement) => `${e.from[1]},${e.from[2]}:${e.to[1]},${e.to[2]}`, + '0,0,-1': (e: BlockElement) => `${e.to[1]},${e.to[2]}:${e.from[1]},${e.from[2]}`, + }[dirStr]! + const elementEdgeValidator = { + '0,1,0': (e: BlockElement) => currentElement.from[1] === 0 && e.to[2] === 16, + '0,-1,0': (e: BlockElement) => currentElement.from[1] === 0 && e.to[2] === 16, + '1,0,0': (e: BlockElement) => currentElement.from[0] === 0 && e.to[1] === 16, + '-1,0,0': (e: BlockElement) => currentElement.from[0] === 0 && e.to[1] === 16, + '0,0,1': (e: BlockElement) => currentElement.from[2] === 0 && e.to[0] === 16, + '0,0,-1': (e: BlockElement) => currentElement.from[2] === 0 && e.to[0] === 16, + }[dirStr]! + const useVar = 0 + const models = neighbor.models?.map(m => m[useVar] ?? m[0]) ?? [] + // TODO we should support it! rewrite with optimizing general pipeline + if (models.some(m => m.x || m.y || m.z)) return + for (const model of models) { + for (const element of model.elements ?? []) { + // todo check alfa on texture + if (element.faces[lookForOppositeSide]?.cullface && elemCompareForm(currentElement) === elemCompareForm(element) && elementEdgeValidator(element)) { + return true + } + } + } +} + +let needSectionRecomputeOnChange = false 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 - const cullIfIdentical = block.name.includes('glass') + const cullIfIdentical = block.name.includes('glass') || block.name.includes('ice') // eslint-disable-next-line guard-for-in for (const face in element.faces) { @@ -211,12 +251,12 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: const dir = matmul3(globalMatrix, elemFaces[face].dir) if (eFace.cullface) { - const neighbor = world.getBlock(cursor.plus(new Vec3(...dir))) + const neighbor = world.getBlock(cursor.plus(new Vec3(...dir)), blockProvider, {}) if (neighbor) { - if (cullIfIdentical && neighbor.type === block.type) continue - if (!neighbor.transparent && isCube(neighbor)) continue + if (cullIfIdentical && neighbor.stateId === block.stateId) continue + if (!neighbor.transparent && (isCube(neighbor) || identicalCull(element, neighbor, new Vec3(...dir)))) continue } else { - needRecompute = true + needSectionRecomputeOnChange = true continue } } @@ -391,7 +431,6 @@ const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier']) const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' let unknownBlockModel: BlockModelPartsResolved -let erroredBlockModel: BlockModelPartsResolved export function getSectionGeometry (sx, sy, sz, world: World) { let delayedRender = [] as Array<() => void> @@ -421,7 +460,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) { for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) { for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) { - const block = world.getBlock(cursor)! + let block = world.getBlock(cursor, blockProvider, attr)! if (!invisibleBlocks.has(block.name)) { const highest = attr.highestBlocks[`${cursor.x},${cursor.z}`] if (!highest || highest.y < cursor.y) { @@ -459,6 +498,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { if (block.models && JSON.stringify(block._originalProperties) !== JSON.stringify(block._properties)) { // recompute models block.models = undefined + block = world.getBlock(cursor, blockProvider, attr)! } } else { block._properties = block._originalProperties ?? block._properties @@ -481,37 +521,6 @@ 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) { - const props = block.getProperties() - try { - // fixme - if (world.preflat) { - if (block.name === 'cobblestone_wall') { - props.up = 'true' - for (const key of ['north', 'south', 'east', 'west']) { - const val = props[key] - if (val === 'false' || val === 'true') { - props[key] = val === 'true' ? 'low' : 'none' - } - } - } - } - - models = blockProvider.getAllResolvedModels0_1({ - name: block.name, - properties: props, - }, world.preflat)! // fixme! this is a hack (also need a setting for all versions) - if (!models.length) { - console.debug('[mesher] block to render not found', block.name, props) - models = null - } - } catch (err) { - models ??= erroredBlockModel - console.error(`Critical assets error. Unable to get block model for ${block.name}[${JSON.stringify(props)}]: ` + err.message, err.stack) - attr.hadErrors = true - } - } - block.models = models ?? null models ??= unknownBlockModel @@ -607,7 +616,6 @@ export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTil globalThis.blockProvider = blockProvider if (useUnknownBlockModel) { unknownBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'unknown', properties: {} }) - erroredBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'errored', properties: {} }) } needTiles = _needTiles diff --git a/prismarine-viewer/viewer/lib/mesher/test/playground.ts b/prismarine-viewer/viewer/lib/mesher/test/playground.ts index 2a0ce774..0441dd60 100644 --- a/prismarine-viewer/viewer/lib/mesher/test/playground.ts +++ b/prismarine-viewer/viewer/lib/mesher/test/playground.ts @@ -1,3 +1,4 @@ +import { BlockNames } from '../../../../../src/mcDataTypes' import { setup } from './mesherTester' const addPositions = [ @@ -10,8 +11,10 @@ const addPositions = [ [[0, 0, -1], 'stone'], ] as const -const { mesherWorld, getGeometry, pos, mcData } = setup('1.18.1', addPositions as any) +const { mesherWorld, getGeometry, pos, mcData } = setup('1.21.1', addPositions as any) -// mesherWorld.setBlockStateId(pos, mcData.blocksByName.soul_sand.defaultState) +// mesherWorld.setBlockStateId(pos, 712) +// mesherWorld.setBlockStateId(pos, mcData.blocksByName.stone_slab.defaultState) +mesherWorld.setBlockStateId(pos, 11_225) -// console.log(getGeometry().centerTileNeighbors) +console.log(getGeometry().centerTileNeighbors) diff --git a/prismarine-viewer/viewer/lib/mesher/world.ts b/prismarine-viewer/viewer/lib/mesher/world.ts index 2cdee1d5..491b5650 100644 --- a/prismarine-viewer/viewer/lib/mesher/world.ts +++ b/prismarine-viewer/viewer/lib/mesher/world.ts @@ -38,6 +38,7 @@ export class World { blockCache = {} biomeCache: { [id: number]: mcData.Biome } preflat: boolean + erroredBlockModel: BlockModelPartsResolved constructor (version) { this.Chunk = Chunks(version) as any @@ -112,7 +113,7 @@ export class World { return this.getColumn(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) } - getBlock (pos: Vec3): WorldBlock | null { + getBlock (pos: Vec3, blockProvider?, attr?): WorldBlock | null { // for easier testing if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number]) const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) @@ -126,8 +127,7 @@ export class World { const stateId = column.getBlockStateId(locInChunk) if (!this.blockCache[stateId]) { - const b = column.getBlock(locInChunk) - //@ts-expect-error + const b = column.getBlock(locInChunk) as unknown as WorldBlock b.isCube = isCube(b.shapes) this.blockCache[stateId] = b Object.defineProperty(b, 'position', { @@ -136,7 +136,6 @@ export class World { } }) if (this.preflat) { - //@ts-expect-error b._properties = {} const namePropsStr = legacyJson.blocks[b.type + ':' + b.metadata] || findClosestLegacyBlockFallback(b.type, b.metadata, pos) @@ -149,7 +148,6 @@ export class World { if (!isNaN(val)) val = parseInt(val, 10) return [key, val] })) - //@ts-expect-error b._properties = newProperties } } @@ -157,6 +155,40 @@ export class World { } const block = this.blockCache[stateId] + + if (block.models === undefined && blockProvider) { + if (!attr) throw new Error('attr is required') + const props = block.getProperties() + try { + // fixme + if (this.preflat) { + if (block.name === 'cobblestone_wall') { + props.up = 'true' + for (const key of ['north', 'south', 'east', 'west']) { + const val = props[key] + if (val === 'false' || val === 'true') { + props[key] = val === 'true' ? 'low' : 'none' + } + } + } + } + + block.models = blockProvider.getAllResolvedModels0_1({ + name: block.name, + properties: props, + }, this.preflat)! // fixme! this is a hack (also need a setting for all versions) + if (!block.models!.length) { + console.debug('[mesher] block to render not found', block.name, props) + block.models = null + } + } catch (err) { + this.erroredBlockModel ??= blockProvider.getAllResolvedModels0_1({ name: 'errored', properties: {} }) + block.models ??= this.erroredBlockModel + console.error(`Critical assets error. Unable to get block model for ${block.name}[${JSON.stringify(props)}]: ` + err.message, err.stack) + attr.hadErrors = true + } + } + if (block.name === 'flowing_water') block.name = 'water' if (block.name === 'flowing_lava') block.name = 'lava' // block.position = loc // it overrides position of all currently loaded blocks diff --git a/scripts/makeOptimizedMcData.mjs b/scripts/makeOptimizedMcData.mjs index d0d2088d..74477d7f 100644 --- a/scripts/makeOptimizedMcData.mjs +++ b/scripts/makeOptimizedMcData.mjs @@ -60,7 +60,7 @@ const dataTypeBundling = { processData (current, prev) { for (const block of current) { if (block.transparent) { - const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) + const forceOpaque = block.name.includes('shulker_box') || block.name.match(/^double_.+_slab\d?$/) || ['melon_block', 'lit_pumpkin', 'lit_redstone_ore', 'lit_furnace'].includes(block.name) const prevBlock = prev?.find(x => x.name === block.name); if (forceOpaque || (prevBlock && !prevBlock.transparent)) {