Skip to content

Commit

Permalink
feat: rewrite playground from scratch + extras (#202)
Browse files Browse the repository at this point in the history
  • Loading branch information
zardoy authored Oct 17, 2024
1 parent 0368e12 commit e199808
Show file tree
Hide file tree
Showing 25 changed files with 1,146 additions and 549 deletions.
318 changes: 318 additions & 0 deletions prismarine-viewer/examples/baseScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
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'

window.THREE = THREE

export class BasePlaygroundScene {
continuousRender = 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
}>>
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
enableCameraOrbitControl = true
gui = new GUI()
onParamUpdate = {} as Record<string, () => void>
alwaysIgnoreQs = [] as string[]
skipUpdateQs = false
controls: any
windowHidden = false

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

onParamsUpdate (paramName: string, object: any) {}
updateQs () {
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)
}
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)
}
this.gui.open(false)

this.gui.onChange(({ property, object }) => {
if (object === this.params) {
this.onParamUpdate[property]?.()
this.onParamsUpdate(property, object)
} else {
this.onParamsUpdate(property, object)
}
this.updateQs()
})
}

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

setupWorld () { }

// 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.mainChunk.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)

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

this.initGui()

const worldView = new WorldDataEmitter(world, this.viewDistance, this.targetPos)
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, })
window.viewer = viewer
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
this.setupWorld()

viewer.connect(worldView)

await worldView.init(this.targetPos)

if (this.enableCameraOrbitControl) {
const { targetPos } = this
const controls = new OrbitControls(viewer.camera, renderer.domElement)
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')
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)
controls.addEventListener('change', () => {
throttledCamQsUpdate()
this.render()
})
// #endregion
}

// 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()
requestAnimationFrame(() => this.loop())
}
}

render () {
statsStart()
viewer.render()
statsEnd()
}

addKeyboardShortcuts () {
document.addEventListener('keydown', (e) => {
if (e.code === 'KeyR' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
this.controls.reset()
this.resetCamera()
}
})
document.addEventListener('visibilitychange', () => {
this.windowHidden = document.visibilityState === 'hidden'
})
document.addEventListener('blur', () => {
this.windowHidden = true
})
document.addEventListener('focus', () => {
this.windowHidden = false
})

const updateKeys = () => {
// 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)
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()
}
}
1 change: 0 additions & 1 deletion prismarine-viewer/examples/examples/index.ts

This file was deleted.

9 changes: 0 additions & 9 deletions prismarine-viewer/examples/examples/rotation.ts

This file was deleted.

6 changes: 0 additions & 6 deletions prismarine-viewer/examples/examples/type.ts

This file was deleted.

Loading

0 comments on commit e199808

Please sign in to comment.