diff --git a/prismarine-viewer/viewer/lib/entities.ts b/prismarine-viewer/viewer/lib/entities.ts index 47a0303ef..efc80b5de 100644 --- a/prismarine-viewer/viewer/lib/entities.ts +++ b/prismarine-viewer/viewer/lib/entities.ts @@ -8,15 +8,19 @@ import { PlayerObject, PlayerAnimation } from 'skinview3d' import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils' // todo replace with url import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png' +import { degreesToRadians } from '@nxg-org/mineflayer-tracker/lib/mathUtils' import { NameTagObject } from 'skinview3d/libs/nametag' import { flat, fromFormattedString } from '@xmcl/text-component' import mojangson from 'mojangson' import { snakeCase } from 'change-case' +import { Item } from 'prismarine-item' import { EntityMetadataVersions } from '../../../src/mcDataTypes' import * as Entity from './entity/EntityMesh' +import { getMesh } from './entity/EntityMesh' import { WalkingGeneralSwing } from './entity/animations' -import externalTexturesJson from './entity/externalTextures.json' import { disposeObject } from './threeJsUtils' +import { armorModels } from './entity/objModels' +const { loadTexture } = globalThis.isElectron ? require('./utils.electron.js') : require('./utils') export const TWEEN_DURATION = 120 @@ -57,6 +61,26 @@ function toQuaternion (quaternion: any, defaultValue?: THREE.Quaternion) { return new THREE.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w) } +function poseToEuler (pose: any, defaultValue?: THREE.Euler) { + if (pose === undefined) { + return defaultValue ?? new THREE.Euler() + } + if (pose instanceof THREE.Euler) { + return pose + } + if (pose['yaw'] !== undefined && pose['pitch'] !== undefined && pose['roll'] !== undefined) { + // Convert Minecraft pitch, yaw, roll definitions to our angle system + return new THREE.Euler(-degreesToRadians(pose.pitch), -degreesToRadians(pose.yaw), degreesToRadians(pose.roll), 'ZYX') + } + if (pose['x'] !== undefined && pose['y'] !== undefined && pose['z'] !== undefined) { + return new THREE.Euler(pose.z, pose.y, pose.x, 'ZYX') + } + if (Array.isArray(pose)) { + return new THREE.Euler(pose[0], pose[1], pose[2]) + } + return defaultValue ?? new THREE.Euler() +} + function getUsernameTexture ({ username, nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', @@ -369,13 +393,17 @@ export class Entities extends EventEmitter { return jsonLike.value } const parsed = typeof jsonLike === 'string' ? mojangson.simplify(mojangson.parse(jsonLike)) : nbt.simplify(jsonLike) - const text = flat(parsed).map(x => x.text) + const text = flat(parsed).map(this.textFromComponent) return text.join('') } catch (err) { return jsonLike } } + private textFromComponent (component) { + return typeof component === 'string' ? component : component.text ?? '' + } + getItemMesh (item) { const textureUv = this.getItemUv?.(item.itemId ?? item.blockId) if (textureUv) { @@ -418,14 +446,38 @@ export class Entities extends EventEmitter { } } + setVisible (mesh: THREE.Object3D, visible: boolean) { + //mesh.visible = visible + //TODO: Fix workaround for visibility setting + if (visible) { + mesh.scale.set(1, 1, 1) + } else { + mesh.scale.set(0, 0, 0) + } + } + 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') { overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}` } - if (!this.entities[entity.id] && !entity.delete) { + // 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) + disposeObject(e) + // todo dispose textures as well ? + delete this.entities[entity.id] + return + } + + let mesh + if (e === undefined) { const group = new THREE.Group() - let mesh if (entity.name === 'item') { const item = entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount) if (item) { @@ -508,7 +560,8 @@ export class Entities extends EventEmitter { boxHelper.visible = false this.scene.add(group) - this.entities[entity.id] = group + e = group + this.entities[entity.id] = e this.emit('add', entity) @@ -517,6 +570,16 @@ export class Entities extends EventEmitter { } this.setDebugMode(this.debugMode, group) this.setRendering(this.rendering, group) + } else { + mesh = e.children.find(c => c.name === 'mesh') + } + + // check if entity has armor + if (entity.equipment) { + addArmorModel(e, 'feet', entity.equipment[2]) + addArmorModel(e, 'legs', entity.equipment[3], 2) + addArmorModel(e, 'chest', entity.equipment[4]) + addArmorModel(e, 'head', entity.equipment[5]) } const meta = getGeneralEntitiesMetadata(entity) @@ -524,7 +587,7 @@ export class Entities extends EventEmitter { //@ts-expect-error // set visibility const isInvisible = entity.metadata?.[0] & 0x20 - for (const child of this.entities[entity.id]?.children.find(c => c.name === 'mesh')?.children ?? []) { + for (const child of mesh.children ?? []) { if (child.name !== 'nametag') { child.visible = !isInvisible } @@ -547,10 +610,77 @@ export class Entities extends EventEmitter { nameTagScale: textDisplayMeta?.scale, nameTagTranslation: textDisplayMeta && (textDisplayMeta.translation || new THREE.Vector3(0, 0, 0)), nameTagRotationLeft: toQuaternion(textDisplayMeta?.left_rotation), nameTagRotationRight: toQuaternion(textDisplayMeta?.right_rotation) }, this.entitiesOptions, - this.entities[entity.id].children.find(c => c.name === 'mesh') + mesh ) } + const armorStandMeta = getSpecificEntityMetadata('armor_stand', entity) + if (armorStandMeta) { + const isSmall = (parseInt(armorStandMeta.client_flags, 10) & 0x01) !== 0 + const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0 + const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0 + const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0 + mesh.castShadow = !isMarker + mesh.receiveShadow = !isMarker + if (isSmall) { + e.scale.set(0.5, 0.5, 0.5) + } else { + e.scale.set(1, 1, 1) + } + e.traverse(c => { + switch (c.name) { + case 'bone_baseplate': + this.setVisible(c, hasBasePlate) + c.rotation.y = -e.rotation.y + break + case 'bone_head': + if (armorStandMeta.head_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.head_pose)) + } + break + case 'bone_body': + if (armorStandMeta.body_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.body_pose)) + } + break + case 'bone_leftarm': + if (c.parent?.name !== 'bone_armor') { + this.setVisible(c, hasArms) + } + if (armorStandMeta.left_arm_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.left_arm_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': -10, 'pitch': -10, 'roll': 0 })) + } + break + case 'bone_rightarm': + if (c.parent?.name !== 'bone_armor') { + this.setVisible(c, hasArms) + } + if (armorStandMeta.right_arm_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.right_arm_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': 10, 'pitch': -10, 'roll': 0 })) + } + break + case 'bone_leftleg': + if (armorStandMeta.left_leg_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.left_leg_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': -1, 'pitch': -1, 'roll': 0 })) + } + break + case 'bone_rightleg': + if (armorStandMeta.right_leg_pose) { + c.setRotationFromEuler(poseToEuler(armorStandMeta.right_leg_pose)) + } else { + c.setRotationFromEuler(poseToEuler({ 'yaw': 1, 'pitch': 1, 'roll': 0 })) + } + break + } + }) + } + // todo handle map, map_chunks events // if (entity.name === 'item_frame' || entity.name === 'glow_item_frame') { // const example = { @@ -578,9 +708,6 @@ export class Entities extends EventEmitter { // } // } - // this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted) - const e = this.entities[entity.id] - if (entity.username) { e.username = entity.username } @@ -592,15 +719,6 @@ export class Entities extends EventEmitter { playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0 } - if (entity.delete && e) { - if (e.additionalCleanup) e.additionalCleanup() - this.emit('remove', entity) - this.scene.remove(e) - disposeObject(e) - // todo dispose textures as well ? - delete this.entities[entity.id] - } - if (entity.pos) { new TWEEN.Tween(e.position).to({ x: entity.pos.x, y: entity.pos.y, z: entity.pos.z }, TWEEN_DURATION).start() } @@ -645,3 +763,73 @@ function getSpecificEntityMetadata (name if (entity.name !== name) return return getGeneralEntitiesMetadata(entity) as any } + +function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item, layer = 1, overlay = false) { + if (!item) { + removeArmorModel(entityMesh, slotType) + return + } + const itemParts = item.name.split('_') + const armorMaterial = itemParts[0] + if (!armorMaterial) { + removeArmorModel(entityMesh, slotType) + return + } + // TODO: Support resource pack + // TODO: Support mirroring on certain parts of the model + const texturePath = armorModels[`${armorMaterial}Layer${layer}${overlay ? 'Overlay' : ''}`] + if (!texturePath || !armorModels.armorModel[slotType]) { + return + } + + const meshName = `geometry_armor_${slotType}${overlay ? '_overlay' : ''}` + let mesh = entityMesh.children.findLast(c => c.name === meshName) as THREE.Mesh + let material + if (mesh) { + material = mesh.material + loadTexture(texturePath, texture => { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + texture.flipY = false + texture.wrapS = THREE.MirroredRepeatWrapping + texture.wrapT = THREE.MirroredRepeatWrapping + material.map = texture + }) + } else { + mesh = getMesh(texturePath, armorModels.armorModel[slotType]) + mesh.name = meshName + material = mesh.material + material.side = THREE.DoubleSide + } + if (armorMaterial === 'leather' && !overlay) { + const color = (item.nbt?.value as any)?.display?.value?.color?.value + if (color) { + const r = color >> 16 & 0xff + const g = color >> 8 & 0xff + const b = color & 0xff + material.color.setRGB(r / 255, g / 255, b / 255) + } else { + material.color.setHex(0xB5_6D_51) // default brown color + } + addArmorModel(entityMesh, slotType, item, layer, true) + } + const group = new THREE.Object3D() + group.name = `armor_${slotType}${overlay ? '_overlay' : ''}` + group.add(mesh) + + const skeletonHelper = new THREE.SkeletonHelper(mesh) + //@ts-expect-error + skeletonHelper.material.linewidth = 2 + skeletonHelper.visible = false + group.add(skeletonHelper) + + entityMesh.add(mesh) +} + +function removeArmorModel (entityMesh: THREE.Object3D, slotType: string) { + for (const c of entityMesh.children) { + if (c.name === `geometry_armor_${slotType}` || c.name === `geometry_armor_${slotType}_overlay`) { + c.removeFromParent() + } + } +} diff --git a/prismarine-viewer/viewer/lib/entity/EntityMesh.js b/prismarine-viewer/viewer/lib/entity/EntityMesh.js index 1350dcf0d..69dd95d65 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) { +function addCube(attr, boneId, bone, cube, 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 @@ -104,15 +104,20 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64) { for (const { dir, corners, u0, v0, u1, v1 } of Object.values(elemFaces)) { const ndx = Math.floor(attr.positions.length / 3) + 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 + const posX = eastOrWest && mirror ? pos[0] ^ 1 : pos[0] + const posY = pos[1] + const posZ = eastOrWest && mirror ? pos[2] ^ 1 : pos[2] const inflate = cube.inflate ?? 0 let vecPos = new THREE.Vector3( - cube.origin[0] + pos[0] * cube.size[0] + (pos[0] ? inflate : -inflate), - cube.origin[1] + pos[1] * cube.size[1] + (pos[1] ? inflate : -inflate), - cube.origin[2] + pos[2] * cube.size[2] + (pos[2] ? inflate : -inflate) + cube.origin[0] + posX * cube.size[0] + (posX ? inflate : -inflate), + cube.origin[1] + posY * cube.size[1] + (posY ? inflate : -inflate), + cube.origin[2] + posZ * cube.size[2] + (posZ ? inflate : -inflate) ) vecPos = vecPos.applyEuler(cubeRotation) @@ -122,16 +127,28 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64) { attr.positions.push(vecPos.x, vecPos.y, vecPos.z) attr.normals.push(...dir) - attr.uvs.push(u, v) + faceUvs.push(u, v) attr.skinIndices.push(boneId, 0, 0, 0) attr.skinWeights.push(1, 0, 0, 0) } + if (mirror) { + for (let i = 0; i + 1 < corners.length; i += 2) { + const faceIndex = i * 2 + const tempFaceUvs = faceUvs.slice(faceIndex, faceIndex + 4) + faceUvs[faceIndex] = tempFaceUvs[2] + faceUvs[faceIndex + 1] = tempFaceUvs[eastOrWest ? 1 : 3] + faceUvs[faceIndex + 2] = tempFaceUvs[0] + faceUvs[faceIndex + 3] = tempFaceUvs[eastOrWest ? 3 : 1] + } + } + attr.uvs.push(...faceUvs) + attr.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3) } } -function getMesh(texture, jsonModel, overrides = {}) { +export function getMesh(texture, jsonModel, overrides = {}) { const bones = {} const geoData = { @@ -169,7 +186,7 @@ function getMesh(texture, jsonModel, overrides = {}) { if (jsonBone.cubes) { for (const cube of jsonBone.cubes) { - addCube(geoData, i, bone, cube, jsonModel.texturewidth, jsonModel.textureheight) + addCube(geoData, i, bone, cube, jsonModel.texturewidth, jsonModel.textureheight, jsonBone.mirror) } } i++ diff --git a/prismarine-viewer/viewer/lib/entity/armorModels.json b/prismarine-viewer/viewer/lib/entity/armorModels.json new file mode 100644 index 000000000..3632bad19 --- /dev/null +++ b/prismarine-viewer/viewer/lib/entity/armorModels.json @@ -0,0 +1,158 @@ +{ + "head": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "head", + "parent": "armor", + "pivot": [0, 12, 0], + "cubes": [ + { + "origin": [-4, 23, -4], + "size": [8, 8, 8], + "uv": [0, 0], + "inflate": 1 + } + ] + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + }, + "chest": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "body", + "parent": "armor", + "pivot": [0, 13, 0], + "cubes": [ + { + "origin": [-4, 12, -2], + "size": [8, 12, 4], + "uv": [16, 16], + "inflate": 1 + } + ] + }, + { + "name": "rightarm", + "parent": "armor", + "pivot": [5, 10, 0], + "cubes": [ + { + "origin": [4, 12, -2], + "size": [4, 12, 4], + "uv": [40, 16], + "inflate": 0.75 + } + ] + }, + { + "name": "leftarm", + "parent": "armor", + "pivot": [-5, 10, 0], + "cubes": [ + { + "origin": [-8, 12, -2], + "size": [4, 12, 4], + "uv": [40, 16], + "inflate": 0.75 + } + ], + "mirror": true + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + }, + "legs": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "body", + "parent": "armor", + "pivot": [0, 13, 0], + "cubes": [ + { + "origin": [-4, 12, -2], + "size": [8, 12, 4], + "uv": [16, 16], + "inflate": 0.75 + } + ] + }, + { + "name": "rightleg", + "parent": "armor", + "pivot": [1.9, 1, 0], + "cubes": [ + { + "origin": [-0.1, 0, -2], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.5 + } + ] + }, + { + "name": "leftleg", + "parent": "armor", + "pivot": [-1.9, 1, 0], + "cubes": [ + { + "origin": [-3.9, 0, -2], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.5 + } + ], + "mirror": true + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + }, + "feet": { + "bones": [ + {"name": "armor", "pivot": [0, 12, 0]}, + { + "name": "rightleg", + "parent": "armor", + "pivot": [1.9, 1, 0], + "cubes": [ + { + "origin": [-0.1, 0, -2], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.75 + } + ] + }, + { + "name": "leftleg", + "parent": "armor", + "pivot": [-1.9, 1, 0], + "cubes": [ + { + "origin": [-3.9, 0, -2], + "size": [4, 12, 4], + "uv": [0, 16], + "inflate": 0.75 + } + ], + "mirror": true + } + ], + "visible_bounds_width": 1.5, + "visible_bounds_offset": [0, 0.5, 0], + "texturewidth": 64, + "textureheight": 32 + } +} \ No newline at end of file diff --git a/prismarine-viewer/viewer/lib/entity/armorModels.ts b/prismarine-viewer/viewer/lib/entity/armorModels.ts new file mode 100644 index 000000000..e1d3f87bb --- /dev/null +++ b/prismarine-viewer/viewer/lib/entity/armorModels.ts @@ -0,0 +1,36 @@ +/* + * prismarine-web-client - prismarine-web-client + * Copyright (C) 2024 Max Lee aka Phoenix616 (mail@moep.tv) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// TODO: replace with load from resource pack +export { default as chainmailLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_1.png' +export { default as chainmailLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/chainmail_layer_2.png' +export { default as diamondLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_1.png' +export { default as diamondLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/diamond_layer_2.png' +export { default as goldenLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_1.png' +export { default as goldenLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/gold_layer_2.png' +export { default as ironLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_1.png' +export { default as ironLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/iron_layer_2.png' +export { default as leatherLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1.png' +export { default as leatherLayer1Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_1_overlay.png' +export { default as leatherLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2.png' +export { default as leatherLayer2Overlay } from 'mc-assets/dist/other-textures/latest/models/armor/leather_layer_2_overlay.png' +export { default as netheriteLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_1.png' +export { default as netheriteLayer2 } from 'mc-assets/dist/other-textures/latest/models/armor/netherite_layer_2.png' +export { default as turtleLayer1 } from 'mc-assets/dist/other-textures/latest/models/armor/turtle_layer_1.png' + +export { default as armorModel } from './armorModels.json' diff --git a/prismarine-viewer/viewer/lib/entity/entities.json b/prismarine-viewer/viewer/lib/entity/entities.json index e084a5cd3..9824d4182 100644 --- a/prismarine-viewer/viewer/lib/entity/entities.json +++ b/prismarine-viewer/viewer/lib/entity/entities.json @@ -186,15 +186,16 @@ "bones": [ { "name": "baseplate", + "parent": "waist", "cubes": [ {"origin": [-6, 0, -6], "size": [12, 1, 12], "uv": [0, 32]} ] }, - {"name": "waist", "parent": "baseplate", "pivot": [0, 12, 0]}, + {"name": "waist", "pivot": [0, 12, 0]}, { "name": "body", "parent": "waist", - "pivot": [0, 24, 0], + "pivot": [0, 13, 0], "cubes": [ {"origin": [-6, 21, -1.5], "size": [12, 3, 3], "uv": [0, 26]}, {"origin": [-3, 14, -1], "size": [2, 7, 2], "uv": [16, 0]}, @@ -204,50 +205,50 @@ }, { "name": "head", - "parent": "body", - "pivot": [0, 24, 0], + "parent": "waist", + "pivot": [0, 12, 0], "cubes": [{"origin": [-1, 24, -1], "size": [2, 7, 2], "uv": [0, 0]}] }, { "name": "hat", "parent": "head", - "pivot": [0, 24, 0], + "pivot": [0, 12, 0], "cubes": [ {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [32, 0]} ] }, { - "name": "leftarm", - "parent": "body", + "name": "rightarm", + "parent": "waist", "mirror": true, - "pivot": [5, 22, 0], + "pivot": [5, 10, 0], "cubes": [ {"origin": [5, 12, -1], "size": [2, 12, 2], "uv": [32, 16]} ] }, - {"name": "leftitem", "parent": "leftarm", "pivot": [6, 15, 1]}, + {"name": "rightitem", "parent": "leftarm", "pivot": [6, 15, 1]}, { - "name": "leftleg", - "parent": "body", + "name": "rightleg", + "parent": "waist", "mirror": true, - "pivot": [1.9, 12, 0], + "pivot": [1.9, 1, 0], "cubes": [ {"origin": [0.9, 1, -1], "size": [2, 11, 2], "uv": [40, 16]} ] }, { - "name": "rightarm", - "parent": "body", - "pivot": [-5, 22, 0], + "name": "leftarm", + "parent": "waist", + "pivot": [-5, 10, 0], "cubes": [ {"origin": [-7, 12, -1], "size": [2, 12, 2], "uv": [24, 0]} ] }, - {"name": "rightitem", "parent": "rightarm", "pivot": [-6, 15, 1]}, + {"name": "leftitem", "parent": "rightarm", "pivot": [-6, 15, 1]}, { - "name": "rightleg", - "parent": "body", - "pivot": [-1.9, 12, 0], + "name": "leftleg", + "parent": "waist", + "pivot": [-1.9, 1, 0], "cubes": [ {"origin": [-2.9, 1, -1], "size": [2, 11, 2], "uv": [8, 0]} ] diff --git a/prismarine-viewer/viewer/lib/entity/objModels.js b/prismarine-viewer/viewer/lib/entity/objModels.js index edff440b9..5af296067 100644 --- a/prismarine-viewer/viewer/lib/entity/objModels.js +++ b/prismarine-viewer/viewer/lib/entity/objModels.js @@ -1 +1,2 @@ export * as externalModels from './exportedModels' +export * as armorModels from './armorModels'