Skip to content

Commit

Permalink
Item Frame support
Browse files Browse the repository at this point in the history
  • Loading branch information
Phoenix616 committed Jan 9, 2025
1 parent dd7c9c1 commit 3db17c5
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 61 deletions.
141 changes: 105 additions & 36 deletions prismarine-viewer/viewer/lib/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -160,15 +161,16 @@ const addNametag = (entity, options, mesh) => {

// todo cleanup
const nametags = {}
const itemFrameMaps = {}

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)
Expand Down Expand Up @@ -220,7 +222,7 @@ export class Entities extends EventEmitter {
size?: number;
})

constructor (public scene: THREE.Scene) {
constructor (public viewer: Viewer) {
super()
this.entitiesOptions = {}
this.debugMode = 'none'
Expand All @@ -229,7 +231,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 = {}
Expand All @@ -251,9 +253,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)
}
}
}
Expand Down Expand Up @@ -405,6 +407,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!
Expand Down Expand Up @@ -458,17 +461,21 @@ 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]

if (entity.delete) {
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]
Expand Down Expand Up @@ -539,7 +546,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'
Expand All @@ -558,7 +565,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
Expand Down Expand Up @@ -682,31 +689,35 @@ 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) {
this.removeItemFrameItemModel(e)
// TODO: Figure out why this doesn't match the Item mineflayer type
const item = itemFrameMeta?.item as any as { nbtData: { value: { map: { value: number } } } }
mesh.scale.set(1, 1, 1)
if (item) {
const rotation = (itemFrameMeta.rotation as any as number)
const mapNumber = item.nbtData?.value?.map?.value
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.45)
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
Expand All @@ -729,6 +740,64 @@ export class Entities extends EventEmitter {
}
}

updateMap(mapNumber, data) {
const itemFrameMeshs = itemFrameMaps[mapNumber]
itemFrameMeshs?.forEach(mesh => {
mesh.material.map = this.loadMap(data)
mesh.material.needsUpdate = true
mesh.visible = true
})
}

addMapModel(entityMesh: THREE.Object3D, mapNumber: number, rotation: number) {
const material = new THREE.MeshLambertMaterial({
transparent: true,
alphaTest: 0.1,
})
const mapMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material)
const imageData = bot.mapDownloader.maps?.[mapNumber] as any as string
if (imageData) {
material.map = this.loadMap(imageData)
} else {
mapMesh.visible = false
}
mapMesh.rotation.set(0, Math.PI, 0)
let isInvisible = true;
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.name = 'map'
mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2)
entityMesh.add(mapMesh)

if (!itemFrameMaps[mapNumber]) {
itemFrameMaps[mapNumber] = []
}
itemFrameMaps[mapNumber].push(mapMesh)
}

loadMap(data: any) {
const texture = new THREE.TextureLoader().load(data)
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
return texture
}

removeItemFrameItemModel (entityMesh: THREE.Object3D) {
for (const c of entityMesh.children) {
if (c.name === `map` || c.name === `item`) {
c.removeFromParent()
}
}
}

handleDamageEvent (entityId, damageAmount) {
const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh')
if (entityMesh) {
Expand Down Expand Up @@ -796,7 +865,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
Expand Down
82 changes: 60 additions & 22 deletions prismarine-viewer/viewer/lib/entity/EntityMesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -148,7 +155,22 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror
}
}

export function getMesh(texture, jsonModel, overrides = {}) {
export function getMesh(world, texture, jsonModel, overrides = {}) {
let textureWidth = jsonModel.texturewidth ?? 64
let textureHeight = jsonModel.textureheight ?? 64
let textureOffset = undefined
if (texture.startsWith('block:')) {
const blockName = texture.substring(6)
const textureInfo = world.blocksAtlasParser.getTextureInfo(blockName)
if (textureInfo) {
textureWidth = world.material.map.image.width
textureHeight = world.material.map.image.height
textureOffset = [textureInfo.u, textureInfo.v]
} else {
console.error(`Unknown block ${blockName}`)
}
}

const bones = {}

const geoData = {
Expand Down Expand Up @@ -186,7 +208,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, textureOffset !== undefined, textureWidth, textureHeight, jsonBone.mirror)
}
}
i++
Expand Down Expand Up @@ -215,18 +237,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 = world.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
}
Expand All @@ -252,6 +281,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',
Expand Down Expand Up @@ -321,7 +351,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, world, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
const originalType = type
const mappedValue = temporaryMap[type]
if (mappedValue) type = mappedValue
Expand All @@ -348,14 +378,22 @@ export class EntityMesh {
texturePath = `textures/${version}/entity/cat/ocelot.png`
}
if (!texturePath) throw new Error(`No texture for ${type}`)
const texture = new THREE.TextureLoader().load(texturePath)
texture.minFilter = THREE.NearestFilter
texture.magFilter = THREE.NearestFilter
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
alphaTest: 0.1
})
loadTexture(texturePath, 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
})
const obj = objLoader.parse(externalModels[type])
const scale = scaleEntity[originalType]
if (scale) obj.scale.set(scale, scale, scale)
Expand Down Expand Up @@ -388,7 +426,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(world, texture, jsonModel, overrides)
mesh.name = `geometry_${name}`
this.mesh.add(mesh)

Expand Down
Loading

0 comments on commit 3db17c5

Please sign in to comment.