Diving Into WebGL
March 6, 2021
My Lovely Rotating Cube
I’ve been spending some time wrapping my head around graphics programming lately. After a week of wrestling with JavaScript in Hugo and studying WebGL and GLSL, I’ve made it all the way to a rotating, lit cube! It doesn’t look like much, but I’ve learned a ton! Even this simple example required understanding a huge variety of concepts.
- I’m manipulating and applying matrices for coordinate system conversion, keeping matrix calculations on the CPU where possible. Inconsistent vocabulary in existing literature was one of my biggest stumbling blocks - I settled on calling my matrices “model”, “view”, and “projection”, and when I combine them, I write the resulting matrix’s name in matrix-math order, so that
projection * view * model = projectionViewModel
. If you’re reading this and also find yourself confused with the disparate naming conventions, know that:- Local and object space are the same thing.
- The model matrix is sometimes called a world matrix since it converts to world space.
- The view matrix is sometimes called an inverse-camera matrix, because it’s created by applying a matrix inversion to the camera’s transform. I’ve even seen the camera’s transform referred to as the inverse-view, which is just needlessly obfuscating.
- View space is sometimes called eye space or camera space
- OpenGL’s state machine model has some fairly verbose conventions - many of which I now skip over thanks to twgl.js. Getting attribute locations and filling buffers through
GL_ARRAY_BUFFER
are just a couple bits of nonsense I would love to never do again. - Coding for a GPU requires a shift in perspective from coding for a CPU. There are lots of restrictions to keep the GPU’s massively parallel design working efficiently. My cube uses a GLSL shader that can handle simple ambient and point lighting, using a dot product to calculate a surface’s orientation towards a light source and applying a basic quadratic light falloff equation - all math I had very little familiarity with prior to writing this.
- Vertices need to be stored with the correct counter-clockwise winding so that
GL_CULL_FACES
works correctly. This isn’t much of an issue when exporting a model, since modeling software typically handles it for you, but in these early stages, experimenting with basic shapes written by hand, it can be a stumbling block. - I’m animating by updating GL’s state each frame, using time as an argument to rotate the model matrix on the y-axis. I’m only calculating the model matrix and the projectionViewModel matrix each frame, both on the CPU. A lot of tutorials I found send the time to the shader as a uniform and do these calculations on the GPU, but that’s a waste of resources - the GPU will perform these calculations for every vertex, even though they don’t change across an object’s vertices. The GPU might be the better choice when instancing lots of objects in a single draw call, though. I’m not sure yet.
- There’s a little bit of color theory involved here.
- The ambient color is a sky blue, since the sky is the source of most of the ambient light outdoors. I set
GL_CLEAR_COLOR
to the same blue for a pseudo-HDRI effect. - The light is slightly yellow, which creates a pleasing contrast with the ambient light.
- All my colors are somewhat desaturated. If you try to use a blue ambient light with no red or green component and a red object with no green or blue component, the two won’t actually interact at all.
- The ambient color is a sky blue, since the sky is the source of most of the ambient light outdoors. I set
Off to the Blender Mines
Next up, I want to create and export a slightly more complex object in Blender and get it to display in WebGL. Maybe I’ll figure out how to slap a texture on it, too, though I’ve always found texturing a bit beyond my meager art skills.