Tutoriel Three.js

Florian
16 min readDec 19, 2020

--

Intégrer un modèle 3D dans un site web

scène 3D sur navigateur réalisée grâce à Three.JS

Dans ce tutoriel je vais vous apprendre à créer une scène 3D sur votre navigateur mettant en avant la moto phare du manga dragon ball Z ! Le but de ce tutoriel sera de vous permettre de comprendre les bases de three.js afin que vous puissiez à votre tour créer de nouveaux projets 3D toujours plus innovant en creusant la documentation du site three.js. Je vais partir du constat que vous avez quelques notions en 3D, html, css et javascript. Afin d’y aller pas à pas, j’ai divisé ce cours en 2 exercices, le premier exercice sera chargé de vous apprendre les bases de three.js et le 2ème sera chargé de les approfondir.

I/ Premier exercice : le cube 📚

screen du rendu du cube

Création du document html

Créons dès à présent notre fichier html. Copier collez le code suivant dans un éditeur de code ( brackets, notepad ++, sublime text peu importe…).

|<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Capsule corp</title>
<style>
body {
margin: 0;
}

canvas {
display: block;
}

#c {
width: 100%;
height: 100%;
display: block;
top: 0;
left: 0;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<!-- le fichier principal three.js-->
<script src="https://threejs.org/build/three.min.js"></script>
<!-- Une extension de Three.js qui permet de faire une rotation ou zoomer sur un objet -->
<script src='https://threejs.org/examples/js/controls/OrbitControls.js'></script>
<script>
// Ici notre Javascript personnalisé
</script>
</body>
</html>

Prenons quelques instant pour expliquer ce bout de code.

L’élément <canvas></canvas> permet d’afficher la scène 3D dans le navigateur. 3 scripts sont ensuite appelé :

  • Le 1er script permet d’appeler l’api three.js
<script src="https://threejs.org/build/three.min.js"></script>
  • Le 2ème permettra de contrôler notre cube 3D afin que l’internaute puisse le manipuler
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

Le 3ème script nous permettra d’écrire notre code javascript personnalisé.

<script>
// Ici notre Javascript personnalisé
</script>

Prêt à écrire votre premier script three.Js 🤗 ?

C’est partie ! Rendez-vous dans la partie “ Ici notre Javascript personnalisé ”

Pour pouvoir afficher quoi que ce soit avec three.js, nous avons besoin de 5 éléments: la scène, la caméra, le moteur de rendu, un objet 3D et une boucle de rendu.

1. La scène 🎬

Pour créer une nouvelle scène avec Javascript nous devons faire référence à cette scène en créant une variable comme ceci :

var scene = new THREE.Scene();

2. La caméra 🎥

Afin de rendre une scène nous avons besoin d’une caméra. Ajoutons la dès à présent :

var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

Que signifie tout cela ?

Afin de créer une caméra nous devons paramétrer ses réglages. Le premier attribut désigne la taille de la focale ( 50 dans notre exemple) il sera exprimé en degré. Le second attribut est le rapport hauteur / largeur de la caméra par rapport à la taille du navigateur, afin de ne pas obtenir une image écrasé il convient de toujours écrire “ window .innerWidth / window .innerHeight ” Les deux derniers attributs désignent la zone de rendu de la caméra. Cela signifie que les objets plus éloignés de la caméra que la valeur 1000 ou plus proche que la valeur 0.1 ne seront pas rendus.

camera.position.z = 5; ” permet de placer votre caméra sur l’axe z de la scène. En effet, si je ne précise pas la position de la caméra, le navigateur positionnera votre caméra au centre sur l’axe x,y et z. Afin de pouvoir visionner mon cube qui sera positionné au centre de la scène j’ai besoin de reculer ma caméra d’une distance de 5 sur l’axe z.

3. Le moteur de rendu ⚙️

Tout d’abord je référence l’élément canvas

var canvas = document.querySelector('#c');

Ensuite je déclare un nouveau WEBGL (la technologie qui permet de gérer la 3D dans le navigateur), nous y ajouterons un anti-crénelage afin de créer des bords plus lisse lors du rendu.

// Lance le moteur de rendu 

var renderer = new THREE.WebGLRenderer({
canvas, antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

4. L’objet 3D 📦

Il est temps à présent d’ajouter notre cube à la scène avec le code suivant

var geometry = new THREE.BoxGeometry();
var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);

Détaillons un peu ce bout de code.

Pour créer un cube en 3D nous avons besoin de l’objet boxGeometry, c’est un objet qui contient tous les sommets et faces du cube. Ce qui donne la variable suivante : “ var geometry = new THREE.BoxGeometry();

Ensuite nous devons définir le matériaux du cube pour définir sa matière, il existe plusieurs type de matériaux mais nous utiliserons le matériaux “MeshBasicMaterial” Ce qui donne : “ var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );

Tous les matériaux prennent un objet de propriétés qui leur sera appliqué. Pour rester simple, nous fournissons uniquement un attribut de couleur de 0x00ff00 , qui est vert. Cela fonctionne de la même manière que les couleurs fonctionnent dans CSS ou Photoshop ( couleurs hexadécimales ) il suffit juste de remplacer “#” par “0x” au début du code hexadécimal.

La troisième chose dont nous avons besoin est un maillage . Un maillage est un objet qui prend une géométrie et lui applique un matériaux. Ce qui donne : “ var cube = new THREE.Mesh( geometry, material ); ”

Enfin nous appelons “scene.add ()” afin d’ajouter le cube à la scène.

5. La boucle de rendu 🎞️

Si vous copiez le code ci-dessus dans le fichier HTML que nous avons créé précédemment, vous ne pourrez rien voir. C’est parce que nous ne rendons encore rien. Pour cela, nous avons besoin de ce qu’on appelle une boucle de rendu ou d’animation .

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

Cela créera une boucle qui amènera le moteur de rendu à dessiner la scène à chaque fois que l’écran est actualisé (sur un écran typique, cela signifie 60 fois par seconde). Si tout c’est bien passé vous devriez voir apparaître un jolie cube vert lorsque vous lancez votre code.

Qu’est ce ce que c’est que cette arnaque ! Je ne vois qu’un carré vert en 2D ! Pas d’inquiétude, c’est vraiment un cube mais vous ne pouvez pas encore le manipuler car nous devons ajouter la fonction “controls” afin de faire fonctionner le script chargé de permettre la manipulation d’objet.

6. Ajout des contrôles pour l’utilisateur 👋

Au dessus de notre fonction “animate” nous allons ajouter ceci

var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;

La variable “var controls” permet d’appeler le module OrbitControls de three.js, nous pouvons ajouter plusieurs paramètres à la variable controls ici nous avons rajouté le paramètre “Damping” qui donne un effet d’amortissement lorsque l’on tournera le cube, ce qui lui donnera la sensation qu’il pèse un certain poids.

Pour finir à l’intérieur de la fonction “animate” ajoutez controls .update ();

function animate(){
controls.update();
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();

Et voilà vous maîtrisez à présent les bases de three.js ! Voici le code en entier pour ceux qui se sont perdu en cours de route.

code du cube 3D

II/ Deuxième exercice : la moto 📚

screen du rendu final

1. Création d’un serveur web pour le développement web local 💻

logo du logiciel servez

Avant toute chose il faut savoir que nous ne pouvons pas charger un modèle 3D sur un serveur local, si vous essayez de suivre ce tutoriel avec votre propre serveur, vous obtiendrez une plage blanche avec ce code erreur affiché dans la console de votre navigateur.

Pour contourner ce problème il suffit de charger ce code dans un hébergement en ligne ou bien d’installer le logiciel Servez qui est gratuit et simulera un serveur web. J’utiliserai la deuxième solution pour cet exercice, ne vous inquiétez pas il est très simple d’utilisation. Créez un dossier “projet_moto” sur votre bureau puis à l’intérieur créez un nouveau fichier html que vous nommerez “moto.html”. Copiez-collez le code du cube dans votre nouveau fichier html.

Ouvrez Servez puis collez la destination de votre fichier dans mon cas ce sera C:\Users\flori\OneDrive\Bureau\tuto\projet_moto

screen servez

Lancez le serveur et cliquez sur votre fichier moto.html

extrait du navigateur

Vous devriez voir apparaître votre cube avec un nouveau serveur localhost dans le navigateur.

nouveau serveur localhost

2. Téléchargement du modèle 3D 🏍️

Nous allons commencer par télécharger notre modèle 3D en nous rendant sur la bibliothèque en ligne de sketchfab, le lien est disponible ici.

modèle 3D réalisé par JoseVG sur sketchfab

Quand vous cliquerez sur télécharger le modèle, une pop-up va s’ouvrir, il est important de télécharger le modèle au format gltf uniquement.

options d’export sur sketchfab

3. Conversion du fichier compressé en .glb 📁

Nous avons besoin de convertir le dossier compressé que nous avons téléchargé en fichier “.glb”. Décompressez votre zip et rendez-vous sur le site https://glb-packer.glitch.me/. Faites un drag and drop de l’ensemble des fichiers ( textures, scene.bin et scene.glb) sur le le site glbpacker.

site pour convertir des fichiers gltf au format glb

Un nouveau fichier nommé “out.glb” va être téléchargé sur votre navigateur. Renommez ce fichier en “moto.glb”. Récupérez le et et glissez le au sein de votre dossier projet_moto.

4. Intégration du modèle 3D 👨‍💻

Parfait ! Maintenant que nous avons notre serveur et notre fichier 3D au format .glb nous pouvons nous remettre à coder ! Commençons par ajouter le script permettant de charger des modèles 3D au dessus de notre script personnalisé :

<script src='https://cdn.jsdelivr.net/gh/mrdoob/three.js@r92/examples/js/loaders/GLTFLoader.js'></script>
<script>

Dans notre script personnalisé en dessous de “ scene.background = new THREE.Color(0x000000); ” nous allons référencer le modèle 3D “moto.glb” en déclarant la variable “ theModel ” et la constante “ Model_PATH ”.

Ce qui donne :

 var scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
// référencement du modèle 3D
var theModel;
const MODEL_PATH = "moto.glb";

Cela définit la “ theModel ” scène entière de nos modèles 3D. Nous pouvons maintenant créer un nouveau chargeur et utiliser la méthode “ load ” . Sous la variable de la caméra nous collons cet extrait de code.

// Initialise le chargeur d'objet
var loader = new THREE.GLTFLoader();
loader.load(MODEL_PATH, function (gltf) {

theModel = gltf.scene;
// Dimensions du modèle 3D
theModel.scale.set(2.2, 2.2, 2.2);

// Positionnement du modèle sur l'axe y
theModel.position.y = -1;

// Ajoute le modèle à la scène
scene.add(theModel);

}, undefined, function (error) {
console.error(error)
});

Qu’est ce que nous venons de faire ?

Nous avons crées un chargeur avec la variable loader. Nous avons défini les dimensions du modèle 3D dans notre scène, avec “ theModel.glb.scale.set ”, les 3 chiffres dans la parenthèse ( 2.2 , 2.2 ,2. 2 ) représente ses dimension sur l’axe x,y et z.

“ theModel.position.y = -1 ” permet de positionner le modèle sur le sol.

Le dernier paramètre gère les erreurs si notre fonction est fausse. Nous n’avons plus besoin du code du cube nous pouvons supprimer l’extrait suivant :

var geometry = new THREE.BoxGeometry();
var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);

Ok lancez à présent votre code sur servez, vous devriez voir apparaître un phare au beau milieu d’un fond noir.

Voici le code que nous avons écrit jusque là ( Vous noterez que j’ai changé le lien de redirection de la constante TheModel, qui pointe vers un lien, n’en tenez pas compte, j’avais besoin de faire ça pour intégrer le fichier .glb sur codepen)

Ok lancez à présent votre code sur servez, vous devriez voir apparaître un phare au beau milieu d’un fond noir.

screen avec importation du modèle 3D sans lumière

Si la scène est noir c’est parceque notre objet 3D n’est pas éclairée, ça tombe bien, ajouter des lumières c’est la prochaine étape !

5. Ajouts de lumières 💡

Il existe un certain nombre de lumières disponibles pour Three.js, et un certain nombre d’options pour les modifier toutes. Nous n’utiliserons que la “point Light” dans cet exercice, si vous désirez en savoir plus sur les différents éclairages, n’hésitez pas à consulter la documentation du site officiel de three.js. Ajoutons dès à présent le code de nos 4 points lights sous const MODEL_PATH = “moto.glb”.

//-------------------------- début chargement des lumières--------------------------------------// début point light 1
var pointLight = new THREE.PointLight(0x546AFF, 4, 30);
pointLight.position.set(5, 3, 2);
scene.add(pointLight);
/*
var sphereSize = 0.4;
var pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );
scene.add( pointLightHelper );
*/
// fin point light 1// début point light 2
var pointLight = new THREE.PointLight(0xffffff, 4, 10);
pointLight.position.set(-1, 3, 3);
pointLight.castShadow = true;
pointLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
scene.add(pointLight);
/*
var sphereSize = 0.4;
var pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );
scene.add( pointLightHelper );
*/
// fin point light 2
// début point light 3
var pointLight = new THREE.PointLight(0xFF7B00, 4, 40);
pointLight.position.set(-4, 2, 0);
scene.add(pointLight);
/*
var sphereSize = 0.4;
var pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );
scene.add( pointLightHelper );
*/
// fin point light 3


// début point light 4
var pointLight = new THREE.PointLight(0xFF7B00, 2, 4);
pointLight.position.set(-0.6, 1, -3);
scene.add(pointLight);
/*
var sphereSize = 0.4;
var pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );
scene.add( pointLightHelper );
*/

// fin point light 4
//--------------------fin chargement des lumières--------------

Votre moto apparait enfin ! 😎

screen moto avec importation des lumières

Comment tout cela fonctionne-t-il ?

Parlons des point lights

Intéressons nous aux chiffres situés dans la parenthèse de l’expression suivante :

var pointLight = new THREE.PointLight(0x546AFF, 4, 30);
  • Le premier attribut 0x546AFF désigne la couleur hexadécimal du ciel
  • Le second attribut 4 désigne l’intensité de la lumière
  • Le troisième attribut 30 désigne la portée de la lumière

Analysons à présent cette expression :

pointLight.position.set(5, 3, 2);

Les attributs situés dans la parenthèse positionne la lumière sur l’axe x,y et z.

Afin de voir où sont situé chacune de mes points lights j’ai utilisé l’expression pointLightHelper qui permet de générer un losange en 3D d’une taille de 0.4

var sphereSize = 0.4;
var pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );
scene.add( pointLightHelper );

Si vous dé-zoomez légèrement la scène avec le scroll vous pourrez observer les 4 points lights de la scène, c’est très pratique car sans ce bout de code vous ne pouvez pas savoir exactement où se situe vos lumières.

screen moto avec visualisation des 4 points lights

Une fois que vous êtes satisfait du placement de vos lights vous pouvez mettre ce bout de code en commentaire afin de faire disparaître les losanges.

Le code de la point light 2 est plus long que les autres car j’ai dû rajouter ces 2 expressions :

pointLight.castShadow = true;
pointLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
  • pointLight.castShadow= true; permet d’indiquer à three.js de calculer les ombres crées par ma deuxième point light.
  • pointLight.shadow.mapSize définit la qualité du rendu de l’ombre en pixel. En effet, si vous réduisez les chiffres entre parenthèse vous pourrez observez que l’ombre pixelise ou au contraire augmente en netteté si vous les augmentez. Plus les chiffres seront élevés et plus les temps de rendus augmenteront sur votre navigateur, 1024*1024px est une valeur raisonnable.

Vous allez me dire “ Je ne vois pourtant pas d’ombre sur le sol “, c’est normal nous ne l’avons pas encore créé.

6. Ajout du sol ⚒️

Pour créer un sol il faut écrire l’expression suivante sous nos lumières.

// Sol
var floorGeometry = new THREE.PlaneGeometry(5000, 5000);
var floorMaterial = new THREE.MeshPhongMaterial({
color: 0x000000,
shininess: 0
});

var floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -0.5 * Math.PI;
floor.receiveShadow = true;
floor.position.y = -1;
scene.add(floor);

Vous devriez maintenant obtenir ceci

screen moto avec importation du sol

Vos lumières réfléchissent à présent sur le sol.

Analysons la première expression :

var floorGeometry = new THREE.PlaneGeometry(5000, 5000);

Les attributs dans la parenthèse définissent les dimensions sur l’axe x et y du sol, ici 5000*5000.

Parlons de la seconde expression :

var floorMaterial = new THREE.MeshPhongMaterial({
color: 0x000000,
shininess: 0
});

Elle définit un matériaux au sol nommé “MesPhongMaterial”, nous luis avons assigné une couleur gris foncé et une brillance nul avec shiness:0, nous pouvons ajouter plus de paramètre à ce genre de matériaux comme de la transparence, ou de la réflection mais nous n’en parlerons pas ici.

Le deuxième paragraphe créé une variable appelée floor et fusionne la géométrie et le matériau dans un maillage ce qui donne :

var floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -0.5 * Math.PI;
floor.receiveShadow = true;
floor.position.y = -1;
scene.add(floor);

Nous avons réglé la rotation du sol pour qu’elle soit plate grâce à “ floor.rotation.x = -0.5 * Math.PI;” opté pour la possibilité de recevoir des ombres grâce à “floor.receiveShadow = true;”, l’avons déplacée vers le bas de la même manière que nous avons abaissé la chaise avec “ floor.position.y = -1; ”, puis avons ajoutée à la scène avec “ scene.add(floor); ”.

Mais où sont les ombres ? Il y a deux choses que nous devons faire pour cela. Ajouter une expression pour déclarer le calcul des ombres dans le moteur de rendu et intégrer une expression qui indique à three.js de calculer les ombres projetées par notre moto 3D.

7. Calcul des ombres 👻

Tout d’abord, sous notre moteur de rendu nous devons intégrer l’expression suivante.

// Calcul des ombres dans le moteur de rendu
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio);
// Création des lumières réalistiques
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 2.3;

Nous avons activé le calcul d’ombre grâce à “ renderer.shadowMap.enabled = true;” et fixé le pixel ratio de l’écran avec celui de notre scène grâce à “ renderer.setPixelRatio(window.devicePixelRatio); ” Puis dans un second temps nous avons utilisé “ tone Mapping ” pour créer des ombres plus douces et plus réalistes au rendu.

A présent nous devons dire à three.js de calculer l’ombre de notre moto 3D afin qu’elle puisse projeter et recevoir des ombres. Pour ce faire : Ajoutez cette ligne sous “ theModel = gltf.scene;”

// Calcul des ombres sur le modèle 3D
theModel.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});

Pour terminer, retournez sur l’élément floor et changez la couleur du rouge (0x474040) au noir (0x000000). Vous devriez obtenir le résultat suivant

screen moto avec calcul des ombres

Nous avons presque terminé !

8. Ajout de paramètres de contrôles et rotation automatique 🥏

Bien, à présent afin que l’utilisateur ne puisse pas regarder sous le modèle nous allons compléter notre partie “controls” afin de restreindre les possibilités de mouvement avec “maxPolarAngle” et inclure la rotation automatique du modèle avec un “speed de 0.8”. Ce qui donne ceci :

// Add controls
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.maxPolarAngle = Math.PI / 2;
controls.minPolarAngle = Math.PI / 8;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.8; // 30

9. Responsive 📱

Pour l’instant notre site n’est pas responsive, si on regarde notre code sur mobile on obtiendra ceci :

screen mobile

Autre problème on peut observer que si l’on change la taille de notre fenêtre, le modèle 3D n’est plus au centre et devient pixelisé, nous allons remédier à tous ça.

screen de notre scène 3D après redimensionnement de la fenêtre

Dans un premier temps, afin que notre scène puisse prendre toute la taille de l’écran nous devons faire quelques petites opérations. Nous allons rajouter ce bout de code en tant que balise meta dans la partie <head><head> pour éviter les zooms involontaires ainsi que pour optimiser la compatibilité sur microsoft edge.

<!-- Méta-informations importantes pour rendre la page aussi rigide que possible sur les mobiles, pour éviter un zoom involontaire  -->
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">

Pour s’assurer que la scène 3D reste nette sur les téléphones mobiles ajoutez ceci au bas de votre javascript

// Function - New resizing method
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
var width = window.innerWidth;
var height = window.innerHeight;
var canvasPixelWidth = canvas.width / window.devicePixelRatio;
var canvasPixelHeight = canvas.height / window.devicePixelRatio;
const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}

Cette fonction écoute essentiellement 2 choses : la taille du canevas et la taille de la fenêtre, et renvoie un booléen selon que les deux tailles sont identiques ou non. Nous utiliserons cette fonction à l’intérieur de la fonction d’animation pour déterminer s’il faut refaire le rendu de la scène. Cette fonction va également prendre en compte le rapport de pixels de l’appareil pour s’assurer que notre scène 3D reste nette quelque soit la taille de l’écran.

Maintenant, afin que notre fonction “ recizing ” soit prise en compte dans le calcul de rendu, mettez à jour votre fonction d’animation pour qu’elle ressemble à ceci :

function animate() {
controls.update();
requestAnimationFrame(animate);
renderer.render(scene, camera);
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}

À présent votre modèle 3D sera nette quelque soit les résolutions d’écrans. Afin que la moto reste centré à chaque fois mettez à jour le css du body.

body {display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
overflow: hidden;
}

Et voilà ! Votre site est à présent responsive, regardez le résultat sur mobile :)

screen mobile

À présent vous connaissez les bases de three.js, en espérant que ce tutoriel vous aura donné envie de vous former davantage pour en savoir plus.

Merci pour votre attention 😁 N’hésitez pas à me faire vos retours afin d’améliorer ce tutoriel.

Je suis Product designer et je m’intéresse au développement front-end à mes heures perdues, pour en savoir plus sur mon travail rendez-vous ici https://floriangouloubi.com/

Si vous voulez supporter mon travail et m’offrir un café rendez-vous sur https://www.buymeacoffee.com/flopinou

--

--

Florian
Florian

No responses yet