minecraft-renderer 0.1.55 → 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.
- package/dist/mesher.js +54 -54
- package/dist/mesher.js.map +3 -3
- package/dist/mesherWasm.js +46 -46
- package/dist/minecraft-renderer.js +60 -60
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +476 -476
- package/package.json +1 -1
- package/src/lib/buildWorkerMcDataIndexes.test.ts +18 -0
- package/src/lib/buildWorkerMcDataIndexes.ts +34 -6
- package/src/lib/items.ts +4 -3
- package/src/lib/utils.ts +1 -1
- package/src/lib/workerMessageSanitize.test.ts +14 -0
- package/src/lib/workerMessageSanitize.ts +48 -0
- package/src/resourcesManager/resourcesManager.ts +83 -8
- package/src/resourcesManager/resourcesManager.worker.test.ts +61 -0
- package/src/three/appShared.ts +1 -1
- package/src/three/entities.ts +19 -9
- package/src/three/entity/EntityMesh.ts +4 -10
- package/src/three/itemMesh.ts +20 -7
- package/src/three/threeJsMedia.ts +31 -4
- package/src/three/threeJsUtils.ts +11 -0
- package/src/worldView/worldView.ts +2 -1
package/package.json
CHANGED
|
@@ -14,6 +14,24 @@ describe('augmentWorkerMcData', () => {
|
|
|
14
14
|
expect(Array.isArray(mcData.blocksArray)).toBe(true)
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
it('keeps *Array sources after indexing (esbuild mc-data plugin reads blocksArray)', () => {
|
|
18
|
+
const mcData: Record<string, unknown> = {
|
|
19
|
+
blocks: [{ name: 'stone', id: 3, minStateId: 48, maxStateId: 63, defaultState: 48 }],
|
|
20
|
+
}
|
|
21
|
+
augmentWorkerMcData(mcData)
|
|
22
|
+
expect(Array.isArray(mcData.blocksArray)).toBe(true)
|
|
23
|
+
expect((mcData.blocksArray as unknown[]).length).toBe(1)
|
|
24
|
+
expect(Array.isArray(mcData.blocks)).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('coerces valtio-style dense objects into arrays', () => {
|
|
28
|
+
const mcData: Record<string, unknown> = {
|
|
29
|
+
entities: { 0: { name: 'bat', id: 1, type: 'mob' } },
|
|
30
|
+
}
|
|
31
|
+
augmentWorkerMcData(mcData)
|
|
32
|
+
expect((mcData.entitiesByName as Record<string, { name: string }>).bat?.name).toBe('bat')
|
|
33
|
+
})
|
|
34
|
+
|
|
17
35
|
it('builds entitiesByName and items indexes from arrays', () => {
|
|
18
36
|
const mcData: Record<string, unknown> = {
|
|
19
37
|
entities: [{ name: 'bat', id: 1, type: 'mob' }],
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
type McElement = Record<string, unknown>
|
|
3
3
|
|
|
4
|
+
function coerceDenseArray (value: unknown): McElement[] | undefined {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return value as McElement[]
|
|
7
|
+
}
|
|
8
|
+
if (!value || typeof value !== 'object') {
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
const record = value as Record<string, McElement>
|
|
12
|
+
const keys = Object.keys(record).filter((k) => /^\d+$/.test(k)).map(Number).sort((a, b) => a - b)
|
|
13
|
+
if (!keys.length || keys[0] !== 0) {
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
for (let i = 0; i < keys.length; i++) {
|
|
17
|
+
if (keys[i] !== i) {
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return keys.map((k) => record[String(k)])
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
function buildIndexFromArray<T extends McElement> (
|
|
5
25
|
array: T[],
|
|
6
26
|
field: keyof T
|
|
7
27
|
): Record<string | number, T> {
|
|
28
|
+
if (!Array.isArray(array)) {
|
|
29
|
+
console.warn('[augmentWorkerMcData] buildIndexFromArray expected array, got', typeof array)
|
|
30
|
+
return {}
|
|
31
|
+
}
|
|
8
32
|
return array.reduce<Record<string | number, T>>((index, element) => {
|
|
9
33
|
index[element[field] as string | number] = element
|
|
10
34
|
return index
|
|
@@ -16,6 +40,10 @@ function buildIndexFromArrayWithRanges<T extends McElement> (
|
|
|
16
40
|
minField: keyof T,
|
|
17
41
|
maxField: keyof T
|
|
18
42
|
): Record<number, T> {
|
|
43
|
+
if (!Array.isArray(array)) {
|
|
44
|
+
console.warn('[augmentWorkerMcData] buildIndexFromArrayWithRanges expected array, got', typeof array)
|
|
45
|
+
return {}
|
|
46
|
+
}
|
|
19
47
|
return array.reduce<Record<number, T>>((index, element) => {
|
|
20
48
|
const min = element[minField] as number
|
|
21
49
|
const max = element[maxField] as number
|
|
@@ -42,13 +70,13 @@ function getSourceArray (
|
|
|
42
70
|
arrayKey: string,
|
|
43
71
|
rawKey: string
|
|
44
72
|
): McElement[] | undefined {
|
|
45
|
-
const fromArrayKey = mcData[arrayKey]
|
|
46
|
-
if (
|
|
47
|
-
return fromArrayKey
|
|
73
|
+
const fromArrayKey = coerceDenseArray(mcData[arrayKey])
|
|
74
|
+
if (fromArrayKey?.length) {
|
|
75
|
+
return fromArrayKey
|
|
48
76
|
}
|
|
49
|
-
const raw = mcData[rawKey]
|
|
50
|
-
if (
|
|
51
|
-
return raw
|
|
77
|
+
const raw = coerceDenseArray(mcData[rawKey])
|
|
78
|
+
if (raw?.length) {
|
|
79
|
+
return raw
|
|
52
80
|
}
|
|
53
81
|
return undefined
|
|
54
82
|
}
|
package/src/lib/items.ts
CHANGED
|
@@ -4,7 +4,7 @@ import nbt from 'prismarine-nbt'
|
|
|
4
4
|
import { fromFormattedString } from '@xmcl/text-component'
|
|
5
5
|
import { getItemSelector } from '../playerState/playerState'
|
|
6
6
|
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
|
7
|
-
import { ResourcesManagerCommon } from '../resourcesManager'
|
|
7
|
+
import { getItemsDefinitionsStoreForRender, ResourcesManagerCommon } from '../resourcesManager'
|
|
8
8
|
import { ItemSpecificContextProperties } from '../playerState/types'
|
|
9
9
|
import { PlayerStateRenderer } from '../playerState/playerState'
|
|
10
10
|
|
|
@@ -131,9 +131,10 @@ export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpec
|
|
|
131
131
|
const itemSelector = getItemSelector(playerState, {
|
|
132
132
|
...specificProps
|
|
133
133
|
})
|
|
134
|
-
const
|
|
134
|
+
const resources = resourcesManager.currentResources!
|
|
135
|
+
const modelFromDef = getItemDefinition(getItemsDefinitionsStoreForRender(resources), {
|
|
135
136
|
name: itemModelName,
|
|
136
|
-
version:
|
|
137
|
+
version: resources.version,
|
|
137
138
|
properties: itemSelector
|
|
138
139
|
})?.model
|
|
139
140
|
const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName
|
package/src/lib/utils.ts
CHANGED
|
@@ -42,7 +42,7 @@ const detectFullOffscreenCanvasSupport = () => {
|
|
|
42
42
|
const hasFullOffscreenCanvasSupport = detectFullOffscreenCanvasSupport()
|
|
43
43
|
|
|
44
44
|
export const createCanvas = (width: number, height: number): OffscreenCanvas => {
|
|
45
|
-
if (hasFullOffscreenCanvasSupport) {
|
|
45
|
+
if (hasFullOffscreenCanvasSupport || typeof document === 'undefined') {
|
|
46
46
|
return new OffscreenCanvas(width, height)
|
|
47
47
|
}
|
|
48
48
|
const canvas = document.createElement('canvas')
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { sanitizeForWorkerPostMessage } from './workerMessageSanitize'
|
|
4
|
+
|
|
5
|
+
describe('sanitizeForWorkerPostMessage', () => {
|
|
6
|
+
it('drops functions such as debug loggers', () => {
|
|
7
|
+
const debug = Object.assign(() => {}, { enabled: false })
|
|
8
|
+
const entity = { id: 1, name: 'zombie', debug, position: { x: 1, y: 2, z: 3 } }
|
|
9
|
+
const sanitized = sanitizeForWorkerPostMessage(entity) as Record<string, unknown>
|
|
10
|
+
expect(sanitized.debug).toBeUndefined()
|
|
11
|
+
expect(sanitized.id).toBe(1)
|
|
12
|
+
expect(() => structuredClone(sanitized)).not.toThrow()
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Strip non–structured-clone values before Worker.postMessage (e.g. mineflayer `debug` on entities).
|
|
4
|
+
*/
|
|
5
|
+
export function sanitizeForWorkerPostMessage (value: unknown, depth = 0): unknown {
|
|
6
|
+
if (depth > 16) return undefined
|
|
7
|
+
if (value === null || value === undefined) return value
|
|
8
|
+
|
|
9
|
+
const t = typeof value
|
|
10
|
+
if (t === 'function' || t === 'symbol') return undefined
|
|
11
|
+
if (t === 'bigint') return value.toString()
|
|
12
|
+
if (t !== 'object') return value
|
|
13
|
+
|
|
14
|
+
if (value instanceof ArrayBuffer) return value
|
|
15
|
+
if (ArrayBuffer.isView(value)) {
|
|
16
|
+
const view = value as ArrayBufferView
|
|
17
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)
|
|
18
|
+
}
|
|
19
|
+
if (value instanceof Date) return value.toISOString()
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value
|
|
23
|
+
.map((entry) => sanitizeForWorkerPostMessage(entry, depth + 1))
|
|
24
|
+
.filter((entry) => entry !== undefined)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const record = value as Record<string, unknown>
|
|
28
|
+
if (
|
|
29
|
+
typeof record.x === 'number'
|
|
30
|
+
&& typeof record.y === 'number'
|
|
31
|
+
&& typeof record.z === 'number'
|
|
32
|
+
&& !('w' in record)
|
|
33
|
+
) {
|
|
34
|
+
return { x: record.x, y: record.y, z: record.z }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const out: Record<string, unknown> = {}
|
|
38
|
+
for (const key of Object.keys(record)) {
|
|
39
|
+
if (key === '_client' || key === '_events' || key === '_eventsCount') continue
|
|
40
|
+
const sanitized = sanitizeForWorkerPostMessage(record[key], depth + 1)
|
|
41
|
+
if (sanitized !== undefined) out[key] = sanitized
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sanitizeWorkerEventArgs (args: unknown[]): unknown[] {
|
|
47
|
+
return args.map((arg) => sanitizeForWorkerPostMessage(arg))
|
|
48
|
+
}
|
|
@@ -14,7 +14,8 @@ import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser'
|
|
|
14
14
|
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
|
15
15
|
import { isWebWorker } from '../three/documentRenderer'
|
|
16
16
|
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
|
17
|
-
import { getLoadedItemDefinitionsStore } from 'mc-assets'
|
|
17
|
+
import { getLoadedItemDefinitionsStore } from 'mc-assets/dist/stores'
|
|
18
|
+
import { sanitizeWorkerEventArgs } from '../lib/workerMessageSanitize'
|
|
18
19
|
|
|
19
20
|
type ResourceManagerEvents = {
|
|
20
21
|
assetsTexturesUpdated: () => void
|
|
@@ -22,16 +23,43 @@ type ResourceManagerEvents = {
|
|
|
22
23
|
assetsInventoryReady: () => void
|
|
23
24
|
}
|
|
24
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
|
+
|
|
25
52
|
export class LoadedResourcesTransferrable {
|
|
26
53
|
// todo transfer instead!
|
|
27
54
|
readonly sourceItemDefinitionsJson: any = itemDefinitionsJson
|
|
28
|
-
|
|
55
|
+
itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
|
|
29
56
|
|
|
30
57
|
allReady = false
|
|
31
58
|
// Atlas parsers
|
|
32
59
|
itemsAtlasImage!: ImageBitmap
|
|
33
60
|
blocksAtlasImage!: ImageBitmap
|
|
34
61
|
blocksAtlasJson!: ItemsAtlasesOutputJson
|
|
62
|
+
itemsAtlasJson!: ItemsAtlasesOutputJson
|
|
35
63
|
// User data (specific to current resourcepack/version)
|
|
36
64
|
customBlockStates?: Record<string, any>
|
|
37
65
|
customModels?: Record<string, any>
|
|
@@ -55,7 +83,11 @@ export class LoadedResourcesTransferrable {
|
|
|
55
83
|
|
|
56
84
|
constructor(data?: any) {
|
|
57
85
|
if (data) {
|
|
58
|
-
|
|
86
|
+
const safe = { ...data }
|
|
87
|
+
delete safe.itemsDefinitionsStore
|
|
88
|
+
delete safe.sourceItemDefinitionsJson
|
|
89
|
+
Object.assign(this, safe)
|
|
90
|
+
ensureItemsDefinitionsStore(this)
|
|
59
91
|
}
|
|
60
92
|
if (this.version) {
|
|
61
93
|
const globalMc = (globalThis as { loadedData?: IndexedData, mcData?: IndexedData }).loadedData
|
|
@@ -89,6 +121,8 @@ export class LoadedResourcesTransferrable {
|
|
|
89
121
|
}
|
|
90
122
|
|
|
91
123
|
cloned.customTextures = {}
|
|
124
|
+
delete cloned.itemsDefinitionsStore
|
|
125
|
+
delete cloned.sourceItemDefinitionsJson
|
|
92
126
|
return cloned as LoadedResourcesTransferrable
|
|
93
127
|
}
|
|
94
128
|
|
|
@@ -117,10 +151,36 @@ const STABLE_MODELS_VERSION = '1.21.4'
|
|
|
117
151
|
export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) {
|
|
118
152
|
static restorerName = 'ResourcesManager'
|
|
119
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
|
+
|
|
120
179
|
static restoreTransferred(data: any, worker?: Worker) {
|
|
121
180
|
const resourcesManager = new ResourcesManager()
|
|
122
|
-
const upResources = (
|
|
123
|
-
resourcesManager.currentResources = new LoadedResourcesTransferrable(
|
|
181
|
+
const upResources = (transferData: LoadedResourcesTransferrable) => {
|
|
182
|
+
resourcesManager.currentResources = new LoadedResourcesTransferrable(transferData)
|
|
183
|
+
resourcesManager.rebuildWorkerRenderers(resourcesManager.currentResources)
|
|
124
184
|
}
|
|
125
185
|
upResources(data.currentResources)
|
|
126
186
|
if (worker) {
|
|
@@ -139,6 +199,17 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
|
|
|
139
199
|
return resourcesManager
|
|
140
200
|
}
|
|
141
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
|
+
|
|
142
213
|
prepareForTransfer(worker?: Worker) {
|
|
143
214
|
if (worker) {
|
|
144
215
|
// todo do it automatically
|
|
@@ -149,21 +220,24 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
|
|
|
149
220
|
class: ResourcesManager.restorerName,
|
|
150
221
|
type: 'event',
|
|
151
222
|
eventName,
|
|
152
|
-
args,
|
|
223
|
+
args: sanitizeWorkerEventArgs(args),
|
|
153
224
|
})
|
|
154
225
|
// todo handle assetsInventoryReady
|
|
155
226
|
if (eventName === 'assetsTexturesUpdated' || eventName === 'assetsInventoryReady') {
|
|
227
|
+
const currentResources = this.enrichTransferSnapshot(
|
|
228
|
+
this.currentResources?.prepareForTransfer(),
|
|
229
|
+
)
|
|
156
230
|
worker.postMessage({
|
|
157
231
|
class: ResourcesManager.restorerName,
|
|
158
232
|
type: 'newResources',
|
|
159
|
-
currentResources
|
|
233
|
+
currentResources,
|
|
160
234
|
})
|
|
161
235
|
}
|
|
162
236
|
}) as any
|
|
163
237
|
}
|
|
164
238
|
return {
|
|
165
239
|
__restorer: ResourcesManager.restorerName,
|
|
166
|
-
currentResources: this.currentResources?.prepareForTransfer(),
|
|
240
|
+
currentResources: this.enrichTransferSnapshot(this.currentResources?.prepareForTransfer()),
|
|
167
241
|
}
|
|
168
242
|
}
|
|
169
243
|
|
|
@@ -316,6 +390,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
|
|
|
316
390
|
|
|
317
391
|
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
|
|
318
392
|
resources.itemsAtlasImage = await createImageBitmap(itemsCanvas)
|
|
393
|
+
resources.itemsAtlasJson = this.itemsAtlasParser.atlas.latest
|
|
319
394
|
}
|
|
320
395
|
|
|
321
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
|
+
})
|
package/src/three/appShared.ts
CHANGED
|
@@ -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 {
|
|
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): {
|
package/src/three/entities.ts
CHANGED
|
@@ -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 {
|
|
@@ -1086,7 +1099,7 @@ export class Entities {
|
|
|
1086
1099
|
? { name: entity.name }
|
|
1087
1100
|
: entity.name === 'falling_block'
|
|
1088
1101
|
? { blockState: entity['objectData'] }
|
|
1089
|
-
: entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
|
1102
|
+
: metadataAsArray(entity.metadata)?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
|
1090
1103
|
if (item) {
|
|
1091
1104
|
const object = this.getItemMesh(item, {
|
|
1092
1105
|
'minecraft:display_context': 'ground',
|
|
@@ -1177,7 +1190,8 @@ export class Entities {
|
|
|
1177
1190
|
|
|
1178
1191
|
const meta = getGeneralEntitiesMetadata(entity, this.mcData)
|
|
1179
1192
|
|
|
1180
|
-
const
|
|
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())
|
|
1181
1195
|
for (const child of mesh!.children ?? []) {
|
|
1182
1196
|
if (child.name !== 'nametag') {
|
|
1183
1197
|
child.visible = !isInvisible
|
|
@@ -1512,12 +1526,8 @@ export class Entities {
|
|
|
1512
1526
|
}
|
|
1513
1527
|
|
|
1514
1528
|
loadMap(data: any) {
|
|
1515
|
-
const texture =
|
|
1516
|
-
|
|
1517
|
-
texture.magFilter = THREE.NearestFilter
|
|
1518
|
-
texture.minFilter = THREE.NearestFilter
|
|
1519
|
-
texture.needsUpdate = true
|
|
1520
|
-
}
|
|
1529
|
+
const texture = loadNearestFilterTexture(data)
|
|
1530
|
+
texture.needsUpdate = true
|
|
1521
1531
|
return texture
|
|
1522
1532
|
}
|
|
1523
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
package/src/three/itemMesh.ts
CHANGED
|
@@ -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:
|
|
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
|
-
):
|
|
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 =
|
|
257
|
-
canvas.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import * as THREE from 'three'
|
|
3
3
|
import { WorldRendererThree } from './worldRendererThree'
|
|
4
4
|
import { ThreeJsSound } from './threeJsSound'
|
|
5
|
+
import { isWebWorker } from './documentRenderer'
|
|
6
|
+
import { loadThreeJsTextureFromUrlSync } from './threeJsUtils'
|
|
5
7
|
|
|
6
8
|
type ControlModeConfig = {
|
|
7
9
|
mouseButton: 'both' | 'left' | 'right'
|
|
@@ -187,7 +189,11 @@ export class ThreeJsMedia {
|
|
|
187
189
|
|
|
188
190
|
let video: HTMLVideoElement | undefined
|
|
189
191
|
let positionalAudio: THREE.PositionalAudio | undefined
|
|
190
|
-
|
|
192
|
+
const workerVideoUnsupported = isWebWorker && !isImage
|
|
193
|
+
if (workerVideoUnsupported) {
|
|
194
|
+
console.warn(`[addMedia] Video "${id}" skipped in off-thread renderer (no HTMLVideoElement)`)
|
|
195
|
+
}
|
|
196
|
+
if (!isImage && !workerVideoUnsupported) {
|
|
191
197
|
video = document.createElement('video')
|
|
192
198
|
video.src = props.src.endsWith('.gif') ? props.src.replace('.gif', '.mp4') : props.src
|
|
193
199
|
video.loop = props.loop ?? true
|
|
@@ -268,14 +274,35 @@ export class ThreeJsMedia {
|
|
|
268
274
|
alphaTest: 0.1
|
|
269
275
|
})
|
|
270
276
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
277
|
+
let texture: THREE.Texture
|
|
278
|
+
if (video) {
|
|
279
|
+
texture = new THREE.VideoTexture(video)
|
|
280
|
+
} else if (workerVideoUnsupported) {
|
|
281
|
+
texture = this.createErrorTexture(
|
|
282
|
+
props.size.width,
|
|
283
|
+
props.size.height,
|
|
284
|
+
props.background,
|
|
285
|
+
'Video unavailable (multi-thread)',
|
|
286
|
+
)
|
|
287
|
+
} else if (isWebWorker) {
|
|
288
|
+
const loaded = loadThreeJsTextureFromUrlSync(props.src)
|
|
289
|
+
texture = loaded.texture
|
|
290
|
+
texture.minFilter = THREE.NearestFilter
|
|
291
|
+
texture.magFilter = THREE.NearestFilter
|
|
292
|
+
void loaded.promise.then(() => {
|
|
293
|
+
if (this.customMedia.get(id)?.texture === texture) {
|
|
294
|
+
material.map = texture
|
|
295
|
+
material.needsUpdate = true
|
|
296
|
+
}
|
|
297
|
+
}).catch(() => handleError())
|
|
298
|
+
} else {
|
|
299
|
+
texture = new THREE.TextureLoader().load(props.src, () => {
|
|
274
300
|
if (this.customMedia.get(id)?.texture === texture) {
|
|
275
301
|
material.map = texture
|
|
276
302
|
material.needsUpdate = true
|
|
277
303
|
}
|
|
278
304
|
}, undefined, () => handleError()) // todo cache
|
|
305
|
+
}
|
|
279
306
|
texture.minFilter = THREE.NearestFilter
|
|
280
307
|
texture.magFilter = THREE.NearestFilter
|
|
281
308
|
// texture.format = THREE.RGBAFormat
|