minecraft-renderer 0.1.38 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mesher.js +20 -20
- package/dist/mesher.js.map +4 -4
- package/dist/mesherWasm.js +72 -75
- package/dist/minecraft-renderer.js +54 -54
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +336 -336
- package/package.json +3 -1
- package/src/graphicsBackend/config.ts +5 -0
- package/src/lib/worldrendererCommon.ts +119 -53
- package/src/mesher/blockEntityMetadata.ts +70 -0
- package/src/mesher/computeHeightmap.ts +66 -0
- package/src/mesher/mesher.ts +13 -20
- package/src/mesher/mesherWasm.ts +432 -140
- package/src/mesher/mesherWasmConversionCache.ts +155 -0
- package/src/mesher/mesherWasmRequestTracker.ts +56 -0
- package/src/mesher/models.ts +2 -46
- package/src/mesher/shared.ts +22 -3
- package/src/mesher/test/heightmapParity.test.ts +231 -0
- package/src/mesher/test/mesherWasmConversionCache.test.ts +128 -0
- package/src/mesher/test/run/chunk.ts +2 -2
- package/src/mesher/test/splitColumnWasmOutput.test.ts +163 -0
- package/src/three/chunkMeshManager.ts +177 -1
- package/src/three/modules/cameraBobbing.ts +8 -1
- package/src/three/worldRendererThree.ts +7 -0
- package/src/wasm-lib/render-from-wasm.ts +158 -13
- package/wasm/wasm_mesher.d.ts +4 -4
- package/wasm/wasm_mesher.js +23 -66
- package/wasm/wasm_mesher_bg.wasm +0 -0
- package/wasm/wasm_mesher_bg.wasm.d.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecraft-renderer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -121,6 +121,8 @@
|
|
|
121
121
|
"watch:lib": "node scripts/buildLib.mjs -w",
|
|
122
122
|
"build:mesher": "node scripts/buildMesherWorker.mjs",
|
|
123
123
|
"build:wasm": "cd wasm-mesher && ./build.sh web",
|
|
124
|
+
"test:wasm": "cd wasm-mesher && wasm-pack build --target nodejs --out-dir pkg --dev && node build.mjs && node test-chunk.cjs && node test-section-boundary.cjs",
|
|
125
|
+
"test:wasm:boundary": "cd wasm-mesher && wasm-pack build --target nodejs --out-dir pkg --dev && node build.mjs && node test-section-boundary.cjs",
|
|
124
126
|
"watch:mesher": "pnpm build:mesher -w",
|
|
125
127
|
"build:threeworker": "node scripts/buildThreeWorker.mjs",
|
|
126
128
|
"watch:threeworker": "pnpm build:threeworker -w",
|
|
@@ -20,6 +20,7 @@ export const defaultWorldRendererConfig = {
|
|
|
20
20
|
// Debug settings
|
|
21
21
|
showChunkBorders: false,
|
|
22
22
|
enableDebugOverlay: false,
|
|
23
|
+
debugWasmPerf: false,
|
|
23
24
|
debugModelVariant: undefined as undefined | number[],
|
|
24
25
|
futuristicReveal: false,
|
|
25
26
|
|
|
@@ -30,6 +31,10 @@ export const defaultWorldRendererConfig = {
|
|
|
30
31
|
_experimentalSmoothChunkLoading: true,
|
|
31
32
|
_renderByChunks: false,
|
|
32
33
|
autoLowerRenderDistance: false,
|
|
34
|
+
/** Disable WASM mesher worker-side conversion cache (memory hotfix for
|
|
35
|
+
* iOS Safari and other low-RAM environments). Trades performance for
|
|
36
|
+
* lower per-worker RAM. */
|
|
37
|
+
disableMesherConversionCache: false,
|
|
33
38
|
|
|
34
39
|
// Rendering engine settings
|
|
35
40
|
/** Face shading: vanilla Minecraft vs higher-contrast client look */
|
|
@@ -9,7 +9,7 @@ import { proxy, subscribe } from 'valtio'
|
|
|
9
9
|
import type { ResourcesManagerTransferred } from '../resourcesManager/resourcesManager'
|
|
10
10
|
import { dynamicMcDataFiles } from './buildSharedConfig.mjs'
|
|
11
11
|
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState, SoundSystem } from '../graphicsBackend/types'
|
|
12
|
-
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent,
|
|
12
|
+
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent, SECTION_HEIGHT } from '../mesher/shared'
|
|
13
13
|
import { chunkPos } from './simpleUtils'
|
|
14
14
|
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
|
|
15
15
|
import { getPlayerStateUtils } from '../graphicsBackend/playerState'
|
|
@@ -70,6 +70,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
70
70
|
customTexturesDataUrl = undefined as string | undefined
|
|
71
71
|
workers: any[] = []
|
|
72
72
|
viewerChunkPosition?: Vec3
|
|
73
|
+
// Last viewer chunk-grid coords for which `onViewerChunkPositionChanged`
|
|
74
|
+
// fired — throttles the hook to chunk-grid changes.
|
|
75
|
+
private lastViewerChunkGridX?: number
|
|
76
|
+
private lastViewerChunkGridZ?: number
|
|
73
77
|
lastCamUpdate = 0
|
|
74
78
|
droppedFpsPercentage = 0
|
|
75
79
|
initialChunkLoadWasStartedIn: number | undefined
|
|
@@ -91,6 +95,20 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
91
95
|
workersProcessAverageTime = 0
|
|
92
96
|
workersProcessAverageTimeCount = 0
|
|
93
97
|
maxWorkersProcessTime = 0
|
|
98
|
+
workersPreAverageTime = 0
|
|
99
|
+
workersWasmAverageTime = 0
|
|
100
|
+
workersPostAverageTime = 0
|
|
101
|
+
workersPhaseSampleCount = 0
|
|
102
|
+
// Pre-stage substage averages (column-mode perf instrumentation).
|
|
103
|
+
workersPreTargetConvertAverageTime = 0
|
|
104
|
+
workersPreNeighborConvertAverageTime = 0
|
|
105
|
+
workersPreNeighborCountAverage = 0
|
|
106
|
+
workersPreTypedArrayBuildAverageTime = 0
|
|
107
|
+
workersPreOtherAverageTime = 0
|
|
108
|
+
// Cumulative cache hit/miss counters (column-mode conversion cache).
|
|
109
|
+
workersPreCacheHitsTotal = 0
|
|
110
|
+
workersPreCacheMissesTotal = 0
|
|
111
|
+
private static readonly PHASE_PERF_LOG_INTERVAL = 64
|
|
94
112
|
geometryReceiveCount = {} as Record<number, number>
|
|
95
113
|
allLoadedIn: undefined | number
|
|
96
114
|
onWorldSwitched = [] as Array<() => void>
|
|
@@ -392,18 +410,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
392
410
|
if (this.loadedChunks[chunkKey]) { // ensure chunk data was added, not a neighbor chunk update
|
|
393
411
|
let loaded = true
|
|
394
412
|
const sectionHeight = this.getSectionHeight()
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const sectionY = this.worldMinYRender
|
|
398
|
-
if (!this.finishedSections[`${chunkCoords[0]},${sectionY},${chunkCoords[2]}`]) {
|
|
413
|
+
for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
414
|
+
if (!this.finishedSections[`${chunkCoords[0]},${y},${chunkCoords[2]}`]) {
|
|
399
415
|
loaded = false
|
|
400
|
-
|
|
401
|
-
} else {
|
|
402
|
-
for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
403
|
-
if (!this.finishedSections[`${chunkCoords[0]},${y},${chunkCoords[2]}`]) {
|
|
404
|
-
loaded = false
|
|
405
|
-
break
|
|
406
|
-
}
|
|
416
|
+
break
|
|
407
417
|
}
|
|
408
418
|
}
|
|
409
419
|
if (loaded) {
|
|
@@ -442,6 +452,46 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
442
452
|
this.workersProcessAverageTime = ((this.workersProcessAverageTime * (this.workersProcessAverageTimeCount - 1)) + data.processTime) / this.workersProcessAverageTimeCount
|
|
443
453
|
this.maxWorkersProcessTime = Math.max(this.maxWorkersProcessTime, data.processTime)
|
|
444
454
|
}
|
|
455
|
+
if (typeof data.pre === 'number' && typeof data.wasm === 'number' && typeof data.post === 'number'
|
|
456
|
+
&& (data.pre > 0 || data.wasm > 0 || data.post > 0)) {
|
|
457
|
+
const n = ++this.workersPhaseSampleCount
|
|
458
|
+
this.workersPreAverageTime = ((this.workersPreAverageTime * (n - 1)) + data.pre) / n
|
|
459
|
+
this.workersWasmAverageTime = ((this.workersWasmAverageTime * (n - 1)) + data.wasm) / n
|
|
460
|
+
this.workersPostAverageTime = ((this.workersPostAverageTime * (n - 1)) + data.post) / n
|
|
461
|
+
// Pre-stage substages — additive schema; treat undefined as 0 so
|
|
462
|
+
// events from older mesher builds don't poison the running mean.
|
|
463
|
+
const ptc = typeof data.preTargetConvert === 'number' ? data.preTargetConvert : 0
|
|
464
|
+
const pnc = typeof data.preNeighborConvert === 'number' ? data.preNeighborConvert : 0
|
|
465
|
+
const pncn = typeof data.preNeighborCount === 'number' ? data.preNeighborCount : 0
|
|
466
|
+
const ptab = typeof data.preTypedArrayBuild === 'number' ? data.preTypedArrayBuild : 0
|
|
467
|
+
const po = typeof data.preOther === 'number' ? data.preOther : 0
|
|
468
|
+
const pch = typeof data.preCacheHits === 'number' ? data.preCacheHits : 0
|
|
469
|
+
const pcm = typeof data.preCacheMisses === 'number' ? data.preCacheMisses : 0
|
|
470
|
+
this.workersPreTargetConvertAverageTime = ((this.workersPreTargetConvertAverageTime * (n - 1)) + ptc) / n
|
|
471
|
+
this.workersPreNeighborConvertAverageTime = ((this.workersPreNeighborConvertAverageTime * (n - 1)) + pnc) / n
|
|
472
|
+
this.workersPreNeighborCountAverage = ((this.workersPreNeighborCountAverage * (n - 1)) + pncn) / n
|
|
473
|
+
this.workersPreTypedArrayBuildAverageTime = ((this.workersPreTypedArrayBuildAverageTime * (n - 1)) + ptab) / n
|
|
474
|
+
this.workersPreOtherAverageTime = ((this.workersPreOtherAverageTime * (n - 1)) + po) / n
|
|
475
|
+
this.workersPreCacheHitsTotal += pch
|
|
476
|
+
this.workersPreCacheMissesTotal += pcm
|
|
477
|
+
if (this.worldRendererConfig.debugWasmPerf && n % WorldRendererCommon.PHASE_PERF_LOG_INTERVAL === 0) {
|
|
478
|
+
const total = this.workersPreAverageTime + this.workersWasmAverageTime + this.workersPostAverageTime
|
|
479
|
+
const prePct = total > 0 ? (this.workersPreAverageTime / total) * 100 : 0
|
|
480
|
+
const wasmPct = total > 0 ? (this.workersWasmAverageTime / total) * 100 : 0
|
|
481
|
+
const postPct = total > 0 ? (this.workersPostAverageTime / total) * 100 : 0
|
|
482
|
+
const preAvg = this.workersPreAverageTime
|
|
483
|
+
const tgtPct = preAvg > 0 ? (this.workersPreTargetConvertAverageTime / preAvg) * 100 : 0
|
|
484
|
+
const nbrPct = preAvg > 0 ? (this.workersPreNeighborConvertAverageTime / preAvg) * 100 : 0
|
|
485
|
+
const tabPct = preAvg > 0 ? (this.workersPreTypedArrayBuildAverageTime / preAvg) * 100 : 0
|
|
486
|
+
const othPct = preAvg > 0 ? (this.workersPreOtherAverageTime / preAvg) * 100 : 0
|
|
487
|
+
const nbrCnt = this.workersPreNeighborCountAverage
|
|
488
|
+
const nbrPerAvg = nbrCnt > 0 ? this.workersPreNeighborConvertAverageTime / nbrCnt : 0
|
|
489
|
+
const cacheTotal = this.workersPreCacheHitsTotal + this.workersPreCacheMissesTotal
|
|
490
|
+
const cacheHitPct = cacheTotal > 0 ? (this.workersPreCacheHitsTotal / cacheTotal) * 100 : 0
|
|
491
|
+
// eslint-disable-next-line no-console
|
|
492
|
+
console.log(`[wasm-mesher perf] n=${n} pre=${this.workersPreAverageTime.toFixed(2)}ms (${prePct.toFixed(1)}%) wasm=${this.workersWasmAverageTime.toFixed(2)}ms (${wasmPct.toFixed(1)}%) post=${this.workersPostAverageTime.toFixed(2)}ms (${postPct.toFixed(1)}%) | pre.targetConvert=${this.workersPreTargetConvertAverageTime.toFixed(2)}ms (${tgtPct.toFixed(1)}%) pre.neighborConvert=${this.workersPreNeighborConvertAverageTime.toFixed(2)}ms (${nbrPct.toFixed(1)}%) [n̄=${nbrCnt.toFixed(2)}, per-nbr=${nbrPerAvg.toFixed(2)}ms] pre.typedArrayBuild=${this.workersPreTypedArrayBuildAverageTime.toFixed(2)}ms (${tabPct.toFixed(1)}%) pre.other=${this.workersPreOtherAverageTime.toFixed(2)}ms (${othPct.toFixed(1)}%) | pre.cache hits=${this.workersPreCacheHitsTotal} misses=${this.workersPreCacheMissesTotal} (${cacheHitPct.toFixed(1)}% hit)`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
445
495
|
}
|
|
446
496
|
|
|
447
497
|
if (data.type === 'blockStateModelInfo') {
|
|
@@ -503,6 +553,21 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
503
553
|
if (!value) continue
|
|
504
554
|
this.updatePosDataChunk?.(key)
|
|
505
555
|
}
|
|
556
|
+
const gridX = Math.floor(pos.x / 16)
|
|
557
|
+
const gridZ = Math.floor(pos.z / 16)
|
|
558
|
+
if (gridX !== this.lastViewerChunkGridX || gridZ !== this.lastViewerChunkGridZ) {
|
|
559
|
+
this.lastViewerChunkGridX = gridX
|
|
560
|
+
this.lastViewerChunkGridZ = gridZ
|
|
561
|
+
this.onViewerChunkPositionChanged()
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Fired only when the viewer crosses a chunk-grid boundary.
|
|
567
|
+
* Three subclass overrides this to refresh the near-first reveal gate.
|
|
568
|
+
*/
|
|
569
|
+
protected onViewerChunkPositionChanged(): void {
|
|
570
|
+
// overridden by WorldRendererThree
|
|
506
571
|
}
|
|
507
572
|
|
|
508
573
|
sendWorkers(message: WorkerSend) {
|
|
@@ -562,6 +627,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
562
627
|
disableBlockEntityTextures: !this.worldRendererConfig.extraBlockRenderers,
|
|
563
628
|
worldMinY: this.worldMinYRender,
|
|
564
629
|
worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight,
|
|
630
|
+
disableConversionCache: this.worldRendererConfig.disableMesherConversionCache,
|
|
565
631
|
}
|
|
566
632
|
}
|
|
567
633
|
|
|
@@ -594,9 +660,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
594
660
|
}
|
|
595
661
|
|
|
596
662
|
getSectionHeight() {
|
|
597
|
-
if (IS_FULL_WORLD_SECTION) {
|
|
598
|
-
return this.worldSizeParams.worldHeight
|
|
599
|
-
}
|
|
600
663
|
return SECTION_HEIGHT
|
|
601
664
|
}
|
|
602
665
|
|
|
@@ -636,19 +699,25 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
636
699
|
customBlockModels: customBlockModels || undefined
|
|
637
700
|
})
|
|
638
701
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
702
|
+
// WASM mesher pushes heightmaps from `processColumnTick` after each
|
|
703
|
+
// column tick — the main thread no longer requests them on chunk load
|
|
704
|
+
// (would double-compute and starve the WASM hot loop). The JS mesher
|
|
705
|
+
// still needs the explicit request because it has no per-tick column
|
|
706
|
+
// pass.
|
|
707
|
+
if (!this.worldRendererConfig.wasmMesher) {
|
|
708
|
+
this.workers[0].postMessage({
|
|
709
|
+
type: 'getHeightmap',
|
|
710
|
+
x,
|
|
711
|
+
z,
|
|
712
|
+
})
|
|
713
|
+
}
|
|
644
714
|
this.logWorkerWork(() => `-> chunk ${JSON.stringify({ x, z, chunkLength: chunk.length, customBlockModelsLength: customBlockModels ? Object.keys(customBlockModels).length : 0 })}`)
|
|
645
715
|
this.mesherLogReader?.chunkReceived(x, z, chunk.length)
|
|
646
716
|
const sectionHeight = this.getSectionHeight()
|
|
647
717
|
const CHUNK_SIZE = 16
|
|
648
718
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const loc = new Vec3(x, this.worldMinYRender, z)
|
|
719
|
+
for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
720
|
+
const loc = new Vec3(x, y, z)
|
|
652
721
|
this.setSectionDirty(loc)
|
|
653
722
|
if (this.neighborChunkUpdates && (!isLightUpdate || this.worldRendererConfig.smoothLighting)) {
|
|
654
723
|
this.setSectionDirty(loc.offset(-CHUNK_SIZE, 0, 0))
|
|
@@ -656,17 +725,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
656
725
|
this.setSectionDirty(loc.offset(0, 0, -CHUNK_SIZE))
|
|
657
726
|
this.setSectionDirty(loc.offset(0, 0, CHUNK_SIZE))
|
|
658
727
|
}
|
|
659
|
-
} else {
|
|
660
|
-
for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
661
|
-
const loc = new Vec3(x, y, z)
|
|
662
|
-
this.setSectionDirty(loc)
|
|
663
|
-
if (this.neighborChunkUpdates && (!isLightUpdate || this.worldRendererConfig.smoothLighting)) {
|
|
664
|
-
this.setSectionDirty(loc.offset(-CHUNK_SIZE, 0, 0))
|
|
665
|
-
this.setSectionDirty(loc.offset(CHUNK_SIZE, 0, 0))
|
|
666
|
-
this.setSectionDirty(loc.offset(0, 0, -CHUNK_SIZE))
|
|
667
|
-
this.setSectionDirty(loc.offset(0, 0, CHUNK_SIZE))
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
728
|
}
|
|
671
729
|
}
|
|
672
730
|
|
|
@@ -674,6 +732,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
674
732
|
this.loadedChunks[`${x},${z}`] = true
|
|
675
733
|
this.finishedChunks[`${x},${z}`] = true
|
|
676
734
|
this.logWorkerWork(`-> markAsLoaded ${JSON.stringify({ x, z })}`)
|
|
735
|
+
// Mirror the main meshing path so the near-first reveal gate can
|
|
736
|
+
// re-evaluate any farther chunks blocked by this column.
|
|
737
|
+
this.renderUpdateEmitter.emit('chunkFinished', `${x},${z}`)
|
|
677
738
|
this.checkAllFinished()
|
|
678
739
|
}
|
|
679
740
|
|
|
@@ -706,15 +767,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
706
767
|
this.initialChunkLoadWasStartedIn = undefined
|
|
707
768
|
}
|
|
708
769
|
const sectionHeight = this.getSectionHeight()
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
this.
|
|
712
|
-
delete this.finishedSections[`${x},${sectionY},${z}`]
|
|
713
|
-
} else {
|
|
714
|
-
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
715
|
-
this.setSectionDirty(new Vec3(x, y, z), false)
|
|
716
|
-
delete this.finishedSections[`${x},${y},${z}`]
|
|
717
|
-
}
|
|
770
|
+
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
771
|
+
this.setSectionDirty(new Vec3(x, y, z), false)
|
|
772
|
+
delete this.finishedSections[`${x},${y},${z}`]
|
|
718
773
|
}
|
|
719
774
|
this.highestBlocksByChunks.delete(`${x},${z}`)
|
|
720
775
|
this.reactiveState.world.heightmaps.delete(`${Math.floor(x / 16)},${Math.floor(z / 16)}`)
|
|
@@ -885,12 +940,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
885
940
|
const chunkCornerX = Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE
|
|
886
941
|
const chunkCornerZ = Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE
|
|
887
942
|
const chunkKey2 = `${chunkCornerX},${chunkCornerZ}`
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
this.
|
|
893
|
-
|
|
943
|
+
// WASM mesher pushes heightmaps from `processColumnTick`, so the
|
|
944
|
+
// block-update path doesn't need an explicit re-request — the next
|
|
945
|
+
// column tick will repush the recomputed heightmap.
|
|
946
|
+
if (!this.worldRendererConfig.wasmMesher) {
|
|
947
|
+
const existing = this.heightmapDebounceTimers.get(chunkKey2)
|
|
948
|
+
if (existing) clearTimeout(existing)
|
|
949
|
+
this.heightmapDebounceTimers.set(chunkKey2, setTimeout(() => {
|
|
950
|
+
this.heightmapDebounceTimers.delete(chunkKey2)
|
|
951
|
+
this.workers[0]?.postMessage({ type: 'getHeightmap', x: chunkCornerX, z: chunkCornerZ })
|
|
952
|
+
}, 100))
|
|
953
|
+
}
|
|
894
954
|
}
|
|
895
955
|
this.logWorkerWork(`-> blockUpdate ${JSON.stringify({ pos, stateId, customBlockModels })}`)
|
|
896
956
|
this.setSectionDirty(pos, true, true)
|
|
@@ -937,14 +997,19 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
937
997
|
getWorkerNumber(pos: Vec3, updateAction = false) {
|
|
938
998
|
const CHUNK_SIZE = 16
|
|
939
999
|
const sectionHeight = this.getSectionHeight()
|
|
1000
|
+
if (this.worldRendererConfig.wasmMesher) {
|
|
1001
|
+
// WASM column meshing must keep all vertical sections of a chunk column
|
|
1002
|
+
// on one worker. Hash by x/z only and bypass the change-worker shortcut
|
|
1003
|
+
// so block edits cannot remesh the same column concurrently on worker 0.
|
|
1004
|
+
return mod(Math.floor(pos.x / CHUNK_SIZE) + Math.floor(pos.z / CHUNK_SIZE), this.workers.length)
|
|
1005
|
+
}
|
|
940
1006
|
if (updateAction) {
|
|
941
1007
|
const key = `${Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE},${Math.floor(pos.y / sectionHeight) * sectionHeight},${Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE}`
|
|
942
1008
|
const cantUseChangeWorker = this.sectionsWaiting.get(key) && !this.finishedSections[key]
|
|
943
1009
|
if (!cantUseChangeWorker) return 0
|
|
944
1010
|
}
|
|
945
1011
|
|
|
946
|
-
|
|
947
|
-
return hash + 0
|
|
1012
|
+
return mod(Math.floor(pos.x / CHUNK_SIZE) + Math.floor(pos.y / sectionHeight) + Math.floor(pos.z / CHUNK_SIZE), this.workers.length)
|
|
948
1013
|
}
|
|
949
1014
|
|
|
950
1015
|
async debugGetWorkerCustomBlockModel(pos: Vec3) {
|
|
@@ -1064,8 +1129,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
1064
1129
|
// group messages and send as one
|
|
1065
1130
|
for (const workerIndex in this.toWorkerMessagesQueue) {
|
|
1066
1131
|
const worker = this.workers[Number(workerIndex)]
|
|
1067
|
-
|
|
1068
|
-
|
|
1132
|
+
const messages = this.toWorkerMessagesQueue[workerIndex]
|
|
1133
|
+
worker.postMessage(messages)
|
|
1134
|
+
for (const message of messages) {
|
|
1069
1135
|
this.logWorkerWork(`-> ${workerIndex} dispatchMessages ${message.type} ${JSON.stringify({ x: message.x, y: message.y, z: message.z, value: message.value })}`)
|
|
1070
1136
|
}
|
|
1071
1137
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export interface SignMeta { isWall: boolean; isHanging: boolean; rotation: number }
|
|
3
|
+
export interface HeadMeta { isWall: boolean; rotation: number }
|
|
4
|
+
export interface BannerMeta { isWall: boolean; blockName: string; rotation: number }
|
|
5
|
+
|
|
6
|
+
export interface BlockEntityMetadataTarget {
|
|
7
|
+
signs: Record<string, SignMeta>
|
|
8
|
+
heads: Record<string, HeadMeta>
|
|
9
|
+
banners: Record<string, BannerMeta>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BlockEntityMetadataOptions {
|
|
13
|
+
disableBlockEntityTextures?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type BlockLike = { name: string; getProperties(): any }
|
|
17
|
+
|
|
18
|
+
export function collectBlockEntityMetadata(
|
|
19
|
+
block: BlockLike,
|
|
20
|
+
x: number, y: number, z: number,
|
|
21
|
+
target: BlockEntityMetadataTarget,
|
|
22
|
+
options: BlockEntityMetadataOptions
|
|
23
|
+
): void {
|
|
24
|
+
if ((block.name.includes('_sign') || block.name === 'sign') && !options.disableBlockEntityTextures) {
|
|
25
|
+
const key = `${x},${y},${z}`
|
|
26
|
+
const props: any = block.getProperties()
|
|
27
|
+
const facingRotationMap = {
|
|
28
|
+
'north': 2,
|
|
29
|
+
'south': 0,
|
|
30
|
+
'west': 1,
|
|
31
|
+
'east': 3
|
|
32
|
+
}
|
|
33
|
+
const isWall = block.name.endsWith('wall_sign') || block.name.endsWith('wall_hanging_sign')
|
|
34
|
+
const isHanging = block.name.endsWith('hanging_sign')
|
|
35
|
+
target.signs[key] = {
|
|
36
|
+
isWall,
|
|
37
|
+
isHanging,
|
|
38
|
+
rotation: isWall ? facingRotationMap[props.facing] : +props.rotation
|
|
39
|
+
}
|
|
40
|
+
} else if (block.name === 'player_head' || block.name === 'player_wall_head') {
|
|
41
|
+
const key = `${x},${y},${z}`
|
|
42
|
+
const props: any = block.getProperties()
|
|
43
|
+
const facingRotationMap = {
|
|
44
|
+
'north': 0,
|
|
45
|
+
'south': 2,
|
|
46
|
+
'west': 3,
|
|
47
|
+
'east': 1
|
|
48
|
+
}
|
|
49
|
+
const isWall = block.name === 'player_wall_head'
|
|
50
|
+
target.heads[key] = {
|
|
51
|
+
isWall,
|
|
52
|
+
rotation: isWall ? facingRotationMap[props.facing] : +props.rotation
|
|
53
|
+
}
|
|
54
|
+
} else if (block.name.includes('_banner') && !options.disableBlockEntityTextures) {
|
|
55
|
+
const key = `${x},${y},${z}`
|
|
56
|
+
const props: any = block.getProperties()
|
|
57
|
+
const facingRotationMap = {
|
|
58
|
+
'north': 2,
|
|
59
|
+
'south': 0,
|
|
60
|
+
'west': 1,
|
|
61
|
+
'east': 3
|
|
62
|
+
}
|
|
63
|
+
const isWall = block.name.endsWith('_wall_banner')
|
|
64
|
+
target.banners[key] = {
|
|
65
|
+
isWall,
|
|
66
|
+
blockName: block.name, // Pass block name for base color extraction
|
|
67
|
+
rotation: isWall ? facingRotationMap[props.facing] : (props.rotation === undefined ? 0 : +props.rotation)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { Vec3 } from 'vec3'
|
|
3
|
+
import { World } from './world'
|
|
4
|
+
import { INVISIBLE_BLOCKS } from './worldConstants'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sentinel value written to the heightmap for any column that contains no
|
|
8
|
+
* non-INVISIBLE block in `[worldMinY, worldMaxY]`. Matches the value that
|
|
9
|
+
* Rust's `wasm-mesher` writes for empty columns in its `Vec<i16>` heightmap,
|
|
10
|
+
* so JS and Rust heightmaps are element-wise comparable.
|
|
11
|
+
*
|
|
12
|
+
* Downstream consumers (e.g. `src/three/modules/rain.ts`) MUST treat this
|
|
13
|
+
* value as "no surface" rather than as a real Y coordinate.
|
|
14
|
+
*/
|
|
15
|
+
export const EMPTY_COLUMN_HEIGHTMAP_SENTINEL = -32768
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute the surface heightmap for one 16x16 chunk column.
|
|
19
|
+
*
|
|
20
|
+
* Returns a 256-entry Int16Array indexed as `z * 16 + x`, where each entry is
|
|
21
|
+
* the world-Y of the highest non-INVISIBLE block in that column, or
|
|
22
|
+
* `EMPTY_COLUMN_HEIGHTMAP_SENTINEL` (-32768) if no such block exists.
|
|
23
|
+
*
|
|
24
|
+
* Shared by the JS-mode mesher (`mesher.ts`) and WASM-mode mesher
|
|
25
|
+
* (`mesherWasm.ts`) `getHeightmap` handlers to guarantee element-wise parity.
|
|
26
|
+
*/
|
|
27
|
+
export function computeHeightmap(world: World, chunkX: number, chunkZ: number): Int16Array {
|
|
28
|
+
const heightmap = new Int16Array(256)
|
|
29
|
+
|
|
30
|
+
const blockPos = new Vec3(0, 0, 0)
|
|
31
|
+
for (let z = 0; z < 16; z++) {
|
|
32
|
+
for (let x = 0; x < 16; x++) {
|
|
33
|
+
blockPos.x = x + chunkX
|
|
34
|
+
blockPos.z = z + chunkZ
|
|
35
|
+
blockPos.y = world.config.worldMaxY
|
|
36
|
+
let block = world.getBlock(blockPos)
|
|
37
|
+
while (block && INVISIBLE_BLOCKS.has(block.name) && blockPos.y > world.config.worldMinY) {
|
|
38
|
+
blockPos.y -= 1
|
|
39
|
+
block = world.getBlock(blockPos)
|
|
40
|
+
}
|
|
41
|
+
const index = z * 16 + x
|
|
42
|
+
// Loop exits either when we found a visible (non-INVISIBLE) block, or
|
|
43
|
+
// when we hit worldMinY with the column still entirely invisible/empty.
|
|
44
|
+
// Only the former is a real surface; the latter is the empty-column
|
|
45
|
+
// case and must use the sentinel to match Rust's encoding.
|
|
46
|
+
heightmap[index] = block && !INVISIBLE_BLOCKS.has(block.name)
|
|
47
|
+
? blockPos.y
|
|
48
|
+
: EMPTY_COLUMN_HEIGHTMAP_SENTINEL
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return heightmap
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Shared `getHeightmap` worker-handler logic.
|
|
56
|
+
*
|
|
57
|
+
* Both `mesher.ts` and `mesherWasm.ts` route their `case 'getHeightmap'` here so
|
|
58
|
+
* the post-message payload (key + heightmap) is computed in exactly one place.
|
|
59
|
+
* Test fixtures (see `wasm-mesher/test-section-boundary.ts`) invoke this helper
|
|
60
|
+
* directly to exercise the real handler path end-to-end.
|
|
61
|
+
*/
|
|
62
|
+
export function handleGetHeightmap(world: World, x: number, z: number): { key: string, heightmap: Int16Array } {
|
|
63
|
+
const heightmap = computeHeightmap(world, x, z)
|
|
64
|
+
const key = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
|
|
65
|
+
return { key, heightmap }
|
|
66
|
+
}
|
package/src/mesher/mesher.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Vec3 } from 'vec3'
|
|
|
3
3
|
import { World } from './world'
|
|
4
4
|
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
|
|
5
5
|
import { BlockStateModelInfo } from './shared'
|
|
6
|
-
import {
|
|
6
|
+
import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from './computeHeightmap'
|
|
7
7
|
|
|
8
8
|
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
|
9
9
|
|
|
@@ -109,6 +109,7 @@ const handleMessage = data => {
|
|
|
109
109
|
break
|
|
110
110
|
}
|
|
111
111
|
case 'chunk': {
|
|
112
|
+
if (!world) break
|
|
112
113
|
world.addColumn(data.x, data.z, data.chunk)
|
|
113
114
|
if (data.customBlockModels) {
|
|
114
115
|
const chunkKey = `${data.x},${data.z}`
|
|
@@ -117,6 +118,7 @@ const handleMessage = data => {
|
|
|
117
118
|
break
|
|
118
119
|
}
|
|
119
120
|
case 'unloadChunk': {
|
|
121
|
+
if (!world) break
|
|
120
122
|
world.removeColumn(data.x, data.z)
|
|
121
123
|
world.customBlockModels.delete(`${data.x},${data.z}`)
|
|
122
124
|
if (Object.keys(world.columns).length === 0) softCleanup()
|
|
@@ -146,6 +148,10 @@ const handleMessage = data => {
|
|
|
146
148
|
break
|
|
147
149
|
}
|
|
148
150
|
case 'getCustomBlockModel': {
|
|
151
|
+
if (!world) {
|
|
152
|
+
global.postMessage({ type: 'customBlockModel', chunkKey: '', customBlockModel: undefined })
|
|
153
|
+
break
|
|
154
|
+
}
|
|
149
155
|
const pos = new Vec3(data.pos.x, data.pos.y, data.pos.z)
|
|
150
156
|
const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
|
151
157
|
const customBlockModel = world.customBlockModels.get(chunkKey)?.[`${pos.x},${pos.y},${pos.z}`]
|
|
@@ -153,26 +159,13 @@ const handleMessage = data => {
|
|
|
153
159
|
break
|
|
154
160
|
}
|
|
155
161
|
case 'getHeightmap': {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
for (let x = 0; x < 16; x++) {
|
|
161
|
-
const blockX = x + data.x
|
|
162
|
-
const blockZ = z + data.z
|
|
163
|
-
blockPos.x = blockX
|
|
164
|
-
blockPos.z = blockZ
|
|
165
|
-
blockPos.y = world.config.worldMaxY
|
|
166
|
-
let block = world.getBlock(blockPos)
|
|
167
|
-
while (block && INVISIBLE_BLOCKS.has(block.name) && blockPos.y > world.config.worldMinY) {
|
|
168
|
-
blockPos.y -= 1
|
|
169
|
-
block = world.getBlock(blockPos)
|
|
170
|
-
}
|
|
171
|
-
const index = z * 16 + x
|
|
172
|
-
heightmap[index] = block ? blockPos.y : -32768
|
|
173
|
-
}
|
|
162
|
+
if (!world) {
|
|
163
|
+
const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
|
|
164
|
+
postMessage({ type: 'heightmap', key: `${Math.floor(data.x / 16)},${Math.floor(data.z / 16)}`, heightmap: emptyHeightmap })
|
|
165
|
+
break
|
|
174
166
|
}
|
|
175
|
-
|
|
167
|
+
const { key, heightmap } = handleGetHeightmap(world, data.x, data.z)
|
|
168
|
+
postMessage({ type: 'heightmap', key, heightmap })
|
|
176
169
|
|
|
177
170
|
break
|
|
178
171
|
}
|