Three.js Sprites — Billboard Images and Labels
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
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