Skip to content

Three.js Sprites — Billboard Images and Labels

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Three.js Sprites. We cover key concepts, practical examples, and best practices to help you master this topic.

Three.js sprites are flat textures that always face the camera, acting as billboards for labels, particle effects, and 2D overlays in 3D space.

What You'll Learn

By the end of this guide, you will create sprites from textures and canvas elements, position them in 3D space, control size and opacity, use sprite sheets for animations, and build an interactive labeling system.

Why Sprites Matter

Sprites provide 2D information in 3D space. In Durga Antivirus Pro, sprites label network nodes with threat severity text that always faces the user, regardless of camera angle. Text rendered as 3D geometry would be unreadable from most angles.

flowchart LR
    A[Texture Image] --> B[SpriteMaterial]
    B --> C[Sprite]
    D[Canvas Text] --> B
    C --> E[3D Position]
    E --> F[Always Faces Camera]

Basic Sprite

var texture = new THREE.TextureLoader().load('label.png');
var material = new THREE.SpriteMaterial({ map: texture });
var sprite = new THREE.Sprite(material);
sprite.position.set(2, 3, 0);
sprite.scale.set(2, 2, 1);
scene.add(sprite);

Expected output: A texture image rendered at position (2,3,0) that always faces the camera regardless of viewing angle.

Why always-facing: Sprites automatically orient toward the camera. They are useful for labels, health bars, waypoints, and effects.

Creating Text Labels With Canvas

function makeTextSprite(text, color) {
    var canvas = document.createElement('canvas');
    canvas.width = 256;
    canvas.height = 128;
    var ctx = canvas.getContext('2d');

    ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
    ctx.roundRect(0, 0, 256, 128, 16);
    ctx.fill();

    ctx.fillStyle = color || '#ffffff';
    ctx.font = 'bold 32px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(text, 128, 64);

    var texture = new THREE.CanvasTexture(canvas);
    texture.needsUpdate = true;

    var material = new THREE.SpriteMaterial({
        map: texture,
        transparent: true,
        depthTest: false,
        depthWrite: false
    });

    return new THREE.Sprite(material);
}

var label = makeTextSprite('Server Node 1', '#ff6b35');
label.position.set(0, 2, 0);
label.scale.set(3, 1.5, 1);
scene.add(label);

Expected output: A semi-transparent rounded rectangle with text that floats above a position and always faces the camera.

Opacity and Fading

var sprite = new THREE.Sprite(
    new THREE.SpriteMaterial({
        map: texture,
        transparent: true,
        opacity: 0.8,
        blending: THREE.NormalBlending
    })
);

// Animate opacity
function animate() {
    sprite.material.opacity = 0.5 + Math.sin(Date.now() * 0.003) * 0.3;
}

Expected output: A sprite whose opacity pulses between 0.2 and 0.8 over time.

Common Mistakes

1. Sprites Not Visible

Sprites require transparent: true in the material if the texture has alpha. Without it, alpha is ignored.

2. Text Labels Too Small or Blurry

Canvas textures at lower-than-needed resolution look pixelated. Make canvas dimensions at least 256x128 for readable text.

3. Sprites Clipping Through Objects

Set depthTest: true (default) so sprites are occluded by geometry. Set depthWrite: false to prevent sprites from blocking other transparent objects.

4. Scale Not Matching Aspect Ratio

A sprite's scale affects width and height. For square textures, use equal x and y scale. For 2:1 text labels, use 2:1 scale ratio.

5. Too Many Large Sprites

Each sprite renders as a transparent quad. Many large sprites cause overdraw. Keep sprite count reasonable for mobile performance.

Practice Questions

Q1: Do sprites always face the camera? A: Yes. Sprites automatically billboard toward the camera. No manual rotation needed.

Q2: How do you create text sprites? A: Draw text on an HTML Canvas element, create a CanvasTexture from it, and use it in a SpriteMaterial.

Q3: Why do sprites need transparent: true? A: Without transparency, the sprite background (canvas default black) renders as a solid rectangle around the text.

Q4: What is the difference between depthTest and depthWrite? A: depthTest determines if the sprite is occluded by other objects. depthWrite determines if the sprite writes to the depth buffer (affecting occlusion of other transparent objects).

Q5: How do you make a sprite interactive? A: Sprites are Object3D instances. Add event listeners or use raycaster. Raycaster detects sprites as intersectable objects.

Challenge: Build a scene with 10 labeled 3D objects. Each label is a canvas-based sprite that shows the object name and a color indicator. Labels should fade when the camera moves far away.

FAQ

Are sprites affected by lights?

SpriteMaterial has no lighting calculations. Use the color property to tint the texture, but it does not receive shadows or directional light.

Can sprites use animated textures?

Yes. Update the sprite.material.map with a different texture each frame, or use a canvas texture and redraw the canvas for frame-by-frame animation.

What is the performance cost of many sprites?

Each sprite is a single quad with one draw call. Hundreds of sprites are fine. Thousands may cause overdraw if they overlap.

Can I use a sprite as a particle?

Yes. Sprite is the basis for PointsMaterial particles. Use Points with a sprite texture for efficient large particle systems.

Try It Yourself — 3D Label System

Build a page with labeled objects where each label is a canvas-based sprite. Demonstrate text labels, color coding, opacity fading with distance, and click interaction.

<!DOCTYPE html>
<html>
<head>
    <title>3D Sprite Labels</title>
    <style>
        body { margin: 0; overflow: hidden; font-family: sans-serif; }
        #info { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 10px 20px; border-radius: 8px; }
    </style>
</head>
<body>
<div id="info">Hover or click a labeled object</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(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(5, 4, 8);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);
var controls = new OrbitControls(camera, renderer.domElement);

var ambient = new THREE.AmbientLight(0x404060);
scene.add(ambient);
var dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);

function makeLabel(text, bgColor) {
    var canvas = document.createElement('canvas');
    canvas.width = 400;
    canvas.height = 120;
    var ctx = canvas.getContext('2d');

    ctx.fillStyle = bgColor || '#333366';
    ctx.beginPath();
    ctx.roundRect(10, 10, 380, 100, 16);
    ctx.fill();

    ctx.strokeStyle = 'rgba(255,255,255,0.3)';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.roundRect(10, 10, 380, 100, 16);
    ctx.stroke();

    ctx.fillStyle = '#ffffff';
    ctx.font = 'bold 32px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(text, 200, 60);

    var texture = new THREE.CanvasTexture(canvas);
    texture.needsUpdate = true;

    var material = new THREE.SpriteMaterial({
        map: texture,
        transparent: true,
        depthTest: true,
        depthWrite: false,
        sizeAttenuation: true
    });
    return new THREE.Sprite(material);
}

var objects = [];
var names = ['Firewall', 'Database', 'Web Server', 'DNS', 'Mail Server'];
var colors = [0xff6b35, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0xa29bfe];

for (var i = 0; i < 5; i++) {
    var mesh = new THREE.Mesh(
        new THREE.SphereGeometry(0.6, 32, 32),
        new THREE.MeshStandardMaterial({ color: colors[i] })
    );
    var angle = (i / 5) * Math.PI * 2;
    mesh.position.set(Math.cos(angle) * 3, 0.5, Math.sin(angle) * 3);
    mesh.name = names[i];
    scene.add(mesh);
    objects.push(mesh);

    var label = makeLabel(names[i], '#333366');
    label.position.set(mesh.position.x, mesh.position.y + 1.8, mesh.position.z);
    label.scale.set(2.5, 0.75, 1);
    scene.add(label);
    mesh.userData.label = label;
}

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var info = document.getElementById('info');

window.addEventListener('click', function(event) {
    mouse.x = (event.clientX / innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / innerHeight) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    var intersects = raycaster.intersectObjects(objects);
    if (intersects.length > 0) {
        info.textContent = 'Selected: ' + intersects[0].object.name;
    }
});

function animate() {
    objects.forEach(function(obj) {
        if (obj.userData.label) {
            var dist = camera.position.distanceTo(obj.position);
            var opacity = Math.min(1, Math.max(0.2, 1 - (dist - 2) / 8));
            obj.userData.label.material.opacity = opacity;
        }
    });
    controls.update();
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}
animate();

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

What's Next

Draw lines and polylines in Three.js for wireframes and paths.

Lines — Line geometries and materials. Bones Skinning — Skeletal animation with bones.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro