diff --git a/prismarine-viewer/viewer/lib/entities.ts b/prismarine-viewer/viewer/lib/entities.ts
index 5e7a96bfe..f5f4b21fd 100644
--- a/prismarine-viewer/viewer/lib/entities.ts
+++ b/prismarine-viewer/viewer/lib/entities.ts
@@ -20,6 +20,7 @@ import { getMesh } from './entity/EntityMesh'
 import { WalkingGeneralSwing } from './entity/animations'
 import { disposeObject } from './threeJsUtils'
 import { armorModels } from './entity/objModels'
+import { Viewer } from './viewer'
 const { loadTexture } = globalThis.isElectron ? require('./utils.electron.js') : require('./utils')
 
 export const TWEEN_DURATION = 120
@@ -163,12 +164,12 @@ const nametags = {}
 
 const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
 
-function getEntityMesh (entity, scene, options, overrides) {
+function getEntityMesh (entity, world, options, overrides) {
   if (entity.name) {
     try {
       // https://github.com/PrismarineJS/prismarine-viewer/pull/410
       const entityName = (isFirstUpperCase(entity.name) ? snakeCase(entity.name) : entity.name).toLowerCase()
-      const e = new Entity.EntityMesh('1.16.4', entityName, scene, overrides)
+      const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides)
 
       if (e.mesh) {
         addNametag(entity, options, e.mesh)
@@ -211,6 +212,8 @@ export class Entities extends EventEmitter {
   clock = new THREE.Clock()
   rendering = true
   itemsTexture: THREE.Texture | null = null
+  cachedMapsImages = {} as Record<number, string>
+  itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
   getItemUv: undefined | ((idOrName: number | string) => {
     texture: THREE.Texture;
     u: number;
@@ -220,7 +223,7 @@ export class Entities extends EventEmitter {
     size?: number;
   })
 
-  constructor (public scene: THREE.Scene) {
+  constructor (public viewer: Viewer) {
     super()
     this.entitiesOptions = {}
     this.debugMode = 'none'
@@ -229,7 +232,7 @@ export class Entities extends EventEmitter {
 
   clear () {
     for (const mesh of Object.values(this.entities)) {
-      this.scene.remove(mesh)
+      this.viewer.scene.remove(mesh)
       disposeObject(mesh)
     }
     this.entities = {}
@@ -251,9 +254,9 @@ export class Entities extends EventEmitter {
     this.rendering = rendering
     for (const ent of entity ? [entity] : Object.values(this.entities)) {
       if (rendering) {
-        if (!this.scene.children.includes(ent)) this.scene.add(ent)
+        if (!this.viewer.scene.children.includes(ent)) this.viewer.scene.add(ent)
       } else {
-        this.scene.remove(ent)
+        this.viewer.scene.remove(ent)
       }
     }
   }
@@ -417,6 +420,7 @@ export class Entities extends EventEmitter {
   }
 
   getItemMesh (item) {
+    // TODO: Render proper model (especially for blocks) instead of flat texture
     const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
     if (textureUv) {
       // todo use geometry buffer uv instead!
@@ -470,9 +474,13 @@ export class Entities extends EventEmitter {
 
   update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) {
     const isPlayerModel = entity.name === 'player'
-    if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') {
+    if (entity.name === 'zombie_villager' || entity.name === 'husk') {
       overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
     }
+    if (entity.name === 'glow_item_frame') {
+      if (!overrides.textures) overrides.textures = []
+      overrides.textures['background'] = 'block:glow_item_frame'
+    }
     // this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
     let e = this.entities[entity.id]
 
@@ -480,7 +488,7 @@ export class Entities extends EventEmitter {
       if (!e) return
       if (e.additionalCleanup) e.additionalCleanup()
       this.emit('remove', entity)
-      this.scene.remove(e)
+      this.viewer.scene.remove(e)
       disposeObject(e)
       // todo dispose textures as well ?
       delete this.entities[entity.id]
@@ -551,7 +559,7 @@ export class Entities extends EventEmitter {
         //@ts-expect-error
         playerObject.animation.isMoving = false
       } else {
-        mesh = getEntityMesh(entity, this.scene, this.entitiesOptions, overrides)
+        mesh = getEntityMesh(entity, this.viewer.world, this.entitiesOptions, overrides)
       }
       if (!mesh) return
       mesh.name = 'mesh'
@@ -570,7 +578,7 @@ export class Entities extends EventEmitter {
       group.add(mesh)
       group.add(boxHelper)
       boxHelper.visible = false
-      this.scene.add(group)
+      this.viewer.scene.add(group)
 
       e = group
       this.entities[entity.id] = e
@@ -694,31 +702,51 @@ export class Entities extends EventEmitter {
     }
 
     // todo handle map, map_chunks events
-    // if (entity.name === 'item_frame' || entity.name === 'glow_item_frame') {
-    //   const example = {
-    //     "present": true,
-    //     "itemId": 847,
-    //     "itemCount": 1,
-    //     "nbtData": {
-    //         "type": "compound",
-    //         "name": "",
-    //         "value": {
-    //             "map": {
-    //                 "type": "int",
-    //                 "value": 2146483444
-    //             },
-    //             "interactiveboard": {
-    //                 "type": "byte",
-    //                 "value": 1
-    //             }
-    //         }
-    //     }
-    // }
-    //   const item = entity.metadata?.[8]
-    //   if (item.nbtData) {
-    //     const nbt = nbt.simplify(item.nbtData)
-    //   }
-    // }
+    let itemFrameMeta = getSpecificEntityMetadata('item_frame', entity)
+    if (!itemFrameMeta) {
+      itemFrameMeta = getSpecificEntityMetadata('glow_item_frame', entity)
+    }
+    if (itemFrameMeta) {
+      // TODO: fix type
+      // todo! fix errors in mc-data (no entities data prior 1.18.2)
+      const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } }
+      mesh.scale.set(1, 1, 1)
+      e.rotation.x = -entity.pitch
+      e.children.find(c => {
+        if (c.name.startsWith('map_')) {
+          disposeObject(c)
+          const existingMapNumber = parseInt(c.name.split('_')[1], 10)
+          this.itemFrameMaps[existingMapNumber] = this.itemFrameMaps[existingMapNumber]?.filter(mesh => mesh !== c)
+          if (c instanceof THREE.Mesh) {
+            c.material?.map?.dispose()
+          }
+          return true
+        } else if (c.name === 'item') {
+          disposeObject(c)
+          return true
+        }
+        return false
+      })?.removeFromParent()
+      if (item && (item.itemId ?? item.blockId ?? 0) !== 0) {
+        const rotation = (itemFrameMeta.rotation as any as number) ?? 0
+        const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data
+        if (mapNumber) {
+          // TODO: Use proper larger item frame model when a map exists
+          mesh.scale.set(16 / 12, 16 / 12, 1)
+          this.addMapModel(e, mapNumber, rotation)
+        } else {
+          const itemMesh = this.getItemMesh(item)
+          if (itemMesh) {
+            itemMesh.mesh.position.set(0, 0, 0.43)
+            itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
+            itemMesh.mesh.rotateY(Math.PI)
+            itemMesh.mesh.rotateZ(rotation * Math.PI / 4)
+            itemMesh.mesh.name = 'item'
+            e.add(itemMesh.mesh)
+          }
+        }
+      }
+    }
 
     if (entity.username) {
       e.username = entity.username
@@ -741,6 +769,74 @@ export class Entities extends EventEmitter {
     }
   }
 
+  updateMap (mapNumber: string | number, data: string) {
+    this.cachedMapsImages[mapNumber] = data
+    let itemFrameMeshes = this.itemFrameMaps[mapNumber]
+    if (!itemFrameMeshes) return
+    itemFrameMeshes = itemFrameMeshes.filter(mesh => mesh.parent)
+    this.itemFrameMaps[mapNumber] = itemFrameMeshes
+    if (itemFrameMeshes) {
+      for (const mesh of itemFrameMeshes) {
+        mesh.material.map = this.loadMap(data)
+        mesh.material.needsUpdate = true
+        mesh.visible = true
+      }
+    }
+  }
+
+  addMapModel (entityMesh: THREE.Object3D, mapNumber: number, rotation: number) {
+    const imageData = this.cachedMapsImages?.[mapNumber]
+    let texture: THREE.Texture | null = null
+    if (imageData) {
+      texture = this.loadMap(imageData)
+    }
+    const parameters = {
+      transparent: true,
+      alphaTest: 0.1,
+    }
+    if (texture) {
+      parameters['map'] = texture
+    }
+    const material = new THREE.MeshLambertMaterial(parameters)
+
+    const mapMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material)
+
+    mapMesh.rotation.set(0, Math.PI, 0)
+    entityMesh.add(mapMesh)
+    let isInvisible = false
+    entityMesh.traverseVisible(c => {
+      if (c.name === 'geometry_frame') {
+        isInvisible = false
+      }
+    })
+    if (isInvisible) {
+      mapMesh.position.set(0, 0, 0.499)
+    } else {
+      mapMesh.position.set(0, 0, 0.437)
+    }
+    mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2)
+    mapMesh.name = `map_${mapNumber}`
+
+    if (!texture) {
+      mapMesh.visible = false
+    }
+
+    if (!this.itemFrameMaps[mapNumber]) {
+      this.itemFrameMaps[mapNumber] = []
+    }
+    this.itemFrameMaps[mapNumber].push(mapMesh)
+  }
+
+  loadMap (data: any) {
+    const texture = new THREE.TextureLoader().load(data)
+    if (texture) {
+      texture.magFilter = THREE.NearestFilter
+      texture.minFilter = THREE.NearestFilter
+      texture.needsUpdate = true
+    }
+    return texture
+  }
+
   handleDamageEvent (entityId, damageAmount) {
     const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh')
     if (entityMesh) {
@@ -808,7 +904,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
       material.map = texture
     })
   } else {
-    mesh = getMesh(texturePath, armorModels.armorModel[slotType])
+    mesh = getMesh(viewer.world, texturePath, armorModels.armorModel[slotType])
     mesh.name = meshName
     material = mesh.material
     material.side = THREE.DoubleSide
diff --git a/prismarine-viewer/viewer/lib/entity/EntityMesh.js b/prismarine-viewer/viewer/lib/entity/EntityMesh.js
index 69dd95d65..9033489af 100644
--- a/prismarine-viewer/viewer/lib/entity/EntityMesh.js
+++ b/prismarine-viewer/viewer/lib/entity/EntityMesh.js
@@ -94,7 +94,7 @@ function dot(a, b) {
   return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
 }
 
-function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror = false) {
+function addCube(attr, boneId, bone, cube, sameTextureForAllFaces = false, texWidth = 64, texHeight = 64, mirror = false) {
   const cubeRotation = new THREE.Euler(0, 0, 0)
   if (cube.rotation) {
     cubeRotation.x = -cube.rotation[0] * Math.PI / 180
@@ -107,8 +107,15 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror
     const eastOrWest = dir[0] !== 0
     const faceUvs = []
     for (const pos of corners) {
-      const u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
-      const v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
+      let u
+      let v
+      if (sameTextureForAllFaces) {
+        u = (cube.uv[0] + pos[3] * cube.size[0]) / texWidth
+        v = (cube.uv[1] + pos[4] * cube.size[1]) / texHeight
+      } else {
+        u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
+        v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
+      }
 
       const posX = eastOrWest && mirror ? pos[0] ^ 1 : pos[0]
       const posY = pos[1]
@@ -148,7 +155,23 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror
   }
 }
 
-export function getMesh(texture, jsonModel, overrides = {}) {
+export function getMesh(worldRenderer, texture, jsonModel, overrides = {}) {
+  let textureWidth = jsonModel.texturewidth ?? 64
+  let textureHeight = jsonModel.textureheight ?? 64
+  let textureOffset
+  const useBlockTexture = texture.startsWith('block:')
+  if (useBlockTexture) {
+    const blockName = texture.slice(6)
+    const textureInfo = worldRenderer.blocksAtlasParser.getTextureInfo(blockName)
+    if (textureInfo) {
+      textureWidth = worldRenderer.material.map.image.width
+      textureHeight = worldRenderer.material.map.image.height
+      textureOffset = [textureInfo.u, textureInfo.v]
+    } else {
+      console.error(`Unknown block ${blockName}`)
+    }
+  }
+
   const bones = {}
 
   const geoData = {
@@ -186,7 +209,7 @@ export function getMesh(texture, jsonModel, overrides = {}) {
 
     if (jsonBone.cubes) {
       for (const cube of jsonBone.cubes) {
-        addCube(geoData, i, bone, cube, jsonModel.texturewidth, jsonModel.textureheight, jsonBone.mirror)
+        addCube(geoData, i, bone, cube, useBlockTexture, textureWidth, textureHeight, jsonBone.mirror)
       }
     }
     i++
@@ -215,18 +238,25 @@ export function getMesh(texture, jsonModel, overrides = {}) {
   mesh.bind(skeleton)
   mesh.scale.set(1 / 16, 1 / 16, 1 / 16)
 
-  loadTexture(texture, texture => {
-    if (material.map) {
-      // texture is already loaded
-      return
-    }
-    texture.magFilter = THREE.NearestFilter
-    texture.minFilter = THREE.NearestFilter
-    texture.flipY = false
-    texture.wrapS = THREE.RepeatWrapping
-    texture.wrapT = THREE.RepeatWrapping
+  if (textureOffset) {
+    texture = worldRenderer.material.map.clone()
+    texture.offset.set(textureOffset[0], textureOffset[1])
+    texture.needsUpdate = true
     material.map = texture
-  })
+  } else {
+    loadTexture(texture.endsWith('.png') || texture.startsWith('data:image/') ? texture : texture + '.png', texture => {
+      if (material.map) {
+        // texture is already loaded
+        return
+      }
+      texture.magFilter = THREE.NearestFilter
+      texture.minFilter = THREE.NearestFilter
+      texture.flipY = false
+      texture.wrapS = THREE.RepeatWrapping
+      texture.wrapT = THREE.RepeatWrapping
+      material.map = texture
+    })
+  }
 
   return mesh
 }
@@ -252,6 +282,7 @@ export const temporaryMap = {
   'hopper_minecart': 'minecart',
   'command_block_minecart': 'minecart',
   'tnt_minecart': 'minecart',
+  'glow_item_frame': 'item_frame',
   'glow_squid': 'squid',
   'trader_llama': 'llama',
   'chest_boat': 'boat',
@@ -321,7 +352,7 @@ const offsetEntity = {
 
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 export class EntityMesh {
-  constructor(version, type, scene, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
+  constructor(version, type, worldRenderer, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
     const originalType = type
     const mappedValue = temporaryMap[type]
     if (mappedValue) type = mappedValue
@@ -388,7 +419,7 @@ export class EntityMesh {
       const texture = overrides.textures?.[name] ?? e.textures[name]
       if (!texture) continue
       // console.log(JSON.stringify(jsonModel, null, 2))
-      const mesh = getMesh(texture + '.png', jsonModel, overrides)
+      const mesh = getMesh(worldRenderer, texture, jsonModel, overrides)
       mesh.name = `geometry_${name}`
       this.mesh.add(mesh)
 
diff --git a/prismarine-viewer/viewer/lib/entity/entities.json b/prismarine-viewer/viewer/lib/entity/entities.json
index 9824d4182..4436a44bf 100644
--- a/prismarine-viewer/viewer/lib/entity/entities.json
+++ b/prismarine-viewer/viewer/lib/entity/entities.json
@@ -7838,6 +7838,53 @@
       }
     }
   },
+  "item_frame": {
+    "identifier": "minecraft:item_frame",
+    "materials": {"default": "item_frame"},
+    "textures": {
+      "background": "block:item_frame",
+      "frame": "block:oak_planks"
+    },
+    "geometry": {
+      "background": {
+        "bones": [
+          {
+            "name": "base"
+          },
+          {
+            "name": "background",
+            "parent": "base",
+            "rotation": [0, 180, 0],
+            "pivot": [0, 0, 0],
+            "cubes": [
+              {"origin": [-5, -5, -8], "size": [10, 10, 0.5], "uv": [3, 3]}
+            ]
+          }
+        ],
+        "texturewidth": 16,
+        "textureheight": 16
+      },
+      "frame": {
+        "bones": [
+          {
+            "name": "frame",
+            "parent": "base",
+            "rotation": [0, 180, 0],
+            "pivot": [0, 0, 0],
+            "cubes": [
+              {"origin": [-6, -6, -8], "size": [12, 1, 1], "uv": [2, 2]},
+              {"origin": [-6, 5, -8], "size": [12, 1, 1], "uv": [2, 13]},
+              {"origin": [-6, -5, -8], "size": [1, 10, 1], "uv": [2, 3]},
+              {"origin": [5, -5, -8], "size": [1, 10, 1], "uv": [13, 3]}
+            ]
+          }
+        ],
+        "texturewidth": 16,
+        "textureheight": 16
+      }
+    },
+    "render_controllers": ["controller.render.item_frame"]
+  },
   "leash_knot": {
     "identifier": "minecraft:leash_knot",
     "materials": {"default": "leash_knot"},
@@ -7847,7 +7894,8 @@
         "bones": [
           {
             "name": "knot",
-            "cubes": [{"origin": [-3, 2, -3], "size": [6, 8, 6]}]
+            "rotation": [0, 180, 0],
+            "cubes": [{"origin": [5, 6, 5], "size": [6, 8, 6], "uv": [0, 0]}]
           }
         ],
         "texturewidth": 32,
diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts
index c7dd7fe56..82c3e6614 100644
--- a/prismarine-viewer/viewer/lib/viewer.ts
+++ b/prismarine-viewer/viewer/lib/viewer.ts
@@ -48,7 +48,7 @@ export class Viewer {
     this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
     this.setWorld()
     this.resetScene()
-    this.entities = new Entities(this.scene)
+    this.entities = new Entities(this)
     // this.primitives = new Primitives(this.scene, this.camera)
 
     this.domElement = renderer.domElement
diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts
index 61d5a503a..e556f7a32 100644
--- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts
+++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts
@@ -75,6 +75,10 @@ export class WorldDataEmitter extends EventEmitter {
     this.eventListeners = {
       // 'move': botPosition,
       entitySpawn (e: any) {
+        if (e.name === 'item_frame' || e.name === 'glow_item_frame') {
+          // Item frames use block positions in the protocol, not their center. Fix that.
+          e.position.translate(0.5, 0.5, 0.5)
+        }
         emitEntity(e)
       },
       entityUpdate (e: any) {
diff --git a/src/mineflayer/maps.ts b/src/mineflayer/maps.ts
index 75169a9f6..c5d4f7165 100644
--- a/src/mineflayer/maps.ts
+++ b/src/mineflayer/maps.ts
@@ -16,5 +16,8 @@ setImageConverter((buf: Uint8Array) => {
 customEvents.on('mineflayerBotCreated', () => {
   bot.on('login', () => {
     bot.loadPlugin(mapDownloader)
+    bot.mapDownloader.on('new_map', ({ png, id }) => {
+      viewer.entities.updateMap(id, png)
+    })
   })
 })
diff --git a/src/react/HeldMapUi.tsx b/src/react/HeldMapUi.tsx
index b4eaea605..4fadf64f5 100644
--- a/src/react/HeldMapUi.tsx
+++ b/src/react/HeldMapUi.tsx
@@ -19,7 +19,7 @@ export default () => {
       updateHeldMap()
     })
 
-    bot.on('new_map', () => {
+    bot.on('new_map', ({ id }) => {
       // total maps: Object.keys(bot.mapDownloader.maps).length
       updateHeldMap()
     })