rewrite renderers to allow custom ones! worker -> mesher (#102)
This commit is contained in:
parent
930d972dc6
commit
2cc524a4ab
25 changed files with 760 additions and 628 deletions
14
README.MD
14
README.MD
|
|
@ -10,11 +10,11 @@ You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.
|
|||
|
||||
### Big Features
|
||||
|
||||
- Connect to any offline server* (it's possible because of proxy servers, see below)
|
||||
- Open any zip world file or even folder in read-write mode!
|
||||
- Singleplayer mode with simple world generation
|
||||
- Connect to cracked servers* (it's possible because of proxy servers, see below)
|
||||
- Singleplayer mode with simple world generations!
|
||||
- Works offline
|
||||
- Play with friends over global network! (P2P is powered by Peer.js servers)
|
||||
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||
- First-class touch (mobile) & controller support
|
||||
- Resource pack support
|
||||
- even even more!
|
||||
|
|
@ -32,6 +32,14 @@ See the [Mineflayer](https://github.com/PrismarineJS/mineflayer) repo for the li
|
|||
There is a builtin proxy, but you can also host a your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field.
|
||||
MS account authentication will be supported soon.
|
||||
|
||||
### Rendering
|
||||
|
||||
#### Three.js Renderer
|
||||
|
||||
- Uses WebGL2. Chunks are rendered using Geometry Buffers prepared by 4 mesher workers.
|
||||
- Supports FXAA
|
||||
- Doesn't support culling
|
||||
|
||||
<!-- TODO proxy server communication graph -->
|
||||
|
||||
### Things that are not planned yet
|
||||
|
|
|
|||
10
esbuild.mjs
10
esbuild.mjs
|
|
@ -11,7 +11,9 @@ import { build } from 'esbuild'
|
|||
//@ts-ignore
|
||||
try { await import('./localSettings.mjs') } catch { }
|
||||
|
||||
fs.writeFileSync('dist/index.html', fs.readFileSync('index.html', 'utf8').replace('<!-- inject script -->', '<script src="index.js"></script>'), 'utf8')
|
||||
const entrypoint = 'index.ts'
|
||||
|
||||
fs.writeFileSync('dist/index.html', fs.readFileSync('index.html', 'utf8').replace('<!-- inject script -->', `<script src="${entrypoint.replace(/\.tsx?/, '.js')}"></script>`), 'utf8')
|
||||
|
||||
const watch = process.argv.includes('--watch') || process.argv.includes('-w')
|
||||
const prod = process.argv.includes('--prod')
|
||||
|
|
@ -30,7 +32,7 @@ const buildingVersion = new Date().toISOString().split(':')[0]
|
|||
/** @type {import('esbuild').BuildOptions} */
|
||||
const buildOptions = {
|
||||
bundle: true,
|
||||
entryPoints: ['src/index.ts'],
|
||||
entryPoints: [`src/${entrypoint}`],
|
||||
target: ['es2020'],
|
||||
jsx: 'automatic',
|
||||
jsxDev: dev,
|
||||
|
|
@ -76,7 +78,9 @@ const buildOptions = {
|
|||
loader: {
|
||||
// todo use external or resolve issues with duplicating
|
||||
'.png': 'dataurl',
|
||||
'.map': 'empty'
|
||||
'.map': 'empty',
|
||||
'.vert': 'text',
|
||||
'.frag': 'text'
|
||||
},
|
||||
write: false,
|
||||
// todo would be better to enable?
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@
|
|||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
|
||||
"start-experiments": "vite --config experiments/vite.config.ts",
|
||||
"watch-worker": "node prismarine-viewer/buildWorker.mjs -w"
|
||||
"watch-other-workers": "echo NOT IMPLEMENTED",
|
||||
"watch-worker": "node prismarine-viewer/buildMesherWorker.mjs -w",
|
||||
"run-playground": "run-p watch-worker watch-other-workers playground-server watch-playground",
|
||||
"run-all": "run-p start run-playground",
|
||||
"playground-server": "live-server --port=9090 prismarine-viewer/public",
|
||||
"watch-playground": "node prismarine-viewer/esbuild.mjs -w"
|
||||
},
|
||||
"keywords": [
|
||||
"prismarine",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { polyfillNode } from 'esbuild-plugin-polyfill-node'
|
|||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import fs from 'fs'
|
||||
import { dynamicMcDataFiles } from './buildWorkerConfig.mjs'
|
||||
import { dynamicMcDataFiles } from './buildMesherConfig.mjs'
|
||||
|
||||
const allowedBundleFiles = ['legacy', 'versions', 'protocolVersions', 'features']
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ const buildOptions = {
|
|||
js: `globalThis.global = globalThis;process = {env: {}, versions: {} };`,
|
||||
},
|
||||
platform: 'browser',
|
||||
entryPoints: [path.join(__dirname, './viewer/lib/worker.js')],
|
||||
entryPoints: [path.join(__dirname, './viewer/lib/mesher/mesher.ts')],
|
||||
minify: true,
|
||||
logLevel: 'info',
|
||||
drop: !watch ? [
|
||||
|
|
@ -30,6 +30,9 @@ const buildOptions = {
|
|||
write: false,
|
||||
metafile: true,
|
||||
outdir: path.join(__dirname, './public'),
|
||||
define: {
|
||||
'process.env.BROWSER': '"true"',
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'external-json',
|
||||
|
|
@ -101,14 +104,14 @@ const buildOptions = {
|
|||
resolveDir: process.cwd(),
|
||||
}
|
||||
})
|
||||
build.onEnd(({metafile, outputFiles}) => {
|
||||
build.onEnd(({ metafile, outputFiles }) => {
|
||||
if (!metafile) return
|
||||
fs.writeFileSync(path.join(__dirname, './public/metafile.json'), JSON.stringify(metafile))
|
||||
for (const outDir of ['../dist/', './public/']) {
|
||||
for (const outputFile of outputFiles) {
|
||||
if (outDir === '../dist/' && outputFile.path.endsWith('.map')) {
|
||||
// skip writing & browser loading sourcemap there, worker debugging should be done in playground
|
||||
continue
|
||||
// continue
|
||||
}
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(__dirname, outDir, path.basename(outputFile.path)), outputFile.text)
|
||||
|
|
@ -15,7 +15,7 @@ import Entity from '../viewer/lib/entity/Entity'
|
|||
|
||||
globalThis.THREE = THREE
|
||||
//@ts-ignore
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
|
||||
const gui = new GUI()
|
||||
|
||||
|
|
@ -137,7 +137,6 @@ async function main () {
|
|||
viewer.entities.setDebugMode('basic')
|
||||
viewer.setVersion(version)
|
||||
viewer.entities.onSkinUpdate = () => {
|
||||
viewer.update()
|
||||
viewer.render()
|
||||
}
|
||||
|
||||
|
|
@ -257,39 +256,6 @@ async function main () {
|
|||
}
|
||||
}
|
||||
|
||||
// const jsonData = await fetch('https://bluecolored.de/bluemap/maps/overworld/tiles/0/x-2/2/z1/6.json?584662').then(r => r.json())
|
||||
|
||||
// const uniforms = {
|
||||
// distance: { value: 0 },
|
||||
// sunlightStrength: { value: 1 },
|
||||
// ambientLight: { value: 0 },
|
||||
// skyColor: { value: new THREE.Color(0.5, 0.5, 1) },
|
||||
// voidColor: { value: new THREE.Color(0, 0, 0) },
|
||||
// hiresTileMap: {
|
||||
// value: {
|
||||
// map: null,
|
||||
// size: 100,
|
||||
// scale: new THREE.Vector2(1, 1),
|
||||
// translate: new THREE.Vector2(),
|
||||
// pos: new THREE.Vector2(),
|
||||
// }
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// const shader1 = new THREE.ShaderMaterial({
|
||||
// uniforms: uniforms,
|
||||
// vertexShader: [0, 0, 0, 0],
|
||||
// fragmentShader: fragmentShader,
|
||||
// transparent: false,
|
||||
// depthWrite: true,
|
||||
// depthTest: true,
|
||||
// vertexColors: true,
|
||||
// side: THREE.FrontSide,
|
||||
// wireframe: false
|
||||
// })
|
||||
|
||||
|
||||
//@ts-ignore
|
||||
const controls = new OrbitControls(viewer.camera, renderer.domElement)
|
||||
controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
|
|
@ -315,7 +281,7 @@ async function main () {
|
|||
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
|
||||
const { children, isSkeletonHelper } = obj
|
||||
if (!Array.isArray(children)) return
|
||||
if (isSkeletonHelper) {
|
||||
obj.visible = true
|
||||
|
|
@ -327,7 +293,6 @@ async function main () {
|
|||
}
|
||||
enableSkeletonDebug(viewer.entities.entities['id'])
|
||||
setTimeout(() => {
|
||||
viewer.update()
|
||||
viewer.render()
|
||||
}, TWEEN_DURATION)
|
||||
}
|
||||
|
|
@ -441,9 +406,6 @@ async function main () {
|
|||
}
|
||||
})
|
||||
viewer.waitForChunksToRender().then(async () => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
for (const update of Object.values(onUpdate)) {
|
||||
update()
|
||||
}
|
||||
|
|
@ -454,7 +416,6 @@ async function main () {
|
|||
const animate = () => {
|
||||
// if (controls) controls.update()
|
||||
// worldView.updatePosition(controls.target)
|
||||
viewer.update()
|
||||
viewer.render()
|
||||
// window.requestAnimationFrame(animate)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"description": "Web based viewer",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"postinstall": "pnpm generate-textures && node buildWorker.mjs",
|
||||
"postinstall": "pnpm generate-textures && node buildMesherWorker.mjs",
|
||||
"generate-textures": "tsx viewer/prepare/postinstall.ts"
|
||||
},
|
||||
"author": "PrismarineJS",
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
//@ts-check
|
||||
/* global postMessage self */
|
||||
|
||||
if (!global.self) {
|
||||
import { World } from './world'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { getSectionGeometry, setRendererData } from './models'
|
||||
|
||||
if (module.require) {
|
||||
// If we are in a node environement, we need to fake some env variables
|
||||
/* eslint-disable no-eval */
|
||||
const r = eval('require') // yeah I know bad spooky eval, booouh
|
||||
const r = module.require
|
||||
const { parentPort } = r('worker_threads')
|
||||
global.self = parentPort
|
||||
global.postMessage = (value, transferList) => { parentPort.postMessage(value, transferList) }
|
||||
global.performance = r('perf_hooks').performance
|
||||
}
|
||||
|
||||
const { Vec3 } = require('vec3')
|
||||
const { World } = require('./world')
|
||||
const { getSectionGeometry, setBlockStates } = require('./models')
|
||||
|
||||
let world = null
|
||||
let dirtySections = {}
|
||||
let world: World
|
||||
let dirtySections: Map<string, number> = new Map()
|
||||
let blockStatesReady = false
|
||||
|
||||
function sectionKey (x, y, z) {
|
||||
|
|
@ -26,24 +26,30 @@ function setSectionDirty (pos, value = true) {
|
|||
const x = Math.floor(pos.x / 16) * 16
|
||||
const y = Math.floor(pos.y / 16) * 16
|
||||
const z = Math.floor(pos.z / 16) * 16
|
||||
const chunk = world.getColumn(x, z)
|
||||
const key = sectionKey(x, y, z)
|
||||
if (!value) {
|
||||
delete dirtySections[key]
|
||||
dirtySections.delete(key)
|
||||
postMessage({ type: 'sectionFinished', key })
|
||||
} else if (chunk?.getSection(pos)) {
|
||||
dirtySections[key] = value
|
||||
return
|
||||
}
|
||||
|
||||
const chunk = world.getColumn(x, z)
|
||||
if (chunk?.getSection(pos)) {
|
||||
dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
|
||||
} else {
|
||||
postMessage({ type: 'sectionFinished', key })
|
||||
}
|
||||
}
|
||||
|
||||
self.onmessage = ({ data }) => {
|
||||
const globalVar: any = globalThis
|
||||
|
||||
if (data.type === 'mcData') {
|
||||
globalThis.mcData = data.mcData
|
||||
globalVar.mcData = data.mcData
|
||||
world = new World(data.version)
|
||||
} else if (data.type === 'blockStates') {
|
||||
setBlockStates(data.json)
|
||||
} else if (data.type === 'rendererData') {
|
||||
setRendererData(data.json/* , data.textureSize */)
|
||||
world.outputFormat = data.outputFormat ?? world.outputFormat
|
||||
blockStatesReady = true
|
||||
} else if (data.type === 'dirty') {
|
||||
const loc = new Vec3(data.x, data.y, data.z)
|
||||
|
|
@ -56,36 +62,39 @@ self.onmessage = ({ data }) => {
|
|||
const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored()
|
||||
world.setBlockStateId(loc, data.stateId)
|
||||
} else if (data.type === 'reset') {
|
||||
world = null
|
||||
blocksStates = null
|
||||
dirtySections = {}
|
||||
world = undefined as any
|
||||
// blocksStates = null
|
||||
dirtySections = new Map()
|
||||
// todo also remove cached
|
||||
globalThis.mcData = null
|
||||
globalVar.mcData = null
|
||||
blockStatesReady = false
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (world === null || !blockStatesReady) return
|
||||
const sections = Object.keys(dirtySections)
|
||||
|
||||
if (sections.length === 0) return
|
||||
if (dirtySections.size === 0) return
|
||||
// console.log(sections.length + ' dirty sections')
|
||||
|
||||
// const start = performance.now()
|
||||
for (const key of sections) {
|
||||
let [x, y, z] = key.split(',')
|
||||
x = parseInt(x, 10)
|
||||
y = parseInt(y, 10)
|
||||
z = parseInt(z, 10)
|
||||
for (const key of dirtySections.keys()) {
|
||||
let [x, y, z] = key.split(',').map(v => parseInt(v, 10))
|
||||
const chunk = world.getColumn(x, z)
|
||||
if (chunk?.getSection(new Vec3(x, y, z))) {
|
||||
delete dirtySections[key]
|
||||
const geometry = getSectionGeometry(x, y, z, world)
|
||||
const transferable = [geometry.positions.buffer, geometry.normals.buffer, geometry.colors.buffer, geometry.uvs.buffer]
|
||||
//@ts-ignore
|
||||
postMessage({ type: 'geometry', key, geometry }, transferable)
|
||||
} else {
|
||||
console.info('[mesher] Missing section', x, y, z)
|
||||
}
|
||||
postMessage({ type: 'sectionFinished', key })
|
||||
const dirtyTimes = dirtySections.get(key)
|
||||
if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy')
|
||||
for (let i = 0; i < dirtyTimes; i++) {
|
||||
postMessage({ type: 'sectionFinished', key })
|
||||
}
|
||||
dirtySections.delete(key)
|
||||
}
|
||||
// const time = performance.now() - start
|
||||
// console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import { BlockStatesOutput } from '../prepare/modelsBuilder'
|
||||
import type { BlockStatesOutput } from '../../prepare/modelsBuilder'
|
||||
import { World } from './world'
|
||||
import { Block } from 'prismarine-block'
|
||||
|
||||
|
|
@ -536,6 +536,6 @@ function getModelVariants (block: import('prismarine-block').Block) {
|
|||
return []
|
||||
}
|
||||
|
||||
export const setBlockStates = (_blockStates: BlockStatesOutput | null) => {
|
||||
export const setRendererData = (_blockStates: BlockStatesOutput | null) => {
|
||||
blockStates = _blockStates!
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import Chunks from 'prismarine-chunk'
|
|||
import mcData from 'minecraft-data'
|
||||
import { Block } from "prismarine-block"
|
||||
import { Vec3 } from 'vec3'
|
||||
import moreBlockDataGeneratedJson from './moreBlockDataGenerated.json'
|
||||
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
|
||||
|
||||
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
||||
|
||||
|
|
@ -30,12 +30,13 @@ export type WorldBlock = Block & {
|
|||
}
|
||||
|
||||
export class World {
|
||||
outputFormat = 'threeJs' as 'threeJs' | 'webgl'
|
||||
Chunk: any/* import('prismarine-chunk/types/index').PCChunk */
|
||||
columns = {}
|
||||
blockCache = {}
|
||||
biomeCache: { [id: number]: mcData.Biome }
|
||||
|
||||
constructor (version) {
|
||||
constructor(version) {
|
||||
this.Chunk = Chunks(version)
|
||||
this.biomeCache = mcData(version).biomes
|
||||
}
|
||||
|
|
@ -2,9 +2,11 @@
|
|||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
function loadTexture (texture, cb) {
|
||||
function loadTexture (texture, cb, onLoad) {
|
||||
if (!textureCache[texture]) {
|
||||
textureCache[texture] = new THREE.TextureLoader().load(texture)
|
||||
textureCache[texture] = new THREE.TextureLoader().load(texture, onLoad)
|
||||
} else {
|
||||
onLoad?.()
|
||||
}
|
||||
cb(textureCache[texture])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { WorldRenderer } from './worldrenderer'
|
||||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import { getVersion } from './version'
|
||||
import EventEmitter from 'events'
|
||||
import { EffectComposer, RenderPass, ShaderPass, FXAAShader } from 'three-stdlib'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import { WorldRendererCommon } from './worldrendererCommon'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
ambientLight: THREE.AmbientLight
|
||||
directionalLight: THREE.DirectionalLight
|
||||
camera: THREE.PerspectiveCamera
|
||||
world: WorldRenderer
|
||||
world: WorldRendererCommon
|
||||
entities: Entities
|
||||
primitives: Primitives
|
||||
// primitives: Primitives
|
||||
domElement: HTMLCanvasElement
|
||||
playerHeight = 1.62
|
||||
isSneaking = false
|
||||
|
|
@ -24,11 +24,8 @@ export class Viewer {
|
|||
audioListener: THREE.AudioListener
|
||||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
composer?: EffectComposer
|
||||
fxaaPass: ShaderPass
|
||||
renderPass: RenderPass
|
||||
|
||||
constructor(public renderer: THREE.WebGLRenderer, numWorkers?: number, public enableFXAA = false) {
|
||||
constructor(public renderer: THREE.WebGLRenderer, numWorkers?: number) {
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
|
|
@ -36,12 +33,9 @@ export class Viewer {
|
|||
this.scene = new THREE.Scene()
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.resetScene()
|
||||
if (this.enableFXAA) {
|
||||
this.enableFxaaScene()
|
||||
}
|
||||
this.world = new WorldRenderer(this.scene, numWorkers)
|
||||
this.world = new WorldRendererThree(this.scene, this.renderer, this.camera, numWorkers)
|
||||
this.entities = new Entities(this.scene)
|
||||
this.primitives = new Primitives(this.scene, this.camera)
|
||||
// this.primitives = new Primitives(this.scene, this.camera)
|
||||
|
||||
this.domElement = renderer.domElement
|
||||
}
|
||||
|
|
@ -67,7 +61,7 @@ export class Viewer {
|
|||
this.resetScene()
|
||||
this.world.resetWorld()
|
||||
this.entities.clear()
|
||||
this.primitives.clear()
|
||||
// this.primitives.clear()
|
||||
}
|
||||
|
||||
setVersion (userVersion: string) {
|
||||
|
|
@ -76,7 +70,7 @@ export class Viewer {
|
|||
this.version = userVersion
|
||||
this.world.setVersion(userVersion, texturesVersion)
|
||||
this.entities.clear()
|
||||
this.primitives.clear()
|
||||
// this.primitives.clear()
|
||||
}
|
||||
|
||||
addColumn (x, z, chunk) {
|
||||
|
|
@ -103,18 +97,13 @@ export class Viewer {
|
|||
}))
|
||||
}
|
||||
|
||||
updatePrimitive (p) {
|
||||
this.primitives.update(p)
|
||||
}
|
||||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
if (pos) {
|
||||
let y = pos.y + this.playerHeight
|
||||
if (this.isSneaking) y -= 0.3
|
||||
new tweenJs.Tween(cam.position).to({ x: pos.x, y, z: pos.z }, 50).start()
|
||||
}
|
||||
cam.rotation.set(pitch, yaw, roll, 'ZYX')
|
||||
let yOffset = this.playerHeight
|
||||
if (this.isSneaking) yOffset -= 0.3
|
||||
|
||||
if (this.world instanceof WorldRendererThree) this.world.camera = cam as THREE.PerspectiveCamera
|
||||
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
}
|
||||
|
||||
playSound (position: Vec3, path: string, volume = 1) {
|
||||
|
|
@ -151,7 +140,7 @@ export class Viewer {
|
|||
})
|
||||
|
||||
emitter.on('primitive', (p) => {
|
||||
this.updatePrimitive(p)
|
||||
// this.updatePrimitive(p)
|
||||
})
|
||||
|
||||
emitter.on('loadChunk', ({ x, z, chunk, worldConfig }) => {
|
||||
|
|
@ -160,7 +149,7 @@ export class Viewer {
|
|||
})
|
||||
// todo remove and use other architecture instead so data flow is clear
|
||||
emitter.on('blockEntities', (blockEntities) => {
|
||||
this.world.blockEntities = blockEntities
|
||||
if (this.world instanceof WorldRendererThree) this.world.blockEntities = blockEntities
|
||||
})
|
||||
|
||||
emitter.on('unloadChunk', ({ x, z }) => {
|
||||
|
|
@ -175,64 +164,20 @@ export class Viewer {
|
|||
this.world.updateViewerPosition(pos)
|
||||
})
|
||||
|
||||
emitter.emit('listening')
|
||||
|
||||
this.domElement.addEventListener?.('pointerdown', (evt) => {
|
||||
const raycaster = new THREE.Raycaster()
|
||||
const mouse = new THREE.Vector2()
|
||||
mouse.x = (evt.clientX / this.domElement.clientWidth) * 2 - 1
|
||||
mouse.y = -(evt.clientY / this.domElement.clientHeight) * 2 + 1
|
||||
raycaster.setFromCamera(mouse, this.camera)
|
||||
const { ray } = raycaster
|
||||
emitter.emit('mouseClick', { origin: ray.origin, direction: ray.direction, button: evt.button })
|
||||
emitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
})
|
||||
}
|
||||
|
||||
update () {
|
||||
tweenJs.update()
|
||||
emitter.emit('listening')
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.composer) {
|
||||
this.renderPass.camera = this.camera
|
||||
this.composer.render()
|
||||
} else {
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
this.world.render()
|
||||
this.entities.render()
|
||||
}
|
||||
|
||||
async waitForChunksToRender () {
|
||||
await this.world.waitForChunksToRender()
|
||||
}
|
||||
|
||||
enableFxaaScene () {
|
||||
let renderTarget
|
||||
if (this.renderer.capabilities.isWebGL2) {
|
||||
// Use float precision depth if possible
|
||||
// see https://github.com/bs-community/skinview3d/issues/111
|
||||
renderTarget = new THREE.WebGLRenderTarget(0, 0, {
|
||||
depthTexture: new THREE.DepthTexture(0, 0, THREE.FloatType),
|
||||
})
|
||||
}
|
||||
this.composer = new EffectComposer(this.renderer, renderTarget)
|
||||
this.renderPass = new RenderPass(this.scene, this.camera)
|
||||
this.composer.addPass(this.renderPass)
|
||||
this.fxaaPass = new ShaderPass(FXAAShader)
|
||||
this.composer.addPass(this.fxaaPass)
|
||||
this.updateComposerSize()
|
||||
this.enableFXAA = true
|
||||
}
|
||||
|
||||
// todo
|
||||
updateComposerSize (): void {
|
||||
if (!this.composer) return
|
||||
const { width, height } = this.renderer.getSize(new THREE.Vector2())
|
||||
this.composer.setSize(width, height)
|
||||
// todo auto-update
|
||||
const pixelRatio = this.renderer.getPixelRatio()
|
||||
this.composer.setPixelRatio(pixelRatio)
|
||||
this.fxaaPass.material.uniforms["resolution"].value.x = 1 / (width * pixelRatio)
|
||||
this.fxaaPass.material.uniforms["resolution"].value.y = 1 / (height * pixelRatio)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
120
prismarine-viewer/viewer/lib/viewerWrapper.ts
Normal file
120
prismarine-viewer/viewer/lib/viewerWrapper.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { statsEnd, statsStart } from '../../../src/topRightStats'
|
||||
|
||||
// wrapper for now
|
||||
export class ViewerWrapper {
|
||||
previousWindowWidth: number
|
||||
previousWindowHeight: number
|
||||
globalObject = globalThis as any
|
||||
stopRenderOnBlur = true
|
||||
addedToPage = false
|
||||
renderInterval = 0
|
||||
fpsInterval
|
||||
|
||||
constructor(public canvas: HTMLCanvasElement, public renderer?: THREE.WebGLRenderer) {
|
||||
}
|
||||
addToPage (startRendering = true) {
|
||||
if (this.addedToPage) throw new Error('Already added to page')
|
||||
let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
|
||||
if (this.renderer) {
|
||||
if (!this.renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped)
|
||||
this.renderer.setPixelRatio(pixelRatio)
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
} else {
|
||||
this.canvas.width = window.innerWidth * pixelRatio
|
||||
this.canvas.height = window.innerHeight * pixelRatio
|
||||
}
|
||||
this.previousWindowWidth = window.innerWidth
|
||||
this.previousWindowHeight = window.innerHeight
|
||||
|
||||
this.canvas.id = 'viewer-canvas'
|
||||
document.body.appendChild(this.canvas)
|
||||
|
||||
if (this.renderer) this.globalObject.renderer = this.renderer
|
||||
this.addedToPage = true
|
||||
|
||||
let max = 0
|
||||
this.fpsInterval = setInterval(() => {
|
||||
if (max > 0) {
|
||||
viewer.world.droppedFpsPercentage = this.renderedFps / max
|
||||
}
|
||||
max = Math.max(this.renderedFps, max)
|
||||
this.renderedFps = 0
|
||||
}, 1000)
|
||||
if (startRendering) {
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
// this.trackWindowFocus()
|
||||
}
|
||||
}
|
||||
|
||||
windowFocused = true
|
||||
trackWindowFocus () {
|
||||
window.addEventListener('focus', () => {
|
||||
this.windowFocused = true
|
||||
})
|
||||
window.addEventListener('blur', () => {
|
||||
this.windowFocused = false
|
||||
})
|
||||
}
|
||||
|
||||
dispose () {
|
||||
if (!this.addedToPage) throw new Error('Not added to page')
|
||||
document.body.removeChild(this.canvas)
|
||||
this.renderer?.dispose()
|
||||
// this.addedToPage = false
|
||||
clearInterval(this.fpsInterval)
|
||||
}
|
||||
|
||||
|
||||
renderedFps = 0
|
||||
lastTime = performance.now()
|
||||
delta = 0
|
||||
preRender = () => { }
|
||||
postRender = () => { }
|
||||
render (time: DOMHighResTimeStamp) {
|
||||
if (this.globalObject.stopLoop) return
|
||||
for (const fn of beforeRenderFrame) fn()
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
if (this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return
|
||||
if (this.renderInterval) {
|
||||
this.delta += time - this.lastTime
|
||||
this.lastTime = time
|
||||
if (this.delta > this.renderInterval) {
|
||||
this.delta %= this.renderInterval
|
||||
// continue rendering
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.preRender()
|
||||
statsStart()
|
||||
// ios bug: viewport dimensions are updated after the resize event
|
||||
if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) {
|
||||
this.resizeHandler()
|
||||
this.previousWindowWidth = window.innerWidth
|
||||
this.previousWindowHeight = window.innerHeight
|
||||
}
|
||||
viewer.render()
|
||||
this.renderedFps++
|
||||
statsEnd()
|
||||
this.postRender()
|
||||
}
|
||||
|
||||
resizeHandler () {
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight
|
||||
|
||||
viewer.camera.aspect = width / height
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.setSize(width, height)
|
||||
}
|
||||
// canvas updated by renderer
|
||||
|
||||
// if (viewer.composer) {
|
||||
// viewer.updateComposerSize()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,11 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
updateViewDistance (viewDistance: number) {
|
||||
this.viewDistance = viewDistance
|
||||
this.emitter.emit('renderDistance', viewDistance)
|
||||
}
|
||||
|
||||
listenToBot (bot: typeof __type_bot) {
|
||||
const emitEntity = (e) => {
|
||||
if (!e || e === bot.entity) return
|
||||
|
|
@ -73,6 +78,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
return bot.world.getBlock(new Vec3(x, y, z)).entity
|
||||
},
|
||||
}))
|
||||
this.emitter.emit('renderDistance', this.viewDistance)
|
||||
})
|
||||
// node.js stream data event pattern
|
||||
if (this.emitter.listenerCount('blockEntities')) {
|
||||
|
|
@ -97,6 +103,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
}
|
||||
|
||||
async init (pos: Vec3) {
|
||||
this.updateViewDistance(this.viewDistance)
|
||||
this.emitter.emit('chunkPosUpdate', { pos })
|
||||
const [botX, botZ] = chunkPos(pos)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,373 +0,0 @@
|
|||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { loadTexture, loadJSON } from './utils'
|
||||
import { EventEmitter } from 'events'
|
||||
import mcDataRaw from 'minecraft-data/data.js' // handled correctly in esbuild plugin
|
||||
import nbt from 'prismarine-nbt'
|
||||
import { dynamicMcDataFiles } from '../../buildWorkerConfig.mjs'
|
||||
import { dispose3 } from './dispose'
|
||||
import { toMajor } from './version.js'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { renderSign } from '../sign-renderer/'
|
||||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
|
||||
export class WorldRenderer {
|
||||
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 })
|
||||
|
||||
blockEntities = {}
|
||||
sectionObjects: Record<string, THREE.Object3D> = {}
|
||||
showChunkBorders = false
|
||||
active = false
|
||||
version = undefined as string | undefined
|
||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||
loadedChunks = {}
|
||||
sectionsOutstanding = new Set()
|
||||
renderUpdateEmitter = new EventEmitter()
|
||||
customBlockStatesData = undefined as any
|
||||
customTexturesDataUrl = undefined as string | undefined
|
||||
downloadedBlockStatesData = undefined as any
|
||||
downloadedTextureImage = undefined as any
|
||||
workers: any[] = []
|
||||
viewerPosition?: Vec3
|
||||
lastCamUpdate = 0
|
||||
droppedFpsPercentage = 0
|
||||
initialChunksLoad = true
|
||||
enableChunksLoadDelay = false
|
||||
|
||||
texturesVersion?: string
|
||||
|
||||
promisesQueue = [] as Promise<any>[]
|
||||
|
||||
constructor(public scene: THREE.Scene, numWorkers = 4) {
|
||||
// init workers
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
// Node environment needs an absolute path, but browser needs the url of the file
|
||||
let src = __dirname
|
||||
if (typeof window === 'undefined') src += '/worker.js'
|
||||
else src = 'worker.js'
|
||||
|
||||
const worker: any = new Worker(src)
|
||||
worker.onmessage = async ({ data }) => {
|
||||
if (!this.active) return
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
if (data.type === 'geometry') {
|
||||
let object: THREE.Object3D = this.sectionObjects[data.key]
|
||||
if (object) {
|
||||
this.scene.remove(object)
|
||||
dispose3(object)
|
||||
delete this.sectionObjects[data.key]
|
||||
}
|
||||
|
||||
const chunkCoords = data.key.split(',')
|
||||
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
|
||||
|
||||
// if (!this.initialChunksLoad && this.enableChunksLoadDelay) {
|
||||
// const newPromise = new Promise(resolve => {
|
||||
// if (this.droppedFpsPercentage > 0.5) {
|
||||
// setTimeout(resolve, 1000 / 50 * this.droppedFpsPercentage)
|
||||
// } else {
|
||||
// setTimeout(resolve)
|
||||
// }
|
||||
// })
|
||||
// this.promisesQueue.push(newPromise)
|
||||
// for (const promise of this.promisesQueue) {
|
||||
// await promise
|
||||
// }
|
||||
// }
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
|
||||
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
|
||||
geometry.setIndex(data.geometry.indices)
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, this.material)
|
||||
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
mesh.name = 'mesh'
|
||||
object = new THREE.Group()
|
||||
object.add(mesh)
|
||||
const boxHelper = new THREE.BoxHelper(mesh, 0xffff00)
|
||||
boxHelper.name = 'helper'
|
||||
object.add(boxHelper)
|
||||
object.name = 'chunk'
|
||||
if (!this.showChunkBorders) {
|
||||
boxHelper.visible = false
|
||||
}
|
||||
// should not compute it once
|
||||
if (Object.keys(data.geometry.signs).length) {
|
||||
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.signs)) {
|
||||
const [x, y, z] = posKey.split(',')
|
||||
const signBlockEntity = this.blockEntities[posKey]
|
||||
if (!signBlockEntity) continue
|
||||
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity));
|
||||
if (!sign) continue
|
||||
object.add(sign)
|
||||
}
|
||||
}
|
||||
this.sectionObjects[data.key] = object
|
||||
this.updatePosDataChunk(data.key)
|
||||
this.scene.add(object)
|
||||
} else if (data.type === 'sectionFinished') {
|
||||
this.sectionsOutstanding.delete(data.key)
|
||||
this.renderUpdateEmitter.emit('update')
|
||||
}
|
||||
}
|
||||
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
|
||||
this.workers.push(worker)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
// 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')!
|
||||
section.renderOrder = 500 - chunkDistance
|
||||
}
|
||||
|
||||
updateViewerPosition (pos: Vec3) {
|
||||
this.viewerPosition = pos
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
this.updatePosDataChunk(key)
|
||||
}
|
||||
}
|
||||
|
||||
signsCache = new Map<string, any>()
|
||||
|
||||
getSignTexture (position: Vec3, blockEntity, backSide = false) {
|
||||
const chunk = chunkPos(position)
|
||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||
if (!textures) {
|
||||
textures = {}
|
||||
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
||||
}
|
||||
const texturekey = `${position.x},${position.y},${position.z}`;
|
||||
// todo investigate bug and remove this so don't need to clean in section dirty
|
||||
if (textures[texturekey]) return textures[texturekey]
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(this.version!)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
if (!canvas) return
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.magFilter = THREE.NearestFilter
|
||||
tex.minFilter = THREE.NearestFilter
|
||||
tex.needsUpdate = true
|
||||
textures[texturekey] = tex
|
||||
return tex
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||
const tex = this.getSignTexture(position, blockEntity)
|
||||
|
||||
if (!tex) return
|
||||
|
||||
// todo implement
|
||||
// const key = JSON.stringify({ position, rotation, isWall })
|
||||
// if (this.signsCache.has(key)) {
|
||||
// console.log('cached', key)
|
||||
// } else {
|
||||
// this.signsCache.set(key, tex)
|
||||
// }
|
||||
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true, }))
|
||||
mesh.renderOrder = 999
|
||||
|
||||
// todo @sa2urami shouldnt all this be done in worker?
|
||||
mesh.scale.set(1, 7 / 16, 1)
|
||||
if (isWall) {
|
||||
mesh.position.set(0, 0, -(8 - 1.5) / 16 + 0.001)
|
||||
} else {
|
||||
// standing
|
||||
const faceEnd = 8.75
|
||||
mesh.position.set(0, 0, (faceEnd - 16 / 2) / 16 + 0.001)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.rotation.set(0, -THREE.MathUtils.degToRad(
|
||||
rotation * (isWall ? 90 : 45 / 2)
|
||||
), 0)
|
||||
group.add(mesh)
|
||||
const y = isWall ? 4.5 / 16 + mesh.scale.y / 2 : (1 - (mesh.scale.y / 2))
|
||||
group.position.set(position.x + 0.5, position.y + y, position.z + 0.5)
|
||||
return group
|
||||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
this.showChunkBorders = value
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
for (const child of object.children) {
|
||||
if (child.name === 'helper') {
|
||||
child.visible = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetWorld () {
|
||||
this.active = false
|
||||
for (const mesh of Object.values(this.sectionObjects)) {
|
||||
this.scene.remove(mesh)
|
||||
}
|
||||
this.sectionObjects = {}
|
||||
this.loadedChunks = {}
|
||||
this.sectionsOutstanding = new Set()
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'reset' })
|
||||
}
|
||||
}
|
||||
|
||||
setVersion (version, texturesVersion = version) {
|
||||
this.version = version
|
||||
this.texturesVersion = texturesVersion
|
||||
this.resetWorld()
|
||||
this.active = true
|
||||
|
||||
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajor(this.version)]
|
||||
for (const worker of this.workers) {
|
||||
const mcData = Object.fromEntries(Object.entries(allMcData).filter(([key]) => dynamicMcDataFiles.includes(key)))
|
||||
mcData.version = JSON.parse(JSON.stringify(mcData.version))
|
||||
worker.postMessage({ type: 'mcData', mcData, version: this.version })
|
||||
}
|
||||
|
||||
this.updateTexturesData()
|
||||
}
|
||||
|
||||
updateTexturesData () {
|
||||
loadTexture(this.customTexturesDataUrl || `textures/${this.texturesVersion}.png`, (texture: import('three').Texture) => {
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
this.material.map = texture
|
||||
this.material.map.onUpdate = () => {
|
||||
this.downloadedTextureImage = this.material.map!.image
|
||||
}
|
||||
})
|
||||
|
||||
const loadBlockStates = async () => {
|
||||
return new Promise(resolve => {
|
||||
if (this.customBlockStatesData) return resolve(this.customBlockStatesData)
|
||||
return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => {
|
||||
this.downloadedBlockStatesData = data
|
||||
// todo
|
||||
this.renderUpdateEmitter.emit('blockStatesDownloaded')
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
loadBlockStates().then((blockStates) => {
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'blockStates', json: blockStates })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
||||
const [currentX, currentY, currentZ] = sectionPos(pos)
|
||||
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
|
||||
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
|
||||
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
|
||||
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
|
||||
return [setKey, o]
|
||||
}))
|
||||
}
|
||||
|
||||
addColumn (x, z, chunk) {
|
||||
this.initialChunksLoad = false
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'chunk', x, z, chunk })
|
||||
}
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
const loc = new Vec3(x, y, z)
|
||||
this.setSectionDirty(loc)
|
||||
this.setSectionDirty(loc.offset(-16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(0, 0, -16))
|
||||
this.setSectionDirty(loc.offset(0, 0, 16))
|
||||
}
|
||||
}
|
||||
|
||||
cleanChunkTextures (x, z) {
|
||||
const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {}
|
||||
for (const key of Object.keys(textures)) {
|
||||
textures[key].dispose()
|
||||
delete textures[key]
|
||||
}
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
this.cleanChunkTextures(x, z)
|
||||
|
||||
delete this.loadedChunks[`${x},${z}`]
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'unloadChunk', x, z })
|
||||
}
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
const key = `${x},${y},${z}`
|
||||
const mesh = this.sectionObjects[key]
|
||||
if (mesh) {
|
||||
this.scene.remove(mesh)
|
||||
dispose3(mesh)
|
||||
}
|
||||
delete this.sectionObjects[key]
|
||||
}
|
||||
}
|
||||
|
||||
setBlockStateId (pos, stateId) {
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'blockUpdate', pos, stateId })
|
||||
}
|
||||
this.setSectionDirty(pos)
|
||||
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))
|
||||
}
|
||||
|
||||
setSectionDirty (pos, value = true) {
|
||||
this.renderUpdateEmitter.emit('dirty', pos, value)
|
||||
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
||||
// 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)
|
||||
this.workers[hash].postMessage({ type: 'dirty', x: pos.x, y: pos.y, z: pos.z, value })
|
||||
this.sectionsOutstanding.add(`${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`)
|
||||
}
|
||||
|
||||
// Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number
|
||||
// of sections not rendered are 0
|
||||
async waitForChunksToRender () {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if ([...this.sectionsOutstanding].length === 0) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const updateHandler = () => {
|
||||
if (this.sectionsOutstanding.size === 0) {
|
||||
this.renderUpdateEmitter.removeListener('update', updateHandler)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
this.renderUpdateEmitter.on('update', updateHandler)
|
||||
})
|
||||
}
|
||||
}
|
||||
246
prismarine-viewer/viewer/lib/worldrendererCommon.ts
Normal file
246
prismarine-viewer/viewer/lib/worldrendererCommon.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { loadJSON } from './utils'
|
||||
import { loadTexture } from './utils.web'
|
||||
import { EventEmitter } from 'events'
|
||||
import mcDataRaw from 'minecraft-data/data.js' // handled correctly in esbuild plugin
|
||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajor } from './version.js'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
|
||||
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
|
||||
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 })
|
||||
|
||||
showChunkBorders = false
|
||||
active = false
|
||||
version = undefined as string | undefined
|
||||
loadedChunks = {} as Record<string, boolean>
|
||||
finishedChunks = {} as Record<string, boolean>
|
||||
sectionsOutstanding = new Map<string, number>()
|
||||
renderUpdateEmitter = new EventEmitter()
|
||||
customBlockStatesData = undefined as any
|
||||
customTexturesDataUrl = undefined as string | undefined
|
||||
downloadedBlockStatesData = undefined as any
|
||||
downloadedTextureImage = undefined as any
|
||||
workers: any[] = []
|
||||
viewerPosition?: Vec3
|
||||
lastCamUpdate = 0
|
||||
droppedFpsPercentage = 0
|
||||
initialChunksLoad = true
|
||||
enableChunksLoadDelay = false
|
||||
texturesVersion?: string
|
||||
viewDistance = -1
|
||||
chunksLength = 0
|
||||
|
||||
abstract outputFormat: 'threeJs' | 'webgl'
|
||||
|
||||
constructor(numWorkers: number) {
|
||||
// init workers
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
// Node environment needs an absolute path, but browser needs the url of the file
|
||||
const workerName = 'mesher.js'
|
||||
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
|
||||
|
||||
const worker: any = new Worker(src)
|
||||
worker.onmessage = async ({ data }) => {
|
||||
if (!this.active) return
|
||||
this.handleWorkerMessage(data)
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
if (data.type === 'sectionFinished') {
|
||||
if (!this.sectionsOutstanding.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`)
|
||||
this.sectionsOutstanding.set(data.key, this.sectionsOutstanding.get(data.key)! - 1)
|
||||
if (this.sectionsOutstanding.get(data.key) === 0) this.sectionsOutstanding.delete(data.key)
|
||||
|
||||
const chunkCoords = data.key.split(',').map(Number)
|
||||
if (this.loadedChunks[`${chunkCoords[0]},${chunkCoords[2]}`]) { // ensure chunk data was added, not a neighbor chunk update
|
||||
const loadingKeys = [...this.sectionsOutstanding.keys()]
|
||||
if (!loadingKeys.some(key => {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
return x === chunkCoords[0] && z === chunkCoords[2]
|
||||
})) {
|
||||
this.finishedChunks[`${chunkCoords[0]},${chunkCoords[2]}`] = true
|
||||
}
|
||||
}
|
||||
if (this.sectionsOutstanding.size === 0) {
|
||||
const allFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
if (allFinished) {
|
||||
this.allChunksLoaded?.()
|
||||
}
|
||||
}
|
||||
|
||||
this.renderUpdateEmitter.emit('update')
|
||||
}
|
||||
}
|
||||
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
|
||||
this.workers.push(worker)
|
||||
}
|
||||
}
|
||||
|
||||
abstract handleWorkerMessage (data: WorkerReceive): void
|
||||
|
||||
abstract updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void
|
||||
|
||||
abstract render (): void
|
||||
|
||||
/**
|
||||
* Optionally update data that are depedendent on the viewer position
|
||||
*/
|
||||
updatePosDataChunk?(key: string): void
|
||||
|
||||
allChunksLoaded?(): void
|
||||
|
||||
updateViewerPosition (pos: Vec3) {
|
||||
this.viewerPosition = pos
|
||||
for (const [key, value] of Object.entries(this.loadedChunks)) {
|
||||
if (!value) continue
|
||||
this.updatePosDataChunk?.(key)
|
||||
}
|
||||
}
|
||||
|
||||
sendWorkers (message: WorkerSend) {
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
getDistance (posAbsolute: Vec3) {
|
||||
const [botX, botZ] = chunkPos(this.viewerPosition!)
|
||||
const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16))
|
||||
const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16))
|
||||
return [dx, dz] as [number, number]
|
||||
}
|
||||
|
||||
abstract updateShowChunksBorder (value: boolean): void
|
||||
|
||||
resetWorld () {
|
||||
this.active = false
|
||||
this.loadedChunks = {}
|
||||
this.sectionsOutstanding = new Map()
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'reset' })
|
||||
}
|
||||
}
|
||||
|
||||
setVersion (version, texturesVersion = version) {
|
||||
this.version = version
|
||||
this.texturesVersion = texturesVersion
|
||||
this.resetWorld()
|
||||
this.active = true
|
||||
|
||||
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajor(this.version)]
|
||||
for (const worker of this.workers) {
|
||||
const mcData = Object.fromEntries(Object.entries(allMcData).filter(([key]) => dynamicMcDataFiles.includes(key)))
|
||||
mcData.version = JSON.parse(JSON.stringify(mcData.version))
|
||||
worker.postMessage({ type: 'mcData', mcData, version: this.version })
|
||||
}
|
||||
|
||||
this.updateTexturesData()
|
||||
}
|
||||
|
||||
updateTexturesData () {
|
||||
loadTexture(this.customTexturesDataUrl || `textures/${this.texturesVersion}.png`, (texture: import('three').Texture) => {
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
this.material.map = texture
|
||||
}, (tex) => {
|
||||
this.downloadedTextureImage = this.material.map!.image
|
||||
const loadBlockStates = async () => {
|
||||
return new Promise(resolve => {
|
||||
if (this.customBlockStatesData) return resolve(this.customBlockStatesData)
|
||||
return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => {
|
||||
this.downloadedBlockStatesData = data
|
||||
// todo
|
||||
this.renderUpdateEmitter.emit('blockStatesDownloaded')
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
loadBlockStates().then((blockStates) => {
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'rendererData', json: blockStates, textureSize: tex.image.width, outputFormat: this.outputFormat })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
addColumn (x, z, chunk) {
|
||||
this.initialChunksLoad = false
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
for (const worker of this.workers) {
|
||||
// todo optimize
|
||||
worker.postMessage({ type: 'chunk', x, z, chunk })
|
||||
}
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
const loc = new Vec3(x, y, z)
|
||||
this.setSectionDirty(loc)
|
||||
this.setSectionDirty(loc.offset(-16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(0, 0, -16))
|
||||
this.setSectionDirty(loc.offset(0, 0, 16))
|
||||
}
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
delete this.loadedChunks[`${x},${z}`]
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'unloadChunk', x, z })
|
||||
}
|
||||
}
|
||||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'blockUpdate', pos, stateId })
|
||||
}
|
||||
this.setSectionDirty(pos)
|
||||
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))
|
||||
}
|
||||
|
||||
setSectionDirty (pos: Vec3, value = true) {
|
||||
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
||||
const distance = this.getDistance(pos)
|
||||
if (distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
|
||||
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
// if (this.sectionsOutstanding.has(key)) return
|
||||
this.renderUpdateEmitter.emit('dirty', pos, value)
|
||||
// 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)
|
||||
this.sectionsOutstanding.set(key, (this.sectionsOutstanding.get(key) ?? 0) + 1)
|
||||
this.workers[hash].postMessage({ type: 'dirty', x: pos.x, y: pos.y, z: pos.z, value })
|
||||
}
|
||||
|
||||
// Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number
|
||||
// of sections not rendered are 0
|
||||
async waitForChunksToRender () {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if ([...this.sectionsOutstanding].length === 0) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const updateHandler = () => {
|
||||
if (this.sectionsOutstanding.size === 0) {
|
||||
this.renderUpdateEmitter.removeListener('update', updateHandler)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
this.renderUpdateEmitter.on('update', updateHandler)
|
||||
})
|
||||
}
|
||||
}
|
||||
236
prismarine-viewer/viewer/lib/worldrendererThree.ts
Normal file
236
prismarine-viewer/viewer/lib/worldrendererThree.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import nbt from 'prismarine-nbt'
|
||||
import { dispose3 } from './dispose'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { renderSign } from '../sign-renderer/'
|
||||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
import { WorldRendererCommon } from './worldrendererCommon'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
|
||||
export class WorldRendererThree extends WorldRendererCommon {
|
||||
outputFormat = 'threeJs' as const
|
||||
blockEntities = {}
|
||||
sectionObjects: Record<string, THREE.Object3D> = {}
|
||||
showChunkBorders = false
|
||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||
signsCache = new Map<string, any>()
|
||||
|
||||
constructor(public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public camera: THREE.PerspectiveCamera, numWorkers = 4) {
|
||||
super(numWorkers)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
// 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')!
|
||||
section.renderOrder = 500 - chunkDistance
|
||||
}
|
||||
|
||||
updateViewerPosition (pos: Vec3): void {
|
||||
this.viewerPosition = pos
|
||||
for (const [key, value] of Object.entries(this.sectionObjects)) {
|
||||
if (!value) continue
|
||||
this.updatePosDataChunk(key)
|
||||
}
|
||||
}
|
||||
|
||||
handleWorkerMessage (data: any): void {
|
||||
if (data.type !== 'geometry') return
|
||||
let object: THREE.Object3D = this.sectionObjects[data.key]
|
||||
if (object) {
|
||||
this.scene.remove(object)
|
||||
dispose3(object)
|
||||
delete this.sectionObjects[data.key]
|
||||
}
|
||||
|
||||
const chunkCoords = data.key.split(',')
|
||||
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
|
||||
|
||||
// if (!this.initialChunksLoad && this.enableChunksLoadDelay) {
|
||||
// const newPromise = new Promise(resolve => {
|
||||
// if (this.droppedFpsPercentage > 0.5) {
|
||||
// setTimeout(resolve, 1000 / 50 * this.droppedFpsPercentage)
|
||||
// } else {
|
||||
// setTimeout(resolve)
|
||||
// }
|
||||
// })
|
||||
// this.promisesQueue.push(newPromise)
|
||||
// for (const promise of this.promisesQueue) {
|
||||
// await promise
|
||||
// }
|
||||
// }
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
|
||||
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
|
||||
geometry.setIndex(data.geometry.indices)
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, this.material)
|
||||
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
mesh.name = 'mesh'
|
||||
object = new THREE.Group()
|
||||
object.add(mesh)
|
||||
const boxHelper = new THREE.BoxHelper(mesh, 0xffff00)
|
||||
boxHelper.name = 'helper'
|
||||
object.add(boxHelper)
|
||||
object.name = 'chunk'
|
||||
if (!this.showChunkBorders) {
|
||||
boxHelper.visible = false
|
||||
}
|
||||
// should not compute it once
|
||||
if (Object.keys(data.geometry.signs).length) {
|
||||
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.signs)) {
|
||||
const [x, y, z] = posKey.split(',')
|
||||
const signBlockEntity = this.blockEntities[posKey]
|
||||
if (!signBlockEntity) continue
|
||||
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity))
|
||||
if (!sign) continue
|
||||
object.add(sign)
|
||||
}
|
||||
}
|
||||
this.sectionObjects[data.key] = object
|
||||
this.updatePosDataChunk(data.key)
|
||||
this.scene.add(object)
|
||||
}
|
||||
|
||||
getSignTexture (position: Vec3, blockEntity, backSide = false) {
|
||||
const chunk = chunkPos(position)
|
||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||
if (!textures) {
|
||||
textures = {}
|
||||
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
||||
}
|
||||
const texturekey = `${position.x},${position.y},${position.z}`
|
||||
// todo investigate bug and remove this so don't need to clean in section dirty
|
||||
if (textures[texturekey]) return textures[texturekey]
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(this.version!)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
if (!canvas) return
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.magFilter = THREE.NearestFilter
|
||||
tex.minFilter = THREE.NearestFilter
|
||||
tex.needsUpdate = true
|
||||
textures[texturekey] = tex
|
||||
return tex
|
||||
}
|
||||
|
||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void {
|
||||
if (pos) {
|
||||
new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
||||
}
|
||||
this.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
}
|
||||
|
||||
render () {
|
||||
tweenJs.update()
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||
const tex = this.getSignTexture(position, blockEntity)
|
||||
|
||||
if (!tex) return
|
||||
|
||||
// todo implement
|
||||
// const key = JSON.stringify({ position, rotation, isWall })
|
||||
// if (this.signsCache.has(key)) {
|
||||
// console.log('cached', key)
|
||||
// } else {
|
||||
// this.signsCache.set(key, tex)
|
||||
// }
|
||||
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true, }))
|
||||
mesh.renderOrder = 999
|
||||
|
||||
// todo @sa2urami shouldnt all this be done in worker?
|
||||
mesh.scale.set(1, 7 / 16, 1)
|
||||
if (isWall) {
|
||||
mesh.position.set(0, 0, -(8 - 1.5) / 16 + 0.001)
|
||||
} else {
|
||||
// standing
|
||||
const faceEnd = 8.75
|
||||
mesh.position.set(0, 0, (faceEnd - 16 / 2) / 16 + 0.001)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.rotation.set(0, -THREE.MathUtils.degToRad(
|
||||
rotation * (isWall ? 90 : 45 / 2)
|
||||
), 0)
|
||||
group.add(mesh)
|
||||
const y = isWall ? 4.5 / 16 + mesh.scale.y / 2 : (1 - (mesh.scale.y / 2))
|
||||
group.position.set(position.x + 0.5, position.y + y, position.z + 0.5)
|
||||
return group
|
||||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
this.showChunkBorders = value
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
for (const child of object.children) {
|
||||
if (child.name === 'helper') {
|
||||
child.visible = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetWorld () {
|
||||
super.resetWorld()
|
||||
|
||||
for (const mesh of Object.values(this.sectionObjects)) {
|
||||
this.scene.remove(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
||||
const [currentX, currentY, currentZ] = sectionPos(pos)
|
||||
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
|
||||
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
|
||||
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
|
||||
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
|
||||
return [setKey, o]
|
||||
}))
|
||||
}
|
||||
|
||||
cleanChunkTextures (x, z) {
|
||||
const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {}
|
||||
for (const key of Object.keys(textures)) {
|
||||
textures[key].dispose()
|
||||
delete textures[key]
|
||||
}
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
super.removeColumn(x, z)
|
||||
|
||||
this.cleanChunkTextures(x, z)
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
const key = `${x},${y},${z}`
|
||||
const mesh = this.sectionObjects[key]
|
||||
if (mesh) {
|
||||
this.scene.remove(mesh)
|
||||
dispose3(mesh)
|
||||
}
|
||||
delete this.sectionObjects[key]
|
||||
}
|
||||
}
|
||||
|
||||
setSectionDirty (pos, value = true) {
|
||||
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
||||
super.setSectionDirty(pos, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ const entityMcAssets = McAssets('1.16.4')
|
|||
// these files could be copied at build time eg with copy plugin, but copy plugin slows down the config so we copy them there, alternative we could inline it in esbuild config
|
||||
const filesToCopy = [
|
||||
{ from: `${prismarineViewerBase}/public/blocksStates/`, to: 'dist/blocksStates/' },
|
||||
{ from: `${prismarineViewerBase}/public/worker.js`, to: 'dist/worker.js' },
|
||||
{ from: `${prismarineViewerBase}/public/mesher.js`, to: 'dist/mesher.js' },
|
||||
{ from: './assets/', to: './dist/' },
|
||||
{ from: './config.json', to: 'dist/config.json' },
|
||||
{ from: path.join(entityMcAssets.directory, 'entity'), to: 'dist/textures/1.16.4/entity' },
|
||||
|
|
@ -60,7 +60,8 @@ exports.getSwAdditionalEntries = () => {
|
|||
'*.ttf',
|
||||
'*.png',
|
||||
'*.woff',
|
||||
'worker.js',
|
||||
'mesher.js',
|
||||
'worldSaveWorker.js',
|
||||
// todo-low preload entity atlas?
|
||||
`textures/${singlePlayerVersion}.png`,
|
||||
`textures/1.16.4/entity/squid.png`,
|
||||
|
|
@ -68,7 +69,8 @@ exports.getSwAdditionalEntries = () => {
|
|||
const filesNeedsCacheKey = [
|
||||
'index.js',
|
||||
'index.css',
|
||||
'worker.js',
|
||||
'mesher.js',
|
||||
'worldSaveWorker.js',
|
||||
]
|
||||
const output = []
|
||||
console.log('Generating sw additional entries...')
|
||||
|
|
@ -92,7 +94,7 @@ exports.getSwAdditionalEntries = () => {
|
|||
}
|
||||
|
||||
exports.moveStorybookFiles = () => {
|
||||
fsExtra.moveSync('storybook-static', 'dist/storybook', {overwrite: true,})
|
||||
fsExtra.moveSync('storybook-static', 'dist/storybook', { overwrite: true, })
|
||||
fsExtra.copySync('dist/storybook', '.vercel/output/static/storybook')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ const { supportedVersions } = MCProtocol
|
|||
const prod = process.argv.includes('--prod')
|
||||
let connectedClients = []
|
||||
|
||||
const watchExternal = [
|
||||
'dist/mesher.js',
|
||||
'dist/webglRendererWorker.js'
|
||||
]
|
||||
|
||||
/** @type {import('esbuild').Plugin[]} */
|
||||
const plugins = [
|
||||
{
|
||||
|
|
@ -41,6 +46,8 @@ const plugins = [
|
|||
return {
|
||||
contents: `window.mcData ??= ${JSON.stringify(defaultVersionsObj)};module.exports = { pc: window.mcData }`,
|
||||
loader: 'js',
|
||||
// todo use external watchers
|
||||
watchFiles: watchExternal,
|
||||
}
|
||||
})
|
||||
build.onResolve({
|
||||
|
|
@ -123,8 +130,26 @@ const plugins = [
|
|||
let count = 0
|
||||
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[]} */
|
||||
|
|
@ -147,7 +172,9 @@ const plugins = [
|
|||
/** @type {import('esbuild').OutputFile} */
|
||||
//@ts-ignore
|
||||
const outputFile = outputFiles.find(x => x.path.endsWith('.js'))
|
||||
if (outputFile.hash === prevHash) {
|
||||
const updateWorkers = updateMtime()
|
||||
if (outputFile.hash === prevHash && !updateWorkers) {
|
||||
// todo also check workers and css
|
||||
console.log('Ignoring reload as contents the same')
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// global variables useful for debugging
|
||||
|
||||
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
|
||||
import { getEntityCursor } from './worldInteractions'
|
||||
|
||||
// Object.defineProperty(window, 'cursorBlock', )
|
||||
|
|
@ -19,6 +20,6 @@ window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/
|
|||
|
||||
Object.defineProperty(window, 'debugSceneChunks', {
|
||||
get () {
|
||||
return viewer.world.getLoadedChunksRelative(bot.entity.position, true)
|
||||
return (viewer.world as WorldRendererThree).getLoadedChunksRelative?.(bot.entity.position, true)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
79
src/index.ts
79
src/index.ts
|
|
@ -96,6 +96,7 @@ import { handleMovementStickDelta, joystickPointer } from './react/TouchAreasCon
|
|||
import { possiblyHandleStateVariable } from './googledrive'
|
||||
import flyingSquidEvents from './flyingSquidEvents'
|
||||
import { hideNotification, notificationProxy } from './react/NotificationProvider'
|
||||
import { ViewerWrapper } from 'prismarine-viewer/viewer/lib/viewerWrapper'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
|
|
@ -121,13 +122,11 @@ try {
|
|||
|
||||
// renderer.localClippingEnabled = true
|
||||
initWithRenderer(renderer.domElement)
|
||||
window.renderer = renderer
|
||||
let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
|
||||
if (!renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped)
|
||||
renderer.setPixelRatio(pixelRatio)
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
renderer.domElement.id = 'viewer-canvas'
|
||||
document.body.appendChild(renderer.domElement)
|
||||
const renderWrapper = new ViewerWrapper(renderer.domElement, renderer)
|
||||
renderWrapper.addToPage()
|
||||
watchValue(options, (o) => {
|
||||
renderWrapper.renderInterval = o.frameLimit ? 1000 / o.frameLimit : 0
|
||||
})
|
||||
|
||||
const isFirefox = ua.getBrowser().name === 'Firefox'
|
||||
if (isFirefox) {
|
||||
|
|
@ -166,68 +165,6 @@ viewer.entities.entitiesOptions = {
|
|||
watchOptionsAfterViewerInit()
|
||||
watchTexturepackInViewer(viewer)
|
||||
|
||||
let renderInterval: number | false
|
||||
watchValue(options, (o) => {
|
||||
renderInterval = o.frameLimit && 1000 / o.frameLimit
|
||||
})
|
||||
|
||||
let postRenderFrameFn = () => { }
|
||||
let delta = 0
|
||||
let lastTime = performance.now()
|
||||
let previousWindowWidth = window.innerWidth
|
||||
let previousWindowHeight = window.innerHeight
|
||||
let max = 0
|
||||
let rendered = 0
|
||||
const renderFrame = (time: DOMHighResTimeStamp) => {
|
||||
if (window.stopLoop) return
|
||||
for (const fn of beforeRenderFrame) fn()
|
||||
window.requestAnimationFrame(renderFrame)
|
||||
if (window.stopRender || renderer.xr.isPresenting) return
|
||||
if (renderInterval) {
|
||||
delta += time - lastTime
|
||||
lastTime = time
|
||||
if (delta > renderInterval) {
|
||||
delta %= renderInterval
|
||||
// continue rendering
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
// ios bug: viewport dimensions are updated after the resize event
|
||||
if (previousWindowWidth !== window.innerWidth || previousWindowHeight !== window.innerHeight) {
|
||||
resizeHandler()
|
||||
previousWindowWidth = window.innerWidth
|
||||
previousWindowHeight = window.innerHeight
|
||||
}
|
||||
statsStart()
|
||||
viewer.update()
|
||||
viewer.render()
|
||||
rendered++
|
||||
postRenderFrameFn()
|
||||
statsEnd()
|
||||
}
|
||||
renderFrame(performance.now())
|
||||
setInterval(() => {
|
||||
if (max > 0) {
|
||||
viewer.world.droppedFpsPercentage = rendered / max
|
||||
}
|
||||
max = Math.max(rendered, max)
|
||||
rendered = 0
|
||||
}, 1000)
|
||||
|
||||
const resizeHandler = () => {
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight
|
||||
|
||||
viewer.camera.aspect = width / height
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
renderer.setSize(width, height)
|
||||
|
||||
if (viewer.composer) {
|
||||
viewer.updateComposerSize()
|
||||
}
|
||||
}
|
||||
|
||||
const hud = document.getElementById('hud')
|
||||
const pauseMenu = document.getElementById('pause-screen')
|
||||
|
||||
|
|
@ -345,7 +282,7 @@ async function connect (connectOptions: {
|
|||
viewer.resetAll()
|
||||
localServer = window.localServer = window.server = undefined
|
||||
|
||||
postRenderFrameFn = () => { }
|
||||
renderWrapper.postRender = () => { }
|
||||
if (bot) {
|
||||
bot.end()
|
||||
// ensure mineflayer plugins receive this event for cleanup
|
||||
|
|
@ -640,7 +577,7 @@ async function connect (connectOptions: {
|
|||
|
||||
void initVR()
|
||||
|
||||
postRenderFrameFn = () => {
|
||||
renderWrapper.postRender = () => {
|
||||
viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ export const guiOptionsScheme: {
|
|||
dayCycleAndLighting: {
|
||||
text: 'Day Cycle',
|
||||
},
|
||||
antiAliasing: {},
|
||||
},
|
||||
],
|
||||
main: [
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const defaultOptions = {
|
|||
unimplementedContainers: false,
|
||||
dayCycleAndLighting: true,
|
||||
loadPlayerSkins: true,
|
||||
antiAliasing: false,
|
||||
// antiAliasing: false,
|
||||
|
||||
showChunkBorders: false, // todo rename option
|
||||
frameLimit: false as number | false,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
|
||||
import FullScreenWidget from './FullScreenWidget'
|
||||
|
||||
export const name = 'signs'
|
||||
|
||||
export default () => {
|
||||
const signs = [...viewer.world.chunkTextures.values()].flatMap(textures => {
|
||||
const signs = viewer.world instanceof WorldRendererThree ? [...viewer.world.chunkTextures.values()].flatMap(textures => {
|
||||
return Object.entries(textures).map(([signPosKey, texture]) => {
|
||||
const pos = signPosKey.split(',').map(Number)
|
||||
return <div key={signPosKey}>
|
||||
|
|
@ -14,7 +15,7 @@ export default () => {
|
|||
</div>
|
||||
</div>
|
||||
})
|
||||
})
|
||||
}) : []
|
||||
|
||||
return <FullScreenWidget name='signs' title='Loaded Signs'>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -33,15 +33,6 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
viewer.entities.setDebugMode(o.showChunkBorders ? 'basic' : 'none')
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
if (o.antiAliasing) {
|
||||
viewer.enableFxaaScene()
|
||||
} else {
|
||||
viewer.enableFXAA = false
|
||||
viewer.composer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
viewer.entities.setVisible(o.renderEntities)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue