feat: support fetching skins by username for players! Use popular skinview3 for rendering players, so things like ears should render propertly

feat: Add support for head rotation.
feat: add `window.cursorEntity` for inspecting or watching entity props you're looking at
fix: fix entity in playground
This commit is contained in:
Vitaly 2024-01-17 06:32:38 +05:30
commit 86a4ac68f5
13 changed files with 333 additions and 87 deletions

View file

@ -23,7 +23,7 @@ const buildOptions = {
entryPoints: [path.join(__dirname, './viewer/lib/worker.js')],
minify: true,
logLevel: 'info',
drop: watch ? [
drop: !watch ? [
'debugger'
] : [],
sourcemap: 'linked',
@ -121,7 +121,7 @@ const buildOptions = {
],
}
if (process.argv.includes('-w')) {
if (watch) {
const ctx = await context(buildOptions)
await ctx.watch()
} else {

View file

@ -50,6 +50,9 @@ const buildOptions = {
},
inject: [],
metafile: true,
loader: {
'.png': 'dataurl',
},
plugins: [
{
name: 'minecraft-data',

View file

@ -10,6 +10,8 @@ import { GUI } from 'lil-gui'
import { toMajor } from '../viewer/lib/version'
import { loadScript } from '../viewer/lib/utils'
import JSZip from 'jszip'
import { TWEEN_DURATION } from '../viewer/lib/entities'
import Entity from '../viewer/lib/entity/Entity'
globalThis.THREE = THREE
//@ts-ignore
@ -82,14 +84,15 @@ async function main () {
gui.add(params, 'block', mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b)))
const metadataGui = gui.add(params, 'metadata')
gui.add(params, 'supportBlock')
gui.add(params, 'entity', mcData.entitiesArray.map(b => b.name)).listen()
gui.add(params, 'entity', mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b))).listen()
gui.add(params, 'removeEntity')
gui.add(params, 'entityRotate')
gui.add(params, 'skip')
gui.add(params, 'playSound')
gui.add(params, 'blockIsomorphicRenderBundle')
gui.open(false)
let folder = gui.addFolder('metadata')
let metadataFolder = gui.addFolder('metadata')
let entityRotationFolder = gui.addFolder('entity rotation')
const Chunk = ChunkLoader(version)
const Block = BlockLoader(version)
@ -293,19 +296,44 @@ async function main () {
controls.update()
let blockProps = {}
let entityOverrides = {}
const getBlock = () => {
return mcData.blocksByName[params.block || 'air']
}
const entityUpdateShared = () => {
viewer.entities.clear()
if (!params.entity) return
worldView.emit('entity', {
id: 'id', name: params.entity, pos: targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: 'username', yaw: Math.PI, pitch: 0
})
const enableSkeletonDebug = (obj) => {
const {children, isSkeletonHelper} = obj
if (!Array.isArray(children)) return
if (isSkeletonHelper) {
obj.visible = true
return
}
for (const child of children) {
if (typeof child === 'object') enableSkeletonDebug(child)
}
}
enableSkeletonDebug(viewer.entities.entities['id'])
setTimeout(() => {
viewer.update()
viewer.render()
}, TWEEN_DURATION)
}
const onUpdate = {
block () {
folder.destroy()
metadataFolder.destroy()
const block = mcData.blocksByName[params.block]
if (!block) return
const props = new Block(block.id, 0, 0).getProperties()
//@ts-ignore
const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {}
folder = gui.addFolder('metadata')
metadataFolder = gui.addFolder('metadata')
if (states) {
for (const state of states) {
let defaultValue
@ -328,25 +356,31 @@ async function main () {
}
blockProps[state.name] = defaultValue
if (state.type === 'enum') {
folder.add(blockProps, state.name, state.values)
metadataFolder.add(blockProps, state.name, state.values)
} else {
folder.add(blockProps, state.name)
metadataFolder.add(blockProps, state.name)
}
}
} else {
for (const [name, value] of Object.entries(props)) {
blockProps[name] = value
folder.add(blockProps, name)
metadataFolder.add(blockProps, name)
}
}
folder.open()
metadataFolder.open()
},
entity () {
viewer.entities.clear()
entityUpdateShared()
if (!params.entity) return
worldView.emit('entity', {
id: 'id', name: params.entity, pos: targetPos.offset(0, 1, 0), width: 1, height: 1, username: 'username'
})
Entity.getStaticData(params.entity)
entityRotationFolder.destroy()
entityRotationFolder = gui.addFolder('entity rotation')
entityRotationFolder.add(params, 'entityRotate')
entityRotationFolder.open()
},
supportBlock () {
viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
}
}
@ -357,7 +391,7 @@ async function main () {
if (metadataUpdate) {
block = new Block(blockId, 0, params.metadata)
Object.assign(blockProps, block.getProperties())
for (const _child of folder.children) {
for (const _child of metadataFolder.children) {
const child = _child as import('lil-gui').Controller
child.updateDisplay()
}
@ -373,18 +407,21 @@ async function main () {
//@ts-ignore
viewer.setBlockStateId(targetPos, block.stateId)
console.log('up', block.stateId)
console.log('up stateId', block.stateId)
params.metadata = block.metadata
metadataGui.updateDisplay()
// viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
if (!skipQs) {
setQs()
}
}
gui.onChange(({ property }) => {
if (property === 'camera') return
onUpdate[property]?.()
applyChanges(property === 'metadata')
gui.onChange(({ property, object }) => {
if (object === params) {
if (property === 'camera') return
onUpdate[property]?.()
applyChanges(property === 'metadata')
} else {
applyChanges()
}
})
viewer.waitForChunksToRender().then(async () => {
await new Promise(resolve => {
@ -395,12 +432,8 @@ async function main () {
}
applyChanges(true)
gui.openAnimated()
// worldView.emit('entity', {
// id: 'id', name: 'player', pos: targetPos.offset(1, -2, 0), width: 1, height: 1, username: 'username'
// })
})
// Browser animation loop
const animate = () => {
// if (controls) controls.update()
// worldView.updatePosition(controls.target)
@ -455,11 +488,5 @@ async function main () {
params.playSound()
}
}, { capture: true })
setTimeout(() => {
// worldView.emit('entity', {
// id: 'id', name: 'player', pos: center.offset(1, -2, 0), width: 1, height: 1, username: 'username'
// })
}, 1500)
}
main()

View file

@ -6,6 +6,7 @@
<style type="text/css">
html {
overflow: hidden;
background: black;
}
html, body {

View file

@ -1,12 +1,21 @@
//@ts-check
const THREE = require('three')
const TWEEN = require('@tweenjs/tween.js')
const Entity = require('./entity/Entity')
const { dispose3 } = require('./dispose')
const EventEmitter = require('events')
import { PlayerObject } from 'skinview3d'
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
// todo replace with url
import stevePng from 'minecraft-assets/minecraft-assets/data/1.20.2/entity/player/wide/steve.png'
function getUsernameTexture(username, { fontFamily = 'sans-serif' }) {
export const TWEEN_DURATION = 50 // todo should be 100
function getUsernameTexture (username, { fontFamily = 'sans-serif' }) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Could not get 2d context')
const fontSize = 50
const padding = 5
@ -27,11 +36,12 @@ function getUsernameTexture(username, { fontFamily = 'sans-serif' }) {
return canvas
}
function getEntityMesh (entity, scene, options) {
function getEntityMesh (entity, scene, options, overrides) {
if (entity.name) {
try {
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
const e = new Entity('1.16.4', entity.name.toLowerCase(), scene)
const entityName = entity.name.toLowerCase()
const e = new Entity('1.16.4', entityName, scene, overrides)
if (entity.username !== undefined) {
const canvas = getUsernameTexture(entity.username, options)
@ -58,8 +68,9 @@ function getEntityMesh (entity, scene, options) {
return cube
}
class Entities {
export class Entities extends EventEmitter {
constructor (scene) {
super()
this.scene = scene
this.entities = {}
this.entitiesOptions = {}
@ -73,31 +84,132 @@ class Entities {
this.entities = {}
}
update (entity) {
updatePlayerSkin (entityId, skinUrl, capeUrl = undefined) {
const getPlayerObject = () => {
/** @type {PlayerObject} */
return this.entities[entityId]?.playerObject
}
let playerObject = getPlayerObject()
if (!playerObject) return
loadImage(skinUrl).then((image) => {
playerObject = getPlayerObject()
if (!playerObject) return
const skinCanvas = document.createElement('canvas')
loadSkinToCanvas(skinCanvas, image)
const skinTexture = new THREE.CanvasTexture(skinCanvas)
skinTexture.magFilter = THREE.NearestFilter
skinTexture.minFilter = THREE.NearestFilter
skinTexture.needsUpdate = true
//@ts-ignore
playerObject.skin.map = skinTexture
playerObject.skin.modelType = inferModelType(skinCanvas)
const earsCanvas = document.createElement('canvas')
loadEarsToCanvasFromSkin(earsCanvas, skinCanvas)
const earsTexture = new THREE.CanvasTexture(earsCanvas)
earsTexture.magFilter = THREE.NearestFilter
earsTexture.minFilter = THREE.NearestFilter
earsTexture.needsUpdate = true
//@ts-ignore
playerObject.ears.map = earsTexture
})
if (capeUrl) {
loadImage(capeUrl).then((capeImage) => {
playerObject = getPlayerObject()
if (!playerObject) return
const capeCanvas = document.createElement('canvas')
loadCapeToCanvas(capeCanvas, capeImage)
const capeTexture = new THREE.CanvasTexture(capeCanvas)
capeTexture.magFilter = THREE.NearestFilter
capeTexture.minFilter = THREE.NearestFilter
capeTexture.needsUpdate = true
//@ts-ignore
playerObject.cape.map = capeTexture
//@ts-ignore
playerObject.elytra.map = capeTexture
playerObject.skin.rotation.y = Math.PI
})
} else {
playerObject.backEquipment = null
playerObject.elytra.map = null
if (playerObject.cape.map) {
playerObject.cape.map.dispose()
}
playerObject.cape.map = null
}
}
update (/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
if (!this.entities[entity.id]) {
const mesh = getEntityMesh(entity, this.scene, this.entitiesOptions)
const group = new THREE.Group()
let mesh
if (entity.name === 'player') {
const wrapper = new THREE.Group()
const playerObject = new PlayerObject()
playerObject.position.set(0, 16, 0)
//@ts-ignore
wrapper.add(playerObject)
const scale = 1 / 16
wrapper.scale.set(scale, scale, scale)
//@ts-ignore
group.playerObject = playerObject
wrapper.rotation.set(0, Math.PI, 0)
mesh = wrapper
} else {
mesh = getEntityMesh(entity, this.scene, this.entitiesOptions, overrides)
}
if (!mesh) return
this.entities[entity.id] = mesh
this.scene.add(mesh)
// set initial position so there are no weird jumps update after
group.position.set(entity.pos.x, entity.pos.y, entity.pos.z)
const boxHelper = new THREE.BoxHelper(mesh,
entity.type === 'hostile' ? 0xff0000 :
entity.type === 'mob' ? 0x00ff00 :
entity.type === "player" ? 0x0000ff :
0xffa500
)
group.add(mesh)
group.add(boxHelper)
this.scene.add(group)
this.entities[entity.id] = group
this.emit('add', entity)
if (entity.name === 'player') {
this.updatePlayerSkin(entity.id, stevePng)
}
}
const e = this.entities[entity.id]
if (e.playerObject && overrides?.rotation?.head) {
/** @type {PlayerObject} */
const playerObject = e.playerObject
const headRotationDiff = overrides.rotation.head.y ? overrides.rotation.head.y - entity.yaw : 0
playerObject.skin.head.rotation.y = -headRotationDiff
playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0
}
if (entity.delete) {
this.emit('remove', entity)
this.scene.remove(e)
dispose3(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 }, 50).start()
new TWEEN.Tween(e.position).to({ x: entity.pos.x, y: entity.pos.y, z: entity.pos.z }, TWEEN_DURATION).start()
}
if (entity.yaw) {
const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
const dy = 2 * da % (Math.PI * 2) - da
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, 50).start()
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, TWEEN_DURATION).start()
}
}
}
module.exports = { Entities }

View file

@ -1,3 +1,4 @@
//@ts-check
/* global THREE */
const entities = require('./entities.json')
@ -128,7 +129,7 @@ function addCube (attr, boneId, bone, cube, texWidth = 64, texHeight = 64) {
}
}
function getMesh (texture, jsonModel) {
function getMesh (texture, jsonModel, overrides = {}) {
const bones = {}
const geoData = {
@ -156,6 +157,12 @@ function getMesh (texture, jsonModel) {
bone.rotation.y = -jsonBone.rotation[1] * Math.PI / 180
bone.rotation.z = -jsonBone.rotation[2] * Math.PI / 180
}
if (overrides.rotation?.[jsonBone.name]) {
bone.rotation.x -= (overrides.rotation[jsonBone.name].x ?? 0) * Math.PI / 180
bone.rotation.y -= (overrides.rotation[jsonBone.name].y ?? 0) * Math.PI / 180
bone.rotation.z -= (overrides.rotation[jsonBone.name].z ?? 0) * Math.PI / 180
}
bone.name = `bone_${jsonBone.name}`
bones[jsonBone.name] = bone
if (jsonBone.cubes) {
@ -169,7 +176,9 @@ function getMesh (texture, jsonModel) {
const rootBones = []
for (const jsonBone of jsonModel.bones) {
if (jsonBone.parent) bones[jsonBone.parent].add(bones[jsonBone.name])
else rootBones.push(bones[jsonBone.name])
else {
rootBones.push(bones[jsonBone.name])
}
}
const skeleton = new THREE.Skeleton(Object.values(bones))
@ -189,6 +198,10 @@ function getMesh (texture, jsonModel) {
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
@ -208,14 +221,17 @@ const entitiesMap = {
// const unknownEntitiesSet = new Set()
class Entity {
constructor (version, type, scene) {
let mappedValue = entitiesMap[type]
// todo is it okay?
if (mappedValue === null) return
else if (mappedValue) type = mappedValue
const getEntity = (name) => {
let mappedValue = entitiesMap[name]
if (mappedValue) name = mappedValue
return entities[name]
}
class Entity {
constructor (version, type, scene, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
const e = getEntity(type)
const e = entities[type]
if (!e) {
// if (unknownEntitiesSet.has(type)) throw new Error('ignore...')
// unknownEntitiesSet.add(type)
@ -224,16 +240,27 @@ class Entity {
this.mesh = new THREE.Object3D()
for (const [name, jsonModel] of Object.entries(e.geometry)) {
const texture = e.textures[name]
const texture = overrides.textures?.[name] ?? e.textures[name]
if (!texture) continue
// console.log(JSON.stringify(jsonModel, null, 2))
const mesh = getMesh(texture.replace('textures', 'textures/' + version) + '.png', jsonModel)
/* const skeletonHelper = new THREE.SkeletonHelper( mesh )
const mesh = getMesh(texture.replace('textures', 'textures/' + version) + '.png', jsonModel, overrides)
mesh.name = `geometry_${name}`
const skeletonHelper = new THREE.SkeletonHelper(mesh)
//@ts-ignore
skeletonHelper.material.linewidth = 2
scene.add( skeletonHelper ) */
this.mesh.add(skeletonHelper)
skeletonHelper.visible = false
this.mesh.add(mesh)
}
}
static getStaticData (name) {
const e = getEntity(name)
if (!e) throw new Error(`Unknown entity ${name}`)
return {
boneNames: Object.values(e.geometry).flatMap(x => x.name)
}
}
}
module.exports = Entity

View file

@ -48,7 +48,8 @@ export const loadScript = async function (/** @type {string} */scriptSrc) {
})
scriptElement.onerror = (error) => {
reject(error)
reject(new Error(error.message))
scriptElement.remove()
}
document.head.appendChild(scriptElement)

View file

@ -5,6 +5,7 @@ import { WorldRenderer } from './worldrenderer'
import { Entities } from './entities'
import { Primitives } from './primitives'
import { getVersion } from './version'
import EventEmitter from 'events'
export class Viewer {
scene: THREE.Scene
@ -77,7 +78,15 @@ export class Viewer {
}
updateEntity (e) {
this.entities.update(e)
this.entities.update(e, this.processEntityOverrides(e, {
rotation: {
head: {
x: e.headPitch ?? e.pitch,
y: e.headYaw,
z: 0
}
}
}))
}
updatePrimitive (p) {
@ -122,7 +131,7 @@ export class Viewer {
}
// todo type
listen (emitter) {
listen (emitter: EventEmitter) {
emitter.on('entity', (e) => {
this.updateEntity(e)
})
@ -154,7 +163,7 @@ export class Viewer {
emitter.emit('listening')
this.domElement.addEventListener('pointerdown', (evt) => {
this.domElement.addEventListener?.('pointerdown', (evt) => {
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
mouse.x = (evt.clientX / this.domElement.clientWidth) * 2 - 1

View file

@ -1,5 +1,7 @@
// global variables useful for debugging
import { getEntityCursor } from './worldInteractions'
// Object.defineProperty(window, 'cursorBlock', )
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
@ -7,3 +9,7 @@ window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
if (!newPos) return
return bot.world.getBlock(newPos)
}
window.cursorEntity = () => {
return getEntityCursor()
}

55
src/entities.ts Normal file
View file

@ -0,0 +1,55 @@
import { Entity } from 'prismarine-entity'
import { TextureLoader } from 'three'
customEvents.on('gameLoaded', () => {
const enableSkeletonHelpers = localStorage.enableSkeletonHelpers ?? false
const entityData = (e: Entity) => {
if (!e.username) return
// const firstRender = !!window.debugEntityMetadata
window.debugEntityMetadata ??= {}
window.debugEntityMetadata[e.username] = e
}
const entityFirstRendered = (e) => {
const mesh = viewer.entities.entities[e.id]
if (!mesh) throw new Error('mesh still not loaded')
const visitChildren = (obj) => {
if (!Array.isArray(obj?.children)) return
const { children, isSkeletonHelper } = obj
if (isSkeletonHelper && enableSkeletonHelpers) {
obj.visible = true
}
if (e.type === 'player' && e.username) {
if (e.name === 'geometry_default') {
console.log('request', e.uuid)
new TextureLoader().load(`https://mulv.tycrek.dev/api/lookup?username=${e.username}&type=skin`, (texture) => {
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.flipY = false
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
obj.material.map = texture
})
}
if (e.name === 'geometry_cape') {
// todo
}
}
for (const child of children) {
if (typeof child === 'object') visitChildren(child)
}
}
visitChildren(mesh)
}
viewer.entities.addListener('add', entityFirstRendered)
for (const entity of Object.values(bot.entities)) {
if (entity !== bot.entity) {
entityData(entity)
}
}
bot.on('entitySpawn', entityData)
bot.on('entityUpdate', entityData)
})

View file

@ -1,4 +1,4 @@
//@ts-nocheck
import EventEmitter from 'events'
window.bot = undefined
window.THREE = undefined
@ -6,3 +6,4 @@ window.localServer = undefined
window.worldView = undefined
window.viewer = undefined
window.loadedData = undefined
window.customEvents = new EventEmitter()

View file

@ -4,6 +4,7 @@ import './styles.css'
import './globals'
import 'iconify-icon'
import './devtools'
import './entities'
import initCollisionShapes from './getCollisionShapes'
import { onGameLoad } from './playerWindows'
@ -43,6 +44,8 @@ import worldInteractions from './worldInteractions'
import * as THREE from 'three'
import MinecraftData, { versionsByMinecraftVersion } from 'minecraft-data'
import debug from 'debug'
import _ from 'lodash-es'
import { initVR } from './vr'
import {
@ -69,12 +72,9 @@ import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalS
import defaultServerOptions from './defaultLocalServerOptions'
import dayCycle from './dayCycle'
import _ from 'lodash-es'
import { genTexturePackTextures, watchTexturepackInViewer } from './texturePack'
import { connectToPeer } from './localServerMultiplayer'
import CustomChannelClient from './customClient'
import debug from 'debug'
import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
import { registerServiceWorker } from './serviceWorker'
import { appStatusState, lastConnectOptions } from './react/AppStatusProvider'
@ -85,11 +85,9 @@ import { loadInMemorySave } from './react/SingleplayerProvider'
// side effects
import { downloadSoundsIfNeeded } from './soundSystem'
import EventEmitter from 'events'
window.debug = debug
window.THREE = THREE
window.customEvents = new EventEmitter()
window.beforeRenderFrame = []
// ACTUAL CODE

View file

@ -83,27 +83,7 @@ class WorldInteraction {
if (!isGameActive(true)) return
this.buttons[e.button] = true
const entity = bot.nearestEntity((e) => {
if (e.position.distanceTo(bot.entity.position) <= (bot.game.gameMode === 'creative' ? 5 : 3)) {
const dir = getViewDirection(bot.entity.pitch, bot.entity.yaw)
const { width, height } = e
const { x: eX, y: eY, z: eZ } = e.position
const { x: bX, y: bY, z: bZ } = bot.entity.position
const box = new THREE.Box3(
new THREE.Vector3(eX - width / 2, eY, eZ - width / 2),
new THREE.Vector3(eX + width / 2, eY + height, eZ + width / 2)
)
const r = new THREE.Raycaster(
new THREE.Vector3(bX, bY + 1.52, bZ),
new THREE.Vector3(dir.x, dir.y, dir.z)
)
const int = r.ray.intersectBox(box, new THREE.Vector3(eX, eY, eZ))
return int !== null
}
return false
})
const entity = getEntityCursor()
if (entity) {
bot.attack(entity)
@ -135,9 +115,10 @@ class WorldInteraction {
const upLineMaterial = () => {
const inCreative = bot.game.gameMode === 'creative'
const pixelRatio = viewer.renderer.getPixelRatio()
this.lineMaterial = new LineMaterial({
color: inCreative ? 0x40_80_ff : 0x00_00_00,
linewidth: viewer.renderer.getPixelRatio() * 2,
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
// dashed: true,
// dashSize: 5,
})
@ -319,4 +300,29 @@ const getDataFromShape = (shape) => {
return { position, width, height, depth }
}
export const getEntityCursor = () => {
const entity = bot.nearestEntity((e) => {
if (e.position.distanceTo(bot.entity.position) <= (bot.game.gameMode === 'creative' ? 5 : 3)) {
const dir = getViewDirection(bot.entity.pitch, bot.entity.yaw)
const { width, height } = e
const { x: eX, y: eY, z: eZ } = e.position
const { x: bX, y: bY, z: bZ } = bot.entity.position
const box = new THREE.Box3(
new THREE.Vector3(eX - width / 2, eY, eZ - width / 2),
new THREE.Vector3(eX + width / 2, eY + height, eZ + width / 2)
)
const r = new THREE.Raycaster(
new THREE.Vector3(bX, bY + 1.52, bZ),
new THREE.Vector3(dir.x, dir.y, dir.z)
)
const int = r.ray.intersectBox(box, new THREE.Vector3(eX, eY, eZ))
return int !== null
}
return false
})
return entity
}
export default new WorldInteraction()