minecraft-renderer 0.1.107 → 0.1.108

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.
Files changed (90) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +80 -80
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +513 -513
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +5 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +13 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +3 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  19. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  20. package/src/lib/worldrendererCommon.ts +329 -67
  21. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  22. package/src/lighting/cache/rendererLightCache.ts +248 -0
  23. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  25. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  26. package/src/lighting/debug/clientLightProbe.ts +20 -0
  27. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  28. package/src/lighting/debug/clientLightTrace.ts +335 -0
  29. package/src/lighting/entity/entityLightController.test.ts +216 -0
  30. package/src/lighting/entity/entityLightController.ts +322 -0
  31. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  32. package/src/lighting/owner/clientLightOwner.ts +523 -0
  33. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  34. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  35. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  36. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  37. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  38. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  39. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  40. package/src/lighting/tables/lightTables1171.ts +59 -0
  41. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  42. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  43. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  44. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  45. package/src/mesher-shared/lightNibblePack.ts +26 -0
  46. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  47. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  48. package/src/mesher-shared/shared.ts +48 -1
  49. package/src/three/chunkMeshManager.ts +26 -0
  50. package/src/three/entities.ts +77 -11
  51. package/src/three/entity/EntityMesh.ts +1 -0
  52. package/src/three/entity/legWalkAnimation.test.ts +11 -2
  53. package/src/three/entity/legWalkAnimation.ts +1 -9
  54. package/src/three/globalBlockBuffer.ts +94 -2
  55. package/src/three/globalLegacyBuffer.ts +94 -2
  56. package/src/three/graphicsBackendBase.ts +2 -9
  57. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  58. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  59. package/src/three/worldRendererThree.ts +75 -34
  60. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  61. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  62. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  63. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  64. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  65. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  66. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  67. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  68. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  69. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  70. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  71. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  72. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  73. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  74. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  75. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  76. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  77. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  78. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  79. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  80. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  81. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  82. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  83. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  84. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  85. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  86. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  87. package/src/worldView/types.ts +1 -0
  88. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  89. package/src/worldView/worldView.spiral.test.ts +1 -1
  90. package/src/worldView/worldView.ts +46 -1
@@ -0,0 +1,123 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Flush policy for buffered section geometry (`pendingSectionUpdates`).
4
+ *
5
+ * A remesh of an already-visible section must not be installed alone while a
6
+ * face neighbour is still being re-meshed: the neighbour's faces towards this
7
+ * section were culled against the OLD block state, so installing one side
8
+ * first exposes a see-through hole (the "sky flash" on dig/place).
9
+ *
10
+ * The policy therefore works on *groups*: pending sections that are face
11
+ * adjacent form a connected group, and a group is installed only when every
12
+ * outstanding neighbour of every member has arrived. When the buffer deadline
13
+ * expires we still flush, but we flush the whole group in one batch — never a
14
+ * single member, which is exactly the hole the buffer exists to prevent.
15
+ */
16
+
17
+ export type PendingFlushQuery = {
18
+ /** Keys currently buffered, in insertion order. */
19
+ pendingKeys: Iterable<string>
20
+ /** When each key was first buffered. */
21
+ startedAt: (key: string) => number | undefined
22
+ now: number
23
+ maxBufferMs: number
24
+ sectionHeight: number
25
+ /** Section is dirty on a worker and its geometry has not arrived yet. */
26
+ isOutstanding: (key: string) => boolean
27
+ /**
28
+ * When set, neighbour waits use topology outstanding instead of covering/light
29
+ * outstanding. Omit for bit-for-bit legacy flush behavior.
30
+ */
31
+ isTopologyOutstanding?: (key: string) => boolean
32
+ /** Section currently has geometry on screen (an air section has none). */
33
+ hasSectionObject: (key: string) => boolean
34
+ }
35
+
36
+ export function faceNeighborKeys(key: string, sectionHeight: number): string[] {
37
+ const [sx, sy, sz] = key.split(',').map(Number)
38
+ return [
39
+ `${sx - 16},${sy},${sz}`,
40
+ `${sx + 16},${sy},${sz}`,
41
+ `${sx},${sy - sectionHeight},${sz}`,
42
+ `${sx},${sy + sectionHeight},${sz}`,
43
+ `${sx},${sy},${sz - 16}`,
44
+ `${sx},${sy},${sz + 16}`
45
+ ]
46
+ }
47
+
48
+ /**
49
+ * Connected components of the buffered sections under face adjacency. Members
50
+ * of one component must be installed in the same frame.
51
+ */
52
+ export function pendingSectionGroups(pendingKeys: Iterable<string>, sectionHeight: number): string[][] {
53
+ const remaining = new Set(pendingKeys)
54
+ const groups: string[][] = []
55
+ for (const seed of [...remaining]) {
56
+ if (!remaining.has(seed)) continue
57
+ const group: string[] = []
58
+ const stack = [seed]
59
+ remaining.delete(seed)
60
+ while (stack.length > 0) {
61
+ const key = stack.pop()!
62
+ group.push(key)
63
+ for (const neighbor of faceNeighborKeys(key, sectionHeight)) {
64
+ if (remaining.delete(neighbor)) stack.push(neighbor)
65
+ }
66
+ }
67
+ groups.push(group)
68
+ }
69
+ return groups
70
+ }
71
+
72
+ export type PendingFlushReason = 'group-complete' | 'deadline'
73
+
74
+ export type ReadySectionFlush = {
75
+ key: string
76
+ reason: PendingFlushReason
77
+ }
78
+
79
+ /**
80
+ * Keys to install this frame. Either a group is complete (no member still
81
+ * waiting on a visible neighbour) or its oldest member has run out of buffer
82
+ * time — in both cases the whole group goes out together.
83
+ */
84
+ export function selectReadySectionFlushes(query: PendingFlushQuery): ReadySectionFlush[] {
85
+ const { now, maxBufferMs, sectionHeight } = query
86
+ const ready: ReadySectionFlush[] = []
87
+
88
+ for (const group of pendingSectionGroups(query.pendingKeys, sectionHeight)) {
89
+ const members = new Set(group)
90
+
91
+ let oldestStart = now
92
+ for (const key of group) {
93
+ const startedAt = query.startedAt(key) ?? now
94
+ if (startedAt < oldestStart) oldestStart = startedAt
95
+ }
96
+ const expired = now - oldestStart >= maxBufferMs
97
+
98
+ let waitingOnNeighbor = false
99
+ if (!expired) {
100
+ for (const key of group) {
101
+ for (const neighbor of faceNeighborKeys(key, sectionHeight)) {
102
+ if (members.has(neighbor)) continue
103
+ const neighborOutstanding = query.isTopologyOutstanding ? query.isTopologyOutstanding(neighbor) : query.isOutstanding(neighbor)
104
+ if (neighborOutstanding && query.hasSectionObject(neighbor)) {
105
+ waitingOnNeighbor = true
106
+ break
107
+ }
108
+ }
109
+ if (waitingOnNeighbor) break
110
+ }
111
+ }
112
+
113
+ if (waitingOnNeighbor) continue
114
+ const reason: PendingFlushReason = expired ? 'deadline' : 'group-complete'
115
+ for (const key of group) ready.push({ key, reason })
116
+ }
117
+
118
+ return ready
119
+ }
120
+
121
+ export function selectReadySectionUpdates(query: PendingFlushQuery): string[] {
122
+ return selectReadySectionFlushes(query).map(item => item.key)
123
+ }
@@ -0,0 +1,26 @@
1
+ //@ts-nocheck
2
+ /** Packed 2048-byte nibble section ↔ unpacked 4096-byte per-block values. */
3
+
4
+ export const PACKED_LIGHT_SECTION_BYTES = 2048
5
+ export const UNPACKED_LIGHT_SECTION_BYTES = 4096
6
+
7
+ export function packUnpackedLightSection(unpacked: Uint8Array): Uint8Array {
8
+ const packed = new Uint8Array(PACKED_LIGHT_SECTION_BYTES)
9
+ const n = Math.min(unpacked.length, UNPACKED_LIGHT_SECTION_BYTES)
10
+ for (let i = 0; i < n; i++) {
11
+ const v = unpacked[i]! & 0x0f
12
+ const byte = i >> 1
13
+ packed[byte] = i & 1 ? (packed[byte]! & 0x0f) | (v << 4) : (packed[byte]! & 0xf0) | v
14
+ }
15
+ return packed
16
+ }
17
+
18
+ export function unpackPackedLightSection(packed: Uint8Array): Uint8Array {
19
+ const unpacked = new Uint8Array(UNPACKED_LIGHT_SECTION_BYTES)
20
+ const n = Math.min(packed.length, PACKED_LIGHT_SECTION_BYTES)
21
+ for (let i = 0; i < n * 2 && i < UNPACKED_LIGHT_SECTION_BYTES; i++) {
22
+ const byte = packed[i >> 1] ?? 0
23
+ unpacked[i] = i & 1 ? byte >> 4 : byte & 0x0f
24
+ }
25
+ return unpacked
26
+ }
@@ -11,7 +11,7 @@
11
11
  const mcDataToNode = require('minecraft-data/lib/loader')
12
12
  const indexer = require('minecraft-data/lib/indexer.js')
13
13
 
14
- function createVersion (type, suppliedVersion, protocolVersions) {
14
+ function createVersion(type, suppliedVersion, protocolVersions) {
15
15
  const versions = indexer.buildIndexFromArray(protocolVersions, 'minecraftVersion') ?? {}
16
16
  // Allows comparisons against majorVersion even if `other` is not present in the versions list (e.g. 1.17.0 exists but not 1.17)
17
17
  for (const key in versions) {
@@ -23,12 +23,14 @@ function createVersion (type, suppliedVersion, protocolVersions) {
23
23
  // the supplied dataVersion is authoritative: the current version doesn't have to be present in the supplied list
24
24
  self.dataVersion = suppliedVersion.dataVersion ?? versions[suppliedVersion.minecraftVersion]?.dataVersion
25
25
  const v1 = self.dataVersion ?? 0
26
- const raise = other => { throw new RangeError(`Version '${other}' not found in protocolVersions supplied by the main thread for ${type}`) }
27
- self['>='] = other => versions[other] ? v1 >= versions[other].dataVersion : raise(other)
28
- self['>'] = other => versions[other] ? v1 > versions[other].dataVersion : raise(other)
29
- self['<'] = other => versions[other] ? v1 < versions[other].dataVersion : raise(other)
30
- self['<='] = other => versions[other] ? v1 <= versions[other].dataVersion : raise(other)
31
- self['=='] = other => versions[other] ? v1 === versions[other].dataVersion : raise(other)
26
+ const raise = other => {
27
+ throw new RangeError(`Version '${other}' not found in protocolVersions supplied by the main thread for ${type}`)
28
+ }
29
+ self['>='] = other => (versions[other] ? v1 >= versions[other].dataVersion : raise(other))
30
+ self['>'] = other => (versions[other] ? v1 > versions[other].dataVersion : raise(other))
31
+ self['<'] = other => (versions[other] ? v1 < versions[other].dataVersion : raise(other))
32
+ self['<='] = other => (versions[other] ? v1 <= versions[other].dataVersion : raise(other))
33
+ self['=='] = other => (versions[other] ? v1 === versions[other].dataVersion : raise(other))
32
34
  self.type = type
33
35
  self.majorVersion = suppliedVersion.majorVersion
34
36
  return self
@@ -44,7 +44,12 @@ describe('buildElementRotation', () => {
44
44
 
45
45
  it('treats a rotation without any angle as no rotation', () => {
46
46
  const { localMatrix, localShift } = buildElementRotation({ origin })
47
- localMatrix.forEach((row, i) => close(row, [0, 1, 2].map(j => (i === j ? 1 : 0))))
47
+ localMatrix.forEach((row, i) =>
48
+ close(
49
+ row,
50
+ [0, 1, 2].map(j => (i === j ? 1 : 0))
51
+ )
52
+ )
48
53
  close(localShift, [0, 0, 0])
49
54
  })
50
55
  })
@@ -87,8 +87,53 @@ export type MesherGeometryOutput = {
87
87
  visibilitySet?: number
88
88
  }
89
89
 
90
+ export type MeshGeometryMode = 'owner' | 'legacyBootstrap'
91
+
92
+ export type MesherGeometryVersionFields = {
93
+ sessionEpoch?: number
94
+ columnIncarnation?: number
95
+ requestId?: number
96
+ topologyRevision?: number
97
+ lightPublicationVersion?: number
98
+ worldGeneration?: number
99
+ neighborTopologyRevisions?: Array<{ key: string; topologyRevision: number }>
100
+ meshMode?: MeshGeometryMode
101
+ clientLightRequestId?: number
102
+ clientLightEditSeq?: number
103
+ clientLightSessionEpoch?: number
104
+ }
105
+
106
+ export type MesherWorkerDirtyMessage = {
107
+ type: 'dirty'
108
+ x: number
109
+ y: number
110
+ z: number
111
+ value?: boolean
112
+ config?: MesherConfig
113
+ } & MesherGeometryVersionFields
114
+
115
+ export type MesherWorkerOwnerPublicationMessage = {
116
+ type: 'applyOwnerLightPublication'
117
+ worldGeneration: number
118
+ publicationVersion: number
119
+ sessionEpoch?: number
120
+ sections: Array<{ sx: number; sy: number; sz: number; blockLight: Uint8Array; skyLight?: Uint8Array }>
121
+ }
122
+
123
+ export type MesherWorkerRevertOwnerMessage = {
124
+ type: 'revertOwnerLightToIncoming'
125
+ sessionEpoch: number
126
+ worldGeneration: number
127
+ }
128
+
129
+ export type MesherWorkerOwnerFenceMessage = {
130
+ type: 'setOwnerAcceptFence'
131
+ sessionEpoch: number
132
+ rejectOwnerPublications: boolean
133
+ }
134
+
90
135
  export interface MesherMainEvents {
91
- geometry: { type: 'geometry'; key: string; geometry: MesherGeometryOutput; workerIndex: number }
136
+ geometry: { type: 'geometry'; key: string; geometry: MesherGeometryOutput; workerIndex: number } & MesherGeometryVersionFields
92
137
  sectionFinished: {
93
138
  type: 'sectionFinished'
94
139
  key: string
@@ -108,6 +153,8 @@ export interface MesherMainEvents {
108
153
  // Per-event counts for the column-mode conversion cache.
109
154
  preCacheHits?: number
110
155
  preCacheMisses?: number
156
+ preWasmParseHits?: number
157
+ preWasmParseMisses?: number
111
158
  chunkCount?: number
112
159
  worldColumns3x3?: number
113
160
  parsedCache3x3?: number
@@ -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 { isClientLightTraceEnabled, recordClientLightTraceGpuSample } from '../lighting/debug/clientLightTrace'
38
39
  import { SectionOcclusionCull, hsvToRgb } from './occlusion/sectionOcclusionCull'
39
40
 
40
41
  export interface ChunkMeshPool {
@@ -285,6 +286,15 @@ export class ChunkMeshManager {
285
286
  if (this.globalLegacyShaderMaterial) setLegacySkyLevel(this.globalLegacyShaderMaterial, value)
286
287
  if (this.globalLegacyBlendShaderMaterial) setLegacySkyLevel(this.globalLegacyBlendShaderMaterial, value)
287
288
  this.blockEntityLightRegistry.setSkyLevel(value)
289
+ this.worldRenderer.entityLightController?.setSkyLevel(value)
290
+ }
291
+
292
+ getSkyLevel(): number {
293
+ return this.blockEntityLightRegistry.getSkyLevel()
294
+ }
295
+
296
+ getLightmapParams(): BlockLightmapParams {
297
+ return this.blockEntityLightRegistry.getLightmapParams()
288
298
  }
289
299
 
290
300
  setShadingTheme(theme: 'vanilla' | 'high-contrast', cardinalLight: string): void {
@@ -300,6 +310,7 @@ export class ChunkMeshManager {
300
310
  if (this.globalLegacyShaderMaterial) setLegacyLightmapParams(this.globalLegacyShaderMaterial, params)
301
311
  if (this.globalLegacyBlendShaderMaterial) setLegacyLightmapParams(this.globalLegacyBlendShaderMaterial, params)
302
312
  this.blockEntityLightRegistry.setLightmapParams(params)
313
+ this.worldRenderer.entityLightController?.setLightmapParams(params)
303
314
  }
304
315
 
305
316
  private getLegacyShaderMaterial(): THREE.ShaderMaterial {
@@ -591,6 +602,21 @@ export class ChunkMeshManager {
591
602
  gb.setVisibleSpans(spans)
592
603
  }
593
604
 
605
+ if (isClientLightTraceEnabled()) {
606
+ const cubeFaces = gb?.getVisibleFaceCount() ?? 0
607
+ const legacyQuads = (opaqueBuf?.getVisibleQuadCount() ?? 0) + (blendBuf?.getVisibleQuadCount() ?? 0)
608
+ const drawableFaces = cubeFaces + legacyQuads
609
+ recordClientLightTraceGpuSample('gpuDrawn', drawableFaces, () => ({
610
+ phase: 'gpuDrawn',
611
+ drawableFaces,
612
+ pendingReplace: !!(gb?.hasPendingReplace() || opaqueBuf?.hasPendingReplace() || blendBuf?.hasPendingReplace()),
613
+ pendingMove: !!(gb?.getPendingMove() || opaqueBuf?.getPendingMove() || blendBuf?.getPendingMove()),
614
+ unuploadedRanges:
615
+ (gb?.getPendingDirtyRanges().length ?? 0) + (opaqueBuf?.getPendingDirtyRanges().length ?? 0) + (blendBuf?.getPendingDirtyRanges().length ?? 0),
616
+ cullReason: smartCull ? 'frustum+occlusion' : 'frustum'
617
+ }))
618
+ }
619
+
594
620
  this.lastBufferStateKey = this.bufferStateKey()
595
621
  this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ, smartCull, occlusionVisible)
596
622
  }
@@ -9,7 +9,6 @@ import { inferModelType, loadCapeToCanvas, loadEarsToCanvasFromSkin } from 'skin
9
9
  import { flat, fromFormattedString } from '@xmcl/text-component'
10
10
  import mojangson from 'mojangson'
11
11
  import { snakeCase } from 'change-case'
12
- import { Item } from 'prismarine-item'
13
12
  import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity'
14
13
  import { Team } from 'mineflayer'
15
14
  import PrismarineChatLoader from 'prismarine-chat'
@@ -34,6 +33,7 @@ import { releaseVehiclePassengerPosition } from './entity/vehiclePassengerRender
34
33
  import { updateVehiclePassengerPositions as applyVehiclePassengerPositions } from './entity/vehiclePassengerUpdate'
35
34
  import { applyNetworkHeadPitch, storeNetworkHeadPitch, storeNetworkHeadYaw } from './entity/networkHeadPitchRendering'
36
35
  import { processRemoteBoatPassengerRotations, type RemoteBoatPassengerEntity } from './entity/remoteBoatPassengerRotation'
36
+ import { isClientLightTraceEnabled, recordClientLightTrace } from '../lighting/debug/clientLightTrace'
37
37
  import {
38
38
  ENTITY_TWEEN_DURATION_MS,
39
39
  applyLocalHorseCameraYawLock,
@@ -59,16 +59,11 @@ import {
59
59
  type HorseHeadRig
60
60
  } from './entity/horseHeadAnimation'
61
61
  import { WalkingGeneralSwing } from './entity/animations'
62
- import {
63
- LEG_ANIMATION_USER_DATA_KEY,
64
- LEG_RIG_USER_DATA_KEY,
65
- applyLegRig,
66
- createLegRig,
67
- type LegRig
68
- } from './entity/legWalkAnimation'
62
+ import { LEG_ANIMATION_USER_DATA_KEY, LEG_RIG_USER_DATA_KEY, applyLegRig, createLegRig, type LegRig } from './entity/legWalkAnimation'
69
63
  import { disposeObject, loadNearestFilterTexture, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
70
64
  import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
71
65
  import { WorldRendererThree } from './worldRendererThree'
66
+ import type { EntityLightMeta } from '../lighting/entity/entityLightController'
72
67
  import { IndexedData } from 'minecraft-data'
73
68
  import { ItemSpecificContextProperties } from '../playerState/types'
74
69
 
@@ -387,6 +382,8 @@ function getEntityMesh(
387
382
  return cube
388
383
  }
389
384
 
385
+ type EquipmentItem = import('prismarine-entity').Entity['equipment'][number]
386
+
390
387
  export type SceneEntity = THREE.Object3D & {
391
388
  playerObject?: PlayerObjectType
392
389
  username?: string
@@ -476,9 +473,12 @@ export class Entities {
476
473
  }
477
474
 
478
475
  this.updateEntityEquipment(this.playerEntity, playerData)
476
+ this.registerEntityLight(this.playerEntity, this.playerEntity.originalEntity)
477
+ this.invalidateEntityLight(this.playerEntity)
479
478
  }
480
479
 
481
480
  clear() {
481
+ this.worldRenderer.entityLightController?.clear()
482
482
  for (const mesh of Object.values(this.entities)) {
483
483
  this.worldRenderer.sceneOrigin.removeAndUntrack(mesh)
484
484
  disposeObject(mesh)
@@ -658,6 +658,7 @@ export class Entities {
658
658
  this.applyRemoteBoatPassengerRotations()
659
659
  this.applyLocalThirdPersonPlayerRotation()
660
660
  this.updateBoatPaddleAnimations(dt)
661
+ this.applyEntityLights()
661
662
  }
662
663
 
663
664
  private applyRemoteBoatPassengerRotations() {
@@ -1050,6 +1051,7 @@ export class Entities {
1050
1051
  if (!playerObject || !playerObject.playerObject?.animation) return
1051
1052
  const anim = playerObject.playerObject.animation as any
1052
1053
  if (anim.swingArm) {
1054
+ if (isClientLightTraceEnabled()) recordClientLightTrace({ phase: 'inputHand' })
1053
1055
  anim.swingArm()
1054
1056
  }
1055
1057
  }
@@ -1062,6 +1064,7 @@ export class Entities {
1062
1064
  if (!anim) return
1063
1065
 
1064
1066
  if (animation === 'oneSwing') {
1067
+ if (isClientLightTraceEnabled()) recordClientLightTrace({ phase: 'inputHand' })
1065
1068
  anim.swingArm()
1066
1069
  return
1067
1070
  }
@@ -1212,6 +1215,7 @@ export class Entities {
1212
1215
  if (c['additionalCleanup']) c['additionalCleanup']()
1213
1216
  })
1214
1217
  this.onRemoveEntity(entity)
1218
+ this.worldRenderer.entityLightController?.unregister(e)
1215
1219
  this.worldRenderer.sceneOrigin.removeAndUntrack(e)
1216
1220
  disposeObject(e)
1217
1221
  // todo dispose textures as well ?
@@ -1541,6 +1545,12 @@ export class Entities {
1541
1545
  this.updateNameTagVisibility(e)
1542
1546
 
1543
1547
  this.updateEntityPosition(entity, justAdded, overrides)
1548
+ if (justAdded) {
1549
+ this.registerEntityLight(e, entity)
1550
+ } else {
1551
+ this.worldRenderer.entityLightController?.setMeta(e, this.entityLightMeta(entity))
1552
+ this.invalidateEntityLight(e)
1553
+ }
1544
1554
  }
1545
1555
 
1546
1556
  applyEntityRenderHints(e: SceneEntity, entity: SceneEntity['originalEntity']) {
@@ -1784,7 +1794,7 @@ export class Entities {
1784
1794
  return texture
1785
1795
  }
1786
1796
 
1787
- addItemModel(entityMesh: SceneEntity, hand: 'left' | 'right', item: Item, isPlayer = false) {
1797
+ addItemModel(entityMesh: SceneEntity, hand: 'left' | 'right', item: EquipmentItem, isPlayer = false) {
1788
1798
  const bedrockParentName = `bone_${hand}item`
1789
1799
  const itemName = `custom_item_${hand}`
1790
1800
 
@@ -1837,8 +1847,52 @@ export class Entities {
1837
1847
  }
1838
1848
  }
1839
1849
 
1850
+ onEntityMeshReady(mesh: THREE.Object3D) {
1851
+ this.worldRenderer.entityLightController?.invalidateFromDescendant(mesh)
1852
+ }
1853
+
1854
+ private entityLightSharedMaterials() {
1855
+ return new Set<THREE.Material>([this.worldRenderer.material])
1856
+ }
1857
+
1858
+ private entityLightMeta(entity: SceneEntity['originalEntity']): EntityLightMeta {
1859
+ const localPlayerId = this.playerEntity?.originalEntity.id
1860
+ const useLocalEyeHeight = entity.name === 'player' && localPlayerId !== undefined && entity.id === localPlayerId
1861
+ return {
1862
+ name: entity.name,
1863
+ height: entity.height,
1864
+ eyeHeight: useLocalEyeHeight ? this.worldRenderer.playerStateReactive.eyeHeight : undefined
1865
+ }
1866
+ }
1867
+
1868
+ private registerEntityLight(root: SceneEntity, entity: SceneEntity['originalEntity']) {
1869
+ this.worldRenderer.entityLightController?.register(root, this.entityLightMeta(entity), this.entityLightSharedMaterials())
1870
+ }
1871
+
1872
+ private invalidateEntityLight(root: THREE.Object3D) {
1873
+ this.worldRenderer.entityLightController?.invalidateMaterials(root)
1874
+ }
1875
+
1876
+ private applyEntityLights() {
1877
+ const controller = this.worldRenderer.entityLightController
1878
+ if (!controller) return
1879
+ const shared = this.entityLightSharedMaterials()
1880
+ const apply = (root: SceneEntity | null) => {
1881
+ if (!root) return
1882
+ if (root === this.playerEntity) {
1883
+ controller.setMeta(root, this.entityLightMeta(root.originalEntity))
1884
+ }
1885
+ const worldPos = this.worldRenderer.sceneOrigin.getWorldPosition(root)
1886
+ if (!worldPos) return
1887
+ controller.update(root, worldPos, shared)
1888
+ }
1889
+ for (const entity of Object.values(this.entities)) apply(entity)
1890
+ apply(this.playerEntity)
1891
+ }
1892
+
1840
1893
  handleDamageEvent(entityId, damageAmount) {
1841
- const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh')
1894
+ const entity = this.entities[entityId]
1895
+ const entityMesh = entity?.children.find(c => c.name === 'mesh')
1842
1896
  if (entityMesh) {
1843
1897
  entityMesh.traverse(child => {
1844
1898
  if (child instanceof THREE.Mesh && child.material.clone) {
@@ -1851,6 +1905,18 @@ export class Entities {
1851
1905
  }
1852
1906
  })
1853
1907
  }
1908
+ if (entity) {
1909
+ this.worldRenderer.entityLightController?.markFlash(entity)
1910
+ this.invalidateEntityLight(entity)
1911
+ setTimeout(() => {
1912
+ if (!this.entities[entityId]) return
1913
+ this.worldRenderer.entityLightController?.endFlash(entity)
1914
+ const worldPos = this.worldRenderer.sceneOrigin.getWorldPosition(entity)
1915
+ if (worldPos) {
1916
+ this.worldRenderer.entityLightController?.update(entity, worldPos, this.entityLightSharedMaterials())
1917
+ }
1918
+ }, 500)
1919
+ }
1854
1920
  }
1855
1921
 
1856
1922
  raycastSceneDebug() {
@@ -1944,7 +2010,7 @@ function getSpecificEntityMetadata<T extends keyof EntityMetadataVersions>(name:
1944
2010
  return getGeneralEntitiesMetadata(entity, mcData) as any
1945
2011
  }
1946
2012
 
1947
- function addArmorModel(worldRenderer: WorldRendererThree, entityMesh: THREE.Object3D, slotType: string, item: Item, layer = 1, overlay = false) {
2013
+ function addArmorModel(worldRenderer: WorldRendererThree, entityMesh: THREE.Object3D, slotType: string, item: EquipmentItem, layer = 1, overlay = false) {
1948
2014
  if (!item) {
1949
2015
  removeArmorModel(entityMesh, slotType)
1950
2016
  return
@@ -536,6 +536,7 @@ export class EntityMesh {
536
536
  controller.playAnimation(gltf.animations[0].name, loop)
537
537
  }
538
538
  }
539
+ worldRenderer?.entities.onEntityMeshReady(this.mesh)
539
540
  })
540
541
  .catch(err => {
541
542
  console.error('Failed to load GLTF model:', err)
@@ -53,7 +53,12 @@ test('calculateLegSwing follows vanilla gait and is zero at rest', () => {
53
53
  })
54
54
 
55
55
  test('assignLegPhases alternates bipeds and pairs quadruped diagonals', () => {
56
- expect(assignLegPhases([{ x: -1.9, z: 0 }, { x: 1.9, z: 0 }])).toEqual([Math.PI, 0])
56
+ expect(
57
+ assignLegPhases([
58
+ { x: -1.9, z: 0 },
59
+ { x: 1.9, z: 0 }
60
+ ])
61
+ ).toEqual([Math.PI, 0])
57
62
  // leg0..leg3 as hind-right, hind-left, front-right, front-left
58
63
  const quad = assignLegPhases([
59
64
  { x: -4, z: 7 },
@@ -82,7 +87,11 @@ test('bedrock legs rotate about their own pivot even when parented to an offset
82
87
  const pivot = new THREE.Vector3(...(leg.pivot as [number, number, number]))
83
88
  const expectedAngle = calculateLegSwing(0.4 * Math.PI, 1, rig.legs[i].phase)
84
89
  const foot = new THREE.Vector3(pivot.x, pivot.y - 12, pivot.z)
85
- const expected = foot.clone().sub(pivot).applyAxisAngle(new THREE.Vector3(1, 0, 0), expectedAngle).add(pivot)
90
+ const expected = foot
91
+ .clone()
92
+ .sub(pivot)
93
+ .applyAxisAngle(new THREE.Vector3(1, 0, 0), expectedAngle)
94
+ .add(pivot)
86
95
  mesh.getVertexPosition(i, vertex).multiplyScalar(16)
87
96
  expect(close(vertex, expected), `leg ${i}`).toBe(true)
88
97
  // the pivot itself never moves
@@ -9,15 +9,7 @@ export const LEG_SWING_FREQUENCY = 0.6662
9
9
  export const LEG_SWING_AMPLITUDE = 1.4
10
10
 
11
11
  /** Entities whose legs are not driven by ground walking (fly, hop, swim or use bespoke animations). */
12
- const LEG_ANIMATION_EXCLUDED = new Set([
13
- 'armor_stand',
14
- 'axolotl',
15
- 'ender_dragon',
16
- 'frog',
17
- 'parrot',
18
- 'rabbit',
19
- 'turtle'
20
- ])
12
+ const LEG_ANIMATION_EXCLUDED = new Set(['armor_stand', 'axolotl', 'ender_dragon', 'frog', 'parrot', 'rabbit', 'turtle'])
21
13
 
22
14
  const LEG_NAME_PATTERN = /leg/i
23
15