Type-safe WebGL Framebuffer Object wrapper for off-screen rendering.
+----------------------------------------------------------+
| Off-screen Render Pass |
| |
| +---------------------+ +---------------------+ |
| | FBO (512 x 512) |---►| Screen Framebuffer | |
| | | | | |
| | colour texture ----+---►| post-process quad | |
| | depth buffer ----+ | (fbo.texture) | |
| +---------------------+ +---------------------+ |
| |
| fbo.bind() -> draw scene -> fbo.unbind() |
+----------------------------------------------------------+
A real interactive demo will be added in a future release.
- About
- Features
- Tech Stack
- Getting Started
- API Reference
- Examples
- Configuration
- Development
- Testing
- Docker
- Contributing
- License
WebGraphicLibrary FBO is a lightweight, zero-dependency TypeScript library that wraps the verbose WebGL framebuffer API into a clean, ergonomic interface.
A Framebuffer Object (FBO) redirects GPU draw calls away from the screen and into a texture. That texture can then be sampled by another shader, post-processed, or read back to the CPU — forming the backbone of techniques like:
- Shadow mapping
- Reflections and refractions
- Screen-space ambient occlusion (SSAO)
- Bloom, blur, and other post-processing effects
- Deferred rendering
- Screen capture / export to image
The library handles the full object lifecycle — creation, completeness validation, bind/unbind state, optional depth attachment, resize, pixel readback, and safe disposal — with full TypeScript types throughout.
- Zero runtime dependencies — fully self-contained
- TypeScript-first — ships
.d.tsdeclarations for every export - WebGL and WebGL2 supported via one unified API
- Optional depth / depth+stencil renderbuffer attachment
resize()— updates texture storage in-place, stable framebuffer referencereadPixels()— convenience method to pull rendered data back to the CPU- Framebuffer completeness validation — descriptive error if the driver reports incomplete status
- Disposal guard — prevents use-after-free and double-dispose
- Debug logging — opt-in via
FBO_DEBUGenvironment variable; zero cost in production - Dual ESM + CJS output — works with every modern bundler and Node.js
| Tool | Version | Purpose |
|---|---|---|
| TypeScript | 5.x | Type-safe source language |
| Rollup | 4.x | Library bundler (ESM + CJS output) |
| Vitest | 3.x | Unit testing framework |
| ESLint | 9.x | Static analysis (flat config) |
| Prettier | 3.x | Opinionated code formatting |
| GitHub Actions | — | CI/CD pipeline |
- Node.js >= 18
- A project that targets a WebGL-capable browser (the
FBOclass is a browser runtime library)
npm install @ahmerhh/webgraphiclibrary-fixedbaseoperator# yarn
yarn add @ahmerhh/webgraphiclibrary-fixedbaseoperator
# pnpm
pnpm add @ahmerhh/webgraphiclibrary-fixedbaseoperatorimport { FBO } from '@ahmerhh/webgraphiclibrary-fixedbaseoperator';
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const gl = canvas.getContext('webgl')!;
// Create a 512 x 512 off-screen render target
const fbo = new FBO(gl, 512, 512);
// Render into the FBO
fbo.bind();
gl.clear(gl.COLOR_BUFFER_BIT);
// ...your draw calls...
fbo.unbind();
// fbo.texture is now ready to be sampled in a shader
// Free GPU resources when done
fbo.dispose();new FBO(gl, width, height, options?)| Parameter | Type | Required | Description |
|---|---|---|---|
gl |
WebGLRenderingContext | WebGL2RenderingContext |
Yes | Active WebGL context |
width |
number |
Yes | Framebuffer width in pixels (positive integer, max 8192) |
height |
number |
Yes | Framebuffer height in pixels (positive integer, max 8192) |
options |
Partial<FBOOptions> |
No | Texture format and attachment configuration |
Throws:
| Error type | Condition |
|---|---|
TypeError |
gl is not a valid WebGL context |
TypeError |
width or height is not a positive integer |
RangeError |
Either dimension exceeds 8192 |
Error |
WebGL driver fails to allocate framebuffer, texture, or renderbuffer |
Error |
gl.checkFramebufferStatus returns a non-complete status |
| Property | Type | Description |
|---|---|---|
gl |
GLContext |
The WebGL context provided at construction time |
width |
number |
Current framebuffer width — updated by resize() |
height |
number |
Current framebuffer height — updated by resize() |
framebuffer |
WebGLFramebuffer |
The underlying WebGL framebuffer object |
texture |
WebGLTexture |
The colour attachment — bind this in a shader |
renderbuffer |
WebGLRenderbuffer | null |
Depth/stencil renderbuffer (null if not requested) |
disposed |
boolean |
true after dispose() has been called |
Makes this FBO the active render target. All subsequent draw calls write into fbo.texture.
fbo.bind();
gl.drawArrays(gl.TRIANGLES, 0, 6);
fbo.unbind();Throws Error if called on a disposed FBO.
Restores the default (screen) framebuffer. Equivalent to gl.bindFramebuffer(gl.FRAMEBUFFER, null).
Re-allocates the texture (and depth renderbuffer if present) at new dimensions. The fbo.framebuffer reference remains stable — no need to rebind uniforms.
window.addEventListener('resize', () => {
fbo.resize(canvas.width, canvas.height);
});Reads the current pixel data from the colour attachment into a Uint8Array of length width * height * 4 (RGBA). Requires the default RGBA / UNSIGNED_BYTE format.
const pixels = fbo.readPixels();
const [r, g, b, a] = pixels; // bottom-left pixelDeletes all GPU objects (framebuffer, texture, renderbuffer). Safe to call multiple times — subsequent calls are no-ops. The disposed getter returns true afterwards.
interface FBOOptions {
internalFormat?: number; // default: gl.RGBA
format?: number; // default: gl.RGBA
type?: number; // default: gl.UNSIGNED_BYTE
minFilter?: number; // default: gl.LINEAR
magFilter?: number; // default: gl.LINEAR
wrapS?: number; // default: gl.CLAMP_TO_EDGE
wrapT?: number; // default: gl.CLAMP_TO_EDGE
depth?: boolean; // default: false — attach a DEPTH_COMPONENT16 renderbuffer
stencil?: boolean; // default: false — attach a combined DEPTH_STENCIL renderbuffer
}import { FBO } from '@ahmerhh/webgraphiclibrary-fixedbaseoperator';
const fbo = new FBO(gl, 512, 512);
function renderFrame(): void {
// Off-screen pass
fbo.bind();
gl.viewport(0, 0, fbo.width, fbo.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
renderScene();
fbo.unbind();
// Screen pass — use fbo.texture as a shader input
gl.viewport(0, 0, canvas.width, canvas.height);
gl.bindTexture(gl.TEXTURE_2D, fbo.texture);
renderFullscreenQuad();
requestAnimationFrame(renderFrame);
}const fbo = new FBO(gl, 1024, 1024, { depth: true });const fbo = new FBO(gl, canvas.width, canvas.height);
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
fbo.resize(canvas.width, canvas.height);
});// Useful for thumbnails, pixel-picking, or image export
fbo.bind();
renderScene();
fbo.unbind();
const pixels = fbo.readPixels(); // Uint8Array, RGBAconst gl2 = canvas.getContext('webgl2')!;
const hdrFbo = new FBO(gl2, 512, 512, {
internalFormat: gl2.RGBA16F,
format: gl2.RGBA,
type: gl2.FLOAT,
});Copy .env.example to .env and adjust as needed:
cp .env.example .env| Variable | Default | Description |
|---|---|---|
FBO_DEBUG |
(unset) | Set to any truthy string to enable library debug logging |
NODE_ENV |
development |
Affects test and build behaviour |
In browser contexts, set window.__FBO_DEBUG__ = true before importing the library.
# Install dependencies
npm install
# Start Rollup watcher
npm run dev
# Lint
npm run lint
npm run lint:fix
# Format
npm run format:check
npm run format
# Type-check (no emit)
npm run typecheck
# Full pre-publish check (lint + typecheck + test + build)
npm run prepublishOnly# Run tests once
npm test
# Watch mode
npm run test:watch
# With coverage report
npm run test:coverageCoverage thresholds enforced in CI:
| Metric | Threshold |
|---|---|
| Lines | 80% |
| Functions | 80% |
| Branches | 75% |
| Statements | 80% |
# Run the full test suite
docker compose run test
# Build the dist/ artefacts (exported to ./dist on the host)
docker compose run build
# Start Rollup in watch mode with sources mounted
docker compose run devContributions are welcome!
- Fork this repository
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes and add tests
- Verify everything passes:
npm run prepublishOnly - Open a pull request with a clear description
Please follow the existing code style (enforced by ESLint + Prettier) and add tests for any new behaviour.
MIT — see LICENSE.md for full details.
Made with care by ahmerhh