Three.JS physics

Comprendre et utiliser la physique dans Three.JS avec Ammo.js : Les bases

Avant de commencer la mise en place de notre projet basé sur l’utilisation couplée de Three.JS et Ammo.js, je vous conseille de lire l’introduction de ce thème :

Introduction – Monde physique et monde graphique

Tout d’abord il est très important de faire la distinction entre l’univers physique et graphique. Lorsque nous utiliserons le duo Three.JS et Ammo.js, ces deux concepts travaillerons séparément.

Le monde physique représente l’univers simulé dans lequel le moteur physique calcule les interactions basées sur des lois ( collisions, gravité, résistance, friction …) . Cette simulation dynamique est gérée par Ammo.js.

Le monde graphique inclus l’ensemble des fonctionnalités visuelles de rendu 3D avec lesquelles nous avons déjà travaillé dans les divers articles de mon blog. Le rendu 3D est géré par Three.JS.

Ammo.js and Three.JS
Univers Graphique et Univers Physique – Simplification

Mise en place d’un environnement de base

Commençons par la mise en place d’un environnement HTML / CSS de base :

<head>
	<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />

	<style>
		body
		{
			margin: 0; touch-action: none;
			background-image: -webkit-gradient(linear, 0 0, 100% 100%, color-stop(0, #877fa8), color-stop(100%, #f9ae91));
			background-image: -webkit-linear-gradient(135deg, #877fa8, #f9ae91);
			background-image: -moz-linear-gradient(45deg, #877fa8, #f9ae91);
			background-image: -ms-linear-gradient(45deg, #877fa8 0, #f9ae91 100%);
			background-image: -o-linear-gradient(45deg, #877fa8, #f9ae91);
			background-image: linear-gradient(135deg, #877fa8, #f9ae91);
		}

		canvas { width: 100%; height: 100% ; touch-action: none;}
	</style>
</head>

<body>
</body>

Puis, dans la balise head, importons la librairie Ammo.js et préparons une balise script module :

<script src="../js/ammo.js"></script>

<script  type="module">
    //code
</script>

Pour finir, importons le module Three.JS dans notre balise script :

import * as THREE from '../js/build/three.module.js';

Notre environnement de base est prêt !

Configuration de Ammo.js et de l’univers physique

Commençons par initialiser la librairie Ammo.js dans notre module JavaScript. Pour cela, utilisons le code de base ci dessous :

// ------ Ammo.js Init ------

Ammo().then( AmmoStart );

function AmmoStart()
{
    //code
}

La fonction AmmoStart sera exécutée lorsque la librairie Ammo.js sera initialisée. Cette fonction sera le point d’entrée de notre code.

Puis, créons une variable globale physicsUniverse :

// Ammo.js
var physicsUniverse 	= undefined;

Lorsque c’est fait, créons une fonction initPhysicsUniverse destinée à l’initialisation de notre univers dynamique :

// ------ Phisics World setup ------

function initPhysicsUniverse()
{
    var collisionConfiguration  = new Ammo.btDefaultCollisionConfiguration();
    var dispatcher              = new Ammo.btCollisionDispatcher(collisionConfiguration);
    var overlappingPairCache    = new Ammo.btDbvtBroadphase();
    var solver                  = new Ammo.btSequentialImpulseConstraintSolver();

    physicsUniverse           	= new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
    physicsUniverse.setGravity(new Ammo.btVector3(0, -75, 0));
}

Voici rapidement quelques précisions sur ce code, ces informations sont principalement extraites de la documentation originale de Bullet physics engine :

  • btDefaultCollisionConfiguration configure la détection des collisions par Bullet ( Ammo.js).
  • btCollisionDispatcher prend en charge les algorithmes qui gèrent les collisions convexes et concaves.
  • btSequentialImpulseConstraintSolver permet le calcul de la résolution des contraintes en fonction des règles physiques de notre univers (gravité, forces …).
  • btDiscreteDynamicsWorld correspond à notre monde dynamique, c’est le type de notre variable physicsUniverse.

Et pour finir, nous avons configuré la gravité avec une valeur de -75 sur l’axe Y, grâce à la méthode setGravity.

Ammo.js et notre univers physique sont désormais configurés.

Configuration de Three.JS et de l’univers graphique

A présent, occupons nous de notre univers graphique Three.JS !

Créons une fonction initGraphicsUniverse :

// ------ Three.js setup ------

function initGraphicsUniverse()
{
    //code
}

Puis, créons quelques variables globales que nous initialiserons dans initGraphicsUniverse :

// Three.js
var scene			= undefined ;
var camera			= undefined ;
var renderer 			= undefined ;
var clock 			= undefined ;

Cette partie concerne uniquement Three.JS et ses concepts de base, elle vous sera normalement plus familière. Mais, si ce n’est pas le cas, je vous conseille de vous rafraîchir la mémoire avec cet article :

Dans initGraphicsUniverse, commençons par créer notre horloge Three.JS :

clock = new THREE.Clock();

Puis, initialisons notre scène :

scene = new THREE.Scene();

Poursuivons avec notre caméra :

camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.set( -25, 20, -25 );
camera.lookAt(new THREE.Vector3(0, 6, 0));

Continuons avec le moteur de rendu :

//renderer
renderer = new THREE.WebGLRenderer({antialias : true, alpha : true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement) ;

Et pour finir, éclairons notre scène :

//light
var ambientLight = new THREE.AmbientLight(0xcccccc, 0.2);
scene.add(ambientLight);

var directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(-1, 0.9, 0.4);
scene.add(directionalLight);

Rigid Body et structure géométrique de collision

Commençons par définir certains concepts. On appelle Rigid Body toute entité de notre univers physique soumise à des forces dynamiques ( ayant une masse, une vélocité et capable d’entrer en collision …).

Il est donc nécessaire de créer des Rigid Bodies dans notre univers physique afin de pouvoir correctement simuler les forces dynamiques. Pour cela, chaque objet 3D (de l’univers graphique) que nous souhaitons inclure dans la simulation physique, aura son Rigid Body (dans l’univers physique) .

Mais, chaque objet 3D de la simulation n’a pas la même forme, et donc ne réagis pas de la même façon a la physique. Par exemple, il est plus facile d’empiler des Rigid Bodies cubiques que sphériques !

C’est pour cette raison que les Rigid Bodies n’ont pas tous la même forme géométrique. C’est ce que l’on appelle la structure géométrique de collision.

Prenons un exemple simple : Nous souhaitons créer un cube 3D réagissant à la physique. Coté univers graphique nous allons créer un Mesh composé d’une Geometry de type BoxGeometry et un Material. Puis, coté univers physique, créer un Rigid Body avec une structure géométrique de collision cubique, de type btBoxShape.

Three.JS and Ammo.js cube
Notre cube composé des concepts physiques et graphiques

Revenons dans notre code, créons une variable globale rigidBody_List de type Array. Cette dernière listera tous nos Rigid Bodies.

var rigidBody_List = new Array();

Création d’objets 3D dans les univers graphiques et physiques

Comme expliqué plus haut, les univers graphiques et physiques fonctionnent séparément. Mais, pour chaque objet 3D graphique que nous souhaitons inclure dans la simulation dynamique, il faut créer un objet physique qui lui est lié !

Paramètres et variables de configuration de l’objet

Pour cela, créons une fonction createCube qui sera chargée de facilement créer des cubes 3D synchronisés dans les deux univers.

Cette méthode nécessite deux paramètres :

  • scale – Un nombre représentant la taille de notre cube.
  • position – Un objet de type Vector3 contenant la position du cube sur les trois axes XYZ.
  • mass – La masse physique de notre objet 3D.
  • rot_quaternion – La rotation initiale de notre cube (Quaternion).

Il existe des outils en ligne très pratiques pour visualiser et convertir facilement les quaternions !

function createCube(scale, position, mass, rot_quaternion)
{
    // code
}

Dans notre fonction createCube, créons une variable quaternion qui prend la valeur du paramètre rot_quaternion, si celui ci est défini ! Si ce n’est pas le cas, quaternion prend une valeur par défaut.

Nous devons créer notre objet 3D dans les deux univers, et le faire à base de valeurs contenues dans des paramètres ou variables nous simplifiera la tache et limitera les erreurs de code.

Création dans l’univers graphique

Commençons par créer l’objet 3D de l’univers graphique (Three.JS).

function createCube(scale , position, mass, rot_quaternion)
{
    let quaternion = undefined;

    if(rot_quaternion == null)
    {
        quaternion = {x: 0, y: 0, z: 0, w:  1};
    }
    else
    {
      quaternion = rot_quaternion;
    }

    // ------ Graphics Universe - Three.JS ------
    let newcube = new THREE.Mesh(new THREE.BoxBufferGeometry(scale, scale, scale), new THREE.MeshPhongMaterial({color: Math.random() * 0xffffff}));
    newcube.position.set(position.x, position.y, position.z);
    scene.add(newcube);


    [...]

Création dans l’univers physique

Puis, toujours dans notre fonction createCube, occupons nous de l’univers physique. Commençons par la partie Default Motion State qui précisera la position et la rotation initiale de notre objet dans l’univers physique :

// ------ Physics Universe - Ammo.js ------
let transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin( new Ammo.btVector3( position.x, position.y, position.z ) );
transform.setRotation( new Ammo.btQuaternion( quaternion.x, quaternion.y, quaternion.z, quaternion.w ) );
let defaultMotionState = new Ammo.btDefaultMotionState( transform );

Continuons avec la structure géométrique de collision de notre objet :

let structColShape = new Ammo.btBoxShape( new Ammo.btVector3( scale*0.5, scale*0.5, scale*0.5 ) );
structColShape.setMargin( 0.05 );

Occupons nous du calcul de son inertie initiale (important si vous définissez une rotation initiale par exemple) :

let localInertia = new Ammo.btVector3( 0, 0, 0 );
structColShape.calculateLocalInertia( mass, localInertia );

Puis, créons notre Rigid Body avec les éléments que nous venons d’initialiser :

let RBody_Info = new Ammo.btRigidBodyConstructionInfo( mass, defaultMotionState, structColShape, localInertia );
let RBody = new Ammo.btRigidBody( RBody_Info );

Ensuite, nous ajoutons ce Rigid Body dans l’univers physique :

physicsUniverse.addRigidBody( RBody );

C’est ici que la liaison entre l’univers physique et graphique s’effectue.

Définissons ce nouveau Rigid Body comme valeur de propriété userData.physicsBody de notre cube 3D newcube :

newcube.userData.physicsBody = RBody;

Et pour finir notre fonction createCube, ajoutons notre cube newcube dans la liste rigidBody_List :

rigidBody_List.push(newcube);

Notre fonction createCube est enfin terminée ! Cette dernière est désormais capable de créer des cubes 3D dans les deux univers, physique et graphique.

Mise a jour de la simulation physique en fonction du temps

Maintenant que nous pouvons créer des objets 3D dynamiques, nous devons êtres capables de faire avancer la simulation physique en fonction du temps écoulé.

Pour cela, créons une fonction updatePhysicsUniverse et son paramètre deltaTime.

Dans cette dernière, utilisons la fonction stepSimulation pour mettre à jour la simulation dynamique en fonction du temps écoulé :

function updatePhysicsUniverse( deltaTime )
{
    physicsUniverse.stepSimulation( deltaTime, 10 );
}

Toujours dans updatePhysicsUniverse, créons une structure for pour boucler sur chaque élément de rigidBody_List :

for ( let i = 0; i < rigidBody_List.length; i++ )
{
    //code
}

Dans notre structure for, créons deux variables pour stocker les objets physiques et graphiques du tour de boucle courant :

 let Graphics_Obj = rigidBody_List[ i ];
 let Physics_Obj = Graphics_Obj.userData.physicsBody;

Nous allons désormais extraire de l’univers physique, la position et la rotation, mises à jour par la simulation dynamique, et les injecter dans l’univers graphique. Ainsi, les modifications apportées par la simulation de l’univers physique seront visibles dans l’univers graphique.

Commençons par créer une variable globale tmpTransformation en haut de notre module JavaScript. Elle nous servira à stocker temporairement la transformation à appliquer lors de chaque tour de boucle.

var tmpTransformation 	= undefined;

Puis, initialisons cette variable dans la fonction AmmoStart , le point d’entrée de notre code :

tmpTransformation = new Ammo.btTransform();

Une fois cette variable créée et initialisée, retournons dans la boucle for de notre fonction updatePhysicsUniverse.

Comme expliqué plus haut, extrayons la position et la rotation de chaque objet physique, pour l’injecter dans son équivalent de l’univers graphique :

let motionState = Physics_Obj.getMotionState();
if ( motionState )
{
    motionState.getWorldTransform( tmpTransformation );
    let new_pos = tmpTransformation.getOrigin();
    let new_qua = tmpTransformation.getRotation();
    Graphics_Obj.position.set( new_pos.x(), new_pos.y(), new_pos.z() );
    Graphics_Obj.quaternion.set( new_qua.x(), new_qua.y(), new_qua.z(), new_qua.w() );
}

Cette opération sera effectuée pour chaque tour de boucle, donc pour chaque objet de la liste rigidBody_List.

Désormais, lors de l’appel de updatePhysicsUniverse, nos univers physiques et graphiques seront synchronisés !

Finalisation du projet

La boucle d’animation – render

Créons une boucle d’animation render classique, nous y appellerons updatePhysicsUniverse :

function render()
{
        let deltaTime = clock.getDelta();
        updatePhysicsUniverse( deltaTime );
				
        renderer.render( scene, camera );
        requestAnimationFrame( render );
}

Initialisation de nos univers

Puis, appelons nos fonctions d’initialisations initPhysicsUniverse et initGraphicsUniverse dans la fonction AmmoStart:

function AmmoStart()
{
      tmpTransformation = new Ammo.btTransform();

      initPhysicsUniverse();
      initGraphicsUniverse();

}

Création de cubes avec createCube

Toujours dans AmmoStart, utilisons notre fonction createCube pour créer des cubes dynamiques !

Commençons par créer un cube avec une valeur de masse égale à 0. Ainsi, il ne sera pas affecté par la gravité, ce sera notre support.

createCube(40 , new THREE.Vector3(15, -30, 15) , 0 );

Puis, créons autant de cube que nous le souhaitons, avec une masse positive et une position initiale en hauteur. Ces derniers seront en chute libre jusqu’à ce qu’ils entrent en collision avec notre cube “support” :

createCube(4 , new THREE.Vector3(0, 10, 0) , 1, null );
createCube(2 , new THREE.Vector3(10, 30, 0) , 1, null );
createCube(4 , new THREE.Vector3(10, 20, 10) , 1, null );
createCube(6 , new THREE.Vector3(5, 40, 20) , 1, null );
createCube(8 , new THREE.Vector3(25, 100, 5) , 1, null );
createCube(8 , new THREE.Vector3(20, 60, 25) , 1, null );
createCube(4 , new THREE.Vector3(20, 100, 25) , 1, null );
createCube(2 , new THREE.Vector3(20, 200, 25) , 1, null );

Finalisation de la fonction AmmoStart

Pour finir, ajoutons l’appel de la fonction render à la fin de AmmoStart pour démarrer la boucle principale.

Voici l’état final de notre fonction AmmoStart :

// ------ Ammo.js Init ------

Ammo().then( AmmoStart );

function AmmoStart()
{
    tmpTransformation = new Ammo.btTransform();

    initPhysicsUniverse();
    initGraphicsUniverse();

    // base
    createCube(40 , new THREE.Vector3(10, -30, 10) , 0 );

    // falling cubes
    createCube(4 , new THREE.Vector3(0, 10, 0) , 1, null );
    createCube(2 , new THREE.Vector3(10, 30, 0) , 1, null );
    createCube(4 , new THREE.Vector3(10, 20, 10) , 1, null );
    createCube(6 , new THREE.Vector3(5, 40, 20) , 1, null );
    createCube(8 , new THREE.Vector3(25, 100, 5) , 1, null );
    createCube(8 , new THREE.Vector3(20, 60, 25) , 1, null );
    createCube(4 , new THREE.Vector3(20, 100, 25) , 1, null );
    createCube(2 , new THREE.Vector3(20, 200, 25) , 1, null );

    render();
}

Conclusion et Résultat final

Nous arrivons à la fin de ce premier chapitre. Il nous reste encore beaucoup à apprendre concernant l’utilisation de la physique dans Three.JS, c’est en effet un très vaste sujet !

Voici un aperçu de l’état final de notre premier projet :

Notre premier projet

Les bases de notre projet dynamique sont désormais posées. Il nous est maintenant possible de créer facilement des scènes physiques un peu plus complexes !

Dans le prochain chapitre, nous réaliserons cet exemple assez spectaculaire :

(2 commentaires)

Les commentaires sont fermés.