diff --git a/prismarine-viewer/examples/baseScene.ts b/prismarine-viewer/examples/baseScene.ts new file mode 100644 index 000000000..53e106c4d --- /dev/null +++ b/prismarine-viewer/examples/baseScene.ts @@ -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 + 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 + enableCameraOrbitControl = true + gui = new GUI() + onParamUpdate = {} as Record 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) { + 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() + 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() + } +} diff --git a/prismarine-viewer/examples/examples/index.ts b/prismarine-viewer/examples/examples/index.ts deleted file mode 100644 index ee2031063..000000000 --- a/prismarine-viewer/examples/examples/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as rotation } from './rotation' diff --git a/prismarine-viewer/examples/examples/rotation.ts b/prismarine-viewer/examples/examples/rotation.ts deleted file mode 100644 index 0fdafce92..000000000 --- a/prismarine-viewer/examples/examples/rotation.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Vec3 } from 'vec3' -import { ExampleSetupFunction } from './type' - -const setup: ExampleSetupFunction = (world, mcData, mesherConfig, setupParam) => { - mesherConfig.debugModelVariant = [3] - void world.setBlockStateId(new Vec3(0, 0, 0), mcData.blocksByName.sand.defaultState!) -} - -export default setup diff --git a/prismarine-viewer/examples/examples/type.ts b/prismarine-viewer/examples/examples/type.ts deleted file mode 100644 index 37dfcbe8a..000000000 --- a/prismarine-viewer/examples/examples/type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CustomWorld } from 'flying-squid/dist/lib/modules/world' -import { IndexedData } from 'minecraft-data' -import { MesherConfig } from '../../viewer/lib/mesher/shared' - -type SetupParams = {} -export type ExampleSetupFunction = (world: CustomWorld, mcData: IndexedData, mesherConfig: MesherConfig, setupParam: SetupParams) => void diff --git a/prismarine-viewer/examples/playground.ts b/prismarine-viewer/examples/playground.ts index 1726fe86a..c6d216e60 100644 --- a/prismarine-viewer/examples/playground.ts +++ b/prismarine-viewer/examples/playground.ts @@ -1,498 +1,11 @@ -import _ from 'lodash' -import { Vec3 } from 'vec3' -import BlockLoader from 'prismarine-block' -import ChunkLoader from 'prismarine-chunk' -import WorldLoader from 'prismarine-world' -import * as THREE from 'three' -import { GUI } from 'lil-gui' -import JSZip from 'jszip' -import blockstatesModels from 'mc-assets/dist/blockStatesModels.json' +import { BasePlaygroundScene } from './baseScene' +import { playgroundGlobalUiState } from './playgroundUi' +import * as scenes from './scenes' -//@ts-expect-error -import { OrbitControls } from 'three/addons/controls/OrbitControls.js' -import { IndexedData } from 'minecraft-data' -import { loadScript } from '../viewer/lib/utils' -import { TWEEN_DURATION } from '../viewer/lib/entities' -import { EntityMesh } from '../viewer/lib/entity/EntityMesh' -import { WorldDataEmitter, Viewer } from '../viewer' -import '../../src/getCollisionShapes' -import { toMajorVersion } from '../../src/utils' +const qsScene = new URLSearchParams(window.location.search).get('scene') +const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main +playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates'] +playgroundGlobalUiState.selected = qsScene ?? 'main' -window.THREE = THREE - -const gui = new GUI() - -// initial values -const params = { - skipQs: '', - version: globalThis.includedVersions.sort((a, b) => { - const s = (x) => { - const parts = x.split('.') - return +parts[0] + (+parts[1]) - } - return s(a) - s(b) - }).at(-1), - block: '', - metadata: 0, - supportBlock: false, - entity: '', - removeEntity () { - this.entity = '' - }, - entityRotate: false, - camera: '', - playSound () { }, - blockIsomorphicRenderBundle () { }, - modelVariant: 0 -} - -const qs = new URLSearchParams(window.location.search) -for (const [key, value] of qs.entries()) { - const parsed = /^-?\d+$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value - params[key] = parsed -} -const setQs = () => { - const newQs = new URLSearchParams() - for (const [key, value] of Object.entries(params)) { - if (!value || typeof value === 'function' || params.skipQs.includes(key)) continue - newQs.set(key, value) - } - window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`) -} - -let ignoreResize = false - -async function main () { - let continuousRender = false - - const { version } = params - await window._LOAD_MC_DATA() - // temporary solution until web worker is here, cache data for faster reloads - // const globalMcData = window['mcData'] - // if (!globalMcData['version']) { - // const major = toMajorVersion(version) - // const sessionKey = `mcData-${major}` - // if (sessionStorage[sessionKey]) { - // Object.assign(globalMcData, JSON.parse(sessionStorage[sessionKey])) - // } else { - // if (sessionStorage.length > 1) sessionStorage.clear() - // try { - // sessionStorage[sessionKey] = JSON.stringify(Object.fromEntries(Object.entries(globalMcData).filter(([ver]) => ver.startsWith(major)))) - // } catch { } - // } - // } - - const mcData: IndexedData = require('minecraft-data')(version) - window['loadedData'] = mcData - - gui.add(params, 'version', globalThis.includedVersions) - gui.add(params, 'block', mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b))) - const metadataGui = gui.add(params, 'metadata') - gui.add(params, 'modelVariant') - gui.add(params, 'supportBlock') - gui.add(params, 'entity', mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b))).listen() - gui.add(params, 'removeEntity') - gui.add(params, 'entityRotate') - gui.add(params, 'skipQs') - gui.add(params, 'playSound') - gui.add(params, 'blockIsomorphicRenderBundle') - gui.open(false) - let metadataFolder = gui.addFolder('metadata') - // let entityRotationFolder = gui.addFolder('entity metadata') - - const Chunk = ChunkLoader(version) - const Block = BlockLoader(version) - // const data = await fetch('smallhouse1.schem').then(r => r.arrayBuffer()) - // const schem = await Schematic.read(Buffer.from(data), version) - - const viewDistance = 0 - const targetPos = new Vec3(2, 90, 2) - - const World = WorldLoader(version) - - // const diamondSquare = require('diamond-square')({ version, seed: Math.floor(Math.random() * Math.pow(2, 31)) }) - - //@ts-expect-error - const chunk1 = new Chunk() - //@ts-expect-error - const chunk2 = new Chunk() - chunk1.setBlockStateId(targetPos, 34) - chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 34) - //@ts-expect-error - const world = new World((chunkX, chunkZ) => { - // if (chunkX === 0 && chunkZ === 0) return chunk1 - // if (chunkX === 1 && chunkZ === 0) return chunk2 - //@ts-expect-error - const chunk = new Chunk() - return chunk - }) - - // await schem.paste(world, new Vec3(0, 60, 0)) - - const worldView = new WorldDataEmitter(world, viewDistance, targetPos) - - // 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) - document.body.appendChild(renderer.domElement) - - // Create viewer - const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false, }) - viewer.world.blockstatesModels = blockstatesModels - viewer.entities.setDebugMode('basic') - viewer.setVersion(version) - viewer.entities.onSkinUpdate = () => { - viewer.render() - } - viewer.world.mesherConfig.enableLighting = false - - viewer.listen(worldView) - // Load chunks - await worldView.init(targetPos) - window['worldView'] = worldView - window['viewer'] = viewer - - params.blockIsomorphicRenderBundle = () => { - const canvas = renderer.domElement - const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one') - const sizeRaw = prompt('Size', '512') - if (!sizeRaw) return - const size = parseInt(sizeRaw, 10) - // const size = 512 - - ignoreResize = true - canvas.width = size - canvas.height = size - renderer.setSize(size, size) - - //@ts-expect-error - viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10) - viewer.scene.background = null - - const rad = THREE.MathUtils.degToRad(-120) - viewer.directionalLight.position.set( - Math.cos(rad), - Math.sin(rad), - 0.2 - ).normalize() - viewer.directionalLight.intensity = 1 - - const cameraPos = targetPos.offset(2, 2, 2) - const pitch = THREE.MathUtils.degToRad(-30) - const yaw = THREE.MathUtils.degToRad(45) - viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') - // viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5) - viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1) - - const allBlocks = mcData.blocksArray.map(b => b.name) - // const allBlocks = ['stone', 'warped_slab'] - - let blockCount = 1 - let blockName = allBlocks[0] - - const updateBlock = () => { - // viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId) - params.block = blockName - // todo cleanup (introduce getDefaultState) - onUpdate.block() - applyChanges(false, true) - } - void viewer.waitForChunksToRender().then(async () => { - // wait for next macro task - await new Promise(resolve => { - setTimeout(resolve, 0) - }) - if (onlyCurrent) { - viewer.render() - onWorldUpdate() - } else { - // will be called on every render update - viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate) - updateBlock() - } - }) - - const zip = new JSZip() - zip.file('description.txt', 'Generated with prismarine-viewer') - - const end = async () => { - // download zip file - - const a = document.createElement('a') - const blob = await zip.generateAsync({ type: 'blob' }) - const dataUrlZip = URL.createObjectURL(blob) - a.href = dataUrlZip - a.download = 'blocks_render.zip' - a.click() - URL.revokeObjectURL(dataUrlZip) - console.log('end') - - viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate) - } - - async function onWorldUpdate () { - // await new Promise(resolve => { - // setTimeout(resolve, 50) - // }) - const dataUrl = canvas.toDataURL('image/png') - - zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true }) - - if (onlyCurrent) { - end() - } else { - nextBlock() - } - } - const nextBlock = async () => { - blockName = allBlocks[blockCount++] - console.log(allBlocks.length, '/', blockCount, blockName) - if (blockCount % 5 === 0) { - await new Promise(resolve => { - setTimeout(resolve, 100) - }) - } - if (blockName) { - updateBlock() - } else { - end() - } - } - } - - const controls = new OrbitControls(viewer.camera, renderer.domElement) - 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) - controls.update() - - let blockProps = {} - const entityOverrides = {} - const getBlock = () => { - return mcData.blocksByName[params.block || 'air'] - } - - const entityUpdateShared = () => { - viewer.entities.clear() - if (!params.entity) return - worldView.emit('entity', { - id: 'id', name: params.entity, pos: targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: localStorage.testUsername, yaw: Math.PI, pitch: 0 - }) - const enableSkeletonDebug = (obj) => { - const { children, isSkeletonHelper } = obj - if (!Array.isArray(children)) return - if (isSkeletonHelper) { - obj.visible = true - return - } - for (const child of children) { - if (typeof child === 'object') enableSkeletonDebug(child) - } - } - enableSkeletonDebug(viewer.entities.entities['id']) - setTimeout(() => { - viewer.render() - }, TWEEN_DURATION) - } - - const onUpdate = { - version (initialUpdate) { - // if (initialUpdate) return - // viewer.world.texturesVersion = params.version - // viewer.world.updateTexturesData() - // todo warning - }, - block () { - blockProps = {} - metadataFolder.destroy() - const block = mcData.blocksByName[params.block] - if (!block) return - console.log('block', block.name) - const props = new Block(block.id, 0, 0).getProperties() - //@ts-expect-error - const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {} - metadataFolder = gui.addFolder('metadata') - if (states) { - for (const state of states) { - let defaultValue: string | number | boolean - if (state.values) { // int, enum - defaultValue = state.values[0] - } else { - switch (state.type) { - case 'bool': - defaultValue = false - break - case 'int': - defaultValue = 0 - break - case 'direction': - defaultValue = 'north' - break - - default: - continue - } - } - blockProps[state.name] = defaultValue - if (state.values) { - metadataFolder.add(blockProps, state.name, state.values) - } else { - metadataFolder.add(blockProps, state.name) - } - } - } else { - for (const [name, value] of Object.entries(props)) { - blockProps[name] = value - metadataFolder.add(blockProps, name) - } - } - console.log('props', blockProps) - metadataFolder.open() - }, - entity () { - continuousRender = params.entity === 'player' - entityUpdateShared() - if (!params.entity) return - if (params.entity === 'player') { - viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, true, true) - viewer.entities.playAnimation('id', 'running') - } - // let prev = false - // setInterval(() => { - // viewer.entities.playAnimation('id', prev ? 'running' : 'idle') - // prev = !prev - // }, 1000) - - EntityMesh.getStaticData(params.entity) - // entityRotationFolder.destroy() - // entityRotationFolder = gui.addFolder('entity metadata') - // entityRotationFolder.add(params, 'entityRotate') - // entityRotationFolder.open() - }, - supportBlock () { - viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0) - }, - modelVariant () { - viewer.world.mesherConfig.debugModelVariant = params.modelVariant === 0 ? undefined : [params.modelVariant] - } - } - - - const applyChanges = (metadataUpdate = false, skipQs = false) => { - const blockId = getBlock()?.id - let block: BlockLoader.Block - if (metadataUpdate) { - block = new Block(blockId, 0, params.metadata) - Object.assign(blockProps, block.getProperties()) - for (const _child of metadataFolder.children) { - const child = _child as import('lil-gui').Controller - child.updateDisplay() - } - } else { - try { - block = Block.fromProperties(blockId ?? -1, blockProps, 0) - } catch (err) { - console.error(err) - block = Block.fromStateId(0, 0) - } - } - - //@ts-expect-error - viewer.setBlockStateId(targetPos, block.stateId) - console.log('up stateId', block.stateId) - params.metadata = block.metadata - metadataGui.updateDisplay() - if (!skipQs) { - setQs() - } - } - gui.onChange(({ property, object }) => { - if (object === params) { - if (property === 'camera') return - onUpdate[property]?.() - applyChanges(property === 'metadata') - } else { - applyChanges() - } - }) - void viewer.waitForChunksToRender().then(async () => { - // TODO! - await new Promise(resolve => { - setTimeout(resolve, 50) - }) - for (const update of Object.values(onUpdate)) { - update(true) - } - applyChanges() - gui.openAnimated() - }) - - const animate = () => { - // if (controls) controls.update() - // worldView.updatePosition(controls.target) - viewer.render() - // window.requestAnimationFrame(animate) - } - viewer.world.renderUpdateEmitter.addListener('update', () => { - animate() - }) - animate() - - // #region camera rotation param - if (params.camera) { - const [x, y] = params.camera.split(',') - viewer.camera.rotation.set(parseFloat(x), parseFloat(y), 0, 'ZYX') - controls.update() - console.log(viewer.camera.rotation.x, parseFloat(x)) - } - const throttledCamQsUpdate = _.throttle(() => { - const { camera } = viewer - // params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}` - setQs() - }, 200) - controls.addEventListener('change', () => { - throttledCamQsUpdate() - animate() - }) - // #endregion - - const continuousUpdate = () => { - if (continuousRender) { - animate() - } - requestAnimationFrame(continuousUpdate) - } - continuousUpdate() - - window.onresize = () => { - if (ignoreResize) return - // const vec3 = new THREE.Vector3() - // vec3.set(-1, -1, -1).unproject(viewer.camera) - // console.log(vec3) - // box.position.set(vec3.x, vec3.y, vec3.z-1) - - const { camera } = viewer - viewer.camera.aspect = window.innerWidth / window.innerHeight - viewer.camera.updateProjectionMatrix() - renderer.setSize(window.innerWidth, window.innerHeight) - - animate() - } - window.dispatchEvent(new Event('resize')) - - params.playSound = () => { - viewer.playSound(targetPos, 'button_click.mp3') - } - addEventListener('keydown', (e) => { - if (e.code === 'KeyE') { - params.playSound() - } - }, { capture: true }) -} -main() +const scene = new Scene() +globalThis.scene = scene diff --git a/prismarine-viewer/examples/playgroundUi.tsx b/prismarine-viewer/examples/playgroundUi.tsx new file mode 100644 index 000000000..0de20d62d --- /dev/null +++ b/prismarine-viewer/examples/playgroundUi.tsx @@ -0,0 +1,129 @@ +import { renderToDom } from '@zardoy/react-util' +import { useEffect } from 'react' +import { proxy, useSnapshot } from 'valtio' +import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface' +import { css } from '@emotion/css' +import { Vec3 } from 'vec3' + +export const playgroundGlobalUiState = proxy({ + scenes: [] as string[], + selected: '' +}) + +renderToDom() + +function Playground () { + useEffect(() => { + const style = document.createElement('style') + style.innerHTML = /* css */ ` + .lil-gui { + top: 40px !important; + right: 0 !important; + } + ` + document.body.appendChild(style) + return () => { + style.remove() + } + }, []) + + return
+ + +
+} + +function SceneSelector () { + const { scenes, selected } = useSnapshot(playgroundGlobalUiState) + + return
+ {scenes.map(scene =>
{ + const qs = new URLSearchParams(window.location.search) + qs.set('scene', scene) + location.search = qs.toString() + }} + >{scene}
)} +
+} + +const Controls = () => { + // todo setting + const usingTouch = navigator.maxTouchPoints > 0 + + useEffect(() => { + window.addEventListener('touchstart', (e) => { + e.preventDefault() + }) + + const pressedKeys = new Set() + useInterfaceState.setState({ + isFlying: false, + uiCustomization: { + touchButtonSize: 40, + }, + updateCoord ([coord, state]) { + const vec3 = new Vec3(0, 0, 0) + vec3[coord] = state + let key: string | undefined + if (vec3.z < 0) key = 'KeyW' + if (vec3.z > 0) key = 'KeyS' + if (vec3.y > 0) key = 'Space' + if (vec3.y < 0) key = 'ShiftLeft' + if (vec3.x < 0) key = 'KeyA' + if (vec3.x > 0) key = 'KeyD' + if (key) { + if (!pressedKeys.has(key)) { + pressedKeys.add(key) + window.dispatchEvent(new KeyboardEvent('keydown', { code: key })) + } + } + for (const k of pressedKeys) { + if (k !== key) { + window.dispatchEvent(new KeyboardEvent('keyup', { code: k })) + pressedKeys.delete(k) + } + } + } + }) + }, []) + + if (!usingTouch) return null + return ( +
div { + pointer-events: auto; + } + `} + > + +
+ +
+ ) +} diff --git a/prismarine-viewer/examples/scenes/entities.ts b/prismarine-viewer/examples/scenes/entities.ts new file mode 100644 index 000000000..11b1591af --- /dev/null +++ b/prismarine-viewer/examples/scenes/entities.ts @@ -0,0 +1,36 @@ +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' +import { WorldRendererThree } from '../../viewer/lib/worldrendererThree' + +export default class extends BasePlaygroundScene { + continuousRender = true + + override initGui (): void { + this.params = { + starfield: false, + entity: 'player', + count: 4 + } + } + + override renderFinish (): void { + if (this.params.starfield) { + ;(viewer.world as WorldRendererThree).scene.background = new THREE.Color(0x00_00_00) + ;(viewer.world as WorldRendererThree).starField.enabled = true + ;(viewer.world as WorldRendererThree).starField.addToScene() + } + + for (let i = 0; i < this.params.count; i++) { + for (let j = 0; j < this.params.count; j++) { + for (let k = 0; k < this.params.count; k++) { + viewer.entities.update({ + id: i * 1000 + j * 100 + k, + name: this.params.entity, + pos: this.targetPos.offset(i, j, k) + } as any, {}) + } + } + } + } +} diff --git a/prismarine-viewer/examples/scenes/floorRandom.ts b/prismarine-viewer/examples/scenes/floorRandom.ts new file mode 100644 index 000000000..249842fb0 --- /dev/null +++ b/prismarine-viewer/examples/scenes/floorRandom.ts @@ -0,0 +1,33 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class RailsCobwebScene extends BasePlaygroundScene { + viewDistance = 5 + continuousRender = true + + override initGui (): void { + this.params = { + squareSize: 50 + } + + super.initGui() + } + + setupWorld () { + const squareSize = this.params.squareSize ?? 30 + const maxSquareSize = this.viewDistance * 16 * 2 + if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`) + // const fullBlocks = loadedData.blocksArray.map(x => x.name) + const fullBlocks = loadedData.blocksArray.filter(block => { + const b = this.Block.fromStateId(block.defaultState!, 0) + if (b.shapes?.length !== 1) return false + const shape = b.shapes[0] + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + }) + for (let x = -squareSize; x <= squareSize; x++) { + for (let z = -squareSize; z <= squareSize; z++) { + const i = Math.abs(x + z) * squareSize + worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(fullBlocks[i % fullBlocks.length].defaultState!, 0)) + } + } + } +} diff --git a/prismarine-viewer/examples/scenes/frequentUpdates.ts b/prismarine-viewer/examples/scenes/frequentUpdates.ts new file mode 100644 index 000000000..6b0718f49 --- /dev/null +++ b/prismarine-viewer/examples/scenes/frequentUpdates.ts @@ -0,0 +1,85 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class extends BasePlaygroundScene { + viewDistance = 5 + continuousRender = true + + override initGui (): void { + this.params = { + squareSize: 50 + } + + super.initGui() + } + + setupTimer () { + // const limit = 1000 + // const limit = 100 + const limit = 1 + const updatedChunks = new Set() + const updatedBlocks = new Set() + 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) + } + + 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}`) + // const fullBlocks = loadedData.blocksArray.map(x => x.name) + for (let x = -squareSize; x <= squareSize; x++) { + for (let z = -squareSize; z <= squareSize; z++) { + const i = Math.abs(x + z) * squareSize + worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(1, 0)) + } + } + let done = false + viewer.world.renderUpdateEmitter.on('update', () => { + if (!viewer.world.allChunksFinished || done) return + done = true + this.setupTimer() + }) + } +} diff --git a/prismarine-viewer/examples/scenes/index.ts b/prismarine-viewer/examples/scenes/index.ts new file mode 100644 index 000000000..a526686e3 --- /dev/null +++ b/prismarine-viewer/examples/scenes/index.ts @@ -0,0 +1,9 @@ +// export { default as rotation } from './rotation' +export { default as main } from './main' +export { default as railsCobweb } from './railsCobweb' +export { default as floorRandom } from './floorRandom' +export { default as lightingStarfield } from './lightingStarfield' +export { default as transparencyIssue } from './transparencyIssue' +export { default as rotationIssue } from './rotationIssue' +export { default as entities } from './entities' +export { default as frequentUpdates } from './frequentUpdates' diff --git a/prismarine-viewer/examples/scenes/lightingStarfield.ts b/prismarine-viewer/examples/scenes/lightingStarfield.ts new file mode 100644 index 000000000..4b259b897 --- /dev/null +++ b/prismarine-viewer/examples/scenes/lightingStarfield.ts @@ -0,0 +1,39 @@ +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' +import { WorldRendererThree } from '../../viewer/lib/worldrendererThree' + +export default class extends BasePlaygroundScene { + continuousRender = true + + override setupWorld (): void { + viewer.world.mesherConfig.enableLighting = true + viewer.world.mesherConfig.skyLight = 0 + this.addWorldBlock(0, 0, 0, 'stone') + this.addWorldBlock(0, 0, 1, 'stone') + this.addWorldBlock(1, 0, 0, 'stone') + this.addWorldBlock(1, 0, 1, 'stone') + // chess like + worldView?.world.setBlockLight(this.targetPos.offset(0, 1, 0), 15) + worldView?.world.setBlockLight(this.targetPos.offset(0, 1, 1), 0) + worldView?.world.setBlockLight(this.targetPos.offset(1, 1, 0), 0) + worldView?.world.setBlockLight(this.targetPos.offset(1, 1, 1), 15) + } + + override renderFinish (): void { + viewer.scene.background = new THREE.Color(0x00_00_00) + // starfield and test entities + ;(viewer.world as WorldRendererThree).starField.enabled = true + ;(viewer.world as WorldRendererThree).starField.addToScene() + viewer.entities.update({ + id: 0, + name: 'player', + pos: this.targetPos.clone() + } as any, {}) + viewer.entities.update({ + id: 1, + name: 'creeper', + pos: this.targetPos.offset(1, 0, 0) + } as any, {}) + } +} diff --git a/prismarine-viewer/examples/scenes/main.ts b/prismarine-viewer/examples/scenes/main.ts new file mode 100644 index 000000000..7b1b89b96 --- /dev/null +++ b/prismarine-viewer/examples/scenes/main.ts @@ -0,0 +1,314 @@ +// eslint-disable-next-line import/no-named-as-default +import GUI, { Controller } from 'lil-gui' +import * as THREE from 'three' +import JSZip from 'jszip' +import { BasePlaygroundScene } from '../baseScene' +import { TWEEN_DURATION } from '../../viewer/lib/entities' +import { EntityMesh } from '../../viewer/lib/entity/EntityMesh' + +class MainScene extends BasePlaygroundScene { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor (...args) { + //@ts-expect-error + super(...args) + } + + override initGui (): void { + // initial values + this.params = { + version: globalThis.includedVersions.at(-1), + skipQs: '', + block: '', + metadata: 0, + supportBlock: false, + entity: '', + removeEntity () { + this.entity = '' + }, + entityRotate: false, + camera: '', + playSound () { }, + blockIsomorphicRenderBundle () { }, + modelVariant: 0 + } + this.metadataGui = this.gui.add(this.params, 'metadata') + this.paramOptions = { + version: { + options: globalThis.includedVersions, + hide: false + }, + block: { + options: mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b)) + }, + entity: { + options: mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b)) + }, + camera: { + hide: true, + } + } + super.initGui() + } + + blockProps = {} + metadataFolder: GUI | undefined + metadataGui: Controller + + override onParamUpdate = { + version () { + // if (initialUpdate) return + // viewer.world.texturesVersion = params.version + // viewer.world.updateTexturesData() + // todo warning + }, + block: () => { + this.blockProps = {} + this.metadataFolder?.destroy() + const block = mcData.blocksByName[this.params.block] + if (!block) return + console.log('block', block.name) + const props = new this.Block(block.id, 0, 0).getProperties() + const { states } = mcData.blocksByStateId[this.getBlock()?.minStateId] ?? {} + this.metadataFolder = this.gui.addFolder('metadata') + if (states) { + for (const state of states) { + let defaultValue: string | number | boolean + if (state.values) { // int, enum + defaultValue = state.values[0] + } else { + switch (state.type) { + case 'bool': + defaultValue = false + break + case 'int': + defaultValue = 0 + break + case 'direction': + defaultValue = 'north' + break + + default: + continue + } + } + this.blockProps[state.name] = defaultValue + if (state.values) { + this.metadataFolder.add(this.blockProps, state.name, state.values) + } else { + this.metadataFolder.add(this.blockProps, state.name) + } + } + } else { + for (const [name, value] of Object.entries(props)) { + this.blockProps[name] = value + this.metadataFolder.add(this.blockProps, name) + } + } + console.log('props', this.blockProps) + this.metadataFolder.open() + }, + entity: () => { + this.continuousRender = this.params.entity === 'player' + this.entityUpdateShared() + if (!this.params.entity) return + if (this.params.entity === 'player') { + viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, true, true) + viewer.entities.playAnimation('id', 'running') + } + // let prev = false + // setInterval(() => { + // viewer.entities.playAnimation('id', prev ? 'running' : 'idle') + // prev = !prev + // }, 1000) + + EntityMesh.getStaticData(this.params.entity) + // entityRotationFolder.destroy() + // entityRotationFolder = gui.addFolder('entity metadata') + // entityRotationFolder.add(params, 'entityRotate') + // entityRotationFolder.open() + }, + supportBlock: () => { + viewer.setBlockStateId(this.targetPos.offset(0, -1, 0), this.params.supportBlock ? 1 : 0) + }, + modelVariant: () => { + viewer.world.mesherConfig.debugModelVariant = this.params.modelVariant === 0 ? undefined : [this.params.modelVariant] + } + } + + entityUpdateShared () { + viewer.entities.clear() + if (!this.params.entity) return + worldView!.emit('entity', { + id: 'id', name: this.params.entity, pos: this.targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: localStorage.testUsername, yaw: Math.PI, pitch: 0 + }) + const enableSkeletonDebug = (obj) => { + const { children, isSkeletonHelper } = obj + if (!Array.isArray(children)) return + if (isSkeletonHelper) { + obj.visible = true + return + } + for (const child of children) { + if (typeof child === 'object') enableSkeletonDebug(child) + } + } + enableSkeletonDebug(viewer.entities.entities['id']) + setTimeout(() => { + viewer.render() + }, TWEEN_DURATION) + } + + blockIsomorphicRenderBundle () { + const { renderer } = viewer + + const canvas = renderer.domElement + const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one') + const sizeRaw = prompt('Size', '512') + if (!sizeRaw) return + const size = parseInt(sizeRaw, 10) + // const size = 512 + + this.ignoreResize = true + canvas.width = size + canvas.height = size + renderer.setSize(size, size) + + //@ts-expect-error + viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10) + viewer.scene.background = null + + const rad = THREE.MathUtils.degToRad(-120) + viewer.directionalLight.position.set( + Math.cos(rad), + Math.sin(rad), + 0.2 + ).normalize() + viewer.directionalLight.intensity = 1 + + const cameraPos = this.targetPos.offset(2, 2, 2) + const pitch = THREE.MathUtils.degToRad(-30) + const yaw = THREE.MathUtils.degToRad(45) + viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') + // viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5) + viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1) + + const allBlocks = mcData.blocksArray.map(b => b.name) + // const allBlocks = ['stone', 'warped_slab'] + + let blockCount = 1 + let blockName = allBlocks[0] + + const updateBlock = () => { + // viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId) + this.params.block = blockName + // todo cleanup (introduce getDefaultState) + // TODO + // onUpdate.block() + // applyChanges(false, true) + } + void viewer.waitForChunksToRender().then(async () => { + // wait for next macro task + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + if (onlyCurrent) { + viewer.render() + onWorldUpdate() + } else { + // will be called on every render update + viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate) + updateBlock() + } + }) + + const zip = new JSZip() + zip.file('description.txt', 'Generated with prismarine-viewer') + + const end = async () => { + // download zip file + + const a = document.createElement('a') + const blob = await zip.generateAsync({ type: 'blob' }) + const dataUrlZip = URL.createObjectURL(blob) + a.href = dataUrlZip + a.download = 'blocks_render.zip' + a.click() + URL.revokeObjectURL(dataUrlZip) + console.log('end') + + viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate) + } + + async function onWorldUpdate () { + // await new Promise(resolve => { + // setTimeout(resolve, 50) + // }) + const dataUrl = canvas.toDataURL('image/png') + + zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true }) + + if (onlyCurrent) { + end() + } else { + nextBlock() + } + } + const nextBlock = async () => { + blockName = allBlocks[blockCount++] + console.log(allBlocks.length, '/', blockCount, blockName) + if (blockCount % 5 === 0) { + await new Promise(resolve => { + setTimeout(resolve, 100) + }) + } + if (blockName) { + updateBlock() + } else { + end() + } + } + } + + getBlock () { + return mcData.blocksByName[this.params.block || 'air'] + } + + // applyChanges (metadataUpdate = false, skipQs = false) { + override onParamsUpdate (paramName: string, object: any) { + const metadataUpdate = paramName === 'metadata' + + const blockId = this.getBlock()?.id + let block: import('prismarine-block').Block + if (metadataUpdate) { + block = new this.Block(blockId, 0, this.params.metadata) + Object.assign(this.blockProps, block.getProperties()) + for (const _child of this.metadataFolder!.children) { + const child = _child as import('lil-gui').Controller + child.updateDisplay() + } + } else { + try { + block = this.Block.fromProperties(blockId ?? -1, this.blockProps, 0) + } catch (err) { + console.error(err) + block = this.Block.fromStateId(0, 0) + } + } + + viewer.setBlockStateId(this.targetPos, block.stateId!) + console.log('up stateId', block.stateId) + this.params.metadata = block.metadata + this.metadataGui.updateDisplay() + } + + override renderFinish () { + for (const update of Object.values(this.onParamUpdate)) { + // update(true) + update() + } + this.onParamsUpdate('', {}) + this.gui.openAnimated() + } +} + +export default MainScene diff --git a/prismarine-viewer/examples/scenes/railsCobweb.ts b/prismarine-viewer/examples/scenes/railsCobweb.ts new file mode 100644 index 000000000..bc1c271a3 --- /dev/null +++ b/prismarine-viewer/examples/scenes/railsCobweb.ts @@ -0,0 +1,14 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class RailsCobwebScene extends BasePlaygroundScene { + setupWorld () { + this.addWorldBlock(0, 0, 0, 'cobweb') + this.addWorldBlock(0, -1, 0, 'cobweb') + this.addWorldBlock(1, -1, 0, 'cobweb') + this.addWorldBlock(1, 0, 0, 'cobweb') + + this.addWorldBlock(0, 0, 1, 'powered_rail', { shape: 'north_south', waterlogged: false }) + this.addWorldBlock(0, 0, 2, 'powered_rail', { shape: 'ascending_south', waterlogged: false }) + this.addWorldBlock(0, 1, 3, 'powered_rail', { shape: 'north_south', waterlogged: false }) + } +} diff --git a/prismarine-viewer/examples/scenes/rotationIssue.ts b/prismarine-viewer/examples/scenes/rotationIssue.ts new file mode 100644 index 000000000..2c56876a3 --- /dev/null +++ b/prismarine-viewer/examples/scenes/rotationIssue.ts @@ -0,0 +1,7 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class RotationIssueScene extends BasePlaygroundScene { + setupWorld () { + // todo + } +} diff --git a/prismarine-viewer/examples/scenes/transparencyIssue.ts b/prismarine-viewer/examples/scenes/transparencyIssue.ts new file mode 100644 index 000000000..9ce1b967a --- /dev/null +++ b/prismarine-viewer/examples/scenes/transparencyIssue.ts @@ -0,0 +1,11 @@ +import { BasePlaygroundScene } from '../baseScene' + +export default class extends BasePlaygroundScene { + setupWorld () { + this.addWorldBlock(0, 0, 0, 'water') + this.addWorldBlock(0, 1, 0, 'lime_stained_glass') + this.addWorldBlock(0, 0, -1, 'lime_stained_glass') + this.addWorldBlock(0, -1, 0, 'lime_stained_glass') + this.addWorldBlock(0, -1, -1, 'stone') + } +} diff --git a/prismarine-viewer/viewer/lib/entities.ts b/prismarine-viewer/viewer/lib/entities.ts index c7476f6c3..29bdc97f4 100644 --- a/prismarine-viewer/viewer/lib/entities.ts +++ b/prismarine-viewer/viewer/lib/entities.ts @@ -378,6 +378,12 @@ export class Entities extends EventEmitter { const playerObject = new PlayerObject() as PlayerObjectType playerObject.position.set(0, 16, 0) + // fix issues with starfield + playerObject.traverse((obj) => { + if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) { + obj.material.transparent = true + } + }) //@ts-expect-error wrapper.add(playerObject) const scale = 1 / 16 diff --git a/prismarine-viewer/viewer/lib/mesher/models.ts b/prismarine-viewer/viewer/lib/mesher/models.ts index e63a92ea4..92f4b0b5d 100644 --- a/prismarine-viewer/viewer/lib/mesher/models.ts +++ b/prismarine-viewer/viewer/lib/mesher/models.ts @@ -413,7 +413,8 @@ export function getSectionGeometry (sx, sy, sz, world: World) { signs: {}, // isFull: true, highestBlocks: {}, // todo migrate to map for 2% boost perf - hadErrors: false + hadErrors: false, + blocksCount: 0 } const cursor = new Vec3(0, 0, 0) @@ -472,8 +473,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) { delayedRender.push(() => { renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr) }) + attr.blocksCount++ } else if (block.name === 'lava') { renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr) + attr.blocksCount++ } if (block.name !== 'water' && block.name !== 'lava' && !invisibleBlocks.has(block.name)) { // cache @@ -552,7 +555,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { } } } - + if (part > 0) attr.blocksCount++ } } } diff --git a/prismarine-viewer/viewer/lib/mesher/shared.ts b/prismarine-viewer/viewer/lib/mesher/shared.ts index 30b62c45d..07b667471 100644 --- a/prismarine-viewer/viewer/lib/mesher/shared.ts +++ b/prismarine-viewer/viewer/lib/mesher/shared.ts @@ -32,4 +32,5 @@ export type MesherGeometryOutput = { // isFull: boolean highestBlocks: Record hadErrors: boolean + blocksCount: number } diff --git a/prismarine-viewer/viewer/lib/ui/newStats.ts b/prismarine-viewer/viewer/lib/ui/newStats.ts new file mode 100644 index 000000000..5c11d7c73 --- /dev/null +++ b/prismarine-viewer/viewer/lib/ui/newStats.ts @@ -0,0 +1,45 @@ +/* eslint-disable unicorn/prefer-dom-node-text-content */ +const rightOffset = 0 + +const stats = {} + +let lastY = 20 +export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) => { + const pane = document.createElement('div') + pane.id = 'fps-counter' + pane.style.position = 'fixed' + pane.style.top = `${y}px` + pane.style.right = `${x}px` + // gray bg + pane.style.backgroundColor = 'rgba(0, 0, 0, 0.7)' + pane.style.color = 'white' + pane.style.padding = '2px' + pane.style.fontFamily = 'monospace' + pane.style.fontSize = '12px' + pane.style.zIndex = '10000' + pane.style.pointerEvents = 'none' + document.body.appendChild(pane) + stats[id] = pane + if (y === 0) { // otherwise it's a custom position + // rightOffset += width + lastY += 20 + } + + return { + updateText (text: string) { + pane.innerText = text + } + } +} + +export const updateStatText = (id, text) => { + if (!stats[id]) return + stats[id].innerText = text +} + +if (typeof customEvents !== 'undefined') { + customEvents.on('gameLoaded', () => { + const chunksLoaded = addNewStat('chunks-loaded', 80, 0, 0) + const chunksTotal = addNewStat('chunks-read', 80, 0, 0) + }) +} diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index 7cd759e40..44366d862 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -8,6 +8,7 @@ import { Primitives } from './primitives' import { WorldRendererThree } from './worldrendererThree' import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon' import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer' +import { addNewStat } from './ui/newStats' export class Viewer { scene: THREE.Scene @@ -105,7 +106,7 @@ export class Viewer { const pos = cursorBlockRel(0, 1, 0).position const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlases, 'latest') const models = blockProvider.getAllResolvedModels0_1({ - name: 'furnace', + name: 'item_frame', properties: { // map: false } @@ -186,13 +187,12 @@ export class Viewer { addChunksBatchWaitTime = 200 - // todo type - listen (emitter: EventEmitter) { - emitter.on('entity', (e) => { + connect (worldEmitter: EventEmitter) { + worldEmitter.on('entity', (e) => { this.updateEntity(e) }) - emitter.on('primitive', (p) => { + worldEmitter.on('primitive', (p) => { // this.updatePrimitive(p) }) @@ -200,51 +200,51 @@ export class Viewer { timeout data } | null - emitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => { + worldEmitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => { this.world.worldConfig = worldConfig + const args = [x, z, chunk, isLightUpdate] if (!currentLoadChunkBatch) { // add a setting to use debounce instead currentLoadChunkBatch = { data: [], timeout: setTimeout(() => { for (const args of currentLoadChunkBatch!.data) { - //@ts-expect-error - this.addColumn(...args) + this.addColumn(...args as Parameters) } currentLoadChunkBatch = null }, this.addChunksBatchWaitTime) } } - currentLoadChunkBatch.data.push([x, z, chunk, isLightUpdate]) + currentLoadChunkBatch.data.push(args) }) // todo remove and use other architecture instead so data flow is clear - emitter.on('blockEntities', (blockEntities) => { + worldEmitter.on('blockEntities', (blockEntities) => { if (this.world instanceof WorldRendererThree) this.world.blockEntities = blockEntities }) - emitter.on('unloadChunk', ({ x, z }) => { + worldEmitter.on('unloadChunk', ({ x, z }) => { this.removeColumn(x, z) }) - emitter.on('blockUpdate', ({ pos, stateId }) => { + worldEmitter.on('blockUpdate', ({ pos, stateId }) => { this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId) }) - emitter.on('chunkPosUpdate', ({ pos }) => { + worldEmitter.on('chunkPosUpdate', ({ pos }) => { this.world.updateViewerPosition(pos) }) - emitter.on('renderDistance', (d) => { + worldEmitter.on('renderDistance', (d) => { this.world.viewDistance = d this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length this.world.allChunksFinished = Object.keys(this.world.finishedChunks).length === this.world.chunksLength }) - emitter.on('updateLight', ({ pos }) => { + worldEmitter.on('updateLight', ({ pos }) => { if (this.world instanceof WorldRendererThree) this.world.updateLight(pos.x, pos.z) }) - emitter.on('time', (timeOfDay) => { + worldEmitter.on('time', (timeOfDay) => { this.world.timeUpdated?.(timeOfDay) let skyLight = 15 @@ -265,7 +265,7 @@ export class Viewer { (this.world as WorldRendererThree).rerenderAllChunks?.() }) - emitter.emit('listening') + worldEmitter.emit('listening') } render () { diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 92a8ac4fb..114372f14 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -21,6 +21,7 @@ export class WorldDataEmitter extends EventEmitter { private eventListeners: Record = {} private readonly emitter: WorldDataEmitter keepChunksDistance = 0 + addWaitTime = 1 _handDisplay = false get handDisplay () { return this._handDisplay @@ -155,19 +156,23 @@ export class WorldDataEmitter extends EventEmitter { const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16)) this.lastPos.update(pos) - this._loadChunks(positions) + await this._loadChunks(positions) } - _loadChunks (positions: Vec3[], sliceSize = 5, waitTime = 0) { + async _loadChunks (positions: Vec3[], sliceSize = 5) { let i = 0 - const interval = setInterval(() => { - if (i >= positions.length) { - clearInterval(interval) - return - } - void this.loadChunk(positions[i]) - i++ - }, 1) + const promises = [] as Array> + return new Promise(resolve => { + const interval = setInterval(() => { + if (i >= positions.length) { + clearInterval(interval) + void Promise.all(promises).then(() => resolve()) + return + } + promises.push(this.loadChunk(positions[i])) + i++ + }, this.addWaitTime) + }) } readdDebug () { @@ -247,7 +252,7 @@ export class WorldDataEmitter extends EventEmitter { return undefined! }).filter(a => !!a) this.lastPos.update(pos) - this._loadChunks(positions) + void this._loadChunks(positions) } else { this.emitter.emit('chunkPosUpdate', { pos }) // todo-low this.lastPos.update(pos) diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts index 92fc96819..f7f37d1e7 100644 --- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -17,6 +17,7 @@ import { buildCleanupDecorator } from './cleanupDecorator' import { MesherGeometryOutput, defaultMesherConfig } from './mesher/shared' import { chunkPos } from './simpleUtils' import { HandItemBlock } from './holdingBlock' +import { updateStatText } from './ui/newStats' function mod (x, n) { return ((x % n) + n) % n @@ -101,12 +102,18 @@ export abstract class WorldRendererCommon z: number } neighborChunkUpdates = true + lastChunkDistance = 0 abstract outputFormat: 'threeJs' | 'webgpu' constructor (public config: WorldRendererConfig) { // this.initWorkers(1) // preload script on page load this.snapshotInitialValues() + + this.renderUpdateEmitter.on('update', () => { + const loadedChunks = Object.keys(this.finishedChunks).length + updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance})`) + }) } snapshotInitialValues () { } @@ -131,6 +138,8 @@ export abstract class WorldRendererCommon this.highestBlocks[key] = highest } } + const chunkCoords = data.key.split(',').map(Number) + this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2]))) } if (data.type === 'sectionFinished') { // on after load & unload section if (!this.sectionsOutstanding.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`) diff --git a/prismarine-viewer/viewer/lib/worldrendererThree.ts b/prismarine-viewer/viewer/lib/worldrendererThree.ts index 17abfad95..b9a9427e4 100644 --- a/prismarine-viewer/viewer/lib/worldrendererThree.ts +++ b/prismarine-viewer/viewer/lib/worldrendererThree.ts @@ -10,6 +10,8 @@ import { chunkPos, sectionPos } from './simpleUtils' import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon' import { disposeObject } from './threeJsUtils' import HoldingBlock, { HandItemBlock } from './holdingBlock' +import { addNewStat } from './ui/newStats' +import { MesherGeometryOutput } from './mesher/shared' export class WorldRendererThree extends WorldRendererCommon { outputFormat = 'threeJs' as const @@ -25,6 +27,10 @@ export class WorldRendererThree extends WorldRendererCommon { return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0) } + get blocksRendered () { + return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0) + } + constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) { super(config) this.starField = new StarField(scene) @@ -36,6 +42,8 @@ export class WorldRendererThree extends WorldRendererCommon { this.holdingBlock.toBeRenderedItem = undefined } }) + + this.addDebugOverlay() } onHandItemSwitch (item: HandItemBlock | undefined) { @@ -65,9 +73,19 @@ export class WorldRendererThree extends WorldRendererCommon { } } + debugOverlayAdded = false + addDebugOverlay () { + if (this.debugOverlayAdded) return + this.debugOverlayAdded = true + const pane = addNewStat('debug-overlay') + setInterval(() => { + pane.updateText(`C: ${this.renderer.info.render.calls} TR: ${this.renderer.info.render.triangles} TE: ${this.renderer.info.memory.textures} F: ${this.tilesRendered} B: ${this.blocksRendered}`) + }, 100) + } + /** - * Optionally update data that are depedendent on the viewer position - */ + * Optionally update data that are depedendent on the viewer position + */ updatePosDataChunk (key: string) { const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16)) // sum of distances: x + y + z @@ -89,7 +107,7 @@ export class WorldRendererThree extends WorldRendererCommon { } // debugRecomputedDeletedObjects = 0 - handleWorkerMessage (data: any): void { + handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void { if (data.type !== 'geometry') return let object: THREE.Object3D = this.sectionObjects[data.key] if (object) { @@ -137,9 +155,9 @@ export class WorldRendererThree extends WorldRendererCommon { const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00) boxHelper.name = 'helper' object.add(boxHelper) - object.name = 'chunk' - //@ts-expect-error - object.tilesCount = data.geometry.positions.length / 3 / 4 + object.name = 'chunk'; + (object as any).tilesCount = data.geometry.positions.length / 3 / 4; + (object as any).blocksCount = data.geometry.blocksCount if (!this.config.showChunkBorders) { boxHelper.visible = false } diff --git a/src/devtools.ts b/src/devtools.ts index 1e9305065..16337c1d7 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -25,6 +25,24 @@ Object.defineProperty(window, 'debugSceneChunks', { window.len = (obj) => Object.keys(obj).length +customEvents.on('gameLoaded', () => { + bot._client.on('packet', (data, { name }) => { + if (sessionStorage.ignorePackets?.includes(name)) { + console.log('ignoring packet', name) + const oldEmit = bot._client.emit + let i = 0 + // ignore next 3 emits + //@ts-expect-error + bot._client.emit = (...args) => { + if (i++ === 3) { + oldEmit.apply(bot._client, args) + bot._client.emit = oldEmit + } + } + } + }) +}) + window.inspectPacket = (packetName, full = false) => { const listener = (...args) => console.log('packet', packetName, full ? args : args[0]) const attach = () => { diff --git a/src/index.ts b/src/index.ts index b26619c33..cf894bd7e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -709,7 +709,7 @@ async function connect (connectOptions: ConnectOptions) { // Link WorldDataEmitter and Viewer - viewer.listen(worldView) + viewer.connect(worldView) worldView.listenToBot(bot) void worldView.init(bot.entity.position)