minecraft-renderer 0.1.90 → 0.1.92
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 +76 -76
- package/dist/mesher.js.map +4 -4
- package/dist/mesherWasm.js +202 -202
- package/dist/minecraft-renderer.js +67 -67
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +424 -424
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +3 -0
- package/src/graphicsBackend/types.ts +8 -1
- package/src/lib/worldrendererCommon.ts +4 -1
- package/src/mesher-shared/exportedGeometryTypes.ts +1 -0
- package/src/mesher-shared/models.ts +10 -0
- package/src/mesher-shared/occludingBlocks.ts +53 -0
- package/src/mesher-shared/shared.ts +2 -0
- package/src/mesher-shared/tests/visGraph.test.ts +62 -0
- package/src/mesher-shared/visGraph.ts +155 -0
- package/src/mesher-shared/visibilitySet.ts +124 -0
- package/src/three/chunkMeshManager.ts +103 -4
- package/src/three/entities.ts +4 -1
- package/src/three/globalBlockBuffer.ts +6 -0
- package/src/three/globalLegacyBuffer.ts +6 -0
- package/src/three/occlusion/sectionOcclusionCull.ts +90 -0
- package/src/three/occlusion/sectionOcclusionGraph.ts +272 -0
- package/src/three/positionalAudioAttenuation.ts +35 -0
- package/src/three/tests/chunkMeshManagerLegacy.test.ts +7 -7
- package/src/three/tests/positionalAudioAttenuation.test.ts +51 -0
- package/src/three/tests/pr3PerFrameCpuCuts.test.ts +9 -9
- package/src/three/tests/sectionOcclusionCull.test.ts +51 -0
- package/src/three/tests/sectionOcclusionGraph.test.ts +134 -0
- package/src/three/tests/threeJsSound.test.ts +110 -0
- package/src/three/threeJsSound.ts +29 -11
- package/src/three/worldRendererThree.ts +30 -6
- package/src/wasm-mesher/bridge/convertChunk.ts +5 -51
- package/src/wasm-mesher/tests/sectionVisibilityFromWorld.test.ts +108 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +13 -2
|
@@ -35,6 +35,7 @@ import { disposeObject } from './threeJsUtils'
|
|
|
35
35
|
import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
|
|
36
36
|
import { getSignTexture, releaseSignTexture, disposeAllSignTextures } from './signTextureCache'
|
|
37
37
|
import { BlockEntityLightRegistry } from '../lib/blockEntityLightRegistry'
|
|
38
|
+
import { SectionOcclusionCull, hsvToRgb } from './occlusion/sectionOcclusionCull'
|
|
38
39
|
|
|
39
40
|
export interface ChunkMeshPool {
|
|
40
41
|
mesh: THREE.Mesh
|
|
@@ -72,6 +73,8 @@ export interface SectionObject extends THREE.Group {
|
|
|
72
73
|
worldX?: number
|
|
73
74
|
worldY?: number
|
|
74
75
|
worldZ?: number
|
|
76
|
+
/** Packed VisibilitySet from mesher (section occlusion graph). */
|
|
77
|
+
visibilitySet?: number
|
|
75
78
|
foutain?: boolean
|
|
76
79
|
/**
|
|
77
80
|
* True while the section is held invisible by the "Batch Chunks Display"
|
|
@@ -170,6 +173,9 @@ export class ChunkMeshManager {
|
|
|
170
173
|
globalLegacyBlendBuffer: GlobalLegacyBuffer | null = null
|
|
171
174
|
/** Tight world AABBs for shader-cube frustum culling (section centers). */
|
|
172
175
|
private readonly shaderSectionRaycastBoxes = new Map<string, ShaderSectionRaycastEntry>()
|
|
176
|
+
private readonly sectionOcclusionCull = new SectionOcclusionCull()
|
|
177
|
+
private _lastOcclusionVisibleKeys = ''
|
|
178
|
+
private _lastSmartCullEnabled: boolean | undefined
|
|
173
179
|
|
|
174
180
|
// Performance tracking
|
|
175
181
|
private hits = 0
|
|
@@ -399,7 +405,13 @@ export class ChunkMeshManager {
|
|
|
399
405
|
}
|
|
400
406
|
}
|
|
401
407
|
|
|
402
|
-
private updatePooledLegacyCullState(
|
|
408
|
+
private updatePooledLegacyCullState(
|
|
409
|
+
cameraWorldX: number,
|
|
410
|
+
cameraWorldY: number,
|
|
411
|
+
cameraWorldZ: number,
|
|
412
|
+
smartCull: boolean,
|
|
413
|
+
occlusionVisible: ReadonlySet<string>
|
|
414
|
+
): void {
|
|
403
415
|
for (const poolEntry of this.activeSections.values()) {
|
|
404
416
|
const sectionKey = poolEntry.sectionKey
|
|
405
417
|
if (!sectionKey) continue
|
|
@@ -419,16 +431,44 @@ export class ChunkMeshManager {
|
|
|
419
431
|
this._legacyCullBoxMin,
|
|
420
432
|
this._legacyCullBoxMax
|
|
421
433
|
)
|
|
434
|
+
if (smartCull && !occlusionVisible.has(sectionKey)) {
|
|
435
|
+
poolEntry.mesh.visible = false
|
|
436
|
+
}
|
|
422
437
|
}
|
|
423
438
|
}
|
|
424
439
|
|
|
440
|
+
isSectionOcclusionVisible(sectionKey: string): boolean {
|
|
441
|
+
return this.sectionOcclusionCull.isSectionVisible(sectionKey)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Invalidate occlusion graph and mark cull dirty when smart-cull enablement changes. */
|
|
445
|
+
notifySmartCullChanged(smartCull: boolean): void {
|
|
446
|
+
if (this._lastSmartCullEnabled === smartCull) return
|
|
447
|
+
this._lastSmartCullEnabled = smartCull
|
|
448
|
+
this.sectionOcclusionCull.invalidate()
|
|
449
|
+
this.markCullDirty()
|
|
450
|
+
}
|
|
451
|
+
|
|
425
452
|
/**
|
|
426
453
|
* Shared section visibility + span groups for global legacy and cube buffers.
|
|
427
454
|
*/
|
|
428
|
-
updateSectionCullAndSort(camera: THREE.Camera, cameraWorldX: number, cameraWorldY: number, cameraWorldZ: number): void {
|
|
455
|
+
updateSectionCullAndSort(camera: THREE.Camera, cameraWorldX: number, cameraWorldY: number, cameraWorldZ: number, smartCull: boolean): void {
|
|
429
456
|
this._legacyCullProjScreen.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
|
|
430
457
|
this._legacyCullFrustum.setFromProjectionMatrix(this._legacyCullProjScreen)
|
|
431
458
|
|
|
459
|
+
const sectionHeight = this.worldRenderer.getSectionHeight?.() ?? 16
|
|
460
|
+
const worldSizeParams = this.worldRenderer.worldSizeParams ?? { minY: 0, worldHeight: 256 }
|
|
461
|
+
const occlusionVisible = this.sectionOcclusionCull.update({
|
|
462
|
+
smartCull,
|
|
463
|
+
cameraWorldX,
|
|
464
|
+
cameraWorldY,
|
|
465
|
+
cameraWorldZ,
|
|
466
|
+
viewDistance: this.worldRenderer.viewDistance,
|
|
467
|
+
sectionHeight,
|
|
468
|
+
worldMinY: worldSizeParams.minY,
|
|
469
|
+
worldMaxY: worldSizeParams.minY + worldSizeParams.worldHeight
|
|
470
|
+
})
|
|
471
|
+
|
|
432
472
|
const visible = this._visibleSectionSpans
|
|
433
473
|
visible.length = 0
|
|
434
474
|
|
|
@@ -452,6 +492,14 @@ export class ChunkMeshManager {
|
|
|
452
492
|
}
|
|
453
493
|
}
|
|
454
494
|
|
|
495
|
+
if (smartCull) {
|
|
496
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
497
|
+
if (!occlusionVisible.has(visible[i]!.key)) {
|
|
498
|
+
visible.splice(i, 1)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
455
503
|
const opaqueBuf = this.globalLegacyBuffer
|
|
456
504
|
const blendBuf = this.globalLegacyBlendBuffer
|
|
457
505
|
const gb = this.globalBlockBuffer
|
|
@@ -493,6 +541,9 @@ export class ChunkMeshManager {
|
|
|
493
541
|
if (!inFrustum) {
|
|
494
542
|
return
|
|
495
543
|
}
|
|
544
|
+
if (smartCull && !occlusionVisible.has(key)) {
|
|
545
|
+
return
|
|
546
|
+
}
|
|
496
547
|
cubeVisibleKeys.push(key)
|
|
497
548
|
const drawStart = gb.getSectionDrawStart(key)
|
|
498
549
|
const drawCount = gb.getSectionDrawCount(key)
|
|
@@ -503,10 +554,13 @@ export class ChunkMeshManager {
|
|
|
503
554
|
}
|
|
504
555
|
cubeVisibleKeys.sort()
|
|
505
556
|
|
|
557
|
+
const occlusionFp = smartCull ? [...occlusionVisible].sort().join(',') : 'off'
|
|
506
558
|
const fingerprint = [
|
|
507
559
|
opaqueKeys.join(','),
|
|
508
560
|
blendKeys.join(','),
|
|
509
561
|
cubeVisibleKeys.join(','),
|
|
562
|
+
occlusionFp,
|
|
563
|
+
smartCull ? '1' : '0',
|
|
510
564
|
opaqueBuf?.getLayoutVersion() ?? 0,
|
|
511
565
|
blendBuf?.getLayoutVersion() ?? 0,
|
|
512
566
|
gb?.getLayoutVersion() ?? 0,
|
|
@@ -516,10 +570,12 @@ export class ChunkMeshManager {
|
|
|
516
570
|
].join('|')
|
|
517
571
|
|
|
518
572
|
if (fingerprint === this._lastCullFingerprint && !this.hasPendingBufferWork()) {
|
|
519
|
-
this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ)
|
|
573
|
+
this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ, smartCull, occlusionVisible)
|
|
574
|
+
this._lastOcclusionVisibleKeys = occlusionFp
|
|
520
575
|
return
|
|
521
576
|
}
|
|
522
577
|
this._lastCullFingerprint = fingerprint
|
|
578
|
+
this._lastOcclusionVisibleKeys = occlusionFp
|
|
523
579
|
|
|
524
580
|
opaqueBuf?.updateDrawSpans(visible, 'opaque')
|
|
525
581
|
blendBuf?.updateDrawSpans(visible, 'sortedBlend')
|
|
@@ -536,7 +592,7 @@ export class ChunkMeshManager {
|
|
|
536
592
|
}
|
|
537
593
|
|
|
538
594
|
this.lastBufferStateKey = this.bufferStateKey()
|
|
539
|
-
this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ)
|
|
595
|
+
this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ, smartCull, occlusionVisible)
|
|
540
596
|
}
|
|
541
597
|
|
|
542
598
|
private static readonly BLEND_RESORT_DISTANCE = 1.0
|
|
@@ -999,6 +1055,8 @@ export class ChunkMeshManager {
|
|
|
999
1055
|
sectionObject.worldX = geometryData.sx
|
|
1000
1056
|
sectionObject.worldY = geometryData.sy
|
|
1001
1057
|
sectionObject.worldZ = geometryData.sz
|
|
1058
|
+
sectionObject.visibilitySet = geometryData.visibilitySet
|
|
1059
|
+
this.sectionOcclusionCull.registerSection(sectionKey, geometryData.visibilitySet, geometryData.sx, geometryData.sy, geometryData.sz)
|
|
1002
1060
|
// Stamp the section key so modules (e.g. sciFiWorldReveal) can resolve
|
|
1003
1061
|
// mesh -> section without falling back to sceneOrigin world-position math.
|
|
1004
1062
|
;(sectionObject as any).sectionKey = sectionKey
|
|
@@ -1313,6 +1371,7 @@ export class ChunkMeshManager {
|
|
|
1313
1371
|
this.globalLegacyBlendBuffer?.removeSection(sectionKey)
|
|
1314
1372
|
this.maybeUnregisterLegacyCullSection(sectionKey)
|
|
1315
1373
|
this.unregisterShaderSectionRaycastBox(sectionKey)
|
|
1374
|
+
this.sectionOcclusionCull.unregisterSection(sectionKey)
|
|
1316
1375
|
}
|
|
1317
1376
|
this.markCullDirty()
|
|
1318
1377
|
delete sectionObject.deferredLegacyOpaque
|
|
@@ -1450,6 +1509,38 @@ export class ChunkMeshManager {
|
|
|
1450
1509
|
}
|
|
1451
1510
|
}
|
|
1452
1511
|
|
|
1512
|
+
/**
|
|
1513
|
+
* Debug overlay: tint section borders by occlusion BFS step (Java ChunkCullingDebugRenderer paths mode).
|
|
1514
|
+
*/
|
|
1515
|
+
updateCaveCullingDebug(enabled: boolean, smartCull: boolean): void {
|
|
1516
|
+
if (!enabled) {
|
|
1517
|
+
for (const sectionKey of Object.keys(this.sectionObjects)) {
|
|
1518
|
+
const sectionObject = this.sectionObjects[sectionKey]
|
|
1519
|
+
if (sectionObject?.boxHelper && !this.worldRenderer.displayOptions?.inWorldRenderingConfig?.showChunkBorders) {
|
|
1520
|
+
sectionObject.boxHelper.visible = false
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
|
|
1527
|
+
if (!sectionObject) continue
|
|
1528
|
+
if (!sectionObject.boxHelper) {
|
|
1529
|
+
this.updateBoxHelper(sectionKey, true)
|
|
1530
|
+
}
|
|
1531
|
+
const helper = sectionObject.boxHelper
|
|
1532
|
+
if (!helper) continue
|
|
1533
|
+
helper.visible = true
|
|
1534
|
+
const color = !smartCull
|
|
1535
|
+
? 0x00_ff_00
|
|
1536
|
+
: this.sectionOcclusionCull.isSectionVisible(sectionKey)
|
|
1537
|
+
? hsvToRgb(this.sectionOcclusionCull.getStep(sectionKey) ?? 0)
|
|
1538
|
+
: 0xff_00_00
|
|
1539
|
+
const mat = helper.material as THREE.LineBasicMaterial
|
|
1540
|
+
mat.color.setHex(color)
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1453
1544
|
/**
|
|
1454
1545
|
* Get mesh for section if it exists
|
|
1455
1546
|
*/
|
|
@@ -1516,6 +1607,14 @@ export class ChunkMeshManager {
|
|
|
1516
1607
|
}
|
|
1517
1608
|
}
|
|
1518
1609
|
|
|
1610
|
+
getDrawnStats(): { cubeFaces: number; legacyOpaqueQuads: number; legacyBlendQuads: number } {
|
|
1611
|
+
return {
|
|
1612
|
+
cubeFaces: this.globalBlockBuffer?.getVisibleFaceCount() ?? 0,
|
|
1613
|
+
legacyOpaqueQuads: this.globalLegacyBuffer?.getVisibleQuadCount() ?? 0,
|
|
1614
|
+
legacyBlendQuads: this.globalLegacyBlendBuffer?.getVisibleQuadCount() ?? 0
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1519
1618
|
getStats() {
|
|
1520
1619
|
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
|
|
1521
1620
|
const hitRate = this.hits + this.misses > 0 ? ((this.hits / (this.hits + this.misses)) * 100).toFixed(1) : '0'
|
package/src/three/entities.ts
CHANGED
|
@@ -457,7 +457,10 @@ export class Entities {
|
|
|
457
457
|
const dz = entity.position.z - botPos.z
|
|
458
458
|
const distanceSquared = dx * dx + dy * dy + dz * dz
|
|
459
459
|
|
|
460
|
-
|
|
460
|
+
const sectionKey = this.worldRenderer.entitySectionKey(entity.position.x, entity.position.y, entity.position.z)
|
|
461
|
+
const occlusionVisible = this.worldRenderer.isSectionOcclusionVisible(sectionKey)
|
|
462
|
+
|
|
463
|
+
entity.visible = !!((distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.shouldObjectVisible(entity)) && occlusionVisible)
|
|
461
464
|
|
|
462
465
|
this.maybeRenderPlayerSkin(entityIdRaw)
|
|
463
466
|
}
|
|
@@ -147,6 +147,12 @@ export class GlobalBlockBuffer {
|
|
|
147
147
|
return this.visibleSpans
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
getVisibleFaceCount(): number {
|
|
151
|
+
let faces = 0
|
|
152
|
+
for (const s of this.visibleSpans) faces += s.count
|
|
153
|
+
return faces
|
|
154
|
+
}
|
|
155
|
+
|
|
150
156
|
forEachSectionSlot(cb: (key: string, slot: { start: number; count: number }) => void): void {
|
|
151
157
|
for (const [key, slot] of this.sectionSlots) {
|
|
152
158
|
cb(key, slot)
|
|
@@ -216,6 +216,12 @@ export class GlobalLegacyBuffer {
|
|
|
216
216
|
return this.visibleIndexSpans
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
getVisibleQuadCount(): number {
|
|
220
|
+
let indices = 0
|
|
221
|
+
for (const s of this.visibleIndexSpans) indices += s.indexCount
|
|
222
|
+
return indices / INDICES_PER_QUAD
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
private syncDefaultDrawGroups(): void {
|
|
220
226
|
const geometry = this.mesh.geometry
|
|
221
227
|
geometry.clearGroups()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { SectionOcclusionGraph, type OcclusionSectionRecord, type OcclusionUpdateParams } from './sectionOcclusionGraph'
|
|
3
|
+
import { VISIBILITY_SET_ALL_TRUE } from '../../mesher-shared/visibilitySet'
|
|
4
|
+
|
|
5
|
+
export type { OcclusionSectionRecord, OcclusionUpdateParams }
|
|
6
|
+
|
|
7
|
+
export class SectionOcclusionCull {
|
|
8
|
+
private readonly graph = new SectionOcclusionGraph()
|
|
9
|
+
private readonly registered = new Set<string>()
|
|
10
|
+
private lastVisible = new Set<string>()
|
|
11
|
+
|
|
12
|
+
registerSection(key: string, visibilitySet: number | undefined, worldX: number, worldY: number, worldZ: number): void {
|
|
13
|
+
this.registered.add(key)
|
|
14
|
+
this.graph.registerSection(key, {
|
|
15
|
+
visibilitySet: visibilitySet ?? VISIBILITY_SET_ALL_TRUE,
|
|
16
|
+
worldX,
|
|
17
|
+
worldY,
|
|
18
|
+
worldZ
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
unregisterSection(key: string): void {
|
|
23
|
+
this.registered.delete(key)
|
|
24
|
+
this.graph.unregisterSection(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
invalidate(): void {
|
|
28
|
+
this.graph.invalidate()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
update(params: OcclusionUpdateParams): Set<string> {
|
|
32
|
+
this.lastVisible = this.graph.update(params)
|
|
33
|
+
return this.lastVisible
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isSectionVisible(key: string): boolean {
|
|
37
|
+
if (!this.registered.has(key)) return true
|
|
38
|
+
return this.lastVisible.has(key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
hasRegisteredSection(key: string): boolean {
|
|
42
|
+
return this.registered.has(key)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getVisibleKeys(): ReadonlySet<string> {
|
|
46
|
+
return this.lastVisible
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getStep(key: string): number | undefined {
|
|
50
|
+
return this.graph.getStep(key)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getGraph(): SectionOcclusionGraph {
|
|
54
|
+
return this.graph
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function hsvToRgb(step: number): number {
|
|
59
|
+
const hue = (step % 50) / 50
|
|
60
|
+
const h = hue * 6
|
|
61
|
+
const c = 0.9
|
|
62
|
+
const x = c * (1 - Math.abs((h % 2) - 1))
|
|
63
|
+
let r = 0
|
|
64
|
+
let g = 0
|
|
65
|
+
let b = 0
|
|
66
|
+
if (h < 1) {
|
|
67
|
+
r = c
|
|
68
|
+
g = x
|
|
69
|
+
} else if (h < 2) {
|
|
70
|
+
r = x
|
|
71
|
+
g = c
|
|
72
|
+
} else if (h < 3) {
|
|
73
|
+
g = c
|
|
74
|
+
b = x
|
|
75
|
+
} else if (h < 4) {
|
|
76
|
+
g = x
|
|
77
|
+
b = c
|
|
78
|
+
} else if (h < 5) {
|
|
79
|
+
r = x
|
|
80
|
+
b = c
|
|
81
|
+
} else {
|
|
82
|
+
r = c
|
|
83
|
+
b = x
|
|
84
|
+
}
|
|
85
|
+
const m = 0.1
|
|
86
|
+
const ri = Math.round((r + m) * 255)
|
|
87
|
+
const gi = Math.round((g + m) * 255)
|
|
88
|
+
const bi = Math.round((b + m) * 255)
|
|
89
|
+
return (ri << 16) | (gi << 8) | bi
|
|
90
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Sync port of Minecraft SectionOcclusionGraph BFS (v1 — no Octree/async/advanced ray-march).
|
|
4
|
+
* @see extracted_minecraft_data/client/net/minecraft/client/renderer/SectionOcclusionGraph.java
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Vec3 } from 'vec3'
|
|
8
|
+
import { Direction, DIRECTIONS, VISIBILITY_SET_ALL_TRUE, oppositeDirection, visibilityBetweenPacked } from '../../mesher-shared/visibilitySet'
|
|
9
|
+
|
|
10
|
+
export type OcclusionSectionRecord = {
|
|
11
|
+
visibilitySet: number
|
|
12
|
+
worldX: number
|
|
13
|
+
worldY: number
|
|
14
|
+
worldZ: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type OcclusionUpdateParams = {
|
|
18
|
+
smartCull: boolean
|
|
19
|
+
cameraWorldX: number
|
|
20
|
+
cameraWorldY: number
|
|
21
|
+
cameraWorldZ: number
|
|
22
|
+
viewDistance: number
|
|
23
|
+
sectionHeight: number
|
|
24
|
+
worldMinY: number
|
|
25
|
+
worldMaxY: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class OcclusionNode {
|
|
29
|
+
readonly sectionKey: string
|
|
30
|
+
private sourceDirections = 0
|
|
31
|
+
directions = 0
|
|
32
|
+
readonly step: number
|
|
33
|
+
|
|
34
|
+
constructor(sectionKey: string, sourceDir: Direction | null, step: number) {
|
|
35
|
+
this.sectionKey = sectionKey
|
|
36
|
+
this.step = step
|
|
37
|
+
if (sourceDir != null) {
|
|
38
|
+
this.addSourceDirection(sourceDir)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setDirections(fromParent: number, exitDir: Direction): void {
|
|
43
|
+
this.directions = (fromParent | this.directions | (1 << exitDir)) >>> 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
hasDirection(dir: Direction): boolean {
|
|
47
|
+
return (this.directions & (1 << dir)) !== 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addSourceDirection(dir: Direction): void {
|
|
51
|
+
this.sourceDirections = (this.sourceDirections | (1 << dir)) >>> 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
hasSourceDirection(index: number): boolean {
|
|
55
|
+
return (this.sourceDirections & (1 << index)) !== 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
hasSourceDirections(): boolean {
|
|
59
|
+
return this.sourceDirections !== 0
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sectionKeyFromWorld(worldX: number, worldY: number, worldZ: number): string {
|
|
64
|
+
return `${worldX},${worldY},${worldZ}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseSectionKey(key: string): { x: number; y: number; z: number } {
|
|
68
|
+
const [x, y, z] = key.split(',').map(Number)
|
|
69
|
+
return { x: x!, y: y!, z: z! }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getNeighborKey(key: string, dir: Direction, sectionHeight: number): string {
|
|
73
|
+
const { x, y, z } = parseSectionKey(key)
|
|
74
|
+
switch (dir) {
|
|
75
|
+
case Direction.DOWN:
|
|
76
|
+
return sectionKeyFromWorld(x, y - sectionHeight, z)
|
|
77
|
+
case Direction.UP:
|
|
78
|
+
return sectionKeyFromWorld(x, y + sectionHeight, z)
|
|
79
|
+
case Direction.NORTH:
|
|
80
|
+
return sectionKeyFromWorld(x, y, z - 16)
|
|
81
|
+
case Direction.SOUTH:
|
|
82
|
+
return sectionKeyFromWorld(x, y, z + 16)
|
|
83
|
+
case Direction.WEST:
|
|
84
|
+
return sectionKeyFromWorld(x - 16, y, z)
|
|
85
|
+
case Direction.EAST:
|
|
86
|
+
return sectionKeyFromWorld(x + 16, y, z)
|
|
87
|
+
default:
|
|
88
|
+
return key
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isInViewDistance(cameraKey: string, sectionKey: string, viewDistance: number, sectionHeight: number): boolean {
|
|
93
|
+
const cam = parseSectionKey(cameraKey)
|
|
94
|
+
const sec = parseSectionKey(sectionKey)
|
|
95
|
+
const camChunkX = Math.floor(cam.x / 16)
|
|
96
|
+
const camChunkZ = Math.floor(cam.z / 16)
|
|
97
|
+
const secChunkX = Math.floor(sec.x / 16)
|
|
98
|
+
const secChunkZ = Math.floor(sec.z / 16)
|
|
99
|
+
if (Math.abs(secChunkX - camChunkX) > viewDistance || Math.abs(secChunkZ - camChunkZ) > viewDistance) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
const camSecY = Math.floor(cam.y / sectionHeight)
|
|
103
|
+
const secSecY = Math.floor(sec.y / sectionHeight)
|
|
104
|
+
return Math.abs(secSecY - camSecY) <= viewDistance
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class SectionOcclusionGraph {
|
|
108
|
+
private readonly sections = new Map<string, OcclusionSectionRecord>()
|
|
109
|
+
private readonly nodeByKey = new Map<string, OcclusionNode>()
|
|
110
|
+
private visibleKeys = new Set<string>()
|
|
111
|
+
private stepByKey = new Map<string, number>()
|
|
112
|
+
private needsFullUpdate = true
|
|
113
|
+
private lastCameraKey = ''
|
|
114
|
+
|
|
115
|
+
registerSection(key: string, record: OcclusionSectionRecord): void {
|
|
116
|
+
// v1: every register/unregister triggers a synchronous full BFS on next update (no partial graph).
|
|
117
|
+
this.sections.set(key, record)
|
|
118
|
+
this.needsFullUpdate = true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
unregisterSection(key: string): void {
|
|
122
|
+
if (this.sections.delete(key)) {
|
|
123
|
+
this.needsFullUpdate = true
|
|
124
|
+
}
|
|
125
|
+
this.nodeByKey.delete(key)
|
|
126
|
+
this.visibleKeys.delete(key)
|
|
127
|
+
this.stepByKey.delete(key)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
invalidate(): void {
|
|
131
|
+
this.needsFullUpdate = true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getVisibleKeys(): ReadonlySet<string> {
|
|
135
|
+
return this.visibleKeys
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getStep(key: string): number | undefined {
|
|
139
|
+
return this.stepByKey.get(key)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
isVisible(key: string): boolean {
|
|
143
|
+
return this.visibleKeys.has(key)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
update(params: OcclusionUpdateParams): Set<string> {
|
|
147
|
+
const cameraKey = sectionKeyFromWorld(
|
|
148
|
+
Math.floor(params.cameraWorldX / 16) * 16,
|
|
149
|
+
Math.floor(params.cameraWorldY / params.sectionHeight) * params.sectionHeight,
|
|
150
|
+
Math.floor(params.cameraWorldZ / 16) * 16
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if (cameraKey !== this.lastCameraKey) {
|
|
154
|
+
this.needsFullUpdate = true
|
|
155
|
+
this.lastCameraKey = cameraKey
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!params.smartCull) {
|
|
159
|
+
this.visibleKeys = new Set(this.sections.keys())
|
|
160
|
+
this.stepByKey.clear()
|
|
161
|
+
for (const key of this.visibleKeys) {
|
|
162
|
+
this.stepByKey.set(key, 0)
|
|
163
|
+
}
|
|
164
|
+
return this.visibleKeys
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (this.needsFullUpdate) {
|
|
168
|
+
this.runFullUpdate(cameraKey, params)
|
|
169
|
+
this.needsFullUpdate = false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return this.visibleKeys
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private runFullUpdate(cameraKey: string, params: OcclusionUpdateParams): void {
|
|
176
|
+
this.nodeByKey.clear()
|
|
177
|
+
this.visibleKeys = new Set()
|
|
178
|
+
this.stepByKey.clear()
|
|
179
|
+
|
|
180
|
+
const queue: OcclusionNode[] = []
|
|
181
|
+
this.initializeQueueForFullUpdate(cameraKey, queue, params)
|
|
182
|
+
|
|
183
|
+
while (queue.length > 0) {
|
|
184
|
+
const node = queue.shift()!
|
|
185
|
+
const sectionKey = node.sectionKey
|
|
186
|
+
if (!this.sections.has(sectionKey)) continue
|
|
187
|
+
|
|
188
|
+
this.visibleKeys.add(sectionKey)
|
|
189
|
+
this.stepByKey.set(sectionKey, node.step)
|
|
190
|
+
|
|
191
|
+
const visibilitySet = this.sections.get(sectionKey)?.visibilitySet ?? VISIBILITY_SET_ALL_TRUE
|
|
192
|
+
|
|
193
|
+
for (const exitDir of DIRECTIONS) {
|
|
194
|
+
const neighborSectionKey = getNeighborKey(sectionKey, exitDir, params.sectionHeight)
|
|
195
|
+
if (!this.sections.has(neighborSectionKey)) continue
|
|
196
|
+
if (!isInViewDistance(cameraKey, neighborSectionKey, params.viewDistance, params.sectionHeight)) continue
|
|
197
|
+
|
|
198
|
+
if (node.hasDirection(oppositeDirection(exitDir))) continue
|
|
199
|
+
|
|
200
|
+
if (node.hasSourceDirections()) {
|
|
201
|
+
let canSee = false
|
|
202
|
+
for (let i = 0; i < DIRECTIONS.length; i++) {
|
|
203
|
+
if (node.hasSourceDirection(i) && visibilityBetweenPacked(visibilitySet, oppositeDirection(DIRECTIONS[i]!), exitDir)) {
|
|
204
|
+
canSee = true
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!canSee) continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const existing = this.nodeByKey.get(neighborSectionKey)
|
|
212
|
+
if (existing) {
|
|
213
|
+
existing.addSourceDirection(exitDir)
|
|
214
|
+
} else {
|
|
215
|
+
const next = new OcclusionNode(neighborSectionKey, exitDir, node.step + 1)
|
|
216
|
+
next.setDirections(node.directions, exitDir)
|
|
217
|
+
this.nodeByKey.set(neighborSectionKey, next)
|
|
218
|
+
queue.push(next)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @see SectionOcclusionGraph.initializeQueueForFullUpdate */
|
|
225
|
+
private initializeQueueForFullUpdate(cameraKey: string, queue: OcclusionNode[], params: OcclusionUpdateParams): void {
|
|
226
|
+
if (this.sections.has(cameraKey)) {
|
|
227
|
+
const node = new OcclusionNode(cameraKey, null, 0)
|
|
228
|
+
this.nodeByKey.set(cameraKey, node)
|
|
229
|
+
queue.push(node)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const cam = parseSectionKey(cameraKey)
|
|
234
|
+
const camSecY = Math.floor(cam.y / params.sectionHeight)
|
|
235
|
+
const minSecY = Math.floor(params.worldMinY / params.sectionHeight)
|
|
236
|
+
const maxSecY = Math.floor((params.worldMaxY - 1) / params.sectionHeight)
|
|
237
|
+
const belowMin = camSecY < minSecY
|
|
238
|
+
const surfaceSecY = belowMin ? minSecY : maxSecY
|
|
239
|
+
const surfaceY = surfaceSecY * params.sectionHeight
|
|
240
|
+
|
|
241
|
+
const camChunkX = Math.floor(cam.x / 16)
|
|
242
|
+
const camChunkZ = Math.floor(cam.z / 16)
|
|
243
|
+
const seeds: Array<{ node: OcclusionNode; distSq: number }> = []
|
|
244
|
+
|
|
245
|
+
for (let dx = -params.viewDistance; dx <= params.viewDistance; dx++) {
|
|
246
|
+
for (let dz = -params.viewDistance; dz <= params.viewDistance; dz++) {
|
|
247
|
+
const key = sectionKeyFromWorld((camChunkX + dx) * 16, surfaceY, (camChunkZ + dz) * 16)
|
|
248
|
+
if (!this.sections.has(key)) continue
|
|
249
|
+
if (!isInViewDistance(cameraKey, key, params.viewDistance, params.sectionHeight)) continue
|
|
250
|
+
|
|
251
|
+
const entryDir = belowMin ? Direction.UP : Direction.DOWN
|
|
252
|
+
const node = new OcclusionNode(key, entryDir, 0)
|
|
253
|
+
node.setDirections(0, entryDir)
|
|
254
|
+
if (dx > 0) node.setDirections(node.directions, Direction.EAST)
|
|
255
|
+
else if (dx < 0) node.setDirections(node.directions, Direction.WEST)
|
|
256
|
+
if (dz > 0) node.setDirections(node.directions, Direction.SOUTH)
|
|
257
|
+
else if (dz < 0) node.setDirections(node.directions, Direction.NORTH)
|
|
258
|
+
|
|
259
|
+
const center = new Vec3(cam.x + 8, cam.y + 8, cam.z + 8)
|
|
260
|
+
const sec = parseSectionKey(key)
|
|
261
|
+
const distSq = center.distanceTo(new Vec3(sec.x + 8, sec.y + 8, sec.z + 8))
|
|
262
|
+
seeds.push({ node, distSq })
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
seeds.sort((a, b) => a.distSq - b.distSq)
|
|
267
|
+
for (const seed of seeds) {
|
|
268
|
+
this.nodeByKey.set(seed.node.sectionKey, seed.node)
|
|
269
|
+
queue.push(seed.node)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export const DEFAULT_ATTENUATION_DISTANCE = 16
|
|
3
|
+
|
|
4
|
+
export function computeEffectiveVolume(soundEntryVolume: number, packetVolume: number): number {
|
|
5
|
+
return soundEntryVolume * Math.max(packetVolume, 0)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function computeIndividualGain(effectiveVolume: number): number {
|
|
9
|
+
return Math.min(Math.max(effectiveVolume, 0), 1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function computePositionalAudioParams(effectiveVolume: number, attenuationDistance: number): { individualGain: number; maxDistance: number } {
|
|
13
|
+
return {
|
|
14
|
+
individualGain: computeIndividualGain(effectiveVolume),
|
|
15
|
+
maxDistance: Math.max(effectiveVolume, 1) * attenuationDistance
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function applyVanillaLinearPanner(panner: PannerNode, maxDistance: number): void {
|
|
20
|
+
panner.distanceModel = 'linear'
|
|
21
|
+
panner.refDistance = 0
|
|
22
|
+
panner.rolloffFactor = 1
|
|
23
|
+
panner.maxDistance = maxDistance
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setPannerPositionImmediate(panner: PannerNode, audioContext: AudioContext, x: number, y: number, z: number): void {
|
|
27
|
+
const t = audioContext.currentTime
|
|
28
|
+
if (panner.positionX) {
|
|
29
|
+
panner.positionX.setValueAtTime(x, t)
|
|
30
|
+
panner.positionY.setValueAtTime(y, t)
|
|
31
|
+
panner.positionZ.setValueAtTime(z, t)
|
|
32
|
+
} else {
|
|
33
|
+
panner.setPosition(x, y, z)
|
|
34
|
+
}
|
|
35
|
+
}
|