//@ts-nocheck import { Vec3 } from 'vec3' import * as THREE from 'three' import '../../src/getCollisionShapes' import { IndexedData } from 'minecraft-data' import BlockLoader from 'prismarine-block' import blockstatesModels from 'mc-assets/dist/blockStatesModels.json' import ChunkLoader from 'prismarine-chunk' import WorldLoader from 'prismarine-world' //@ts-expect-error import { OrbitControls } from 'three/addons/controls/OrbitControls.js' // eslint-disable-next-line import/no-named-as-default import GUI from 'lil-gui' import _ from 'lodash' import { toMajorVersion } from '../../src/utils' import { WorldDataEmitter } from '../viewer' import { Viewer } from '../viewer/lib/viewer' import { BlockNames } from '../../src/mcDataTypes' import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats' import { defaultWorldRendererConfig } from '../viewer/lib/worldrendererCommon' import { getSyncWorld } from './shared' window.THREE = THREE export class BasePlaygroundScene { continuousRender = false stopRender = false guiParams = {} viewDistance = 0 targetPos = new Vec3(2, 90, 2) params = {} as Record paramOptions = {} as Partial> 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 void> alwaysIgnoreQs = [] as string[] skipUpdateQs = false controls: any windowHidden = false world: ReturnType _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 (paramName: string, valueSet: any) { if (this.skipUpdateQs) return 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()}`) } // async initialSetup () {} renderFinish () { this.render() } initGui () { const qs = new URLSearchParams(window.location.search) for (const key of Object.keys(this.params)) { const value = qs.get(key) if (!value) continue const parsed = /^-?\d+$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value this.params[key] = parsed } for (const param of Object.keys(this.params)) { const option = this.paramOptions[param] if (option?.hide) continue this.gui.add(this.params, param, option?.options ?? option?.min, option?.max) } if (window.innerHeight < 700) { this.gui.open(false) } else { // const observer = new MutationObserver(() => { // this.gui.domElement.classList.remove('transition') // }) // observer.observe(this.gui.domElement, { // attributes: true, // attributeFilter: ['class'], // }) setTimeout(() => { this.gui.domElement.classList.remove('transition') }, 500) } this.gui.onChange(({ property, object }) => { 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() }) } this.updateQs(property, value) } else { this.onParamsUpdate(property, object) } }) } // 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) { if (xOffset > 16 || yOffset > 16 || zOffset > 16) throw new Error('Offset too big') const block = properties ? this.Block.fromProperties(loadedData.blocksByName[blockName].id, properties ?? {}, 0) : 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) const cameraPos = targetPos.offset(2, 2, 2) const pitch = THREE.MathUtils.degToRad(-45) const yaw = THREE.MathUtils.degToRad(45) 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() } async initData () { await window._LOAD_MC_DATA() const mcData: IndexedData = require('minecraft-data')(this.version) window.loadedData = window.mcData = mcData this.Chunk = (ChunkLoader as any)(this.version) this.Block = (BlockLoader as any)(this.version) 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) // Create viewer const viewer = new Viewer(renderer, this.worldConfig) window.viewer = viewer window.world = window.viewer.world const isWebgpu = false const promises = [] as Array> 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') viewer.setVersion(this.version) viewer.entities.onSkinUpdate = () => { viewer.render() } viewer.world.mesherConfig.enableLighting = false await Promise.all(promises) this.setupWorld() viewer.connect(worldView) await worldView.init(this.targetPos) if (this.enableCameraControls) { const { targetPos } = this const canvas = document.querySelector('#viewer-canvas') const controls = this.enableCameraOrbitControl ? new OrbitControls(viewer.camera, canvas) : undefined this.controls = controls this.resetCamera() // #region camera rotation param const cameraSet = this.params.camera || localStorage.camera if (cameraSet) { 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') this.controls?.update() } const throttledCamQsUpdate = _.throttle(() => { const { camera } = viewer // params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}` // this.updateQs() localStorage.camera = [ camera.position.x.toFixed(2), camera.position.y.toFixed(2), camera.position.z.toFixed(2), camera.rotation.x.toFixed(2), camera.rotation.y.toFixed(2), ].join(',') }, 200) 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()) void viewer.waitForChunksToRender().then(async () => { this.renderFinish() }) viewer.world.renderUpdateEmitter.addListener('update', () => { this.render() }) this.loop() } loop () { if (this.continuousRender && !this.windowHidden) { this.render(true) requestAnimationFrame(() => this.loop()) } } render (fromLoop = false) { if (!fromLoop && this.continuousRender) return if (this.stopRender) return statsStart() viewer.render() statsEnd() } addKeyboardShortcuts () { document.addEventListener('keydown', (e) => { if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { if (e.code === 'KeyR') { this.controls?.reset() this.resetCamera() } 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) } } }) document.addEventListener('visibilitychange', () => { this.windowHidden = document.visibilityState === 'hidden' }) document.addEventListener('blur', () => { this.windowHidden = true }) document.addEventListener('focus', () => { this.windowHidden = false }) const updateKeys = () => { if (pressedKeys.has('ControlLeft') || pressedKeys.has('MetaLeft')) { return } // if (typeof viewer === 'undefined') return // Create a vector that points in the direction the camera is looking const direction = new THREE.Vector3(0, 0, 0) if (pressedKeys.has('KeyW')) { direction.z = -0.5 } if (pressedKeys.has('KeyS')) { direction.z += 0.5 } if (pressedKeys.has('KeyA')) { direction.x -= 0.5 } if (pressedKeys.has('KeyD')) { direction.x += 0.5 } if (pressedKeys.has('ShiftLeft')) { viewer.camera.position.y -= 0.5 } if (pressedKeys.has('Space')) { viewer.camera.position.y += 0.5 } direction.applyQuaternion(viewer.camera.quaternion) direction.y = 0 if (pressedKeys.has('ShiftLeft')) { direction.y *= 2 direction.x *= 2 direction.z *= 2 } // Add the vector to the camera's position to move the camera viewer.camera.position.add(direction.normalize()) this.controls?.update() this.render() } setInterval(updateKeys, 1000 / 30) const pressedKeys = new Set() const keys = (e) => { const { code } = e const pressed = e.type === 'keydown' if (pressed) { pressedKeys.add(code) } else { pressedKeys.delete(code) } } window.addEventListener('keydown', keys) window.addEventListener('keyup', keys) window.addEventListener('blur', (e) => { for (const key of pressedKeys) { keys(new KeyboardEvent('keyup', { code: key })) } }) } onResize () { if (this.ignoreResize) return const { camera, renderer } = viewer viewer.camera.aspect = window.innerWidth / window.innerHeight viewer.camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) this.render() } }