Présentations de produits en trois dimensions sur Three.js pour les plus petits


Toutes sortes de présentations de produits en 3D ne sont pas si rares à notre époque, mais ces tâches posent beaucoup de questions aux développeurs débutants. Aujourd'hui, nous allons examiner quelques notions de base qui vous aideront à entrer dans ce sujet et à ne pas tomber sur une tâche aussi simple que d'afficher un modèle tridimensionnel dans un navigateur. Pour vous aider, nous utiliserons Three.js comme l'outil le plus populaire dans ce domaine.


Se rendre au travail


Tout d'abord, créons un modèle HTML pour nous-mêmes. Afin de ne pas compliquer l'exemple, nous n'utiliserons rien de superflu, pas d'assembleurs, de préprocesseurs, etc.


Nous avons besoin d'un conteneur pour le canevas et d'un ensemble de scripts - en fait three.js, un chargeur pour les modèles au format obj et un script pour contrôler la caméra avec la souris.


<div class='canvas-container'></div> <script src='https://unpkg.com/three@0.99.0/build/three.min.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/loaders/OBJLoader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/controls/OrbitControls.js'></script> <script src='./main.js'></script> 

Si votre projet utilise NPM et des générateurs, vous pouvez importer tout cela à partir du package trois. En fait, si quelqu'un ne le sait pas, Unpkg prend tous les packages NPM.


Si vous avez besoin de connecter rapidement quelque chose d'un package à votre page, mais que vous n'avez pas trouvé le lien vers le CDN, rappelez-vous Unpkg, très probablement vous en avez besoin.

Le script principal commencera par un tas de variables globales. Cela simplifiera l'exemple.


 let SCENE; let CAMERA; let RENDERER; let LOADING_MANAGER; let IMAGE_LOADER; let OBJ_LOADER; let CONTROLS; let MOUSE; let RAYCASTER; let TEXTURE; let OBJECT; 

Dans Three.js, tout commence par la scène, alors initialisez-la et créez quelques sources de lumière:


 function initScene() { SCENE = new THREE.Scene(); initLights(); } function initLights() { const ambient = new THREE.AmbientLight(0xffffff, 0.7); SCENE.add(ambient); const directionalLight = new THREE.DirectionalLight(0xffffff); directionalLight.position.set(0, 1, 1); SCENE.add(directionalLight); } 

Les sources lumineuses sont différentes. Le plus souvent dans de telles tâches, la lumière ambiante est utilisée - lumière de remplissage et la lumière directionnelle - dans une certaine direction. Il existe encore des sources ponctuelles de lumière, mais nous n'en avons pas encore besoin. Nous rendons la couleur luminescente blanche afin qu'il n'y ait pas de distorsions.


Il peut être utile de jouer avec la couleur de la lueur de remplissage, en particulier avec les nuances de gris, afin de créer une image plus douce.

La deuxième chose importante est l'appareil photo. C'est une telle entité qui détermine le point où nous en sommes et la direction dans laquelle nous regardons.


 function initCamera() { CAMERA = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); CAMERA.position.z = 100; } 

Les paramètres de la caméra sont généralement sélectionnés à l'œil nu et dépendent des modèles utilisés.


Le troisième objet dont nous avons besoin est un moteur de rendu. Il est responsable du rendu de l'image. Son initialisation parle d'elle-même:


 function initRenderer() { RENDERER = new THREE.WebGLRenderer({ alpha: true }); RENDERER.setPixelRatio(window.devicePixelRatio); RENDERER.setSize(window.innerWidth, window.innerHeight); } 

Des chargeurs sont nécessaires pour charger des données de différents formats. Ici vous pouvez trouver une longue liste d'options, mais nous n'en avons besoin que de deux - une pour les photos (elle est livrée avec le kit) et une pour les modèles (nous l'avons connectée au début).


 function initLoaders() { LOADING_MANAGER = new THREE.LoadingManager(); IMAGE_LOADER = new THREE.ImageLoader(LOADING_MANAGER); OBJ_LOADER = new THREE.OBJLoader(LOADING_MANAGER); } 

Nous procédons au chargement du modèle. Comme prévu, cela se produit de manière asynchrone. Après avoir chargé le modèle, nous pouvons jouer avec ses paramètres:


 function loadModel() { OBJ_LOADER.load('./model.obj', (object) => { object.scale.x = 0.3; object.scale.y = 0.3; object.scale.z = 0.3; object.rotation.x = -Math.PI / 2; object.position.y = -30; OBJECT = object; SCENE.add(OBJECT); }); } 

Reste à démarrer l'orgue de Barbarie:


 function animate() { requestAnimationFrame(animate); render(); } function render() { CAMERA.lookAt(SCENE.position); RENDERER.render(SCENE, CAMERA); } 

En conséquence, nous obtenons juste un arbre de Noël blanc avec des ombres (j'ai pris le modèle d'ici ).



Un, deux, trois, un arbre de Noël brûle! Mais sans textures, bien sûr, il ne brûlera pas. Oui, et nous parlerons des shaders de feu et d'autres éléments une autre fois ... Mais au moins, nous pouvons voir que le modèle du sapin de Noël est "à la télé".


Avant de passer aux textures, il est utile d'ajouter un gestionnaire d'événements de redimensionnement de fenêtre de navigateur standard:


 function initEventListeners() { window.addEventListener('resize', onWindowResize); onWindowResize(); } function onWindowResize() { CAMERA.aspect = window.innerWidth / window.innerHeight; CAMERA.updateProjectionMatrix(); RENDERER.setSize(window.innerWidth, window.innerHeight); } 

Ajouter de la texture


Nos modèles et textures d'image fonctionnent sur le principe des autocollants-traducteurs sur les modèles d'équipement pour enfants. Comme nous le savons déjà, les objets dans le contexte de WebGL sont constitués d'un tas de triangles. A eux seuls, ils n'ont pas de couleur. Pour chaque triangle, il y a le même «autocollant» triangulaire avec la texture que vous devez coller dessus. Mais si nous avons 1000 triangles, alors nous devons charger 1000 images de texture? Bien sûr que non. Un sprite est créé, de la même manière que pour les icônes CSS (vous les avez probablement rencontrées au travail), et des informations sur les triangles et l'endroit où ils sont ajoutés au modèle lui-même. Et puis Three.js comprend déjà tout indépendamment et nous voyons le résultat final. En fait, tout est un peu plus compliqué, mais l'idée doit être comprise de cette façon.


Les branches d'arbres de Noël ne sont pas un exemple très révélateur. Ils sont tous pareils. La structure d'une telle texture sera bien mieux vue sur l'exemple d'un bulbasaur:



Mais assez de mots, passons à l'action. Initialisez la texture et chargez une image avec:


 function initTexture() { TEXTURE = new THREE.Texture(); } function loadTexture() { IMAGE_LOADER.load('./texture.jpg', (image) => { TEXTURE.image = image; TEXTURE.needsUpdate = true; }); } 

Nous devons maintenant étendre la fonction de chargement du modèle. Si nous avions la même texture que le bulbasaur, tout serait simple. Mais avec un arbre de Noël, la texture ne couvre que les branches. Il est nécessaire de les séparer d'une manière ou d'une autre et de ne les appliquer qu'à eux. Comment faire Vous pouvez aborder ce problème de différentes manières. Il est temps d'utiliser console.log et de regarder le modèle lui-même.


Si vous ne savez pas comment mettre en évidence une partie spécifique du modèle, utilisez console.log. C'est généralement le moyen le plus rapide de découvrir en quoi les pièces diffèrent.

Habituellement, nous avons deux options pour diviser le modèle en parties. Le premier (bon) est lorsque l'artiste 3D a signé les composants du modèle et que nous avons accès à leurs champs de nom et que nous pouvons déterminer à partir de quoi est quoi. Dans notre exemple, ce n'est pas le cas, mais il y a des noms de matériaux. Nous les utiliserons. Pour certaines parties du modèle du matériau «Christmas_Tree», nous utiliserons la texture:


 function loadModel() { OBJ_LOADER.load('./model.obj', (object) => { object.traverse(function(child) { if (child instanceof THREE.Mesh) { switch (child.material.name) { case 'Christmas_Tree': child.material.map = TEXTURE; break; // . . . } } }); // . . . 

Nous obtenons donc quelque chose comme ceci:



Pour les pièces en matériaux «rouges» et «roses» (ce sont des boules - boules de Noël), nous définissons simplement une couleur aléatoire. Dans de tels cas, il est pratique d'utiliser HSL:


 switch (child.material.name) { case 'Christmas_Tree': child.material.map = TEXTURE; break; case 'red': child.material.color.setHSL(Math.random(), 1, 0.5); break; case 'pink': child.material.color.setHSL(Math.random(), 1, 0.5); break; } 

Remarque pour les artistes: donnez des noms significatifs à tout dans les modèles. Les noms des matériaux dans notre exemple brisent simplement le cerveau. Ici, le rouge peut être vert. Je ne les ai pas modifiés pour montrer toute l'absurdité de ce qui se passait. Le nom abstrait «matériel pour balles» serait plus universel.

Projection équirectangulaire


Le mot composé projection équirectangulaire en traduction en russe est projection égale à intermédiaire. Traduit en ménage - a tiré la balle sur un rectangle. Vous pouvez me citer. Nous avons tous vu une carte du monde à l'école - elle est rectangulaire, mais nous comprenons que si nous la transformons un peu, nous obtenons un globe. Ça y est. Pour mieux comprendre comment ces distorsions sont organisées, jetez un œil à l'image:



Lors de la création de vignettes de différents produits, l'arrière-plan est souvent créé à l'aide de telles projections. On prend une photo déformée avec l'environnement et on l'affiche sur une grande sphère. La caméra semble être à l'intérieur. Cela ressemble à ceci:


 function initWorld() { const sphere = new THREE.SphereGeometry(500, 64, 64); sphere.scale(-1, 1, 1); const texture = new THREE.Texture(); const material = new THREE.MeshBasicMaterial({ map: texture }); IMAGE_LOADER.load('./world.jpg', (image) => { texture.image = image; texture.needsUpdate = true; }); SCENE.add(new THREE.Mesh(sphere, material)); } 

À titre d'exemple, j'ai délibérément miné les bords, donc si vous utilisez l'exemple du github, vous pouvez trouver une couture distincte le long de laquelle l'image se ferme. Si quelqu'un est intéressé, son original est tiré d'ici .


Total en ce moment, nous avons quelque chose comme ça:



Un arbre de Noël avec des boules colorées est plutôt mignon.


Contrôle de l'orbite


Afin d'apprécier la beauté d'une pièce en trois dimensions, ajoutez le contrôle de la souris. Et puis tout semble être en 3D, vous devez tout tordre. OrbitControls est généralement utilisé dans de telles tâches.


 function initControls() { CONTROLS = new THREE.OrbitControls(CAMERA); CONTROLS.minPolarAngle = Math.PI * 1 / 4; CONTROLS.maxPolarAngle = Math.PI * 3 / 4; CONTROLS.update(); } 

Il est possible de définir des restrictions sur les angles de rotation de l'appareil photo, des restrictions sur le zoom et d'autres options. Il est utile de regarder la documentation, il y a beaucoup de choses intéressantes.


Vous ne pouvez pas en dire beaucoup sur ce type de contrôle. Connecté, allumé et ça marche. N'oubliez pas de mettre régulièrement à jour l'état:


 function animate() { requestAnimationFrame(animate); CONTROLS.update(); render(); } 

Ici, vous pouvez vous distraire un peu, tourner l'arbre de Noël dans différentes directions ...


Raycaster


Raycaster vous permet de faire ce qui suit: il trace une ligne droite dans l'espace et trouve tous les objets avec lesquels il s'est croisé. Cela vous permet de faire beaucoup de choses intéressantes, mais dans le contexte des présentations de produits, il y aura deux cas principaux - c'est de répondre au survol de la souris sur quelque chose et de répondre au clic de la souris sur quelque chose. Pour ce faire, vous devrez tracer des lignes perpendiculaires à l'écran en passant par un point avec les coordonnées de la souris et rechercher des intersections. Voilà ce que nous allons faire. Étendez la fonction de rendu, recherchez les intersections avec des boules et repeignez-les:


 function render() { RAYCASTER.setFromCamera(MOUSE, CAMERA); paintHoveredBalls(); // . . . } function paintHoveredBalls() { if (OBJECT) { const intersects = RAYCASTER.intersectObjects(OBJECT.children); for (let i = 0; i < intersects.length; i++) { switch (intersects[i].object.material.name) { case 'red': intersects[i].object.material.color.set(0x000000); break; case 'pink': intersects[i].object.material.color.set(0xffffff); break; } } } } 

Avec des mouvements simples de la souris d'avant en arrière, nous nous assurons que tout fonctionne.



Mais il y a une subtilité - Three.js ne sait pas comment changer les couleurs en douceur. Et en général, cette bibliothèque ne concerne pas les changements en douceur des valeurs. Voici le moment de connecter un outil conçu pour cela, par exemple Anime.js.


 <script src='https://unpkg.com/animejs@2.2.0/anime.min.js'></script> 

Nous utilisons cette bibliothèque pour animer des valeurs:


 switch (intersects[i].object.material.name) { case 'red': // intersects[i].object.material.color.set(0x000000); anime({ targets: intersects[i].object.material.color, r: 0, g: 0, b: 0, easing: 'easeInOutQuad' }); break; // . . . } 

Maintenant, les couleurs changent en douceur, mais seulement après que la souris s'est éloignée de la balle. Quelque chose doit être réparé. Pour ce faire, nous utiliserons des symboles - ils nous permettent d'ajouter en toute sécurité des méta-informations aux objets, et nous avons juste besoin d'ajouter des informations pour savoir si la balle est animée ou non.


Les symboles dans ES6 + est un outil très puissant qui, entre autres, vous permet d'ajouter des informations à des objets de bibliothèques tierces sans craindre que cela n'entraîne un conflit de nom ou ne casse la logique.

Nous faisons une constante globale (en théorie, cela vaudrait la peine de créer un objet global pour tous ces symboles, mais nous avons un exemple simple, nous ne le compliquerons pas):


 const _IS_ANIMATED = Symbol('is animated'); 

Et nous ajoutons une vérification à la fonction de repeindre les boules:


 if (!intersects[i].object[_IS_ANIMATED]) { anime({ targets: intersects[i].object.material.color, r: 0, g: 0, b: 0, easing: 'easeInOutQuad' }); intersects[i].object[_IS_ANIMATED] = true; } 

Maintenant, ils repeignent en douceur immédiatement en vol stationnaire. Ainsi, à l'aide de symboles, vous pouvez rapidement ajouter des contrôles similaires dans les animations sans enregistrer les états de toutes les boules dans un endroit séparé.


Info-bulles


La dernière chose que nous faisons aujourd'hui, ce sont les info-bulles. Cette tâche est souvent rencontrée. Pour commencer, nous avons juste besoin de les inventer.


 <div class='popup-3d'>  !</div> 

 .popup-3d { color: #fff; font-family: 'Pacifico', cursive; font-size: 10rem; pointer-events: none; } 

N'oubliez pas de désactiver les événements de pointeur si vous n'en avez pas besoin.

Il reste à ajouter CSS3DRenderer. Ce n'est en fait pas tout à fait un moteur de rendu, c'est plutôt une chose qui ajoute simplement des transformations CSS aux éléments et il semble qu'ils se trouvent dans une scène commune. Pour les étiquettes contextuelles - c'est exactement ce dont vous avez besoin. Nous créons la variable globale CSSRENDERER, l'initialisons et n'oubliez pas d'appeler la fonction de rendu elle-même. Tout ressemble à un rendu normal:


 function initCSSRenderer() { CSSRENDERER = new THREE.CSS3DRenderer(); CSSRENDERER.setSize(window.innerWidth, window.innerHeight); CSSRENDERER.domElement.style.position = 'absolute'; CSSRENDERER.domElement.style.top = 0; } function render() { CAMERA.lookAt(SCENE.position); RENDERER.render(SCENE, CAMERA); CSSRENDERER.render(SCENE, CAMERA); } 

Il ne s'est rien passé pour le moment. En fait, nous n'avons rien fait. On initialise l'élément pop-up, on peut immédiatement jouer avec sa taille et sa position dans l'espace:


 function initPopups() { const popupSource = document.querySelector('.popup-3d'); const popup = new THREE.CSS3DObject(popupSource); popup.position.x = 0; popup.position.y = -10; popup.position.z = 30; popup.scale.x = 0.05; popup.scale.y = 0.05; popup.scale.z = 0.05; console.log(popup); SCENE.add(popup); } 

Nous voyons maintenant l'inscription «en 3D». En fait, ce n'est pas entièrement en 3D, il se trouve au-dessus du canevas, mais pour les astuces, ce n'est pas si important, l'effet est


La dernière touche reste - pour montrer en douceur l'inscription dans une certaine gamme d'angles. Encore une fois, utilisez le symbole global:


 const _IS_VISIBLE = Symbol('is visible'); 

Et nous mettons à jour l'état de l'élément contextuel en fonction de l'angle de rotation de la caméra:


 function updatePopups() { const popupSource = document.querySelector('.popup-3d'); const angle = CONTROLS.getAzimuthalAngle(); if (Math.abs(angle) > .9 && popupSource[_IS_VISIBLE]) { anime({ targets: popupSource, opacity: 0, easing: 'easeInOutQuad' }); popupSource[_IS_VISIBLE] = false; } else if (Math.abs(angle) < .9 && !popupSource[_IS_VISIBLE]) { anime({ targets: popupSource, opacity: 1, easing: 'easeInOutQuad' }); popupSource[_IS_VISIBLE] = true; } } 

Tout est assez simple. Maintenant, l'inscription apparaît et disparaît en douceur. Vous pouvez ajouter une rotation automatique et profiter du résultat.


 CONTROLS.autoRotate = true; CONTROLS.autoRotateSpeed = -1.0; 


Conclusion


Aujourd'hui, nous avons examiné comment afficher des modèles tridimensionnels sur notre page, comment les tourner avec la souris, comment créer des info-bulles, comment répondre au survol de la souris sur certaines parties du modèle et comment utiliser des caractères dans le contexte de diverses animations. J'espère que ces informations vous seront utiles. Eh bien, avec le prochain, vous savez maintenant ce que vous pouvez apprendre pendant les vacances.


PS: Les sources complètes de l'exemple de chevrons sont disponibles sur le github .

Source: https://habr.com/ru/post/fr433876/


All Articles