Metallic-Flakes Material in three.js und next.js

Wir wir einen Metallic Flakes Material erstellen

11.09.2024

deentypescriptnext.jsreactthree.js
Gregor Wedlich
Gregor Wedlich
Life, the Universe and Everything.
Donate with:
Lightning
Alby

Inhaltsverzeichnis

    Als Softwareentwickler:in gibt es fast keinen Tag, an dem du dich nicht mit neuen Dingen beschäftigen musst. So auch bei einem aktuellen Projekt von mir, wo ich einen Produktkonfigurator mit Three.js und Next.js umsetze. Bei diesem Konfigurator benötigte ich unter anderem realistisch aussehende Lackierungen. Die meisten Lackierungen (matt, glänzend) sind auch relativ einfach zu erstellen, aber bei einer wurde es dann doch etwas komplizierter, denn ich benötige einen sogenannten "Flakes-Effekt".

    Da ich im Netz nicht viel Hilfreiches gefunden habe, möchte ich in dieser Anleitung zeigen, wie wir anhand unseres "Flakes-Effekts" ein Material erstellen und dieses auf unser Modell mappen. Ich verwende dazu das auf React basierende Framework Next.js.

    Wenn ihr Fragen, Kritik und Tipps habt, dann schreibt sie doch einfach in die Kommentare.

    Das Ergebnis könnt ihr euch direkt bei Codesandbox anschauen.

    1. Projektvorbereitungen

    Wir benötigen zuerst eine saubere Installation von Next.js. Ich gehe jetzt davon aus, dass ihr selbst wisst, wie ihr diese aufsetzt. Damit ihr aber der Anleitung folgen könnt, hier kurz meine Einstellungen und meine Struktur:

    • Next.js in der Version 14
    • TypeScript
    • App Directory (ohne src)
    • TailwindCss
    1project 2... 3... 45└───app 6│ │ ... 7│ └───components 8│ │ Model.tsx 9│ └───canvas 10│ │ index.tsx 11│ └───hooks 12│ └───materials 13│ │ useFlakesMaterial.ts 14│ │ ... 15└───public 16│ └───assets 17│ │ flakes.png 18│ │ suzanne.glb

    Letztlich ist es euch aber überlassen, wie ihr die Ordner strukturiert. Dies hier ist nur ein Vorschlag meinerseits.

    1.1 3D-Modell

    In meinem Projekt nutzen wir das Modell "Suzanne". Suzanne ist ein Affenmodell, das als Maskottchen von Blender, der beliebten Open-Source-3D-Software, dient. Es wurde entwickelt, um eine einfache Geometrie bereitzustellen, die für Tests, Demos und Tutorials genutzt werden kann. Ihr könnt natürlich auch mit eurem eigenen Modell arbeiten, beachtet aber bitte, dass wir ein GLB-File in unserem Projekt hier verwenden. Wie wir ein gLTF bzw. GLB-File in Blender erstellen, möchte ich euch kurz zeigen.

    GLB ist die binäre Dateiformatdarstellung von 3D-Modellen, die im GL-Übertragungsformat (glTF) gespeichert sind. Informationen zu 3D-Modellen wie Knotenhierarchie, Kameras, Materialien, Animationen und Meshes im Binärformat. Dieses Binärformat speichert das glTF-Asset (JSON, .bin und Bilder) in einem binären Blob. Quelle

    Für alle, die das 3D-Modell haben möchten, können es sich hier downloaden, und für alle anderen hier eine kleine Anleitung, wie ihr es euch mit Blender 4.2 selbst erstellen könnt.

    Wir starten also Blender und löschen als erstes den Cube und alle Lichter, sodass wir einen sauberen Raum erhalten.

    Nun gehen wir auf "Add" -> "Mesh" -> "Monkey" und erhalten somit Suzanne, unsere Affen-Dame.

    Diese schaut aber noch etwas kantig aus, das lässt sich schnell mit dem "Subdivision Surface Modifier" ändern. Geht dazu einfach auf "Modifiers" (Blauer Schraubenschlüssel, rechts), klickt nun auf "Add Modifier" -> "Generate" -> "Subdivision Surface". Hier stellen wir jetzt noch "Level Viewport" und "Render" auf 3 (Höhere Werte gehen auf die Performance). Als Letztes machen wir einen Rechts-Klick auf unser Modell und klicken auf "Shade Auto Smooth", damit wird unser Modell noch etwas smoother.

    Wenn wir nun zufrieden sind, exportieren wir unser Modell "File" -> "Export" -> "glTF 2.0" und speichern es in unserem Projekt im Ordner /public/assets ab. Wichtig ist hier, setzt einen Haken bei "Data" -> "Mesh" -> "Apply Modifiers".

    Da wir ja im Web unterwegs sind, ist es uns natürlich ein Anliegen, kleine Dateien zu nutzen. Dies können wir mit dem zauberhaften Tool "gltf-pipeline" tun. Damit kann man glTF-Files komprimieren. Wir nutzen zusätzlich noch die Bibliothek "Draco", welche sich auf die Komprimierung von Meshes spezialisiert hat.

    Dazu einfach in eurem Terminal folgenden Befehl ausführen:

    1npx gltf-pipeline -i suzanne.glb -o suzanne.glb --draco.compressionLevel=7

    Spielt hier gerne mit dem Wert bei "--draco.compressionLevel=" herum, bei mir hat sich 7 bewährt. Die Größe von meiner GLB-Datei hat sich von 1.5M auf 82k verringert, was durchaus ein guter Wert ist, mit dem wir arbeiten können.

    Das Modell und die Textur speichert ihr einfach im Ordner "/public/assets". Wichtig ist hier nur, dass es im Public-Ordner liegt!

    1.2 Textur

    Hier wird's jetzt etwas individuell. Nutzt am besten eine Textur aus dem Internet oder erstellt euch selbst eine. Wenn ihr nicht lange suchen wollt, nutzt diese hier: Textur.

    1.3 Pakete installieren

    Als Erstes installieren wir uns alle Pakete, die wir zur Umsetzung benötigen.

    1npm install three three-stdlib @react-three/drei @react-three/fiber

    Three.js (three)

    Three.js ist eine sehr mächtige JavaScript-Bibliothek, welche es ermöglicht, Browser-übergreifend 3D-Computer-Grafiken mit der Nutzung von WebGL darzustellen.

    three-stdlib (three-stdlib)

    In Three.js gibt es einige Vorlagen, die man durchaus im Alltag benötigt, und da wir nicht per Copy/Paste diese nutzen wollen, greifen wir auf das Stand-alone-Paket zurück und machen uns die Arbeit damit einfacher.

    @react-three/fiber (@react-three/fiber)

    @react-three/fiber verbindet die Welt von React mit Three.js und bietet dabei eine deklarative, komponentenbasierte Möglichkeit, 3D-Szenen zu erstellen. Es fungiert als Custom Renderer für React, der speziell auf die Anforderungen von Three.js zugeschnitten ist.

    Zusammengefasst ermöglicht uns @react-three/fiber, Three.js Code in React-Komponenten zu schreiben, und wir können somit die komplette Reaktivität von React (useEffect, useRef) nutzen.

    @react-three/drei (@react-three/drei)

    Das Paket @react-three/drei ist eine Sammlung von nützlichen Helferkomponenten, Erweiterungen und Abstraktionen für die Nutzung mit @react-three/fiber. Das Paket stellt eine Vielzahl an Hilfskomponenten und Erweiterungen bereit, die auf Three.js aufbauen und speziell für die Nutzung mit @react-three/fiber optimiert sind.

    2. Modellkomponente

    So, nachdem wir nun alle Dateien erstellt oder heruntergeladen haben und diese in unserem Next.js-Projekt abgelegt haben, erstellen wir als Erstes unsere Modellkomponente im Ordner "/components/Model.tsx".

    Wir importieren zuerst unsere Pakete und auch gleich unser Material, welches wir später noch erstellen.

    1// components/Model.tsx 2import * as THREE from "three"; 3import { ThreeElements } from "@react-three/fiber"; 4import { Html, useGLTF, useTexture } from "@react-three/drei"; 5import { GLTF } from "three-stdlib"; 6 7import { useFlakesMaterial } from "../hooks/materials/useFlakesMaterial"; 8 9...

    Als Nächstes typisieren wir unser Modell (Suzanne), dafür nutze ich das Tool "GLTJSX". GLTJSX hilft uns, mit 3D-Modellen zu arbeiten und erstellt JSX-Komponenten zu unserem Modell.

    Es gibt zwei Möglichkeiten: ihr nutzt das Commandline-Tool per NPX oder den Online-Generator.

    Der Befehl für das Terminal:

    1npx gltfjsx --types suzanne.glb

    --types erstellt uns die richtige Typisierung für unser Modell.

    Wir erhalten nun eine Datei namens "suzanne.tsx", welche unsere Komponente für unser Modell beinhaltet.

    1/* 2Auto-generated by: https://github.com/pmndrs/gltfjsx 3Command: npx gltfjsx@6.5.0 --types suzanne.glb 4*/ 5 6import * as THREE from 'three' 7import React from 'react' 8import { useGLTF } from '@react-three/drei' 9import { GLTF } from 'three-stdlib' 10 11type GLTFResult = GLTF & { 12 nodes: { 13 Suzanne: THREE.Mesh 14 } 15 materials: {} 16 animations: GLTFAction[] 17} 18 19export function Model(props: JSX.IntrinsicElements['group']) { 20 const { nodes, materials } = useGLTF('/suzanne.glb') as GLTFResult 21 return ( 22 <group {...props} dispose={null}> 23 <mesh geometry={nodes.Suzanne.geometry} material={nodes.Suzanne.material} /> 24 </group> 25 ) 26} 27 28useGLTF.preload('/suzanne.glb')

    Nun gehen wir wieder in unsere Datei "Model.tsx" und fügen unter den Imports unsere eben generierten Typen ein. Wir haben in unserem Modell kein Material aus Blender exportiert, aber damit TypeScript keinen Fehler wirft, sollten wir das Material typisieren. "animations: GLTFAction[]" können wir entfernen, da wir es in unserem Beispiel nicht benötigen und auch kein Teil von GLTFResult ist.

    1// components/Model.tsx 2 materials: { 3 ["default"]: THREE.MeshStandardMaterial; 4 };
    1// components/Model.tsx 2import * as THREE from "three"; 3import { ThreeElements } from "@react-three/fiber"; 4import { Html, useGLTF, useTexture } from "@react-three/drei"; 5import { GLTF } from "three-stdlib"; 6 7import { useFlakesMaterial } from "../hooks/materials/useFlakesMaterial"; 8 9// Model Types generated with GLTJSX 10type GLTFResult = GLTF & { 11 nodes: { 12 Suzanne: THREE.Mesh; 13 }; 14 materials: { 15 ["default"]: THREE.MeshStandardMaterial; 16 }; 17}; 18...

    Nachdem wir nun für unser Modell die Typen festgelegt haben, können wir uns um den Rest des Modells kümmern.

    Als Erstes binden wir nun unser Modell mit "useGLTF()" ein und in der zweiten Zeile laden wir auch gleich das Material, welches wir im nächsten Schritt erstellen werden.

    Im return() nutzen wir das Element <group {...props}>. Es ermöglicht uns die logische Gruppierung von 3D-Objekten. Es dient als Container für die Mesh-Elemente und ermöglicht eine einfachere Handhabung und Transformation der gesamten Gruppe von Objekten. Durch die Übergabe von Props an das <group>-Element können Eigenschaften von außen gesteuert werden, was die Wiederverwendbarkeit und Anpassbarkeit der Komponente erhöht.

    Das Modell binden wir mit dem Element <mesh geometry={nodes.Suzanne.geometry}> ein. Hier gibt es nun verschiedene Wege, ein Material dem Modell oder Teilen des Modells zuzuweisen. Wenn wir das Material direkt aus unserem GLB-File nutzen möchten, könnten wir es folgendermaßen einbinden: <mesh material={nodes.Suzanne.material}>. Bei dieser Variante ist aber wichtig zu wissen, dass Three.js komplexe Shader aus Blender oder anderen 3D-Programmen nicht umgehen kann. Eine andere Variante ist, dass wir unser Material direkt mit hauseigenen Werkzeugen machen, dafür nutzen wir das <meshPhysicalMaterial ...>. Dieses Material liefert uns nützliche Werkzeuge, um realistische und komplexe Materialien darzustellen. Auf das Material und die Eigenschaften gehen wir aber erst im nächsten Punkt detailliert ein.

    Da uns das <group>-Element ermöglicht, mehrere Modelle und Materialien zu verschachteln, können wir es hier für einen realistischeren Lackeffekt nutzen, indem wir unterschiedliche Lackschichten simulieren. Mit {...useFlakesMaterialProps} übergeben wir unserem Material die Eigenschaften, die wir später noch definieren werden. Im zweiten <meshPhysicalMaterial>-Element stellen wir mit <... renderOrder=100> sicher, dass es nach dem äußeren Mesh gerendert wird. Dies ist wichtig für die korrekte Darstellung der Schichten und verhindert mögliche Rendering-Artefakte. Mit transmission={0.95} regeln wir die Transparenz des äußeren Materials.

    Das <Html>-Element ist Teil der @react-three/drei-Bibliothek und ermöglicht es, HTML-Inhalte direkt in der 3D-Szene zu platzieren. Es ist besonders nützlich für Benutzeroberflächen, Beschriftungen oder, wie in diesem Fall, für Lade- und Fehleranzeigen innerhalb der 3D-Umgebung.

    1// components/Model.tsx 2... 3const Model = (props: ThreeElements["group"]) => { 4 const { nodes } = useGLTF("/assets/suzanne-draco.glb") as GLTFResult; 5 const useFlakesMaterialProps = useFlakesMaterial(); 6 7 return ( 8 <group {...props}> 9 {nodes.Suzanne ? ( 10 <> 11 <mesh geometry={nodes.Suzanne.geometry}> 12 <meshPhysicalMaterial {...useFlakesMaterialProps} /> 13 </mesh> 14 <mesh geometry={nodes.Suzanne.geometry} renderOrder={100}> 15 <meshPhysicalMaterial 16 {...useFlakesMaterialProps} 17 transmission={0.95} 18 /> 19 </mesh> 20 </> 21 ) : ( 22 <Html> 23 <p>Model not found</p> 24 </Html> 25 )} 26 </group> 27 ); 28}; 29 30export default Model;

    3. Material

    Kommen wir nun zu unserem Material, das wir ja in unserer Modellkomponente schon eingebunden haben. Wir erstellen dazu einen Hook im Ordner /hooks/materials/useFlakesMaterial.ts und fügen folgenden Inhalt ein.

    In der ersten Zeile laden wir mit useTexture unsere Textur in unser Projekt. Ein paar Zeilen tiefer sagen wir, wie die Textur sich verhalten soll. Ich gehe hier im Detail auf die Settings ein:

    texture.wrapS = texture.wrapT = THREE.RepeatWrapping; Diese Zeile stellt ein, wie die Textur sich verhält, wenn sie über die Grenzen des UV-Mappings hinausgeht. THREE.RepeatWrapping bedeutet, dass die Textur in beide Richtungen (S und T, entsprechend X und Y) wiederholt wird. Das ist nützlich für nahtlose, sich wiederholende Muster.

    texture.repeat.set(35, 35); Hier wird festgelegt, wie oft die Textur wiederholt werden soll. In diesem Fall wird die Textur 60 Mal in beide Richtungen wiederholt. Das erzeugt ein sehr dichtes Muster von Flakes auf der Oberfläche.

    texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); Anisotrope Filterung verbessert die Qualität von Texturen bei schrägen Blickwinkeln. Diese Zeile setzt die anisotrope Filterung auf den höchsten Wert, den die Grafikkarte des Benutzers unterstützt. Das resultiert in schärferen Texturen, besonders bei flachen Blickwinkeln, was für den Flakes-Effekt wichtig ist. Damit dies funktioniert, ermitteln wir im useEffect() den Maximalwert, den die Grafikkarte des Benutzers unterstützt.

    Im Objekt materialProps legen wir die Eigenschaften unseres Materials fest. Hier eine Erklärung zu den einzelnen Werten:

    clearcoat: Simuliert eine dünne, klare Schicht über dem Basismaterial.

    clearcoatRoughness: Bestimmt die Rauheit der Klarlackschicht.

    metalness: Gibt an, wie metallisch das Material erscheint.

    roughness: Definiert die Oberflächenrauheit des Basismaterials.

    color: Setzt die Grundfarbe des Materials.

    normalMap: Verwendet die geladene Textur als Normal-Map, um Oberflächendetails zu simulieren.

    transmission: Ermöglicht eine gewisse Lichtdurchlässigkeit, was einen halbtransparenten Effekt erzeugt.

    transparent: true Aktiviert die Transparenz für das Material.

    normalScale: new THREE.Vector2(0.4, 0.6) Steuert die Intensität der Normal-Map.

    1// hooks/materials/useFlakesMaterial.ts 2import * as THREE from "three"; 3import { useTexture } from "@react-three/drei"; 4import { useEffect, useState } from "react"; 5 6export const useFlakesMaterial = () => { 7 const texture = useTexture("/assets/flakes.png"); 8 9 const [anisotropy, setAnisotropy] = useState<number>(1); 10 11 // Setting texture wrapping and repeating properties 12 texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 13 texture.repeat.set(35, 35); 14 texture.anisotropy = anisotropy; 15 16 useEffect(() => { 17 if (texture.anisotropy) { 18 const renderer = new THREE.WebGLRenderer(); 19 setAnisotropy(renderer.capabilities.getMaxAnisotropy()); 20 } 21 }, [anisotropy, texture]); 22 23 const materialProps: THREE.MeshPhysicalMaterialParameters = { 24 clearcoat: 0.7, 25 clearcoatRoughness: 0.03, 26 metalness: 0.65, 27 roughness: 0.6, 28 color: "green", 29 normalMap: texture, 30 transmission: 0.6, 31 transparent: true, 32 normalScale: new THREE.Vector2(0.4, 0.6), 33 }; 34 35 return materialProps; 36};

    4. Modell im Canvas rendern

    Das Canvas-Element ist ein zentraler Bestandteil von React Three Fiber. Es dient als Container für die gesamte 3D-Szene und stellt den Renderbereich dar, in dem alle 3D-Objekte angezeigt werden. Es initialisiert automatisch einen WebGL-Renderer und eine Szene und kümmert sich um den Render-Loop und aktualisiert die Szene bei Änderungen. Wir erstellen also eine Datei "components/canvas/index.tsx" und fügen den Inhalt ein.

    Innerhalb des Canvas laden wir unser Modell, Lichter, Umgebungs-Grafiken (z.B. HDR Maps), aber auch, wie sich unser Modell innerhalb unseres Canvas-Raums verhalten soll (OrbitControls). Ich gehe jetzt hier nicht weiter auf die Eigenschaften von OrbitControls ein, wenn ihr dazu mehr erfahren wollt, schaut einfach hier in der Dokumentation nach. Wichtig für die Darstellung unseres Materials ist die Umgebungsbeleuchtung und die damit verbundenen Reflektionen. Am besten eignen sich dazu HDRI-Bilder, da diese ein realistisches Gesamtbild ermöglichen. Wir binden dazu im <Environment ...>-Element die HDRI-Map ein, sagen mit resolution={256} noch, welche Auflösung diese haben soll, und mit background={false}, dass wir diese nicht im Hintergrund sehen möchten.

    1// components/canvas/index.tsx 2"use client"; 3import { Suspense } from "react"; 4import { Canvas } from "@react-three/fiber"; 5import { OrbitControls, Environment, Html } from "@react-three/drei"; 6 7import Model from "../Model"; 8 9const CanvasModel = () => { 10 return ( 11 <Canvas> 12 <directionalLight position={[10, 10, 5]} intensity={1} /> 13 <directionalLight position={[-10, -10, -5]} intensity={1} /> 14 <OrbitControls 15 enableDamping 16 dampingFactor={0.05} 17 enableZoom={true} 18 enablePan={false} 19 rotateSpeed={1.1} 20 minDistance={Math.PI / 1.8} 21 maxDistance={Math.PI / 0.4} 22 minPolarAngle={Math.PI / 3.5} 23 maxPolarAngle={Math.PI / 1.5} 24 /> 25 <Suspense fallback={<Html>loading...</Html>}> 26 <Environment 27 files={ 28 "https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/evening_road_01_2k.hdr" 29 } 30 background={false} 31 resolution={256} // Resolution of the environment map 256x256, compromise between quality and performance 32 /> 33 <Model /> 34 </Suspense> 35 </Canvas> 36 ); 37}; 38 39export default CanvasModel;

    Die CanvasModel Komponente müssen wir jetzt noch in unserem Projekt einbinden in der Datei app/pages.tsx.

    1// app/pages.tsx 2import CanvasModel from "./components/canvas"; 3 4export default function Home() { 5 return ( 6 <main className="flex w-fulll h-screen bg-amber-100"> 7 <CanvasModel /> 8 </main> 9 ); 10}

    Fazit

    Wir haben jetzt Schritt für Schritt gelernt, wie wir einen realistischen “Flakes-Effekt” in einem Projekt mit Three.js und Next.js umsetzen können. Vom Setup der Projektstruktur über die Erstellung und Anpassung eines 3D-Modells in Blender bis hin zur Implementierung eines speziellen Materials in React Three Fiber wurde der gesamte Prozess detailliert beschrieben. Durch die Verwendung von speziellen Bibliotheken und Werkzeugen wie Three.js, @react-three/fiber, und @react-three/drei wird ein komplexer Effekt auf eine verständliche Art umgesetzt, ohne tief in Shader-Programmierung einsteigen zu müssen. Dieses Projekt bietet eine solide Grundlage, um 3D-Modelle mit realistischen Texturen in Webanwendungen zu integrieren.

    Comments: