Presentasi produk tiga dimensi di Three.js untuk yang terkecil


Semua jenis presentasi produk dalam 3D tidak terlalu langka di zaman kita, tetapi tugas-tugas ini menimbulkan banyak pertanyaan dari para pengembang pemula. Hari ini kita akan melihat beberapa dasar yang akan membantu Anda masuk ke topik ini dan tidak tersandung pada tugas sederhana seperti menampilkan model tiga dimensi di browser. Sebagai bantuan, kami akan menggunakan Three.js sebagai alat paling populer di bidang ini.


Mulai bekerja


Pertama-tama, mari kita buat templat HTML untuk diri kita sendiri. Agar tidak menyulitkan contoh, kami tidak akan menggunakan apa pun yang berlebihan, tidak ada assembler, preprosesor, dll.


Kami membutuhkan wadah untuk kanvas dan satu set skrip - sebenarnya three.js, loader untuk model dalam format obj, dan skrip untuk mengontrol kamera dengan 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> 

Jika proyek Anda menggunakan NPM dan pembangun, maka Anda dapat mengimpor semua ini dari paket tiga. Sebenarnya, jika seseorang tidak tahu, Unpkg mengambil semua paket NPM.


Jika Anda perlu menghubungkan sesuatu dengan cepat dari beberapa paket ke halaman Anda, tetapi Anda tidak menemukan tautan ke CDN, ingatlah Unpkg, kemungkinan besar Anda membutuhkannya.

Script utama akan dimulai dengan sekelompok variabel global. Ini akan menyederhanakan contoh.


 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; 

Di Three.js, semuanya dimulai dengan adegan, jadi inisialisasi dan buat beberapa sumber cahaya:


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

Sumber cahaya berbeda. Paling sering dalam tugas-tugas tersebut, ambient digunakan - mengisi cahaya, dan directional - light dalam arah tertentu. Masih ada sumber cahaya titik, tetapi kita belum membutuhkannya. Kami membuat warna cahaya putih sehingga tidak ada distorsi.


Berguna untuk bermain dengan warna cahaya pengisi, terutama dengan nuansa abu-abu, sehingga Anda dapat membuat gambar yang lebih lembut.

Yang penting kedua adalah kamera. Ini adalah entitas yang menentukan titik di mana kita berada dan arah di mana kita melihat.


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

Parameter kamera biasanya dipilih berdasarkan mata dan tergantung pada model yang digunakan.


Objek ketiga yang kita butuhkan adalah renderer. Dia bertanggung jawab untuk membuat gambar. Inisialisasi berbicara untuk dirinya sendiri:


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

Loader diperlukan untuk memuat data dari berbagai format. Di sini Anda dapat menemukan daftar panjang opsi, tetapi kami hanya membutuhkan dua - satu untuk gambar (dilengkapi dengan kit) dan satu untuk model (kami menghubungkannya di awal).


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

Kami melanjutkan memuat model. Seperti yang diharapkan, ini terjadi secara tidak sinkron. Setelah memuat model, kita dapat bermain dengan parameternya:


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

Tetap memulai organ barel:


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

Akibatnya, kami hanya mendapatkan pohon Natal putih dengan bayangan (saya mengambil model dari sini ).



Satu, dua, tiga, pohon Natal terbakar! Tetapi tanpa tekstur, tentu saja, itu tidak akan terbakar. Ya, dan kita akan berbicara tentang bayangan api dan unsur-unsur lain kali ... Tapi setidaknya kita dapat melihat bahwa model pohon Natal adalah "di TV".


Sebelum beralih ke tekstur, sebaiknya tambahkan jendela browser standar yang mengubah ukuran handler:


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

Tambahkan tekstur


Model dan tekstur gambar kami bekerja berdasarkan prinsip stiker-penerjemah pada model peralatan anak-anak. Seperti yang telah kita ketahui, objek dalam konteks WebGL terdiri dari tumpukan segitiga. Sendiri, mereka tidak memiliki warna. Untuk setiap segitiga ada "stiker" segitiga yang sama dengan tekstur yang harus Anda tempel. Tetapi jika kita memiliki 1000 segitiga, maka kita perlu memuat 1000 gambar tekstur? Tentu saja tidak. Sprite dibuat, sama seperti untuk ikon CSS (Anda mungkin menemukan mereka di tempat kerja), dan informasi tentang segitiga mana dan di mana ditambahkan ke model itu sendiri. Dan Three.js sudah memahami segalanya secara independen dan kami melihat hasil akhirnya. Sebenarnya, semuanya sedikit lebih rumit, tetapi gagasan itu harus dipahami dengan cara ini.


Cabang-cabang pohon Natal bukanlah contoh yang sangat terbuka. Semuanya sama saja. Struktur tekstur seperti itu akan jauh lebih baik dilihat pada contoh bulbasaur:



Tapi cukup kata-kata, ayo beraksi. Inisialisasi tekstur dan muat gambar dengan itu:


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

Sekarang kita perlu memperluas fungsi pemuatan model. Jika kami memiliki tekstur yang sama dengan bulbasaur, semuanya akan sederhana. Tetapi dengan pohon Natal, teksturnya hanya menutupi cabang-cabangnya. Penting untuk memisahkan mereka dan menerapkannya hanya untuk mereka. Bagaimana cara melakukannya? Anda dapat mendekati masalah ini dengan berbagai cara. Saatnya menggunakan console.log dan melihat model itu sendiri.


Jika Anda tidak tahu cara menyorot bagian tertentu dari model, gunakan console.log. Ini biasanya merupakan cara tercepat untuk mengetahui perbedaan bagian-bagiannya.

Biasanya kami memiliki dua opsi untuk bagaimana membagi model menjadi beberapa bagian. Yang pertama (bagus) adalah ketika artis 3D menandatangani bagian komponen model dan kami memiliki akses ke bidang nama mereka dan kami dapat menentukan dari mereka apa itu apa. Dalam contoh kita, ini bukan, tetapi ada nama-nama bahan. Kami akan menggunakannya. Untuk bagian-bagian model dari bahan "Christmas_Tree" kami akan menggunakan tekstur:


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

Jadi kita mendapatkan sesuatu seperti ini:



Untuk bagian yang terbuat dari bahan "merah" dan "merah muda" (ini adalah bola - bola Natal), kami cukup menetapkan warna acak. Dalam kasus seperti itu, mudah untuk menggunakan 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; } 

Catatan untuk artis: berikan nama yang bermakna untuk semua yang ada di model. Nama-nama bahan dalam contoh kita hanya mematahkan otak. Di sini merah bisa berwarna hijau. Saya tidak mengubahnya untuk menunjukkan seluruh absurditas dari apa yang terjadi. Nama abstrak "material for balls" akan lebih universal.

Proyeksi segi empat


Proyeksi kata majemuk equirectangular dalam terjemahan ke dalam bahasa Rusia adalah proyeksi setara-ke-perantara. Diterjemahkan ke dalam rumah tangga - menarik bola pada persegi panjang. Anda bisa mengutip saya. Kita semua melihat peta dunia di sekolah - itu berbentuk persegi panjang, tetapi kita mengerti bahwa jika kita mengubahnya sedikit, kita mendapatkan bola dunia. Ini dia. Untuk lebih memahami bagaimana distorsi ini diatur, lihat gambarnya:



Saat membuat thumbnail dari berbagai produk, latar belakang sering dibuat menggunakan proyeksi seperti itu. Kami mengambil gambar yang terdistorsi dengan lingkungan dan menampilkannya pada bidang yang besar. Kamera tampaknya ada di dalamnya. Itu terlihat seperti ini:


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

Sebagai contoh, saya sengaja merusak tepinya, jadi jika Anda menggunakan contoh dari github, maka Anda dapat menemukan jahitan yang berbeda di sepanjang gambar ditutup. Jika ada yang tertarik, maka aslinya diambil dari sini .


Total saat ini kami memiliki sesuatu seperti ini:



Pohon Natal dengan bola-bola berwarna terlihat sangat imut.


Kontrol orbit


Untuk menghargai keindahan ruang tiga dimensi, tambahkan kontrol mouse. Dan kemudian semuanya tampak dalam 3D, Anda harus memutar semuanya. Biasanya, OrbitControls digunakan dalam tugas-tugas seperti itu.


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

Dimungkinkan untuk menetapkan batasan pada sudut di mana Anda dapat memutar kamera, pembatasan zoom dan opsi lainnya. Sangat berguna untuk melihat dokumentasi, ada banyak hal menarik.


Anda tidak bisa memberi tahu banyak tentang jenis kontrol ini. Terhubung, dihidupkan dan berfungsi. Hanya saja, jangan lupa untuk memperbarui keadaan secara teratur:


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

Di sini Anda bisa sedikit terganggu, memuntir pohon Natal ke arah yang berbeda ...


Raycaster


Raycaster memungkinkan Anda melakukan hal berikut: ia menarik garis lurus di ruang angkasa dan menemukan semua objek yang bersinggungan dengannya. Ini memungkinkan Anda untuk melakukan banyak hal menarik yang berbeda, tetapi dalam konteks presentasi produk akan ada dua kasus utama - ini adalah untuk menanggapi mouse mengarahkan sesuatu dan menanggapi klik mouse pada sesuatu. Untuk melakukan ini, Anda perlu menggambar garis tegak lurus ke layar melalui titik dengan koordinat mouse dan mencari persimpangan. Ini yang akan kita lakukan. Perpanjang fungsi render, cari persimpangan dengan bola dan cat ulang:


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

Dengan gerakan sederhana mouse bolak-balik, kami memastikan bahwa semuanya berfungsi.



Tapi ada satu kehalusan - Three.js tidak tahu cara mengubah warna dengan lancar. Dan secara umum, perpustakaan ini bukan tentang perubahan nilai yang mulus. Inilah saatnya menghubungkan beberapa alat yang dirancang untuk ini, misalnya Anime.js.


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

Kami menggunakan pustaka ini untuk menghidupkan nilai:


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

Sekarang warnanya berubah dengan mulus, tetapi hanya setelah mouse menjauh dari bola. Sesuatu harus diperbaiki. Kami akan menggunakan simbol untuk ini - mereka memungkinkan kami menambahkan informasi meta ke objek dengan aman, dan kami hanya perlu menambahkan informasi tentang apakah bola dianimasikan atau tidak.


Simbol di ES6 + adalah alat yang sangat kuat yang, di antaranya, memungkinkan Anda untuk menambahkan informasi ke objek dari pustaka pihak ketiga tanpa takut hal ini akan menyebabkan konflik nama atau merusak logika.

Kami membuat konstanta global (secara teori, akan sangat berharga untuk membuat objek global untuk semua simbol tersebut, tetapi kami memiliki contoh sederhana, kami tidak akan mempersulitnya):


 const _IS_ANIMATED = Symbol('is animated'); 

Dan kami menambahkan tanda centang pada fungsi pengecatan ulang bola:


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

Sekarang mereka dengan lancar mengecat ulang segera di atas hover. Dengan demikian, dengan bantuan simbol, Anda dapat dengan cepat menambahkan pemeriksaan serupa dalam animasi tanpa menyimpan status semua bola di tempat yang terpisah.


Tip alat


Hal terakhir yang kami lakukan hari ini adalah tooltips. Tugas ini sering dijumpai. Sebagai permulaan, kita hanya perlu menebusnya.


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

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

Ingatlah untuk menonaktifkan event-pointer jika Anda tidak membutuhkannya.

Tetap menambahkan CSS3DRenderer. Ini sebenarnya tidak cukup renderer, itu lebih merupakan sesuatu yang hanya menambahkan transformasi CSS ke elemen dan tampaknya mereka berada di adegan yang umum. Untuk label pop-up - ini hanya yang Anda butuhkan. Kami membuat variabel global CSSRENDERER, menginisialisasi dan jangan lupa memanggil fungsi render itu sendiri. Semuanya tampak seperti penyaji biasa:


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

Tidak ada yang terjadi saat ini. Sebenarnya, kami tidak melakukan apa pun. Kami menginisialisasi elemen pop-up, kami dapat segera bermain dengan ukuran dan posisinya di ruang:


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

Sekarang kita melihat tulisan “dalam 3D”. Faktanya, itu tidak sepenuhnya dalam 3D, itu terletak di atas kanvas, tetapi untuk tips pop-up itu tidak begitu penting, efeknya penting


Sentuhan terakhir tetap - untuk menunjukkan tulisan dengan halus dalam rentang sudut tertentu. Sekali lagi, gunakan simbol global:


 const _IS_VISIBLE = Symbol('is visible'); 

Dan kami memperbarui status elemen pop-up tergantung pada sudut rotasi kamera:


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

Semuanya cukup sederhana. Sekarang prasasti dengan lancar muncul dan menghilang. Anda dapat menambahkan auto-rotate dan nikmati hasilnya.


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


Kesimpulan


Hari ini kita melihat bagaimana menampilkan model tiga dimensi di halaman kita, bagaimana mengubahnya dengan mouse, bagaimana membuat tooltips, bagaimana menanggapi mouse yang melayang di atas bagian-bagian tertentu dari model, dan bagaimana menggunakan karakter dalam konteks berbagai animasi. Semoga informasi ini bermanfaat. Nah, semua dengan yang akan datang, sekarang Anda tahu apa yang bisa Anda pelajari selama liburan.


PS: Sumber lengkap untuk contoh herringbone tersedia di github .

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


All Articles