WebGL Tutorial: Building Interactive 3D Websites

WebGL is transforming the web. From our BMW M4 3D model viewer to complex e-commerce product configurators, WebGL enables truly interactive 3D experiences in the browser. This tutorial covers everything you need to build your own.

What is WebGL?

WebGL (Web Graphics Library) is a JavaScript API that renders 2D and 3D graphics in the browser using your GPU. It's based on OpenGL ES and works in all modern browsers without plugins.

Key advantages of WebGL:

Real-World WebGL Applications

WebGL isn't just for demos. It powers real business applications across industries:

1. E-Commerce Product Configurators

Companies like Nike, Tesla, and BMW use WebGL to let customers customize products in 3D. Users can change colors, materials, and options while seeing real-time updates. This increases engagement and reduces returns.

Example: Car Configurator

Just like our BMW M4 3D model, car manufacturers let buyers customize their vehicle online - choosing paint colors, wheels, interior options - all rendered in real-time 3D.

2. Real Estate Virtual Tours

Real estate companies use WebGL for virtual property tours. Buyers can walk through homes, look around rooms, and get a sense of space without visiting in person. This saves time and reaches remote buyers.

3. Data Visualization

Complex data becomes understandable in 3D. Financial dashboards, scientific visualizations, and geographic mapping all benefit from WebGL's ability to render millions of data points smoothly.

4. Gaming and Entertainment

Browser games have evolved far beyond Flash. WebGL powers sophisticated 3D games that rival native applications, all running in a browser tab.

5. Education and Training

Medical schools use 3D anatomy models. Engineering programs use interactive simulations. Museums offer virtual exhibits. WebGL makes learning interactive and engaging.

Getting Started with Three.js

Raw WebGL is complex. Three.js is a JavaScript library that makes WebGL development accessible. It's what we used to build the BMW M4 3D model viewer.

Basic Three.js Setup

// Import Three.js
import * as THREE from 'three';

// Create scene - the container for everything
const scene = new THREE.Scene();

// Create camera - your viewpoint into the scene
const camera = new THREE.PerspectiveCamera(
  75,                           // Field of view
  window.innerWidth / window.innerHeight,  // Aspect ratio
  0.1,                          // Near clipping plane
  1000                          // Far clipping plane
);

// Create renderer - draws everything to the screen
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Position camera
camera.position.z = 5;

// Animation loop
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

This creates an empty 3D scene. Now let's add something to see:

Adding 3D Objects

// Create a cube
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// Rotate it in the animation loop
function animate() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
}
animate();

Adding Lighting

For realistic materials, you need lights:

// Ambient light - soft overall illumination
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// Directional light - like the sun
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);

// Now use a material that responds to light
const material = new THREE.MeshStandardMaterial({ 
  color: 0x0077ff,
  metalness: 0.5,
  roughness: 0.5
});

Complete Guide to Three.js Lighting

Lighting is what makes 3D scenes come alive. Poor lighting = flat, boring visuals. Great lighting = dramatic, realistic renders. Here's everything you need to know:

1. AmbientLight - Global Fill

AmbientLight illuminates all objects equally from all directions. It has no position or direction - it's everywhere.

const ambientLight = new THREE.AmbientLight(color, intensity);

// Parameters:
// color: Hex color (0xffffff = white)
// intensity: Brightness (0.0 to 1.0+ typical)

When to Use Ambient Light

  • Use it: To fill in dark shadows and simulate indirect light bouncing
  • Don't overuse: Too much ambient light makes scenes look flat and washed out
  • Typical values: 0.1 to 0.5 intensity
Intensity Effect Use Case
0.0 - 0.2 Very dark, dramatic Horror, night scenes
0.3 - 0.5 Balanced, natural Product viewers, games
0.6 - 1.0 Bright, soft shadows Bright outdoor, cartoon

2. SpotLight - Focused Beam

SpotLight emits light in a cone shape from a single point. Perfect for dramatic effects, car showrooms, and stage lighting.

const spotLight = new THREE.SpotLight(color, intensity, distance, angle, penumbra, decay);

// Parameters:
// color: Light color (0xffffff = white)
// intensity: Brightness (0 to 10000+ for physically correct)
// distance: Max range (0 = infinite)
// angle: Cone angle in radians (0 to Math.PI/2)
// penumbra: Edge softness (0 = hard, 1 = soft)
// decay: How fast light dims (2 = realistic)

spotLight.position.set(x, y, z);  // Where the light is
spotLight.target.position.set(x, y, z);  // Where it points

Our BMW M4 3D Model SpotLight Settings

Here are the exact parameters we use for the car showcase:

const spotLight = new THREE.SpotLight(
  0xffffff,  // White light
  9000,      // High intensity for showroom effect
  100,       // 100 unit range
  0.22,      // ~12.6° cone angle
  1          // Soft edges (full penumbra)
);
spotLight.position.set(0, 25, 0);  // 25 units above center
spotLight.castShadow = true;

SpotLight Angle Explained

The angle parameter controls how wide the light cone spreads:

Angle (radians) Degrees Effect
0.05 ~3° Very tight laser beam
0.15 ~8.5° Focused spotlight
0.22 ~12.6° Our M4 setting - showroom feel
0.5 ~28.6° Wide flood light
1.0 ~57° Very wide coverage

SpotLight Intensity for Physically Correct Rendering

Three.js uses physically correct lighting by default. Intensity is measured in candela (cd), so values are much higher than old-style lighting:

Intensity Real-World Equivalent
100 - 500 Candle, dim lamp
1000 - 3000 Room light, desk lamp
5000 - 10000 Stage light, showroom
9000 Our M4 setting
50000+ Bright sun, stadium

3. DirectionalLight - Sun/Moon

DirectionalLight simulates distant light sources like the sun. All rays are parallel - position only affects shadow direction, not intensity.

const directionalLight = new THREE.DirectionalLight(color, intensity);
directionalLight.position.set(5, 10, 5);  // Direction it comes FROM
directionalLight.castShadow = true;

// Shadow camera setup for directional light
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;

4. PointLight - Lightbulb

PointLight emits light in all directions from a single point, like a bare lightbulb.

const pointLight = new THREE.PointLight(color, intensity, distance, decay);
pointLight.position.set(0, 5, 0);

5. HemisphereLight - Sky + Ground

HemisphereLight simulates outdoor lighting with different colors from sky (above) and ground (below).

const hemiLight = new THREE.HemisphereLight(
  0x87ceeb,  // Sky color (light blue)
  0x3d2817,  // Ground color (brown)
  0.6        // Intensity
);

Recommended Lighting Setups

🚗 Car Showroom (What We Use)

// Single dramatic spotlight from above
const spotLight = new THREE.SpotLight(0xffffff, 9000, 100, 0.22, 1);
spotLight.position.set(0, 25, 0);
spotLight.castShadow = true;
scene.add(spotLight);

// NO ambient light = dramatic shadows
// The spotlight creates the classic "car reveal" look

🏠 Product Viewer (Softer)

// Ambient for base illumination
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);

// Key light
const keyLight = new THREE.DirectionalLight(0xffffff, 1);
keyLight.position.set(5, 5, 5);
scene.add(keyLight);

// Fill light (dimmer, opposite side)
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
fillLight.position.set(-5, 3, -5);
scene.add(fillLight);

🌳 Outdoor Scene

// Hemisphere for natural sky/ground bounce
const hemi = new THREE.HemisphereLight(0x87ceeb, 0x3d2817, 0.6);
scene.add(hemi);

// Sun
const sun = new THREE.DirectionalLight(0xfff5e6, 1.5);
sun.position.set(50, 100, 50);
sun.castShadow = true;
scene.add(sun);

Shadow Settings

Shadows add realism but cost performance. Here's how to configure them:

// Enable shadows on renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;  // Soft shadows

// Shadow map types (performance vs quality):
// THREE.BasicShadowMap      - Fast, hard edges
// THREE.PCFShadowMap        - Softer, medium cost
// THREE.PCFSoftShadowMap    - Smoothest, higher cost
// THREE.VSMShadowMap        - Best quality, highest cost

// Light shadow settings
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;   // Higher = sharper (512, 1024, 2048)
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.bias = -0.0001;          // Prevents shadow acne

// Objects must opt-in to shadows
mesh.castShadow = true;     // Object creates shadows
mesh.receiveShadow = true;  // Object shows shadows on it

Loading 3D Models

Real applications use detailed 3D models, not basic shapes. Here's how to load a GLTF model (the format we use for our BMW M4 3D model):

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();

loader.load(
  'model.gltf',           // Path to model
  function (gltf) {        // Success callback
    scene.add(gltf.scene);
    console.log('Model loaded!');
  },
  function (xhr) {         // Progress callback
    console.log((xhr.loaded / xhr.total * 100) + '% loaded');
  },
  function (error) {       // Error callback
    console.error('Error loading model:', error);
  }
);

Where to Get 3D Models

Making It Interactive

Static 3D isn't enough. Users expect to interact with 3D content. Three.js provides OrbitControls for camera interaction:

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Create controls
const controls = new OrbitControls(camera, renderer.domElement);

// Configure controls
controls.enableDamping = true;    // Smooth movement
controls.dampingFactor = 0.05;
controls.enableZoom = true;       // Allow zoom
controls.enablePan = false;       // Disable panning
controls.minDistance = 2;         // Minimum zoom
controls.maxDistance = 10;        // Maximum zoom
controls.autoRotate = true;       // Auto-rotate when idle
controls.autoRotateSpeed = 1.0;

// Update in animation loop
function animate() {
  requestAnimationFrame(animate);
  controls.update();              // Required for damping
  renderer.render(scene, camera);
}
animate();

This is exactly how our BMW M4 3D model viewer works - you can rotate, zoom, and explore the car from any angle.

Click/Touch Interaction with Raycasting

Want users to click on 3D objects? Use raycasting:

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function onMouseClick(event) {
  // Convert mouse position to normalized coordinates
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

  // Cast ray from camera through mouse position
  raycaster.setFromCamera(mouse, camera);

  // Check for intersections
  const intersects = raycaster.intersectObjects(scene.children, true);

  if (intersects.length > 0) {
    const clickedObject = intersects[0].object;
    console.log('Clicked:', clickedObject.name);
    // Do something with the clicked object
    clickedObject.material.color.set(0xff0000);
  }
}

window.addEventListener('click', onMouseClick);

Performance Optimization

3D is resource-intensive. Here's how to keep it smooth:

1. Limit Pixel Ratio

// Cap at 2x for performance on high-DPI displays
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

2. Use Simpler Shadows

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.BasicShadowMap; // Faster than PCFSoftShadowMap

3. Reduce Geometry Complexity

// Use fewer segments for simple shapes
const sphere = new THREE.SphereGeometry(1, 16, 16); // Not 64, 64

4. Dispose Unused Resources

// When removing objects, clean up memory
geometry.dispose();
material.dispose();
texture.dispose();

5. Use Level of Detail (LOD)

const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0);      // Close up
lod.addLevel(mediumDetailMesh, 50);   // Medium distance
lod.addLevel(lowDetailMesh, 100);     // Far away
scene.add(lod);

Building a Complete 3D Website

Here's the architecture for a full 3D website like our BMW M4 3D model viewer:

File Structure

project/
├── index.html          # Main page
├── style.css           # Styles
├── main.js             # Three.js code
├── models/
│   └── car.gltf        # 3D model
├── textures/
│   └── metal.jpg       # Textures
└── lib/
    └── three.module.js # Three.js library

HTML Structure

<!DOCTYPE html>
<html>
<head>
  <title>3D Product Viewer</title>
  <style>
    body { margin: 0; overflow: hidden; }
    canvas { display: block; }
    #loading { position: fixed; /* loading overlay */ }
    #ui { position: fixed; /* UI controls */ }
  </style>
</head>
<body>
  <div id="loading">Loading...</div>
  <div id="ui">
    <button onclick="changeColor('red')">Red</button>
    <button onclick="changeColor('blue')">Blue</button>
  </div>
  <script type="module" src="main.js"></script>
</body>
</html>

Advanced Features

Environment Maps (Reflections)

import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

new RGBELoader().load('environment.hdr', function(texture) {
  texture.mapping = THREE.EquirectangularReflectionMapping;
  scene.environment = texture;
  scene.background = texture;
});

Post-Processing Effects

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  0.5,  // Strength
  0.4,  // Radius
  0.85  // Threshold
));

// Use composer.render() instead of renderer.render()

Resources for Learning More

Conclusion

WebGL and Three.js open up incredible possibilities for web development. From simple product viewers like our BMW M4 3D model to complex virtual worlds, 3D on the web is becoming mainstream.

Start with the basics - a simple scene, a loaded model, orbit controls. Then gradually add features like lighting, shadows, and interactions. Before you know it, you'll be building immersive 3D experiences.

The code behind our BMW M4 3D model viewer uses everything covered in this tutorial. Try it yourself and see WebGL in action.