
The Rendering of Operius DX
Wait, a 3D game with GameMaker? Or is it 2.5D instead? 3D? 2.5D? 3D? 2.5D? I'm confused! WRAAH!!!
Operius DX is finally out on both Nintendo Switch and PC!
And with that, I want to talk about the rendering of Operius DX, because let's be honest here, that's by far the most interesting part of the game. I mean, it's a GameMaker game, but it seems to be...3D? Wait, is GameMaker even capable of 3D? Are we using its built-in 3D functions?
Nope.
So how did we do it?
Software Rendering vs. Hardware Rendering
Before diving into how Operius DX works, let's go over the fundamentals.
Software rendering is when we handle all rendering calculations (geometry, projection, EVERYTHING) on the CPU, with no help from any specialized graphics hardware. We're writing all the math by hand: transforming vertices, projecting them onto the screen, and finally rasterizing them into pixels. It's harder, but also a more straightforward approach.
This was the preferred way of doing things way back in the day, before GPUs were ever a thing. Like, remember Doom? That's software rendering! How does it run so smoothly on such old hardware? Well, it cuts all sorts of corners and implements tons of insanely clever tricks. We could be here all day talking about those!
Hardware rendering, on the other hand, offloads most of the rendering to the GPU. GPUs are optimized for parallel tasks, perfect for rendering thousands of pixels all at once.
With hardware rendering, instead of micromanaging everything like we do with software rendering, we utilize APIs like DirectX, OpenGL, and Vulkan, which abstract the underlying hardware for our ease of use. This means we don't have to implement complicated nerdy shit like “perspective projection” or “rasterization” all by ourselves, we just describe what we want (e.g., a triangle, a transformation, a texture), and the GPU handles the rest. It's way less exciting, but also way more accessible.
This is what 99.9% of modern games use these days, because it's fast, scalable, and works across many devices. There really isn't much of a reason to use software rendering anymore. Like, why would you? Are you stupid?
So, which approach do we take? Software rendering? Or hardware rendering?
Both!
What the Hell is 3D Anyway?
At the core of any 3D rendering system is perspective projection. That's the bit where things further away look smaller, and closer objects appear bigger, giving the illusion (get it?) of depth to a flat 2D image.
In a traditional 3D engine, you have to define a camera with a projection matrix, and the GPU handles transforming your 3D world coordinates into screen space using a series of complicated matrix multiplications (model -> world -> view -> projection).
Or in simpler terms, GPU makes 3D points 2D, with some complicated math stuff.
But here with Operius DX, we decided to take a more software-rendering-like approach, and run just a few lines of math on the CPU that squashes all those 3D points into screen space.
Or in simpler terms, CPU makes 3D points 2D, with some simple math stuff!
var _scale = (_depth)/(-_depth/144 - 1) / 128; var _final_x = _x - (room_width/2 - _x) * _scale; var _final_y = _y - (room_height/2 - _y) * _scale;
Which means, yes, we're not using any projection matrices here. No near/far planes, no frustums, no clip space nonsense. Just good ol' math scrabbled together until it all felt right.
And yes, those are magic numbers I just came up with randomly. Don't judge me, they work.
Drawing It All
Okay, we got our 2D projected points, but we still need to draw them on the screen. And for this one, we do rely on the GPU!
See, Operius DX uses GameMaker's draw_primitive_begin()
function and its friends to draw lines directly to the screen. You know, essentially, treating the GPU as a dumb rasterizer. A pretty genius idea, if you ask me!
Notice how I said lines though, not triangles. Operius DX doesn't use models in the traditional sense. All the “models” you see, like the UFOs and whatnot, are generated by code. Each object is just a collection of vertices, drawn with lines connecting them.
This was a necessity for the specific art style we were going for. Like, imagine we used actual 3D models here. Imagine if there were lines going through each segment of the tunnel. That'd look awful!
This approach also allows us to draw sprites really easily. Instead of drawing a whole billboard mesh, we just draw a sprite where a single vertex would be!
What About Depth?
Since we're manually drawing everything, we're also responsible for handling things like depth sorting ourselves.
In most 3D pipelines, the GPU handles this with a z-buffer, a special buffer that keeps track of the depth of every pixel to figure out which surfaces are in front. We don't get that luxury here.
Instead, we first draw the tunnel behind everything, then manually sort all our in-game objects from back to front every single frame, and draw them in that order. Kinda like painter's algorithm, but instead of sorting polygons, we're sorting groups of vertices (objects).
// Count the number of instances to draw var _instances = instance_number(obj_3dparent); // Create the grid var _grid = ds_grid_create(2, _instances); // Fill the grid var _counter = 0; with (obj_3dparent) { ds_grid_set(_grid, 0, _counter, id); ds_grid_set(_grid, 1, _counter, id.y); _counter++; } // Sort the grid ds_grid_sort(_grid, 1, false); // Draw it all for (var i = 0; i < _instances; i++) { with (ds_grid_get(_grid, 0, i)) { if (visible) event_user(0); // This is where the magic happens! } } // Clean up ds_grid_destroy(_grid);
To explain it in simpler terms again, essentially, we create a list of objects, then sort them by their y position, then draw them in that order. Objects that are furthest get drawn first, behind everything else, objects that are closest get drawn the last, on top of everything else.
Yep, this means that the actual positions of the individual vertices don't really matter, it's the object's position that determines depth. You'd think that'd be an issue, but nah. It's not very noticeable at all, since everything moves so fast, and each object is rather small.
What About The Tunnel?
The tunnel is also fake!
We have two render modes. One is a standard 3D-ish projection (used in menus), and the other is tunnel mode, where we apply a math transform that warps geometry into a cylindrical shape.
The whole game is actually played on a flat plane where you wrap around on the sides. All the tunnel visuals are just fancy math on flat geometry. It's a trick, you've been duped!
About Performance…
Operius DX runs surprisingly well on a large variety of hardware. I tried it on a bunch of different weird devices, including a 2007 iMac and one of those cheap emulation devices you can get from AliExpress, and the game was able to hit 60 FPS no problem in all of them, at least after some tweaks. Okay, that's cool. But on the Switch, things are a bit different.
The Switch version of the game has a tendency to drop frames when there's too much going on. It's not significant enough to be a problem, but it's there and I can imagine some (but not all) of you might have already noticed it. What gives?
The issue is that we don't really use persistent vertex buffers like how modern GPUs expect us to. Instead, we submit the vertex positions dynamically each frame. Since all the geometry is generated procedurally and changes constantly, it's not possible for us to cache anything ahead of time. The game just rebuilds the vertex buffer every single frame. Because of this, the CPU ends up spending a lot of its time generating geometry, copying that data to memory, and preparing draw calls.
That alone wouldn't be a huge issue...except that on the Switch, the CPU and GPU share the memory bandwidth, and there's a pretty strict pipeline between them. Every time the CPU pushes a vertex buffer to the GPU, it often has to wait until the GPU is done using the last one. If we're not careful, the CPU might end up stalling, waiting for the GPU to catch up, and vice versa.
There are possible ways to mitigate this...but remember, we're using GameMaker here. That engine doesn't really give us much of a way to fuck with the GPU directly. Most of it is abstracted behind simple functions like draw_primitive_begin()
which is what we're forced to use.
...which is also why most of what I've written here is just speculation. Sure, we can hook RenderDoc up and analyze the GPU pipeline, or even look at GameMaker's source code (you do get access to that with an Enterprise license), but it doesn't really tell us enough to have a definitive answer.
I guess it wasn't a genius idea after all.
But the worst part is that I know Switch can do better than this. Like, as I mentioned earlier, I tried this game on one of those cheap Aliexpress emulation devices, with the help of the Portmaster guys, and the game run at around 40-60FPS. And I'm pretty sure that the hardware on that thing is way, WAY worse than what Switch has. So I'm fully convinced that there's something fucky going on in the GameMaker side as well.
I did try to optimize things in other ways, at the very least. I did all the usual tricks: tried to reduce the draw calls, number of dynamic buffers, vertex counts… But the truth is, when your render model is built around procedural generation every single frame, and when you don't have low-level GPU access, there's only so much you can do…
Isn't Doing Perspective Math On The CPU Expensive Too?
Not really.
CPUs are fast. Even the older ones! Especially when the math is relatively simple and we're dealing with only a few hundred vertices total. Remember, the bottleneck here isn't the projection, it's rasterization.
Why Not Use a Different Engine Then?
Because it works. On PC, the game runs perfectly. On Steam Deck it's all fine too. Nintendo Switch just happens to be the one weird outlier where our approach, plus GameMaker's quirks, plus whatever's happening under the hood with the Switch, all conspire against us. And the result is...occasionally the game dropping to 58 FPS. It's not a big deal.
And sure, we could rewrite the renderer to use GameMaker's proper 3D functions, or maybe even switch engines entirely to something like Unity or Godot! ...but then Operius wouldn't be Operius anymore. The weird dynamic renderer is part of what gives the game its feel, its character. Even if you don't think you notice it, you subconsciously do.
Like, go download GZDoom and try toggling between the hardware and software rendering. You'll notice a difference. And if you played Doom enough times, the hardware renderer will immediately feel wrong, even though on a technical level, it's much more accurate. It's all about the feel.
So Wait...Is Operius Actually Not a 3D Game Then?
This is an interesting question, and one I've seen come up a lot when people talk about games like Doom.
In terms of gameplay...I guess? I mean, most of the action here happens on a flat 2D plane, so you could think of it as a 2D game.
But there are also elements that operate in full 3D. For example, one of the new bosses is an octopus whose tentacles move in 3D space, and if they dip low enough, they can actually hit your ship. There's also a wave with bouncing balls that you have to dodge, and yes, you can go under them. So...maybe it is 3D?
In terms of rendering, though...what determines what is 3D and what isn't anyway?
Like, we ARE doing perspective projection here. We're rendering geometry with depth, we're drawing lines that twist and turn and scale with distance. Surely, that should just count as 3D rendering?
But we're also faking a lot of the infrastructure that a true 3D renderer would use. As I said earlier, we make no use of projection matrices, clip planes, and all that complicated nerdy shit you see in those. Instead, we just say “Here's a math function that makes these vertices wiggle in a convincing way”, which just so ends up being convincing enough to sell the illusion!
So from that perspective, it's more like a 2.5D renderer that pretends to be 3D just well enough to get the effect we want. But I'm not sure if I'd really agree with this definition.
So if you ask me, yes, Operius absolutely is a 3D game. Even if the rendering pipeline is very much unusual.
Would We Make Another Game Like This Again?
Maybe!
I mean, I love how the result looks, and the whole semi-software-rendering thing opens up lots of creative possibilities.
But on GameMaker it just isn't very viable. A while ago we made an F1-style racing game for the dudes at Opera GX, and we ended up building a completely different software renderer, heavily inspired from how OutRun did it way back in the day. I decided to try to port it to the Switch, just for fun.
It ran at 13 FPS. Ouch.
Operius DX is what it is because of how it was made. It's a little janky, but also uniquely its own thing, and we're proud of that!
You can get Operius DX from Steam, itch.io, or Nintendo eShop for your Switch or Switch 2! It's only $5, come on.
Share this post
If you like what you just read, share it with your friends or on social media!
Stay in touch!
Sign up for our newsletter to get monthly exclusive newsletter posts, and more!
Keep Reading

DI MONTHLY #4: Distant Ambitions, Broken Dreams
Was it all for nothing?

DI MONTHLY #3: Off To The Next Chapter
Operius DX is out...now what?

The Operius DX Soundtrack Is Out Now Too!!!
Good news!