Dans ce tutoriel nous allons voir ensemble comment utiliser la librairie three.js afin de créer un panorama 360° interactif. Le principe est de permettre à l'utilisateur de visualiser l'image mais aussi de pouvoir cliquer sur des points d'intérêts de l'image.

Principe de base

Dans un premier temps nous allons créer l'outil de visualisation d'image. Cela se fait assez simplement en projetant l'image comme une texture sur une sphère et en plaçant ensuite la caméra au centre de cette dernière.

// Scène et contrôles
const container = document.body
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200)
const controls = new THREE.OrbitControls(camera)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
container.appendChild(renderer.domElement)
controls.rotateSpeed = 0.2
controls.enableZoom = false
controls.enablePan = false
controls.enableZoom = false
camera.position.set(-0.1, 0, 0)
controls.update()

// Sphère
const geometry = new THREE.SphereGeometry(50, 32, 32)
const texture = new THREE.TextureLoader().load('360.jpg')
texture.wrapS = THREE.RepeatWrapping
texture.repeat.x = -1
const material = new THREE.MeshBasicMaterial({
    map: texture,
    side: THREE.DoubleSide
})
material.transparent = true
const sphere = new THREE.Mesh(geometry, material)
scene.add(this.sphere)
function render () {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}
render()

L'OrbitControls permet de gérer simplement la navigation au sein de la sphère en orbitant autour de son centre et facilite ainsi grandement la partie interaction.

Placement de marqueurs

Les marqueurs sont plus problématiques car il faut trouver le positionnement de ces derniers dans l'espace 3D. Pour cela on va recourir à un objet Raycaster qui permet d'émettre un faisceau et de pouvoir ensuite vérifier les intersections avec les différents objets de notre scène :

const rayCaster = new THREE.Raycaster()
function onClick (e) {
  // On convertit la position de la souris dans le repère de la caméra 
  let mouse = new THREE.Vector2(
    (e.clientX / window.innerWidth) * 2 - 1,
    -(e.clientY / window.innerHeight) * 2 + 1
  )
  rayCaster.setFromCamera(mouse, camera)
  let intersects = rayCaster.intersectObject(s.sphere)
  if (intersects.length > 0) {
    console.log('Sphère touchée au point : ', intersects[0].point)
  }
}
container.addEventListener('click', onClick)

Une fois la position du point d'intérêt connu il suffit de créer une Sprite pour mettre en place une bulle d'information. Cet objet se comporte comme un plan avec la particularité de toujours faire face à la caméra.

let point = new THREE.Vector3(14, 1.9, -47)
let spriteMap = new THREE.TextureLoader().load('info.png')
let spriteMaterial = new THREE.SpriteMaterial({
    map: spriteMap
})
let sprite = new THREE.Sprite(spriteMaterial)
sprite.name = point.name
// On place le point un peu plus proche de la caméra pour un léger effet de parallaxe
sprite.position.copy(point.position.clone().normalize().multiplyScalar(30)) 
scene.add(sprite)

Détecter le hover sur les marqueurs

Maintenant que nos marqueurs sont placés il faut être capable de détecter lorsque la souris survol ces éléments. Pour cela nous allons utiliser la même méthode que précédemment avec l'utilisation d'un Raycaster projetté depuis la caméra. Une fois que l'on trouve le point d'intersection il faut faire le cheminement inverse pour connaitre la position de la sprite dans le repère du plan de la caméra. La méthode project sur les Vector3 permet justement de faire cela simplement.

let intersects = rayCaster.intersectObjects(scene.children)
intersects.forEach(function (intersect) {
    if (intersect.object.type === 'Sprite') {
        let p = intersect.object.position.clone().project(camera)
        tooltip.style.top = ((-1 * p.y + 1) * window.innerHeight / 2) + 'px'
        tooltip.style.left = ((p.x + 1) * window.innerWidth / 2) + 'px'
        tooltip.classList.add('is-active')
        tooltip.innerHTML = intersect.object.name
    }
})

Animation et transition

Enfin, pour passer d'une vue à l'autre il suffit de faire disparaitre notre sphere et d'en créer une nouvelle (et de faire la même chose avec les Sprites). Si vous souhaitez avoir un effet de transition, il faudra jouer sur la transparence du matériau utilisé sur la sphere. Vous pouvez utiliser n'importe qu'elle librairie d'animation (j'utilise ici TweenLite)

// Pour masquer la sphère
TweenLite.to(sphere.material, 1, {
    opacity: 0,
    onComplete: () => {
        scene.remove(sphere)
    }
})

// Pour afficher la sphère
sphere.material.opacity = 0
TweenLite.to(sphere.material, 1, {
    opacity: 1
})

Une librairie plus complète ?

Si vous cherchez une librairie dédiée aux panorama et que vous n'avez pas forcément de besoins trop spécifiques vous pouvez tester la librairie panolens.js qui se base justement sur three.js (le code source peut vous aider à mieux comprendre comment gérer certains format d'images / vidéos).