From a4c86d707baf74b34345f16b1475fad63595d3ec Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 12 Dec 2024 09:18:54 +0300 Subject: [PATCH] fix: update changed blocks quicker in the world by using a dedicated mesher thread reserved for blocks updates only --- prismarine-viewer/viewer/lib/mesher/mesher.ts | 2 +- .../viewer/lib/worldrendererCommon.ts | 52 +++++++--- .../viewer/lib/worldrendererThree.ts | 55 +++++++++-- src/optionsGuiScheme.tsx | 1 - src/react/DebugOverlay.tsx | 15 +-- src/worldInteractions.ts | 94 ++++++------------- 6 files changed, 121 insertions(+), 98 deletions(-) diff --git a/prismarine-viewer/viewer/lib/mesher/mesher.ts b/prismarine-viewer/viewer/lib/mesher/mesher.ts index b38f04274..7c120abc1 100644 --- a/prismarine-viewer/viewer/lib/mesher/mesher.ts +++ b/prismarine-viewer/viewer/lib/mesher/mesher.ts @@ -159,7 +159,7 @@ setInterval(() => { const geometry = getSectionGeometry(x, y, z, world) const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean) //@ts-expect-error - postMessage({ type: 'geometry', key, geometry }, transferable) + postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable) processTime = performance.now() - start } else { // console.info('[mesher] Missing section', x, y, z) diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts index 995e7d29c..4f4545a79 100644 --- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -11,6 +11,7 @@ import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png' import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png' import { AtlasParser } from 'mc-assets' import TypedEmitter from 'typed-emitter' +import { LineMaterial } from 'three-stdlib' import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' import { toMajorVersion } from '../../../src/utils' import { buildCleanupDecorator } from './cleanupDecorator' @@ -18,6 +19,7 @@ import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput } from './m import { chunkPos } from './simpleUtils' import { HandItemBlock } from './holdingBlock' import { updateStatText } from './ui/newStats' +import { WorldRendererThree } from './worldrendererThree' function mod (x, n) { return ((x % n) + n) % n @@ -38,8 +40,14 @@ type CustomTexturesData = { } export abstract class WorldRendererCommon { + // todo + @worldCleanup() + threejsCursorLineMaterial: LineMaterial + @worldCleanup() + cursorBlock = null as Vec3 | null isPlayground = false displayStats = true + @worldCleanup() worldConfig = { minY: 0, worldHeight: 256 } // todo need to cleanup material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) @@ -72,9 +80,13 @@ export abstract class WorldRendererCommon @worldCleanup() currentTextureImage = undefined as any workers: any[] = [] + @worldCleanup() viewerPosition?: Vec3 lastCamUpdate = 0 droppedFpsPercentage = 0 + @worldCleanup() + initialChunkLoadWasStartedIn: number | undefined + @worldCleanup() initialChunksLoad = true enableChunksLoadDelay = false texturesVersion?: string @@ -102,6 +114,10 @@ export abstract class WorldRendererCommon workersProcessAverageTime = 0 workersProcessAverageTimeCount = 0 maxWorkersProcessTime = 0 + geometryReceiveCount = {} + allLoadedIn: undefined | number + rendererDevice = '...' + edgeChunks = {} as Record lastAddChunk = null as null | { timeout: any @@ -129,7 +145,7 @@ export abstract class WorldRendererCommon initWorkers (numWorkers = this.config.numWorkers) { // init workers - for (let i = 0; i < numWorkers; i++) { + for (let i = 0; i < numWorkers + 1; i++) { // Node environment needs an absolute path, but browser needs the url of the file const workerName = 'mesher.js' // eslint-disable-next-line node/no-path-concat @@ -140,6 +156,8 @@ export abstract class WorldRendererCommon if (!this.active) return this.handleWorkerMessage(data) if (data.type === 'geometry') { + this.geometryReceiveCount[data.workerIndex] ??= 0 + this.geometryReceiveCount[data.workerIndex]++ const geometry = data.geometry as MesherGeometryOutput for (const key in geometry.highestBlocks) { const highest = geometry.highestBlocks[key] @@ -336,16 +354,17 @@ export abstract class WorldRendererCommon return Math.floor(Math.max(this.worldConfig.minY, this.mesherConfig.clipWorldBelowY ?? -Infinity) / 16) * 16 } - updateDownloadedChunksText () { - updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D`) + updateChunksStatsText () { + updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`) } addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) { if (!this.active) return if (this.workers.length === 0) throw new Error('workers not initialized yet') this.initialChunksLoad = false + this.initialChunkLoadWasStartedIn ??= Date.now() this.loadedChunks[`${x},${z}`] = true - this.updateDownloadedChunksText() + this.updateChunksStatsText() for (const worker of this.workers) { // todo optimize worker.postMessage({ type: 'chunk', x, z, chunk }) @@ -394,21 +413,26 @@ export abstract class WorldRendererCommon for (const worker of this.workers) { worker.postMessage({ type: 'blockUpdate', pos, stateId }) } - this.setSectionDirty(pos) + this.setSectionDirty(pos, true, true) if (this.neighborChunkUpdates) { - if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0)) - if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0)) - if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0)) - if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0)) - if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16)) - if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16)) + if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, true) + if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, true) + if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, true) + if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, true) + if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, true) + if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, true) } } queueAwaited = false messagesQueue = {} as { [workerIndex: string]: any[] } - setSectionDirty (pos: Vec3, value = true) { // value false is used for unloading chunks + getWorkerNumber (pos: Vec3) { + const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length - 1) + return hash + 1 + } + + setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks if (this.viewDistance === -1) throw new Error('viewDistance not set') this.allChunksFinished = false const distance = this.getDistance(pos) @@ -419,7 +443,7 @@ export abstract class WorldRendererCommon // Dispatch sections to workers based on position // This guarantees uniformity accross workers and that a given section // is always dispatched to the same worker - const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length) + const hash = useChangeWorker ? 0 : this.getWorkerNumber(pos) this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1) this.messagesQueue[hash] ??= [] this.messagesQueue[hash].push({ @@ -487,4 +511,6 @@ export abstract class WorldRendererCommon destroy () { console.warn('world destroy is not implemented') } + + abstract setHighlightCursorBlock (block: typeof this.cursorBlock, shapePositions?: Array<{ position; width; height; depth }>): void } diff --git a/prismarine-viewer/viewer/lib/worldrendererThree.ts b/prismarine-viewer/viewer/lib/worldrendererThree.ts index bf0999a4a..8671bfaf0 100644 --- a/prismarine-viewer/viewer/lib/worldrendererThree.ts +++ b/prismarine-viewer/viewer/lib/worldrendererThree.ts @@ -3,7 +3,7 @@ import { Vec3 } from 'vec3' import nbt from 'prismarine-nbt' import PrismarineChatLoader from 'prismarine-chat' import * as tweenJs from '@tweenjs/tween.js' -import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib' +import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass, LineSegmentsGeometry, Wireframe, LineMaterial } from 'three-stdlib' import worldBlockProvider from 'mc-assets/dist/worldBlockProvider' import { renderSign } from '../sign-renderer' import { chunkPos, sectionPos } from './simpleUtils' @@ -14,6 +14,7 @@ import { addNewStat } from './ui/newStats' import { MesherGeometryOutput } from './mesher/shared' export class WorldRendererThree extends WorldRendererCommon { + interactionLines: null | { blockPos; mesh } = null outputFormat = 'threeJs' as const blockEntities = {} sectionObjects: Record = {} @@ -22,6 +23,7 @@ export class WorldRendererThree extends WorldRendererCommon { starField: StarField cameraSectionPos: Vec3 = new Vec3(0, 0, 0) holdingBlock: HoldingBlock + rendererDevice = '...' get tilesRendered () { return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0) @@ -33,6 +35,7 @@ export class WorldRendererThree extends WorldRendererCommon { constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) { super(config) + this.rendererDevice = String(WorldRendererThree.getRendererInfo(this.renderer)) this.starField = new StarField(scene) this.holdingBlock = new HoldingBlock(this.scene) @@ -54,10 +57,6 @@ export class WorldRendererThree extends WorldRendererCommon { void this.holdingBlock.initHandObject(this.material, this.blockstatesModels, this.blocksAtlases, item) } - changeBackgroundColor (color: [number, number, number]): void { - this.scene.background = new THREE.Color(color[0], color[1], color[2]) - } - changeHandSwingingState (isAnimationPlaying: boolean) { if (isAnimationPlaying) { this.holdingBlock.startSwing() @@ -66,6 +65,10 @@ export class WorldRendererThree extends WorldRendererCommon { } } + changeBackgroundColor (color: [number, number, number]): void { + this.scene.background = new THREE.Color(color[0], color[1], color[2]) + } + timeUpdated (newTime: number): void { const nightTime = 13_500 const morningStart = 23_000 @@ -365,9 +368,47 @@ export class WorldRendererThree extends WorldRendererCommon { } } - setSectionDirty (pos, value = true) { + setSectionDirty (...args: Parameters) { + const [pos] = args this.cleanChunkTextures(pos.x, pos.z) // todo don't do this! - super.setSectionDirty(pos, value) + super.setSectionDirty(...args) + } + + setHighlightCursorBlock (blockPos: typeof this.cursorBlock, shapePositions?: Array<{ position: any; width: any; height: any; depth: any; }>): void { + this.cursorBlock = blockPos + if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) { + return + } + if (this.interactionLines !== null) { + this.scene.remove(this.interactionLines.mesh) + this.interactionLines = null + } + if (blockPos === null) { + return + } + + const group = new THREE.Group() + for (const { position, width, height, depth } of shapePositions ?? []) { + const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const + const geometry = new THREE.BoxGeometry(...scale) + const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry)) + const wireframe = new Wireframe(lines, this.threejsCursorLineMaterial) + const pos = blockPos.plus(position) + wireframe.position.set(pos.x, pos.y, pos.z) + wireframe.computeLineDistances() + group.add(wireframe) + } + this.scene.add(group) + this.interactionLines = { blockPos, mesh: group } + } + + static getRendererInfo (renderer: THREE.WebGLRenderer) { + try { + const gl = renderer.getContext() + return `${gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)} powered by three.js r{THREE.REVISION}` + } catch (err) { + console.warn('Failed to get renderer info', err) + } } } diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 94d6cdf8a..c6a979c16 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -89,7 +89,6 @@ export const guiOptionsScheme: { tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far', }, handDisplay: {}, - neighborChunkUpdates: {}, renderDebug: { values: [ 'advanced', diff --git a/src/react/DebugOverlay.tsx b/src/react/DebugOverlay.tsx index 9079eed9b..54d8cb404 100644 --- a/src/react/DebugOverlay.tsx +++ b/src/react/DebugOverlay.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useMemo, useState } from 'react' import * as THREE from 'three' +import type { Block } from 'prismarine-block' import { getFixedFilesize } from '../downloadAndOpenFile' import { options } from '../optionsStorage' -import worldInteractions from '../worldInteractions' import styles from './DebugOverlay.module.css' export default () => { @@ -35,10 +35,10 @@ export default () => { const [day, setDay] = useState(0) const [entitiesCount, setEntitiesCount] = useState(0) const [dimension, setDimension] = useState('') - const [cursorBlock, setCursorBlock] = useState(null) - const [rendererDevice, setRendererDevice] = useState('') + const [cursorBlock, setCursorBlock] = useState(null) const minecraftYaw = useRef(0) const minecraftQuad = useRef(0) + const { rendererDevice } = viewer.world const quadsDescription = [ 'north (towards negative Z)', @@ -118,13 +118,6 @@ export default () => { managePackets('sent', name, data) }) - try { - const gl = window.renderer.getContext() - setRendererDevice(gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)) - } catch (err) { - console.warn(err) - } - return () => { document.removeEventListener('keydown', handleF3) clearInterval(packetsUpdateInterval) @@ -159,7 +152,7 @@ export default () => {
-

Renderer: {rendererDevice} powered by three.js r{THREE.REVISION}

+

Renderer: {rendererDevice}

{cursorBlock ? (<>

{cursorBlock.name}

diff --git a/src/worldInteractions.ts b/src/worldInteractions.ts index dcd5dc3d0..d6674ce31 100644 --- a/src/worldInteractions.ts +++ b/src/worldInteractions.ts @@ -4,7 +4,7 @@ import * as THREE from 'three' // wouldn't better to create atlas instead? import { Vec3 } from 'vec3' -import { LineMaterial, Wireframe, LineSegmentsGeometry } from 'three-stdlib' +import { LineMaterial } from 'three-stdlib' import { Entity } from 'prismarine-entity' import destroyStage0 from '../assets/destroy_stage_0.png' import destroyStage1 from '../assets/destroy_stage_1.png' @@ -34,7 +34,6 @@ function getViewDirection (pitch, yaw) { class WorldInteraction { ready = false - interactionLines: null | { blockPos; mesh } = null prevBreakState currentDigTime prevOnGround @@ -44,11 +43,9 @@ class WorldInteraction { lastButtons = [false, false, false] breakStartTime: number | undefined = 0 lastDugBlock: Vec3 | null = null - cursorBlock: import('prismarine-block').Block | null = null blockBreakMesh: THREE.Mesh breakTextures: THREE.Texture[] lastDigged: number - lineMaterial: LineMaterial debugDigStatus: string oneTimeInit () { @@ -109,10 +106,10 @@ class WorldInteraction { }) beforeRenderFrame.push(() => { - if (this.lineMaterial) { + if (viewer.world.threejsCursorLineMaterial) { const { renderer } = viewer - this.lineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height) - this.lineMaterial.dashOffset = performance.now() / 750 + viewer.world.threejsCursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height) + viewer.world.threejsCursorLineMaterial.dashOffset = performance.now() / 750 } }) } @@ -133,7 +130,7 @@ class WorldInteraction { this.debugDigStatus = 'done' }) bot.on('diggingAborted', (block) => { - if (!this.cursorBlock?.position.equals(block.position)) return + if (!viewer.world.cursorBlock?.equals(block.position)) return this.debugDigStatus = 'aborted' // if (this.lastDugBlock) this.breakStartTime = undefined @@ -151,7 +148,7 @@ class WorldInteraction { const upLineMaterial = () => { const inCreative = bot.game.gameMode === 'creative' const pixelRatio = viewer.renderer.getPixelRatio() - this.lineMaterial = new LineMaterial({ + viewer.world.threejsCursorLineMaterial = new LineMaterial({ color: inCreative ? 0x40_80_ff : 0x00_00_00, linewidth: Math.max(pixelRatio * 0.7, 1) * 2, // dashed: true, @@ -192,34 +189,6 @@ class WorldInteraction { } } - updateBlockInteractionLines (blockPos: Vec3 | null, shapePositions?: Array<{ position; width; height; depth }>) { - assertDefined(viewer) - if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) { - return - } - if (this.interactionLines !== null) { - viewer.scene.remove(this.interactionLines.mesh) - this.interactionLines = null - } - if (blockPos === null) { - return - } - - const group = new THREE.Group() - for (const { position, width, height, depth } of shapePositions ?? []) { - const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const - const geometry = new THREE.BoxGeometry(...scale) - const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry)) - const wireframe = new Wireframe(lines, this.lineMaterial) - const pos = blockPos.plus(position) - wireframe.position.set(pos.x, pos.y, pos.z) - wireframe.computeLineDistances() - group.add(wireframe) - } - viewer.scene.add(group) - this.interactionLines = { blockPos, mesh: group } - } - // todo this shouldnt be done in the render loop, migrate the code to dom events to avoid delays on lags update () { const inSpectator = bot.game.gameMode === 'spectator' @@ -232,10 +201,7 @@ class WorldInteraction { let cursorBlockDiggable = cursorBlock if (cursorBlock && !bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null - let cursorChanged = !cursorBlock !== !this.cursorBlock - if (cursorBlock && this.cursorBlock) { - cursorChanged = !cursorBlock.position.equals(this.cursorBlock.position) - } + const cursorChanged = cursorBlock && viewer.world.cursorBlock ? !viewer.world.cursorBlock.equals(cursorBlock.position) : viewer.world.cursorBlock !== cursorBlock // Place / interact / activate if (this.buttons[2] && this.lastBlockPlaced >= 4) { @@ -363,30 +329,24 @@ class WorldInteraction { this.prevOnGround = onGround // Show cursor + const allShapes = [...cursorBlock?.shapes ?? [], ...cursorBlock?.['interactionShapes'] ?? []] if (cursorBlock) { - const allShapes = [...cursorBlock.shapes, ...cursorBlock['interactionShapes'] ?? []] - this.updateBlockInteractionLines(cursorBlock.position, allShapes.map(shape => { - return getDataFromShape(shape) - })) - { - // union of all values - const breakShape = allShapes.reduce((acc, cur) => { - return [ - Math.min(acc[0], cur[0]), - Math.min(acc[1], cur[1]), - Math.min(acc[2], cur[2]), - Math.max(acc[3], cur[3]), - Math.max(acc[4], cur[4]), - Math.max(acc[5], cur[5]) - ] - }) - const { position, width, height, depth } = getDataFromShape(breakShape) - this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001) - position.add(cursorBlock.position) - this.blockBreakMesh.position.set(position.x, position.y, position.z) - } - } else { - this.updateBlockInteractionLines(null) + // BREAK MESH + // union of all values + const breakShape = allShapes.reduce((acc, cur) => { + return [ + Math.min(acc[0], cur[0]), + Math.min(acc[1], cur[1]), + Math.min(acc[2], cur[2]), + Math.max(acc[3], cur[3]), + Math.max(acc[4], cur[4]), + Math.max(acc[5], cur[5]) + ] + }) + const { position, width, height, depth } = getDataFromShape(breakShape) + this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001) + position.add(cursorBlock.position) + this.blockBreakMesh.position.set(position.x, position.y, position.z) } // Show break animation @@ -411,7 +371,11 @@ class WorldInteraction { } // Update state - this.cursorBlock = cursorBlock + if (cursorChanged) { + viewer.world.setHighlightCursorBlock(cursorBlock?.position ?? null, allShapes.map(shape => { + return getDataFromShape(shape) + })) + } this.lastButtons[0] = this.buttons[0] this.lastButtons[1] = this.buttons[1] this.lastButtons[2] = this.buttons[2]