feat: optimize slabs render performance by rendering less not visible tiles

Improve performance in Greenfield by 6%
This commit is contained in:
Vitaly Turovsky 2024-10-30 11:33:26 +03:00
commit 2fbfc18d2e
8 changed files with 117 additions and 53 deletions

View file

@ -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', () => {

View file

@ -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()

View file

@ -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'

View file

@ -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 })
}
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)) {