Skip to content

Creative Coding

OGL — When Three.js Is Overkill and You Need 5kb of WebGL

Three.js is the industry standard for 3D on the web — and it's 150kb. OGL is a surgical strike: 5kb of WebGL that skips the game engine overhead and gives you raw shader control. Use it for animated backgrounds, particle systems, post-processing effects, and GPU-bound creative code. Real code examples. When OGL beats Three.js. And how to pair it with Shader Park for maximum visual impact on marketing sites.

🎨

Three.js is powerful. It's also bloated. A full scene graph, physics engine abstractions, built-in geometries, material system, and enough feature flags to choke a design document. For a game or a product viewer, that's worth it. For a shader-driven background or a particle system? You're paying for features you'll never use.

Enter OGL. It's not a framework. It's a thin WebGL abstraction that lets you write GLSL shaders and bind geometry without the Three.js tax. Five kilobytes. Gzipped. No scene graph, no physics, no abstraction layers. Just your vertex shader, your fragment shader, your uniforms, and your GPU. If you've been living under a rock, Three.js feels essential for any 3D. It's not. OGL proves it.

OGL vs. Three.js: Bundle Size Tells the Story

Bundle size isn't everything. But when you're shipping to a 4G phone on a 3000ms page load budget, it matters.

  • Three.js: ~150kb gzipped. Scene graph, built-in geometries, lights, shadows, postprocessing, animation loops.
  • React Three Fiber: +30kb. Renderer for Three.js. Worth it if you're already heavy on 3D.
  • OGL: ~5kb gzipped. WebGL bindings. Program, Texture, Geometry, Mesh. That's it.

If your use case is "shader background that responds to mouse" or "particle system on scroll", you don't need Three.js's scene graph. You need GLSL and buffer binding. OGL gives you exactly that.

When to Reach for OGL

Three rules:

  1. Pure shader work. If your effect is mostly GLSL (vertex + fragment), and you're not juggling 50 objects, OGL wins.
  2. Particle systems. Thousands of points, GPU compute, instanced rendering. OGL's Geometry and buffer bindings are perfect.
  3. Post-processing. Blur, chromatic aberration, color grading. OGL's Texture and RenderTarget are lightweight.

Reach for Three.js when you need: complex lighting, shadows, multiple materials on multiple objects, or a polished interactive scene. Reach for OGL when your effect fits in a shader and a buffer.

The Minimal Setup: Triangle in WebGL, the OGL Way

Here's the baseline. A single triangle, no dependencies besides OGL.

import { Renderer, Camera, Transform, Program, Geometry, Mesh } from 'ogl';

// Create a WebGL context
const renderer = new Renderer({
  width: window.innerWidth,
  height: window.innerHeight
});
document.body.appendChild(renderer.gl.canvas);

// Camera setup
const camera = new Camera(renderer.gl, { fov: 45 });
camera.position.z = 5;

// Define vertex shader (transforms geometry in 3D space)
const vertex = `
  attribute vec3 position;
  uniform mat4 uMatrix;

  void main() {
    gl_Position = uMatrix * vec4(position, 1.0);
  }
`;

// Define fragment shader (colors each pixel)
const fragment = `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.5, 1.0); // Magenta
  }
`;

// Compile shader program
const program = new Program(renderer.gl, { vertex, fragment });

// Define triangle geometry (3 vertices, 1 triangle)
const geometry = new Geometry(renderer.gl, {
  position: { size: 3, data: new Float32Array([
    0.0,  0.5,  0.0,  // top
   -0.5, -0.5,  0.0,  // bottom-left
    0.5, -0.5,  0.0,  // bottom-right
  ]) }
});

// Create mesh (geometry + material)
const mesh = new Mesh(renderer.gl, { geometry, program });

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

That's a complete WebGL triangle. No scene graph, no Three.js overhead. 40 lines of code, all transparent. You see the vertex shader. You see the fragment shader. You see the buffer data. Nothing hidden.

Three Patterns: Animated Backgrounds, Particles, Post-FX

Pattern 1: Shader-Driven Animated Background

This is the bread and butter for Aidxn: full-screen shader that evolves over time. Use Shader Park or raw GLSL.

import { Renderer, Camera, Transform, Program, Geometry, Mesh } from 'ogl';

const renderer = new Renderer({
  dpr: Math.min(window.devicePixelRatio, 1.5),
  width: window.innerWidth,
  height: window.innerHeight
});
document.body.appendChild(renderer.gl.canvas);

const camera = new Camera(renderer.gl);
camera.position.z = 0;

let time = 0;

const vertex = `
  attribute vec2 uv;
  attribute vec3 position;

  varying vec2 vUv;

  void main() {
    vUv = uv;
    gl_Position = vec4(position, 1.0);
  }
`;

const fragment = `
  precision highp float;

  varying vec2 vUv;
  uniform float uTime;
  uniform vec2 uMouse;

  void main() {
    vec2 st = vUv;

    // Sine wave distortion
    float wave = sin(st.x * 10.0 + uTime * 0.5) * 0.1;
    float noise = sin(st.y * 15.0 + uTime * 0.3 + wave) * 0.5 + 0.5;

    // Mouse pulls the pattern
    vec2 mouseEffect = normalize(uMouse - st) * 0.3;
    float pattern = sin(length(st + mouseEffect) * 5.0 + uTime) * 0.5 + 0.5;

    vec3 color = vec3(
      noise * 0.6 + 0.2,
      pattern * 0.5 + 0.3,
      sin(uTime * 0.2) * 0.5 + 0.5
    );

    gl_FragColor = vec4(color, 1.0);
  }
`;

const program = new Program(renderer.gl, {
  vertex,
  fragment,
  uniforms: {
    uTime: { value: 0 },
    uMouse: { value: [0, 0] }
  }
});

// Full-screen quad
const geometry = new Geometry(renderer.gl, {
  position: { size: 3, data: new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]) },
  uv: { size: 2, data: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]) },
  index: { data: new Uint32Array([0, 1, 2, 0, 2, 3]) }
});

const mesh = new Mesh(renderer.gl, { geometry, program });

// Mouse tracking
window.addEventListener('mousemove', (e) => {
  const x = e.clientX / window.innerWidth;
  const y = 1 - e.clientY / window.innerHeight;
  program.uniforms.uMouse.value = [x, y];
});

function animate() {
  time += 0.016; // ~60fps
  program.uniforms.uTime.value = time;
  renderer.render({ scene: mesh, camera });
  requestAnimationFrame(animate);
}
animate();

That's your animated background. No Three.js, no scene graph, no built-in geometries. Just a full-screen quad, two shaders, and a time uniform. Port it to a Shader Park preset and you get visual editing UI for free.

Pattern 2: Particle System (Instanced Rendering)

Thousands of points, all from one draw call. OGL's instancing is clean.

// Generate 10,000 particle positions
const count = 10000;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
  positions[i] = (Math.random() - 0.5) * 10;     // x
  positions[i + 1] = (Math.random() - 0.5) * 10; // y
  positions[i + 2] = (Math.random() - 0.5) * 10; // z
}

const vertex = `
  attribute vec3 position;
  attribute vec3 instancePosition;

  uniform mat4 uMatrix;

  void main() {
    gl_Position = uMatrix * vec4(position + instancePosition, 1.0);
    gl_PointSize = 2.0;
  }
`;

const fragment = `
  void main() {
    gl_FragColor = vec4(1.0, 0.5, 0.2, 0.8);
  }
`;

const program = new Program(renderer.gl, { vertex, fragment });

const geometry = new Geometry(renderer.gl, {
  position: { size: 3, data: new Float32Array([0, 0, 0]) }, // single point
  instancePosition: { size: 3, data: positions, instanced: 1 }
});

const mesh = new Mesh(renderer.gl, {
  geometry,
  program,
  mode: renderer.gl.POINTS
});

// Render 10,000 particles in one draw call
function animate() {
  renderer.render({ scene: mesh, camera });
  requestAnimationFrame(animate);
}
animate();

Ten thousand particles. One draw call. Three.js would need a BufferGeometry + Points material, or you'd loop through individual meshes and tank performance. OGL's instancing is native WebGL, zero abstraction.

Pattern 3: Post-Processing (Chromatic Aberration)

Render the scene to a texture, then apply a post-effect.

import { Renderer, RenderTarget, Texture, Program, Mesh, Geometry } from 'ogl';

// Create an off-screen render target
const renderTarget = new RenderTarget(renderer.gl, {
  width: window.innerWidth,
  height: window.innerHeight
});

const postVertex = `
  attribute vec2 uv;
  attribute vec3 position;
  varying vec2 vUv;

  void main() {
    vUv = uv;
    gl_Position = vec4(position, 1.0);
  }
`;

const postFragment = `
  precision highp float;

  varying vec2 vUv;
  uniform sampler2D uTexture;
  uniform float uStrength;

  void main() {
    // Chromatic aberration: separate RGB channels
    float offset = 0.01 * uStrength;

    float r = texture2D(uTexture, vUv + vec2(offset, 0.0)).r;
    float g = texture2D(uTexture, vUv).g;
    float b = texture2D(uTexture, vUv - vec2(offset, 0.0)).b;

    gl_FragColor = vec4(r, g, b, 1.0);
  }
`;

const postProgram = new Program(renderer.gl, {
  vertex: postVertex,
  fragment: postFragment,
  uniforms: {
    uTexture: { value: renderTarget.texture },
    uStrength: { value: 2.0 }
  }
});

const postGeometry = new Geometry(renderer.gl, {
  position: { size: 3, data: new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]) },
  uv: { size: 2, data: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]) }
});

const postMesh = new Mesh(renderer.gl, { geometry: postGeometry, program: postProgram });

function animate() {
  // Render main scene to texture
  renderer.render({ scene: yourMesh, camera, target: renderTarget });

  // Apply post-effect quad
  renderer.render({ scene: postMesh, camera });

  requestAnimationFrame(animate);
}
animate();

Chromatic aberration, bloom, blur, color grading — all possible with OGL's RenderTarget + Texture API. Three.js has EffectComposer, which is polished but heavy. OGL lets you build your own effects pipeline at 5% of the bundle cost.

Bundle Math: Why 5kb > 150kb

Assume a marketing site with one hero animation. Three.js: 150kb. OGL: 5kb. That's 145kb saved. On a 3G connection (1.6 Mbps), that's an extra 0.7 seconds of load time, gone. On mobile, it's the difference between animation loading before the user scrolls and loading after. And if you're shipping to 20 markets, and 40% of traffic is 3G or worse (spoiler: it is), that 145kb compounds.

OGL doesn't have built-in primitives or a full material system. You trade polish for weight. For shader work, it's the right trade.

Pairing OGL with Shader Park

Shader Park is a visual shader editor that compiles to GLSL. Instead of hand-writing fragment shaders, you build a node graph, tweak parameters in real time, and export GLSL. Pair it with OGL and you get:

  • Visual shader authoring (Shader Park UI).
  • Real-time preview (running on OGL in your browser).
  • Copy-paste GLSL into your codebase (5kb runtime overhead).
  • Design-engineer iteration loop (stakeholders watch, no waiting for builds).

This is how Aidxn ships animated backgrounds across Velocity clients. Design in Shader Park, export, mount on OGL, game over.

Mounting OGL in React

OGL is a vanilla WebGL library, but mounting in React is straightforward.

import { useEffect, useRef } from 'react';
import { Renderer, Program, Mesh, Geometry } from 'ogl';

export function OGLBackground() {
  const canvasRef = useRef(null);

  useEffect(() => {
    if (!canvasRef.current) return;

    const renderer = new Renderer({
      canvas: canvasRef.current,
      width: window.innerWidth,
      height: window.innerHeight
    });

    // Your OGL setup here
    let time = 0;

    function animate() {
      time += 0.016;
      renderer.render({ scene: mesh, camera });
      requestAnimationFrame(animate);
    }
    animate();

    // Cleanup
    return () => {
      renderer.gl.getExtension('WEBGL_lose_context')?.loseContext();
    };
  }, []);

  return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />;
}

Pass the canvas to OGL's Renderer, run setup in a useEffect, cleanup on unmount. OGL handles the render loop. You handle React's lifecycle.

Six FAQs

Does OGL work on mobile?

Yes. OGL is WebGL, which all modern mobiles support. Cap device pixel ratio (like we do with R3F) and test on real hardware. A simple shader runs smooth on iPhone 12 and newer.

Can I combine OGL and Three.js in the same scene?

No. They're separate WebGL contexts. But you can render OGL output as a texture and pass it to Three.js, or vice versa. Rare pattern.

Is OGL good for interactive 3D models?

No. If you need click detection, raycasting, or complex model interactions, use Three.js. OGL is for fire-and-forget effects.

What if I need shadows and advanced lighting?

Hand-write shadow mapping and lighting in GLSL, or use Three.js. OGL assumes you're comfortable with shader math.

Can I use OGL with Astro islands?

Yes. Lazy-load it as a React island. The same React pattern applies. OGL doesn't care where it mounts.

What's the learning curve?

If you know WebGL basics and can read GLSL, OGL is trivial. If you're WebGL-first-timer, spend a week on learnopengl.com, then OGL feels like a breeze.

The Bottom Line

Three.js is a full-featured 3D engine. For most use cases, it's worth the 150kb. But if your effect is "animated background" or "particle system", you're paying for a game engine you won't use. OGL is the surgical alternative: 5kb, raw GLSL, zero abstractions. Pair it with Shader Park for visual authoring and you get the best of both worlds — designer-friendly tools and production-lean code.

Start with a simple full-screen shader. Add mouse interaction. Add time-based animation. Watch how fast your iteration loop becomes when you're writing GLSL directly instead of fighting a scene graph. For marketing sites that prioritise loading speed and shader-driven aesthetics, OGL is the obvious choice.

For deeper 3D work, dive into React Three Fiber for complex hero scenes. Or check Velocity X templates which layer OGL backgrounds, R3F objects, and Shader Park presets for maximum visual impact. Choose your tool. Build fast.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.