OpenGL bindings for Bash
OpenGL bindings for Bash
A project that started as a joke can be useful to people wanting to learn the concepts of OpenGL.
In my previous article describing the design of Perl 5 and its suitability as a "glue language," I mentioned I had previously written OpenGL bindings for Bash. This was perhaps too incredulous of a statement to make without some proof, so I went back into the dusty corners of my hard drive, dug it out, freshened it up a bit, improved the font support, wrote up documentation, and published it on my site and on GitHub. (You'll need a system with both Bash and OpenGL support to experience it firsthand, but here's a video.)
So now my confession: The Perl graphics in the DeLorean dashboard I described in My DeLorean runs Perl share a history with my OpenGL for Bash project. In a fun and ironic twist, I originally started the project 13 years ago after witnessing Frozen Bubble and having my technical sensibilities offended that someone had written a real-time video game in Perl. Back then, my primary language was C++, and I was studying OpenGL for video game purposes. I declared to my friends that the only thing worse would be if it had been 3D and written in Bash. Having said the idea out loud, it kept prodding me, and I eventually decided to give it a try to one-up the "awfulness" of using Perl for real-time graphics.
Extending BashThe most direct way to add OpenGL support to Bash would have been to alter the source code and add each OpenGL function as a shell "builtin"; however, the only people who would be able to experience it would be those willing to install a custom version of Bash, and probably nobody would want to do that. It also didn't seem in the spirit of doing things "the Bash way."
I then came up with the idea of a client-server, where each OpenGL function would be installed in the path and invoke a client that would connect to an OpenGL server to deliver a message to execute that function. That would have been a delightfully "awful" solution, but no matter how hard I optimized things, it was still too slow for me to say I had succeeded at the overall goal.
Finally, I settled on a design where I would have an "OpenGL Interpreter" process that would read OpenGL commands on standard-in and write any user input events to standard-out. Then a Bash script could start this interpreter connected to pipes, and each OpenGL command could be a Bash function that writes to the pipe. While I was at it, I decided to throw in my entire bag of efficiency tricks and write the interpreter in plain C with some statically compiled hash tables and red-black trees.
OpenGL immediate mode
The OpenGL details are where this project starts to intersect with the DeLorean dashboard project. First, I'll take a step back and recap the OpenGL API.
OpenGL is all about writing a program that plans out its graphics in terms of 3D coordinates and textures, then ships that data over to the graphics card to be rendered to a 2D screen. To be really simplistic, there is a set of math you use to describe the region of the screen being painted, and a set of math to describe the color of each pixel in that region.
The most common operation is to describe three corners of a triangle in a virtual 3D space, let OpenGL figure out where that lands on a 2D screen, then tell it to stretch a 2D image across that area, maybe also combined with another image or altered by some brightness calculations. The main loop of the program generates one frame of video by wiping the buffer, plotting every polygon that can be seen, and sending it to the screen. If you pull off this entire stunt in 16 milliseconds or less, you can maintain 60 frames per second and have nice fluid graphics.
I can't speak with authority about the origins of the OpenGL API, but it seems pretty clear that it is built around the idea of streaming, probably to be compatible with the X11 display protocol, but also just because it's a good idea. So, most OpenGL functions do not have a return value, as opposed to other APIs where each function call returns a status that tells you the outcome of the operation. If you visualize an OpenGL function such as
glVertex3f(1,2,3) as a print statement that writes three numbers over a pipe, you've got a pretty good idea how it works behind the scenes. In fact, for the Bash bindings, I literally write
glVertex 1 2 3 over the pipe when
glVertex 1 2 3 is executed. The Bash script runs blind, without any idea whether its graphics commands are doing anything as intended.
There have been several big revisions to the OpenGL API over the years, and people write entire chapters in their books about "retained" vs. "immediate" modes, but it all boils down to two concepts:
- It's slow to resend all that data over the pipe each frame, so let's cache some of it on the other end.
- We can never satisfy everyone's needs with the built-in math, so let's give people a language to describe their own custom math.
That said, while the caching and custom math offered by the new OpenGL APIs are much more efficient and certainly needed for top-tier video games, they are also more effort to set up and work with (and learn) and probably do more harm than good to hobbyists. They're also unnecessary unless you're doing advanced graphics effects or high-detail models.
So, although the Bash OpenGL bindings only deal with the "deprecated" API, I still encourage people to look at it for education and tinkering purposes.
Luckily, the original OpenGL API has some caching mechanisms, and they're easy to use. They work roughly along the sequence of:
"Hey OpenGL, I want you to create object 37 on the remote end"
"Here's some data describing object 37"
"Use object 37 in the next rendering steps"
To make it even easier, the Bash bindings let you use names instead of numbers.
The first main type of object is the texture, where you load a 2D image into the graphics card and then "paint" polygons with it. The second is the "display list," where you record a sequence of OpenGL commands, then play them back as if they were a single OpenGL command.
Display lists work great for ad-hoc prototyping. You can plot out some points describing your 3D (or 2D) model using simple vertex commands, then record that sequence of points as a display list, and now you can render that model with a single command.
For an example of this power, check out Robot.sh in the Examples directory. It creates one display list for each segment of the robot's body at startup, and while running, it only emits 58 lines of text per frame to render the robot. Bash can pretty easily generate 58 lines of text in 16 milliseconds (it even could 12 years ago), so the demo was able to run at full speed on common hardware.
Display lists are the primary trick I carried over to the Perl graphics I'm using in the DeLorean dashboard software. Perl function calls are a bit expensive compared to C, especially for a function-heavy API like OpenGL 1.4, but by combining things into display lists, Perl doesn't have much work to do per video frame. If I hadn't learned this trick for the Bash project, the Perl project wouldn't have been nearly as successful.
Why didn't I publish this 12 years ago?
While I was able to create some fancy animation demos in Bash, my actual goal was to create a complete game with it. I was aiming for a clone of the old flight-sim game Terminal Velocity, and I got as far as a spaceship that could fly through a field of cubes (see Flight.sh in the Examples), but the next problem was collision detection. The primitives available in Bash are the global "associative array" of variable names and array variables (although version 4 now has associate-array variables), and all math is integers. While I was able to pull off 3D matrix rotations in fixed-point integer math, implementing collision detection looked like too large of an obstacle. I was also not satisfied with my font API. While pondering solutions for these problems, the project gradually fell off my list.
This is where I concluded (as I wrote), "Bash is a horrible glue language." While this project started and ended as a joke (or education tool, maybe), the same program style in Perl turned out to be quite useful for real applications. If I'd given Frozen Bubble more serious consideration 13 years ago, I might have been able to get a head start on Perl, my now-favorite language.
I don't have any plans to enhance this project aside from bug fixes, but I figured it could at least be useful to people wanting to learn the concepts of OpenGL or someone with a bunch of free time and a strong desire to write a Doom clone in M4.
Enjoy! And have fun forwarding this link, otherwise, no one will believe you.