Dreidimensionale Produktpräsentationen auf Three.js für die Kleinsten


Alle Arten von Produktpräsentationen in 3D sind in unserer Zeit nicht so selten, aber diese Aufgaben werfen bei Anfängern viele Fragen auf. Heute werden wir uns einige Grundlagen ansehen, die Ihnen helfen, in dieses Thema einzusteigen und nicht auf eine so einfache Aufgabe wie das Anzeigen eines dreidimensionalen Modells in einem Browser zu stoßen. Als Hilfe werden wir Three.js als das beliebteste Tool in diesem Bereich verwenden.


An die Arbeit gehen


Lassen Sie uns zunächst eine HTML-Vorlage für uns selbst erstellen. Um das Beispiel nicht zu komplizieren, verwenden wir nichts Überflüssiges, keine Assembler, Präprozessoren usw.


Wir brauchen einen Container für Canvas und eine Reihe von Skripten - eigentlich three.js, einen Loader für Modelle im obj-Format und ein Skript zur Steuerung der Kamera mit der Maus.


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

Wenn Ihr Projekt NPM und Builder verwendet, können Sie all dies aus Paket drei importieren. Wenn jemand es nicht weiß, nimmt Unpkg tatsächlich alle NPM-Pakete.


Wenn Sie schnell etwas aus einem Paket mit Ihrer Seite verbinden müssen, aber den Link zum CDN nicht gefunden haben, denken Sie an Unpkg, wahrscheinlich benötigen Sie ihn.

Das Hauptskript beginnt mit einer Reihe globaler Variablen. Dies vereinfacht das Beispiel.


 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; 

In Three.js beginnt alles mit der Szene. Initialisieren Sie sie also und erstellen Sie ein paar Lichtquellen:


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

Lichtquellen sind unterschiedlich. Am häufigsten wird bei solchen Aufgaben Umgebungslicht verwendet - Fülllicht und Richtungslicht in eine bestimmte Richtung. Es gibt immer noch punktuelle Lichtquellen, aber wir brauchen sie noch nicht. Wir machen die Leuchtfarbe weiß, damit es nicht zu Verzerrungen kommt.


Es kann nützlich sein, mit der Farbe des Fülllichts zu spielen, insbesondere mit Graustufen, damit Sie ein weicheres Bild erhalten.

Das zweite wichtige ist die Kamera. Dies ist eine solche Einheit, die den Punkt bestimmt, an dem wir uns befinden, und die Richtung, in die wir schauen.


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

Die Kameraparameter werden normalerweise per Auge ausgewählt und hängen von den verwendeten Modellen ab.


Das dritte Objekt, das wir brauchen, ist ein Renderer. Er ist für das Rendern des Bildes verantwortlich. Ihre Initialisierung spricht für sich:


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

Lader werden benötigt, um Daten in verschiedenen Formaten zu laden. Hier finden Sie eine lange Liste von Optionen, aber wir benötigen nur zwei - eine für Bilder (im Lieferumfang des Kits enthalten) und eine für Modelle (wir haben sie am Anfang angeschlossen).


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

Wir laden das Modell. Wie erwartet erfolgt dies asynchron. Nach dem Laden des Modells können wir mit seinen Parametern spielen:


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

Es bleibt die Drehorgel zu starten:


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

Als Ergebnis erhalten wir nur einen weißen Weihnachtsbaum mit Schatten (ich habe das Modell von hier genommen ).



Eins, zwei, drei, Weihnachtsbaum brennen! Aber ohne Texturen brennt es natürlich nicht. Und wir werden ein anderes Mal über Feuershader und andere Elemente sprechen ... Aber zumindest können wir sehen, dass das Modell des Weihnachtsbaums "im Fernsehen" ist.


Bevor Sie mit Texturen fortfahren, sollten Sie einen Standard-Handler für die Größenänderung des Browserfensters hinzufügen:


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

Textur hinzufügen


Unsere Modell- und Bildtextur basiert auf dem Prinzip der Aufkleber-Übersetzer für Kinderausrüstungsmodelle. Wie wir bereits wissen, bestehen Objekte im Kontext von WebGL aus einem Haufen Dreiecke. An sich haben sie keine Farbe. Für jedes Dreieck gibt es den gleichen dreieckigen „Aufkleber“ mit der Textur, die Sie zum Aufkleben benötigen. Aber wenn wir 1000 Dreiecke haben, müssen wir 1000 Texturbilder laden? Natürlich nicht. Es wird ein Sprite erstellt, genau wie bei CSS-Symbolen (auf die Sie wahrscheinlich bei der Arbeit gestoßen sind), und Informationen darüber, welche Dreiecke und wo sich darauf befinden, werden dem Modell selbst hinzugefügt. Und dann versteht Three.js schon unabhängig alles und wir sehen das Endergebnis. Tatsächlich ist alles etwas komplizierter, aber die Idee sollte so verstanden werden.


Weihnachtsbaumzweige sind kein sehr aufschlussreiches Beispiel. Sie sind alle gleich. Die Struktur einer solchen Textur wird am Beispiel eines Bulbasaur viel besser zu sehen sein:



Aber genug Worte, lasst uns zum Handeln kommen. Initialisieren Sie die Textur und laden Sie ein Bild damit:


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

Jetzt müssen wir die Modellladefunktion erweitern. Wenn wir die gleiche Textur wie der Bulbasaur hätten, wäre alles einfach. Bei einem Weihnachtsbaum bedeckt die Textur jedoch nur die Zweige. Es ist notwendig, sie irgendwie zu trennen und nur auf sie anzuwenden. Wie kann man das machen? Sie können dieses Problem auf verschiedene Arten angehen. Es ist Zeit, console.log zu verwenden und sich das Modell selbst anzusehen.


Wenn Sie nicht wissen, wie ein bestimmter Teil des Modells hervorgehoben werden soll, verwenden Sie console.log. Dies ist normalerweise der schnellste Weg, um herauszufinden, wie sich die Teile unterscheiden.

Normalerweise haben wir zwei Möglichkeiten, das Modell in Teile zu unterteilen. Das erste (gute) ist, wenn der 3D-Künstler die Bestandteile des Modells signiert hat und wir Zugriff auf die Namensfelder von ihnen haben und daraus bestimmen können, was was ist. In unserem Beispiel ist dies nicht der Fall, aber es gibt Materialnamen. Wir werden sie benutzen. Für Teile des Modells aus dem Material „Christmas_Tree“ verwenden wir die Textur:


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

Also bekommen wir so etwas:



Für Teile aus „roten“ und „rosa“ Materialien (dies sind Kugeln - Weihnachtskugeln) stellen wir einfach eine zufällige Farbe ein. In solchen Fällen ist es zweckmäßig, HSL zu verwenden:


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

Hinweis für Künstler: Geben Sie allen Modellen aussagekräftige Namen. Die Namen der Materialien in unserem Beispiel brechen einfach das Gehirn. Hier kann Rot grün sein. Ich habe sie nicht geändert, um die ganze Absurdität dessen zu zeigen, was geschah. Der abstrakte Name "Material für Bälle" wäre universeller.

Gleichwinklige Projektion


Die zusammengesetzte wortgleiche rechteckige Projektion bei der Übersetzung ins Russische ist eine Projektion gleich der Zwischenstufe. Übersetzt in den Haushalt - zog den Ball auf ein Rechteck. Du kannst mich zitieren. Wir alle haben in der Schule eine Weltkarte gesehen - sie ist rechteckig, aber wir verstehen, dass wir einen Globus bekommen, wenn wir sie ein wenig verändern. Das ist es. Schauen Sie sich das Bild an, um besser zu verstehen, wie diese Verzerrungen angeordnet sind:



Beim Erstellen von Miniaturansichten verschiedener Produkte wird der Hintergrund häufig mithilfe solcher Projektionen erstellt. Wir machen ein verzerrtes Bild mit der Umgebung und zeigen es auf einer großen Kugel an. Die Kamera scheint sich darin zu befinden. Es sieht ungefähr so ​​aus:


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

Als Beispiel habe ich die Kanten absichtlich untergraben. Wenn Sie also das Beispiel aus dem Github verwenden, finden Sie dort eine deutliche Naht, entlang der sich das Bild schließt. Wenn jemand interessiert ist, dann wird das Original von hier genommen .


Insgesamt haben wir im Moment so etwas:



Ein Weihnachtsbaum mit bunten Kugeln sieht ziemlich süß aus.


Orbit-Kontrollen


Fügen Sie die Maussteuerung hinzu, um die Schönheit eines dreidimensionalen Raums zu schätzen. Und dann scheint alles in 3D zu sein, Sie müssen alles verdrehen. Normalerweise wird OrbitControls für solche Aufgaben verwendet.


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

Es ist möglich, Einschränkungen für die Winkel festzulegen, um die Sie die Kamera drehen können, Einschränkungen für den Zoom und andere Optionen. Es ist nützlich, in die Dokumentation zu schauen, es gibt viele interessante Dinge.


Über diese Art der Steuerung kann man nicht viel erzählen. Verbunden, eingeschaltet und es funktioniert. Vergessen Sie nicht, den Status regelmäßig zu aktualisieren:


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

Hier kann man sich etwas ablenken lassen, den Weihnachtsbaum in verschiedene Richtungen drehen ...


Raycaster


Mit Raycaster können Sie Folgendes tun: Es zeichnet eine gerade Linie im Raum und findet alle Objekte, mit denen es sich schneidet. Auf diese Weise können Sie viele verschiedene interessante Dinge tun. Im Zusammenhang mit Produktpräsentationen gibt es jedoch zwei Hauptfälle: Sie müssen auf den Mauszeiger über etwas reagieren und auf einen Mausklick auf etwas reagieren. Dazu müssen Sie Linien senkrecht zum Bildschirm durch einen Punkt mit den Koordinaten der Maus zeichnen und nach Schnittpunkten suchen. Das werden wir tun. Erweitern Sie die Renderfunktion, suchen Sie nach Schnittpunkten mit Kugeln und streichen Sie sie neu:


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

Mit einfachen Bewegungen der Maus hin und her stellen wir sicher, dass alles funktioniert.



Aber es gibt eine Subtilität: Three.js weiß nicht, wie man Farben reibungslos ändert. Im Allgemeinen geht es in dieser Bibliothek nicht um reibungslose Wertänderungen. Hier ist die Zeit, um ein Tool zu verbinden, das dafür entwickelt wurde, zum Beispiel Anime.js.


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

Wir verwenden diese Bibliothek, um Werte zu animieren:


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

Jetzt ändern sich die Farben sanft, aber erst, nachdem sich die Maus vom Ball entfernt hat. Es muss etwas repariert werden. Dazu verwenden wir Symbole - sie ermöglichen es uns, Objekten sicher Metainformationen hinzuzufügen, und wir müssen nur Informationen darüber hinzufügen, ob der Ball animiert ist oder nicht.


Symbole in ES6 + sind ein sehr leistungsfähiges Tool, mit dem Sie unter anderem Informationen zu Objekten aus Bibliotheken von Drittanbietern hinzufügen können, ohne befürchten zu müssen, dass dies zu einem Namenskonflikt führt oder die Logik verletzt.

Wir machen eine globale Konstante (theoretisch wäre es wert, ein globales Objekt für alle diese Symbole zu erstellen, aber wir haben ein einfaches Beispiel, wir werden es nicht komplizieren):


 const _IS_ANIMATED = Symbol('is animated'); 

Und wir fügen der Funktion zum Neulackieren der Kugeln einen Scheck hinzu:


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

Jetzt streichen sie beim Schweben sofort glatt neu. So können Sie mithilfe von Symbolen schnell ähnliche Überprüfungen in Animationen hinzufügen, ohne die Zustände aller Bälle an einem separaten Ort zu speichern.


Tooltips


Das Letzte, was wir heute tun, sind Tooltips. Diese Aufgabe wird häufig angetroffen. Für den Anfang müssen wir sie nur erfinden.


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

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

Denken Sie daran, Zeigerereignisse zu deaktivieren, wenn Sie sie nicht benötigen.

Es bleibt noch CSS3DRenderer hinzuzufügen. Dies ist eigentlich kein richtiger Renderer, sondern eine Sache, die den Elementen einfach CSS-Transformationen hinzufügt, und es scheint, dass sie sich in einer gemeinsamen Szene befinden. Für Popup-Labels ist dies genau das, was Sie brauchen. Wir machen die globale Variable CSSRENDERER, initialisieren sie und vergessen nicht, die Renderfunktion selbst aufzurufen. Alles sieht aus wie ein normaler Renderer:


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

Im Moment ist nichts passiert. Eigentlich haben wir nichts gemacht. Wir initialisieren das Popup-Element und können sofort mit seiner Größe und Position im Raum spielen:


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

Jetzt sehen wir die Inschrift „in 3D“. Tatsächlich ist es nicht vollständig in 3D, es liegt oben auf der Leinwand, aber für Popup-Tipps ist es nicht so wichtig, der Effekt ist wichtig


Die letzte Berührung bleibt - um die Inschrift in einem bestimmten Winkelbereich reibungslos darzustellen. Verwenden Sie erneut das globale Symbol:


 const _IS_VISIBLE = Symbol('is visible'); 

Und wir aktualisieren den Status des Popup-Elements abhängig vom Drehwinkel der 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; } } 

Alles ist ganz einfach. Jetzt erscheint die Inschrift reibungslos und verschwindet. Sie können Auto-Rotation hinzufügen und das Ergebnis genießen.


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


Fazit


Heute haben wir uns angesehen, wie dreidimensionale Modelle auf unserer Seite angezeigt werden, wie man sie mit der Maus dreht, wie man QuickInfos erstellt, wie man auf Mausbewegungen über bestimmten Teilen des Modells reagiert und wie man Zeichen im Kontext verschiedener Animationen verwendet. Hoffe, diese Informationen sind hilfreich. Nun, alle mit dem bevorstehenden, jetzt wissen Sie, was Sie in den Ferien lernen können.


PS: Die vollständigen Quellen für das Fischgrätenbeispiel finden Sie auf dem Github .

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


All Articles