This blog post is unfinished!
I don’t normally write blog posts about my day-to-day implementation details but I ended up spending some time this morning adding support for drawing rounded rectangles and rounded rectangle outlines because the Clay UI library command list requires that the renderer have support for drawing such things. The implementation I wound up with isn’t anything super clever, or mathematically complicated, it just ends up being a lot of plumbing and basic math with a few specific limitations adopted in order to get the effect I want in the use cases I actually have. For example because of the way my Sokol-based renderer is currently set up, I decided not to use SDF functions in a shader to draw these shapes but rather a small set of vertex buffers that get created on startup.
So although we won’t cover any novel topics I think this may be a great way to talk about some basic maths, basic rendering, and basic UI/UX, all in a very visual way. The work I’m covering is all in my open source PigCore repository as well which means that the entire source code is available, and I have a demo compiled for Windows that you can download at the end and play with as well.
The Goal
As part of standing up various pieces of PigCore I needed to add some support for creating UI in a relatively efficient manner, mostly for debug purposes but also for early prototype game UI. I thought about using Dear ImGui since that tends to be the industry standard option for debug UI in games, but there was a new library called Clay which had a few aspects that seemed potentially useful for my intended use case. So I decided to give that a try first, knowing that if it didn’t work I could fall back to Dear ImGui instead.
PigCore currently has support for both Raylib and sokol_gfx.h
+ sokol_app.h
, so I could have decided to use the example raylib renderer that Clay provides, but the particular project I am developing on top of PigCore is a PBR project built exclusively on Sokol so I needed a renderer for Clay that would work with that. I had already done some work to build a few simple layers on top of Sokol but the requirements of the Clay rendering API would force me to add a few new features to those layers.
First, a simple one, adding support for scissor rectangles. Dear ImGui would have also required this so I saw it coming and it ended up being very easy in Sokol, basically just need to call sg_apply_scissor_rect(…)
, no need to change pipelines or anything like that. Here’s the primary commit for that change in PigCore in gfx_system.h
(and a related change in the PBR project in app_main.c
). This allows us to restrict the pixels that actually get rendered to a particular rectangle inside our window. This is very useful in UI where you often have containers that have contents that are larger than the inner bounds and need to be cut off at the edges. But you can also use this functionality for other elements, even 3D geometry, so the test I added to the PBR project draws half of the 3D chest models with a scissor rec enabled:
Second, the Clay BORDER
render command also has separate options for the thickness of the border on all 4 sides, which is not something I had needed before, but it was very easy to add, just had to split a single “thickness” option on my DrawRectangleOutline function into four arguments and make sure the math for each side uses the correct thickness value.
However, both the RECTANGLE
and BORDER
commands also come with corner radius options which require me to be able to render a rectangle with rounded corners. This wasn’t entirely unexpected, given the video demo for Clay has a bunch of rounded rectangles, but it is distinctly harder to render these shapes on modern graphics pipelines.
Rendering smooth curves in general is not something graphics APIs have baked in, in my experience there are three options:
1. Use a pre-rasterized texture of the curve
2. Use a per-pixel shader technique to color the pixels inside the curve properly based on math (usually using SDF function and probably fwidth and smooth_step
to get an antialias effect at the edge)
3. Approximate the curve with enough vertices that you can’t tell it’s not curved
Number 1 Isn’t really an option for my use case since I want the texture to be chosen separately from the shape we are rendering. This isn’t a requirement of Clay, since it only asks us to draw single color shapes, so you could take this approach, I just chose not to. Number 2 is what I’ve usually done in the past, since it allows for arbitrarily large rounded shapes on screen with decent anti-aliasing, but it requires that we bind a particular shader that has uniforms for all the shape parameters. This means have to have these uniforms and SDF functions in all the shaders we are going to use which gets a bit annoying. If our graphics system had support for automatically compositing shader “pieces” into whatever shader is needed then we could have taken this approach. Number 3 seems a little unsavory…