@babylonjs/lite-gl
A tiny, function-based, tree-shakeable WebGL2 micro-engine for fullscreen
shader effects, sprites and dynamic textures — the WebGL counterpart of
@babylonjs/lite. No classes, no
scene graph: you call plain functions against an opaque GLEngineContext, so a
bundler keeps only what you import.
It is a focused subset of Babylon.js' rendering primitives, validated to render
near-identically (within ±1–2 LSB ANGLE / SwiftShader codegen noise) to
Babylon's ThinEngine / EffectRenderer / SpriteRenderer / HtmlElementTexture
path — every feature has a side-by-side parity scene in tests/gl/parity/ (and,
downstream, the NeonBrush effect suite). Swapping @babylonjs/core for lite-gl
typically shrinks an effect's shipped bundle ~10–16× (≈4–6 KB gzip vs
≈40–80 KB).
WebGL2 only. The context is created with
canvas.getContext("webgl2").
Install
npm install @babylonjs/lite-gl
Quick start — an animated fullscreen effect
import { createGLEngine, createEffectWrapper, applyEffectWrapper, drawEffect, setViewport, setEffectFloat, runRenderLoop, resizeGLEngine } from "@babylonjs/lite-gl";
const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
const engine = createGLEngine(canvas, { alpha: false });
// The wrapper compiles + owns the effect. `vertexSource` defaults to a built-in
// fullscreen-quad shader (exposing a `vUv` varying), so only `fragmentSource`
// is required.
const wrapper = createEffectWrapper(engine, {
name: "gradient",
fragmentSource: `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 glFragColor;
uniform float uTime;
void main() {
glFragColor = vec4(0.5 + 0.5 * cos(uTime + vUv.xyx + vec3(0.0, 2.0, 4.0)), 1.0);
}`,
uniformNames: ["uTime"],
});
const start = performance.now();
runRenderLoop(engine, () => {
resizeGLEngine(engine);
setViewport(engine);
// false (and nothing bound) until the shaders finish compiling asynchronously
if (!applyEffectWrapper(wrapper)) return;
setEffectFloat(wrapper.effect, "uTime", (performance.now() - start) / 1000);
drawEffect(engine);
});
Effect readiness and errors
createEffect / createEffectWrapper submit compilation and linking without
querying their results. With KHR_parallel_shader_compile, readiness checks
poll completion before inspecting link status. Shader status and info logs are
queried only after link failure, avoiding premature compilation stalls.
Without the extension, checking link status can block.
Manual polling stays available: call isEffectReady(effect) in your
render loop and inspect getEffectCompilationError(effect) when it
returns false. The error accessor also polls once; null means no failure has
been recorded, not necessarily that the effect is ready. Errors include the
program log and failing shader stages' logs and are logged once. Neither this
accessor nor executeWhenCompiled starts automatic polling.
To wait before starting a render loop, opt into the tree-shakeable helper:
import { waitForEffect } from "@babylonjs/lite-gl";
const controller = new AbortController();
try {
await waitForEffect(wrapper.effect, { signal: controller.signal });
// The effect is finalized; rendering can now start.
} catch (error) {
console.error("Effect could not become ready:", error);
}
The helper polls once per animation frame without registering a render loop.
It rejects on compile/link/restore failure, context loss, disposal (on the next
poll), or cancellation via controller.abort(), and cleans up its frame and
listeners on every exit. Cancelling a wait does not dispose a shared effect.
After context restoration, call it again to wait for the replacement program.
Already ready effects resolve without scheduling a frame; consumers that only
use manual polling ship none of the helper's scheduling code.
Migration: shader syntax errors no longer throw synchronously from
createEffect or createEffectWrapper. Poll isEffectReady and inspect
getEffectCompilationError, or catch a rejected waitForEffect promise instead.
For wrappers, pass wrapper.effect to these readiness/error APIs.
Shader/program allocation failures still throw synchronously.
Upgrading: changes the type checker won't catch
Most of the receiver-first reshape surfaces as compile errors. These changes still compile but alter behavior, so check them by hand:
applyEffectWrapper(wrapper)returnsboolean. Onfalse(still compiling, failed, disposed, or context lost) nothing is bound; skip the setters anddrawEffect, or they target whichever program was current before. lite-gl deliberately does not clear the program for you, so hosts sharing the context keep their GL state.createHtmlElementTexturedefaults to linear filtering without mips (previously TRILINEAR + mips). Restore the old sampling with{ ...getSamplingModeFilters(GLSamplingMode.TRILINEAR, true) }and callgenerateTextureMipMaps(tex).updateHtmlElementTexture'sinvertYis the rawUNPACK_FLIP_Y, video included. Babylon flips video with!invertY; port those calls as{ invertY: !invertY }.
Entry points
The entire public API is available from the single @babylonjs/lite-gl entry:
engine + render loop, effects & uniform setters, textures (incl. float / HDR +
dynamic), the EffectWrapper fullscreen-quad renderer, render targets, meshes /
vertex-index buffers + instancing, depth / stencil / color-mask / clear, scissor,
blend modes, the sprite renderer, and HTML-element textures. The package is
sideEffects: false, so a bundler tree-shakes away whichever features you don't
use.
Core API (@babylonjs/lite-gl)
- Engine / lifecycle —
createGLEngine,disposeGLEngine,resizeGLEngine,getRenderWidth,getRenderHeight,get/setHardwareScalingLevel,getRenderingCanvas, andon/offContextLost+on/offContextRestored(context-loss is handled: effects and textures are rebuilt on restore). - Render loop —
runRenderLoop,stopRenderLoop, plusbeginFrame/endFramefor hosts that drive their own frame. - GPU frame timing (optional) —
captureGPUFrameTime,getGPUFrameTimeCounter(EXT_disjoint_timer_query_webgl2; ≙ Babylon'sThinEngineAPIs, tree-shaken unless imported). - Effects —
createEffect,isEffectReady,getEffectCompilationError,executeWhenCompiled,waitForEffect(optional cancellable readiness promise),useEffect,disposeEffect, and the cached uniform setterssetEffectFloat/…Float2/…Float3/…Float4/…Int/…Color3/…Color4/…Texture. - Fullscreen renderer —
createEffectWrapper,applyEffectWrapper,drawEffect,setViewport,disposeEffectWrapper. A wrapper always holds an effect (there is nosetEffectWrapper);applyEffectWrapperreturnsfalseand binds nothing until that effect is ready, so skip the frame's setters and draw onfalse.drawEffectlazily creates and binds the shared quad itself, so an existing effect can be drawn fullscreen without a wrapper:isEffectReady→useEffect→ setters →drawEffect. - Textures —
createRawTexture(typed-array upload, LDR byte formats),createTextureFromSource(single upload from already-decoded bitmaps/canvases/images),createTexture3DFromPixels+setEffectTexture3D(optional native RGBA8 LUT volumes with trilinear sampling and zero 2D-only bundle cost),createFloatTexture(float / half-float HDR opt-in),generateTextureMipMaps,loadTexture2D(async URL upload with a 1×1 placeholder),bindTexture,disposeTexture,isTextureReady, plusupdateRawTexture,updateTextureSamplingMode(raw filters),updateTextureSamplingPreset(Babylon preset +generateMipMaps),updateTextureWrapMode,createTextureFromHandle. Every texture creator takes the sameGLTextureOptions(invertY,premultiplyAlpha, filters / wraps). Babylon-styleGLSamplingModepresets resolve to filters via the opt-ingetSamplingModeFilters;generateTextureMipMapsbuilds the chain and keeps it current on every later upload and context restore. - Dynamic textures —
createDynamicTexture,updateDynamicTexture,clearDynamicTextureSource(the retained source is replayed on context-restore).updateRawTexture/updateDynamicTexture/updateHtmlElementTexturetake optionalGLTextureUpdateOptions(invertY,premultiplyAlpha). - Render targets —
createRenderTarget(RGBA8 FBO, or single/two-channelR8/RG8viaformat: gl.RED/gl.RG, with a sampleable colorGLTexture+ optional depth / stencil renderbuffer),createFloatRenderTarget(float / half-float HDR opt-in),bindRenderTarget(cached) /restoreDefaultFramebuffer(back to the canvas),resizeRenderTarget,generateRenderTargetMipMaps,readRenderTargetPixels(CPU readback; the optional{ x, y, width, height, into, format }defaults to the whole target, andformatreadsRED/RGtargets tightly packed),readRenderTargetPixelsToBuffer(same, into aPIXEL_PACK_BUFFERat a byteoffset),disposeRenderTarget. - Meshes / buffers —
createVertexBuffer,updateVertexBuffer,createIndexBuffer,bindAttributes,drawIndexed(instanced),disposeBuffer. - Depth / stencil / scissor —
setDepthState,setStencilState,setStencilOpSeparate(independent front/back operations),setCullState,setColorMask,clearEngine;setScissor,disableScissor. - Blend —
setBlendMode+GLBlendMode(DISABLE/ADD/ALPHA/PREMULTIPLIED), matching Babylon'ssetAlphaModeparameters. - Text (Slug) — analytic, resolution-independent glyph rendering, the WebGL2
counterpart of Lite's
TextRenderer:loadFont/createFontFromBuffer,createDefaultTextData/updateDefaultTextData/disposeDefaultTextData(shape + extract + pack in one call), orcreateGlyphStorage+createTextData/updateTextData/disposeTextDatato drive your own layout;createTextLayer/setTextLayerPosition,createTextRenderer,registerTextRenderer/unregisterTextRenderer/renderText/isTextRendererReady, and the optional synthetic-weight settersetFontWeightOffset(its shader is only bundled when you import it). Text usestext-shaperfor shaping; it is a dependency, not bundled by consumers that never import the text API. See Text below.
Text (Slug)
import { createGLEngine, loadFont, createDefaultTextData, createTextLayer, createTextRenderer, registerTextRenderer } from "@babylonjs/lite-gl";
const engine = createGLEngine(canvas);
const font = await loadFont("/fonts/Inter-Regular.ttf");
const data = createDefaultTextData(font, 48, "Hello, WebGL2", [1, 1, 1, 1]);
const layer = createTextLayer(data, { positionPx: { x: 24, y: 80 } });
const renderer = createTextRenderer(engine, { layers: [layer], clearValue: { r: 0.1, g: 0.1, b: 0.12 } });
registerTextRenderer(renderer); // draws every frame; unregisterTextRenderer to stop
Glyph outlines are stored as quadratic curves in a float texture with a spatial
band index in an rg32uint texture; the fragment shader resolves analytic
coverage per pixel, so text stays sharp at any scale or rotation. The text data
(and its glyph storage) is borrowed by layers and renderers — dispose it
yourself with disposeDefaultTextData / disposeTextData. One glyph storage
can be drawn by a single engine only. Text is blended as premultiplied
source-over, with no depth test.
Already-decoded textures
import { createTextureFromSource, disposeTexture } from "@babylonjs/lite-gl";
const texture = createTextureFromSource(engine, bitmap);
// One texImage2D upload, linear filtering and clamp-to-edge by default.
// Later, release the GPU resource:
disposeTexture(texture);
The factory accepts TexImageSource (including ImageBitmap, ImageData,
canvas/offscreen canvas, decoded images, video and video frames), derives the
intrinsic size, and performs no placeholder or blank allocation first.
Video elements must have a decoded current frame (readyState >= 2,
HAVE_CURRENT_DATA), not just metadata with positive dimensions. A retained
video that loses its current frame restores as transparent black with a logged
error, like other unusable retained sources.
GLTextureSourceOptions configures filtering/wrapping and unpack flags.
For ImageBitmap, WebGL ignores unpack flip/premultiplication; choose those
settings during decoding instead. Convert SVG or density-selected (srcset)
images to an ImageBitmap first, since WebGL may upload them at a size other
than their natural size.
Sources are retained for automatic context restoration by default, so keep
retained bitmaps/frames open while their texture is live. Pass
{ retainSource: false } when your application owns recovery: the source
reference is dropped after upload and can be closed, but context restoration
produces a ready, transparent-black texture at the original size. Recreate
the texture after restore (for example from onContextRestored) to recover its
content. Mutable retained sources replay their current pixels and intrinsic
size. If a retained source was closed anyway, restore logs an error and falls
back to transparent black for that texture only. Mipmaps requested with
generateTextureMipMaps are rebuilt after the restored upload. No source is
closed by lite-gl.
Demos
Runnable scenes for every feature live in the repo's GL lab
(lab/gl/) — fullscreen effects, textures, sprites, blend modes,
HTML-element textures and render-to-texture round-trips. The standalone
/gl/texture-3d-test.html source test (lab/gl/src/texture-3d-test.ts) checks
native 3D texel addressing and trilinear filtering without a gallery demo.
License
Apache-2.0