Skip to content
Three.js Geometries & Materials Explained — Complete Guide to Shapes, PBR & Textures

Three.js Geometries & Materials Explained — Complete Guide to Shapes, PBR & Textures

DodaTech Updated Jun 6, 2026 12 min read

Three.js geometries define the shape of your 3D objects (the skeleton), while materials define their appearance (the skin). Together they form a Mesh — the fundamental building block of any Three.js scene.

What You’ll Learn

By the end of this guide, you’ll understand all built-in geometries, the differences between material types (Basic, Standard, Physical, Toon), how to apply textures with UV mapping, create custom BufferGeometry from scratch, and build a complete geometry showcase.

Why Geometries & Materials Matter

The choice of geometry and material determines both visual quality and performance. Using too many polygons kills frame rate; using the wrong material creates flat, lifeless scenes. At DodaTech’s Durga Antivirus Pro, the 3D threat visualization uses custom BufferGeometry to render real-time network node connections and MeshPhysicalMaterial for realistic glass-like threat indicators that glow based on severity.

    flowchart LR
    A[Mesh] --> B[Geometry = Shape]
    A --> C[Material = Appearance]
    B --> D[Built-in: Box, Sphere, Torus...]
    B --> E[Custom: BufferGeometry]
    C --> F[BasicMaterial: Unlit, flat]
    C --> G[StandardMaterial: PBR, needs lights]
    C --> H[PhysicalMaterial: Glass, clearcoat]
    C --> I[ToonMaterial: Cartoon style]
    D --> J[Texture: Surface detail]
    E --> J
    J --> K[Geometry Showcase Project]
    style G fill:#44aa88,color:#fff,stroke:none
  
🎨
Prerequisites: Complete https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-basics/ first. You need Scene, Camera, Renderer setup knowledge before creating meshes.

Geometry = Shape (Think Clay)

A geometry defines the vertex positions — the 3D points that form a shape. Think of it like clay before it’s painted. Three.js provides many ready-to-use geometries.

Built-in Geometries Reference

// Box / Cube — width, height, depth, (optional segments)
new THREE.BoxGeometry(1, 1, 1);

// Sphere — radius, widthSegments, heightSegments
// More segments = smoother but more polygons
new THREE.SphereGeometry(1, 32, 32);

// Cylinder — radiusTop, radiusBottom, height, radialSegments
new THREE.CylinderGeometry(0.8, 0.8, 1.5, 32);

// Cone — radius, height, radialSegments
new THREE.ConeGeometry(1, 1.5, 32);

// Torus (donut) — radius, tube (thickness), radialSegments, tubularSegments
new THREE.TorusGeometry(1, 0.4, 16, 48);

// Torus Knot — like a torus but twisted
new THREE.TorusKnotGeometry(0.8, 0.3, 64, 16);

// Plane — width, height, (optional segments)
new THREE.PlaneGeometry(5, 5);

// Ring — innerRadius, outerRadius, thetaSegments
new THREE.RingGeometry(0.3, 1, 32);

// Circle — radius, segments
new THREE.CircleGeometry(1, 32);

// Tube — follows a 3D curve path
new THREE.TubeGeometry(path, tubularSegments, radius, radialSegments);

// Polyhedron variants
new THREE.DodecahedronGeometry(radius, detail);
new THREE.IcosahedronGeometry(radius, detail);
new THREE.OctahedronGeometry(radius, detail);
new THREE.TetrahedronGeometry(radius, detail);

// Text — requires a font JSON file
new THREE.TextGeometry("Hello", { font: font, size: 1, height: 0.2 });

Why segments matter: A sphere with widthSegments: 3 is a crude octahedron. With 64 it’s a smooth ball. Each segment doubles/triples the vertex count. For real-time rendering, 16-32 segments is usually enough — let normal maps fake the detail.

Custom BufferGeometry — Building Shapes from Scratch

When built-in geometries aren’t enough (e.g., data visualization, custom meshes), you create a BufferGeometry by specifying vertices manually.

var geometry = new THREE.BufferGeometry();

// Each vertex needs 3 floats: X, Y, Z
var vertices = new Float32Array([
    -1, -1,  0,    // vertex 0 (bottom-left)
     1, -1,  0,    // vertex 1 (bottom-right)
     1,  1,  0,    // vertex 2 (top-right)
    -1,  1,  0     // vertex 3 (top-left)
]);

// Indices define which vertices form triangles (3 indices per triangle)
var indices = [
    0, 1, 2,    // Triangle 1: bottom-left, bottom-right, top-right
    0, 2, 3     // Triangle 2: bottom-left, top-right, top-left
];

// UV coordinates map a 2D texture onto the surface
var uvs = new Float32Array([
    0, 0,    // vertex 0 → bottom-left of texture
    1, 0,    // vertex 1 → bottom-right of texture
    1, 1,    // vertex 2 → top-right of texture
    0, 1     // vertex 3 → top-left of texture
]);

geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(indices);
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
geometry.computeVertexNormals();  // Calculates lighting direction per vertex

Why UV coordinates? UV mapping is how a 2D texture wraps onto a 3D surface. The U and V values (0 to 1) tell Three.js which part of the texture maps to each vertex. Without UVs, textures won’t show up on custom geometry.

Material = Appearance (Think Paint)

The material determines how light interacts with the surface. Three.js offers several types, each with a different purpose.

MeshBasicMaterial — Unlit (No Lights Needed)

The simplest material. It renders a flat color or texture without any lighting calculation. Objects are always fully visible regardless of scene lights.

new THREE.MeshBasicMaterial({
    color: 0xff0000,        // Red (hex color)
    wireframe: false,        // Show triangle edges
    transparent: false,
    opacity: 1.0,
    side: THREE.FrontSide,  // Render front faces only
    map: texture             // Optional texture
});

When to use: UI elements, wireframe overlays, debug views, or any object that shouldn’t be affected by lighting.

MeshStandardMaterial — PBR (Needs Lights)

Physically-Based Rendering — simulates how real-world materials interact with light. This is your go-to material for realistic scenes. Must have lights in the scene.

new THREE.MeshStandardMaterial({
    color: 0x44aa88,
    roughness: 0.4,           // 0 = mirror smooth, 1 = completely rough
    metalness: 0.1,           // 0 = non-metal (dielectric), 1 = metal
    map: texture,              // Albedo/diffuse texture
    normalMap: normalTexture,  // Fakes surface bumps/dents
    roughnessMap: roughnessTexture,
    metalnessMap: metalnessTexture,
    aoMap: aoTexture,          // Ambient occlusion — shadows in crevices
    emissive: 0x000000,        // Self-illumination color
    emissiveIntensity: 1,
    envMap: envTexture,        // Environment map for reflections
    wireframe: false,
    transparent: false,
    opacity: 1.0,
    side: THREE.FrontSide,
    flatShading: false         // True = faceted look (low-poly style)
});

Why roughness and metalness? These two values describe almost every real-world surface:

  • Wood: roughness=0.9, metalness=0
  • Polished chrome: roughness=0.1, metalness=1
  • Plastic: roughness=0.4, metalness=0
  • Gold: roughness=0.2, metalness=1

MeshPhysicalMaterial — Extended PBR (More Realistic)

Extends MeshStandardMaterial with advanced features. More expensive but stunning.

new THREE.MeshPhysicalMaterial({
    clearcoat: 0.5,           // Clear coat layer (car paint)
    clearcoatRoughness: 0.3,
    reflectivity: 0.5,
    ior: 1.5,                 // Index of refraction (glass = 1.5)
    thickness: 0.5,           // For transmission effects
    transmission: 0.9,        // Glass-like transparency with refraction
    sheen: 0.3,              // Fabric sheen (velvet, satin)
    sheenColor: new THREE.Color(0xffffff)
});

When to use: Glass, water, car paint, gemstones, fabrics — any surface where light interacts in complex ways.

MeshToonMaterial — Cartoon / Cel-Shaded

Renders with hard color bands instead of smooth gradients — the “anime” or “comic” look.

new THREE.MeshToonMaterial({
    color: 0x44aa88,
    gradientMap: gradientTexture  // Custom ramp texture (3-5 color bands)
});

Comparison Table

MaterialNeeds LightsPerformanceUse Case
MeshBasicMaterialNoFastestUI, wireframes, debug
MeshStandardMaterialYesModerateMost 3D objects
MeshPhysicalMaterialYesSlowerGlass, water, car paint
MeshToonMaterialYesModerateCartoon/cel-shaded style
MeshMatcapMaterialNoFastFixed lighting look

Textures — Surface Detail

A texture is an image (or video) mapped onto a 3D surface. Without textures, everything looks like smooth plastic.

Loading Image Textures

var loader = new THREE.TextureLoader();

var texture = loader.load("texture.jpg");
texture.wrapS = THREE.RepeatWrapping;     // Repeat horizontally
texture.wrapT = THREE.RepeatWrapping;     // Repeat vertically
texture.repeat.set(2, 2);                 // Repeat 2x in both directions
texture.rotation = Math.PI / 4;           // Rotate 45 degrees
texture.offset.set(0.5, 0.5);            // Shift the texture
texture.anisotropy = 16;                  // Sharper at angles (0-16, higher = better but slower)

var material = new THREE.MeshStandardMaterial({ map: texture });

Why anisotropy matters: When you view a texture at a shallow angle (like a floor receding into the distance), it becomes blurry. Anisotropic filtering reduces this blur. Values up to 16 are supported but 4-8 is usually enough.

Multiple Map Types

A realistic material uses several textures working together:

var loader = new THREE.TextureLoader();

var material = new THREE.MeshStandardMaterial({
    map: loader.load("diffuse.jpg"),         // Base color (albedo)
    normalMap: loader.load("normal.jpg"),     // Surface bumps (purple-tinted)
    roughnessMap: loader.load("roughness.jpg"),  // Roughness variations
    metalnessMap: loader.load("metalness.jpg"),   // Metal areas
    aoMap: loader.load("ao.jpg"),             // Ambient occlusion
    emissiveMap: loader.load("emissive.jpg"), // Glowing areas
    alphaMap: loader.load("alpha.jpg")        // Transparency mask
});

Why normal maps? A normal map fakes surface detail (bumps, scratches, dents) without adding geometry. A brick wall texture with a normal map looks 3D but only has 2 triangles. Without it, the same wall would need millions of polygons.

Material Utility

// Clone a material (shares the same texture references)
var material2 = material.clone();

// Dispose when no longer needed — frees GPU memory
material.dispose();
geometry.dispose();
texture.dispose();

// Color manipulation
var color = new THREE.Color(0xff0000);
color.setHSL(0.5, 1, 0.5);    // HSL: hue (0-1), saturation, lightness
color.multiplyScalar(0.5);     // Darken by half
material.color = color;

Common Mistakes

1. Texture not showing (no UV coordinates)

Custom BufferGeometry doesn’t automatically have UV coordinates. Textures need UVs to map onto surfaces. Always add geometry.setAttribute("uv", ...) or use a built-in geometry.

2. MeshStandardMaterial renders black

PBR materials need lights. Without at least an AmbientLight, they render black. Add lights or use MeshBasicMaterial for testing.

3. High-poly geometry with too many segments

A sphere with 64×64 segments = 4,096 vertices. Several of these in a scene will kill performance. Use 16-32 segments and normal maps for detail.

4. Texture CORS errors with local files

Opening your HTML via file:// blocks texture loading due to browser security. Use a local server: npx serve . or Python’s python -m http.server.

5. Not disposing old materials/geometries

When replacing objects, old materials/geometries stay in GPU memory, causing memory leaks. Always call .dispose() on replaced materials and geometries.

6. Wrong texture wrapping mode

The default ClampToEdgeWrapping stretches the texture’s edge pixels. If you want a repeating pattern (like tiles), set wrapS and wrapT to RepeatWrapping.

Practice Questions

Q1: What is the difference between MeshStandardMaterial and MeshPhysicalMaterial? A: MeshPhysicalMaterial extends StandardMaterial with clearcoat, transmission (glass), sheen (fabric), and other advanced PBR features. It’s more realistic but slower.

Q2: What are UV coordinates and why do they matter? A: UV coordinates map 2D texture positions to 3D vertices. Each vertex gets a U (horizontal) and V (vertical) value from 0 to 1, defining which part of the texture appears at that vertex. Without UVs, textures don’t appear.

Q3: Why does roughness=0 and metalness=1 create a mirror? A: Roughness=0 means the surface is perfectly smooth (no light scattering). Metalness=1 means all light reflects (no diffuse color). Together, the surface acts as a perfect mirror.

Q4: What’s the performance impact of normal maps vs actual geometry? A: Normal maps are much cheaper. A flat plane with a normal map can look as detailed as a million-polygon surface. The GPU just reads the normal direction from a texture instead of interpolating between vertices.

Q5: How do you create transparent materials? A: Set transparent: true and opacity: 0.5 on the material. For proper rendering, transparent objects should be rendered last — set renderOrder if needed.

Challenge: Create a custom BufferGeometry that forms a pyramid shape (4 triangular faces + 1 square base). Apply a MeshStandardMaterial with a procedural canvas texture. Hint: a pyramid has 5 vertices and 6 triangles total.

FAQ

What is the difference between MeshStandardMaterial and MeshPhysicalMaterial?
MeshPhysicalMaterial extends StandardMaterial with clearcoat, transmission (glass-like transparency with refraction), sheen (fabric), and IOR (index of refraction). It’s more realistic but computationally more expensive.
What are normal maps?
Normal maps simulate surface detail (bumps, dents, grooves, scratches) without adding geometry. They store surface normal directions as RGB colors. The GPU reads these instead of interpolating between vertex normals.
What is UV mapping?
UV mapping assigns 2D coordinates (U = horizontal, V = vertical) to each vertex of a 3D model, defining how a 2D texture image wraps onto the 3D surface.
How do I create transparent materials?
Set transparent: true and opacity (0 to 1) on the material. For glass-like effects, use MeshPhysicalMaterial with transmission: 0.9.
What is anisotropy in texture filtering?
Anisotropic filtering reduces blur when textures are viewed at steep angles (e.g., a floor stretching into the distance). Values range 0-16; 4-8 is usually sufficient. Higher values use more GPU memory.

Try It Yourself

Copy this complete HTML file, save as geometry-showcase.html, and open in a browser.

<!DOCTYPE html>
<html>
<head>
    <title>Geometry Showcase — Three.js</title>
    <style>
        body { margin: 0; overflow: hidden; }
        #controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
            display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; z-index: 10; }
        #controls button { padding: 8px 16px; border: none; border-radius: 4px;
            background: rgba(255,255,255,0.2); color: white; cursor: pointer;
            font-size: 13px; backdrop-filter: blur(4px); }
        #controls button:hover { background: rgba(255,255,255,0.4); }
        #info { position: absolute; top: 10px; left: 10px; color: white;
            font-family: sans-serif; font-size: 14px; background: rgba(0,0,0,0.5);
            padding: 8px 12px; border-radius: 4px; z-index: 10; }
    </style>
</head>
<body>
    <div id="info">Click a button to switch geometry</div>
    <div id="controls">
        <button onclick="showGeo('box')">Box</button>
        <button onclick="showGeo('sphere')">Sphere</button>
        <button onclick="showGeo('cylinder')">Cylinder</button>
        <button onclick="showGeo('cone')">Cone</button>
        <button onclick="showGeo('torus')">Torus</button>
        <button onclick="showGeo('knot')">Torus Knot</button>
    </div>

    <script type="importmap">
    {
        "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
        }
    }
    </script>
    <script type="module">
        import * as THREE from "three";
        import { OrbitControls } from "three/addons/controls/OrbitControls.js";

        var scene = new THREE.Scene();
        scene.background = new THREE.Color(0x1a1a2e);
        var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
        camera.position.set(4, 3, 6);
        var renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        document.body.appendChild(renderer.domElement);
        new OrbitControls(camera, renderer.domElement);

        var ambient = new THREE.AmbientLight(0xffffff, 0.3);
        scene.add(ambient);
        var light = new THREE.DirectionalLight(0xffffff, 1);
        light.position.set(5, 10, 7);
        scene.add(light);
        var light2 = new THREE.DirectionalLight(0x4488ff, 0.3);
        light2.position.set(-5, 0, 5);
        scene.add(light2);

        var mesh;
        function createGeometry(type) {
            if (mesh) { scene.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); }
            var geo;
            switch (type) {
                case "box": geo = new THREE.BoxGeometry(1.5, 1.5, 1.5); break;
                case "sphere": geo = new THREE.SphereGeometry(1, 32, 32); break;
                case "cylinder": geo = new THREE.CylinderGeometry(0.8, 0.8, 1.5, 32); break;
                case "cone": geo = new THREE.ConeGeometry(1, 1.5, 32); break;
                case "torus": geo = new THREE.TorusGeometry(1, 0.4, 16, 48); break;
                case "knot": geo = new THREE.TorusKnotGeometry(0.8, 0.3, 64, 16); break;
                default: geo = new THREE.BoxGeometry(1, 1, 1);
            }
            var mat = new THREE.MeshStandardMaterial({ color: 0x44aa88, roughness: 0.3, metalness: 0.2 });
            mesh = new THREE.Mesh(geo, mat);
            scene.add(mesh);
        }
        createGeometry("torus");

        window.showGeo = function(type) {
            createGeometry(type);
            document.getElementById("info").textContent = "Geometry: " + type.charAt(0).toUpperCase() + type.slice(1);
        };

        function animate() {
            requestAnimationFrame(animate);
            if (mesh) { mesh.rotation.x += 0.005; mesh.rotation.y += 0.01; }
            renderer.render(scene, camera);
        }
        animate();

        window.addEventListener("resize", function() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    </script>
</body>
</html>

What’s Next

TopicDescriptionLink
Lights & ShadowsLight types, shadow maps, and 3-point lightinghttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-lights-shadows/
AnimationClocks, easing, TWEEN.js, and GSAPhttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-animation/
Cameras & ControlsCamera types and control schemeshttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-cameras-controls/
ParticlesParticle systems and effectshttps://tutorials.dodatech.com/frontend/libraries/threejs/threejs-particles/
WebGLThe low-level graphics APIWebGL
JavaScriptLanguage fundamentalsJavaScript
HTMLDocument structure basicsHTML

Geometries and materials are the building blocks of every Three.js scene. You now know how to choose the right shape, the right material type, and how to apply textures for realistic surfaces. The same PBR material system powers the threat indicator nodes in Durga Antivirus Pro’s 3D dashboard.

What’s Next

Congratulations on completing this Threejs Geometries Materials tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro