WebGL : Créer une scène 3D (avec une skybox) avec three.js

Partager cet article

Temps estimé pour la lecture de cet article : 69 min

Introduction à WebGL et three.js

Cliquez sur l’image pour voir la scène en action

WebGL est une spécification d’interface de programmation de 3D dynamique pour les pages et applications HTML5 créée par le groupe Khronos. Elle permet d’utiliser le standard OpenGL ES au sein d’un projet web, en s’aidant du langage JavaScript, de données au format JSON, de l’accélération matérielle pour les calculs et le rendu 3D à l’aide des pilotes OpenGL ES ou des processeurs graphique du terminal informatique. OpenGL ES a été choisi pour son large support au sein des architectures embarquées et mobiles.

Three.js est une bibliothèque JavaScript qui permet de créer et réaliser des scènes 3D. Le créateur de la bibliothèque est mrDoob. Le code source est hébergé sur github. La bibliothèque peut-être utilisée avec la balise canvas du HTML5, elle permet également de réaliser des rendus en webGL, CSS3D et SVG. Elle offre un tas d’outil très confortable pour les développeurs :

  • Animation avec squelette
  • LOD (Level Of Detail)
  • Loader (Chargement de fichier) : .OBJ, .JSON, .FBX
  • Particules (neige,feu)

Avant toute chose, il nous faut voir quelques concepts afin de bien comprendre ce qui va suivre et le code que l’on va produire.

Représentation d’un objet en OpenGL

Une première notion importante, ce sont les meshs. En résumé, un mesh c’est la représentation informatique d’un objet 3D. Or un objet est constitué d’un ensemble de points dans l’espace. En OpenGL, un point 3D est appelé un vertex ! Un mesh est donc composé d’un ensemble de vertex. On stocke ces vertex dans un ordre défini afin d’obtenir une connexité. Généralement on conserve les vertex par 3 afin qu’il forme un triangle. Vous avez déjà sûrement entendu parler du nombre de polygones qu’une carte graphique est capable de dessiner et bien ce sont ces fameux triangles !

webgl-skybox-cube

Pour mieux comprendre cette notion prenons l’exemple d’un cube. Un cube a six faces or chaque face peut-être dessiner grâce à deux triangles. On a donc besoin de 12 triangles pour représenter un cube. Ainsi pour former le triangle de la figure ci-dessus, on créerait un mesh constitué des trois vertex suivantes : (0,0,0), (0,0,1) et (0,1,1).

Bien sur, ici c’est simple car on veut ne modéliser qu’un simple cube mais comment dessiner des objets beaucoup plus complexes ? Comme un personnage par exemple. Et bien c’est là qu’interviennent les modélisateurs d’objets 3D ! Il crée un modèle dans un logiciel comme blender et exporte le tout pour le moteur 3D. Pas besoin donc de préciser chaque point à la main, sinon vous imaginez la galère ^_^.

Modélisation d’une scène en OpenGL

webgl-scene-camera

Une seconde chose à comprendre c’est comment fonctionne la mise en place d’une scène OpenGL. Comment sont retranscrits nos objets 3D vers la 2D de notre écran ? Voici, une description sommaire du processus :

  • On représente nos objets grâce à des Meshs. Tout d’abord, on décrit chaque point de la scène et réitére l’action jusqu’à avoir représenté tous les points de notre univers.
  • Une fois que c’est fait, il nous faut positionner ces objets dans un espace 3D. Oui, mais comment faire, la solution est… mathématique :p ! On va réaliser des calculs matriciels afin de déplacer, mettre à l’échelle ou encore faire une rotation de nos objets. Heureusement, three.js, va nous permettre d’éviter de tout faire à la main.
  • Ensuite, il nous suffira, d’utiliser une « caméra » afin de viser nos objets et d’effectuer une projection de cette visée afin d’avoir un rendu 3D sur nos écrans 2D.

Les plus malins auront remarqué qu’il manque une étape, on a placé les objets sur notre écran, mais et leur apparence ? Nous allons donc « remplir » chacun de nos triangles, cette étape est appelée la rasterization.

  • Il faut d’abord calculer un éclairage pour définir la couleur des triangles ! Pour cela, on calcule la normale à la face. Plus l’angle entre cette normale et notre source lumineuse sera grand, moins la face sera éclairée. Il y a différents algorithmes d’éclairage connus depuis des années :
    • flat où l’on continue de bien voir les triangles
    • gouraud où l’on ne voit presque plus les triangles avec un dégradé de couleurs effectué entre chaque extrémité du triangle.
    • phong gourmand en ressources mais plus précis
  • Par la suite, on peut si on le désire appliquer une texture à l’objet.

La notion de Skybox

Une skybox c’est un large cube qui va englober toute une scène. On applique sur ce cube six images représentant un environnement complet. Le joueur va avoir l’illusion d’être dans un environnement très large que là où il est actuellement. Cette pratique est très utilisé dans les jeux vidéo. Comme on peut le voir par exemple dans cette capture du dernier Tomb Raider Survival.

webgl-skybox-tomb-raider

Ici, par exemple le joueur a l’impression d’être dans un univers immense, le sentiment étant renforcé par les montagnes qu’on voit au fond mais ce n’est qu’une impression en réalité, le joueur est enfermé dans un cube, possédant 6 faces et sur chaque face on y retrouve une texture.

Généralement, ces images ont le pattern suivant :

webgl-skybox-cubemaps

Il est donc nécessaire de faire correspondre chaque face à son image correspondante et de les donner dans l’ordre désiré par three.js. Voici, un exemple de définition d’une skybox provenant de la documentation, de three.js :

var path = "textures/cube/";
var format = '.jpg';
var urls = [
    path + 'px' + format, path + 'nx' + format,
    path + 'py' + format, path + 'ny' + format,
    path + 'pz' + format, path + 'nz' + format
];

Remarque : on y retrouve des notations très utilisée sur le web, mais pas forcément clair quand l’on débute, voici quelques traductions :

  • px (positive x) : right
  • nx (negative x) : left
  • py (positive y) : top
  • ny (negative y) : bottom
  • pz (positive z) : back
  • nz (negative z) : front

Où trouver des ressources pour créer des skyboxes

Pour vous permettre de pouvoir tester plus facilement le chargement de skybox, voici quelques sites proposant des ressources :

Comme, je suis gentil, je vous ai fait un pack de skybox trouvé sur internet et toutes testées, elles sont utilisables en l’état, have fun !

webgl-skybox-resources

Remarque : parfois certaines textures ne sont pas dans le bon sens, je ne sais pas trop pourquoi mais en passant par des logiciels de retouche photo vous devriez pouvoir corriger une rotation malencontreuse. Si vous avez besoin de convertir une image par exemple du format tga au format png, je vous conseille le site image.ilovefile.com qui m’a bien aidé pour cet article.

Blender : Ajouter un addon pour exporter en JSON pour Three.Js

webgl-blender-example

Blender est un logiciel libre de modélisation, d’animation et de rendu en 3D. Il dispose de fonctions avancées de modélisation, de sculpture 3D, de dépliage UV, de texturage, de rigging, d’armaturage, d’animation 3D, et de rendu. Il gère aussi le montage vidéo non linéaire, la composition, la création nodale de matériaux, la création d’applications 3D interactives ou de jeux vidéo grâce à son moteur de jeu intégré ainsi que diverses simulations physiques telles que les particules, les corps rigides, les corps souples et les fluides. Blender est disponible sur plusieurs plates-formes telles que Microsoft Windows, Mac OS X, GNU/Linux. De plus, il est possible de lui rajouter des plugins grâce au python.

Ainsi, la bibliothèque three.js propose un plugin afin de faciliter l’importation de fichier blender dans une scène. Le plugin permet ainsi d’exporter un modèle au format .json. Voici les différentes étapes d’installation du plugin pour la dernière version de blender. Une fois le projet téléchargé, dézipper le tout, et aller récupérer le dossier io_threedans utils/exporters/blender/, vous n’avez alors plus qu’à copier son contenu vers le dossier correspondant à votre système d’exploitation :

  • Windows: Si vous avez une version supérieure à la 2.6 de blender, c’est ici : C:\Program Files\Blender Foundation\Blender\2.7X\scripts\addons, sinon ici : C:\Users\USERNAME\AppData\Roaming\Blender Foundation\Blender\2.6X\scripts\addons
  • Mac: Version 2.7 : /Applications/blender.app/Contents/Resources/2.7X/scripts/addons et 2.6 : /Applications/blender.app/Contents/MacOS/2.6X/scripts/addons
  • Linux: 2.7 : /home/USERNAME/.config/blender/2.6X/scripts/addons, 2.6 : /usr/lib/blender/scripts/addons

webgl-blender-user

Ensuite, il vous faut activer le script dans blender, il suffit d’aller dans Fichier : Préférences utilisateur, addons et sélectionner three.js, n’oubliez pas de sauvegarder les préférences sinon il vous faudra recommencer.

Chargement de three.js

Nous allons voir comment réaliser la scène que vous avez pu voir au début de l’article. Commençons par le plus simple la page html, comme vous pouvez le voir elle est plutôt vide ^_^. La seule partie qui nous intéresse ce sont les inclusions des scripts. Le premier c’est la librairie three.js, il vaut mieux prendre la version minifiée, vu qu’elle est plus légère ! Ensuite, on inclut le loader de fichier json fourni sur le dépôt. Enfin, on inclut notre propre script.

<!DOCTYPE html>
<html lang="fr">
	<head>
		<title>Three JS</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
	</head>
	<body>
		<script src="js/three.min.js"></script>
		<script src="js/json_loader.js"></script>
		<script src="js/main.js"></script>
	</body>
</html>

Passons maintenant au cœur du sujet (enfin ^_^).

Création d’une scène avec three.js

La première chose à créer, c’est une scène, c’est sur cette dernière, que l’on va pouvoir ajouter des objets, avec three.js, c’est très simple, il suffit de faire :

var scene = new THREE.Scene();
scene.add(object);

Une fois que nous avons la scène, nous allons avoir besoin d’une caméra, voici comment en initialiser une :

var ratio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, ratio, 0.1, 1000);
camera.position.z = 100;

On crée la caméra grâce à quatre arguments :

  • Le champ de vision, exprimé en degrés.
  • Le ratio entre la largeur et la hauteur du rendu (4/3, 16/9…).
  • Les deux derniers arguments sont : le near et le far.

Comme les objets sont placés à l’origine en (0,0,0). On doit augmenter la position z (la profondeur) de la caméra afin de pouvoir observer les objets.

Enfin, la dernière chose de l’on va avoir besoin, c’est un renderer, c’est l’objet qui va se charger grâce à notre scène et notre caméra d’effectuer tous les calculs nécessaires afin d’effectuer un rendu sur l’écran.

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
...	
renderer.render(scene, camera);

Voyons maintenant comment créer des objets !

Chargement des objets et de l’éclairage

Commençons par le chargement d’un objet, ici, je charge donc un objet au format json exporté grâce à blender. Une fois le chargement asynchrone fini, on va pouvoir créer l’objet, pour se faire on crée un objet Mesh grâce à sa géométrie (les vertex et leur ordre d’affichage) et sa texture (c’est ce qu’on appelle un material). On précise ensuite la position et la taille de l’objet.

var loader = new THREE.JSONLoader();
loader.load("obj/android.json", function(geometry, mat) {
        var material = new THREE.MeshFaceMaterial(mat);
        var object = new THREE.Mesh(geometry, material);
        object.scale.set(50, 50, 50);
        object.position.set(0,0,0);
        scene.add(object);
    }
);

Maintenant, nous allons voir comment gérer l’éclairage de la scène, deux objets vont nous intéresser :

  • L’objet AmbientLight qui permet de définir une lumière globale sur la scène de manière égale entre tous les objets.
  • L’objet PointLight permet d’émettre une lumière depuis un point dans toutes les directions. Cela permet par exemple d’obtenir le même effet qu’une lumière issue d’une ampoule.

Voici un exemple de création de ces objets :

var ambient = new THREE.AmbientLight(0xFFFFFF);
scene.add(ambient);

var pointLight = new THREE.PointLight(0xFFFFFF, 2);
scene.add(pointLight);

Chargement de la Skybox

Enfin, la dernière chose dont nous allons parler, c’est la skybox. Pour la définir nous avons besoin d’un ShaderMaterial et d’une BoxGeometry. L’objet ShaderMaterial permet de définir la texture de notre cube, c’est donc à cet objet que l’on va avoir besoin de préciser les différentes images de notre skybox. Enfin l’objet BoxGeometry représente un cube défini par une largeur, hauteur et une profondeur. Il nous restera enfin plus qu’a créé notre Mesh grâce aux deux objets précédents, ce qui nous donne :

var path = "img/skybox/";
var format = '.jpg';
var urls = [
	path + 'right' + format,
	path + 'left' + format,
	path + 'top' + format,
	path + 'bottom' + format,
	path + 'back' + format,
	path + 'front' + format
];

var reflectionCube = THREE.ImageUtils.loadTextureCube(urls);
reflectionCube.format = THREE.RGBFormat;

var shader = THREE.ShaderLib[ "cube" ];
shader.uniforms[ "tCube" ].value = reflectionCube;

var material = new THREE.ShaderMaterial( {
	fragmentShader: shader.fragmentShader,
	vertexShader: shader.vertexShader,
	uniforms: shader.uniforms,
	depthWrite: false,
	side: THREE.BackSide
});

mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), material);
sceneCube.add(mesh);

Rendu de la scène

Tous ceux que nous avons vus précédemment correspond à la phase d’initialisation, par la suite, nous allons donc faire un rendu de la scène toutes les secondes, afin d’avoir un bon rendu, généralement on conseille d’avoir 60 FPS, on pourrait donc définir quelque chose du genre :

setInterval(function() {
    render(); 
}, 1000/60);

Mais, Paul Irish a introduit la fonction requestAnimationFrame qui permet d’optimiser les animations, d’arrêter l’animation si l’onglet est inactif et permet de conserver la batterie, je vous conseille donc de l’utiliser :3. Il nous suffit alors de créer une fonction animate qui va faire un rendu de la scène toutes les secondes.

function animate() {
    requestAnimationFrame(animate);
    camera.lookAt(scene.position);
    renderer.render(scene, camera);
}

On a enfin vu, tous ceux dont nous avons besoin pour réaliser notre scène: alleluia !!

Exemple final d’une scène avec chargement d’une skybox et d’un objet

var container, loader;
var camera, fov = 50, scene, renderer;
var cameraCube, sceneCube;

var pointLight;

var mouseX = 0, mouseY = 0;
var windowHalfX = window.innerWidth / 2, windowHalfY = window.innerHeight / 2;

document.addEventListener('mousemove', onDocumentMouseMove, false);
document.addEventListener('mousewheel', onDocumentMouseWheel, false);

init();
animate();

function init() {
	container = document.createElement('div');
	document.body.appendChild( container );

	// Création camera(s) et scène(s)
	camera = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 5000 );
	camera.position.z = 2000;
	cameraCube = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 100 );

	scene = new THREE.Scene();
	sceneCube = new THREE.Scene();

	// Eclairage de la scène  
	var ambient = new THREE.AmbientLight( 0xffffff );
	scene.add(ambient);

	pointLight = new THREE.PointLight( 0xffffff, 2 );
	scene.add(pointLight);

	// Chargement de notre objet
	var loader = new THREE.JSONLoader();
	loader.load("obj/android.json", function(geometry, mat) {
			var material = new THREE.MeshFaceMaterial(mat);
			var object = new THREE.Mesh(geometry, material);
			object.scale.set(50, 50, 50);
			object.position.set(0,0,0);
			scene.add(object);
	    }
	);


	// Chargement de la Skybox
	var path = "img/skybox/ocean/";
	var format = '.jpg';
	var urls = [
		path + 'right' + format,
		path + 'left' + format,
		path + 'top' + format,
		path + 'bottom' + format,
		path + 'back' + format,
		path + 'front' + format
	];

	var reflectionCube = THREE.ImageUtils.loadTextureCube( urls );
	reflectionCube.format = THREE.RGBFormat;

	var shader = THREE.ShaderLib[ "cube" ];
	shader.uniforms[ "tCube" ].value = reflectionCube;

	var material = new THREE.ShaderMaterial( {
		fragmentShader: shader.fragmentShader,
		vertexShader: shader.vertexShader,
		uniforms: shader.uniforms,
		depthWrite: false,
		side: THREE.BackSide
	});

	mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), material);
	sceneCube.add(mesh);

	// Rendu de la scène
	renderer = new THREE.WebGLRenderer({
  		devicePixelRatio: window.devicePixelRatio || 1
	});
	renderer.setSize(window.innerWidth, window.innerHeight);
	renderer.autoClear = false;
	container.appendChild(renderer.domElement);

	window.addEventListener('resize', onWindowResize, false);
}

function onWindowResize() {
	windowHalfX = window.innerWidth / 2;
	windowHalfY = window.innerHeight / 2;

	camera.aspect = window.innerWidth / window.innerHeight;
	camera.updateProjectionMatrix();

	cameraCube.aspect = window.innerWidth / window.innerHeight;
	cameraCube.updateProjectionMatrix();

	renderer.setSize( window.innerWidth, window.innerHeight );
}


function onDocumentMouseMove(event) {
	mouseX = ( event.clientX - windowHalfX ) * 4;
	mouseY = ( event.clientY - windowHalfY ) * 4;
}

function onDocumentMouseWheel(event) {
	var wDelta = event.wheelDelta < 0 ? 'down' : 'up';
	if(wDelta == 'down') {
   		camera.fov += camera.fov * 0.05;
	} else {
	   	camera.fov -= camera.fov * 0.05;
	}
   	camera.updateProjectionMatrix();
}


function animate() {
	// http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
	requestAnimationFrame(animate);
	render();
}


function render() {
	camera.position.x += (mouseX - camera.position.x) * 0.05;
	camera.position.y += (- mouseY - camera.position.y) * 0.05;

	camera.lookAt(scene.position);
	cameraCube.rotation.copy(camera.rotation);

	renderer.render(sceneCube, cameraCube);
	renderer.render(scene, camera);
}

Debugger avec Chrome

Une des choses qui peut être frustrante quand on débute, c’est quand on essaye de charger un objet, que rien ne s’affiche mais que malheureusement aucune erreur n’est affichée dans la console développeur. Pour pallier à ça, il existe un outil expérimental (encore en développement) sous chrome, que l’on peut activer. Pour ce faire, ouvrez un nouvel onglet (ctrl+t), puis entrer « chrome://flags ». Ensuite, il vous faudra trouver l’option « Activer les expérimentations dans les outils de développement » et l’activer.

webgl-debug-chrome

Ensuite, direction l’outil de développement de chrome (Ctrl+shift+i), ensuite il nous faut activer l’inspecteur de canvas. Cliquez sur l’outil de paramètres, choisissez « Experiments » et cliquez sur « Canvas Insepection ».

webgl-chrome-canvas

Le plus dur est fait, on a réussi à activer l’outil, il ne reste plus qu’a l’utiliser, allez sur le site qui vous intéresse, cliquez sur enable, ensuite sur take snapshot.

webgl-chrome-resultat

À partir de là, vous aurez accès au ensemble de variables qui sont chargées et donc le debug peut commencer !

Conclusion

Si vous avez lu cet article jusque-là, félicitations, ça n’a pas dû être facile. Le sujet étant très vaste et pas forcément simple à aborder. J’ai essayé d’être le plus clair possible. Les ressources utilisées pour ma démonstration sont disponibles sur le site learnopengl, qui propose d’ailleurs un bon article sur les skybox.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.