Filling shapes with rlgl

Almost every scene here draws outlines: lines, circle outlines, rectangles. Two of them fill, and those two are the entire reason the host binds rlgl's immediate mode at all.

What raylib's shapes API cannot do

raylib will fill a circle, a rectangle, a triangle. What it will not do is fill a triangle with a different colour at each corner, and the gradient around the rim of an HSV colour wheel is exactly that: one slice, hue at each rim vertex, white at the centre, and everything between interpolated.

There is no DrawTriangleGradient. There is rlgl.

The four calls

(ffi/defcfn rl-begin     "rlBegin"     [:int] :void)
(ffi/defcfn rl-end       "rlEnd"       [] :void)
(ffi/defcfn rl-vertex-2f "rlVertex2f"  [:float :float] :void)
(ffi/defcfn rl-color-4ub "rlColor4ub"  [:uint8 :uint8 :uint8 :uint8] :void)
(def RL-TRIANGLES 0x0004)

All four are scalar, which matters: they need none of the by-value struct machinery the Vector2 and Color calls elsewhere in raylib do. That makes this the cheapest possible FFI surface, four functions taking numbers.

The shape is the OpenGL 1.x one, and it is a state machine. Colour applies to whatever vertex comes next:

(rl/rl-begin rl/RL-TRIANGLES)
(rl/rl-color-4ub 255 0 0 255)
(rl/rl-vertex-2f x0 y0)        ; this corner is red
(rl/rl-color-4ub 255 255 255 255)
(rl/rl-vertex-2f cx cy)        ; this one is white
(rl/rl-color-4ub 0 255 0 255)
(rl/rl-vertex-2f x1 y1)        ; and this one green
(rl/rl-end)

Three vertices between rlBegin and rlEnd is one triangle. Six is two. rlgl batches them and flushes on rlEnd or when its buffer fills.

The colour wheel

180 slices, each a triangle from rim to rim to the centre. The rim vertices carry their own hue at full saturation and value, so the wheel is the outer surface of the HSV cylinder, and the centre is white. The rasteriser does the rest for free.

It turns because the hue offset advances, not because anything is rotated. That is cheaper and it also means the geometry is recomputed identically every frame, which is fine at 540 vertices and would not be at 54,000.

The pie chart

Same primitive, different problem. raylib-jlt reaches for a sector! helper; there is none here, so a wedge is built from the same triangles, enough of them that the arc reads as curved rather than faceted.

The number is per wedge and proportional to its angle:

(def degrees-per-segment 6.0)
(let [n (max 2 (int (Math/ceil (/ span degrees-per-segment))))] ...)

A 30% wedge gets 18 triangles and a 12% wedge gets 8. Giving every wedge the same twenty is 100 triangles a frame for a picture identical to the 62 this draws.

All three vertices of a pie triangle carry the same colour, so the gradient machinery is not doing anything here. It is used purely because the alternative is not having filled arcs at all.

When to reach for it

Rarely. An outline is cheaper, most of these scenes are outlines by nature, and the immediate-mode state machine is easy to leave in a bad state.

The test is whether raylib's own shapes API can express what you want. If it can, use it: DrawRectangle is one call where the equivalent here is rlBegin, six vertices and rlEnd. If it cannot, because you need per-vertex colour or an arc raylib has no call for, this is the escape hatch and it is a small one.

Winding is not cosmetic, and getting it wrong is silent

rlgl culls back faces. A triangle wound the wrong way round is discarded before it reaches the screen, with no error and no warning, so the symptom is simply that nothing appears.

That is a hard bug to read, because it looks exactly like a geometry mistake. draw-ring here shipped with the wrong winding and drew nothing at all in the two scenes that used it. The analog clock went out without the bezel it was written to have, and a screenshot of its sixty tick marks was mistaken for the missing ring, so the function was reported as working for a day.

The check is the sign of the cross product of a triangle's first two edges:

v1 = p2 - p1
v2 = p3 - p1
cross = v1.x * v2.y - v1.y * v2.x

In this project's screen coordinates, with y growing downward, the triangles that survive have a negative cross. draw-line-ex produces -2hL for a horizontal line and always drew. draw-ring produced +8000 for its first segment and never did. Swapping two vertices in each triangle fixed both callers at once.

If an rlgl shape does not appear, check the winding before the arithmetic. The arithmetic tends to be right, because it is the part you can reason about from a diagram, and the winding is the part that has no visible symptom until it is wrong.

It happened four times. The fourth was the resize handle's corner triangle, written out by hand like the others and culled like the others, and that is when draw-triangle was added: it computes the cross product itself and swaps the last two points when the sign is wrong. One comparison per triangle against a bug that costs a rebuild and a device round-trip every time it appears. Prefer it over writing three vertices yourself.

The first three. Once in draw-ring, once avoided in draw-gradient-quad only because the ring bug was fresh, and once again in the ring scene's own outline, where the six vertices of a thick line were written out afresh and two of them swapped. That third one is the useful lesson: the correct order already existed in draw-line-ex and should have been copied rather than rederived. Winding is not something to work out again each time you write it.

Helitorus hit a second form of this. It first toggled culling around its triangles, and the toggle did nothing, because rlgl draws at the batch flush and not at rlEnd. By the time the flush ran, the state set around the immediate-mode calls was back to what it had been. Nothing in the project binds culling now. Helitorus now emits the front winding rlgl keeps, checking each triangle by its own sign, and so draws at every detail.

Drawing 3D in software

No 3D mode is bound, so the 3D scenes project every vertex themselves in net.b12n.raylib-ios.soft3d and send flat triangles and lines through net.b12n.raylib-ios.host/draw-3d!. Three rules matter. There is no depth buffer, so finish paints the grid and under-lines first, then the faces far to near by mean depth, then the wires. That painter order is wrong where a grid line should cross a cube's lower half, and the cube scenes' rows in raylib-ios-demo's scene catalog say so. Every triangle is built with the front winding rlgl keeps, decided by the sign of its screen-space cross product, because rlgl culls at the batch flush and nothing after that can fix a wrong winding. And a face with any corner behind the near plane is dropped while a line is clipped to it, so nothing wraps through infinity. fit-camera widens the original's fovy in a field narrower than 800 by 450, which keeps the original's horizontal view in a portrait phone.

Cost

Cheaper than it looks. The colour wheel is 540 vertices a frame and holds 59 fps; the pie chart is 186 and holds 58. Compare that against performance on a phone, where a scene making 2400 separate FFI calls a frame also holds 59: the per-call overhead is not what limits these scenes, and batched vertices are further from the limit still.