Skip to content

Commit 0db49e7

Browse files
authored
feat: Full support for rendering blocks in inventory GUI powered by deeplsate (#292)
1 parent 998f0f0 commit 0db49e7

File tree

6 files changed

+336
-19
lines changed

6 files changed

+336
-19
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@
148148
"http-browserify": "^1.7.0",
149149
"http-server": "^14.1.1",
150150
"https-browserify": "^1.0.0",
151+
"mc-assets": "^0.2.42",
151152
"mineflayer-mouse": "^0.0.5",
152-
"mc-assets": "^0.2.37",
153153
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
154154
"mineflayer": "github:zardoy/mineflayer",
155155
"mineflayer-pathfinder": "^2.4.4",

pnpm-lock.yaml

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

renderer/viewer/lib/guiRenderer.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// Import placeholders - replace with actual imports for your environment
2+
import { ItemRenderer, Identifier, ItemStack, NbtString, Structure, StructureRenderer, ItemRendererResources, BlockDefinition, BlockModel, TextureAtlas, Resources, ItemModel } from 'deepslate'
3+
import { mat4, vec3 } from 'gl-matrix'
4+
import { AssetsParser } from 'mc-assets/dist/assetsParser'
5+
import { getLoadedImage } from 'mc-assets/dist/utils'
6+
import { BlockModel as BlockModelMcAssets, AtlasParser } from 'mc-assets'
7+
import { getLoadedBlockstatesStore, getLoadedModelsStore } from 'mc-assets/dist/stores'
8+
import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator'
9+
import { proxy, ref } from 'valtio'
10+
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
11+
12+
export const activeGuiAtlas = proxy({
13+
atlas: null as null | { json, image },
14+
})
15+
16+
export const getNonFullBlocksModels = () => {
17+
const version = viewer.world.texturesVersion ?? 'latest'
18+
const itemsDefinitions = viewer.world.itemsDefinitionsStore.data.latest
19+
const blockModelsResolved = {} as Record<string, any>
20+
const itemsModelsResolved = {} as Record<string, any>
21+
const fullBlocksWithNonStandardDisplay = [] as string[]
22+
const handledItemsWithDefinitions = new Set()
23+
const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(viewer.world.blockstatesModels), getLoadedModelsStore(viewer.world.blockstatesModels))
24+
25+
const standardGuiDisplay = {
26+
'rotation': [
27+
30,
28+
225,
29+
0
30+
],
31+
'translation': [
32+
0,
33+
0,
34+
0
35+
],
36+
'scale': [
37+
0.625,
38+
0.625,
39+
0.625
40+
]
41+
}
42+
43+
const arrEqual = (a: number[], b: number[]) => a.length === b.length && a.every((x, i) => x === b[i])
44+
const addModelIfNotFullblock = (name: string, model: BlockModelMcAssets) => {
45+
if (blockModelsResolved[name]) return
46+
if (!model?.elements?.length) return
47+
const isFullBlock = model.elements.length === 1 && arrEqual(model.elements[0].from, [0, 0, 0]) && arrEqual(model.elements[0].to, [16, 16, 16])
48+
if (isFullBlock) return
49+
model['display'] ??= {}
50+
model['display']['gui'] ??= standardGuiDisplay
51+
blockModelsResolved[name] = model
52+
}
53+
54+
for (const [name, definition] of Object.entries(itemsDefinitions)) {
55+
const item = getItemDefinition(viewer.world.itemsDefinitionsStore, {
56+
version,
57+
name,
58+
properties: {
59+
'minecraft:display_context': 'gui',
60+
},
61+
})
62+
if (item) {
63+
const { resolvedModel } = assetsParser.getResolvedModelsByModel((item.special ? name : item.model).replace('minecraft:', '')) ?? {}
64+
if (resolvedModel) {
65+
handledItemsWithDefinitions.add(name)
66+
}
67+
if (resolvedModel?.elements) {
68+
69+
let hasStandardDisplay = true
70+
if (resolvedModel['display']?.gui) {
71+
hasStandardDisplay =
72+
arrEqual(resolvedModel['display'].gui.rotation, standardGuiDisplay.rotation)
73+
&& arrEqual(resolvedModel['display'].gui.translation, standardGuiDisplay.translation)
74+
&& arrEqual(resolvedModel['display'].gui.scale, standardGuiDisplay.scale)
75+
}
76+
77+
addModelIfNotFullblock(name, resolvedModel)
78+
79+
if (!blockModelsResolved[name] && !hasStandardDisplay) {
80+
fullBlocksWithNonStandardDisplay.push(name)
81+
}
82+
const notSideLight = resolvedModel['gui_light'] && resolvedModel['gui_light'] !== 'side'
83+
if (!hasStandardDisplay || notSideLight) {
84+
blockModelsResolved[name] = resolvedModel
85+
}
86+
}
87+
if (!blockModelsResolved[name] && item.tints && resolvedModel) {
88+
resolvedModel['tints'] = item.tints
89+
if (resolvedModel.elements) {
90+
blockModelsResolved[name] = resolvedModel
91+
} else {
92+
itemsModelsResolved[name] = resolvedModel
93+
}
94+
}
95+
}
96+
}
97+
98+
for (const [name, blockstate] of Object.entries(viewer.world.blockstatesModels.blockstates.latest)) {
99+
if (handledItemsWithDefinitions.has(name)) {
100+
continue
101+
}
102+
const resolvedModel = assetsParser.getResolvedModelFirst({ name: name.replace('minecraft:', ''), properties: {} }, true)
103+
if (resolvedModel) {
104+
addModelIfNotFullblock(name, resolvedModel[0])
105+
}
106+
}
107+
108+
return {
109+
blockModelsResolved,
110+
itemsModelsResolved
111+
}
112+
}
113+
114+
// customEvents.on('gameLoaded', () => {
115+
// const res = getNonFullBlocksModels()
116+
// })
117+
118+
const RENDER_SIZE = 64
119+
120+
const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
121+
const img = await getLoadedImage(isItems ? viewer.world.itemsAtlasParser!.latestImage : viewer.world.blocksAtlasParser!.latestImage)
122+
const canvasTemp = document.createElement('canvas')
123+
canvasTemp.width = img.width
124+
canvasTemp.height = img.height
125+
canvasTemp.style.imageRendering = 'pixelated'
126+
const ctx = canvasTemp.getContext('2d')!
127+
ctx.imageSmoothingEnabled = false
128+
ctx.drawImage(img, 0, 0)
129+
130+
const atlasParser = isItems ? viewer.world.itemsAtlasParser! : viewer.world.blocksAtlasParser!
131+
const textureAtlas = new TextureAtlas(
132+
ctx.getImageData(0, 0, img.width, img.height),
133+
Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
134+
return [key, [
135+
value.u,
136+
value.v,
137+
(value.u + (value.su ?? atlasParser.atlas.latest.suSv)),
138+
(value.v + (value.sv ?? atlasParser.atlas.latest.suSv)),
139+
]] as [string, [number, number, number, number]]
140+
}))
141+
)
142+
143+
const PREVIEW_ID = Identifier.parse('preview:preview')
144+
const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() } }, undefined)
145+
146+
let modelData: any
147+
let currentModelName: string | undefined
148+
const resources: ItemRendererResources = {
149+
getBlockModel (id) {
150+
if (id.equals(PREVIEW_ID)) {
151+
return BlockModel.fromJson(modelData ?? {})
152+
}
153+
return null
154+
},
155+
getTextureUV (texture) {
156+
return textureAtlas.getTextureUV(texture.toString().slice(1).split('/').slice(1).join('/') as any)
157+
},
158+
getTextureAtlas () {
159+
return textureAtlas.getTextureAtlas()
160+
},
161+
getItemComponents (id) {
162+
return new Map()
163+
},
164+
getItemModel (id) {
165+
// const isSpecial = currentModelName === 'shield' || currentModelName === 'conduit' || currentModelName === 'trident'
166+
const isSpecial = false
167+
if (id.equals(PREVIEW_ID)) {
168+
return ItemModel.fromJson({
169+
type: isSpecial ? 'minecraft:special' : 'minecraft:model',
170+
model: isSpecial ? {
171+
type: currentModelName,
172+
} : PREVIEW_ID.toString(),
173+
base: PREVIEW_ID.toString(),
174+
tints: modelData?.tints,
175+
})
176+
}
177+
return null
178+
},
179+
}
180+
181+
const canvas = document.createElement('canvas')
182+
canvas.width = RENDER_SIZE
183+
canvas.height = RENDER_SIZE
184+
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
185+
if (!gl) {
186+
throw new Error('Cannot get WebGL2 context')
187+
}
188+
189+
function resetGLContext (gl) {
190+
gl.clearColor(0, 0, 0, 0)
191+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
192+
}
193+
194+
// const includeOnly = ['powered_repeater', 'wooden_door']
195+
const includeOnly = [] as string[]
196+
197+
const images: Record<string, HTMLImageElement> = {}
198+
const item = new ItemStack(PREVIEW_ID, 1, new Map(Object.entries({
199+
'minecraft:item_model': new NbtString(PREVIEW_ID.toString()),
200+
})))
201+
const renderer = new ItemRenderer(gl, item, resources, { display_context: 'gui' })
202+
const missingTextures = new Set()
203+
for (const [modelName, model] of Object.entries(models)) {
204+
if (includeOnly.length && !includeOnly.includes(modelName)) continue
205+
206+
const patchMissingTextures = () => {
207+
for (const element of model.elements ?? []) {
208+
for (const [faceName, face] of Object.entries(element.faces)) {
209+
if (face.texture.startsWith('#')) {
210+
missingTextures.add(`${modelName} ${faceName}: ${face.texture}`)
211+
face.texture = 'block/unknown'
212+
}
213+
}
214+
}
215+
}
216+
patchMissingTextures()
217+
// TODO eggs
218+
219+
modelData = model
220+
currentModelName = modelName
221+
resetGLContext(gl)
222+
if (!modelData) continue
223+
renderer.setItem(item, { display_context: 'gui' })
224+
renderer.drawItem()
225+
const url = canvas.toDataURL()
226+
// eslint-disable-next-line no-await-in-loop
227+
const img = await getLoadedImage(url)
228+
images[modelName] = img
229+
}
230+
231+
if (missingTextures.size) {
232+
console.warn(`[guiRenderer] Missing textures in ${[...missingTextures].join(', ')}`)
233+
}
234+
235+
return images
236+
}
237+
238+
const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
239+
const atlas = makeTextureAtlas({
240+
input: Object.keys(images),
241+
tileSize: RENDER_SIZE,
242+
getLoadedImage (name) {
243+
return {
244+
image: images[name],
245+
}
246+
},
247+
})
248+
249+
// const atlasParser = new AtlasParser({ latest: atlas.json }, atlas.canvas.toDataURL())
250+
// const a = document.createElement('a')
251+
// a.href = await atlasParser.createDebugImage(true)
252+
// a.download = 'blocks_atlas.png'
253+
// a.click()
254+
255+
activeGuiAtlas.atlas = {
256+
json: atlas.json,
257+
image: ref(await getLoadedImage(atlas.canvas.toDataURL())),
258+
}
259+
260+
return atlas
261+
}
262+
263+
export const generateGuiAtlas = async () => {
264+
const { blockModelsResolved, itemsModelsResolved } = getNonFullBlocksModels()
265+
266+
// Generate blocks atlas
267+
console.time('generate blocks gui atlas')
268+
const blockImages = await generateItemsGui(blockModelsResolved, false)
269+
console.timeEnd('generate blocks gui atlas')
270+
console.time('generate items gui atlas')
271+
const itemImages = await generateItemsGui(itemsModelsResolved, true)
272+
console.timeEnd('generate items gui atlas')
273+
await generateAtlas({ ...blockImages, ...itemImages })
274+
// await generateAtlas(blockImages)
275+
}

renderer/viewer/lib/worldrendererCommon.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { chunkPos } from './simpleUtils'
2424
import { HandItemBlock } from './holdingBlock'
2525
import { updateStatText } from './ui/newStats'
2626
import { WorldRendererThree } from './worldrendererThree'
27+
import { generateGuiAtlas } from './guiRenderer'
2728

2829
function mod (x, n) {
2930
return ((x % n) + n) % n
@@ -354,6 +355,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
354355
}
355356
}
356357

358+
async generateGuiTextures () {
359+
await generateGuiAtlas()
360+
}
361+
357362
async updateAssetsData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
358363
const blocksAssetsParser = new AtlasParser(this.sourceData.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
359364
const itemsAssetsParser = new AtlasParser(this.sourceData.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
@@ -379,6 +384,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
379384
return texture
380385
}, this.customTextures?.items?.tileSize, undefined, customItemTextures)
381386
console.timeEnd('createItemsAtlas')
387+
382388
this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
383389
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
384390

@@ -418,13 +424,20 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
418424
config: this.mesherConfig,
419425
})
420426
}
427+
if (!this.itemsAtlasParser) return
421428
const itemsTexture = await new THREE.TextureLoader().loadAsync(this.itemsAtlasParser.latestImage)
422429
itemsTexture.magFilter = THREE.NearestFilter
423430
itemsTexture.minFilter = THREE.NearestFilter
424431
itemsTexture.flipY = false
425432
viewer.entities.itemsTexture = itemsTexture
433+
if (!this.itemsAtlasParser) return
426434

427435
this.renderUpdateEmitter.emit('textureDownloaded')
436+
437+
console.time('generateGuiTextures')
438+
await this.generateGuiTextures()
439+
console.timeEnd('generateGuiTextures')
440+
if (!this.itemsAtlasParser) return
428441
this.renderUpdateEmitter.emit('itemsTextureDownloaded')
429442
console.log('textures loaded')
430443
}

0 commit comments

Comments
 (0)