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
Changes from 1 commit
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
Prev Previous commit
Next Next commit
put three.js renderer into worker for stable fps
zardoy committed Jun 24, 2024
commit 3b3efdcbfbcdfb57d28d5a849f98a71cf799c09d
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()
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
@@ -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",
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
@@ -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
@@ -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
@@ -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')
@@ -409,7 +419,7 @@ async function main () {
for (const update of Object.values(onUpdate)) {
update()
}
applyChanges(true)
// applyChanges(true)
gui.openAnimated()
})

99 changes: 99 additions & 0 deletions prismarine-viewer/viewer/lib/threeJsWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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 render = () => {
tweenJs.update()
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)
scene.add(mesh)
processedSinceLastRender++
if (processedSinceLastRender > 5) {
render()
}
},
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')
}
})
49 changes: 48 additions & 1 deletion prismarine-viewer/viewer/lib/worldrendererThree.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,17 @@ import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
import * as tweenJs from '@tweenjs/tween.js'
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib'
import { disposeObject } from './threeJsUtils'
import { type threeJsWorkerProxyType } from 'prismarine-viewer/viewer/lib/threeJsWorker'
import { useWorkerProxy } from 'prismarine-viewer/viewer/lib/workerProxy'

async function imageToBlob (url: string): Promise<Blob> {
const response = await fetch(url)
const blob = await response.blob()
return blob
}

const testSharedArrayBuffer = new SharedArrayBuffer(1024)
testSharedArrayBuffer[0] = 1

export class WorldRendererThree extends WorldRendererCommon {
outputFormat = 'threeJs' as const
@@ -17,6 +28,7 @@ export class WorldRendererThree extends WorldRendererCommon {
signsCache = new Map<string, any>()
starField: StarField
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
workerProxy?: ReturnType<typeof useWorkerProxy<typeof threeJsWorkerProxyType>>

get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
@@ -25,6 +37,19 @@ export class WorldRendererThree extends WorldRendererCommon {
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
super(config)
this.starField = new StarField(scene)

this.renderUpdateEmitter.addListener('textureDownloaded', async () => {
const rendererWorker = new Worker('./threeJsWorker.js')
this.workerProxy = useWorkerProxy<typeof threeJsWorkerProxyType>(rendererWorker)
const img: HTMLImageElement = this.downloadedTextureImage
const blob = await imageToBlob(img.src)
const newCanvas = document.createElement('canvas')
newCanvas.width = outerWidth * window.devicePixelRatio
newCanvas.height = outerHeight * window.devicePixelRatio
newCanvas.id = 'viewer-canvas'
document.body.appendChild(newCanvas)
this.workerProxy.canvas(newCanvas.transferControlToOffscreen(), blob)
})
}

timeUpdated (newTime: number): void {
@@ -60,6 +85,9 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}

lastUpdate = 0
updates = [] as number[]

handleWorkerMessage (data: any): void {
if (data.type !== 'geometry') return
let object: THREE.Object3D = this.sectionObjects[data.key]
@@ -72,6 +100,9 @@ export class WorldRendererThree extends WorldRendererCommon {
const chunkCoords = data.key.split(',')
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return

this.updates.push(Math.floor(performance.now() - this.lastUpdate))
this.lastUpdate = performance.now()

// if (!this.initialChunksLoad && this.enableChunksLoadDelay) {
// const newPromise = new Promise(resolve => {
// if (this.droppedFpsPercentage > 0.5) {
@@ -86,6 +117,19 @@ export class WorldRendererThree extends WorldRendererCommon {
// }
// }

this.workerProxy?.transfer(data.geometry.positions.buffer, data.geometry.normals.buffer, data.geometry.uvs.buffer, data.geometry.colors.buffer).addGeometry({
x: data.geometry.sx,
y: data.geometry.sy,
z: data.geometry.sz
}, {
positions: data.geometry.positions,
normals: data.geometry.normals,
uvs: data.geometry.uvs,
colors: data.geometry.colors,
indices: data.geometry.indices,
})
if (!false) return

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))
@@ -158,12 +202,14 @@ export class WorldRendererThree extends WorldRendererCommon {
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')
const posToObj = p => ({ x: p.x, y: p.y, z: p.z })
this.workerProxy?.updateCamera(pos ?? posToObj(this.camera.position), posToObj(this.camera.rotation))
}

render () {
tweenJs.update()
const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
this.renderer.render(this.scene, cam)
// this.renderer.render(this.scene, cam)
}

renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
@@ -292,6 +338,7 @@ export class WorldRendererThree extends WorldRendererCommon {

this.cleanChunkTextures(x, z)
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
this.workerProxy?.addGeometry({ x, y, z })
this.setSectionDirty(new Vec3(x, y, z), false)
const key = `${x},${y},${z}`
const mesh = this.sectionObjects[key]
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -120,7 +120,7 @@ try {
// renderer.localClippingEnabled = true
initWithRenderer(renderer.domElement)
const renderWrapper = new ViewerWrapper(renderer.domElement, renderer)
renderWrapper.addToPage()
// renderWrapper.addToPage()
watchValue(options, (o) => {
renderWrapper.renderInterval = o.frameLimit ? 1000 / o.frameLimit : 0
renderWrapper.renderIntervalUnfocused = o.backgroundRendering === '5fps' ? 1000 / 5 : o.backgroundRendering === '20fps' ? 1000 / 20 : undefined

Unchanged files with check annotations Beta

worker.postMessage({
type: prop,
args,
}, transfer)

Check failure on line 45 in prismarine-viewer/viewer/lib/workerProxy.ts

GitHub Actions / build-and-deploy

No overload matches this call.
}
}
})