OpenGL is needlessly hard... but not impossible
Author: Alexander Avery
Posted:
#game-dev #computer-science
Since around 2020, I’ve tinkered with writing a custom game engine three separate times, each time in an attempt to create a new 2D game. Why a custom engine? Because I wanted to learn how to do it.
I began with moderngl in Python. By the time I was able to render a tilemap and move a character around the screen, I tired of the project and couldn’t find motivation to continue. More than a year later, I gave a different game a shot with raylib bindings for Go, and I made a bit more progress. But to my own detriment, I’m a picky person.
Though I made no effort to figure out if the things I took issue with were real issues, I disliked a few things about using raylib in Go. Firstly, I was using Go for this project because I really enjoy programming in Go. Some of this enjoyment comes from the Go standard library interfaces that work really well together. Raylib is ultimately a C library, and so I found myself passing string arguments to represent filepaths or contents read from files such as GLSL code. What I really wanted to leverage were things like image.Image, io.Reader and io.Writer, and embedding.
Secondly, without taking any benchmarks to see the real impact, I was miffed that every raylib-go call was a CGo call. I just felt a bit silly introducing any overhead for small types like Vector2 and simple arithmetic.
I learned a lot from raylib and really respect it as a C library. For me, it just doesn’t fit Go well, and I really wanted to write my game in Go. This left me with not much choice. If I wanted hardware-accelerated graphics, I would need to eat a CGo cost somewhere, but I would mind that less than making a CGo call every time I needed to calculate a unit vector.
After these failed attempts at making a game in a custom engine, one thing became crystal clear. Before I try to write a novel game in a custom engine, I should learn how to make a game engine by building a game with a fixed scope. What better place to begin than the first game I released?
Revisiting SpaceMail
SpaceMail has a well-understood scope — by me — which means that I won’t go jumping down rabbit holes for both game engine and gameplay code. I wanted something that supported the major three desktop operating systems, so I settled on using OpenGL 4.1. Now, remember, I came into this having failed to write a game engine in OpenGL before, so it was initially daunting. I would have to support a few basic things such as loading textures, writing quad buffers to the GPU, and mirroring textures over the X or Y axis. I would also have to support a few things I had never done before, including rotating sprites, scaling sprites, and running various shader programs. Though this last goal may not have been necessary, I also wanted to write a batch renderer. Again, this is mostly because I just wanted to learn how to do it.
Well, after nearly three months of focused work during spare time, all of the above goals are now met. Would you guess how many lines of code are needed to support this? Well, to my surprise, it’s exactly 599 LOC.
There are still improvements to be made, but it is shocking to me that the basics weigh so little. As of now, my renderer doesn’t support particle effects well or rendering to anything other than the screen. Doing each of those things well happens to be of importance to this game, but I couldn’t imagine either of these doubling the line count.1 Honestly, with the way people discuss graphics programming, I thought the features it currently supports would require thousands of lines of code. Here is a brief demo of what is possible in those measly 599 lines.

The scene above involves a few things. First, the flat spaceship ship, turning spaceship ship, and lasers are sourced from the same GPU texture. Second, the quads for the space ship and lasers are batched into a single draw call. Third, the CPU is performing affine transformations on quads to reflect the turning space ship, and rotate the space ship or lasers appropriately. And fourth, in the second half of the GIF, the renderer uses a shader that modulates the x coordinates of each vertex in a wave pattern. It was trivial to make this secondary shader compatible with the first shader as well as with the sole vertex buffer supplied to the GPU.
The capabilities of this renderer do not impress me, nor should they impress anyone else. What I do hope to demonstrate is that it’s not terribly difficult to achieve useful results in OpenGL with a few hundred lines of code. I intend to continue to improve the capabilities of the renderer as I work on the SpaceMail rewrite in its entirety.
Sources
As you might imagine, since these 599 lines of code took me nearly three months to write, most of that time was spent reading other rendering engines written in Go. I want to share a few resources that are absolutely slept on by most Go developers.
If you are not familiar with the golang.org/x/ packages, you are missing out big time. Foremost, you should read through most of the standard library first before exploring these packages. Allthough you can still learn a lot, many golang.org/x/ packages are meant to extend the stdlib, not circumvent it. They are also not covered by the Go 1 promise as the stdlib is.
Specifically, as it relates to graphics and rendering with OpenGL, the packages listed below helped immensely.
- https://pkg.go.dev/golang.org/x/mobile/gl
- https://pkg.go.dev/golang.org/x/mobile/geom
- https://pkg.go.dev/golang.org/x/mobile/exp/f32
- https://pkg.go.dev/golang.org/x/mobile/exp/gl/glutil
- https://pkg.go.dev/golang.org/x/mobile/exp/sprite
- https://pkg.go.dev/golang.org/x/mobile/exp/sprite/glsprite
- https://pkg.go.dev/golang.org/x/mobile/exp/sprite/portable
You better believe I’ll be referencing golang.org/x/mobile/exp/audio/al when it comes time to add sound to the game.
-
I can’t wait to revisit this later and see if I was spot on or way off base. ↩︎