Metallic Flakes Material in Three.js and Next.js

How to Create a Metallic Flakes Material

11.09.2024

dethree.jsnext.jstypescripten
Gregor Wedlich
Gregor Wedlich
Life, the Universe and Everything.
Donate with:
Lightning
Alby

Table of Contents

    As a software developer, there’s almost never a day when you’re not dealing with new things. This was also true for a recent project of mine, where I am implementing a product configurator using Three.js and Next.js. For this configurator, I needed realistic-looking coatings. Most coatings (matte, glossy) are relatively easy to create, but one of them became a bit more complicated because I needed something called a "flakes effect."

    Since I didn’t find much helpful information online, I want to show you in this guide how to create a material using the "flakes effect" and map it onto our model. For this, I’m using the Next.js framework based on React.

    If you have any questions, feedback, or tips, feel free to leave them in the comments.

    You can see the result directly at Codesandbox.

    1. Project Setup

    First, we need a clean installation of Next.js. I’ll assume that you already know how to set this up. However, to follow along with the guide, here are my settings and structure:

    • Next.js version 14
    • TypeScript
    • App Directory (without 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

    However, it’s entirely up to you how you structure your folders. This is just a suggestion.

    1.1 3D Model

    In my project, we use the model "Suzanne." Suzanne is a monkey model that serves as the mascot of Blender, the popular open-source 3D software. It was developed to provide a simple geometry that can be used for tests, demos, and tutorials. Of course, you can work with your own model, but please note that we are using a GLB file in this project. I’ll quickly show you how to create a gLTF or GLB file in Blender.

    GLB is the binary file format representation of 3D models stored in the GL Transmission Format (glTF). It contains 3D model information like node hierarchy, cameras, materials, animations, and meshes in binary format. This binary format stores the glTF asset (JSON, .bin, and images) in a single binary blob. Source

    For those who want the 3D model, you can download it here, and for everyone else, here’s a brief guide on how you can create it yourself using Blender 4.2.

    First, start Blender and delete the cube and all lights to get a clean workspace.

    Next, go to "Add" -> "Mesh" -> "Monkey," and you’ll get Suzanne, our monkey.

    She looks a bit blocky, but this can be quickly fixed with the "Subdivision Surface Modifier." Just go to "Modifiers" (the blue wrench on the right), click "Add Modifier" -> "Generate" -> "Subdivision Surface." Set "Level Viewport" and "Render" to 3 (higher values will impact performance). Lastly, right-click on the model and select "Shade Auto Smooth" to make it look smoother.

    When satisfied, export your model by going to "File" -> "Export" -> "glTF 2.0" and save it in your project under /public/assets. Make sure to check "Data" -> "Mesh" -> "Apply Modifiers."

    Since we’re working on the web, we want to use smaller files. We can do this with the fantastic tool "gltf-pipeline", which can compress glTF files. We also use the "Draco" library, which specializes in compressing meshes.

    Simply run the following command in your terminal:

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

    Feel free to play around with the value of "--draco.compressionLevel="; for me, 7 worked best. The size of my GLB file was reduced from 1.5MB to 82KB, which is a great result to work with.

    Save the model and texture in the "/public/assets" folder. The important thing is that it’s located in the public folder!

    1.2 Texture

    This part is a bit more individual. It’s best to use a texture from the internet or create one yourself. If you don’t want to search for long, use this one: Texture.

    1.3 Install Packages

    First, install all the packages needed for this project.

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

    Three.js (three)

    Three.js is a powerful JavaScript library that enables cross-browser 3D computer graphics using WebGL.

    three-stdlib (three-stdlib)

    Three.js provides several templates that are commonly needed, and instead of copying/pasting, we use the stand-alone package to make our work easier.

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

    @react-three/fiber connects the world of React with Three.js and offers a declarative, component-based way to create 3D scenes. It functions as a custom renderer for React, specifically tailored to the needs of Three.js.

    In summary, @react-three/fiber allows us to write Three.js code in React components and take advantage of React's full reactivity (useEffect, useRef).

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

    The @react-three/drei package is a collection of useful helper components, extensions, and abstractions for use with @react-three/fiber. It provides a wide range of helper components and extensions built on Three.js and optimized specifically for use with @react-three/fiber.

    2. Model Component

    Now that we have created or downloaded all files and placed them in our Next.js project, we first create our model component in the folder "/components/Model.tsx".

    We start by importing our packages and also our material, which we’ll create later.

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

    Next, we type our model (Suzanne) using the tool "GLTJSX". GLTJSX helps us work with 3D models and creates JSX components for our model.

    There are two options: you can use the command-line tool via NPX or the online generator.

    The command for the terminal:

    1npx gltfjsx --types suzanne.glb

    --types generates the correct typing for our model.

    We now get a file named "suzanne.tsx," which contains our component for the model.

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

    Now, let’s return to our "Model.tsx" file and add the types we just generated below the imports. We didn’t export any materials from Blender, but to prevent TypeScript from throwing an error, we should type the material. "animations: GLTFAction[]" can be removed since we don’t need it in our example and it’s not part of GLTFResult.

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

    Now that we have defined the types for our model, let’s focus on the rest of the model.

    First, we integrate our model using "useGLTF()", and in the second line, we load the material that we will create in the next step.

    In the return() statement, we use the <group {...props}> element. It allows us to logically group 3D objects, serving as a container for mesh elements and enabling easier handling and transformation of the entire group of objects. By passing props to the <group> element, properties can be controlled externally, enhancing the reusability and customizability of the component.

    We bind the model using the <mesh geometry={nodes.Suzanne.geometry}> element. There are various ways to assign materials to the model or parts of it. If we want to use the material directly from our GLB file, we could include it like this: <mesh material={nodes.Suzanne.material}>. However, it's important to note that Three.js cannot handle complex shaders from Blender or other 3D programs. Another way is to create the material using built-in tools, for which we use <meshPhysicalMaterial ...>. This material provides useful tools to represent realistic and complex materials. We’ll go into detail about the material and its properties in the next section.

    Since the <group> element allows us to nest multiple models and materials, we can use it here to simulate different coating layers for a more realistic paint effect. By using {...useFlakesMaterialProps}, we pass the properties we will define later to our material. In the second <meshPhysicalMaterial> element, we ensure it renders after the outer mesh with <... renderOrder=100>. This is important for correct layer rendering and prevents potential artifacts. We adjust the transparency of the outer material with transmission={0.95}.

    The <Html> element is part of the @react-three/drei library and allows you to place HTML content directly within the 3D scene. It’s particularly useful for user interfaces, labels, or, as in this case, loading and error messages within the 3D environment.

    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

    Now let’s move on to our material, which we already referenced in our model component. We create a hook in the folder "/hooks/materials/useFlakesMaterial.ts" and insert the following content.

    In the first line, we load our texture into the project using useTexture. A few lines further, we define how the texture should behave. Here’s a detailed explanation of the settings:

    texture.wrapS = texture.wrapT = THREE.RepeatWrapping; This line sets how the texture behaves when it exceeds the boundaries of the UV mapping. THREE.RepeatWrapping means that the texture will repeat in both directions (S and T, corresponding to X and Y). This is useful for seamless, repeating patterns.

    texture.repeat.set(35, 35); This sets how often the texture should repeat. In this case, the texture repeats 60 times in both directions, creating a very dense flake pattern on the surface.

    texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); Anisotropic filtering improves texture quality at steep viewing angles. This line sets anisotropic filtering to the highest value supported by the user’s graphics card, resulting in sharper textures, especially at low angles, which is crucial for the flakes effect. To make this work, we determine the maximum value the user’s graphics card supports in the useEffect() hook.

    In the materialProps object, we define our material's properties. Here’s an explanation of each value:

    clearcoat: Simulates a thin, clear layer over the base material.

    clearcoatRoughness: Determines the roughness of the clear coat layer.

    metalness: Specifies how metallic the material appears.

    roughness: Defines the surface roughness of the base material.

    color: Sets the base color of the material.

    normalMap: Uses the loaded texture as a normal map to simulate surface details.

    transmission: Allows some light to pass through, creating a semi-transparent effect.

    transparent: true Activates transparency for the material.

    normalScale: new THREE.Vector2(0.4, 0.6) Controls the intensity of the 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. Rendering the Model in Canvas

    The Canvas element is a central part of React Three Fiber. It serves as a container for the entire 3D scene and represents the rendering area where all 3D objects are displayed. It automatically initializes a WebGL renderer and a scene, manages the render loop, and updates the scene when changes occur. We create a file "components/canvas/index.tsx" and insert the following content.

    Within the Canvas, we load our model, lights, environment graphics like HDR maps, and configure how the model behaves within the canvas space (OrbitControls). I won’t go into detail about OrbitControls properties here; if you want to learn more, check out the documentation. The key to rendering our material is the environmental lighting and the associated reflections. HDRI images are best suited as they provide a realistic overall look. We use the <Environment ...> element to include the HDRI map, specify the resolution with resolution={256}, and set background={false} to keep the background invisible.

    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;

    We now need to include the CanvasModel component in our project within the file app/pages.tsx.

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

    Conclusion

    We have now learned step-by-step how to implement a realistic “flakes effect” in a project using Three.js and Next.js. From setting up the project structure, creating and adjusting a 3D model in Blender, to implementing a specialized material in React Three Fiber, the entire process is described in detail. By using special libraries and tools such as Three.js, @react-three/fiber, and @react-three/drei, a complex effect is implemented in an understandable way without having to dive deep into shader programming. This project provides a solid foundation for integrating 3D models with realistic textures into web applications.

    This post is automatic translatet from german to english

    Comments: