fix: improve playground by allowing sync world for fast iterating of advanced use cases

This commit is contained in:
Vitaly Turovsky 2024-10-22 19:28:41 +03:00
commit 347d155884
6 changed files with 157 additions and 28 deletions

View file

@ -17,6 +17,7 @@ import { WorldDataEmitter } from '../viewer'
import { Viewer } from '../viewer/lib/viewer'
import { BlockNames } from '../../src/mcDataTypes'
import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats'
import { getSyncWorld } from './shared'
window.THREE = THREE
@ -31,11 +32,13 @@ export class BasePlaygroundScene {
options?: string[]
min?: number
max?: number
reloadOnChange?: boolean
}>>
version = new URLSearchParams(window.location.search).get('version') || globalThis.includedVersions.at(-1)
Chunk: typeof import('prismarine-chunk/types/index').PCChunk
Block: typeof import('prismarine-block').Block
ignoreResize = false
enableCameraControls = true // not finished
enableCameraOrbitControl = true
gui = new GUI()
onParamUpdate = {} as Record<string, () => void>
@ -43,6 +46,7 @@ export class BasePlaygroundScene {
skipUpdateQs = false
controls: any
windowHidden = false
world: ReturnType<typeof getSyncWorld>
constructor () {
void this.initData().then(() => {
@ -90,6 +94,12 @@ export class BasePlaygroundScene {
if (object === this.params) {
this.onParamUpdate[property]?.()
this.onParamsUpdate(property, object)
const value = this.params[property]
if (this.paramOptions[property]?.reloadOnChange && (typeof value === 'boolean' || this.paramOptions[property].options)) {
setTimeout(() => {
window.location.reload()
})
}
} else {
this.onParamsUpdate(property, object)
}
@ -97,7 +107,7 @@ export class BasePlaygroundScene {
})
}
mainChunk: import('prismarine-chunk/types/index').PCChunk
// mainChunk: import('prismarine-chunk/types/index').PCChunk
setupWorld () { }
@ -107,13 +117,13 @@ export class BasePlaygroundScene {
const block =
properties ?
this.Block.fromProperties(loadedData.blocksByName[blockName].id, properties ?? {}, 0) :
this.Block.fromStateId(loadedData.blocksByName[blockName].defaultState, 0)
this.mainChunk.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block)
this.Block.fromStateId(loadedData.blocksByName[blockName].defaultState!, 0)
this.world.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block)
}
resetCamera () {
const { targetPos } = this
this.controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
this.controls?.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
const cameraPos = targetPos.offset(2, 2, 2)
const pitch = THREE.MathUtils.degToRad(-45)
@ -121,7 +131,7 @@ export class BasePlaygroundScene {
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
this.controls.update()
this.controls?.update()
}
async initData () {
@ -132,29 +142,33 @@ export class BasePlaygroundScene {
this.Chunk = (ChunkLoader as any)(this.version)
this.Block = (BlockLoader as any)(this.version)
this.mainChunk = new this.Chunk(undefined as any)
const World = (WorldLoader as any)(this.version)
const world = new World((chunkX, chunkZ) => {
if (chunkX === 0 && chunkZ === 0) return this.mainChunk
return new this.Chunk(undefined as any)
})
const world = getSyncWorld(this.version)
world.setBlockStateId(this.targetPos, 0)
this.world = world
this.initGui()
const worldView = new WorldDataEmitter(world, this.viewDistance, this.targetPos)
worldView.addWaitTime = 0
window.worldView = worldView
// Create three.js context, add to page
const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] })
renderer.setPixelRatio(window.devicePixelRatio || 1)
renderer.setSize(window.innerWidth, window.innerHeight)
initWithRenderer(renderer.domElement)
document.body.appendChild(renderer.domElement)
// Create viewer
const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false, })
const viewer = new Viewer(renderer, { numWorkers: 6, showChunkBorders: false, })
window.viewer = viewer
viewer.world.isPlayground = true
const isWebgpu = false
const promises = [] as Array<Promise<void>>
if (isWebgpu) {
// promises.push(initWebgpuRenderer(() => { }, true, true)) // todo
} else {
initWithRenderer(renderer.domElement)
renderer.domElement.id = 'viewer-canvas'
document.body.appendChild(renderer.domElement)
}
viewer.addChunksBatchWaitTime = 0
viewer.world.blockstatesModels = blockstatesModels
viewer.entities.setDebugMode('basic')
@ -163,15 +177,17 @@ export class BasePlaygroundScene {
viewer.render()
}
viewer.world.mesherConfig.enableLighting = false
await Promise.all(promises)
this.setupWorld()
viewer.connect(worldView)
await worldView.init(this.targetPos)
if (this.enableCameraOrbitControl) {
if (this.enableCameraControls) {
const { targetPos } = this
const controls = new OrbitControls(viewer.camera, renderer.domElement)
const canvas = document.querySelector('#viewer-canvas')
const controls = this.enableCameraOrbitControl ? new OrbitControls(viewer.camera, canvas) : undefined
this.controls = controls
this.resetCamera()
@ -182,7 +198,7 @@ export class BasePlaygroundScene {
const [x, y, z, rx, ry] = cameraSet.split(',').map(Number)
viewer.camera.position.set(x, y, z)
viewer.camera.rotation.set(rx, ry, 0, 'ZYX')
controls.update()
this.controls?.update()
}
const throttledCamQsUpdate = _.throttle(() => {
const { camera } = viewer
@ -196,13 +212,46 @@ export class BasePlaygroundScene {
camera.rotation.y.toFixed(2),
].join(',')
}, 200)
controls.addEventListener('change', () => {
throttledCamQsUpdate()
this.render()
})
if (this.controls) {
this.controls.addEventListener('change', () => {
throttledCamQsUpdate()
this.render()
})
} else {
setInterval(() => {
throttledCamQsUpdate()
}, 200)
}
// #endregion
}
if (!this.enableCameraOrbitControl) {
// mouse
let mouseMoveCounter = 0
const mouseMove = (e: PointerEvent) => {
if ((e.target as HTMLElement).closest('.lil-gui')) return
if (e.buttons === 1 || e.pointerType === 'touch') {
mouseMoveCounter++
viewer.camera.rotation.x -= e.movementY / 100
//viewer.camera.
viewer.camera.rotation.y -= e.movementX / 100
if (viewer.camera.rotation.x < -Math.PI / 2) viewer.camera.rotation.x = -Math.PI / 2
if (viewer.camera.rotation.x > Math.PI / 2) viewer.camera.rotation.x = Math.PI / 2
// yaw += e.movementY / 20;
// pitch += e.movementX / 20;
}
if (e.buttons === 2) {
viewer.camera.position.set(0, 0, 0)
}
}
setInterval(() => {
// updateTextEvent(`Mouse Events: ${mouseMoveCounter}`)
mouseMoveCounter = 0
}, 1000)
window.addEventListener('pointermove', mouseMove)
}
// await this.initialSetup()
this.onResize()
window.addEventListener('resize', () => this.onResize())
@ -233,7 +282,7 @@ export class BasePlaygroundScene {
addKeyboardShortcuts () {
document.addEventListener('keydown', (e) => {
if (e.code === 'KeyR' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
this.controls.reset()
this.controls?.reset()
this.resetCamera()
}
})
@ -280,8 +329,8 @@ export class BasePlaygroundScene {
direction.z *= 2
}
// Add the vector to the camera's position to move the camera
viewer.camera.position.add(direction)
this.controls.update()
viewer.camera.position.add(direction.normalize())
this.controls?.update()
this.render()
}
setInterval(updateKeys, 1000 / 30)

View file

@ -295,7 +295,7 @@ class MainScene extends BasePlaygroundScene {
}
}
viewer.setBlockStateId(this.targetPos, block.stateId!)
worldView!.setBlockStateId(this.targetPos, block.stateId!)
console.log('up stateId', block.stateId)
this.params.metadata = block.metadata
this.metadataGui.updateDisplay()

View file

@ -1,3 +1,6 @@
import WorldLoader, { world } from 'prismarine-world'
import ChunkLoader from 'prismarine-chunk'
export type BlockFaceType = {
side: number
textureIndex: number
@ -5,8 +8,8 @@ export type BlockFaceType = {
isTransparent?: boolean
// for testing
face: string
neighbor: string
face?: string
neighbor?: string
light?: number
}
@ -16,3 +19,61 @@ export type BlockType = {
// for testing
block: string
}
export const makeError = (str: string) => {
reportError?.(str)
}
export const makeErrorCritical = (str: string) => {
throw new Error(str)
}
export const getSyncWorld = (version: string): world.WorldSync => {
const World = (WorldLoader as any)(version)
const Chunk = (ChunkLoader as any)(version)
const world = new World(version).sync
const methods = getAllMethods(world)
for (const method of methods) {
if (method.startsWith('set') && method !== 'setColumn') {
const oldMethod = world[method].bind(world)
world[method] = (...args) => {
const arg = args[0]
if (arg.x !== undefined && !world.getColumnAt(arg)) {
world.setColumn(Math.floor(arg.x / 16), Math.floor(arg.z / 16), new Chunk(undefined as any))
}
oldMethod(...args)
}
}
}
return world
}
function getAllMethods (obj) {
const methods = new Set()
let currentObj = obj
do {
for (const name of Object.getOwnPropertyNames(currentObj)) {
if (typeof obj[name] === 'function' && name !== 'constructor') {
methods.add(name)
}
}
} while ((currentObj = Object.getPrototypeOf(currentObj)))
return [...methods] as string[]
}
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => void, chunkSize = 1) => {
// if delay is 0 then don't use setTimeout
for (let i = 0; i < arr.length; i += chunkSize) {
if (delay) {
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => {
setTimeout(resolve, delay)
})
}
exec(arr[i], i)
}
}

View file

@ -28,6 +28,9 @@
font-family: mojangles;
src: url(../../../assets/mojangles.ttf);
}
* {
user-select: none;
}
</style>
<script>
if (window.location.pathname.endsWith('playground')) {

View file

@ -98,6 +98,9 @@ export class Viewer {
}
setBlockStateId (pos: Vec3, stateId: number) {
if (!this.world.loadedChunks[`${Math.floor(pos.x / 16)},${Math.floor(pos.z / 16)}`]) {
console.warn('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
}
this.world.setBlockStateId(pos, stateId)
}

View file

@ -47,6 +47,19 @@ export class WorldDataEmitter extends EventEmitter {
})
}
setBlockStateId (position: Vec3, stateId: number) {
const val = this.world.setBlockStateId(position, stateId) as Promise<void> | void
if (val) throw new Error('setBlockStateId returned promise (not supported)')
const chunkX = Math.floor(position.x / 16)
const chunkZ = Math.floor(position.z / 16)
if (!this.loadedChunks[`${chunkX},${chunkZ}`]) {
void this.loadChunk({ x: chunkX, z: chunkZ })
return
}
this.emit('blockUpdate', { pos: position, stateId })
}
updateViewDistance (viewDistance: number) {
this.viewDistance = viewDistance
this.emitter.emit('renderDistance', viewDistance)