TL;DR: I have made my own WebGL framework, and it’s called Glii.


It should be no secret that I have strong opinions about WebGL frameworks. So in true Bender «I’ll make my own, with blackjack and hookers» spirit, I’ve made my own. Save the blackjack and hookers, mind you.

It’s safe to say this is the culmination of five years worth of thoughts. My debut into WebGL was in MazeMap (late 2015, I think), when I was in charge of speeding up the map rendering, after kinda giving up and submitting to mapboxl-gl-js. I tried to learn all I could, and I even created something dubbed Leaflet.GL that, sadly, after 3 or 4 full refacors, never saw the light.

Those refactors were whole mistakes. Luckily, I learned quite a lot from those mistakes.

The most important thing I learned is that WebGL was designed for C people and made by C people for C people. That means it was not designed, nor made by, nor made for, HTML people, nor JS people.

One big obvious thing to notice is that the WebGL API mimicks to perfection the OpenGL API (OpenGL ES 2.0 API, to be exact).

Another big couple things to notice (but only noticeable becuase I did some C back in my university freshman days) are:

  • JS lacks a counterpart to C’s struct
  • Therefore, JS lacks a counterpart to C’s struct[] (an array of structs)
  • The memory layout of vertex attributes (as forced by vertexAttribPointer()) fits a struct[] perfectly
  • Therefore, JS doesn’t have a straightforward way to deal with vertex attributes

So WebGL is a JS API port of a C API… that works with C data types/structures that are not available in JS.

In hindsight, and from the point of view of a web developer, this seems like a huge architectural oversight.

Let me remark the «from the point of view of a web developer» bit. I’m sure the folks that designed WebGL had good reasons for the design, and had time constraints, and had compatibility constraints, and a hundred other things that made the API take its form. But at this point, I bet that ease of use by JS/HTML people was not a (significant) factor.

I like to sum up that in one phrase:

WebGL was never about bringing OpenGL to web developers; it was about bringing OpenGL developers to the web.

As a HTML+JS person, it’s mind-bogglinly complex to get into WebGL. However, since the APIs are practically carbon copies, an OpenGL person could get into WebGL straight away - reusing formatted data (models, meshes) that can be fetched over the network and copy-pasting away chunks of C code.

Fast-forward my thoughts, and you’ll reach the conspiracy theorist part of my brain that thinks WebGL was a ploy by Google to bring 3D-ish Google Maps to Chrome and nothing else.

Anyway, another thing that makes me mad is the way WebGL tries to be object-oriented (since everything in JS is an Object that has a prototype), but fails miserably to provide any kind of object-orientation-derived convenience. Instead, we’ve got freaking pointer logic in JS.

Let me illustrate. Let’s assume we have our GL stuff initialized, get a texture, change some properties of that texture, and dump an image into it.

A HTML+JS developer would dream of a sane way to do this, which would look like:

let myTexture = glContext.createTexture();
myTexture.setParameter('wrapS', 'repeat')
myTexture.fromImage( document.getElementById('my-img') );

Instead, we got this:

let myTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, myTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.BYTE, document.getElementById('my-img') );

Let me comment on the seemingly idiotic code:

// So far, so good
let myTexture = gl.createTexture();

// Set the value of the "currently bound 2D texture" pointer to the texture
gl.bindTexture(gl.TEXTURE_2D, myTexture);

// Set a texture parameter (given some integer constants), not of to the (un)given texture,
// but of the texture pointed by the "bound 2D texture" pointer
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S);

gl.texImage2D(
	// Dump the `ImageData` of a `HTMLImageElement` into the texture currently pointed by
	// the "bound 2D texture" pointer.
	gl.TEXTURE_2D,

	// Affect only the mipmap level 0. This would be a sane thing to specify, if
	// the MIN_FILTER parameter of the texture were using mipmaps.
	0,

	// Set the format of the texture to RGBA. This is kinda sane, since dumping data
	// into a texture might drop some of the data to make the texture size smaller in memory
	// e.g. ignoring the alpha channel of fully-opaque images
	gl.RGBA,

	// Repeat the format, since the WebGL API literally forces this parameter to have
	// the same value of the previous parameter instead of providing overloads.
	gl.RGBA,

	// Specify the byte length of the image data, compeltely ignoring the fact that the
	// JS `ImageData` interface (and `HTMLImageElement`, and `BitmapImage`) can only be
	// 8-bits (one byte) per channel.
	gl.BYTE,

	// And the `HTMLImageElement` reference.
	document.getElementById('my-img')
);

The more time I’ve spent with this, the more idiotic is seems. Again, this is from the point of view of a web developer, with special emphasis on the «from the point of view of a web developer» bit. I do guess that this makes sense to low-level C+OpenGL people somehow.

The prize to the most seemingly idiotic part of the API, however, goes to parameters that must be zero. From the texImage2D documentation:

border A GLint specifying the width of the border. Must be 0.

Listen, I’ve used a fair amount of APIs during my career. I’ve done UML, I’ve read design patterns, I’ve created my fair amount of documented interfaces and APIs. And if I set aside my humility for a second, I can honestly say that having a public API require a parameter, and then force the users to always, unconditionally set that parameter to zero (or any other given static value) without explaining a good reason for that parameter to exist in the first place is utterly stupid.

Y’a know, at least slap “legacy” or “reserved for future use” in there.

But yeah, the more I’ve been dabbling with WebGL over the years, the more its API has begun reeking of C. I really mean no disrespect to the architects of OpenGL ES 2.0… but at the same time I cannot help but feel the reek.

I’m not even going to comment on the death of Flash and how HTML5+WebGL has failed to appeal to non-technical people, and why all artsy game devs are using Pico8 and Unity.

So, frameworks.

There have been a number of WebGL-based framweorks over the years. The most current list I could find is in this gist by Damien Seguin, and it lists about 80 frameworks, toolkits, engines and frameworks.

And yet, none of them fulfill my needs.

Yes, it can be said that I have special needs. My WebGL career has been mostly failing to deliver Leaflet.GL, then doing all kinds of experiments on Leaflet.TileLayer.GL, failing to understand the overly complex architecture of mapbox-gl-js (now maplibre-gl-js), wondering about the OpenLayers abstractions for stitching tiles and reprojection, and creating the OpenLayers GlTiles data source (to replicate and expand the functionality of Leaflet.TileLayer.GL). This means I have a very specific set of requirements in a WebGL framework/toolkit:

  • Access to all the low-ish level functionality
    • Things like Float32 textures
    • Do not assume 3D, no transformation/projection/scene matrices by default
    • No implicit render loop
    • This requirement disqualifies all 3D/game engines
  • Actual OOP wrappers over GL concepts like textures, programs and framebuffers
    • This disqualifies most microframeworks and toolsets.
    • At this point, I’m basically left with regl and luma.gl only.
  • No dragging around the WebGLRenderingContext
    • regl fails to deliver this just about.
    • luma.gl fail to deliver this in a spectacular way, since it does provide lifetime/renderloop functionality but forces the developer to carry around the raw instance of WebGLRenderingContext.
  • Semi-sane equivalent of a C struct[]
    • This was the reason Leaflet.GL development sucked 3 to 4 days every time I wanted to change one data structure
    • No, luma.gl, Accesor misses the point.
  • Tooling for JS project sucks. I do not want to spend 10 seconds since saving a file to being able to see the results in a web browser. That means no typescript, and no bundling (no rollup, no webpack, no parcel).
    • luma.gl particularly sucks at this point, since its “hello world” requires a webpack toolchain.
    • The pre-rollup times of Leaflet (the debug/include-browser.js times) were awesome in a way.

… So. After lots of thinking and lots of learning from failures and a crystal-clear vision of how I wanted a WebGL API to be, and after a good couple of years worth of letting the ideas brew, I’ve done my own WebGL framework.

It’s called Glii, and I think it’s awesome. The name is homophonous with glee, because I can finally quickly prototype low-ish level GL algorithms and structures and be happy at the same time. I should create a good backronym for it, like “GL Ivan’s Interface” or something.

Remember the texture example code from before? This is how it looks in Glii right now:

import { GLFactory } from 'glii/index.mjs';
let glii = new GLFactory(document.getElementById('my-canvas'));
let myTexture = new glii.Texture();
myTexture.wrapS = glii.REPEAT;
myTexture.texImage2D( document.getElementById('my-img') );

It just makes sense. It wraps GL concepts in a OOP fashion; I can forget about the WebGLRenderingContext entirely (because of some clever closure bits); and since browser support for <script type=module> is mature enough, no bundling is needed.

And the preliminary UML class diagrams (generated via Leafdoc and Graphviz) are a thing of beauty IMO:

Class diagram

Look at it. There’s no gigantic class, there are subclasses of stuff for convenience, there’s inheritance and composition and abstract classes, and I can finally understand FrameBuffers from there.

And this is the entire code for a minimal “hello shader” webpage:

<!DOCTYPE html>
<html><head><meta charset="utf-8" /></head><body>
	<canvas height="500" width="500" id="glii-canvas"></canvas>
	<script type="module">
		import { GLFactory as Glii } from "../src/index.mjs";
		const glii = new Glii(document.getElementById("glii-canvas"));
		const program = new glii.WebGL1Program({
			vertexShaderSource: `void main() { gl_Position = vec4(0., 0., 0., 1.); gl_PointSize = 50.; }`,
			fragmentShaderSource: ` void main() { gl_FragColor = vec4(gl_PointCoord ,0.,1.); }`,
			indexBuffer: new glii.SequentialIndices({
				drawMode: glii.POINTS, size: 1,
			}),
		});
		program.run();
	</script>
</body></html>

Obviously Glii has a lot of biases, and I’m taking a gamble with them. But I honestly, heartly think that prioritizing OOP encapsulation/cohesion/decoupling (thus allowing OOP-minded JS people to understand how to put GL concepts together) trumps everything else.

Glii is at https://gitlab.com/IvanSanchez/glii (even though the documentation is a bit in shambles still, the codebase has reached minimally viable maturity). I’ve been keeping that under wraps for years, and now I’m finally taking one of those wraps away.

Because WebGL should be understandable.