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,216 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import * as THREE from 'three'
4
+ import { blockEntityBrightness, DEFAULT_LIGHTMAP_PARAMS } from '../../lib/blockEntityLighting'
5
+ import { EntityLightController, entityProbeBlock, resolveEntityLightOverride } from './entityLightController'
6
+
7
+ const nightSky = 4 / 15
8
+
9
+ function makeController(
10
+ getLight: (x: number, y: number, z: number) => { block: number; sky: number } = () => ({ block: 0, sky: 1 }),
11
+ extras: Partial<ConstructorParameters<typeof EntityLightController>[0]> = {}
12
+ ) {
13
+ let skyLevel = 1
14
+ let lightingEnabled = true
15
+ let globalRevision = 1
16
+ const columnRevisions = new Map<string, number>([['0,0', 1]])
17
+ const controller = new EntityLightController({
18
+ getLight,
19
+ getSkyLevel: () => skyLevel,
20
+ getLightmapParams: () => DEFAULT_LIGHTMAP_PARAMS,
21
+ getColumnRevision: (cx, cz) => columnRevisions.get(`${cx},${cz}`),
22
+ getGlobalRevision: () => globalRevision,
23
+ lightingEnabled: () => lightingEnabled,
24
+ ...extras
25
+ })
26
+ return {
27
+ controller,
28
+ setSkyLevel: (value: number) => {
29
+ skyLevel = value
30
+ controller.setSkyLevel(value)
31
+ },
32
+ setLightingEnabled: (value: boolean) => {
33
+ lightingEnabled = value
34
+ },
35
+ bumpColumn: () => {
36
+ columnRevisions.set('0,0', (columnRevisions.get('0,0') ?? 0) + 1)
37
+ },
38
+ bumpFarColumn: () => {
39
+ columnRevisions.set('16,0', (columnRevisions.get('16,0') ?? 0) + 1)
40
+ }
41
+ }
42
+ }
43
+
44
+ describe('entityProbeBlock', () => {
45
+ it('samples the eye block, not the block above the AABB', () => {
46
+ expect(entityProbeBlock({ x: 8.2, y: 64.3, z: 8.7 }, 1.62)).toEqual({ x: 8, y: 65, z: 8 })
47
+ expect(entityProbeBlock({ x: 8.2, y: 64.3, z: 8.7 }, 1.8)).toEqual({ x: 8, y: 66, z: 8 })
48
+ })
49
+ })
50
+
51
+ describe('resolveEntityLightOverride', () => {
52
+ it('fullbrights only reliably identified glow item frames', () => {
53
+ expect(resolveEntityLightOverride('glow_item_frame')).toBe('fullbright')
54
+ expect(resolveEntityLightOverride('item_frame')).toBe('normal')
55
+ expect(resolveEntityLightOverride('blaze')).toBe('normal')
56
+ })
57
+ })
58
+
59
+ describe('EntityLightController', () => {
60
+ it('multiplies immutable base tint and is idempotent', () => {
61
+ const { controller } = makeController(() => ({ block: 0, sky: 1 }))
62
+ const material = new THREE.MeshLambertMaterial({ color: 0xb5_6d_51 })
63
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(), material)
64
+ const root = new THREE.Group()
65
+ root.add(mesh)
66
+ const base = material.color.clone()
67
+ controller.register(root, { eyeHeight: 1.62 })
68
+ controller.update(root, { x: 8, y: 64, z: 8 })
69
+ const brightness = blockEntityBrightness(0, 1, 1)
70
+ expect(material.color.r).toBeCloseTo(base.r * brightness, 5)
71
+ expect(material.color.g).toBeCloseTo(base.g * brightness, 5)
72
+ expect(material.color.b).toBeCloseTo(base.b * brightness, 5)
73
+ controller.update(root, { x: 8, y: 64, z: 8 })
74
+ expect(material.color.r).toBeCloseTo(base.r * brightness, 5)
75
+ })
76
+
77
+ it('clones a shared material and leaves the original fullbright', () => {
78
+ const shared = new THREE.MeshBasicMaterial({ color: 0xff_00_00 })
79
+ const { controller } = makeController(() => ({ block: 0, sky: 0 }))
80
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(), shared)
81
+ const root = new THREE.Group()
82
+ root.add(mesh)
83
+ controller.register(root, { eyeHeight: 0 }, new Set([shared]))
84
+ controller.update(root, { x: 1, y: 64, z: 1 })
85
+ expect(shared.color.getHex()).toBe(0xff_00_00)
86
+ expect(mesh.material).not.toBe(shared)
87
+ const brightness = blockEntityBrightness(0, 0, 1)
88
+ expect((mesh.material as THREE.MeshBasicMaterial).color.r).toBeCloseTo(1 * brightness, 5)
89
+ })
90
+
91
+ it('keeps two roots independent and skips debug helpers', () => {
92
+ let sample = { block: 1, sky: 1 }
93
+ const { controller } = makeController(() => sample)
94
+ const brightMat = new THREE.MeshBasicMaterial({ color: 0xff_ff_ff })
95
+ const darkMat = new THREE.MeshBasicMaterial({ color: 0xff_ff_ff })
96
+ const debugMat = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 })
97
+ const bright = new THREE.Group()
98
+ bright.add(new THREE.Mesh(new THREE.BoxGeometry(), brightMat))
99
+ const dark = new THREE.Group()
100
+ dark.add(new THREE.Mesh(new THREE.BoxGeometry(), darkMat))
101
+ const debug = new THREE.Mesh(new THREE.BoxGeometry(), debugMat)
102
+ debug.name = 'debug'
103
+ dark.add(debug)
104
+ controller.register(bright, { eyeHeight: 0 })
105
+ controller.register(dark, { eyeHeight: 0 })
106
+ controller.update(bright, { x: 1, y: 64, z: 1 })
107
+ sample = { block: 0, sky: 0 }
108
+ controller.update(dark, { x: 20, y: 64, z: 20 })
109
+ expect(brightMat.color.r).toBeCloseTo(1, 5)
110
+ expect(darkMat.color.r).toBeCloseTo(blockEntityBrightness(0, 0, 1), 5)
111
+ expect(debugMat.color.getHex()).toBe(0x00_ff_00)
112
+ })
113
+
114
+ it('refreshes on sky change and stays fullbright when lighting is disabled', () => {
115
+ const { controller, setSkyLevel, setLightingEnabled } = makeController(() => ({ block: 0, sky: 1 }))
116
+ const material = new THREE.MeshBasicMaterial({ color: 0xff_ff_ff })
117
+ const root = new THREE.Group()
118
+ root.add(new THREE.Mesh(new THREE.BoxGeometry(), material))
119
+ controller.register(root, { eyeHeight: 0 })
120
+ controller.update(root, { x: 8, y: 64, z: 8 })
121
+ setSkyLevel(nightSky)
122
+ expect(material.color.r).toBeCloseTo(blockEntityBrightness(0, 1, nightSky), 5)
123
+ setLightingEnabled(false)
124
+ controller.update(root, { x: 8, y: 64, z: 8 })
125
+ expect(material.color.r).toBeCloseTo(1, 5)
126
+ })
127
+
128
+ it('resamples a stationary root when the column revision changes', () => {
129
+ let sample = { block: 1, sky: 1 }
130
+ const { controller, bumpColumn } = makeController(() => sample)
131
+ const material = new THREE.MeshBasicMaterial({ color: 0xff_ff_ff })
132
+ const root = new THREE.Group()
133
+ root.add(new THREE.Mesh(new THREE.BoxGeometry(), material))
134
+ controller.register(root, { eyeHeight: 0 })
135
+ controller.update(root, { x: 8, y: 64, z: 8 })
136
+ expect(material.color.r).toBeCloseTo(1, 5)
137
+ sample = { block: 0, sky: 0 }
138
+ controller.update(root, { x: 8, y: 64, z: 8 })
139
+ expect(material.color.r).toBeCloseTo(1, 5)
140
+ bumpColumn()
141
+ controller.update(root, { x: 8, y: 64, z: 8 })
142
+ expect(material.color.r).toBeCloseTo(blockEntityBrightness(0, 0, 1), 5)
143
+ })
144
+
145
+ it('does not resample when only a distant column revision changes', () => {
146
+ let sample = { block: 1, sky: 1 }
147
+ const { controller, bumpFarColumn } = makeController(() => sample)
148
+ const material = new THREE.MeshBasicMaterial({ color: 0xff_ff_ff })
149
+ const root = new THREE.Group()
150
+ root.add(new THREE.Mesh(new THREE.BoxGeometry(), material))
151
+ controller.register(root, { eyeHeight: 0 })
152
+ controller.update(root, { x: 8, y: 64, z: 8 })
153
+ expect(material.color.r).toBeCloseTo(1, 5)
154
+ sample = { block: 0, sky: 0 }
155
+ bumpFarColumn()
156
+ controller.update(root, { x: 8, y: 64, z: 8 })
157
+ expect(material.color.r).toBeCloseTo(1, 5)
158
+ })
159
+
160
+ it('adopts an authored material retint after invalidation', () => {
161
+ const { controller } = makeController(() => ({ block: 0, sky: 0 }))
162
+ const material = new THREE.MeshBasicMaterial({ color: 0xb5_6d_51 })
163
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(), material)
164
+ const root = new THREE.Group()
165
+ root.add(mesh)
166
+ controller.register(root, { eyeHeight: 0 })
167
+ controller.update(root, { x: 8, y: 64, z: 8 })
168
+ const brightness = blockEntityBrightness(0, 0, 1)
169
+ const brown = new THREE.Color(0xb5_6d_51)
170
+ expect(material.color.r).toBeCloseTo(brown.r * brightness, 5)
171
+ material.color.setHex(0x00_ff_00)
172
+ controller.invalidateMaterials(root)
173
+ controller.update(root, { x: 8, y: 64, z: 8 })
174
+ expect(material.color.r).toBeCloseTo(0, 5)
175
+ expect(material.color.g).toBeCloseTo(brightness, 5)
176
+ expect(material.color.b).toBeCloseTo(0, 5)
177
+ })
178
+
179
+ it('keeps glow item frames fullbright and recovers after a damage flash', () => {
180
+ const { controller } = makeController(() => ({ block: 0, sky: 0 }))
181
+ const material = new THREE.MeshBasicMaterial({ color: 0xaa_aa_aa })
182
+ const baseR = material.color.r
183
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(), material)
184
+ const root = new THREE.Group()
185
+ root.add(mesh)
186
+ controller.register(root, { name: 'glow_item_frame', eyeHeight: 0 })
187
+ controller.update(root, { x: 8, y: 64, z: 8 })
188
+ expect(material.color.r).toBeCloseTo(baseR, 5)
189
+
190
+ const flashed = material.clone()
191
+ flashed.color.set(0xff_00_00)
192
+ mesh.material = flashed
193
+ controller.markFlash(root, 500)
194
+ controller.invalidateMaterials(root)
195
+ controller.update(root, { x: 8, y: 64, z: 8 })
196
+ expect(flashed.color.getHex()).toBe(0xff_00_00)
197
+ controller.endFlash(root)
198
+ controller.update(root, { x: 8, y: 64, z: 8 })
199
+ expect(flashed.color.r).toBeCloseTo(baseR, 5)
200
+ })
201
+
202
+ it('unregisters so later updates do not keep mutating materials', () => {
203
+ const { controller } = makeController(() => ({ block: 0, sky: 0 }))
204
+ const material = new THREE.MeshBasicMaterial({ color: 0xff_ff_ff })
205
+ const root = new THREE.Group()
206
+ root.add(new THREE.Mesh(new THREE.BoxGeometry(), material))
207
+ controller.register(root, { eyeHeight: 0 })
208
+ controller.update(root, { x: 8, y: 64, z: 8 })
209
+ const dim = material.color.r
210
+ controller.unregister(root)
211
+ material.color.setHex(0xff_ff_ff)
212
+ controller.update(root, { x: 8, y: 64, z: 8 })
213
+ expect(material.color.getHex()).toBe(0xff_ff_ff)
214
+ expect(dim).toBeLessThan(1)
215
+ })
216
+ })
@@ -0,0 +1,322 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import { blockEntityBrightness, DEFAULT_LIGHTMAP_PARAMS, type BlockLightmapParams } from '../../lib/blockEntityLighting'
4
+
5
+ export type EntityLightOverride = 'normal' | 'fullbright'
6
+
7
+ export type EntityLightWorldPos = { x: number; y: number; z: number }
8
+
9
+ export type EntityLightMeta = {
10
+ name?: string
11
+ height?: number
12
+ eyeHeight?: number
13
+ }
14
+
15
+ export type EntityLightSampler = {
16
+ getLight: (x: number, y: number, z: number) => { block: number; sky: number }
17
+ getSkyLevel: () => number
18
+ getLightmapParams: () => BlockLightmapParams
19
+ getColumnRevision: (chunkX: number, chunkZ: number) => number | undefined
20
+ getGlobalRevision: () => number
21
+ lightingEnabled: () => boolean
22
+ }
23
+
24
+ const BASE_COLOR_KEY = 'entityLightBaseColor'
25
+ const LAST_APPLIED_COLOR_KEY = 'entityLightLastAppliedColor'
26
+ const COLOR_EPSILON = 1e-4
27
+ const LIVING_EYE_HEIGHT_RATIO = 0.85
28
+
29
+ type ColorBearingMaterial = THREE.Material & { color: THREE.Color }
30
+
31
+ type ControlledMaterial = {
32
+ material: ColorBearingMaterial
33
+ base: THREE.Color
34
+ }
35
+
36
+ type TrackedRoot = {
37
+ meta: EntityLightMeta
38
+ override: EntityLightOverride
39
+ materials: ControlledMaterial[]
40
+ materialsDirty: boolean
41
+ flashing: boolean
42
+ lastProbe?: { x: number; y: number; z: number }
43
+ lastColumnRevision?: number
44
+ lastGlobalRevision?: number
45
+ lastSkyLevel?: number
46
+ lastLightmapParams?: BlockLightmapParams
47
+ lastLightingEnabled?: boolean
48
+ lastBrightness?: number
49
+ }
50
+
51
+ const alignColumn = (coord: number) => Math.floor(coord / 16) * 16
52
+
53
+ export function resolveEntityEyeHeight(meta: EntityLightMeta = {}): number {
54
+ if (Number.isFinite(meta.eyeHeight)) return meta.eyeHeight as number
55
+ if (Number.isFinite(meta.height)) return (meta.height as number) * LIVING_EYE_HEIGHT_RATIO
56
+ return 0
57
+ }
58
+
59
+ export function entityProbeBlock(worldPos: EntityLightWorldPos, eyeHeight = 0): { x: number; y: number; z: number } {
60
+ return {
61
+ x: Math.floor(worldPos.x),
62
+ y: Math.floor(worldPos.y + eyeHeight),
63
+ z: Math.floor(worldPos.z)
64
+ }
65
+ }
66
+
67
+ export function resolveEntityLightOverride(name?: string): EntityLightOverride {
68
+ return name === 'glow_item_frame' ? 'fullbright' : 'normal'
69
+ }
70
+
71
+ type Rgb = { r: number; g: number; b: number }
72
+
73
+ const hasColor = (material: THREE.Material): material is ColorBearingMaterial => !!material && (material as ColorBearingMaterial).color instanceof THREE.Color
74
+
75
+ const readRgb = (material: ColorBearingMaterial, key: string): THREE.Color | undefined => {
76
+ const stored = material.userData?.[key] as Rgb | undefined
77
+ if (!stored || !Number.isFinite(stored.r) || !Number.isFinite(stored.g) || !Number.isFinite(stored.b)) return undefined
78
+ return new THREE.Color(stored.r, stored.g, stored.b)
79
+ }
80
+
81
+ const storeRgb = (material: ColorBearingMaterial, key: string, color: THREE.Color) => {
82
+ material.userData[key] = { r: color.r, g: color.g, b: color.b }
83
+ }
84
+
85
+ const colorsClose = (a: THREE.Color, b: THREE.Color) =>
86
+ Math.abs(a.r - b.r) < COLOR_EPSILON && Math.abs(a.g - b.g) < COLOR_EPSILON && Math.abs(a.b - b.b) < COLOR_EPSILON
87
+
88
+ const readStoredBase = (material: ColorBearingMaterial): THREE.Color | undefined => readRgb(material, BASE_COLOR_KEY)
89
+
90
+ const storeBase = (material: ColorBearingMaterial, base: THREE.Color) => {
91
+ storeRgb(material, BASE_COLOR_KEY, base)
92
+ }
93
+
94
+ const readLastApplied = (material: ColorBearingMaterial): THREE.Color | undefined => readRgb(material, LAST_APPLIED_COLOR_KEY)
95
+
96
+ const storeLastApplied = (material: ColorBearingMaterial, color: THREE.Color) => {
97
+ storeRgb(material, LAST_APPLIED_COLOR_KEY, color)
98
+ }
99
+
100
+ const resolveBaseColor = (material: ColorBearingMaterial, flashing: boolean): THREE.Color => {
101
+ const stored = readStoredBase(material)
102
+ if (!stored) {
103
+ const base = material.color.clone()
104
+ storeBase(material, base)
105
+ return base
106
+ }
107
+ if (flashing) return stored
108
+ const lastApplied = readLastApplied(material)
109
+ if (lastApplied && colorsClose(material.color, lastApplied)) return stored
110
+ const base = material.color.clone()
111
+ storeBase(material, base)
112
+ return base
113
+ }
114
+
115
+ const applyBrightnessToMaterial = (entry: ControlledMaterial, brightness: number) => {
116
+ entry.material.color.setRGB(entry.base.r * brightness, entry.base.g * brightness, entry.base.b * brightness)
117
+ storeLastApplied(entry.material, entry.material.color)
118
+ }
119
+
120
+ const lightmapParamsEqual = (a?: BlockLightmapParams, b?: BlockLightmapParams) =>
121
+ a === b ||
122
+ (!!a &&
123
+ !!b &&
124
+ (a.curve ?? DEFAULT_LIGHTMAP_PARAMS.curve) === (b.curve ?? DEFAULT_LIGHTMAP_PARAMS.curve) &&
125
+ (a.minBrightness ?? DEFAULT_LIGHTMAP_PARAMS.minBrightness) === (b.minBrightness ?? DEFAULT_LIGHTMAP_PARAMS.minBrightness) &&
126
+ (a.gamma ?? DEFAULT_LIGHTMAP_PARAMS.gamma) === (b.gamma ?? DEFAULT_LIGHTMAP_PARAMS.gamma))
127
+
128
+ export class EntityLightController {
129
+ private readonly roots = new Map<THREE.Object3D, TrackedRoot>()
130
+ private skyLevel = 1
131
+ private lightmapParams: BlockLightmapParams = { ...DEFAULT_LIGHTMAP_PARAMS }
132
+
133
+ constructor(private readonly sampler: EntityLightSampler) {
134
+ this.skyLevel = sampler.getSkyLevel()
135
+ this.lightmapParams = { ...DEFAULT_LIGHTMAP_PARAMS, ...sampler.getLightmapParams() }
136
+ }
137
+
138
+ setSkyLevel(value: number) {
139
+ this.skyLevel = value
140
+ this.refreshAll()
141
+ }
142
+
143
+ setLightmapParams(params: BlockLightmapParams) {
144
+ this.lightmapParams = { ...this.lightmapParams, ...params }
145
+ this.refreshAll()
146
+ }
147
+
148
+ register(root: THREE.Object3D, meta: EntityLightMeta = {}, sharedMaterials?: Iterable<THREE.Material>) {
149
+ const shared = sharedMaterials ? new Set(sharedMaterials) : undefined
150
+ const existing = this.roots.get(root)
151
+ if (existing) {
152
+ existing.meta = { ...existing.meta, ...meta }
153
+ existing.override = resolveEntityLightOverride(existing.meta.name)
154
+ existing.materialsDirty = true
155
+ existing.lastProbe = undefined
156
+ if (shared) this.discoverMaterials(root, existing, shared)
157
+ return
158
+ }
159
+ const tracked: TrackedRoot = {
160
+ meta,
161
+ override: resolveEntityLightOverride(meta.name),
162
+ materials: [],
163
+ materialsDirty: true,
164
+ flashing: false
165
+ }
166
+ this.roots.set(root, tracked)
167
+ if (shared) this.discoverMaterials(root, tracked, shared)
168
+ }
169
+
170
+ setMeta(root: THREE.Object3D, meta: EntityLightMeta) {
171
+ const tracked = this.roots.get(root)
172
+ if (!tracked) return
173
+ tracked.meta = { ...tracked.meta, ...meta }
174
+ tracked.override = resolveEntityLightOverride(tracked.meta.name)
175
+ tracked.lastProbe = undefined
176
+ }
177
+
178
+ unregister(root: THREE.Object3D) {
179
+ this.roots.delete(root)
180
+ }
181
+
182
+ clear() {
183
+ this.roots.clear()
184
+ }
185
+
186
+ invalidateMaterials(root: THREE.Object3D) {
187
+ const tracked = this.roots.get(root)
188
+ if (!tracked) return
189
+ tracked.materialsDirty = true
190
+ tracked.lastProbe = undefined
191
+ }
192
+
193
+ invalidateFromDescendant(node: THREE.Object3D) {
194
+ let current: THREE.Object3D | null = node
195
+ while (current) {
196
+ if (this.roots.has(current)) {
197
+ this.invalidateMaterials(current)
198
+ return
199
+ }
200
+ current = current.parent
201
+ }
202
+ }
203
+
204
+ markFlash(root: THREE.Object3D, _durationMs?: number) {
205
+ const tracked = this.roots.get(root)
206
+ if (!tracked) return
207
+ tracked.flashing = true
208
+ }
209
+
210
+ endFlash(root: THREE.Object3D) {
211
+ const tracked = this.roots.get(root)
212
+ if (!tracked) return
213
+ tracked.flashing = false
214
+ tracked.lastProbe = undefined
215
+ }
216
+
217
+ update(root: THREE.Object3D, worldPos: EntityLightWorldPos, sharedMaterials?: Iterable<THREE.Material>) {
218
+ const tracked = this.roots.get(root)
219
+ if (!tracked) return
220
+ const materialsWereDirty = tracked.materialsDirty
221
+ if (tracked.materialsDirty) {
222
+ this.discoverMaterials(root, tracked, sharedMaterials ? new Set(sharedMaterials) : undefined)
223
+ }
224
+
225
+ const lightingEnabled = this.sampler.lightingEnabled()
226
+ const probe = entityProbeBlock(worldPos, resolveEntityEyeHeight(tracked.meta))
227
+ const columnRevision = this.sampler.getColumnRevision(alignColumn(probe.x), alignColumn(probe.z))
228
+ const globalRevision = this.sampler.getGlobalRevision()
229
+ const skyLevel = this.skyLevel
230
+ const lightmapParams = this.lightmapParams
231
+
232
+ const unchanged =
233
+ tracked.lastProbe &&
234
+ tracked.lastProbe.x === probe.x &&
235
+ tracked.lastProbe.y === probe.y &&
236
+ tracked.lastProbe.z === probe.z &&
237
+ tracked.lastColumnRevision === columnRevision &&
238
+ tracked.lastGlobalRevision === globalRevision &&
239
+ tracked.lastSkyLevel === skyLevel &&
240
+ tracked.lastLightingEnabled === lightingEnabled &&
241
+ lightmapParamsEqual(tracked.lastLightmapParams, lightmapParams)
242
+
243
+ if (unchanged && !materialsWereDirty) return
244
+
245
+ tracked.lastProbe = probe
246
+ tracked.lastColumnRevision = columnRevision
247
+ tracked.lastGlobalRevision = globalRevision
248
+ tracked.lastSkyLevel = skyLevel
249
+ tracked.lastLightmapParams = lightmapParams
250
+ tracked.lastLightingEnabled = lightingEnabled
251
+
252
+ if (tracked.flashing) return
253
+
254
+ const brightness = this.computeBrightness(tracked, probe, lightingEnabled)
255
+ tracked.lastBrightness = brightness
256
+ for (const entry of tracked.materials) {
257
+ applyBrightnessToMaterial(entry, brightness)
258
+ }
259
+ }
260
+
261
+ private refreshAll() {
262
+ for (const [root, tracked] of this.roots) {
263
+ if (tracked.flashing || tracked.materialsDirty) {
264
+ tracked.lastProbe = undefined
265
+ continue
266
+ }
267
+ const lightingEnabled = this.sampler.lightingEnabled()
268
+ const probe = tracked.lastProbe ?? { x: 0, y: 0, z: 0 }
269
+ const brightness = tracked.lastProbe ? this.computeBrightness(tracked, probe, lightingEnabled) : undefined
270
+ if (brightness === undefined) {
271
+ tracked.lastProbe = undefined
272
+ continue
273
+ }
274
+ tracked.lastSkyLevel = this.skyLevel
275
+ tracked.lastLightmapParams = this.lightmapParams
276
+ tracked.lastLightingEnabled = lightingEnabled
277
+ tracked.lastBrightness = brightness
278
+ for (const entry of tracked.materials) {
279
+ applyBrightnessToMaterial(entry, brightness)
280
+ }
281
+ void root
282
+ }
283
+ }
284
+
285
+ private computeBrightness(tracked: TrackedRoot, probe: { x: number; y: number; z: number }, lightingEnabled: boolean) {
286
+ if (!lightingEnabled || tracked.override === 'fullbright') return 1
287
+ const sample = this.sampler.getLight(probe.x, probe.y, probe.z)
288
+ return blockEntityBrightness(sample.block, sample.sky, this.skyLevel, this.lightmapParams)
289
+ }
290
+
291
+ private discoverMaterials(root: THREE.Object3D, tracked: TrackedRoot, sharedMaterials?: Set<THREE.Material>) {
292
+ const next: ControlledMaterial[] = []
293
+ const seen = new Set<THREE.Material>()
294
+ root.traverse(child => {
295
+ if (child.name === 'debug') return
296
+ const raw = (child as THREE.Mesh).material
297
+ if (!raw) return
298
+ const list = Array.isArray(raw) ? raw : [raw]
299
+ let replacedArray = false
300
+ const nextList = list.map((material, index) => {
301
+ if (!hasColor(material) || seen.has(material)) return material
302
+ seen.add(material)
303
+ let current = material
304
+ if (sharedMaterials?.has(current)) {
305
+ current = current.clone() as ColorBearingMaterial
306
+ if (Array.isArray(raw)) {
307
+ list[index] = current
308
+ replacedArray = true
309
+ } else {
310
+ ;(child as THREE.Mesh).material = current
311
+ }
312
+ }
313
+ const base = resolveBaseColor(current, tracked.flashing)
314
+ next.push({ material: current, base })
315
+ return current
316
+ })
317
+ if (replacedArray) (child as THREE.Mesh).material = nextList
318
+ })
319
+ tracked.materials = next
320
+ tracked.materialsDirty = false
321
+ }
322
+ }