cannon js handle fast movement : class World {
    constructor() {
        this.game = new Game()
        this.scene = this.game.scene
        this.physics = this.initPhysics()
        this.physics.gravity.set(0, -10, 0)
        this.defaultMaterial = new CANNON.Material('default')
        this.raycaster = new THREE.Raycaster()
        this.mouse = new THREE.Vector2()
        this.leftPortal = []
        this.rightPortal = []
        this.portalPhysics = false
        this.collisionDetected = false
        this.virtualCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
        this.scene.add(this.virtualCamera)
        this.collisionDetected = false
        this.portalHandler = new Portal(this.scene, this.game.renderer.instance)
        this.active = ''
        this.previousContacts = []
        this.worldBounds = {
            min: new CANNON.Vec3(-100, -100, -100),
            max: new CANNON.Vec3(100, 100, 100)
        }
        const defaultContactMaterial = new CANNON.ContactMaterial(
            this.defaultMaterial,
            this.defaultMaterial,
            {
                friction: 0,
                restitution: 0
            }
        )
        this.physics.defaultContactMaterial = defaultContactMaterial
        window.addEventListener('click', this.shoot.bind(this))

        this.isHoldingCube = false
        this.holdDistance = 10
        this.pickupRange = 20

        this.holdingConstraint = null
        this.holdBody = null

        this.onKeyDown = this.onKeyDown.bind(this)
        window.addEventListener('keydown', this.onKeyDown)

        this.setWorld()
        this.createCompanionCube()
        this.loadTextures()
    }

    initPhysics() {
        let world = new CANNON.World()
        world.quatNormalizeSkip = 0
        world.quatNormalizeFast = false

        var solver = new CANNON.GSSolver()

        world.defaultContactMaterial.contactEquationStiffness = 1e9
        world.defaultContactMaterial.contactEquationRelaxation = 4

        solver.iterations = 10
        solver.tolerance = 0.1
        let split = true
        if (split)
            world.solver = new CANNON.SplitSolver(solver)
        else
            world.solver = solver

        world.gravity.set(0, -9.8, 0)
        world.broadphase = new CANNON.NaiveBroadphase()

        return world
    }

    worldToLocal(object, vector) {
        const worldInverse = new THREE.Matrix4().copy(object.matrixWorld).invert()
        return vector.clone().applyMatrix4(worldInverse)
    }

    localToWorld(object, vector) {
        return vector.clone().applyMatrix4(object.matrixWorld)
    }

    shoot(event) {
        this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
        this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1

        this.raycaster.setFromCamera({ x: 0, y: 0 }, this.game.camera.instance)

        const intersects = this.raycaster.intersectObjects([
            this.plane, this.roof, this.scene.getObjectByName('backWall'),
            this.scene.getObjectByName('frontWall'), this.scene.getObjectByName('leftWall'),
            this.scene.getObjectByName('rightWall')
        ])

        if (intersects.length > 0) {
            const intersection = intersects[0]
            const object = intersection.object

            const geometry = new THREE.CircleGeometry(5, 32)
            const material = new THREE.MeshBasicMaterial({ color: 0x000000 })
            const circle = new THREE.Mesh(geometry, material)

            const portalSize = new THREE.Vector2(10, 15)
            const halfPortalSize = portalSize.clone().multiplyScalar(0.5)
            const wallSize = new THREE.Vector2(object.geometry.parameters.width, object.geometry.parameters.height)
            const halfWallSize = wallSize.clone().multiplyScalar(0.5)

            let localPosition = this.worldToLocal(object, intersection.point)

            localPosition.x = THREE.MathUtils.clamp(localPosition.x, -halfWallSize.x + halfPortalSize.x, halfWallSize.x - halfPortalSize.x)
            localPosition.y = THREE.MathUtils.clamp(localPosition.y, -halfWallSize.y + halfPortalSize.y, halfWallSize.y - halfPortalSize.y)

            const validPoint = this.localToWorld(object, localPosition)

            circle.position.copy(validPoint)
            circle.rotation.copy(object.rotation)
            circle.scale.y = 1.5

            circle.position.addScaledVector(circle.getWorldDirection(new THREE.Vector3()), 0.01)
            this.boxBody = new CANNON.Body({
                collisionFilterGroup: GROUP_PORTAL,
                collisionFilterMask: GROUP_WALL | GROUP_DEFAULT | GROUP_CUBE | GROUP_PLAYER,
            })
            this.boxBody.mass = 0
            this.boxBody.material = this.defaultMaterial
            this.boxBody.collisionResponse = false
            this.boxBody.addShape(new CANNON.Box(new CANNON.Vec3(3.34, 5, 1.5)))
            this.boxBody.quaternion.copy(circle.quaternion)
            this.boxBody.position.copy(circle.position)
            this.boxBody.class = 'portal'
            this.boxBody.object = intersects[0].object

            let intersectionDetected = false
            if (event.button === 0) {
                this.rightPortal.forEach((rightPortal) => {
                    if (this.boxBody.object === rightPortal.physicObject.object && this.detectIntersection(this.boxBody, rightPortal.physicObject)) {
                        intersectionDetected = true
                    }
                })
            } else if (event.button === 2) {
                this.leftPortal.forEach((leftPortal) => {
                    if (this.boxBody.object === leftPortal.physicObject.object && this.detectIntersection(this.boxBody, leftPortal.physicObject)) {
                        intersectionDetected = true
                    }
                })
            }

            if (intersectionDetected) {
                return
            }

            this.physics.addBody(this.boxBody)
            circle.physicObject = this.boxBody

            const geometry1 = new THREE.PlaneGeometry(19, 19)
            let materialPortal

            if (event.button === 0) {
                this.materialBlue = new THREE.ShaderMaterial({
                    uniforms: {
                        iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                        iTime: { value: 0 }
                    },
                    vertexShader: document.getElementById('vertexShader').textContent,
                    fragmentShader: document.getElementById('fragmentShaderOrange').textContent
                })
                materialPortal = this.materialBlue
                this.boxBody.ref = 'left'
                this.leftPortal.push(circle)
            } else if (event.button === 2) {
                this.materialOrange = new THREE.ShaderMaterial({
                    uniforms: {
                        iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                        iTime: { value: 0 }
                    },
                    vertexShader: document.getElementById('vertexShader').textContent,
                    fragmentShader: document.getElementById('fragmentShaderBlue').textContent
                })
                materialPortal = this.materialOrange
                this.boxBody.ref = 'right'
                this.rightPortal.push(circle)
            }

            const planePortal = new THREE.Mesh(geometry1, materialPortal)
            planePortal.position.copy(circle.position)
            planePortal.rotation.copy(circle.rotation)
            planePortal.position.addScaledVector(circle.getWorldDirection(new THREE.Vector3()), 0.01)

            this.scene.add(planePortal)
            circle.torus = planePortal
        }
    }

    detectIntersection(bodyA, bodyB, buffer = 4) {
        const shapeA = bodyA.shapes[0]
        const shapeB = bodyB.shapes[0]

        const posA = bodyA.position
        const posB = bodyB.position

        const halfSizeA = shapeA.halfExtents
        const halfSizeB = shapeB.halfExtents

        return (
            Math.abs(posA.x - posB.x) < (halfSizeA.x + halfSizeB.x + buffer) &&
            Math.abs(posA.y - posB.y) < (halfSizeA.y + halfSizeB.y + buffer) &&
            Math.abs(posA.z - posB.z) < (halfSizeA.z + halfSizeB.z + buffer)
        )
    }

    calculateAngle(v1, v2) {
        const dot = v1.dot(v2)
        const angle = Math.acos(dot / (v1.length() * v2.length()))
        return angle
    }

    isBodyOutOfBounds(body) {
        const position = body.position
        return (
            position.x < this.worldBounds.min.x ||
            position.x > this.worldBounds.max.x ||
            position.y < this.worldBounds.min.y ||
            position.y > this.worldBounds.max.y ||
            position.z < this.worldBounds.min.z ||
            position.z > this.worldBounds.max.z
        )
    }

    setWorld() {
        const floorShape = new CANNON.Plane()
        const floorBody = new CANNON.Body({
            collisionFilterGroup: GROUP_WALL,
            collisionFilterMask: GROUP_PLAYER | GROUP_CUBE | GROUP_PORTAL | GROUP_DEFAULT,
        })
        floorBody.mass = 0
        floorBody.addShape(floorShape)
        floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5)
        this.physics.addBody(floorBody)

        const floorTexture = new THREE.TextureLoader().load('/textures/concrete_modular_floor001c.png')
        floorTexture.repeat.set(4, 4)
        floorTexture.wrapS = THREE.RepeatWrapping
        floorTexture.wrapT = THREE.RepeatWrapping

        const floorMaterial = new THREE.MeshStandardMaterial({
            map: floorTexture,
            roughness: 0.2,
            metalness: 0.1
        })

        this.plane = new THREE.Mesh(
            new THREE.PlaneGeometry(200, 200),
            floorMaterial
        )
        this.plane.rotation.x = -Math.PI / 2
        this.plane.receiveShadow = true
        this.plane.position.set(0, 0, 0)
        this.scene.add(this.plane)
        this.plane.physicObject = floorBody

        const wallShape = new CANNON.Box(new CANNON.Vec3(100, 50, 1))

        const createWall = (width, height, depth, color, position, rotation) => {
            const wallTexture = new THREE.TextureLoader().load('/textures/concrete_modular_wall001a.png')
            wallTexture.repeat.set(4, 3)
            wallTexture.wrapS = THREE.RepeatWrapping
            wallTexture.wrapT = THREE.RepeatWrapping

            const wallMaterial = new THREE.MeshStandardMaterial({
                map: wallTexture,
                side: THREE.DoubleSide
            })

            const wallMesh = new THREE.Mesh(
                new THREE.PlaneGeometry(width, height),
                wallMaterial
            )
            wallMesh.position.set(position.x, position.y, position.z)
            wallMesh.rotation.set(rotation.x, rotation.y, rotation.z)
            this.scene.add(wallMesh)

            const wallBody = new CANNON.Body({
                collisionFilterGroup: GROUP_WALL,
                collisionFilterMask: GROUP_PLAYER | GROUP_CUBE | GROUP_PORTAL | GROUP_DEFAULT,
            })
            wallBody.mass = 0
            wallBody.addShape(wallShape)
            wallBody.position.copy(wallMesh.position)
            wallBody.quaternion.copy(wallMesh.quaternion)
            this.physics.addBody(wallBody)

            wallMesh.physicObject = wallBody
            return wallMesh
        }

        const wallHeight = 100
        const wallYPosition = wallHeight / 2

        let backWallMesh = createWall(200, wallHeight, 1, 0xf0ffff, { x: 0, y: wallYPosition, z: 100 }, { x: 0, y: Math.PI, z: 0 })
        backWallMesh.name = 'backWall'

        let frontWallMesh = createWall(200, wallHeight, 1, 0xff0fff, { x: 0, y: wallYPosition, z: -100 }, { x: 0, y: 0, z: 0 })
        frontWallMesh.name = 'frontWall'

        let leftWallMesh = createWall(200, wallHeight, 1, 0xfff0ff, { x: -100, y: wallYPosition, z: 0 }, { x: 0, y: Math.PI / 2, z: 0 })
        leftWallMesh.name = 'leftWall'

        let rightWallMesh = createWall(200, wallHeight, 1, 0xffff0f, { x: 100, y: wallYPosition, z: 0 }, { x: 0, y: -Math.PI / 2, z: 0 })
        rightWallMesh.name = 'rightWall'

        const roofTexture = new THREE.TextureLoader().load('/textures/concrete_modular_ceiling001a.png')
        roofTexture.repeat.set(4, 4)
        roofTexture.wrapS = THREE.RepeatWrapping
        roofTexture.wrapT = THREE.RepeatWrapping

        const roofMaterial = new THREE.MeshStandardMaterial({
            map: roofTexture,
        })

        this.roof = new THREE.Mesh(
            new THREE.PlaneGeometry(200, 200),
            roofMaterial
        )
        this.roof.rotation.x = Math.PI / 2
        this.roof.position.y = wallHeight
        this.scene.add(this.roof)

        const roofShape = new CANNON.Plane()
        const roofBody = new CANNON.Body({
            collisionFilterGroup: GROUP_WALL,
            collisionFilterMask: GROUP_PLAYER | GROUP_CUBE | GROUP_PORTAL | GROUP_DEFAULT,
        })
        roofBody.mass = 0
        roofBody.addShape(roofShape)
        roofBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI * 0.5)
        roofBody.position.y = wallHeight
        this.physics.addBody(roofBody)

        this.roof.physicObject = roofBody
    }

    loadTextures() {
        this.game.resources.on('ready', () => {
            // Load resources if needed
        })
    }

    renderPortal() {
        this.game.renderer.instance.clear()
        this.game.camera.instance.updateMatrixWorld(true)
        this.portalHandler.render(this.game.camera.instance, 0, this.virtualCamera, [this.rightPortal[0], this.leftPortal[0]], this.game.camera.instance.matrixWorld.clone(), this.game.camera.instance.projectionMatrix.clone())
    }

    isWithinFunnel(coneOrigin, coneDirection, objectPosition, coneAngle, coneHeight = 10) {
        const upDirection = new THREE.Vector3(0, 1, 0)
        const facingUpward = coneDirection.dot(upDirection) > 0.7

        if (!facingUpward) {
            return false
        }

        const toObject = new THREE.Vector3().subVectors(objectPosition, coneOrigin)

        const projectedDistance = toObject.dot(coneDirection)

        if (projectedDistance > coneHeight || projectedDistance < 0) {
            return false
        }

        const directionToObject = toObject.normalize()
        const angle = coneDirection.angleTo(directionToObject)

        return angle < coneAngle
    }

    funnelObjectTowardsPortal(objectPosition, coneOrigin) {
        const toConeOrigin = new THREE.Vector3().copy(coneOrigin).sub(objectPosition)
        toConeOrigin.y = 0

        const direction = toConeOrigin.normalize()

        const funnelSpeed = 0.2

        const newPosition = new THREE.Vector3().copy(objectPosition)
        newPosition.x += direction.x * funnelSpeed
        newPosition.z += direction.z * funnelSpeed

        this.game.controls.cameraBody.position.set(newPosition.x, this.game.controls.cameraBody.position.y, newPosition.z)
    }

    update() {
        this.physics.step(1 / 75, this.game.time.delta, 3)

        this.contact = false

        if (this.rightPortal.length > 0) {
            document.querySelector('.blue').classList.add('active')
        } else {
            document.querySelector('.blue').classList.remove('active')
        }

        if (this.leftPortal.length > 0) {
            document.querySelector('.orange').classList.add('active')
        } else {
            document.querySelector('.orange').classList.remove('active')
        }

        if (this.rightPortal.length > 0 && this.leftPortal.length > 0) {
            const funnelConeAngle = Math.PI / 6
            const playerPosition = this.game.camera.instance.position

            this.leftPortal.forEach((portal) => {
                const coneOrigin = portal.position
                const coneDirection = new THREE.Vector3()
                portal.getWorldDirection(coneDirection)

                if (this.isWithinFunnel(coneOrigin, coneDirection, playerPosition, funnelConeAngle)) {
                    this.funnelObjectTowardsPortal(playerPosition, coneOrigin)
                }
            })

            this.rightPortal.forEach((portal) => {
                const coneOrigin = portal.position
                const coneDirection = new THREE.Vector3()
                portal.getWorldDirection(coneDirection)

                if (this.isWithinFunnel(coneOrigin, coneDirection, playerPosition, funnelConeAngle)) {
                    this.funnelObjectTowardsPortal(playerPosition, coneOrigin)
                }
            })
        }

        this.physics.contacts.forEach((contact) => {
            let bodyA = contact.bi
            let bodyB = contact.bj
            if (((bodyA.class == 'camera' && bodyB.class == 'portal') || (bodyB.class == 'camera' && bodyA.class == 'portal')) && (this.rightPortal.length > 0 && this.leftPortal.length > 0)) {
                this.contact = true
            }
        })

        if (this.materialBlue) {
            this.materialBlue.uniforms.iTime.value += 0.05
        }
        if (this.materialOrange) {
            this.materialOrange.uniforms.iTime.value += 0.05
        }

        if (this.isBodyOutOfBounds(this.game.controls.cameraBody)) {
            this.game.controls.cameraBody.sleep()
            this.game.controls.cameraBody.position.set(20, 10, 0)
            this.game.controls.cameraBody.wakeUp()
        }

        if (this.rightPortal.length > 0 && this.leftPortal.length > 0) {
            this.rightPortal[0].pair = this.leftPortal[0]
            this.leftPortal[0].pair = this.rightPortal[0]
            this.game.camera.instance.layers.set(0)
            this.renderPortal()
            this.game.renderer.instance.clearDepth()
            this.game.camera.instance.layers.set(1)
            this.game.renderer.instance.render(this.scene, this.game.camera.instance)
        } else {
            this.tmpScene = new THREE.Scene()
            this.tmpScene.children = this.scene.children.filter((child) => child.class !== 'player')
            this.game.camera.instance.layers.set(0)
            this.game.renderer.instance.render(this.tmpScene, this.game.camera.instance)
            this.game.renderer.instance.clearDepth()
            this.game.camera.instance.layers.set(1)
            this.game.renderer.instance.render(this.tmpScene, this.game.camera.instance)
        }

        this.managePortals()

        if (this.rightPortal.length > 0 && this.leftPortal.length > 0) {
            this.checkCubePortalTeleport()

            let cubePortalContact = false
            let playerPortalContact = false

            for (let i = 0; i < this.physics.contacts.length; i++) {
                const contact = this.physics.contacts[i]
                const bodyA = contact.bi
                const bodyB = contact.bj

                if ((bodyA.class === 'companionCube' && bodyB.class === 'portal') ||
                    (bodyA.class === 'portal' && bodyB.class === 'companionCube')) {
                    this.companionCubeBody.collisionFilterMask &= ~GROUP_WALL
                    cubePortalContact = true
                }

                if ((bodyA.class === 'camera' && bodyB.class === 'portal') ||
                    (bodyA.class === 'portal' && bodyB.class === 'camera')) {
                    this.game.controls.cameraBody.collisionFilterMask &= ~GROUP_WALL
                    playerPortalContact = true
                }

                if ((bodyA.class === 'camera' && bodyB.ref === 'right') ||
                    (bodyA.ref === 'right' && bodyB.class === 'camera')) {
                    this.checkLeftPortalTeleport()
                    break
                }
            }

            if (!cubePortalContact) {
                this.companionCubeBody.collisionFilterMask = GROUP_DEFAULT | GROUP_WALL | GROUP_PORTAL | GROUP_PLAYER
            }

            if (!playerPortalContact) {
                this.game.controls.cameraBody.collisionFilterMask = GROUP_DEFAULT | GROUP_WALL | GROUP_PORTAL | GROUP_CUBE
            }

            for (let i = 0; i < this.physics.contacts.length; i++) {
                const contact = this.physics.contacts[i]
                const bodyA = contact.bi
                const bodyB = contact.bj

                if ((bodyA.class === 'camera' && bodyB.ref === 'left') || (bodyA.ref === 'left' && bodyB.class === 'camera')) {
                    this.checkRightPortalTeleport()
                    break
                }
            }
        } else {
            this.resetPortalColors()
        }

        if (!this.collisionDetected) {
            this.game.controls.cameraBody.wakeUp()
        }

        if (this.holdBody) {
            const holdPosition = this.getHoldPosition()
            this.holdBody.position.copy(holdPosition)
            this.holdBody.quaternion.copy(this.game.camera.instance.quaternion)
        }

        if (this.companionCube) {
            this.companionCube.position.copy(this.companionCubeBody.position)
            this.companionCube.quaternion.copy(this.companionCubeBody.quaternion)
        }
    }

    managePortals() {
        this.leftPortal.forEach((portalData) => {
            this.scene.add(portalData)
        })

        if (this.leftPortal.length > 1) {
            const circleToRemove = this.leftPortal.shift()
            this.scene.remove(circleToRemove.box)
            this.scene.remove(circleToRemove.torus)
            this.physics.remove(circleToRemove.physicObject)
            this.scene.remove(circleToRemove)
        }

        this.rightPortal.forEach((portalData) => {
            this.scene.add(portalData)
        })

        if (this.rightPortal.length > 1) {
            const circleToRemove = this.rightPortal.shift()
            this.scene.remove(circleToRemove.box)
            this.scene.remove(circleToRemove.torus)
            this.physics.remove(circleToRemove.physicObject)
            this.scene.remove(circleToRemove)
        }
    }

    teleport(sourcePortal, camera, body) {
        const halfTurn = new THREE.Quaternion()
        halfTurn.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI)

        const forwardDirection = new THREE.Vector3()
        camera.getWorldDirection(forwardDirection)

        const relativePos = sourcePortal.worldToLocal(camera.position.clone())
        relativePos.applyQuaternion(halfTurn)
        const newPos = sourcePortal.pair.localToWorld(relativePos)
        if (newPos.y < 9.98) {
            newPos.y = 10
        }
        body.position.copy(newPos)

        const relativeRot = sourcePortal.quaternion.clone().invert().multiply(camera.quaternion)
        relativeRot.premultiply(halfTurn)
        camera.quaternion.copy(sourcePortal.pair.quaternion.clone().multiply(relativeRot))

        const newForwardDirection = new THREE.Vector3()
        camera.getWorldDirection(newForwardDirection)
        const targetRotation = new THREE.Quaternion().setFromRotationMatrix(
            new THREE.Matrix4().lookAt(
                new THREE.Vector3(0, 0, 0),
                newForwardDirection,
                new THREE.Vector3(0, 1, 0)
            )
        )

        camera.quaternion.copy(targetRotation)

        const inTransform = sourcePortal
        const outTransform = sourcePortal.pair

        if (body) {
            const worldVelocity = new CANNON.Vec3(body.velocity.x, body.velocity.y, body.velocity.z)

            const inTransformQuaternion = new CANNON.Quaternion(
                inTransform.quaternion.x,
                inTransform.quaternion.y,
                inTransform.quaternion.z,
                inTransform.quaternion.w
            )

            const outTransformQuaternion = new CANNON.Quaternion(
                outTransform.quaternion.x,
                outTransform.quaternion.y,
                outTransform.quaternion.z,
                outTransform.quaternion.w
            )

            const halfTurn = new CANNON.Quaternion()
            halfTurn.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), Math.PI)

            const relativeVel = worldVelocity.clone()
            inTransformQuaternion.inverse().vmult(relativeVel, relativeVel)

            halfTurn.vmult(relativeVel, relativeVel)

            outTransformQuaternion.vmult(relativeVel, relativeVel)

            body.velocity.copy(relativeVel)
        }
    }

    checkLeftPortalTeleport() {
        const portalForward = new THREE.Vector3()
        this.rightPortal[0].getWorldDirection(portalForward)

        const travelerPosition = this.game.camera.instance.position.clone()
        const portalPosition = this.rightPortal[0].position.clone()
        const portalToTraveler = travelerPosition.sub(portalPosition)

        const dotProduct = portalForward.dot(portalToTraveler)

        if (dotProduct < 0) {
            this.teleport(this.rightPortal[0], this.game.camera.instance, this.game.controls.cameraBody)
        }
    }

    checkRightPortalTeleport() {
        const portalForward = new THREE.Vector3()
        this.leftPortal[0].getWorldDirection(portalForward)

        const travelerPosition = this.game.camera.instance.position.clone()
        const portalPosition = this.leftPortal[0].position.clone()
        const portalToTraveler = travelerPosition.sub(portalPosition)

        const dotProduct = portalForward.dot(portalToTraveler)

        if (dotProduct < 0) {
            this.teleport(this.leftPortal[0], this.game.camera.instance, this.game.controls.cameraBody)
        }
    }

    resetPortalColors() {
        if (this.leftPortal.length > 0) {
            this.leftPortal[0].material.color = new THREE.Color(0xff9a00)
            this.leftPortal[0].material.needsUpdate = true
        }

        if (this.rightPortal.length > 0) {
            this.rightPortal[0].material.color = new THREE.Color(0x00a2ff)
            this.rightPortal[0].material.needsUpdate = true
        }
    }

    createCompanionCube() {
        const loader = new GLTFLoader()

        loader.load('/cube.glb', (gltf) => {
            this.companionCube = gltf.scene

            const box = new THREE.Box3().setFromObject(this.companionCube)
            const center = box.getCenter(new THREE.Vector3())
            this.companionCube.position.sub(center)

            this.companionCube.position.set(0, 5, -5)
            this.companionCube.scale.set(0.15, 0.15, 0.15)
            this.companionCube.castShadow = true
            this.companionCube.receiveShadow = true

            this.companionCube.traverse((child) => {
                if (child.isMesh && child.material) {
                    if (child.material.map) {
                        child.material.map.colorSpace = THREE.SRGBColorSpace
                        child.material.metalness = 0.7
                    }
                    if (child.material.emissiveMap) {
                        child.material.emissiveMap.colorSpace = THREE.SRGBColorSpace
                    }
                    child.material.needsUpdate = true
                }
            })

            this.scene.add(this.companionCube)

            const cubeShape = new CANNON.Box(new CANNON.Vec3(2.5, 2.5, 2.5))
            this.companionCubeBody = new CANNON.Body({
                mass: 1,
                material: this.defaultMaterial,
                collisionFilterGroup: GROUP_CUBE,
                collisionFilterMask: GROUP_WALL | GROUP_PLAYER | GROUP_DEFAULT | GROUP_PORTAL,
            })
            this.companionCubeBody.addShape(cubeShape)
            this.companionCubeBody.position.copy(this.companionCube.position)
            this.companionCubeBody.linearDamping = 0.5
            this.companionCubeBody.angularDamping = 0.5
            this.companionCubeBody.class = 'companionCube'

            this.physics.addBody(this.companionCubeBody)

            this.companionCube.body = this.companionCubeBody
        }, undefined, (error) => {
            console.error('An error occurred while loading the cube model:', error)
        })
    }

    getHoldPosition() {
        const camera = this.game.camera.instance
        const cameraDirection = new THREE.Vector3()
        camera.getWorldDirection(cameraDirection)
        cameraDirection.normalize()

        return new THREE.Vector3().copy(camera.position)
            .add(cameraDirection.multiplyScalar(this.holdDistance))
    }

    onKeyDown(event) {
        if (event.key === 'e') {
            if (!this.isHoldingCube) {
                this.tryPickupCube()
            } else {
                this.dropCube()
            }
        }
    }

    tryPickupCube() {
        const playerPosition = this.game.camera.instance.position
        const cubePosition = this.companionCube.position.clone()
        const distance = playerPosition.distanceTo(cubePosition)

        if (distance <= this.pickupRange) {
            this.isHoldingCube = true

            const holdPosition = this.getHoldPosition()
            this.holdBody = new CANNON.Body({
                mass: 0,
                type: CANNON.Body.KINEMATIC,
                collisionFilterGroup: 0,
                collisionFilterMask: 0
            })
            this.holdBody.position.copy(holdPosition)
            this.physics.addBody(this.holdBody)

            this.holdingConstraint = new CANNON.PointToPointConstraint(
                this.companionCubeBody,
                new CANNON.Vec3(0, 0, 0),
                this.holdBody,
                new CANNON.Vec3(0, 0, 0),
                1e6
            )
            this.physics.addConstraint(this.holdingConstraint)

            this.companionCubeBody.angularDamping = 1
        }
    }

    dropCube() {
        this.isHoldingCube = false

        if (this.holdingConstraint) {
            this.physics.removeConstraint(this.holdingConstraint)
            this.holdingConstraint = null
        }

        if (this.holdBody) {
            this.physics.removeBody(this.holdBody)
            this.holdBody = null
        }

        this.companionCubeBody.collisionFilterMask |= GROUP_PLAYER
        this.companionCubeBody.angularDamping = 0.5
    }

    checkCubePortalTeleport() {
        const cubePosition = new THREE.Vector3(
            this.companionCubeBody.position.x,
            this.companionCubeBody.position.y,
            this.companionCubeBody.position.z
        )

        const rightPortalPosition = this.rightPortal[0].position.clone()
        const rightPortalForward = new THREE.Vector3()
        this.rightPortal[0].getWorldDirection(rightPortalForward)
        const rightPortalToCube = cubePosition.clone().sub(rightPortalPosition)
        const rightDotProduct = rightPortalForward.dot(rightPortalToCube)

        if (rightDotProduct < 0) {
            this.teleport(this.rightPortal[0], this.companionCube, this.companionCubeBody)
        }

        const leftPortalPosition = this.leftPortal[0].position.clone()
        const leftPortalForward = new THREE.Vector3()
        this.leftPortal[0].getWorldDirection(leftPortalForward)
        const leftPortalToCube = cubePosition.clone().sub(leftPortalPosition)
        const leftDotProduct = leftPortalForward.dot(leftPortalToCube)

        if (leftDotProduct < 0) {
            this.teleport(this.leftPortal[0], this.companionCube, this.companionCubeBody)
        }
    }
}