Créer une grille hexagonale en 3D avec Babylon.js

Pour faire suite aux deux précédents cours sur le monde merveilleux de JavaScript, WebGL et Babylon.js, je vous propose aujourd’hui de créer quelque chose de différent du FPS vue dans le dernier article, mais de particulièrement utile dans un jeu de gestion/stratégie : Une grille à base d’hexagones ! Vous connaissez sans doute le jeu Risk ? ou même Civilization ? Le Gameplay de ces jeux repose en partie sur ce type de grille.

Le jeu Civilization V utilise des grilles d'hexagones.
Le jeu Civilization V utilise des grilles d’hexagones.

Pour suivre ce tutoriel il vous faudra un navigateur web à jour (Internet Explorer 11+, Chrome 38+ ou Firefox 33+ feront l’affaire), un éditeur de texte simple ou lourd à votre convenance (j’utilise l’excellent SublimText 3 pour ma part) et enfin un serveur web local (j’utilise Wamp sous Windows et XAMPP sur Linux et Mac). Si vous utilisez Firefox vous n’êtes pas obligé d’avoir un serveur web local car ce navigateur autorise les requêtes Ajax locales, avec Internet Explorer 11 ça marchera aussi tant que vous ne chargez pas de modèles. Dans ce tutoriel nous ne chargeons pas de modèles, donc il n’y a que les utilisateurs de Google Chrome (et dérivés) qui auront besoin d’un serveur web (et toc).

Prérequis
  1. La programmation orientée objet en JavaScript pour les nuls
  2. Premier pas en 3D avec WebGL et Babylon.js
  3. Un micro FPS en JavaScript avec Babylonjs

1. Un peu de HTML et de JavaScript…

Tout  débute avec la création d’une page HTML et de quelques scripts. Je vous propose la structure suivante :

  • index.html
  • Assets/js/
  • Assets/images/

Il faudra placer 3 images différentes dans le dossier Assets/images qui seront utilisées en tant que textures pour différencier les hexagones sur la grille. Ici ça sera blue.png, green.png et brown.png, 3 images en dégradé.

Un total quatre scripts :

Babylon.js est le moteur 3D JavaScript utilisé pour ce tutoriel, si vous n’êtes pas encore familiarisé avec ce dernier alors je vous encourage à lire mes deux précédents tutoriels où vous apprendrez à utiliser ce fantastique moteur.

Hand.js nous permettra d’assurer une compatibilité touch sans effort (il n’est pas nécessaire si vous ne voulez pas développer sur mobile).

Tutorial.js sera notre point de démarrage, il initialisera le moteur Babylon.js et lancera la génération de la grille.

Enfin HexGridBuilder.js permettra de générer une map d’hexagones en lui passant certains paramètres, comme la taille ou l’espacement entre chaque hexagones.

Pour aller vite, j’ai créé un kit de démarrage téléchargeable ici. Vous êtes évidement libre de créer votre projet de A à Z.

Passons au contenu du fichier Tutorial.js :

var Tutorial = {
	run: function () {
		var canvas = document.getElementById("renderCanvas");
		var engine = new BABYLON.Engine(canvas, true);
		var scene = new BABYLON.Scene(engine);
		var camera = new BABYLON.ArcRotateCamera("ArcRotateCamera", 1, 0.8, 10, new BABYLON.Vector3(0, 3, 0), scene);
		scene.activeCamera.attachControl(canvas);

		var light = new BABYLON.DirectionalLight("DirLight", new BABYLON.Vector3(1, -1, 0), scene);
		light.diffuse = new BABYLON.Color3(1, 1, 1);
		light.specular = new BABYLON.Color3(0.3, 0.3, 0.3);
		light.intensity = 1.5;

		var grid = new HexGridBuilder(15, 15, 1);
		grid.generate(scene);

		engine.runRenderLoop(function() {
			scene.render();
		});
	}
};

Le script de mise en route de Babylon.js est très simple alors je ne reviens pas dessus, vous noterez cependant la création d’un objet de type HexGridBuilder. Nous créons une grille d’hexagones de 15 x 15 cases, soit 225 hexagones ! L’espacement est défini à 1 (donc tout sera collé).

2. Une grille d’hexagones simple

a_hexagones

Commençons par le commencement en examinant comment la classe HexGridBuilder va fonctionner. La grille va être composée de mesh ayant pour forme un hexagone. Il y a plusieurs approches pour en créer, on peut par exemple le modéliser dans un logiciel de 3D ou alors utiliser les fonctions standards de Babylon.js (ou de tout autre moteur digne de ce nom) pour générer le maillage qui va bien. Mais au fait, un hexagone c’est quoi ? Ce n’est rien de plus qu’un cylindre avec un indice de tesselation fixé à 6 (le nombre de faces nécessaires pour former le contour du cylindre). Voici comment en créer un avec Babylon.js :

var prefab = BABYLON.Mesh.CreateCylinder("hex", 1, 3, 3, 6, 1, scene);
prefab.rotation.y += Math.PI / 6;
prefab.material = new BABYLON.StandardMaterial("gridMat", scene);
prefab.material.diffuseTexture = new BABYLON.Texture("assets/images/blue.png", scene);

La méthode CreateCylinder permet de créer… un cylindre, elle prend en paramètre un nom (vous pouvez mettre ce que vous voulez),  la hauteur du cylindre, le diamètre du bas, le diamètre du haut, l’indice de tesselation et enfin le nombre de subdivisions. Avec ces paramètres nous aurons ainsi un bel hexagone ! Vous noterez qu’en jouant sur les paramètres de diamètre haut et bas, on peut créer un cône 😉 Les plus observateurs d’entre vous aurons vue qu’une rotation de PI / 6 a été ajoutée au mesh. Par défaut l’orientation du mesh généré dans Babylon.js n’est pas pratique à utiliser, c’est juste pour corriger ce petit défaut.

Enfin on ajoute un material avec une texture en bleu dégradé . Passons maintenant à l’implémentation de la classe HexGridBuilder.

var HexGridBuilder = function (width, depth, margin) {
	this.width = width || 10;
	this.depth = depth || 10;
	this.margin = margin || 1.0;
	this._hexWidth = 1.0;
	this._hexDepth = 1.0;
	this._initialPosition = BABYLON.Vector3.Zero();
};

HexGridBuilder.prototype.calculateInitialPosition = function () {

};

HexGridBuilder.prototype.getWorldCoordinate = function (x, y, z) {

};

HexGridBuilder.prototype.generate = function (scene) {

};

La classe est initialisée avec trois paramètres qui permettent de définir la taille de la grille et la marge entre chaque hexagone. On stockera la taille d’un hexagone dans les variables _hexWidth et _hexHeight.

La méthode calculateInitialPosition sera appelée une fois et permettra de récupérer la position initiale du premier hexagone. Le point calculé correspond au centre de la grille.

Grid.prototype.calculateInitialPosition = function () {
	var position = BABYLON.Vector3.Zero();
	position.x = -this._hexWidth * this.width / 2.0 + this._hexWidth / 2.0;
	position.z = this.depth / 2.0 * this._hexDepth / 2.0;
	return position;
};

HexaCoordsPour placer les hexagones sur la grille, il va falloir trouver les bonnes coordonnées. Sur la capture de gauche vous pouvez voir comment fonctionne le système de coordonnées sur une grille de ce type. Sur l’axe des hauteurs (Z), les coordonnées sont toujours identiques (comme pour une grille carré), par contre sur l’axe de la largeur (X) il y a un décalage dû au placement de l’hexagone. Effectivement on voit que la hauteur est plus petite que la largeur. Nous utiliserons la formule suivante pour passer d’une coordonnée « Monde » à une coordonnée « Grille ».

Grid.prototype.getWorldCoordinate = function (x, y, z) {
	    	var offset = 0.0;
	
	if (z % 2 !== 0) {
		offset = this._hexWidth / 2.0;
	}
	
	var px = this._initialPosition.x + offset + x * this._hexWidth * this.margin;
	var pz = this._initialPosition.z - z * this._hexDepth * 0.75 * this.margin;

	return new BABYLON.Vector3(px, y, pz);
};

On termine avec la méthode de génération qui va faire 3 choses essentielles :

  1. Créer le modèle de base d’hexagone qui sera cloné pour chaque case ;
  2. Mettre à jour les valeurs _hexWidth et _hexDepth ;
  3. Créer les meshes pour chaque cases.
	var grid = new BABYLON.Mesh("Grid", scene);
	grid.isVisible = false;
	
	var prefab = BABYLON.Mesh.CreateCylinder("hex", 1, 3, 3, 6, 1, scene, false);
	prefab.rotation.y += Math.PI / 6;
	prefab.material = new BABYLON.StandardMaterial("gridMat", scene);
	prefab.material.diffuseTexture = new BABYLON.Texture("assets/images/blue.png", scene);
	
	var boundingInfo = prefab.getBoundingInfo();
	this._hexWidth = boundingInfo.maximum.z - boundingInfo.minimum.z;
	this._hexDepth = boundingInfo.maximum.x - boundingInfo.minimum.x;
	this._initialPosition = this.calculateInitialPosition();
	
	var tile = null;
	for (var z = 0; z < this.depth; z++) {
		for (var x = 0; x < this.width; x++) {
			tile = prefab.clone();
			tile.position = this.getWorldCoordinate(x, 0, z);
			tile.hexPosition = new BABYLON.Vector3(x, 0, z);
			tile.parent = grid;
		}
	} 
	
	prefab.dispose();

La méthode getBoundingInfo permet de récupérer les dimensions du maillage de l’hexagone, grâce aux propriétés maximum et minimum on peut mettre à jour les valeurs de _hexWidth et _hexHeight. Vous noterez que j’ai créé un mesh vide qui contient tous les hexagones. J’attire votre attention sur une chose : Il faut définir la valeur de isVisible à la valeur false, car ce mesh n’a pas de maillage, il n’a donc pas a être affiché. Mettre la valeur false ne veut pas dire que les enfants seront eux aussi invisibles mais uniquement le conteneur.

Une propriété hexPosition est ajoutée à chaque création d’un nouvel hexagone, elle contient les coordonnées de ce dernier sur la grille. Cela va nous permettre plus tard de situer cet objet suite à un clic de souris par exemple. Enfin il faut supprimer le modèle de base car sinon il reste sur la scène, la méthode dispose permet de réaliser cette tâche. Vous devriez avoir ce rendu dans votre navigateur, pas mal non 😀

Et voilà le travail
Et voilà le travail

Une grille un peu plus complexe

Maintenant que vous savez générer une grille d’hexagones, il est vraiment facile de lui donner du volume et du style en faisant varier la hauteur et le material des hexagones. Pour illustrer cette possibilité, je vais utiliser une méthode extrêmement naïve, mais qui années après années continue de faire ses preuve : Math.random ! Nous allons créer 3 materials, une bleu qui représentera l’eau, une verte pour le sol et enfin une marron pour les montagnes. En utilisant un algorithme très compliqué à base de nombres aléatoires et de modulo, nous pourrons générer une grille aléatoire ! Toutes les modifications sont dans la méthode generate.

	var grid = new BABYLON.Mesh("Grid", scene);
	grid.isVisible = false;
	
	var prefab = BABYLON.Mesh.CreateCylinder("cylinder", 1, 3, 3, 6, 1, scene, false);
	prefab.scaling = new BABYLON.Vector3(3, 3, 3);
	prefab.rotation.y += Math.PI / 6;
	
	var boundingInfo = prefab.getBoundingInfo();
	this._hexWidth = (boundingInfo.maximum.z - boundingInfo.minimum.z) * prefab.scaling.x;
	this._hexDepth = (boundingInfo.maximum.x - boundingInfo.minimum.x) * prefab.scaling.z;
	this._initialPosition = this.calculateInitialPosition();
	
	var materials = [
		new BABYLON.StandardMaterial("BlueMaterial", scene),
		new BABYLON.StandardMaterial("GreenMaterial", scene),
		new BABYLON.StandardMaterial("BrownMaterial", scene)
	];
	
	materials[0].diffuseTexture = new BABYLON.Texture("Assets/images/blue.png", scene);
	materials[1].diffuseTexture = new BABYLON.Texture("Assets/images/green.png", scene);
	materials[2].diffuseTexture = new BABYLON.Texture("Assets/images/brown.png", scene);
	
	var tile = null;
	var random = 0;
	
	for (var z = 0; z < this.depth; z++) {
		for (var x = 0; x < this.width; x++) {
			tile = prefab.clone();
			tile.position = this.getWorldCoordinate(x, 0, z);
			tile.hexPosition = new BABYLON.Vector3(x, 0, z);

			random = Math.floor(Math.random() * 10);
			
			if (random % 2 === 0) {
				tile.scaling.y += 1;
				tile.material = materials[0];
			}
			else if (random % 3 === 0) {
				tile.scaling.y += 6;
				tile.material = materials[2];
			}
			else {
				tile.material = materials[1];
			}
			
			tile.parent = grid;
		}
	} 
	
	prefab.dispose();
};
Et voilà le travail !
Et voilà le travail ! (bis)

Détecter un clic sur un hexagone

Comment détecter un clic de souris sur un hexagone ? Si je vous dit que c’est ultra simple, me croirez vous ? Ouvrez le fichier Tutorial.js et ajoutez le code suivant juste après la génération de la grille.

var onClickHandler = function (event) {
	var pick = scene.pick(event.clientX, event.clientY);
	
	if (pick.pickedMesh && pick.pickedMesh.hexPosition) {
		var hexagon = pick.pickedMesh;
		console.log(hexagon.hexPosition);
	}
};

document.body.addEventListener("pointerdown", onClickHandler, false);

Grâce à hand.js nous pouvons utiliser les PointerEvent sans nous soucier du navigateur, qu’il soit mobile ou desktop. L’événement pointerdown correspond à un événement de type TouchStart ou MouseDown, cela nous convient parfaitement car nous voulons faire un teste lors d’un clic (ou un touch) sur un objet. la méthode pick prend en paramètre des coordonnées écran (le pointeur de la souris / le doigt sur l’écran tactile) qu’elle va transformer en coordonnées 3D, de là un rayon sera créé et lancé pour tester si il y a quelque chose à cette position. Comme nous avons créé une propriété hexPosition sur chaque mesh, il est facile de récupérer la position de ce dernier et ainsi agir en conséquence (ajouter un bâtiment, un joueur, un ennemie, etc…). Dans la version finale, je vous propose de changer le material de l’objet sélectionné, mais pour des raison de place cette partie est disponible uniquement dans le projet final (à télécharger à la fin).

Bonus : Passer en mode réalité virtuelle

oculus-rift-inside_0Il y a une dernière chose que j’aimerais vous présenter, ça n’a pas vraiment de rapport avec la génération de grille d’hexagones, mais c’est tellement rapide et fun à implémenter qu’il fallait que je clôture dessus, je parle évidement de la réalité virtuelle ! Vous connaissez sans doute l’Oculus Rift et peut être même le Google Cardboard et autre casques dérivés comme le Homido, le Dive ou encore le Samsung Gear VR pour ne citer qu’eux. La prochaine modification se fait dans le fichier Tutorial.js, on va en réalité remplacer la caméra actuelle (ArcRotateCamera) par une caméra conçue pour la réalité virtuelle (elle affichera deux images).

Vue "VR" de notre réalisation
Vue « VR » de notre réalisation

Note du 16/10/2015 : Ces fonctionnalités font partie de Babylon.js 1.14, encore en développement. Le PostProccess sur mobile n’est plus performant, il est donc normal, aujourd’hui, d’avoir des performances mauvaises, cela sera réglé lors de prochains correctifs.

Ajouter le support pour casques VR mobiles (Google Cardboard, Homidi, Dive, Samsung Gear VR, etc..)

var camera = new BABYLON.VRDeviceOrientationCamera("VRDeviceCamera", new BABYLON.Vector3(0, 3, 0), scene);

Une seule ligne et vous activez le support des casques virtuelles mobiles ! Les rotations de la caméra seront automatiquement mises à jour en fonction de la rotation de votre tête, exactement comme pour l’Oculus Rift ! Placez votre mobile dans un Cardboard ou un Homido et vous voilà immergé dans votre monde 3D ! Pour plus de confort vous pouvez passer en mode plein écran avec BABYLON.Tools.RequestFullscreen(canvas).

Ajouter le support de l’Oculus Rift (Internet Explorer 11 uniquement)

Si vous avez un Oculus Rift DK1 ou DK2, il est possible de l’utiliser nativement avec Internet Explorer en installant ce petit plugin. De là vous pouvez utiliser une BABYLON.OculusCamera et vous n’aurez rien à faire de plus !

var camera = new BABYLON.OculusCamera("OculusCamera", new BABYLON.Vector3(0, 3, 0), scene);

Ajouter le support de l’Oculus Rift via WebVR

Encore une fois, si vous avez un Rift mais que vous ne voulez pas utiliser Internet Explorer alors il est quand même possible d’utiliser votre casque avec les navigateurs Chrome et Firefox en version « VR ». Ce sont des branches qui supportent les périphériques de réalité virtuelle (Il n’y a pas que l’Oculus Rift de supporté d’ailleurs, le Leap Motion fait partie du lot). Je vous invite à lire mon article dédié à ce sujet sur le blog de Wanadev.

var camera = new BABYLON.WebVRCamera("VRDeviceCamera", new BABYLON.Vector3(0, 3, 0), scene);

Conclusion

Créer une grille en 3D avec des hexagones est relativement simple à mettre en places, il y a quelques petites choses à savoir concernant les coordonnées, mais après cette étape c’est très facile de travailler avec. Le moteur Babylon.js nous permet d’aller très vite dans la réalisation de cette tâche, et vous noterez que nous n’avons pas perdu de temps lors de la détection des collisions avec un clic de souris. En bonus nous avons vue comment activer le mode VR qui vous permettra de rendre encore plus immersif vos créations.

Voici quelques liens à étudier pour aller plus loin dans le sujet

Vous pouvez télécharger les sources de cet article sur via le dépôt github dédié à ce blog. Amusez vous bien !