Sac 3D avec Three.js

En 2013 j'avais réalisé une petite démo avec Three.js. À l'époque j'avais pas mal galéré, car je n'avais pas beaucoup de connaissance en JavaScript. Quand est-il en 2024 avec toutes les IA disponibles? J'ai donc décidé de refaire la démo avec chatGPT 3.5 (parce qu'il est gratuit) et Gemini (de Google). Le moins que l'on puisse dire est qu'elles tuent en partie le métier. Tu n'as quasi besoin d'aucune compétence de développeur et tu pourras t'en sortir avec quelques copiés-collés.

Pin It
 

Pour les ressources, il vous faudra un serveur web, npm, pour le modèle je l'ai téléchargé ici (chez cgtrader): https://www.cgtrader.com/search?free=1&file_types%5B%5D=21&keywords=bag (il vous faudra créer un compte après c'est gratuit)

La démo ci-dessous m'a pris une petite heure pour chaque exemple. Le plus long a été de trouver un modèle 3D gratuit sur le net. Blender est toujours très bien, mais c'est nettement plus rapide de chercher un modèle déjà tout fait.

Cependant, les IA ont leurs limites. Par exemple, j'ai demandé à ChatGPT de bouger l'éclairage. Malheureusement, il faisait référence à un objet JavaScript qui n'existe pas ! Du coup, il a fallu mon petit cerveau pour pallier à ce problème. Mais ce n'était pas grand-chose comparé à Gemini.

Pour l'IA Gimini de Google, il ne m'a jamais donné un exemple fonctionnel. En cause l'appel de la librairie inconnue. J'ai donc modifié l'appel. Il y a aussi quelques différences. Gemini va faire appel au fichier gltf, il ne faudra pas oublier de mettre les ressources comme .bin à côté tandis que ChatGPT va faire appel au fichier glb. La différence entre les deux fichiers est que le glb embarque tout à l'intérieur (texture, code etc...) tandis que le gltf fait appel à des ressources externes (ce qui permet de l'optimiser pour le poids par exemple). À titre indicatif, il faut 8Mo pour le sac.

Autre différence entre Gemini et ChatGPT sont les méthodes de gestions de la souris ou du doigt. Gemini va faire appel à un plug-in , ChatGPT va préférer les événements du navigateur.

Gemini fait appel à des librairies qui n'existent pas, ça n'a pas l'air de trop le déranger ... Bref, lui il ne tue pas le métier.

Exemple généré avec Gimini : https://geekmps.fr/media/tests/3D/Animation%20Sac%203D%20Gemini.html

Exemple généré avec ChatGPT :  https://geekmps.fr/media/tests/3D/animation3D three bag.html

Le source généré par Gemini  et adapté vite fait par mes soins (il faudra faire un npm install --save three) pour récupérer les sources de three.js

<!DOCTYPE html>
<html lang="fr">
<head>
  <title>Sac 3D avec Three.js</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
    }
  </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser..0/examples/js/loaders/GLTFLoader.js"></script>
	<script src="/three/examples/jsm/controls/DragControls.js"></script>
	<script src="/dat.gui.min.js"></script>
</head>
</head>
<body>
  <script>
    // Initialisation de la scène
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 0, 0); // Positionnement de la caméra
    camera.lookAt(new THREE.Vector3(0, 0, 0)); // Orientation de la caméra

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
	 renderer.setClearColor(0xDDDDDD); // Couleur de fond gris
    document.body.appendChild(renderer.domElement);

    // Chargement du modèle 3D du sac
    const loader = new THREE.GLTFLoader();
    loader.load('https://geekmps.fr/media/tests/3D/sac.gltf', (gltf) => {
      const bag = gltf.scene;
      scene.add(bag);

      // Positionnement et orientation du sac
      bag.position.set(0, 0, -5);
      bag.rotation.y = Math.PI / 4;

      // Ajout d'une lumière ambiante
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambientLight);

      // Ajout d'une lumière directionnelle
      const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
      directionalLight.position.set(10, 10, 10);
      scene.add(directionalLight);
	  
	  // Contrôles de drag pour la souris et le toucher
      /*const controls = new THREE.DragControls([bag], camera, renderer.domElement);
      controls.addEventListener('change', render); // Met à jour la scène après le drag*/
      // Création d'une interface GUI avec dat.GUI.js
      const gui = new dat.GUI();
      const rotationFolder = gui.addFolder('Rotation');

      // Boutons pour la rotation
      const rotationXButton = rotationFolder.add(bag.rotation, 'x', -Math.PI / 2, Math.PI / 2).step(0.01);
      rotationXButton.onChange(render);

      const rotationYButton = rotationFolder.add(bag.rotation, 'y', -Math.PI / 2, Math.PI / 2).step(0.01);
      rotationYButton.onChange(render);

      const rotationZButton = rotationFolder.add(bag.rotation, 'z', -Math.PI / 2, Math.PI / 2).step(0.01);
      rotationZButton.onChange(render);
	  
	        // Variables pour le suivi de la rotation
      let mouseDown = false;
      let lastMouseX = 0;
      let lastMouseY = 0;
      let mouseX = 0;
      let mouseY = 0;
      let rotationSpeed = 0.01;

      // Événement de clic de la souris
      renderer.domElement.addEventListener('mousedown', (event) => {
        mouseDown = true;
        lastMouseX = event.clientX;
        lastMouseY = event.clientY;
      });

      // Événement de mouvement de la souris
      renderer.domElement.addEventListener('mousemove', (event) => {
        if (mouseDown) {
          mouseX = event.clientX;
          mouseY = event.clientY;

          const deltaX = mouseX - lastMouseX;
          const deltaY = mouseY - lastMouseY;

          bag.rotation.y += deltaX * rotationSpeed;
          bag.rotation.x += deltaY * rotationSpeed;

          lastMouseX = mouseX;
          lastMouseY = mouseY;
          render(); // Forcer le rendu après la rotation
        }
      });

      // Événement de relâchement de la souris
      renderer.domElement.addEventListener('mouseup', () => {
        mouseDown = false;
      });

      // Variables pour le suivi de la rotation
      let touchDown = false;
      let lastTouchX = 0;
      let lastTouchY = 0;
      let touchX = 0;
      let touchY = 0;

      // Événement de toucher
      renderer.domElement.addEventListener('touchstart', (event) => {
        if (event.touches.length === 1) { // Un seul doigt
          touchDown = true;
          lastTouchX = event.touches[0].clientX;
          lastTouchY = event.touches[0].clientY;
        }
      });

      // Événement de mouvement des doigts
      renderer.domElement.addEventListener('touchmove', (event) => {
        if (touchDown && event.touches.length === 1) { // Un seul doigt
          touchX = event.touches[0].clientX;
          touchY = event.touches[0].clientY;

          const deltaX = touchX - lastTouchX;
          const deltaY = touchY - lastTouchY;

          bag.rotation.y += deltaX * rotationSpeed;
          bag.rotation.x += deltaY * rotationSpeed;

          lastTouchX = touchX;
          lastTouchY = touchY;
          render(); // Forcer le rendu après la rotation
        }
      });

      // Événement de relâchement des doigts
      renderer.domElement.addEventListener('touchend', () => {
        touchDown = false;
      });
	  
	  // Chargement du modèle du deuxième téléphone
      loader.load('https://geekmps.fr/media/tests/3D/phone2.glb', (gltf) => {
        const phone2 = gltf.scene;
        scene.add(phone2);
phone2.scale.set(10, 10, 10);  
        // Positionnement du deuxième téléphone
        phone2.position.set(5, 0, -2);
        phone2.rotation.y = -Math.PI / 4; // Rotation initiale
});
	  const controls2 = new THREE.DragControls([phone2], camera, renderer.domElement);

      // Boucle de rendu
      function animate() {
        requestAnimationFrame(animate);
		//controls.update(); // Met à jour les contrôles de drag
		controls2.update();
        renderer.render(scene, camera);
      }
      animate();
    });
	
	    function render() {
      renderer.render(scene, camera);
    }
  </script>
</body>
</html>

Le source généré par ChatGPT, sans presque aucune adaptation de ma part.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rotation du sac</title>
    <style>
        button {
            margin: 10px;
            padding: 5px 10px;
            font-size: 16px;
            cursor: pointer;
        }

	        body {
            margin: 0;
            padding: 0;
            overflow: hidden; /* Pour éviter les barres de défilement */
            background-color: #888; /* Couleur de fond gris */
        }
		    </style>
</head>
<body>
    <button id="rotateUp">Haut</button>
    <button id="rotateDown">Bas</button>
    <button id="rotateLeft">Gauche</button>
    <button id="rotateRight">Droite</button>
	<button id="moveLight">Déplacer la lumière</button>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser..0/examples/js/loaders/GLTFLoader.js"></script>
    <script>
        let mesh; // Variable pour stocker le mesh du sac
		let phone; // Variable pour stocker le mesh du téléphone
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 5;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
		 renderer.setClearColor(0xFFFFFF); // Couleur de fond gris
        document.body.appendChild(renderer.domElement);
        
        // Chargement du fichier GLB
        const loader = new THREE.GLTFLoader();
        loader.load(
            'https://geekmps.fr/media/tests/3D/sac.glb',
            function (gltf) {
                mesh = gltf.scene;
                mesh.scale.set(1, 1, 1);
               // Ajout d'une lumière ambiante
                const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); // Couleur, Intensité
				ambientLight.position.set(2, 5, 0);
                scene.add(ambientLight);

			    const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // Couleur, Intensité
                directionalLight.position.set(4,11, 6); // Position
                scene.add(directionalLight);	
				
				// Lumière directionnelle (soleil)
        const sun = new THREE.DirectionalLight(0xffffff, 1); // Couleur, Intensité
        sun.position.set(5, 5, 5); // Position du soleil
        scene.add(sun);
				
                scene.add(mesh);
                animate();
            },
            undefined,
            function (error) {
                console.error('Erreur de chargement : ', error);
            }
        );
		
		// Chargement du fichier GLB pour le téléphone
        loader.load(
            'https://geekmps.fr/media/tests/3D/phone2.glb',
            function (gltf) {
                phone = gltf.scene;
                phone.scale.set(10, 10, 10);
                phone.position.x = 2; // Position initiale du téléphone
                scene.add(phone);
                animate();
            },
            undefined,
            function (error) {
                console.error('Erreur de chargement : ', error);
            }
        );
        
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        }

        // Rotation du sac vers le haut
        document.getElementById('rotateUp').addEventListener('click', function() {
            if (mesh) {
                mesh.rotation.x -= Math.PI / 180; // Rotation de 1 degré vers le haut
            }
        });

        // Rotation du sac vers le bas
        document.getElementById('rotateDown').addEventListener('click', function() {
            if (mesh) {
                mesh.rotation.x += Math.PI / 180; // Rotation de 1 degré vers le bas
            }
        });

        // Rotation du sac vers la gauche
        document.getElementById('rotateLeft').addEventListener('click', function() {
            if (mesh) {
                mesh.rotation.y += Math.PI / 180; // Rotation de 1 degré vers la gauche
            }
        });

        // Rotation du sac vers la droite
        document.getElementById('rotateRight').addEventListener('click', function() {
            if (mesh) {
                mesh.rotation.y -= Math.PI / 180; // Rotation de 1 degré vers la droite
            }
        });

        // Rotation du sac avec la souris
        let isDragging = false;
        let previousMousePosition = {
            x: 0,
            y: 0
        };

        document.addEventListener('mousedown', function(event) {
            isDragging = true;
        });

        document.addEventListener('mousemove', function(event) {
            if (!isDragging) return;

            const deltaMove = {
                x: event.clientX - previousMousePosition.x,
                y: event.clientY - previousMousePosition.y
            };

            if (mesh) {
                mesh.rotation.y += deltaMove.x * Math.PI / 360;
                mesh.rotation.x += deltaMove.y * Math.PI / 360;
            }

            previousMousePosition = {
                x: event.clientX,
                y: event.clientY
            };
        });

        document.addEventListener('mouseup', function(event) {
            isDragging = false;
        });
       // Gestion des événements tactiles pour la rotation et le zoom du sac
        let touchStartX = 0;
        let touchStartY = 0;
        let touchZoomDistance = 0;

        document.addEventListener('touchstart', function(event) {
            if (event.touches.length === 1) {
                touchStartX = event.touches[0].clientX;
                touchStartY = event.touches[0].clientY;
            } else if (event.touches.length === 2) {
                const dx = event.touches[0].clientX - event.touches[1].clientX;
                const dy = event.touches[0].clientY - event.touches[1].clientY;
                touchZoomDistance = Math.sqrt(dx * dx + dy * dy);
            }
        });

        document.addEventListener('touchmove', function(event) {
            if (event.touches.length === 1) {
                const deltaX = event.touches[0].clientX - touchStartX;
                const deltaY = event.touches[0].clientY - touchStartY;

                if (mesh) {
                    mesh.rotation.y -= deltaX * 0.01;
                    mesh.rotation.x -= deltaY * 0.01;
                }

                touchStartX = event.touches[0].clientX;
                touchStartY = event.touches[0].clientY;
            } else if (event.touches.length === 2) {
                const dx = event.touches[0].clientX - event.touches[1].clientX;
                const dy = event.touches[0].clientY - event.touches[1].clientY;
                const newDistance = Math.sqrt(dx * dx + dy * dy);

                const zoomDelta = newDistance - touchZoomDistance;

                camera.position.z += zoomDelta * 0.1;
                touchZoomDistance = newDistance;
            }
        });
        // Déplacer le téléphone avec les gestes tactiles
        let touchStartPhoneX = 0;

        document.addEventListener('touchstart', function(event) {
            if (event.touches.length === 1 && phone) {
                touchStartPhoneX = event.touches[0].clientX;
            }
        });

        document.addEventListener('touchmove', function(event) {
            if (event.touches.length === 1 && phone) {
                const deltaX = event.touches[0].clientX - touchStartPhoneX;
                phone.position.x += deltaX * 0.01; // Réglage de la vitesse de déplacement
                touchStartPhoneX = event.touches[0].clientX;
            }
        });
        // Gestion du redimensionnement de la fenêtre
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
		
		        // Déplacement de la lumière
        document.getElementById('moveLight').addEventListener('click', function() {
            // Changer la position de la lumière
            directionalLight.position.set(
                Math.random() * 10 - 5, // Valeurs aléatoires pour la position X
                Math.random() * 10 - 5, // Valeurs aléatoires pour la position Y
                Math.random() * 10 - 5  // Valeurs aléatoires pour la position Z
            );
        });
    </script>
</body>
</html>

C'est bien joli de faire une démo comme celle-là, mais ça ne sert pas à grand-chose. On peut imaginer de pouvoir essayer le sac, l'ouvrir et bien d'autres choses.

Pour le moment, les IA ne peuvent pas complètement remplacer des développeurs. Va falloir se méfier tout de même. En attendant c'est un bonne aide. Je me demande du coup à ça sert à quoi de continuer à écrire des blogs.

Et si tu es plus fort que l'IA tu deviens un super code comme ce jeune home

Comments powered by CComment

We use cookies

Nous utilisons des cookies sur notre site web. Certains d’entre eux sont essentiels au fonctionnement du site et d’autres nous aident à améliorer ce site et l’expérience utilisateur (cookies traceurs). Vous pouvez décider vous-même si vous autorisez ou non ces cookies. Merci de noter que, si vous les rejetez, vous risquez de ne pas pouvoir utiliser l’ensemble des fonctionnalités du site.