minecraft-renderer 0.1.106 → 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 (98) 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 +105 -76
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +537 -508
  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 +8 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/skyLight.ts +17 -0
  19. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  20. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  21. package/src/lib/worldrendererCommon.ts +340 -67
  22. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  23. package/src/lighting/cache/rendererLightCache.ts +248 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  25. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  26. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  27. package/src/lighting/debug/clientLightProbe.ts +20 -0
  28. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  29. package/src/lighting/debug/clientLightTrace.ts +335 -0
  30. package/src/lighting/entity/entityLightController.test.ts +216 -0
  31. package/src/lighting/entity/entityLightController.ts +322 -0
  32. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  33. package/src/lighting/owner/clientLightOwner.ts +523 -0
  34. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  35. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  36. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  37. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  38. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  39. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  40. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  41. package/src/lighting/tables/lightTables1171.ts +59 -0
  42. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  43. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  44. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  45. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  46. package/src/mesher-shared/lightNibblePack.ts +26 -0
  47. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  48. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  49. package/src/mesher-shared/shared.ts +48 -1
  50. package/src/three/chunkMeshManager.ts +26 -0
  51. package/src/three/entities.ts +97 -8
  52. package/src/three/entity/EntityMesh.ts +1 -0
  53. package/src/three/entity/legWalkAnimation.test.ts +154 -0
  54. package/src/three/entity/legWalkAnimation.ts +193 -0
  55. package/src/three/globalBlockBuffer.ts +94 -2
  56. package/src/three/globalLegacyBuffer.ts +94 -2
  57. package/src/three/graphicsBackendBase.ts +2 -9
  58. package/src/three/modules/clouds.ts +191 -0
  59. package/src/three/modules/index.ts +4 -0
  60. package/src/three/modules/sunMoon.ts +151 -0
  61. package/src/three/skyboxRenderer.ts +15 -8
  62. package/src/three/tests/clouds.test.ts +59 -0
  63. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  64. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  65. package/src/three/tests/waterFog.test.ts +25 -0
  66. package/src/three/waterFog.ts +28 -0
  67. package/src/three/worldRendererThree.ts +84 -39
  68. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  69. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  70. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  71. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  72. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  73. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  74. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  75. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  76. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  77. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  78. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  79. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  80. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  81. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  82. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  83. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  84. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  85. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  86. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  87. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  88. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  89. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  90. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  91. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  92. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  93. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  94. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  95. package/src/worldView/types.ts +3 -0
  96. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  97. package/src/worldView/worldView.spiral.test.ts +1 -1
  98. package/src/worldView/worldView.ts +49 -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,9 +59,11 @@ import {
59
59
  type HorseHeadRig
60
60
  } from './entity/horseHeadAnimation'
61
61
  import { WalkingGeneralSwing } from './entity/animations'
62
+ import { LEG_ANIMATION_USER_DATA_KEY, LEG_RIG_USER_DATA_KEY, applyLegRig, createLegRig, type LegRig } from './entity/legWalkAnimation'
62
63
  import { disposeObject, loadNearestFilterTexture, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
63
64
  import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
64
65
  import { WorldRendererThree } from './worldRendererThree'
66
+ import type { EntityLightMeta } from '../lighting/entity/entityLightController'
65
67
  import { IndexedData } from 'minecraft-data'
66
68
  import { ItemSpecificContextProperties } from '../playerState/types'
67
69
 
@@ -380,6 +382,8 @@ function getEntityMesh(
380
382
  return cube
381
383
  }
382
384
 
385
+ type EquipmentItem = import('prismarine-entity').Entity['equipment'][number]
386
+
383
387
  export type SceneEntity = THREE.Object3D & {
384
388
  playerObject?: PlayerObjectType
385
389
  username?: string
@@ -469,9 +473,12 @@ export class Entities {
469
473
  }
470
474
 
471
475
  this.updateEntityEquipment(this.playerEntity, playerData)
476
+ this.registerEntityLight(this.playerEntity, this.playerEntity.originalEntity)
477
+ this.invalidateEntityLight(this.playerEntity)
472
478
  }
473
479
 
474
480
  clear() {
481
+ this.worldRenderer.entityLightController?.clear()
475
482
  for (const mesh of Object.values(this.entities)) {
476
483
  this.worldRenderer.sceneOrigin.removeAndUntrack(mesh)
477
484
  disposeObject(mesh)
@@ -592,18 +599,27 @@ export class Entities {
592
599
 
593
600
  const horseHeadRig = entity.userData[HORSE_HEAD_RIG_USER_DATA_KEY] as HorseHeadRig | undefined
594
601
  const horseAnimation = entity.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] as HorseHeadAnimationState | undefined
602
+ let limbAnimation: ReturnType<typeof updateHorseHeadAnimationFrame> | undefined
595
603
  if (horseHeadRig && horseAnimation) {
596
- const animation = updateHorseHeadAnimationFrame(horseAnimation, dt)
604
+ limbAnimation = updateHorseHeadAnimationFrame(horseAnimation, dt)
597
605
  const pose = calculateHorseHeadPose({
598
606
  entityPitch: entity.userData._horseHeadPitch ?? 0,
599
607
  headYaw: entity.userData._horseHeadYaw ?? entity.userData._horseBodyYaw ?? 0,
600
608
  bodyYaw: entity.userData._horseBodyYaw ?? 0,
601
- limbSwing: animation.limbSwing,
602
- limbSwingAmount: animation.limbSwingAmount
609
+ limbSwing: limbAnimation.limbSwing,
610
+ limbSwingAmount: limbAnimation.limbSwingAmount
603
611
  })
604
612
  applyHorseHeadPose(horseHeadRig, pose)
605
613
  }
606
614
 
615
+ const legRig = entity.userData[LEG_RIG_USER_DATA_KEY] as LegRig | undefined
616
+ const legAnimation = entity.userData[LEG_ANIMATION_USER_DATA_KEY] as HorseHeadAnimationState | undefined
617
+ if (legRig && legAnimation) {
618
+ // Horses share one state between head and legs, it must only be advanced once per frame.
619
+ limbAnimation ??= updateHorseHeadAnimationFrame(legAnimation, dt)
620
+ applyLegRig(legRig, limbAnimation.limbSwing, limbAnimation.limbSwingAmount)
621
+ }
622
+
607
623
  if (!isPlayerEntity && playerObject) {
608
624
  applyNetworkHeadPitch(playerObject, entity.userData)
609
625
  }
@@ -642,6 +658,7 @@ export class Entities {
642
658
  this.applyRemoteBoatPassengerRotations()
643
659
  this.applyLocalThirdPersonPlayerRotation()
644
660
  this.updateBoatPaddleAnimations(dt)
661
+ this.applyEntityLights()
645
662
  }
646
663
 
647
664
  private applyRemoteBoatPassengerRotations() {
@@ -1034,6 +1051,7 @@ export class Entities {
1034
1051
  if (!playerObject || !playerObject.playerObject?.animation) return
1035
1052
  const anim = playerObject.playerObject.animation as any
1036
1053
  if (anim.swingArm) {
1054
+ if (isClientLightTraceEnabled()) recordClientLightTrace({ phase: 'inputHand' })
1037
1055
  anim.swingArm()
1038
1056
  }
1039
1057
  }
@@ -1046,6 +1064,7 @@ export class Entities {
1046
1064
  if (!anim) return
1047
1065
 
1048
1066
  if (animation === 'oneSwing') {
1067
+ if (isClientLightTraceEnabled()) recordClientLightTrace({ phase: 'inputHand' })
1049
1068
  anim.swingArm()
1050
1069
  return
1051
1070
  }
@@ -1187,6 +1206,8 @@ export class Entities {
1187
1206
  e.userData._rotTween?.stop()
1188
1207
  e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] = undefined
1189
1208
  e.userData[HORSE_HEAD_RIG_USER_DATA_KEY] = undefined
1209
+ e.userData[LEG_ANIMATION_USER_DATA_KEY] = undefined
1210
+ e.userData[LEG_RIG_USER_DATA_KEY] = undefined
1190
1211
  const boatMesh = e.children.find(c => c.name === 'mesh')
1191
1212
  if (boatMesh) disposeBoatWaterPatch(boatMesh)
1192
1213
  if (e.additionalCleanup) e.additionalCleanup()
@@ -1194,6 +1215,7 @@ export class Entities {
1194
1215
  if (c['additionalCleanup']) c['additionalCleanup']()
1195
1216
  })
1196
1217
  this.onRemoveEntity(entity)
1218
+ this.worldRenderer.entityLightController?.unregister(e)
1197
1219
  this.worldRenderer.sceneOrigin.removeAndUntrack(e)
1198
1220
  disposeObject(e)
1199
1221
  // todo dispose textures as well ?
@@ -1320,6 +1342,11 @@ export class Entities {
1320
1342
  e.userData[HORSE_HEAD_RIG_USER_DATA_KEY] = horseHeadRig
1321
1343
  e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] = createHorseHeadAnimationState()
1322
1344
  }
1345
+ const legRig = isPlayerModel ? undefined : createLegRig(mesh, entity.name)
1346
+ if (legRig) {
1347
+ e.userData[LEG_RIG_USER_DATA_KEY] = legRig
1348
+ e.userData[LEG_ANIMATION_USER_DATA_KEY] = e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] ?? createHorseHeadAnimationState()
1349
+ }
1323
1350
  } else {
1324
1351
  mesh = e.children.find(c => c.name === 'mesh')
1325
1352
  if (entity.renderHints) {
@@ -1518,6 +1545,12 @@ export class Entities {
1518
1545
  this.updateNameTagVisibility(e)
1519
1546
 
1520
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
+ }
1521
1554
  }
1522
1555
 
1523
1556
  applyEntityRenderHints(e: SceneEntity, entity: SceneEntity['originalEntity']) {
@@ -1596,7 +1629,7 @@ export class Entities {
1596
1629
  })
1597
1630
  .start()
1598
1631
  }
1599
- const horseAnimation = e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] as HorseHeadAnimationState | undefined
1632
+ const horseAnimation = (e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] ?? e.userData[LEG_ANIMATION_USER_DATA_KEY]) as HorseHeadAnimationState | undefined
1600
1633
  if (!headRotationOnly && horseAnimation && Number.isFinite(entity.position.x) && Number.isFinite(entity.position.z)) {
1601
1634
  setHorseHeadAnimationPosition(horseAnimation, entity.position)
1602
1635
  }
@@ -1761,7 +1794,7 @@ export class Entities {
1761
1794
  return texture
1762
1795
  }
1763
1796
 
1764
- addItemModel(entityMesh: SceneEntity, hand: 'left' | 'right', item: Item, isPlayer = false) {
1797
+ addItemModel(entityMesh: SceneEntity, hand: 'left' | 'right', item: EquipmentItem, isPlayer = false) {
1765
1798
  const bedrockParentName = `bone_${hand}item`
1766
1799
  const itemName = `custom_item_${hand}`
1767
1800
 
@@ -1814,8 +1847,52 @@ export class Entities {
1814
1847
  }
1815
1848
  }
1816
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
+
1817
1893
  handleDamageEvent(entityId, damageAmount) {
1818
- 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')
1819
1896
  if (entityMesh) {
1820
1897
  entityMesh.traverse(child => {
1821
1898
  if (child instanceof THREE.Mesh && child.material.clone) {
@@ -1828,6 +1905,18 @@ export class Entities {
1828
1905
  }
1829
1906
  })
1830
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
+ }
1831
1920
  }
1832
1921
 
1833
1922
  raycastSceneDebug() {
@@ -1921,7 +2010,7 @@ function getSpecificEntityMetadata<T extends keyof EntityMetadataVersions>(name:
1921
2010
  return getGeneralEntitiesMetadata(entity, mcData) as any
1922
2011
  }
1923
2012
 
1924
- 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) {
1925
2014
  if (!item) {
1926
2015
  removeArmorModel(entityMesh, slotType)
1927
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)