
Todos os tipos de apresentações de produtos em 3D não são tão raros em nossos dias, mas essas tarefas causam muitas perguntas aos desenvolvedores iniciantes. Hoje, examinaremos alguns princípios básicos que ajudarão você a entrar neste tópico e não tropeçar em uma tarefa tão simples como exibir um modelo tridimensional em um navegador. Como auxílio, usaremos o Three.js como a ferramenta mais popular nessa área.
Começando a trabalhar
Primeiro de tudo, vamos criar um modelo HTML para nós mesmos. Para não complicar o exemplo, não usaremos nada supérfluo, nem montadores, pré-processadores, etc.
Precisamos de um contêiner para canvas e um conjunto de scripts - na verdade three.js, um carregador para modelos no formato obj e um script para controlar a câmera com o 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>
Se o seu projeto usa o NPM e os construtores, você pode importar tudo isso do pacote três. Na verdade, se alguém não souber, o Unpkg aceita todos os pacotes do NPM.
Se você precisar conectar rapidamente algo de algum pacote à sua página, mas não encontrou o link para a CDN, lembre-se de Unpkg, provavelmente é necessário.
O script principal começará com várias variáveis globais. Isso simplificará o exemplo.
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;
No Three.js, tudo começa com a cena, então inicialize-a e crie algumas fontes 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); }
As fontes de luz são diferentes. Na maioria das vezes, nessas tarefas, o ambiente é usado - preenchendo a luz e direcionado - a luz em uma determinada direção. Ainda existem fontes pontuais de luz, mas ainda não precisamos delas. Tornamos o brilho da cor branca para que não haja distorções.
Pode ser útil brincar com a cor do brilho de preenchimento, especialmente com tons de cinza, para que você possa criar uma imagem mais suave.
A segunda coisa importante é a câmera. Essa é uma entidade que determina o ponto em que estamos e a direção em que olhamos.
function initCamera() { CAMERA = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); CAMERA.position.z = 100; }
Os parâmetros da câmera geralmente são selecionados a olho nu e dependem dos modelos usados.
O terceiro objeto de que precisamos é um renderizador. Ele é responsável por renderizar a imagem. Sua inicialização fala por si:
function initRenderer() { RENDERER = new THREE.WebGLRenderer({ alpha: true }); RENDERER.setPixelRatio(window.devicePixelRatio); RENDERER.setSize(window.innerWidth, window.innerHeight); }
Carregadores são necessários para carregar dados de diferentes formatos. Aqui você pode encontrar uma longa lista de opções, mas precisamos apenas de duas - uma para as fotos (vem com o kit) e outra para os modelos (nós a conectamos no início).
function initLoaders() { LOADING_MANAGER = new THREE.LoadingManager(); IMAGE_LOADER = new THREE.ImageLoader(LOADING_MANAGER); OBJ_LOADER = new THREE.OBJLoader(LOADING_MANAGER); }
Prosseguimos para carregar o modelo. Como esperado, ocorre de forma assíncrona. Depois de carregar o modelo, podemos brincar com seus 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); }); }
Resta iniciar o órgão de barril:
function animate() { requestAnimationFrame(animate); render(); } function render() { CAMERA.lookAt(SCENE.position); RENDERER.render(SCENE, CAMERA); }
Como resultado, temos apenas uma árvore de Natal branca com sombras (tirei o modelo daqui ).

Um, dois, três, queima de árvore de Natal! Mas sem texturas, é claro, não queimará. Sim, e falaremos sobre os shaders de fogo e outros elementos em outro momento ... Mas pelo menos podemos ver que o modelo da árvore de Natal está “na TV”.
Antes de passar para as texturas, é útil adicionar uma janela padrão do navegador para redimensionar o manipulador de eventos:
function initEventListeners() { window.addEventListener('resize', onWindowResize); onWindowResize(); } function onWindowResize() { CAMERA.aspect = window.innerWidth / window.innerHeight; CAMERA.updateProjectionMatrix(); RENDERER.setSize(window.innerWidth, window.innerHeight); }
Adicionar textura
Nosso modelo e textura de imagem trabalham com o princípio de adesivos-tradutores em modelos infantis de equipamentos. Como já sabemos, os objetos no contexto do WebGL consistem em um monte de triângulos. Sozinhos, eles não têm cor. Para cada triângulo, existe o mesmo "adesivo" triangular com a textura que você precisa colar. Mas se tivermos 1000 triângulos, precisamos carregar 1000 imagens de textura? Claro que não. É feito um sprite, o mesmo que para ícones CSS (você provavelmente os encontrou no trabalho) e informações sobre quais triângulos e onde são adicionados ao próprio modelo. E então o Three.js já entende tudo de forma independente e vemos o resultado final. De fato, tudo é um pouco mais complicado, mas a ideia deve ser entendida dessa maneira.
Galhos de árvores de Natal não são um exemplo muito revelador. Eles são todos iguais. A estrutura dessa textura será muito melhor vista no exemplo de um bulbasaur:

Mas palavras suficientes, vamos começar a ação. Inicialize a textura e carregue uma imagem com ela:
function initTexture() { TEXTURE = new THREE.Texture(); } function loadTexture() { IMAGE_LOADER.load('./texture.jpg', (image) => { TEXTURE.image = image; TEXTURE.needsUpdate = true; }); }
Agora precisamos expandir a função de carregamento do modelo. Se tivéssemos a mesma textura que o bulbasaur, tudo seria simples. Mas com uma árvore de Natal, a textura cobre apenas os galhos. É necessário separá-los de alguma forma e aplicá-los apenas a eles. Como fazer isso? Você pode abordar esse problema de maneiras diferentes. É hora de usar o console.log e ver o próprio modelo.
Se você não sabe como destacar uma parte específica do modelo, use console.log. Geralmente, é a maneira mais rápida de descobrir como as peças diferem.
Normalmente, temos duas opções para dividir o modelo em partes. A primeira (boa) é quando o artista 3D assinou as partes componentes do modelo e temos acesso aos campos de nome delas e podemos determinar a partir delas o que é o quê. No nosso exemplo, isso não é, mas existem nomes de materiais. Nós vamos usá-los. Para partes do modelo do material "Christmas_Tree", usaremos a 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;
Então, temos algo parecido com isto:

Para peças feitas de materiais “vermelhos” e “rosa” (estas são bolas - bolas de Natal), simplesmente definimos uma cor aleatória. Nesses casos, é conveniente usar o 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 artistas: dê nomes significativos a tudo nos modelos. Os nomes dos materiais em nosso exemplo simplesmente quebram o cérebro. Aqui o vermelho pode ser verde. Não os mudei para mostrar todo o absurdo do que estava acontecendo. O nome abstrato "material para bolas" seria mais universal.
Projeção equiretangular
A palavra composta projeção equiretangular na tradução para o russo é projeção igual a intermediária. Traduzido em casa - puxou a bola em um retângulo. Você pode me citar. Todos nós vimos um mapa-múndi na escola - é retangular, mas entendemos que, se o transformarmos um pouco, teremos um globo. É isso. Para entender melhor como essas distorções são organizadas, dê uma olhada na imagem:

Ao criar miniaturas de produtos diferentes, o plano de fundo geralmente é feito usando essas projeções. Tiramos uma foto distorcida com o ambiente e a exibimos em uma grande esfera. A câmera parece estar dentro dela. Parece algo como isto:
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 exemplo, minei deliberadamente as bordas; portanto, se você usar o exemplo no github, poderá encontrar uma costura distinta ao longo da qual a imagem se fecha. Se alguém estiver interessado, o original é retirado daqui .
Total no momento, temos algo parecido com isto:

Uma árvore de Natal com bolas coloridas parece bem fofa.
Controles de órbita
Para apreciar a beleza de uma sala tridimensional, adicione o controle do mouse. E então tudo parece estar em 3D, você precisa torcer tudo. Normalmente, o OrbitControls é usado nessas tarefas.
function initControls() { CONTROLS = new THREE.OrbitControls(CAMERA); CONTROLS.minPolarAngle = Math.PI * 1 / 4; CONTROLS.maxPolarAngle = Math.PI * 3 / 4; CONTROLS.update(); }
É possível definir restrições nos ângulos pelos quais você pode girar a câmera, restrições no zoom e outras opções. É útil olhar para a documentação, há muitas coisas interessantes.
Você não pode dizer muito sobre esse tipo de controle. Conectado, ligado e funciona. Só não se esqueça de atualizar regularmente o estado:
function animate() { requestAnimationFrame(animate); CONTROLS.update(); render(); }
Aqui você pode se distrair, torcer a árvore de Natal em diferentes direções ...
Raycaster
O Raycaster permite que você faça o seguinte: desenha uma linha reta no espaço e encontra todos os objetos com os quais se cruzou. Isso permite que você faça muitas coisas interessantes diferentes, mas no contexto das apresentações do produto, haverá dois casos principais - isso é reagir ao passar o mouse sobre algo e reagir a um clique do mouse em algo. Para fazer isso, você precisará desenhar linhas perpendiculares à tela através de um ponto com as coordenadas do mouse e procurar interseções. É isso que faremos. Estenda a função render, procure interseções com bolas e repinte-as:
function render() { RAYCASTER.setFromCamera(MOUSE, CAMERA); paintHoveredBalls();
Com movimentos simples do mouse para frente e para trás, garantimos que tudo funcione.

Mas há uma sutileza - o Three.js não sabe como alterar as cores suavemente. E, em geral, essa biblioteca não trata de alterações suaves nos valores. Aqui é o momento de conectar alguma ferramenta projetada para isso, por exemplo Anime.js.
<script src='https://unpkg.com/animejs@2.2.0/anime.min.js'></script>
Usamos esta biblioteca para animar valores:
switch (intersects[i].object.material.name) { case 'red':
Agora as cores mudam sem problemas, mas somente depois que o mouse se afasta da bola. Algo precisa ser consertado. Usaremos símbolos para isso - eles nos permitem adicionar com segurança informações meta aos objetos, e precisamos apenas adicionar informações sobre se a bola é animada ou não.
Símbolos no ES6 + é uma ferramenta muito poderosa que, entre outras coisas, permite adicionar informações a objetos de bibliotecas de terceiros sem medo de que isso leve a um conflito de nome ou quebre a lógica.
Fazemos uma constante global (em teoria, valeria a pena criar um objeto global para todos esses símbolos, mas temos um exemplo simples, não o complicaremos):
const _IS_ANIMATED = Symbol('is animated');
E adicionamos uma verificação à função de repintar as 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; }
Agora eles repintam suavemente imediatamente ao passar o mouse. Assim, com a ajuda de símbolos, você pode adicionar rapidamente verificações semelhantes em animações sem salvar os estados de todas as bolas em um local separado.
Dicas de ferramentas
A última coisa que fazemos hoje são as dicas de ferramentas. Esta tarefa é frequentemente encontrada. Para iniciantes, precisamos apenas inventá-los.
<div class='popup-3d'> !</div>
.popup-3d { color: #fff; font-family: 'Pacifico', cursive; font-size: 10rem; pointer-events: none; }
Lembre-se de desativar os eventos do ponteiro se não precisar deles.
Resta adicionar CSS3DRenderer. Na verdade, isso não é um renderizador, é algo que simplesmente adiciona transformações CSS aos elementos e parece que eles estão em uma cena comum. Para etiquetas pop-up - é exatamente isso que você precisa. Criamos a variável global CSSRENDERER, inicializamos e não esquecemos de chamar a função render. Tudo parece um renderizador comum:
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 aconteceu no momento. Na verdade, não fizemos nada. Inicializamos o elemento pop-up, podemos brincar imediatamente com seu tamanho e posição no espaço:
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); }
Agora vemos a inscrição "em 3D". Na verdade, não é totalmente em 3D, está no topo da tela, mas para dicas pop-up não é tão importante, o efeito é importante
O último toque permanece - para mostrar suavemente a inscrição em um determinado intervalo de ângulos. Mais uma vez, use o símbolo global:
const _IS_VISIBLE = Symbol('is visible');
E atualizamos o estado do elemento pop-up, dependendo do ângulo de rotação da câmera:
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; } }
Tudo é bem simples. Agora a inscrição aparece suavemente e desaparece. Você pode adicionar rotação automática e aproveitar o resultado.
CONTROLS.autoRotate = true; CONTROLS.autoRotateSpeed = -1.0;

Conclusão
Hoje, vimos como exibir modelos tridimensionais em nossa página, como transformá-los com o mouse, como fazer dicas de ferramentas, como responder ao mouse pairando sobre certas partes do modelo e como usar caracteres no contexto de várias animações. Espero que esta informação seja útil. Bem, tudo com o próximo, agora você sabe o que pode aprender durante as férias.
PS: As fontes completas para o exemplo de espinha de peixe estão disponíveis no github .