From 976f6cab2bd24ef5cbdd8b9a1b7dec96e9ba8c70 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sat, 20 Apr 2024 15:28:31 +0300 Subject: [PATCH] add mesher HMR! fix: fix all colored glass rendering! --- esbuild.mjs | 9 +-- .../viewer/lib/cleanupDecorator.ts | 28 +++++++++ prismarine-viewer/viewer/lib/mesher/models.ts | 22 ++++++- prismarine-viewer/viewer/lib/mesher/shared.ts | 10 ++++ prismarine-viewer/viewer/lib/viewer.ts | 2 + .../viewer/lib/worldDataEmitter.ts | 1 + .../viewer/lib/worldrendererCommon.ts | 10 ++-- .../viewer/lib/worldrendererThree.ts | 15 ++++- scripts/esbuildPlugins.mjs | 57 ++++++++----------- src/eruda.js | 7 --- src/index.ts | 3 +- 11 files changed, 106 insertions(+), 58 deletions(-) create mode 100644 prismarine-viewer/viewer/lib/cleanupDecorator.ts create mode 100644 prismarine-viewer/viewer/lib/mesher/shared.ts delete mode 100644 src/eruda.js diff --git a/esbuild.mjs b/esbuild.mjs index 2e360e5d..3c35f32d 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -3,7 +3,7 @@ import * as esbuild from 'esbuild' import fs from 'fs' // import htmlPlugin from '@chialab/esbuild-plugin-html' import server from './server.js' -import { clients, plugins } from './scripts/esbuildPlugins.mjs' +import { clients, plugins, startWatchingHmr } from './scripts/esbuildPlugins.mjs' import { generateSW } from 'workbox-build' import { getSwAdditionalEntries } from './scripts/build.js' import { build } from 'esbuild' @@ -21,11 +21,7 @@ const dev = !prod const banner = [ 'window.global = globalThis;', - // report reload time - dev && 'if (sessionStorage.lastReload) { const [rebuild, reloadStart] = sessionStorage.lastReload.split(","); const now = Date.now(); console.log(`rebuild + reload:`, +rebuild, "+", now - reloadStart, "=", ((+rebuild + (now - reloadStart)) / 1000).toFixed(1) + "s");sessionStorage.lastReload = ""; }', - // auto-reload - dev && 'window.noAutoReload ??= false;(() => new EventSource("/esbuild").onmessage = ({ data: _data }) => { if (!_data) return; const data = JSON.parse(_data); if (!data.update) return;console.log("[esbuild] Page is outdated");document.title = `[O] ${document.title}`;if (window.noAutoReload || localStorage.noAutoReload) return; if (localStorage.autoReloadVisible && document.visibilityState !== "visible") return; sessionStorage.lastReload = `${data.update.time},${Date.now()}`; location.reload() })();' -].filter(Boolean) +] const buildingVersion = new Date().toISOString().split(':')[0] @@ -91,6 +87,7 @@ const buildOptions = { if (watch) { const ctx = await esbuild.context(buildOptions) await ctx.watch() + startWatchingHmr() server.app.get('/esbuild', (req, res, next) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', diff --git a/prismarine-viewer/viewer/lib/cleanupDecorator.ts b/prismarine-viewer/viewer/lib/cleanupDecorator.ts new file mode 100644 index 00000000..476546a6 --- /dev/null +++ b/prismarine-viewer/viewer/lib/cleanupDecorator.ts @@ -0,0 +1,28 @@ +export function buildCleanupDecorator (cleanupMethod: string) { + return function () { + return function (_target: {snapshotInitialValues}, propertyKey: string) { + const target = _target as any + // Store the initial value of the property + if (!target._snapshotMethodPatched) { + target.snapshotInitialValues = function () { + this._initialValues = {} + for (const key of target._toCleanup) { + this._initialValues[key] = this[key] + } + } + target._snapshotMethodPatched = true + } + (target._toCleanup ??= []).push(propertyKey) + if (!target._cleanupPatched) { + const originalMethod = target[cleanupMethod] + target[cleanupMethod] = function () { + for (const key of target._toCleanup) { + this[key] = this._initialValues[key] + } + originalMethod.apply(this, arguments) + } + } + target._cleanupPatched = true + } + } +} diff --git a/prismarine-viewer/viewer/lib/mesher/models.ts b/prismarine-viewer/viewer/lib/mesher/models.ts index 29b4edb5..fc86a671 100644 --- a/prismarine-viewer/viewer/lib/mesher/models.ts +++ b/prismarine-viewer/viewer/lib/mesher/models.ts @@ -158,6 +158,7 @@ function renderLiquid (world, cursor, texture, type, biome, water, attr) { const neighbor = world.getBlock(neighborPos) if (!neighbor) continue if (neighbor.type === type) continue + const isGlass = neighbor.name.includes('glass') if ((isCube(neighbor) && !isUp) || neighbor.material === 'plant' || neighbor.getProperties().waterlogged) continue let tint = [1, 1, 1] @@ -438,6 +439,8 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr } export function getSectionGeometry (sx, sy, sz, world: World) { + let delayedRender = [] as (() => void)[] + const attr = { sx: sx + 8, sy: sy + 8, @@ -488,7 +491,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) { if (block.name === 'water' || isWaterlogged) { const waterBlock = block.name === 'water' ? block : { name: 'water', metadata: 0 } const variant = getModelVariants(waterBlock as any)[0] - renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, true, attr) + const pos = cursor.clone() + delayedRender.push(() => { + renderLiquid(world, pos, variant.model.textures.particle, block.type, biome, true, attr) + }) } else if (block.name === 'lava') { renderLiquid(world, cursor, variant.model.textures.particle, block.type, biome, false, attr) } @@ -509,7 +515,14 @@ export function getSectionGeometry (sx, sy, sz, world: World) { } for (const element of variant.model.elements) { - renderElement(world, cursor, element, variant.model.ao, attr, globalMatrix, globalShift, block, biome) + if (block.transparent) { + const pos = cursor.clone() + delayedRender.push(() => { + renderElement(world, pos, element, variant.model.ao, attr, globalMatrix, globalShift, block, biome) + }) + } else { + renderElement(world, cursor, element, variant.model.ao, attr, globalMatrix, globalShift, block, biome) + } } } } @@ -517,6 +530,11 @@ export function getSectionGeometry (sx, sy, sz, world: World) { } } + for (const render of delayedRender) { + render() + } + delayedRender = [] + let ndx = attr.positions.length / 3 for (let i = 0; i < attr.t_positions.length / 12; i++) { attr.indices.push( diff --git a/prismarine-viewer/viewer/lib/mesher/shared.ts b/prismarine-viewer/viewer/lib/mesher/shared.ts new file mode 100644 index 00000000..36c319b1 --- /dev/null +++ b/prismarine-viewer/viewer/lib/mesher/shared.ts @@ -0,0 +1,10 @@ +export const defaultMesherConfig = { + version: '', + enableLighting: true, + skyLight: 15, + smoothLighting: true, + outputFormat: 'threeJs' as 'threeJs' | 'webgl', + textureSize: 1024, // for testing +} + +export type MesherConfig = typeof defaultMesherConfig diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index 9097c7ec..1b11ac63 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -191,6 +191,8 @@ export class Viewer { skyLight = ((timeOfDay - 12000) / 6000) * 15 } + skyLight = Math.floor(skyLight) // todo: remove this after optimization + if (this.world.mesherConfig.skyLight === skyLight) return this.world.mesherConfig.skyLight = skyLight ; (this.world as WorldRendererThree).rerenderAllChunks?.() diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index ed53847d..fa916c51 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -191,6 +191,7 @@ export class WorldDataEmitter extends EventEmitter { this.lastPos.update(pos) await 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 151b2679..b13443d1 100644 --- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -25,8 +25,7 @@ export type WorldRendererConfig = typeof defaultWorldRendererConfig export abstract class WorldRendererCommon { worldConfig = { minY: 0, worldHeight: 256 } - // todo @sa2urami set alphaTest back to 0.1 and instead properly sort transparent and solid objects (needs to be done in worker too) - material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.5 }) + material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) @worldCleanup() active = false @@ -58,14 +57,14 @@ export abstract class WorldRendererCommon abstract outputFormat: 'threeJs' | 'webgl' - constructor (public config: WorldRendererConfig) { + constructor(public config: WorldRendererConfig) { // this.initWorkers(1) // preload script on page load this.snapshotInitialValues() } - snapshotInitialValues() {} + snapshotInitialValues () { } - initWorkers(numWorkers = this.config.numWorkers) { + initWorkers (numWorkers = this.config.numWorkers) { // init workers for (let i = 0; i < numWorkers; i++) { // Node environment needs an absolute path, but browser needs the url of the file @@ -154,7 +153,6 @@ export abstract class WorldRendererCommon this.workers = [] } - // new game load happens here setVersion (version, texturesVersion = version) { this.version = version diff --git a/prismarine-viewer/viewer/lib/worldrendererThree.ts b/prismarine-viewer/viewer/lib/worldrendererThree.ts index 364df12a..1fff112f 100644 --- a/prismarine-viewer/viewer/lib/worldrendererThree.ts +++ b/prismarine-viewer/viewer/lib/worldrendererThree.ts @@ -28,9 +28,8 @@ export class WorldRendererThree extends WorldRendererCommon { * Optionally update data that are depedendent on the viewer position */ updatePosDataChunk (key: string) { - if (!this.viewerPosition) return const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16)) - const [xPlayer, yPlayer, zPlayer] = this.viewerPosition.toArray().map(x => Math.floor(x / 16)) + const [xPlayer, yPlayer, zPlayer] = this.camera.position.toArray().map(x => Math.floor(x / 16)) // sum of distances: x + y + z const chunkDistance = Math.abs(x - xPlayer) + Math.abs(y - yPlayer) + Math.abs(z - zPlayer) const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')! @@ -185,6 +184,18 @@ export class WorldRendererThree extends WorldRendererCommon { } } + async doHmr () { + const oldSections = { ...this.sectionObjects } + this.sectionObjects = {} // skip clearing + worldView!.unloadAllChunks() + this.setVersion(this.version, this.texturesVersion) + this.sectionObjects = oldSections + // this.rerenderAllChunks() + + // supply new data + await worldView!.updatePosition(bot.entity.position, true) + } + rerenderAllChunks () { // todo not clear what to do with loading chunks for (const key of Object.keys(this.sectionObjects)) { const [x, y, z] = key.split(',').map(Number) diff --git a/scripts/esbuildPlugins.mjs b/scripts/esbuildPlugins.mjs index ddc34c18..8e037707 100644 --- a/scripts/esbuildPlugins.mjs +++ b/scripts/esbuildPlugins.mjs @@ -6,16 +6,33 @@ import * as fs from 'fs' import { filesize } from 'filesize' import MCProtocol from 'minecraft-protocol' import MCData from 'minecraft-data' +import { throttle } from 'lodash-es' const { supportedVersions } = MCProtocol const prod = process.argv.includes('--prod') let connectedClients = [] -const watchExternal = [ - // 'dist/mesher.js', - // 'dist/webglRendererWorker.js' -] +const writeToClients = (data) => { + connectedClients.forEach((res) => { + res.write(`data: ${JSON.stringify(data)}\n\n`) + res.flush() + }) +} + +export const startWatchingHmr = () => { + const eventsPerFile = { + 'mesher.js': 'mesher', + // 'dist/webglRendererWorker.js': 'webglRendererWorker', + } + for (const name of Object.keys(eventsPerFile)) { + const file = join('dist', name); + if (!fs.existsSync(file)) console.warn(`[missing worker] File ${name} does not exist`) + fs.watchFile(file, () => { + writeToClients({ replace: { type: eventsPerFile[name] } }) + }) + } +} /** @type {import('esbuild').Plugin[]} */ const plugins = [ @@ -46,8 +63,6 @@ const plugins = [ return { contents: `window.mcData ??= ${JSON.stringify(defaultVersionsObj)};module.exports = { pc: window.mcData }`, loader: 'js', - // todo use external watchers - watchFiles: watchExternal, } }) build.onResolve({ @@ -153,25 +168,8 @@ const plugins = [ let time let prevHash - let prevWorkersMtime - const updateMtime = async () => { - const workersMtime = watchExternal.map(file => { - try { - return fs.statSync(file).mtimeMs - } catch (err) { - console.log('missing file', file) - return 0 - } - }) - if (workersMtime.some((mtime, i) => mtime !== prevWorkersMtime?.[i])) { - prevWorkersMtime = workersMtime - return true - } - return false - } build.onStart(() => { time = Date.now() - updateMtime() }) build.onEnd(({ errors, outputFiles: _outputFiles, metafile, warnings }) => { /** @type {import('esbuild').OutputFile[]} */ @@ -181,10 +179,7 @@ const plugins = [ outputFiles.find(outputFile => outputFile.path) if (errors.length) { - connectedClients.forEach((res) => { - res.write(`data: ${JSON.stringify({ errors: errors.map(error => error.text) })}\n\n`) - res.flush() - }) + writeToClients({ errors: errors.map(error => error.text) }) return } @@ -194,8 +189,7 @@ const plugins = [ /** @type {import('esbuild').OutputFile} */ //@ts-ignore const outputFile = outputFiles.find(x => x.path.endsWith('.js')) - const updateWorkers = updateMtime() - if (outputFile.hash === prevHash && !updateWorkers) { + if (outputFile.hash === prevHash) { // todo also check workers and css console.log('Ignoring reload as contents the same') return @@ -212,10 +206,7 @@ const plugins = [ return } - connectedClients.forEach((res) => { - res.write(`data: ${JSON.stringify({ update: { time: elapsed } })}\n\n`) - res.flush() - }) + writeToClients({ update: { time: elapsed } }) connectedClients.length = 0 }) } diff --git a/src/eruda.js b/src/eruda.js deleted file mode 100644 index 9cb11381..00000000 --- a/src/eruda.js +++ /dev/null @@ -1,7 +0,0 @@ -import { isMobile } from './menus/components/common' - -if (process.env.NODE_ENV === 'development' && isMobile()) { - // can be changed to require('eruda') - import('https://cdn.skypack.dev/eruda').default.init() - console.log('JS Loaded in', Date.now() - window.startLoad) -} diff --git a/src/index.ts b/src/index.ts index 8ee16320..f8aef541 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,7 +35,6 @@ import './reactUi.jsx' import { contro, onBotCreate } from './controls' import './dragndrop' import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs' -import './eruda' import { watchOptionsAfterViewerInit } from './watchOptions' import downloadAndOpenFile from './downloadAndOpenFile' @@ -96,7 +95,7 @@ import { possiblyHandleStateVariable } from './googledrive' import flyingSquidEvents from './flyingSquidEvents' import { hideNotification, notificationProxy } from './react/NotificationProvider' import { ViewerWrapper } from 'prismarine-viewer/viewer/lib/viewerWrapper' -import './hotReload' +import './devReload' window.debug = debug window.THREE = THREE