Presentaciones tridimensionales de productos en Three.js para los más pequeños.


Todos los tipos de presentaciones de productos en 3D no son tan raros en nuestro tiempo, pero estas tareas generan muchas preguntas por parte de los desarrolladores principiantes. Hoy veremos algunos conceptos básicos que lo ayudarán a entrar en este tema y no tropezar con una tarea tan simple como mostrar un modelo tridimensional en un navegador. Como ayuda, utilizaremos Three.js como la herramienta más popular en esta área.


Llegar al trabajo


En primer lugar, hagamos una plantilla HTML para nosotros. Para no complicar el ejemplo, no usaremos nada superfluo, sin ensambladores, preprocesadores, etc.


Necesitamos un contenedor para el lienzo y un conjunto de scripts, en realidad three.js, un cargador para modelos en formato obj y un script para controlar la cámara con el mouse.


<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 su proyecto usa NPM y constructores, puede importar todo esto desde el paquete tres. En realidad, si alguien no lo sabe, Unpkg toma todos los paquetes de NPM.


Si necesita conectar rápidamente algo de algún paquete a su página, pero no encontró el enlace al CDN, recuerde Unpkg, lo más probable es que lo necesite.

El script principal comenzará con un montón de variables globales. Esto simplificará el ejemplo.


 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; 

En Three.js, todo comienza con la escena, así que inicialícela y cree un par de fuentes de luz:


 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); } 

Las fuentes de luz son diferentes. Con mayor frecuencia en tales tareas, se utiliza ambiente (luz de relleno y direccional) luz en una determinada dirección. Todavía hay fuentes puntuales de luz, pero aún no las necesitamos. Hacemos que el brillo sea de color blanco para que no haya distorsiones.


Puede ser útil jugar con el color del brillo de relleno, especialmente con tonos de gris, para que pueda hacer una imagen más suave.

La segunda cosa importante es la cámara. Esta es una entidad que determina el punto en el que estamos y la dirección en la que miramos.


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

Los parámetros de la cámara generalmente se seleccionan por ojo y dependen de los modelos utilizados.


El tercer objeto que necesitamos es un renderizador. Él es responsable de renderizar la imagen. Su inicialización habla por sí misma:


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

Se necesitan cargadores para cargar datos de diferentes formatos. Aquí puede encontrar una larga lista de opciones, pero solo necesitamos dos: una para las imágenes (viene con el kit) y otra para los modelos (la conectamos al principio).


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

Procedemos a cargar el modelo. Como se esperaba, ocurre de forma asincrónica. Después de cargar el modelo, podemos jugar con sus parámetros:


 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); }); } 

Queda por comenzar el órgano de barril:


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

Como resultado, obtenemos solo un árbol de Navidad blanco con sombras (tomé el modelo de aquí ).



Uno, dos, tres, arbol de Navidad quemado! Pero sin texturas, por supuesto, no se quemará. Sí, y hablaremos sobre los sombreadores de fuego y otros elementos en otro momento ... Pero al menos podemos ver que el modelo del árbol de Navidad está "en la TV".


Antes de pasar a las texturas, es útil agregar un controlador de eventos estándar para cambiar el tamaño de la ventana del navegador:


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

Añadir textura


Nuestro modelo y la textura de la imagen funcionan según el principio de los traductores adhesivos en los modelos de equipos para niños. Como ya sabemos, los objetos en el contexto de WebGL consisten en un montón de triángulos. Por sí mismos, no tienen color. Para cada triángulo hay la misma "pegatina" triangular con la textura que necesita pegar en él. Pero si tenemos 1000 triángulos, ¿entonces necesitamos cargar 1000 imágenes de textura? Por supuesto que no. El sprite está hecho, lo mismo que para los íconos CSS (probablemente los encontraste en el trabajo), y la información sobre qué triángulos y dónde se agrega al modelo en sí. Y luego Three.js ya entiende de forma independiente todo y vemos el resultado final. De hecho, todo es un poco más complicado, pero la idea debe entenderse de esta manera.


Las ramas de los árboles de Navidad no son un ejemplo muy revelador. Todos son iguales La estructura de tal textura se verá mucho mejor en el ejemplo de un bulbasaur:



Pero suficientes palabras, pasemos a la acción. Inicialice la textura y cargue una imagen con ella:


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

Ahora necesitamos expandir la función de carga del modelo. Si tuviéramos la misma textura que el bulbasaur, todo sería simple. Pero con un árbol de Navidad, la textura solo cubre las ramas. Es necesario separarlos de alguna manera y aplicarlos solo a ellos. Como hacerlo Puede abordar este problema de diferentes maneras. Es hora de usar console.log y mirar el modelo en sí.


Si no sabe cómo resaltar una parte específica del modelo, use console.log. Esta suele ser la forma más rápida de descubrir cómo difieren las partes.

Por lo general, tenemos dos opciones para dividir el modelo en partes. El primero (bueno) es cuando el artista 3D firmó las partes componentes del modelo y tenemos acceso a los campos de nombre de ellos y podemos determinar a partir de ellos qué es qué. En nuestro ejemplo, esto no es así, pero hay nombres de materiales. Los usaremos. Para partes del modelo del material "Christmas_Tree" usaremos la textura:


 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; // . . . } } }); // . . . 

Entonces obtenemos algo como esto:



Para piezas hechas de materiales "rojos" y "rosados" (estas son bolas - bolas de Navidad), simplemente establecemos un color aleatorio. En tales casos, es conveniente usar 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; } 

Nota para los artistas: dar nombres significativos a todo en las modelos. Los nombres de los materiales en nuestro ejemplo simplemente rompen el cerebro. Aquí el rojo puede ser verde. No los cambié para mostrar todo lo absurdo de lo que estaba sucediendo. El nombre abstracto "material para bolas" sería más universal.

Proyección equirectangular


La palabra compuesta proyección equirrectangular en traducción al ruso es proyección igual a intermedia. Traducido al hogar - sacó la pelota en un rectángulo. Puedes citarme. Todos vimos un mapa del mundo en la escuela, es rectangular, pero entendemos que si lo transformamos un poco, obtenemos un globo. Esto es todo Para comprender mejor cómo se organizan estas distorsiones, eche un vistazo a la imagen:



Al crear miniaturas de diferentes productos, el fondo a menudo se realiza utilizando tales proyecciones. Tomamos una imagen distorsionada con el entorno y la mostramos en una esfera grande. La cámara parece estar dentro de ella. Se parece a esto:


 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)); } 

Como ejemplo, deliberadamente socavado los bordes, por lo que si utiliza el ejemplo del github, puede encontrar una costura distinta a lo largo de la cual se cierra la imagen. Si alguien está interesado, entonces su original se toma de aquí .


Total en este momento tenemos algo como esto:



Un árbol de Navidad con bolas de colores se ve muy lindo.


Controles de órbita


Para apreciar la belleza de una habitación tridimensional, agregue el control del mouse. Y luego todo parece estar en 3D, debes torcerlo todo. Por lo general, OrbitControls se usa en tales tareas.


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

Es posible establecer restricciones en los ángulos mediante los cuales puede girar la cámara, restricciones en el zoom y otras opciones. Es útil examinar la documentación, hay muchas cosas interesantes.


No puedes decir mucho sobre esta opción de control. Conectado, encendido y funciona. Simplemente no olvide actualizar periódicamente el estado:


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

Aquí puedes distraerte un poco, girar el árbol de Navidad en diferentes direcciones ...


Raycaster


Raycaster le permite hacer lo siguiente: dibuja una línea recta en el espacio y encuentra todos los objetos con los que se cruzó. Esto le permite hacer muchas cosas interesantes diferentes, pero en el contexto de las presentaciones de productos habrá dos casos principales: esto es reaccionar al pasar el mouse sobre algo y al hacer clic con el mouse sobre algo. Para hacer esto, necesitará dibujar líneas perpendiculares a la pantalla a través de un punto con las coordenadas del mouse y buscar intersecciones. Esto es lo que haremos. Extienda la función de renderizado, busque intersecciones con bolas y vuelva a pintarlas:


 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; } } } } 

Con simples movimientos del mouse de un lado a otro, nos aseguramos de que todo funcione.



Pero hay una sutileza: Three.js no sabe cómo cambiar suavemente los colores. Y en general, esta biblioteca no se trata de cambios suaves en los valores. Este es el momento de conectar alguna herramienta diseñada para esto, por ejemplo, Anime.js.


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

Utilizamos esta biblioteca para animar valores:


 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; // . . . } 

Ahora los colores cambian suavemente, pero solo después de que el mouse se aleja de la pelota. Algo necesita ser arreglado. Usaremos símbolos para esto: nos permiten agregar metainformación de forma segura a los objetos, y solo necesitamos agregar información sobre si la pelota está animada o no.


Symbols en ES6 + es una herramienta muy poderosa que, entre otras cosas, le permite agregar información a objetos de bibliotecas de terceros sin temor a que esto genere un conflicto de nombres o rompa la lógica.

Hacemos una constante global (en teoría, valdría la pena hacer un objeto global para todos esos símbolos, pero tenemos un ejemplo simple, no lo complicaremos):


 const _IS_ANIMATED = Symbol('is animated'); 

Y agregamos un cheque a la función de volver a pintar las bolas:


 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; } 

Ahora vuelven a pintar sin problemas inmediatamente al pasar el ratón. Por lo tanto, con la ayuda de símbolos, puede agregar rápidamente comprobaciones similares en las animaciones sin guardar los estados de todas las bolas en un lugar separado.


Información sobre herramientas


Lo último que hacemos hoy es información sobre herramientas. Esta tarea a menudo se encuentra. Para empezar, solo necesitamos inventarlos.


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

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

Recuerde deshabilitar los eventos de puntero si no los necesita.

Queda por agregar CSS3DRenderer. En realidad, esto no es un renderizador, es algo que simplemente agrega transformaciones CSS a los elementos y parece que están en una escena común. Para etiquetas emergentes, esto es justo lo que necesita. Hacemos la variable global CSSRENDERER, la inicializamos y no olvidemos llamar a la función de renderización en sí. Todo parece un renderizador regular:


 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); } 

Nada ha sucedido en este momento. En realidad, no hicimos nada. Inicializamos el elemento emergente, podemos jugar inmediatamente con su tamaño y posición en el espacio:


 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); } 

Ahora vemos la inscripción "en 3D". De hecho, no está completamente en 3D, se encuentra en la parte superior del lienzo, pero para las sugerencias emergentes no es tan importante, el efecto es


Queda el último toque: mostrar suavemente la inscripción en un cierto rango de ángulos. Nuevamente, use el símbolo global:


 const _IS_VISIBLE = Symbol('is visible'); 

Y actualizamos el estado del elemento emergente dependiendo del ángulo de rotación de la cámara:


 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; } } 

Todo es bastante simple. Ahora la inscripción aparece suavemente y desaparece. Puede agregar rotación automática y disfrutar del resultado.


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


Conclusión


Hoy vimos cómo mostrar modelos tridimensionales en nuestra página, cómo girarlos con el mouse, cómo hacer información sobre herramientas, cómo responder al mouse sobre ciertas partes del modelo y cómo usar personajes en el contexto de varias animaciones. Espero que esta información sea útil. Bueno, todo con lo que viene, ahora sabes lo que puedes aprender durante las vacaciones.


PD: Las fuentes completas para el ejemplo de espiga están disponibles en el github .

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


All Articles