Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Put three.js renderer into worker for stable fps #151

Open
wants to merge 5 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions buildWorkers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// main worker file intended for computing world geometry is built using prismarine-viewer/buildWorker.mjs
import { build, context } from 'esbuild'
import fs from 'fs'
import path from 'path'

const watch = process.argv.includes('-w')

const workers = ['./prismarine-viewer/viewer/lib/threeJsWorker.ts']

const result = await (watch ? context : build)({
bundle: true,
platform: 'browser',
entryPoints: workers,
outdir: 'prismarine-viewer/public/',
write: false,
sourcemap: watch ? 'inline' : 'external',
minify: !watch,
treeShaking: true,
logLevel: 'info',
alias: {
'three': './node_modules/three/src/Three.js',
events: 'events', // make explicit
buffer: 'buffer',
'fs': 'browserfs/dist/shims/fs.js',
http: 'http-browserify',
perf_hooks: './src/perf_hooks_replacement.js',
crypto: './src/crypto.js',
stream: 'stream-browserify',
net: 'net-browserify',
assert: 'assert',
dns: './src/dns.js'
},
inject: [
'./src/shims.js'
],
plugins: [
{
name: 'writeOutput',
setup(build) {
build.onEnd(({ outputFiles }) => {
for (const file of outputFiles) {
for (const dir of ['prismarine-viewer/public', 'dist']) {
const baseName = path.basename(file.path)
fs.writeFileSync(path.join(dir, baseName), file.contents)
}
}
})
}
}
],
loader: {
'.vert': 'text',
'.frag': 'text',
'.wgsl': 'text',
},
mainFields: [
'browser', 'module', 'main'
],
keepNames: true,
})

if (watch) {
await result.watch()
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"start": "node scripts/build.js copyFilesDev && node scripts/prepareData.mjs && node esbuild.mjs --watch",
"start-watch-script": "nodemon -w esbuild.mjs --watch",
"build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod",
"build": "node scripts/build.js copyFiles && node buildWorkers.mjs && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod",
"check-build": "tsc && pnpm build",
"test:cypress": "cypress run",
"test-unit": "vitest",
Expand All @@ -17,7 +17,7 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
"start-experiments": "vite --config experiments/vite.config.ts --host",
"watch-other-workers": "echo NOT IMPLEMENTED",
"watch-other-workers": "node buildWorkers.mjs -w",
"watch-mesher": "node prismarine-viewer/buildMesherWorker.mjs -w",
"run-playground": "run-p watch-mesher watch-other-workers playground-server watch-playground",
"run-all": "run-p start run-playground",
Expand Down Expand Up @@ -117,6 +117,7 @@
"@types/wait-on": "^5.3.4",
"@xmcl/installer": "^5.1.0",
"assert": "^2.0.0",
"three-latest": "npm:three@latest",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"constants-browserify": "^1.0.0",
Expand Down
14 changes: 11 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 17 additions & 7 deletions prismarine-viewer/examples/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,21 @@ async function main () {
const chunk1 = new Chunk()
//@ts-ignore
const chunk2 = new Chunk()
chunk1.setBlockStateId(targetPos, 34)
chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 34)
const addNeighbor = (x, z, light = 15) => {
x += 2
chunk1.setBlockStateId(targetPos.offset(x, 0, z), 1)
chunk1.setBlockLight(targetPos.offset(x, 1, z), light)
chunk1.setSkyLight(targetPos.offset(x, 1, z), light)
}
addNeighbor(0, 1)
addNeighbor(0, -1)
addNeighbor(1, 0)
addNeighbor(-1, 0)
chunk1.setBlockStateId(targetPos.offset(1, 1, 0), mcData.blocksByName['grass'].minStateId)
addNeighbor(0, 0, 0)
const world = new World((chunkX, chunkZ) => {
// if (chunkX === 0 && chunkZ === 0) return chunk1
// if (chunkX === 1 && chunkZ === 0) return chunk2
if (chunkX === 0 && chunkZ === 0) return chunk1
if (chunkX === 1 && chunkZ === 0) return chunk2
//@ts-ignore
const chunk = new Chunk()
return chunk
Expand All @@ -138,7 +148,7 @@ async function main () {
viewer.entities.onSkinUpdate = () => {
viewer.render()
}
viewer.world.mesherConfig.enableLighting = false
// viewer.world.mesherConfig.enableLighting = false

viewer.listen(worldView)
// Load chunks
Expand Down Expand Up @@ -260,7 +270,7 @@ async function main () {
const controls = new OrbitControls(viewer.camera, renderer.domElement)
controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)

const cameraPos = targetPos.offset(2, 2, 2)
const cameraPos = targetPos.offset(3, 3, 3)
const pitch = THREE.MathUtils.degToRad(-45)
const yaw = THREE.MathUtils.degToRad(45)
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
Expand Down Expand Up @@ -409,7 +419,7 @@ async function main () {
for (const update of Object.values(onUpdate)) {
update()
}
applyChanges(true)
// applyChanges(true)
gui.openAnimated()
})

Expand Down
104 changes: 104 additions & 0 deletions prismarine-viewer/viewer/lib/threeJsWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as THREE from 'three'
import { createWorkerProxy } from './workerProxy'
import * as tweenJs from '@tweenjs/tween.js'
import testGeometryJson from '../../examples/test-geometry.json'

let material: THREE.Material
let scene = new THREE.Scene()
let camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
let renderer: THREE.WebGLRenderer
// scene.add(new THREE.AmbientLight(0xcc_cc_cc))
// scene.add(new THREE.DirectionalLight(0xff_ff_ff, 0.5))
scene.add(camera)
scene.background = new THREE.Color('lightblue')
scene.matrixAutoUpdate = false
scene.add(new THREE.AmbientLight(0xcc_cc_cc))
scene.add(new THREE.DirectionalLight(0xff_ff_ff, 0.5))

THREE.ColorManagement.enabled = false

let sections = new Map<string, THREE.Mesh>()
globalThis.sections = sections
globalThis.camera = camera
globalThis.scene = scene
globalThis.marks = {}

let fps = 0
let processedSinceLastRender = 0
setInterval(() => {
// console.log('FPS', fps)
globalThis.fps = fps
globalThis.worstFps = Math.min(globalThis.worstFps ?? Infinity, fps)
fps = 0
}, 1000)
setInterval(() => {
globalThis.worstFps = Infinity
}, 10000)
const meshesQueue = [] as any[]
const render = () => {
tweenJs.update()
const max = 5
for (let i = 0; i < max; i++) {
const add = meshesQueue.pop()
if (add) {
scene.add(add)
}
}
renderer.render(scene, camera)
globalThis.maxProcessed = Math.max(globalThis.maxProcessed ?? 0, processedSinceLastRender)
processedSinceLastRender = 0
fps++
}

export const threeJsWorkerProxyType = createWorkerProxy({
async canvas (canvas: OffscreenCanvas, textureBlob: Blob) {
const textureBitmap = await createImageBitmap(textureBlob)
const texture = new THREE.CanvasTexture(textureBitmap)
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.flipY = false
texture.needsUpdate = true
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1, map: texture })

renderer = new THREE.WebGLRenderer({ canvas })
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
camera.aspect = canvas.width / canvas.height
camera.updateProjectionMatrix()

renderer.setAnimationLoop(render)
},
addGeometry (position: { x, y, z }, geometry?: { positions, normals, uvs, colors, indices }) {
const key = `${position.x},${position.y},${position.z}`
if (sections.has(key)) {
const section = sections.get(key)!
section.geometry.dispose()
scene.remove(section)
sections.delete(key)
}
if (!geometry) return
const bufferGeometry = new THREE.BufferGeometry()
bufferGeometry.setAttribute('position', new THREE.BufferAttribute(geometry.positions, 3))
bufferGeometry.setAttribute('normal', new THREE.BufferAttribute(geometry.normals, 3))
bufferGeometry.setAttribute('uv', new THREE.BufferAttribute(geometry.uvs, 2))
bufferGeometry.setAttribute('color', new THREE.BufferAttribute(geometry.colors, 3))
bufferGeometry.setIndex(geometry.indices)
const mesh = new THREE.Mesh(bufferGeometry, material)
// mesh.frustumCulled = false
const old = mesh.geometry.computeBoundingSphere
mesh.geometry.computeBoundingSphere = function () {
let start = performance.now()
// old.call(mesh.geometry)
globalThis.marks.computeBoundingSphere ??= 0
globalThis.marks.computeBoundingSphere += performance.now() - start
mesh.geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 16)
}
mesh.position.set(position.x, position.y, position.z)
meshesQueue.push(mesh)
processedSinceLastRender++
},
updateCamera (position: { x, y, z }, rotation: { x, y, z }) {
// camera.position.set(position.x, position.y, position.z)
new tweenJs.Tween(camera.position).to({ x: position.x, y: position.y, z: position.z }, 50).start()
camera.rotation.set(rotation.x, rotation.y, rotation.z, 'ZYX')
}
})
58 changes: 58 additions & 0 deletions prismarine-viewer/viewer/lib/workerProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T): { __workerProxy: T } {
addEventListener('message', (event) => {
const { type, args } = event.data
if (handlers[type]) {
handlers[type](...args)
}
})
return null as any
}

/**
* in main thread
* ```ts
* // either:
* import type { importedTypeWorkerProxy } from './worker'
* // or:
* type importedTypeWorkerProxy = import('./worker').importedTypeWorkerProxy
*
* const workerChannel = useWorkerProxy<typeof importedTypeWorkerProxy>(worker)
* ```
*/
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker, autoTransfer = true): T['__workerProxy'] & {
transfer: (...args: Transferable[]) => T['__workerProxy']
} => {
// in main thread
return new Proxy({} as any, {
get: (target, prop) => {
if (prop === 'transfer') return (...transferable: Transferable[]) => {
return new Proxy({}, {
get: (target, prop) => {
return (...args: any[]) => {
worker.postMessage({
type: prop,
args,
}, transferable)
}
}
})
}
return (...args: any[]) => {
const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : []
worker.postMessage({
type: prop,
args,
}, transfer)
}
}
})
}

// const workerProxy = createWorkerProxy({
// startRender (canvas: HTMLCanvasElement) {
// },
// })

// const worker = useWorkerProxy(null, workerProxy)

// worker.
Loading
Loading