minecraft-renderer 0.1.54 → 0.1.56

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 (34) hide show
  1. package/dist/mesher.js +54 -54
  2. package/dist/mesher.js.map +3 -3
  3. package/dist/mesherWasm.js +46 -46
  4. package/dist/minecraft-renderer.js +61 -61
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +972 -972
  7. package/package.json +1 -1
  8. package/src/lib/buildWorkerMcDataIndexes.test.ts +48 -0
  9. package/src/lib/buildWorkerMcDataIndexes.ts +121 -0
  10. package/src/lib/items.ts +4 -3
  11. package/src/lib/utils.ts +1 -1
  12. package/src/lib/workerMessageSanitize.test.ts +14 -0
  13. package/src/lib/workerMessageSanitize.ts +48 -0
  14. package/src/lib/workerProxy.restore.test.ts +29 -0
  15. package/src/lib/workerProxy.ts +110 -36
  16. package/src/lib/worldrendererCommon.ts +25 -0
  17. package/src/resourcesManager/resourcesManager.ts +97 -11
  18. package/src/resourcesManager/resourcesManager.worker.test.ts +61 -0
  19. package/src/three/appShared.ts +1 -1
  20. package/src/three/chunkMeshManager.ts +4 -3
  21. package/src/three/entities.ts +27 -19
  22. package/src/three/entity/EntityMesh.ts +4 -10
  23. package/src/three/graphicsBackendBase.ts +25 -15
  24. package/src/three/graphicsBackendOffThread.ts +27 -3
  25. package/src/three/hand.ts +3 -3
  26. package/src/three/itemMesh.ts +20 -7
  27. package/src/three/menuBackground/assetUrl.ts +15 -0
  28. package/src/three/menuBackground/classic.ts +11 -5
  29. package/src/three/menuBackground/futuristic.ts +3 -0
  30. package/src/three/threeJsMedia.ts +31 -4
  31. package/src/three/threeJsUtils.ts +11 -0
  32. package/src/three/threeWorker.ts +12 -11
  33. package/src/three/worldRendererThree.ts +7 -0
  34. package/src/worldView/worldView.ts +2 -1
@@ -12,8 +12,10 @@ import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
12
12
  import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
13
13
  import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser'
14
14
  import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
15
+ import { isWebWorker } from '../three/documentRenderer'
15
16
  import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
16
- import { getLoadedItemDefinitionsStore } from 'mc-assets'
17
+ import { getLoadedItemDefinitionsStore } from 'mc-assets/dist/stores'
18
+ import { sanitizeWorkerEventArgs } from '../lib/workerMessageSanitize'
17
19
 
18
20
  type ResourceManagerEvents = {
19
21
  assetsTexturesUpdated: () => void
@@ -21,16 +23,43 @@ type ResourceManagerEvents = {
21
23
  assetsInventoryReady: () => void
22
24
  }
23
25
 
26
+ type ItemDefinitionsStore = ReturnType<typeof getLoadedItemDefinitionsStore>
27
+
28
+ let workerItemDefinitionsStore: ItemDefinitionsStore | null = null
29
+
30
+ export function getItemsDefinitionsStoreForRender (
31
+ resources: LoadedResourcesTransferrable
32
+ ): ItemDefinitionsStore {
33
+ if (!isWebWorker) {
34
+ ensureItemsDefinitionsStore(resources)
35
+ return resources.itemsDefinitionsStore
36
+ }
37
+ if (!workerItemDefinitionsStore || typeof workerItemDefinitionsStore.get !== 'function') {
38
+ workerItemDefinitionsStore = getLoadedItemDefinitionsStore(itemDefinitionsJson)
39
+ }
40
+ return workerItemDefinitionsStore
41
+ }
42
+
43
+ export function ensureItemsDefinitionsStore (resources: LoadedResourcesTransferrable): void {
44
+ if (isWebWorker) return
45
+ const store = resources.itemsDefinitionsStore as { get?: unknown }
46
+ if (typeof store?.get === 'function') return
47
+ resources.itemsDefinitionsStore = getLoadedItemDefinitionsStore(
48
+ resources.sourceItemDefinitionsJson ?? itemDefinitionsJson
49
+ )
50
+ }
51
+
24
52
  export class LoadedResourcesTransferrable {
25
53
  // todo transfer instead!
26
54
  readonly sourceItemDefinitionsJson: any = itemDefinitionsJson
27
- readonly itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
55
+ itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
28
56
 
29
57
  allReady = false
30
58
  // Atlas parsers
31
59
  itemsAtlasImage!: ImageBitmap
32
60
  blocksAtlasImage!: ImageBitmap
33
61
  blocksAtlasJson!: ItemsAtlasesOutputJson
62
+ itemsAtlasJson!: ItemsAtlasesOutputJson
34
63
  // User data (specific to current resourcepack/version)
35
64
  customBlockStates?: Record<string, any>
36
65
  customModels?: Record<string, any>
@@ -54,8 +83,20 @@ export class LoadedResourcesTransferrable {
54
83
 
55
84
  constructor(data?: any) {
56
85
  if (data) {
57
- Object.assign(this, data)
58
- this.mcData = MinecraftData(this.version)
86
+ const safe = { ...data }
87
+ delete safe.itemsDefinitionsStore
88
+ delete safe.sourceItemDefinitionsJson
89
+ Object.assign(this, safe)
90
+ ensureItemsDefinitionsStore(this)
91
+ }
92
+ if (this.version) {
93
+ const globalMc = (globalThis as { loadedData?: IndexedData, mcData?: IndexedData }).loadedData
94
+ ?? (globalThis as { mcData?: IndexedData }).mcData
95
+ if (isWebWorker && globalMc?.entitiesByName) {
96
+ this.mcData = globalMc
97
+ } else {
98
+ this.mcData = MinecraftData(this.version)
99
+ }
59
100
  // this.itemsRenderer = new ItemsRenderer(
60
101
  // this.version,
61
102
  // this.blockstatesModels,
@@ -70,14 +111,18 @@ export class LoadedResourcesTransferrable {
70
111
  for (const key in this) {
71
112
  if (!Object.prototype.hasOwnProperty.call(this, key)) continue
72
113
  if (typeof this[key] === 'function') continue
73
- if (key === 'itemsRenderer' || key === 'worldBlockProvider' || key === 'mcData') {
74
- // Skip these fields
114
+ if (
115
+ key === 'itemsRenderer' || key === 'worldBlockProvider' || key === 'mcData'
116
+ || key === 'itemsDefinitionsStore' || key === 'sourceItemDefinitionsJson'
117
+ ) {
75
118
  continue
76
119
  }
77
120
  cloned[key] = this[key as keyof this]
78
121
  }
79
122
 
80
123
  cloned.customTextures = {}
124
+ delete cloned.itemsDefinitionsStore
125
+ delete cloned.sourceItemDefinitionsJson
81
126
  return cloned as LoadedResourcesTransferrable
82
127
  }
83
128
 
@@ -106,10 +151,36 @@ const STABLE_MODELS_VERSION = '1.21.4'
106
151
  export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) {
107
152
  static restorerName = 'ResourcesManager'
108
153
 
154
+ rebuildWorkerRenderers (resources: LoadedResourcesTransferrable): void {
155
+ if (!isWebWorker) return
156
+ if (!resources.version || !resources.blockstatesModels || !resources.blocksAtlasJson) return
157
+
158
+ this.blocksAtlasParser = new AtlasParser({ latest: resources.blocksAtlasJson }, '')
159
+ if (resources.itemsAtlasJson) {
160
+ this.itemsAtlasParser = new AtlasParser({ latest: resources.itemsAtlasJson }, '')
161
+ } else if (!this.itemsAtlasParser?.atlas?.latest) {
162
+ if (!this.sourceItemsAtlases || Object.keys(this.sourceItemsAtlases).length === 0) return
163
+ this.itemsAtlasParser = new AtlasParser(this.sourceItemsAtlases, '')
164
+ }
165
+
166
+ resources.itemsRenderer = new ItemsRenderer(
167
+ resources.version,
168
+ resources.blockstatesModels,
169
+ this.itemsAtlasParser,
170
+ this.blocksAtlasParser
171
+ )
172
+ resources.worldBlockProvider = worldBlockProvider(
173
+ resources.blockstatesModels,
174
+ this.blocksAtlasParser.atlas,
175
+ STABLE_MODELS_VERSION
176
+ )
177
+ }
178
+
109
179
  static restoreTransferred(data: any, worker?: Worker) {
110
180
  const resourcesManager = new ResourcesManager()
111
- const upResources = (data) => {
112
- resourcesManager.currentResources = new LoadedResourcesTransferrable(data)
181
+ const upResources = (transferData: LoadedResourcesTransferrable) => {
182
+ resourcesManager.currentResources = new LoadedResourcesTransferrable(transferData)
183
+ resourcesManager.rebuildWorkerRenderers(resourcesManager.currentResources)
113
184
  }
114
185
  upResources(data.currentResources)
115
186
  if (worker) {
@@ -128,6 +199,17 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
128
199
  return resourcesManager
129
200
  }
130
201
 
202
+ enrichTransferSnapshot (transfer?: LoadedResourcesTransferrable): LoadedResourcesTransferrable | undefined {
203
+ if (!transfer) return transfer
204
+ if (this.itemsAtlasParser?.atlas?.latest) {
205
+ transfer.itemsAtlasJson = this.itemsAtlasParser.atlas.latest
206
+ }
207
+ if (this.blocksAtlasParser?.atlas?.latest) {
208
+ transfer.blocksAtlasJson = this.blocksAtlasParser.atlas.latest
209
+ }
210
+ return transfer
211
+ }
212
+
131
213
  prepareForTransfer(worker?: Worker) {
132
214
  if (worker) {
133
215
  // todo do it automatically
@@ -138,21 +220,24 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
138
220
  class: ResourcesManager.restorerName,
139
221
  type: 'event',
140
222
  eventName,
141
- args,
223
+ args: sanitizeWorkerEventArgs(args),
142
224
  })
143
225
  // todo handle assetsInventoryReady
144
226
  if (eventName === 'assetsTexturesUpdated' || eventName === 'assetsInventoryReady') {
227
+ const currentResources = this.enrichTransferSnapshot(
228
+ this.currentResources?.prepareForTransfer(),
229
+ )
145
230
  worker.postMessage({
146
231
  class: ResourcesManager.restorerName,
147
232
  type: 'newResources',
148
- currentResources: this.currentResources?.prepareForTransfer(),
233
+ currentResources,
149
234
  })
150
235
  }
151
236
  }) as any
152
237
  }
153
238
  return {
154
239
  __restorer: ResourcesManager.restorerName,
155
- currentResources: this.currentResources?.prepareForTransfer(),
240
+ currentResources: this.enrichTransferSnapshot(this.currentResources?.prepareForTransfer()),
156
241
  }
157
242
  }
158
243
 
@@ -305,6 +390,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
305
390
 
306
391
  this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
307
392
  resources.itemsAtlasImage = await createImageBitmap(itemsCanvas)
393
+ resources.itemsAtlasJson = this.itemsAtlasParser.atlas.latest
308
394
  }
309
395
 
310
396
  async generateGuiTextures() {
@@ -0,0 +1,61 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
4
+ import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
5
+ import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
6
+ import { AtlasParser } from 'mc-assets/dist/atlasParser'
7
+ import {
8
+ getItemsDefinitionsStoreForRender,
9
+ LoadedResourcesTransferrable,
10
+ ResourcesManager,
11
+ } from './resourcesManager'
12
+
13
+ vi.mock('../three/documentRenderer', () => ({
14
+ isWebWorker: true,
15
+ }))
16
+
17
+ describe('ResourcesManager.rebuildWorkerRenderers', () => {
18
+ it('creates ItemsRenderer with working modelsStore.get in worker context', () => {
19
+ const blocksAtlasParser = new AtlasParser(blocksAtlases as any, '')
20
+ const itemsAtlasParser = new AtlasParser(itemsAtlases as any, '')
21
+ const resources = new LoadedResourcesTransferrable({
22
+ version: '1.21.4',
23
+ texturesVersion: '1.21.4',
24
+ blockstatesModels,
25
+ blocksAtlasJson: blocksAtlasParser.atlas.latest,
26
+ itemsAtlasJson: itemsAtlasParser.atlas.latest,
27
+ allReady: true,
28
+ })
29
+
30
+ const manager = new ResourcesManager()
31
+ manager.rebuildWorkerRenderers(resources)
32
+
33
+ expect(resources.itemsRenderer).toBeDefined()
34
+ expect(resources.worldBlockProvider).toBeDefined()
35
+ const tex = resources.itemsRenderer!.getItemTexture('item/missing_texture')
36
+ expect(tex).toBeDefined()
37
+ })
38
+
39
+ it('getItemsDefinitionsStoreForRender returns store with .get in worker', () => {
40
+ const resources = new LoadedResourcesTransferrable({
41
+ version: '1.21.4',
42
+ blockstatesModels,
43
+ blocksAtlasJson: (new AtlasParser(blocksAtlases as any, '')).atlas.latest,
44
+ itemsDefinitionsStore: { data: { latest: {} }, inclusive: false },
45
+ })
46
+ const store = getItemsDefinitionsStoreForRender(resources)
47
+ expect(typeof store.get).toBe('function')
48
+ })
49
+
50
+ it('falls back to bundled items atlas when itemsAtlasJson is missing', () => {
51
+ const resources = new LoadedResourcesTransferrable({
52
+ version: '1.21.4',
53
+ blockstatesModels,
54
+ blocksAtlasJson: (new AtlasParser(blocksAtlases as any, '')).atlas.latest,
55
+ })
56
+ const manager = new ResourcesManager()
57
+ manager.rebuildWorkerRenderers(resources)
58
+ expect(resources.itemsRenderer).toBeDefined()
59
+ expect(resources.itemsRenderer!.getItemTexture('item/missing_texture')).toBeDefined()
60
+ })
61
+ })
@@ -3,7 +3,7 @@ import { BlockModel } from 'mc-assets/dist/types'
3
3
  import { ItemSpecificContextProperties } from '../playerState/types'
4
4
  import { PlayerStateRenderer } from '../playerState/playerState'
5
5
  import { GeneralInputItem, getItemModelName } from '../lib/items'
6
- import { ResourcesManager, ResourcesManagerTransferred } from '../resourcesManager'
6
+ import { ResourcesManagerTransferred } from '../resourcesManager'
7
7
  import { renderSlot } from './renderSlot'
8
8
 
9
9
  export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): {
@@ -544,6 +544,7 @@ export class ChunkMeshManager {
544
544
  this.pendingNearReveal.set(chunkKey, Date.now())
545
545
  this.armNearRevealTimer(chunkKey)
546
546
  this.armExpectedGraceTimer(chunkKey)
547
+ this.tryRevealPending()
547
548
  return
548
549
  }
549
550
  this.flushChunkDisplay(chunkKey)
@@ -598,9 +599,8 @@ export class ChunkMeshManager {
598
599
  * `chunkKey` is not yet `finishedChunks=true`.
599
600
  *
600
601
  * Two regimes by `ageMs` (time spent in `pendingNearReveal`):
601
- * - Within `EXPECTED_NEAR_GRACE_MS`: walks every expected position in
602
- * the view-distance circle; missing-and-not-finished counts as a
603
- * blocker (catches "far worker beats near worker").
602
+ * - Within `EXPECTED_NEAR_GRACE_MS`: nearer columns in the circle that are
603
+ * loaded but not finished block (far worker beats near worker).
604
604
  * - After grace: only actually-loaded-but-not-finished columns block,
605
605
  * so a never-arriving column does not freeze the view.
606
606
  */
@@ -633,6 +633,7 @@ export class ChunkMeshManager {
633
633
  const oz = (playerCz + dCz) << 4
634
634
  const otherKey = `${ox},${oz}`
635
635
  if (otherKey === chunkKey) continue
636
+ if (!loadedChunks[otherKey]) continue
636
637
  if (!finishedChunks[otherKey]) return true
637
638
  }
638
639
  }
@@ -22,7 +22,7 @@ import { createItemMesh } from './itemMesh'
22
22
  import * as Entity from './entity/EntityMesh'
23
23
  import { getMesh } from './entity/EntityMesh'
24
24
  import { WalkingGeneralSwing } from './entity/animations'
25
- import { disposeObject, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
25
+ import { disposeObject, loadNearestFilterTexture, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
26
26
  import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
27
27
  import { WorldRendererThree } from './worldRendererThree'
28
28
  import { IndexedData } from 'minecraft-data'
@@ -213,6 +213,19 @@ const nametags = {}
213
213
 
214
214
  const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
215
215
 
216
+ function metadataAsArray (metadata: unknown): unknown[] | undefined {
217
+ if (metadata == null) return undefined
218
+ if (Array.isArray(metadata)) return metadata
219
+ if (typeof metadata === 'object') {
220
+ const record = metadata as Record<string, unknown>
221
+ const keys = Object.keys(record).filter((k) => /^\d+$/.test(k)).map(Number).sort((a, b) => a - b)
222
+ if (keys.length && keys[0] === 0 && keys.every((k, i) => k === i)) {
223
+ return keys.map((k) => record[String(k)])
224
+ }
225
+ }
226
+ return undefined
227
+ }
228
+
216
229
  function getEntityMesh(mcData: IndexedData | undefined, entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree, options: { fontFamily: string }, overrides) {
217
230
  if (entity.name) {
218
231
  try {
@@ -706,7 +719,7 @@ export class Entities {
706
719
  uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string, capeUrl?: string }>
707
720
  currentSkinUrls = {} as Record<string, string>
708
721
 
709
- private isCanvasBlank(canvas: HTMLCanvasElement): boolean {
722
+ private isCanvasBlank(canvas: HTMLCanvasElement | OffscreenCanvas): boolean {
710
723
  return !canvas.getContext('2d')
711
724
  ?.getImageData(0, 0, canvas.width, canvas.height).data
712
725
  .some(channel => channel !== 0)
@@ -802,12 +815,10 @@ export class Entities {
802
815
  let skinTexture: THREE.Texture
803
816
  let skinCanvas: OffscreenCanvas
804
817
  if (skinUrl === stevePngUrl) {
805
- skinTexture = await steveTexture
806
- const canvas = createCanvas(64, 64)
807
- const ctx = canvas.getContext('2d')
808
- if (!ctx) throw new Error('Failed to get context')
809
- ctx.drawImage(skinTexture.image, 0, 0)
810
- skinCanvas = canvas
818
+ const steveSkin = await loadSkinImage(stevePngUrl)
819
+ playerCustomSkinImage = steveSkin.image
820
+ skinTexture = new THREE.CanvasTexture(steveSkin.canvas)
821
+ skinCanvas = steveSkin.canvas
811
822
  } else {
812
823
  const { canvas, image } = await loadSkinImage(skinUrl)
813
824
  playerCustomSkinImage = image
@@ -822,12 +833,12 @@ export class Entities {
822
833
  playerObject.skin.modelType = inferModelType(skinCanvas)
823
834
  playerObject.skin['isCustom'] = skinUrl !== stevePngUrl
824
835
 
825
- let earsCanvas: HTMLCanvasElement | undefined
836
+ let earsCanvas: OffscreenCanvas | undefined
826
837
  if (!playerCustomSkinImage) {
827
838
  renderEars = false
828
839
  } else if (renderEars) {
829
- earsCanvas = document.createElement('canvas')
830
- loadEarsToCanvasFromSkin(earsCanvas, playerCustomSkinImage)
840
+ earsCanvas = createCanvas(64, 64)
841
+ loadEarsToCanvasFromSkin(earsCanvas as unknown as HTMLCanvasElement, playerCustomSkinImage)
831
842
  renderEars = !this.isCanvasBlank(earsCanvas)
832
843
  }
833
844
  if (renderEars) {
@@ -1088,7 +1099,7 @@ export class Entities {
1088
1099
  ? { name: entity.name }
1089
1100
  : entity.name === 'falling_block'
1090
1101
  ? { blockState: entity['objectData'] }
1091
- : entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
1102
+ : metadataAsArray(entity.metadata)?.find((m: any) => typeof m === 'object' && m?.itemCount)
1092
1103
  if (item) {
1093
1104
  const object = this.getItemMesh(item, {
1094
1105
  'minecraft:display_context': 'ground',
@@ -1179,7 +1190,8 @@ export class Entities {
1179
1190
 
1180
1191
  const meta = getGeneralEntitiesMetadata(entity, this.mcData)
1181
1192
 
1182
- const isInvisible = ((entity.metadata?.[0] ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator())
1193
+ const meta0 = metadataAsArray(entity.metadata)?.[0] ?? (entity.metadata as { 0?: unknown } | undefined)?.[0]
1194
+ const isInvisible = ((meta0 ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator())
1183
1195
  for (const child of mesh!.children ?? []) {
1184
1196
  if (child.name !== 'nametag') {
1185
1197
  child.visible = !isInvisible
@@ -1514,12 +1526,8 @@ export class Entities {
1514
1526
  }
1515
1527
 
1516
1528
  loadMap(data: any) {
1517
- const texture = new THREE.TextureLoader().load(data)
1518
- if (texture) {
1519
- texture.magFilter = THREE.NearestFilter
1520
- texture.minFilter = THREE.NearestFilter
1521
- texture.needsUpdate = true
1522
- }
1529
+ const texture = loadNearestFilterTexture(data)
1530
+ texture.needsUpdate = true
1523
1531
  return texture
1524
1532
  }
1525
1533
 
@@ -8,7 +8,7 @@ import ocelotPng from 'mc-assets/dist/other-textures/latest/entity/cat/ocelot.pn
8
8
  import arrowTexture from 'mc-assets/dist/other-textures/1.21.2/entity/projectiles/arrow.png'
9
9
  import spectralArrowTexture from 'mc-assets/dist/other-textures/1.21.2/entity/projectiles/spectral_arrow.png'
10
10
  import tippedArrowTexture from 'mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
11
- import { loadTexture } from '../threeJsUtils'
11
+ import { loadNearestFilterTexture, loadTexture } from '../threeJsUtils'
12
12
  import { WorldRendererThree } from '../worldRendererThree'
13
13
  import entities from './entities.json'
14
14
  import { externalModels } from './objModels'
@@ -505,9 +505,7 @@ export class EntityMesh {
505
505
  partRoot.position.set(x, y, z)
506
506
  }
507
507
  if (metadata?.texture) {
508
- const texture = new THREE.TextureLoader().load(metadata.texture)
509
- texture.minFilter = THREE.NearestFilter
510
- texture.magFilter = THREE.NearestFilter
508
+ const texture = loadNearestFilterTexture(metadata.texture)
511
509
  partRoot.traverse((child) => {
512
510
  if (child instanceof THREE.Mesh) {
513
511
  child.material = new THREE.MeshBasicMaterial({
@@ -547,9 +545,7 @@ export class EntityMesh {
547
545
  obj.position.set(x, y, z)
548
546
  }
549
547
  if (metadata?.texture) {
550
- const texture = new THREE.TextureLoader().load(metadata.texture)
551
- texture.minFilter = THREE.NearestFilter
552
- texture.magFilter = THREE.NearestFilter
548
+ const texture = loadNearestFilterTexture(metadata.texture)
553
549
  const material = new THREE.MeshBasicMaterial({
554
550
  map: texture,
555
551
  transparent: true,
@@ -616,9 +612,7 @@ export class EntityMesh {
616
612
  debugFlags.isHardcodedTexture = true
617
613
  }
618
614
  if (!texturePath) throw new Error(`No texture for ${type}`)
619
- const texture = new THREE.TextureLoader().load(texturePath)
620
- texture.minFilter = THREE.NearestFilter
621
- texture.magFilter = THREE.NearestFilter
615
+ const texture = loadNearestFilterTexture(texturePath)
622
616
  const material = new THREE.MeshBasicMaterial({
623
617
  map: texture,
624
618
  transparent: true,
@@ -149,23 +149,33 @@ export const createGraphicsBackendBase = () => {
149
149
 
150
150
  const startMenuBackground = async (menuBackgroundStartOptions?: MenuBackgroundOptions) => {
151
151
  if (!documentRenderer) throw new Error('Document renderer not initialized')
152
- if (worldRenderer) return
153
152
 
154
- if (!menuBackgroundRenderer) {
155
- const mergedOptions: MenuBackgroundOptions = {
156
- ...initOptions.config.menuBackground,
157
- ...menuBackgroundStartOptions
158
- }
159
- menuBackgroundRenderer = new MenuBackgroundRenderer(
160
- documentRenderer,
161
- { ...initOptions },
162
- mergedOptions,
163
- !!process.env.SINGLE_FILE_BUILD_MODE
164
- )
165
- callModsMethod('menuBackgroundCreated', menuBackgroundRenderer)
166
- await menuBackgroundRenderer.start(mergedOptions)
167
- callModsMethod('menuBackgroundReady', menuBackgroundRenderer)
153
+ if (worldRenderer) {
154
+ worldRenderer.destroy()
155
+ worldRenderer = null
156
+ frameTimingCollector = null
157
+ ;(globalThis as any).world = undefined
158
+ ;(globalThis as any).frameTimingCollector = undefined
159
+ }
160
+
161
+ if (menuBackgroundRenderer) {
162
+ menuBackgroundRenderer.dispose()
163
+ menuBackgroundRenderer = null
164
+ }
165
+
166
+ const mergedOptions: MenuBackgroundOptions = {
167
+ ...initOptions.config.menuBackground,
168
+ ...menuBackgroundStartOptions
168
169
  }
170
+ menuBackgroundRenderer = new MenuBackgroundRenderer(
171
+ documentRenderer,
172
+ { ...initOptions },
173
+ mergedOptions,
174
+ !!process.env.SINGLE_FILE_BUILD_MODE
175
+ )
176
+ callModsMethod('menuBackgroundCreated', menuBackgroundRenderer)
177
+ await menuBackgroundRenderer.start(mergedOptions)
178
+ callModsMethod('menuBackgroundReady', menuBackgroundRenderer)
169
179
  }
170
180
 
171
181
  const startWorld = async (displayOptionsArg: DisplayWorldOptions) => {
@@ -2,9 +2,11 @@
2
2
  import * as THREE from 'three'
3
3
  import { GraphicsBackend, GraphicsBackendLoader } from '../graphicsBackend'
4
4
  import { useWorkerProxy, deepPrepareForTransfer, findProblemTransfer } from '../lib/workerProxy'
5
- import { meshersSendMcData } from '../lib/worldrendererCommon'
5
+ import { meshersSendMcDataAwait } from '../lib/worldrendererCommon'
6
6
  import { dynamicMcDataFiles } from '../lib/buildSharedConfig.mjs'
7
7
  import { addNewStat } from '../lib/ui/newStats'
8
+ import type { MenuBackgroundOptions } from './menuBackground/types'
9
+ import { MENU_BACKGROUND_MC_VERSION } from './menuBackground/shared'
8
10
  import { createGraphicsBackendBase, type ThreeJsBackendMethods } from './graphicsBackendBase'
9
11
  import { addCanvasForWorker } from './documentRenderer'
10
12
 
@@ -58,14 +60,36 @@ export const createGraphicsBackendOffThread: GraphicsBackendLoader = async (init
58
60
  const backend: GraphicsBackend = {
59
61
  id: 'threejs',
60
62
  displayName: `three.js ${THREE.REVISION}`,
61
- async startMenuBackground() { },
63
+ async startMenuBackground(menuBackgroundStartOptions?: MenuBackgroundOptions) {
64
+ const mcData = menuBackgroundStartOptions?.resourcesManager?.currentResources?.mcData
65
+ if (mcData) {
66
+ const workerThreeSendData = {
67
+ ...dynamicMcDataFiles,
68
+ items: 'itemsArray',
69
+ entities: 'entitiesArray',
70
+ }
71
+ await meshersSendMcDataAwait([worker], MENU_BACKGROUND_MC_VERSION, workerThreeSendData, mcData)
72
+ }
73
+ const prepared = deepPrepareForTransfer(menuBackgroundStartOptions ?? {}, worker)
74
+ try {
75
+ await proxy.startMenuBackground(structuredClone(prepared))
76
+ } catch (err) {
77
+ findProblemTransfer(prepared)
78
+ throw err
79
+ }
80
+ },
62
81
  async startWorld(options) {
63
82
  const workerThreeSendData = {
64
83
  ...dynamicMcDataFiles,
65
84
  items: 'itemsArray',
66
85
  entities: 'entitiesArray',
67
86
  }
68
- meshersSendMcData([worker], options.version, workerThreeSendData, options.resourcesManager.currentResources.mcData)
87
+ await meshersSendMcDataAwait(
88
+ [worker],
89
+ options.version,
90
+ workerThreeSendData,
91
+ options.resourcesManager.currentResources.mcData
92
+ )
69
93
  console.log('mc data sent to three worker')
70
94
 
71
95
  options.inWorldRenderingConfig['__syncToWorker'] = true
package/src/three/hand.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
- import { loadSkinFromUsername, loadSkinImage } from '../lib/utils/skins'
4
- import { steveTexture } from './entities'
3
+ import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
5
4
 
6
5
 
7
6
  export const getMyHand = async (image?: string, userName?: string) => {
8
7
  let newMap: THREE.Texture
9
8
  if (!image && !userName) {
10
- newMap = await steveTexture
9
+ const { canvas } = await loadSkinImage(stevePngUrl)
10
+ newMap = new THREE.CanvasTexture(canvas)
11
11
  } else {
12
12
  if (!image) {
13
13
  image = await loadSkinFromUsername(userName!, 'skin')
@@ -1,6 +1,19 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
3
 
4
+ /** Canvas used for item texture extraction (main thread or worker). */
5
+ export type ItemTextureCanvas = HTMLCanvasElement | OffscreenCanvas
6
+
7
+ function createItemTextureCanvas (width: number, height: number): ItemTextureCanvas {
8
+ if (typeof document !== 'undefined') {
9
+ const canvas = document.createElement('canvas')
10
+ canvas.width = width
11
+ canvas.height = height
12
+ return canvas
13
+ }
14
+ return new OffscreenCanvas(width, height)
15
+ }
16
+
4
17
  export interface Create3DItemMeshOptions {
5
18
  depth: number
6
19
  pixelSize?: number
@@ -17,7 +30,7 @@ export interface Create3DItemMeshResult {
17
30
  * from a canvas containing the item texture
18
31
  */
19
32
  export function create3DItemMesh (
20
- canvas: HTMLCanvasElement,
33
+ canvas: ItemTextureCanvas,
21
34
  options: Create3DItemMeshOptions
22
35
  ): Create3DItemMeshResult {
23
36
  const { depth, pixelSize } = options
@@ -246,18 +259,18 @@ export interface ItemMeshResult {
246
259
  export function extractItemTextureToCanvas (
247
260
  sourceTexture: THREE.Texture,
248
261
  textureInfo: ItemTextureInfo
249
- ): HTMLCanvasElement {
262
+ ): ItemTextureCanvas {
250
263
  const { u, v, sizeX, sizeY } = textureInfo
251
264
 
252
265
  // Calculate canvas size - fix the calculation
253
266
  const canvasWidth = Math.max(1, Math.floor(sizeX * sourceTexture.image.width))
254
267
  const canvasHeight = Math.max(1, Math.floor(sizeY * sourceTexture.image.height))
255
268
 
256
- const canvas = document.createElement('canvas')
257
- canvas.width = canvasWidth
258
- canvas.height = canvasHeight
259
-
260
- const ctx = canvas.getContext('2d')!
269
+ const canvas = createItemTextureCanvas(canvasWidth, canvasHeight)
270
+ const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null
271
+ if (!ctx) {
272
+ throw new Error('Failed to get 2d context for item texture canvas')
273
+ }
261
274
  ctx.imageSmoothingEnabled = false
262
275
 
263
276
  // Draw the item texture region to canvas
@@ -0,0 +1,15 @@
1
+ //@ts-nocheck
2
+ export function menuBackgroundAssetUrl (...segments: string[]): string {
3
+ const relative = segments.filter(s => s.length > 0).join('/')
4
+ const base =
5
+ typeof globalThis.location !== 'undefined' && globalThis.location.href
6
+ ? globalThis.location.href
7
+ : typeof import.meta !== 'undefined' && import.meta.url
8
+ ? import.meta.url
9
+ : `/${relative}`
10
+ try {
11
+ return new URL(relative, base).href
12
+ } catch {
13
+ return `/${relative}`
14
+ }
15
+ }