Skip to content

Commit

Permalink
fix: a lot of edge case world block updates fixes & improvements. Fix…
Browse files Browse the repository at this point in the history
… all known visual incosistencies after chunk edge block updates, make them faster
  • Loading branch information
zardoy committed Dec 18, 2024
1 parent 7b0ead5 commit 064d704
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 83 deletions.
57 changes: 41 additions & 16 deletions prismarine-viewer/examples/baseScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ window.THREE = THREE

export class BasePlaygroundScene {
continuousRender = false
stopRender = false
guiParams = {}
viewDistance = 0
targetPos = new Vec3(2, 90, 2)
Expand All @@ -49,23 +50,35 @@ export class BasePlaygroundScene {
windowHidden = false
world: ReturnType<typeof getSyncWorld>

_worldConfig = defaultWorldRendererConfig
get worldConfig () {
return this._worldConfig
}
set worldConfig (value) {
this._worldConfig = value
viewer.world.config = value
}

constructor () {
void this.initData().then(() => {
this.addKeyboardShortcuts()
})
}

onParamsUpdate (paramName: string, object: any) {}
updateQs () {
updateQs (paramName: string, valueSet: any) {
if (this.skipUpdateQs) return
const oldQs = new URLSearchParams(window.location.search)
const newQs = new URLSearchParams()
if (oldQs.get('scene')) {
newQs.set('scene', oldQs.get('scene')!)
}
for (const [key, value] of Object.entries(this.params)) {
if (!value || typeof value === 'function' || this.params.skipQs?.includes(key) || this.alwaysIgnoreQs.includes(key)) continue
newQs.set(key, value)
const newQs = new URLSearchParams(window.location.search)
// if (oldQs.get('scene')) {
// newQs.set('scene', oldQs.get('scene')!)
// }
for (const [key, value] of Object.entries({ [paramName]: valueSet })) {
if (typeof value === 'function' || this.params.skipQs?.includes(key) || this.alwaysIgnoreQs.includes(key)) continue
if (value) {
newQs.set(key, value)
} else {
newQs.delete(key)
}
}
window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`)
}
Expand All @@ -89,7 +102,9 @@ export class BasePlaygroundScene {
if (option?.hide) continue
this.gui.add(this.params, param, option?.options ?? option?.min, option?.max)
}
this.gui.open(false)
if (window.innerHeight < 700) {
this.gui.open(false)
}

this.gui.onChange(({ property, object }) => {
if (object === this.params) {
Expand All @@ -101,16 +116,18 @@ export class BasePlaygroundScene {
window.location.reload()
})
}
this.updateQs(property, value)
} else {
this.onParamsUpdate(property, object)
}
this.updateQs()
})
}

// mainChunk: import('prismarine-chunk/types/index').PCChunk

// overridables
setupWorld () { }
sceneReset () {}

// eslint-disable-next-line max-params
addWorldBlock (xOffset: number, yOffset: number, zOffset: number, blockName: BlockNames, properties?: Record<string, any>) {
Expand Down Expand Up @@ -159,8 +176,9 @@ export class BasePlaygroundScene {
renderer.setSize(window.innerWidth, window.innerHeight)

// Create viewer
const viewer = new Viewer(renderer, { ...defaultWorldRendererConfig, numWorkers: 6 })
const viewer = new Viewer(renderer, this.worldConfig)
window.viewer = viewer
window.world = window.viewer.world
const isWebgpu = false
const promises = [] as Array<Promise<void>>
if (isWebgpu) {
Expand Down Expand Up @@ -269,12 +287,14 @@ export class BasePlaygroundScene {

loop () {
if (this.continuousRender && !this.windowHidden) {
this.render()
this.render(true)
requestAnimationFrame(() => this.loop())
}
}

render () {
render (fromLoop = false) {
if (!fromLoop && this.continuousRender) return
if (this.stopRender) return
statsStart()
viewer.render()
statsEnd()
Expand All @@ -287,8 +307,13 @@ export class BasePlaygroundScene {
this.controls?.reset()
this.resetCamera()
}
if (e.code === 'KeyE') {
worldView?.setBlockStateId(this.targetPos, this.world.getBlockStateId(this.targetPos))
if (e.code === 'KeyE') { // refresh block (main)
worldView!.setBlockStateId(this.targetPos, this.world.getBlockStateId(this.targetPos))
}
if (e.code === 'KeyF') { // reload all chunks
this.sceneReset()
worldView!.unloadAllChunks()
void worldView!.init(this.targetPos)
}
}
})
Expand Down
162 changes: 112 additions & 50 deletions prismarine-viewer/examples/scenes/frequentUpdates.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Vec3 } from 'vec3'
import { BasePlaygroundScene } from '../baseScene'

export default class extends BasePlaygroundScene {
Expand All @@ -6,69 +7,125 @@ export default class extends BasePlaygroundScene {

override initGui (): void {
this.params = {
squareSize: 50
testActive: false,
testUpdatesPerSecond: 10,
testInitialUpdate: false,
stopGeometryUpdate: false,
manualTest: () => {
this.updateBlock()
},
testNeighborUpdates: () => {
this.testNeighborUpdates()
}
}

super.initGui()
}

lastUpdatedOffset = 0
lastUpdatedId = 2
updateBlock () {
const x = this.lastUpdatedOffset % 16
const z = Math.floor(this.lastUpdatedOffset / 16)
const y = 90
worldView!.setBlockStateId(new Vec3(x, y, z), this.lastUpdatedId++)
this.lastUpdatedOffset++
if (this.lastUpdatedOffset > 16 * 16) this.lastUpdatedOffset = 0
if (this.lastUpdatedId > 500) this.lastUpdatedId = 1
}

testNeighborUpdates () {
viewer.world.setBlockStateId(new Vec3(15, 95, 15), 1)
viewer.world.setBlockStateId(new Vec3(0, 95, 15), 1)
viewer.world.setBlockStateId(new Vec3(15, 95, 0), 1)
viewer.world.setBlockStateId(new Vec3(0, 95, 0), 1)

viewer.world.setBlockStateId(new Vec3(16, 95, 15), 1)
viewer.world.setBlockStateId(new Vec3(-1, 95, 15), 1)
viewer.world.setBlockStateId(new Vec3(15, 95, -1), 1)
viewer.world.setBlockStateId(new Vec3(-1, 95, 0), 1)
setTimeout(() => {
viewer.world.setBlockStateId(new Vec3(16, 96, 16), 1)
viewer.world.setBlockStateId(new Vec3(-1, 96, 16), 1)
viewer.world.setBlockStateId(new Vec3(16, 96, -1), 1)
viewer.world.setBlockStateId(new Vec3(-1, 96, -1), 1)
}, 3000)
}

setupTimer () {
// this.stopRender = true

let lastTime = 0
const tick = () => {
viewer.world.debugStopGeometryUpdate = this.params.stopGeometryUpdate
const updateEach = 1000 / this.params.testUpdatesPerSecond
requestAnimationFrame(tick)
if (!this.params.testActive) return
const updateCount = Math.floor(performance.now() - lastTime) / updateEach
for (let i = 0; i < updateCount; i++) {
this.updateBlock()
}
lastTime = performance.now()
}

requestAnimationFrame(tick)

// const limit = 1000
// const limit = 100
const limit = 1
const updatedChunks = new Set<string>()
const updatedBlocks = new Set<string>()
let lastSecond = 0
setInterval(() => {
const second = Math.floor(performance.now() / 1000)
if (lastSecond !== second) {
lastSecond = second
updatedChunks.clear()
updatedBlocks.clear()
}
const isEven = second % 2 === 0
if (updatedBlocks.size > limit) {
return
}
const changeBlock = (x, z) => {
const chunkKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
const key = `${x},${z}`
if (updatedBlocks.has(chunkKey)) return
// const limit = 1
// const updatedChunks = new Set<string>()
// const updatedBlocks = new Set<string>()
// let lastSecond = 0
// setInterval(() => {
// const second = Math.floor(performance.now() / 1000)
// if (lastSecond !== second) {
// lastSecond = second
// updatedChunks.clear()
// updatedBlocks.clear()
// }
// const isEven = second % 2 === 0
// if (updatedBlocks.size > limit) {
// return
// }
// const changeBlock = (x, z) => {
// const chunkKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
// const key = `${x},${z}`
// if (updatedBlocks.has(chunkKey)) return

updatedChunks.add(chunkKey)
worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(isEven ? 2 : 3, 0))
updatedBlocks.add(key)
}
const { squareSize } = this.params
const xStart = -squareSize
const zStart = -squareSize
const xEnd = squareSize
const zEnd = squareSize
for (let x = xStart; x <= xEnd; x += 16) {
for (let z = zStart; z <= zEnd; z += 16) {
const key = `${x},${z}`
if (updatedChunks.has(key)) continue
changeBlock(x, z)
return
}
}
// for (let x = xStart; x <= xEnd; x += 16) {
// for (let z = zStart; z <= zEnd; z += 16) {
// const key = `${x},${z}`
// if (updatedChunks.has(key)) continue
// changeBlock(x, z)
// return
// }
// }
}, 1)
// updatedChunks.add(chunkKey)
// worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(isEven ? 2 : 3, 0))
// updatedBlocks.add(key)
// }
// const { squareSize } = this.params
// const xStart = -squareSize
// const zStart = -squareSize
// const xEnd = squareSize
// const zEnd = squareSize
// for (let x = xStart; x <= xEnd; x += 16) {
// for (let z = zStart; z <= zEnd; z += 16) {
// const key = `${x},${z}`
// if (updatedChunks.has(key)) continue
// changeBlock(x, z)
// return
// }
// }
// for (let x = xStart; x <= xEnd; x += 16) {
// for (let z = zStart; z <= zEnd; z += 16) {
// const key = `${x},${z}`
// if (updatedChunks.has(key)) continue
// changeBlock(x, z)
// return
// }
// }
// }, 1)
}

setupWorld () {
this.params.squareSize ??= 30
const { squareSize } = this.params
const maxSquareSize = this.viewDistance * 16 * 2
if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`)
this.worldConfig.showChunkBorders = true

const maxSquareRadius = this.viewDistance * 16
// const fullBlocks = loadedData.blocksArray.map(x => x.name)
const squareSize = maxSquareRadius
for (let x = -squareSize; x <= squareSize; x++) {
for (let z = -squareSize; z <= squareSize; z++) {
const i = Math.abs(x + z) * squareSize
Expand All @@ -81,5 +138,10 @@ export default class extends BasePlaygroundScene {
done = true
this.setupTimer()
})
setTimeout(() => {
if (this.params.testInitialUpdate) {
this.updateBlock()
}
})
}
}
10 changes: 9 additions & 1 deletion prismarine-viewer/viewer/lib/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ export class Viewer {
const sectionX = Math.floor(pos.x / 16) * 16
const sectionZ = Math.floor(pos.z / 16) * 16
if (this.world.queuedChunks.has(`${sectionX},${sectionZ}`)) {
await this.world.waitForChunkToLoad(pos)
await new Promise<void>(resolve => {
this.world.queuedFunctions.push(() => {
resolve()
})
})
}
if (!this.world.loadedChunks[`${sectionX},${sectionZ}`]) {
console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
Expand Down Expand Up @@ -222,6 +226,10 @@ export class Viewer {
this.world.queuedChunks.delete(`${args[0]},${args[1]}`)
this.addColumn(...args as Parameters<typeof this.addColumn>)
}
for (const fn of this.world.queuedFunctions) {
fn()
}
this.world.queuedFunctions = []
currentLoadChunkBatch = null
}, this.addChunksBatchWaitTime)
}
Expand Down
Loading

0 comments on commit 064d704

Please sign in to comment.