Les interactions

Les contrôleurs

three.js fournit des exemples d’implémentations de contrôleurs. Ces classes permettent manipuler des objets de la scène à partir du clavier et de la souris. Comme une caméra est également un objet de la scène, il est possible de manipuler la caméra pour créer une sensation d’immersion.

Les exemples d’implémentation ne font pas partie de la bibliothèque de base de three.js. Il faut donc télécharger les fichiers indépendamment. Vous pouvez trouver ces fichiers sur le dépôt Git du projet :

https://github.com/mrdoob/three.js/tree/master/examples/js/controls

À titre d’exemple, nous allons montrer l’utilisation du contrôleur OrbitControls.

Le contrôleur orbital

La classe OrbitControls permet de faire se mouvoir un élément de la scène autour d’un point (rotation orbitale, déplacement, zoom avant et arrière). Si l’élément est la caméra, cela permet à l’utilisateur de tourner autour de la scène.

Pour utiliser un objet OrbitControls, il suffit de le créer en passant en paramètre l’objet contrôlé et l’élément DOM de la page qui devra écouter les événements clavier et souris (généralement le canevas sur lequel la scène est rendue). On peut préciser des contraintes afin de limiter les angles de rotation ou si l’utilisateur peut ou non se déplacer. Ensuite, il suffit d’appeler la méthode update() avant chaque rendu de scène. Le contrôleur met ainsi à jour les transformations de l’objet contrôlé.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8>
    <title>three.js app</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <script src="js/three.min.js"></script>
    <script src="js/OrbitControls.js"></script>
    <script>
      var renderer = new THREE.WebGLRenderer();
      document.body.appendChild(renderer.domElement);

      var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
      renderer.setSize(window.innerWidth, window.innerHeight);

      var scene = new THREE.Scene();

      var obj = new THREE.Mesh(
        new THREE.DodecahedronGeometry(),
        new THREE.MeshPhongMaterial()
      );
      scene.add(obj);

      // Ambient light
      var ambientLight = new THREE.AmbientLight(0xffffff, .1);
      scene.add(ambientLight);

      // Directional light
      var directionalLight = new THREE.DirectionalLight(0xfff0f0, .7);
      directionalLight.position.set(1,1,1);
      scene.add(directionalLight);

      // Point light
      var pointLight = new THREE.DirectionalLight(0xf0f0ff, .3);
      pointLight.position.set(-2,1,1);
      scene.add(pointLight);

      camera.position.z = 4;

      // Création du contrôleur
      var controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.minDistance = 2;
      controls.maxDistance = 30;

      function animate() {
        // mise à jour du contrôleur
        controls.update();

        requestAnimationFrame(animate);
        renderer.render(scene, camera);
      }
      animate();
    </script>
  </body>
</html>

À la ligne 48, on crée le contrôleur pour la caméra et on précise des contraintes. Puis dans la fonction animate() à la ligne 54, on commence par mettre à jour le contrôleur, qui va lui même mettre à jour la position de la caméra.

Astuce

Rappelez-vous, qu’une instance de OrbitControls peut contrôler n’importe quel type d’objet. Il est donc possible de contrôler la rotation d’un objet dans la scène et même d’une lumière.

Le système planétaire (suite)

Ajoutez un contrôleur orbital autour de la Terre.

Le lancer de rayon

Pour sélectionner un ou des objets dans un scène, il est possible d’utiliser le principe du lancer de rayon grâce à la classe RayCaster. La fenêtre de rendu correspond un plan virtuel dans la scène 3D (qui coïncide avec le near plan de la caméra). On peut sélectionner un point sur se plan (par exemple la position de la souris) et lancer un rayon qui part de la caméra et passe par ce point. Les objets traversés par ce rayon sont les objets qui sont pointés par ce point.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8>
    <title>three.js app</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <script src="https://threejs.org/build/three.min.js"></script>
    <script>
      var renderer = new THREE.WebGLRenderer();
      document.body.appendChild(renderer.domElement);

      var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
      renderer.setSize(window.innerWidth, window.innerHeight);

      var scene = new THREE.Scene();
      scene.overrideMaterial = new THREE.MeshPhongMaterial();

      // Ambient light
      var ambientLight = new THREE.AmbientLight(0xffffff, .2);
      scene.add(ambientLight);

      // Directional light
      var directionalLight = new THREE.DirectionalLight(0xffdddd, 1);
      directionalLight.position.set(1,1,1);
      scene.add(directionalLight);

      // On crée les objets sélectionnables dans leur groupe
      var selectionables = new THREE.Group();
      scene.add(selectionables);

      var geometries = {
        dodecahèdre: new THREE.DodecahedronGeometry(),
        sphère: new THREE.SphereGeometry(),
        cube: new THREE.BoxGeometry(),
        cône: new THREE.ConeGeometry(),
      }

      for (var geo in geometries) {
        var obj = new THREE.Mesh(geometries[geo]);
        obj.name = geo;
        obj.position.x = THREE.Math.randFloat(-5, 5);
        obj.position.z = THREE.Math.randFloat(-5, -10);
        selectionables.add(obj);
      }

      var raycaster = new THREE.Raycaster();

      function getSelectionneLePlusProche(position) {
        // Mise à jour de la position du rayon à lancer.
        raycaster.setFromCamera(position, camera);
        // Obtenir la liste des intersections
        var selectionnes = raycaster.intersectObjects(selectionables.children);
        if (selectionnes.length) {
          return selectionnes[0].object;
        }
      }

      function onMouseClick(event) {
        var position = new THREE.Vector2();
        // On conserve la position de la souris dans l'espace de coordonnées
        // NDC (Normalized device coordinates).
        var domRect = renderer.domElement.getBoundingClientRect();
        position.x = ((event.clientX - domRect.left) / domRect.width) * 2 - 1;
        position.y = - ((event.clientY - domRect.top) / domRect.height) * 2 + 1;

        var s = getSelectionneLePlusProche(position);
        if (s) {
          alert("Vous avez sélectionné l'objet " + s.name);
        } else {
          alert("Vous n'avez rien sélectionné");
        };
      }

      renderer.domElement.addEventListener('click', onMouseClick);

      camera.position.z = 10;
      camera.position.y = 2;
      camera.lookAt(0, 0, -7.5);

      renderer.render(scene, camera);
    </script>
  </body>
</html>

Vous pouvez télécharger ce fichier directement.

Dans l’exemple précédent, on prépare une scène puis on crée un objet de type Raycaster à la ligne 53. La fonction getSelectionneLePluProche() met à jour la position du rayon et demande au raycaster la liste des intersections avec le rayon. Comme la liste est triée, le premier élément de la liste (s’il y en a un) et le plus proche de l’origine du rayon (dans ce cas, la caméra. À la ligne 80, on met en place un listener DOM onMouseClick(event), pour détecter un clic de souris. Dans ce cas, on calcule la position de la souris dans le repère normalisé (NDC : Normalized Device Coordinates) et appelle la méthode getSelectionneLePluProche() pour trouver l’élément sélectionné.

Avec un Raycaster, il est possible de réaliser un système de ciblage pour permettre à l’utilisateur d’interagir avec les éléments de la scène.

La projection

À l’inverse du lancer de rayon, la projection permet de connaître la position d’un élément de la scène dans le repère de coordonnées du canevas de rendu ou de la page HTML. Cela peut être très pratique pour positionner un élément HTML par rapport au contenu de la scène.

La méthode project disponible sur un Vector3 permet de projeter la position du vecteur sur le plan de projection de la caméra dans le repère normalisé (NDC : Normalized Device Coordinates). On peut utiliser la fonction ci-dessous pour obtenir les coordonnées en pixels dans la fenêtre. Cette fonction prend en paramètres le renderer, la caméra et l’objet dont on souhaite connaître la position à l’écran :

function getPositionOnScreen(renderer, camera, object3d) {
  var vector = new THREE.Vector3();
  object3d.getWorldPosition(vector).project(camera);
  var domRect = renderer.domElement.getBoundingClientRect();

  // On passe des coordonnées dans le repère normalisé (NDC) aux
  // coordonnées de l'écran
  vector.x = Math.round((vector.x + 1) / 2 * domRect.width) + domRect.left;
  vector.y = Math.round((1 - vector.y) / 2 * domRect.height) + domRect.top;

  return vector;
}

Vous pouvez visualiser un exemple d’utilisation de cette fonction.

Le système planétaire (suite)

Ajoutez une zone de texte en HTML à côté de la Terre et de la Lune.

Astuce

Attention, pour placer correctement les zones de texte, il faut connaître la position des objets dans le monde pour calculer correctement leur projection. Utilisez la méthode Object3D.getWorldPosition() pour connaître les coordonnées d’un objet dans l’espace de coordonnées du monde.