Forge Dev Log 3: IMGUI 2D Canvas

Day three! What a day! I finally managed to move past my writer's block — if you can call it that — and make good progress on the canvas layout for Forge's IMGUI as I had been wanting to. It's not a whole ton of work, but to paraphrase a good friend of mine: when it comes to personal projects, the "initial inertia" is the hardest bit. So I'll pat myself on the back and take this small win.

Rather than starting with a wall of text, I figured it would be much more interesting to see the layout in action. Apologies in advance for the jittery mouse movement, some evenings I work best off-desk so I was stuck with the trackpad for navigation!

Canvas components

Although it may not be obvious, the node graph pretty-much demonstrates all of the IMGUI framework's basic systems working together. The dummy nodes, the rects with the coral outlines, are positioned using set_next_gui_position() as demonstrated in Day 2. The canvas layout itself is special in that it does not dictate the precise position of the elements. Instead, it manages a transformation matrix that transforms any elements drawn within its scope. The matrix components are manipulated by mouse input.

I introduced the begin_gui_transform() and begin_gui_clip() functions in order to set the active transformation matrix and clip rectangle respectively. Both of these functions simply push data into a buffer which is then taken into account in the shaders used to draw the GUI elements. Forge's IMGUI supports any number of shaders to allow for complex GUI rendering if every necessary and it is up to the developer to ensure the shader respects the transform/clip buffers. More on the GUI shaders another day.

The implementation of the dots on the grid was something I mulled over for quite a while. In case you're not already aware, I tend to overthink solutions to simple problems. It gets even worse when it's for a personal project I care about! Ultimately the decision came down to whether I should render all the dots on a single rect via a custom fragment shader, or whether I should push a handful of GUI elements using the existing circle primitive. Bearing in mind a custom shader would mean an additional draw call just for the grid and almost all of the fragments will be transparent, I opted to just push each dot as a separate GUI element. Thankfully, buffer memory both on the CPU and the GPU is pre-allocated in a slab-like manner on IMGUI initialization so we can feel confident in the rapid creation of many GUI elements. Once again, I will defer any lengthier discussion of IMGUI's memory allocation patterns to another time.

Pan/zoom transformation

Surprisingly, the implementation I struggled with most was the zoom behavior of the layout. The pan was trivial, it was the zoom specifically that caught me off guard. My mistake was in trying to approach the solution solely by thinking of the elements being transformed within the canvas. While mathematically it does boil down to doing just that, it helped immensely to frame the problem as a 2d camera problem. With this in mind, a couple of points became clear:

  • Scaling should be done about a pivot centered on the canvas container.
  • Scaling towards the mouse position is a common behavior. We need to translate the view as we are scaling so that at some maximum scale the mouse position is at the center of the view.

It then became trivial to build a suitable transformation matrix \(C\).

\[C = S_pSS_p^{-1}T\]

Where \(S_p\) is the scale pivot, and \(S\) and \(T\) are the scale and translation respectively. The key insight here is that unlike a regular object transformation, we want to first translate, then scale, so that the view behaves like a camera zooming in/out of objects that have already been moved in space.

What next?

Maybe the solution was pretty obvious, in any case, it works well and I am happy. There are some other things, however, that are still bugging me hard. You may have noticed the outline on the rects are looking a bit ugly, they're missing a certain pixel-perfect crispness to them. The dots on the grid, in the meantime, are supposed to be beautiful anti-aliased circles. It is pretty clear I will be knee-deep in shader programming for the next few days. I have my sights set on nailing the shaders for the primitives so I will not have to visit them again for a long while.