pages235/renderer/playground/baseScene.ts

414 lines
13 KiB
TypeScript

//@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<string, any>
paramOptions = {} as Partial<Record<keyof typeof this.params, {
hide?: boolean
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>
alwaysIgnoreQs = [] as string[]
skipUpdateQs = false
controls: any
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 (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<string, any>) {
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<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')
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<string>()
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()
}
}