fix(preflat-worlds): improve mesher performance by 2x by syncing the code from webgpu branch
fixes #191
This commit is contained in:
parent
698fb1d388
commit
c2a34ea9f1
6 changed files with 164 additions and 64 deletions
11
prismarine-viewer/examples/shared.ts
Normal file
11
prismarine-viewer/examples/shared.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export type BlockFaceType = {
|
||||
side: number
|
||||
textureIndex: number
|
||||
textureName?: string
|
||||
tint?: [number, number, number]
|
||||
isTransparent?: boolean
|
||||
}
|
||||
|
||||
export type BlockType = {
|
||||
faces: BlockFaceType[]
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ if (module.require) {
|
|||
global.performance = r('perf_hooks').performance
|
||||
}
|
||||
|
||||
let workerIndex = 0
|
||||
let world: World
|
||||
let dirtySections = new Map<string, number>()
|
||||
let allDataReady = false
|
||||
|
|
@ -85,8 +86,9 @@ const handleMessage = data => {
|
|||
|
||||
switch (data.type) {
|
||||
case 'mesherData': {
|
||||
setMesherData(data.blockstatesModels, data.blocksAtlas)
|
||||
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
||||
allDataReady = true
|
||||
workerIndex = data.workerIndex
|
||||
|
||||
break
|
||||
}
|
||||
|
|
@ -148,18 +150,22 @@ setInterval(() => {
|
|||
for (const key of dirtySections.keys()) {
|
||||
const [x, y, z] = key.split(',').map(v => parseInt(v, 10))
|
||||
const chunk = world.getColumn(x, z)
|
||||
let processTime = 0
|
||||
if (chunk?.getSection(new Vec3(x, y, z))) {
|
||||
const start = performance.now()
|
||||
const geometry = getSectionGeometry(x, y, z, world)
|
||||
const transferable = [geometry.positions.buffer, geometry.normals.buffer, geometry.colors.buffer, geometry.uvs.buffer]
|
||||
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
|
||||
//@ts-expect-error
|
||||
postMessage({ type: 'geometry', key, geometry }, transferable)
|
||||
processTime = performance.now() - start
|
||||
} else {
|
||||
// console.info('[mesher] Missing section', x, y, z)
|
||||
}
|
||||
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 })
|
||||
postMessage({ type: 'sectionFinished', key, workerIndex, processTime })
|
||||
processTime = 0
|
||||
}
|
||||
dirtySections.delete(key)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import legacyJson from '../../../../src/preflatMap.json'
|
||||
import { BlockType } from '../../../examples/shared'
|
||||
import { World, BlockModelPartsResolved, WorldBlock as Block } from './world'
|
||||
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
||||
import { MesherGeometryOutput } from './shared'
|
||||
|
||||
let blockProvider: WorldBlockProvider
|
||||
|
||||
|
|
@ -19,6 +21,19 @@ for (const key of Object.keys(tintsData)) {
|
|||
tints[key] = prepareTints(tintsData[key])
|
||||
}
|
||||
|
||||
type TestTileData = {
|
||||
block: string
|
||||
faces: Array<{
|
||||
face: string
|
||||
neighbor: string
|
||||
light?: number
|
||||
}>
|
||||
}
|
||||
|
||||
type Tiles = {
|
||||
[blockPos: string]: BlockType & TestTileData
|
||||
}
|
||||
|
||||
function prepareTints (tints) {
|
||||
const map = new Map()
|
||||
const defaultValue = tintToGl(tints.default)
|
||||
|
|
@ -54,19 +69,25 @@ export function preflatBlockCalculation (block: Block, world: World, position: V
|
|||
]
|
||||
// set needed props to true: east:'false',north:'false',south:'false',west:'false'
|
||||
const props = {}
|
||||
let changed = false
|
||||
for (const [i, neighbor] of neighbors.entries()) {
|
||||
const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false
|
||||
if (isConnectedToSolid || neighbor?.name === block.name) {
|
||||
props[['south', 'north', 'east', 'west'][i]] = 'true'
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return props
|
||||
return changed ? props : undefined
|
||||
}
|
||||
// case 'gate_in_wall': {}
|
||||
case 'block_snowy': {
|
||||
const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow'
|
||||
return {
|
||||
snowy: `${aboveIsSnow}`
|
||||
if (aboveIsSnow) {
|
||||
return {
|
||||
snowy: `${aboveIsSnow}`
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
case 'door': {
|
||||
|
|
@ -139,7 +160,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
if (!neighbor) continue
|
||||
if (neighbor.type === type) continue
|
||||
const isGlass = neighbor.name.includes('glass')
|
||||
if ((isCube(neighbor) && !isUp) || neighbor.getProperties().waterlogged) continue
|
||||
if ((isCube(neighbor) && !isUp) || neighbor.material === 'plant' || neighbor.getProperties().waterlogged) continue
|
||||
|
||||
let tint = [1, 1, 1]
|
||||
if (water) {
|
||||
|
|
@ -151,11 +172,12 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
}
|
||||
|
||||
if (needTiles) {
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
const tiles = attr.tiles as Tiles
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
block: 'water',
|
||||
faces: [],
|
||||
}
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
face,
|
||||
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
||||
// texture: eFace.texture.name,
|
||||
|
|
@ -183,7 +205,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
|
||||
let needRecompute = false
|
||||
|
||||
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: Record<string, any>, globalMatrix: any, globalShift: any, block: Block, biome: string) {
|
||||
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: MesherGeometryOutput, globalMatrix: any, globalShift: any, block: Block, biome: string) {
|
||||
const position = cursor
|
||||
// const key = `${position.x},${position.y},${position.z}`
|
||||
// if (!globalThis.allowedBlocks.includes(key)) return
|
||||
|
|
@ -192,7 +214,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
// eslint-disable-next-line guard-for-in
|
||||
for (const face in element.faces) {
|
||||
const eFace = element.faces[face]
|
||||
const { corners, mask1, mask2 } = elemFaces[face]
|
||||
const { corners, mask1, mask2, side } = elemFaces[face]
|
||||
const dir = matmul3(globalMatrix, elemFaces[face].dir)
|
||||
|
||||
if (eFace.cullface) {
|
||||
|
|
@ -214,7 +236,10 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
const maxz = element.to[2]
|
||||
|
||||
const texture = eFace.texture as any
|
||||
const { u, v, su, sv } = texture
|
||||
const { u } = texture
|
||||
const { v } = texture
|
||||
const { su } = texture
|
||||
const { sv } = texture
|
||||
|
||||
const ndx = Math.floor(attr.positions.length / 3)
|
||||
|
||||
|
|
@ -246,7 +271,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
let localMatrix = null as any
|
||||
let localShift = null as any
|
||||
|
||||
if (element.rotation) {
|
||||
if (element.rotation && !needTiles) {
|
||||
// todo do we support rescale?
|
||||
localMatrix = buildRotationMatrix(
|
||||
element.rotation.axis,
|
||||
|
|
@ -272,21 +297,23 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
(pos[2] ? maxz : minz)
|
||||
]
|
||||
|
||||
vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
|
||||
vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
|
||||
vertex = vertex.map(v => v / 16)
|
||||
if (!needTiles) {
|
||||
vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
|
||||
vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
|
||||
vertex = vertex.map(v => v / 16)
|
||||
|
||||
attr.positions.push(
|
||||
vertex[0] + (cursor.x & 15) - 8,
|
||||
vertex[1] + (cursor.y & 15) - 8,
|
||||
vertex[2] + (cursor.z & 15) - 8
|
||||
)
|
||||
attr.positions.push(
|
||||
vertex[0] + (cursor.x & 15) - 8,
|
||||
vertex[1] + (cursor.y & 15) - 8,
|
||||
vertex[2] + (cursor.z & 15) - 8
|
||||
)
|
||||
|
||||
attr.normals.push(...dir)
|
||||
attr.normals.push(...dir)
|
||||
|
||||
const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
|
||||
const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
|
||||
attr.uvs.push(baseu * su + u, basev * sv + v)
|
||||
const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
|
||||
const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
|
||||
attr.uvs.push(baseu * su + u, basev * sv + v)
|
||||
}
|
||||
|
||||
let light = 1
|
||||
if (doAO) {
|
||||
|
|
@ -322,40 +349,49 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
aos.push(ao)
|
||||
}
|
||||
|
||||
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
|
||||
if (!needTiles) {
|
||||
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
|
||||
}
|
||||
}
|
||||
|
||||
const lightWithColor = [baseLight * tint[0], baseLight * tint[1], baseLight * tint[2]] as [number, number, number]
|
||||
|
||||
if (needTiles) {
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
const tiles = attr.tiles as Tiles
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
block: block.name,
|
||||
faces: [],
|
||||
}
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
face,
|
||||
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
||||
light: baseLight
|
||||
// texture: eFace.texture.name,
|
||||
})
|
||||
const needsOnlyOneFace = false
|
||||
const isTilesEmpty = tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.length < 1
|
||||
if (isTilesEmpty || !needsOnlyOneFace) {
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
face,
|
||||
side,
|
||||
textureIndex: eFace.texture.tileIndex,
|
||||
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
||||
light: baseLight,
|
||||
tint: lightWithColor,
|
||||
//@ts-expect-error debug prop
|
||||
texture: eFace.texture.debugName || block.name,
|
||||
} satisfies BlockType['faces'][number] & TestTileData['faces'][number] as any)
|
||||
}
|
||||
}
|
||||
|
||||
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
|
||||
attr.indices.push(
|
||||
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
||||
ndx, ndx + 3, ndx + 2,
|
||||
ndx, ndx + 1, ndx + 3
|
||||
)
|
||||
} else {
|
||||
attr.indices.push(
|
||||
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
||||
ndx, ndx + 1, ndx + 2,
|
||||
ndx + 2, ndx + 1, ndx + 3
|
||||
)
|
||||
if (!needTiles) {
|
||||
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
|
||||
attr.indices.push(
|
||||
ndx, ndx + 3, ndx + 2, ndx, ndx + 1, ndx + 3
|
||||
)
|
||||
} else {
|
||||
attr.indices.push(
|
||||
ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const makeLooseObj = <T extends string> (obj: Record<T, any>) => obj
|
||||
|
||||
const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier'])
|
||||
|
||||
const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true'
|
||||
|
|
@ -365,7 +401,7 @@ let erroredBlockModel: BlockModelPartsResolved
|
|||
export function getSectionGeometry (sx, sy, sz, world: World) {
|
||||
let delayedRender = [] as Array<() => void>
|
||||
|
||||
const attr = makeLooseObj({
|
||||
const attr: MesherGeometryOutput = {
|
||||
sx: sx + 8,
|
||||
sy: sy + 8,
|
||||
sz: sz + 8,
|
||||
|
|
@ -381,9 +417,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
tiles: {},
|
||||
// todo this can be removed here
|
||||
signs: {},
|
||||
isFull: true,
|
||||
highestBlocks: {},
|
||||
hadErrors: false
|
||||
} as Record<string, any>)
|
||||
}
|
||||
|
||||
const cursor = new Vec3(0, 0, 0)
|
||||
for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) {
|
||||
|
|
@ -419,19 +456,17 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
}
|
||||
const biome = block.biome.name
|
||||
|
||||
let preflatRecomputeVariant = !!(block as any)._originalProperties
|
||||
if (world.preflat) {
|
||||
const patchProperties = preflatBlockCalculation(block, world, cursor)
|
||||
if (patchProperties) {
|
||||
//@ts-expect-error
|
||||
block._originalProperties ??= block._properties
|
||||
//@ts-expect-error
|
||||
block._properties = { ...block._originalProperties, ...patchProperties }
|
||||
preflatRecomputeVariant = true
|
||||
if (block.models && JSON.stringify(block._originalProperties) !== JSON.stringify(block._properties)) {
|
||||
// recompute models
|
||||
block.models = undefined
|
||||
}
|
||||
} else {
|
||||
//@ts-expect-error
|
||||
block._properties = block._originalProperties ?? block._properties
|
||||
//@ts-expect-error
|
||||
block._originalProperties = undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -449,7 +484,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
if (block.name !== 'water' && block.name !== 'lava' && !invisibleBlocks.has(block.name)) {
|
||||
// cache
|
||||
let { models } = block
|
||||
if (block.models === undefined || preflatRecomputeVariant) {
|
||||
if (block.models === undefined) {
|
||||
try {
|
||||
models = blockProvider.getAllResolvedModels0_1({
|
||||
name: block.name,
|
||||
|
|
@ -515,7 +550,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
delayedRender = []
|
||||
|
||||
let ndx = attr.positions.length / 3
|
||||
for (let i = 0; i < attr.t_positions.length / 12; i++) {
|
||||
for (let i = 0; i < attr.t_positions!.length / 12; i++) {
|
||||
attr.indices.push(
|
||||
ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3,
|
||||
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
||||
|
|
@ -525,10 +560,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
ndx += 4
|
||||
}
|
||||
|
||||
attr.positions.push(...attr.t_positions)
|
||||
attr.normals.push(...attr.t_normals)
|
||||
attr.colors.push(...attr.t_colors)
|
||||
attr.uvs.push(...attr.t_uvs)
|
||||
attr.positions.push(...attr.t_positions!)
|
||||
attr.normals.push(...attr.t_normals!)
|
||||
attr.colors.push(...attr.t_colors!)
|
||||
attr.uvs.push(...attr.t_uvs!)
|
||||
|
||||
delete attr.t_positions
|
||||
delete attr.t_normals
|
||||
|
|
@ -540,6 +575,13 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
attr.colors = new Float32Array(attr.colors) as any
|
||||
attr.uvs = new Float32Array(attr.uvs) as any
|
||||
|
||||
if (needTiles) {
|
||||
delete attr.positions
|
||||
delete attr.normals
|
||||
delete attr.colors
|
||||
delete attr.uvs
|
||||
}
|
||||
|
||||
return attr
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
import { BlockType } from '../../../examples/shared'
|
||||
|
||||
export const defaultMesherConfig = {
|
||||
version: '',
|
||||
enableLighting: true,
|
||||
skyLight: 15,
|
||||
smoothLighting: true,
|
||||
outputFormat: 'threeJs' as 'threeJs' | 'webgl',
|
||||
outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
|
||||
textureSize: 1024, // for testing
|
||||
debugModelVariant: undefined as undefined | number[]
|
||||
}
|
||||
|
||||
export type MesherConfig = typeof defaultMesherConfig
|
||||
|
||||
export type MesherGeometryOutput = {
|
||||
sx: number,
|
||||
sy: number,
|
||||
sz: number,
|
||||
// resulting: float32array
|
||||
positions: any,
|
||||
normals: any,
|
||||
colors: any,
|
||||
uvs: any,
|
||||
t_positions?: number[],
|
||||
t_normals?: number[],
|
||||
t_colors?: number[],
|
||||
t_uvs?: number[],
|
||||
|
||||
indices: number[],
|
||||
tiles: Record<string, BlockType>,
|
||||
signs: Record<string, any>,
|
||||
isFull: boolean
|
||||
highestBlocks: Record<string, { y: number, name: string }>
|
||||
hadErrors: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export type WorldBlock = Omit<Block, 'position'> & {
|
|||
isCube: boolean
|
||||
/** cache */
|
||||
models?: BlockModelPartsResolved | null
|
||||
_originalProperties?: Record<string, any>
|
||||
_properties?: Record<string, any>
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -91,8 +91,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
items?: CustomTexturesData
|
||||
blocks?: CustomTexturesData
|
||||
} = {}
|
||||
workersProcessAverageTime = 0
|
||||
workersProcessAverageTimeCount = 0
|
||||
maxWorkersProcessTime = 0
|
||||
edgeChunks = {} as Record<string, boolean>
|
||||
lastAddChunk = null as null | {
|
||||
timeout: any
|
||||
x: number
|
||||
z: number
|
||||
}
|
||||
|
||||
abstract outputFormat: 'threeJs' | 'webgl'
|
||||
abstract outputFormat: 'threeJs' | 'webgpu'
|
||||
|
||||
constructor (public config: WorldRendererConfig) {
|
||||
// this.initWorkers(1) // preload script on page load
|
||||
|
|
@ -145,6 +154,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
|
||||
this.renderUpdateEmitter.emit('update')
|
||||
if (data.processTime) {
|
||||
this.workersProcessAverageTimeCount++
|
||||
this.workersProcessAverageTime = ((this.workersProcessAverageTime * (this.workersProcessAverageTimeCount - 1)) + data.processTime) / this.workersProcessAverageTimeCount
|
||||
this.maxWorkersProcessTime = Math.max(this.maxWorkersProcessTime, data.processTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
worker.onmessage = ({ data }) => {
|
||||
|
|
@ -265,7 +279,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.currentTextureImage = this.material.map.image
|
||||
this.mesherConfig.textureSize = this.material.map.image.width
|
||||
|
||||
for (const worker of this.workers) {
|
||||
for (const [i, worker] of this.workers.entries()) {
|
||||
const { blockstatesModels } = this
|
||||
if (this.customBlockStates) {
|
||||
// TODO! remove from other versions as well
|
||||
|
|
@ -282,6 +296,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
worker.postMessage({
|
||||
type: 'mesherData',
|
||||
workerIndex: i,
|
||||
blocksAtlas: {
|
||||
latest: blocksAtlas
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue