Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Full support for rendering blocks in inventory GUI powered by deeplsate #292

Merged
merged 6 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@
"http-browserify": "^1.7.0",
"http-server": "^14.1.1",
"https-browserify": "^1.0.0",
"mc-assets": "^0.2.42",
"mineflayer-mouse": "^0.0.5",
"mc-assets": "^0.2.37",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"mineflayer": "github:zardoy/mineflayer",
"mineflayer-pathfinder": "^2.4.4",
Expand Down
22 changes: 11 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

275 changes: 275 additions & 0 deletions renderer/viewer/lib/guiRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
// Import placeholders - replace with actual imports for your environment
import { ItemRenderer, Identifier, ItemStack, NbtString, Structure, StructureRenderer, ItemRendererResources, BlockDefinition, BlockModel, TextureAtlas, Resources, ItemModel } from 'deepslate'
import { mat4, vec3 } from 'gl-matrix'
import { AssetsParser } from 'mc-assets/dist/assetsParser'
import { getLoadedImage } from 'mc-assets/dist/utils'
import { BlockModel as BlockModelMcAssets, AtlasParser } from 'mc-assets'
import { getLoadedBlockstatesStore, getLoadedModelsStore } from 'mc-assets/dist/stores'
import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator'
import { proxy, ref } from 'valtio'
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'

Comment on lines +1 to +11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider replacing placeholder imports with actual imports.

The comment on line 1 suggests these might be placeholder imports. Ensure all imports are correctly used and necessary for the implementation.


🏁 Script executed:

#!/bin/bash
# Check if all imports are actually used
rg -A 2 "import \{[^}]*" renderer/viewer/lib/guiRenderer.ts | grep -o '{[^}]*}' | tr ',' '\n' | grep -v "{" | sed 's/{//g' | sed 's/}//g' | tr -d ' ' > imports.txt
for import in $(cat imports.txt); do
  count=$(rg "\b$import\b" renderer/viewer/lib/guiRenderer.ts | wc -l)
  if [ $count -le 1 ]; then
    echo "Potentially unused import: $import (found $count times)"
  fi
done
rm imports.txt

Length of output: 2619


Action Required: Update Placeholder and Unused Imports in guiRenderer.ts

In renderer/viewer/lib/guiRenderer.ts (lines 1–11), please ensure that the placeholder imports are replaced with the actual, environment-specific implementations. Additionally, our analysis indicates that some imported names appear unused:

  • Structure (referenced only once)
  • StructureRenderer (referenced only once)
  • Resources (referenced only once)
  • vec3 (referenced only once)

Review these imports to decide whether they’re needed for future functionality or should be removed to maintain a clean codebase.

export const activeGuiAtlas = proxy({
atlas: null as null | { json, image },
})

export const getNonFullBlocksModels = () => {
const version = viewer.world.texturesVersion ?? 'latest'
const itemsDefinitions = viewer.world.itemsDefinitionsStore.data.latest
const blockModelsResolved = {} as Record<string, any>
const itemsModelsResolved = {} as Record<string, any>
const fullBlocksWithNonStandardDisplay = [] as string[]
const handledItemsWithDefinitions = new Set()
const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(viewer.world.blockstatesModels), getLoadedModelsStore(viewer.world.blockstatesModels))

const standardGuiDisplay = {
'rotation': [
30,
225,
0
],
'translation': [
0,
0,
0
],
'scale': [
0.625,
0.625,
0.625
]
}

const arrEqual = (a: number[], b: number[]) => a.length === b.length && a.every((x, i) => x === b[i])
const addModelIfNotFullblock = (name: string, model: BlockModelMcAssets) => {
if (blockModelsResolved[name]) return
if (!model?.elements?.length) return
const isFullBlock = model.elements.length === 1 && arrEqual(model.elements[0].from, [0, 0, 0]) && arrEqual(model.elements[0].to, [16, 16, 16])
if (isFullBlock) return
model['display'] ??= {}
model['display']['gui'] ??= standardGuiDisplay
blockModelsResolved[name] = model
}

for (const [name, definition] of Object.entries(itemsDefinitions)) {
const item = getItemDefinition(viewer.world.itemsDefinitionsStore, {
version,
name,
properties: {
'minecraft:display_context': 'gui',
},
})
if (item) {
const { resolvedModel } = assetsParser.getResolvedModelsByModel((item.special ? name : item.model).replace('minecraft:', '')) ?? {}
if (resolvedModel) {
handledItemsWithDefinitions.add(name)
}
if (resolvedModel?.elements) {

let hasStandardDisplay = true
if (resolvedModel['display']?.gui) {
hasStandardDisplay =
arrEqual(resolvedModel['display'].gui.rotation, standardGuiDisplay.rotation)
&& arrEqual(resolvedModel['display'].gui.translation, standardGuiDisplay.translation)
&& arrEqual(resolvedModel['display'].gui.scale, standardGuiDisplay.scale)
}

addModelIfNotFullblock(name, resolvedModel)

if (!blockModelsResolved[name] && !hasStandardDisplay) {
fullBlocksWithNonStandardDisplay.push(name)
}
const notSideLight = resolvedModel['gui_light'] && resolvedModel['gui_light'] !== 'side'
if (!hasStandardDisplay || notSideLight) {
blockModelsResolved[name] = resolvedModel
}
}
if (!blockModelsResolved[name] && item.tints && resolvedModel) {
resolvedModel['tints'] = item.tints
if (resolvedModel.elements) {
blockModelsResolved[name] = resolvedModel
} else {
itemsModelsResolved[name] = resolvedModel
}
}
}
}

for (const [name, blockstate] of Object.entries(viewer.world.blockstatesModels.blockstates.latest)) {
if (handledItemsWithDefinitions.has(name)) {
continue
}
const resolvedModel = assetsParser.getResolvedModelFirst({ name: name.replace('minecraft:', ''), properties: {} }, true)
if (resolvedModel) {
addModelIfNotFullblock(name, resolvedModel[0])
}
}

return {
blockModelsResolved,
itemsModelsResolved
}
}

// customEvents.on('gameLoaded', () => {
// const res = getNonFullBlocksModels()
// })

const RENDER_SIZE = 64

const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
const img = await getLoadedImage(isItems ? viewer.world.itemsAtlasParser!.latestImage : viewer.world.blocksAtlasParser!.latestImage)
const canvasTemp = document.createElement('canvas')
canvasTemp.width = img.width
canvasTemp.height = img.height
canvasTemp.style.imageRendering = 'pixelated'
const ctx = canvasTemp.getContext('2d')!
ctx.imageSmoothingEnabled = false
ctx.drawImage(img, 0, 0)

const atlasParser = isItems ? viewer.world.itemsAtlasParser! : viewer.world.blocksAtlasParser!
const textureAtlas = new TextureAtlas(
ctx.getImageData(0, 0, img.width, img.height),
Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
return [key, [
value.u,
value.v,
(value.u + (value.su ?? atlasParser.atlas.latest.suSv)),
(value.v + (value.sv ?? atlasParser.atlas.latest.suSv)),
]] as [string, [number, number, number, number]]
}))
)

const PREVIEW_ID = Identifier.parse('preview:preview')
const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() } }, undefined)

let modelData: any
let currentModelName: string | undefined
const resources: ItemRendererResources = {
getBlockModel (id) {
if (id.equals(PREVIEW_ID)) {
return BlockModel.fromJson(modelData ?? {})
}
return null
},
getTextureUV (texture) {
return textureAtlas.getTextureUV(texture.toString().slice(1).split('/').slice(1).join('/') as any)
},
getTextureAtlas () {
return textureAtlas.getTextureAtlas()
},
getItemComponents (id) {
return new Map()
},
getItemModel (id) {
// const isSpecial = currentModelName === 'shield' || currentModelName === 'conduit' || currentModelName === 'trident'
const isSpecial = false
if (id.equals(PREVIEW_ID)) {
return ItemModel.fromJson({
type: isSpecial ? 'minecraft:special' : 'minecraft:model',
model: isSpecial ? {
type: currentModelName,
} : PREVIEW_ID.toString(),
base: PREVIEW_ID.toString(),
tints: modelData?.tints,
})
}
return null
},
}

const canvas = document.createElement('canvas')
canvas.width = RENDER_SIZE
canvas.height = RENDER_SIZE
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
if (!gl) {
throw new Error('Cannot get WebGL2 context')
}
Comment on lines +185 to +187
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Error handling lacks fallback.

The error handling for WebGL2 context acquisition throws an error without providing fallback behavior, which could cause the application to crash on unsupported browsers.

Consider adding a fallback or graceful degradation:

  const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
  if (!gl) {
-   throw new Error('Cannot get WebGL2 context')
+   console.error('WebGL2 not supported, trying fallback to WebGL')
+   const fallbackGl = canvas.getContext('webgl', { preserveDrawingBuffer: true })
+   if (!fallbackGl) {
+     console.error('WebGL not supported, using software rendering fallback')
+     // Implement software fallback or show warning to user
+     throw new Error('WebGL not supported in this browser')
+   }
+   return generateFallbackItemsGui(models, isItems, fallbackGl)
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!gl) {
throw new Error('Cannot get WebGL2 context')
}
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
if (!gl) {
console.error('WebGL2 not supported, trying fallback to WebGL')
const fallbackGl = canvas.getContext('webgl', { preserveDrawingBuffer: true })
if (!fallbackGl) {
console.error('WebGL not supported, using software rendering fallback')
// Implement software fallback or show warning to user
throw new Error('WebGL not supported in this browser')
}
return generateFallbackItemsGui(models, isItems, fallbackGl)
}


function resetGLContext (gl) {
gl.clearColor(0, 0, 0, 0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
}

// const includeOnly = ['powered_repeater', 'wooden_door']
const includeOnly = [] as string[]

const images: Record<string, HTMLImageElement> = {}
const item = new ItemStack(PREVIEW_ID, 1, new Map(Object.entries({
'minecraft:item_model': new NbtString(PREVIEW_ID.toString()),
})))
const renderer = new ItemRenderer(gl, item, resources, { display_context: 'gui' })
const missingTextures = new Set()
for (const [modelName, model] of Object.entries(models)) {
if (includeOnly.length && !includeOnly.includes(modelName)) continue

const patchMissingTextures = () => {
for (const element of model.elements ?? []) {
for (const [faceName, face] of Object.entries(element.faces)) {
if (face.texture.startsWith('#')) {
missingTextures.add(`${modelName} ${faceName}: ${face.texture}`)
face.texture = 'block/unknown'
}
}
}
}
patchMissingTextures()
// TODO eggs

modelData = model
currentModelName = modelName
resetGLContext(gl)
if (!modelData) continue
renderer.setItem(item, { display_context: 'gui' })
renderer.drawItem()
const url = canvas.toDataURL()
// eslint-disable-next-line no-await-in-loop
const img = await getLoadedImage(url)
images[modelName] = img
}

if (missingTextures.size) {
console.warn(`[guiRenderer] Missing textures in ${[...missingTextures].join(', ')}`)
}

return images
}

const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
const atlas = makeTextureAtlas({
input: Object.keys(images),
tileSize: RENDER_SIZE,
getLoadedImage (name) {
return {
image: images[name],
}
},
})

// const atlasParser = new AtlasParser({ latest: atlas.json }, atlas.canvas.toDataURL())
// const a = document.createElement('a')
// a.href = await atlasParser.createDebugImage(true)
// a.download = 'blocks_atlas.png'
// a.click()

activeGuiAtlas.atlas = {
json: atlas.json,
image: ref(await getLoadedImage(atlas.canvas.toDataURL())),
}

return atlas
}

export const generateGuiAtlas = async () => {
const { blockModelsResolved, itemsModelsResolved } = getNonFullBlocksModels()

// Generate blocks atlas
console.time('generate blocks gui atlas')
const blockImages = await generateItemsGui(blockModelsResolved, false)
console.timeEnd('generate blocks gui atlas')
console.time('generate items gui atlas')
const itemImages = await generateItemsGui(itemsModelsResolved, true)
console.timeEnd('generate items gui atlas')
await generateAtlas({ ...blockImages, ...itemImages })
// await generateAtlas(blockImages)
}
13 changes: 13 additions & 0 deletions renderer/viewer/lib/worldrendererCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { chunkPos } from './simpleUtils'
import { HandItemBlock } from './holdingBlock'
import { updateStatText } from './ui/newStats'
import { WorldRendererThree } from './worldrendererThree'
import { generateGuiAtlas } from './guiRenderer'

function mod (x, n) {
return ((x % n) + n) % n
Expand Down Expand Up @@ -354,6 +355,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
}

async generateGuiTextures () {
await generateGuiAtlas()
}

async updateAssetsData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
const blocksAssetsParser = new AtlasParser(this.sourceData.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
const itemsAssetsParser = new AtlasParser(this.sourceData.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
Expand All @@ -379,6 +384,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
return texture
}, this.customTextures?.items?.tileSize, undefined, customItemTextures)
console.timeEnd('createItemsAtlas')

this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())

Expand Down Expand Up @@ -418,13 +424,20 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
config: this.mesherConfig,
})
}
if (!this.itemsAtlasParser) return
const itemsTexture = await new THREE.TextureLoader().loadAsync(this.itemsAtlasParser.latestImage)
itemsTexture.magFilter = THREE.NearestFilter
itemsTexture.minFilter = THREE.NearestFilter
itemsTexture.flipY = false
viewer.entities.itemsTexture = itemsTexture
if (!this.itemsAtlasParser) return

this.renderUpdateEmitter.emit('textureDownloaded')

console.time('generateGuiTextures')
await this.generateGuiTextures()
console.timeEnd('generateGuiTextures')
if (!this.itemsAtlasParser) return
this.renderUpdateEmitter.emit('itemsTextureDownloaded')
console.log('textures loaded')
}
Expand Down
Loading
Loading