make your code base a home base
a reasonable api for debug drawing with OpenGL
actually enjoy drawing points, lines, triangles, and quads
when i learned graphics in college, we used the legacy OpenGL glBegin(...) / glEnd() api
this was a good api for "debug drawing" -- not trying to draw stuff beautifully, just visualizing where data is in space :)
the implementation might have been slow because something something submitting vertices to the gpu one by one... -- but i'm talking about the api.
and this was a good api!
just look how easy it was to make a rainbow triangle!
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glEnd();
sidenote: add a little macro fun1, and you could even be living this life:
BOOKEND(glBegin(GL_TRIANGLES), glEnd()) {
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
}
unfortunately, modern OpenGL deprecated/removed this api and didn't provide anything similarly ergonomic to use to take its place. hello triangle ballooned to 196 lines and now required two different languages2
( and want to draw points? lines? line strips?? -- glhf :( )
this decision was, in my opinion, pedagogically disastrous; Khronos upgraded the ladder but forgot to include the bottom 10 rungs.
in my opinion, the old api had real value! it was sufficient for making Quake! plus a whole bunch of people (students) just want to draw something!
luckily, you can implement your own, even better version of the legacy OpenGL glBegin(...) / glEnd() api
i call mine eso, which stands for "Easy SOup" :)
like implementing your own printf, or defeating the machines, this primary barrier to implementing your own debug drawing api is believing such a thing is possible
luckily,
a simple reference implementation is available here, but the really important thing is the usage code
here is usage code of my current api ( it includes a hello triangle :D )
Void main(Void) {
...
while (begin_frame()) {
BOOKEND(eso_begin(...), eso_end()) {
eso_primitive(EsoPrimitiveTriangles);
eso_color_rgb(1.0, 0.0, 0.0);
eso_vertex_xyz(-0.5, -0.5, 0.0);
eso_color_rgb(0.0, 1.0, 0.0);
eso_vertex_xyz(0.5, -0.5, 0.0);
eso_color_rgb(0.0, 0.0, 1.0);
eso_vertex_xyz(0.0, 0.5, 0.0);
eso_sim(&sim, &state);
}
}
}
note that the function eso_sim(...) -- which draws my soft robot simulation -- is written so it can be called in the middle of an eso_begin(...) { ... } eso_end() block
Void eso_sim(Sim *sim, State *state) {
...
if (u64_is_on(state->enabled, SimTermCables)) {
eso_primitive(EsoPrimitiveLines);
eso_size(2.0);
VIEW_FOR(View_Uint *, cable, &sim->cables.outer) {
...
BOOKEND(eso_compound_begin(EsoCompoundLineStrip), eso_compound_end()) {
eso_color_r3(color_pastel_hot_purple);
VIEW_FOR(Uint *, i, cable) {
Real2 p_i = ...;
eso_vertex_r2(p_i);
}
}
}
}
if (u64_is_on(state->enabled, SimTermGravity)) {
eso_primitive(EsoPrimitivePoints);
eso_color_r3(color_monokai_orange);
eso_size(2.0);
VIEW_FOR(Real2 *, x, x_) {
eso_vertex_r2(*x);
}
}
...
}
in summary, in a single eso_begin / eso_end-block, we can
- draw multiple different primitives -- e.g., GL_TRIANGLES and GL_POINTS
- draw multiple "compound primitives" -- e.g., GL_LINE_STRIP
the only important implementation note is that nothing gets sent to the gpu until eso_end()
rather, eso_vertex*(...) buffers data in massive global arrays on the cpu
GL_Uint vertex_primitives[ESO_MAX_GL_VERTICES];
GL_Uint vertex_inner_indices[ESO_MAX_GL_VERTICES];
GL_Real3 vertex_positions[ESO_MAX_GL_VERTICES];
GL_Real4 vertex_colors[ESO_MAX_GL_VERTICES];
...
and eso_end() makes one massive glDrawArrays(GL_TRIANGLES, ...); call
final note: when drawing points, lines, and quads, eso_vertex*(...) duplicates data on the cpu -- duplicated vertex positions get offset later in a single combined vertex shader (to support compiling to wasm, i avoid geometry shaders)
1 Ryan Fleury calls this construct a "defer loop"; it executes BEFORE(...); { ... } AFTER(...); using a for loop and comma expressions
#define BOOKEND(BEFORE, AFTER) \
for ( \
int XCONCAT(_, __LINE__) = (BEFORE, 0); \
XCONCAT(_, __LINE__) == 0; \
(AFTER, XCONCAT(_, __LINE__) = 1) \
)
note: i'm naming the indexing variable using a typical two-layer "extended" concat macro
#define CONCAT(a, b) a ## b
#define XCONCAT(a, b) CONCAT(a, b)
2 unfortunately, Vulkan continued this trend, bringing a 1379 line hello triangle that requires 7 standard library headers plus the external glm math library D: ( the "hello world" of graphics programming is now longer than the longest program many undergrads ever write )