WebGL
January 28, 2024
15 min read
3 views

Creating Stunning 3D Experiences with Three.js and WebGL

Master the art of 3D web development. Learn how to create interactive 3D scenes, handle lighting, animate objects, write custom GLSL shaders, and optimise performance — all running natively in the browser.

Three.jsWebGL3D Web DevelopmentGLSL ShadersJavaScript 3DThree.js TutorialWebGL PerformanceReact Three Fiber3D Animation Browser
Creating Stunning 3D Experiences with Three.js and WebGL

WebGL gives JavaScript developers direct access to the GPU, enabling real-time 3D graphics in the browser without any plugins. Three.js wraps the raw WebGL API in a developer-friendly layer, so you can build complex 3D scenes with far less boilerplate.

1. Installation and Project Setup

bash
npm install three
# Optional React bindings
npm install @react-three/fiber @react-three/drei

2. Core Concepts: Scene, Camera, Renderer

javascript
import * as THREE from 'three';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

3. Geometries, Materials, and Meshes

javascript
const geometry = new THREE.BoxGeometry(1, 1, 1, 4, 4, 4);
const material = new THREE.MeshStandardMaterial({
  color: 0x6366f1,
  metalness: 0.3,
  roughness: 0.4,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

4. Lighting

javascript
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);

const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight.position.set(5, 10, 7.5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
scene.add(dirLight);

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

5. The Animation Loop

javascript
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();
  cube.rotation.x += 0.5 * delta;
  cube.rotation.y += 0.8 * delta;
  renderer.render(scene, camera);
}
animate();

6. OrbitControls — Interactive Camera

javascript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

7. Loading 3D Models (GLTF + Draco)

javascript
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');

const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('/models/robot.glb', (gltf) => {
  scene.add(gltf.scene);
});

8. Custom GLSL Shaders

javascript
const shaderMaterial = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color(0x6366f1) } },
  vertexShader: /* glsl */ `
    uniform float uTime;
    varying float vElevation;
    void main() {
      vec3 pos = position;
      pos.z = sin(pos.x * 4.0 + uTime) * 0.2 + sin(pos.y * 3.0 + uTime * 1.3) * 0.15;
      vElevation = pos.z;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    uniform vec3 uColor;
    varying float vElevation;
    void main() {
      vec3 col = mix(uColor, vec3(0.13, 0.83, 0.93), vElevation + 0.5);
      gl_FragColor = vec4(col, 1.0);
    }
  `,
});

9. Post-Processing Effects

javascript
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass }     from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/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), 1.2, 0.4, 0.85));
// Replace renderer.render() with composer.render() in the loop

10. Performance Optimisation

  • Dispose of geometries, materials, and textures you no longer use — geometry.dispose(), material.dispose(), texture.dispose() — to free GPU memory.
  • Use InstancedMesh when rendering hundreds of identical objects instead of separate Mesh instances.
  • Cap devicePixelRatio at 2 — renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)).
  • Use renderer.info to inspect draw calls, triangles, and textures in real time during development.
  • For mobile, reduce shadow map resolution and disable post-processing passes on low-end devices.

Pro tip: Use Spector.js (browser extension) or the Chrome GPU debugger to profile WebGL draw calls and identify bottlenecks before they ship to production.

Want to add immersive 3D visuals or WebGL-powered product configurators to your website? BitPixel Coders builds custom Three.js and WebGL experiences — reach out for a free consultation.

Get a Free Consultation