/* * azgl: see azgl.h. Vertices are transformed on the CPU by the current projection and modelview * matrices and drawn in clip space by one shader. Primitives WebGL lacks become triangles (GL_QUADS, * GL_QUAD_STRIP, GL_POLYGON), and lines become quads so that glLineWidth works at any size, as it * does on desktop OpenGL. Display lists are recorded as a list of calls and replayed. * * Copyright 2026 FredyGames. Part of the Azimuth build for fredygames.com; distributed with Azimuth * under the GNU General Public License, version 3 or (at your option) any later version. */ #define AZGL_IMPLEMENTATION #include "azgl.h" #include #include #include #include /*===========================================================================*/ // Matrices (column-major, as in OpenGL): typedef struct { float m[16]; } mat4; #define STACK_DEPTH 32 static mat4 modelview_stack[STACK_DEPTH], projection_stack[STACK_DEPTH]; static int modelview_top = 0, projection_top = 0; static GLenum matrix_mode = GL_MODELVIEW; static int initialized = 0; static void identity(mat4 *a) { memset(a, 0, sizeof(*a)); a->m[0] = a->m[5] = a->m[10] = a->m[15] = 1.0f; } static mat4 *current(void) { return matrix_mode == GL_PROJECTION ? &projection_stack[projection_top] : &modelview_stack[modelview_top]; } // *a = *a * b static void multiply(mat4 *a, const float *b) { mat4 r; for (int col = 0; col < 4; ++col) { for (int row = 0; row < 4; ++row) { float sum = 0.0f; for (int k = 0; k < 4; ++k) sum += a->m[k * 4 + row] * b[col * 4 + k]; r.m[col * 4 + row] = sum; } } *a = r; } static void init(void) { if (initialized) return; initialized = 1; identity(&modelview_stack[0]); identity(&projection_stack[0]); } /*===========================================================================*/ // Display lists: enum { OP_BEGIN, OP_END, OP_VERTEX, OP_COLOR, OP_PUSH, OP_POP, OP_IDENTITY, OP_MODE, OP_TRANSLATE, OP_SCALE, OP_ROTATE, OP_MULT, OP_CALL, OP_LINEWIDTH }; typedef struct { int op; float v[4]; } call_t; typedef struct { call_t *calls; int count, capacity; int defined; } list_t; static list_t *lists = NULL; static GLuint num_lists = 0; static list_t *recording = NULL; static int execute_while_recording = 0; static void record(int op, float a, float b, float c, float d) { if (recording->count == recording->capacity) { recording->capacity = recording->capacity ? recording->capacity * 2 : 64; recording->calls = realloc(recording->calls, recording->capacity * sizeof(call_t)); } call_t *call = &recording->calls[recording->count++]; call->op = op; call->v[0] = a; call->v[1] = b; call->v[2] = c; call->v[3] = d; } // Records the call when a list is being compiled; returns whether to also run it now. #define RECORD(op, a, b, c, d) \ do { if (recording != NULL) { record((op), (a), (b), (c), (d)); \ if (!execute_while_recording) return; } } while (0) GLuint azgl_GenLists(GLsizei range) { const GLuint first = num_lists + 1; num_lists += range; lists = realloc(lists, (num_lists + 1) * sizeof(list_t)); memset(&lists[first], 0, range * sizeof(list_t)); return first; } GLboolean azgl_IsList(GLuint list) { return list >= 1 && list <= num_lists && lists[list].defined; } void azgl_NewList(GLuint list, GLenum mode) { if (list < 1 || list > num_lists) return; recording = &lists[list]; recording->count = 0; recording->defined = 1; execute_while_recording = (mode == GL_COMPILE_AND_EXECUTE); } void azgl_EndList(void) { recording = NULL; } /*===========================================================================*/ // Matrix calls: void azgl_MatrixMode(GLenum mode) { init(); RECORD(OP_MODE, mode, 0, 0, 0); matrix_mode = mode; } void azgl_PushMatrix(void) { init(); RECORD(OP_PUSH, 0, 0, 0, 0); if (matrix_mode == GL_PROJECTION) { if (projection_top + 1 < STACK_DEPTH) { projection_stack[projection_top + 1] = projection_stack[projection_top]; ++projection_top; } } else if (modelview_top + 1 < STACK_DEPTH) { modelview_stack[modelview_top + 1] = modelview_stack[modelview_top]; ++modelview_top; } } void azgl_PopMatrix(void) { init(); RECORD(OP_POP, 0, 0, 0, 0); if (matrix_mode == GL_PROJECTION) { if (projection_top > 0) --projection_top; } else if (modelview_top > 0) { --modelview_top; } } void azgl_LoadIdentity(void) { init(); RECORD(OP_IDENTITY, 0, 0, 0, 0); identity(current()); } void azgl_Translate(double x, double y, double z) { init(); RECORD(OP_TRANSLATE, x, y, z, 0); mat4 t; identity(&t); t.m[12] = x; t.m[13] = y; t.m[14] = z; multiply(current(), t.m); } void azgl_Scale(double x, double y, double z) { init(); RECORD(OP_SCALE, x, y, z, 0); mat4 s; identity(&s); s.m[0] = x; s.m[5] = y; s.m[10] = z; multiply(current(), s.m); } void azgl_Rotate(double angle, double x, double y, double z) { init(); RECORD(OP_ROTATE, angle, x, y, z); const double len = sqrt(x * x + y * y + z * z); if (len == 0.0) return; x /= len; y /= len; z /= len; const double rad = angle * 3.14159265358979323846 / 180.0, c = cos(rad), s = sin(rad), ic = 1.0 - c; mat4 r; identity(&r); r.m[0] = x * x * ic + c; r.m[4] = x * y * ic - z * s; r.m[8] = x * z * ic + y * s; r.m[1] = y * x * ic + z * s; r.m[5] = y * y * ic + c; r.m[9] = y * z * ic - x * s; r.m[2] = x * z * ic - y * s; r.m[6] = y * z * ic + x * s; r.m[10] = z * z * ic + c; multiply(current(), r.m); } void azgl_MultMatrixf(const GLfloat *m) { init(); if (recording != NULL) { // A matrix doesn't fit in one call; record it as four rows of the same op. for (int i = 0; i < 4; ++i) record(OP_MULT, m[i * 4], m[i * 4 + 1], m[i * 4 + 2], m[i * 4 + 3]); if (!execute_while_recording) return; } multiply(current(), m); } void azgl_Ortho(double l, double r, double b, double t, double n, double f) { init(); mat4 o; identity(&o); o.m[0] = 2.0 / (r - l); o.m[5] = 2.0 / (t - b); o.m[10] = -2.0 / (f - n); o.m[12] = -(r + l) / (r - l); o.m[13] = -(t + b) / (t - b); o.m[14] = -(f + n) / (f - n); multiply(current(), o.m); } /*===========================================================================*/ // Drawing: typedef struct { float x, y, r, g, b, a; } vertex_t; static GLenum begin_mode = 0; static float color[4] = {1.0f, 1.0f, 1.0f, 1.0f}; static vertex_t *verts = NULL, *tris = NULL; static int num_verts = 0, verts_capacity = 0, num_tris = 0, tris_capacity = 0; static float line_width = 1.0f; static GLint viewport[4] = {0, 0, 1, 1}; static GLuint program = 0, buffer = 0; static GLint attr_position, attr_color; void azgl_LineWidth(GLfloat width) { RECORD(OP_LINEWIDTH, width, 0, 0, 0); line_width = width > 0.0f ? width : 1.0f; } void azgl_Viewport(GLint x, GLint y, GLsizei w, GLsizei h) { viewport[0] = x; viewport[1] = y; viewport[2] = w > 0 ? w : 1; viewport[3] = h > 0 ? h : 1; glViewport(x, y, w, h); } // Only the capabilities WebGL has; the smoothing hints of desktop GL are left to the canvas's own // antialiasing. void azgl_Enable(GLenum cap) { if (cap == GL_BLEND || cap == GL_SCISSOR_TEST || cap == GL_DEPTH_TEST) glEnable(cap); } void azgl_Disable(GLenum cap) { if (cap == GL_BLEND || cap == GL_SCISSOR_TEST || cap == GL_DEPTH_TEST) glDisable(cap); } void azgl_Color4f(GLfloat r, GLfloat g, GLfloat b, GLfloat a) { RECORD(OP_COLOR, r, g, b, a); color[0] = r; color[1] = g; color[2] = b; color[3] = a; } void azgl_Begin(GLenum mode) { init(); RECORD(OP_BEGIN, mode, 0, 0, 0); begin_mode = mode; num_verts = 0; } void azgl_Vertex2f(GLfloat x, GLfloat y) { RECORD(OP_VERTEX, x, y, 0, 0); if (num_verts == verts_capacity) { verts_capacity = verts_capacity ? verts_capacity * 2 : 256; verts = realloc(verts, verts_capacity * sizeof(vertex_t)); } // Clip-space position: projection * modelview * (x, y, 0, 1). The game draws in 2D with an // orthographic projection, so w stays 1. const float *mv = modelview_stack[modelview_top].m, *p = projection_stack[projection_top].m; const float ex = mv[0] * x + mv[4] * y + mv[12]; const float ey = mv[1] * x + mv[5] * y + mv[13]; const float ez = mv[2] * x + mv[6] * y + mv[14]; const float ew = mv[3] * x + mv[7] * y + mv[15]; vertex_t *v = &verts[num_verts++]; const float cw = p[3] * ex + p[7] * ey + p[11] * ez + p[15] * ew; v->x = (p[0] * ex + p[4] * ey + p[8] * ez + p[12] * ew) / cw; v->y = (p[1] * ex + p[5] * ey + p[9] * ez + p[13] * ew) / cw; v->r = color[0]; v->g = color[1]; v->b = color[2]; v->a = color[3]; } static void emit(const vertex_t *v) { if (num_tris == tris_capacity) { tris_capacity = tris_capacity ? tris_capacity * 2 : 1024; tris = realloc(tris, tris_capacity * sizeof(vertex_t)); } tris[num_tris++] = *v; } static void emit_triangle(const vertex_t *a, const vertex_t *b, const vertex_t *c) { emit(a); emit(b); emit(c); } // A line from a to b as a quad line_width pixels wide (the viewport converts pixels to clip space). static void emit_line(const vertex_t *a, const vertex_t *b) { const float px = 2.0f / viewport[2], py = 2.0f / viewport[3]; const float dx = (b->x - a->x) / px, dy = (b->y - a->y) / py; const float len = sqrtf(dx * dx + dy * dy); if (len == 0.0f) return; const float half = line_width * 0.5f; const float ox = -dy / len * half * px, oy = dx / len * half * py; vertex_t a1 = *a, a2 = *a, b1 = *b, b2 = *b; a1.x += ox; a1.y += oy; a2.x -= ox; a2.y -= oy; b1.x += ox; b1.y += oy; b2.x -= ox; b2.y -= oy; emit_triangle(&a1, &b1, &b2); emit_triangle(&a1, &b2, &a2); } // A point as a square line_width pixels wide (WebGL point sizes vary between browsers). static void emit_point(const vertex_t *v) { const float hx = line_width / viewport[2], hy = line_width / viewport[3]; vertex_t c[4]; for (int i = 0; i < 4; ++i) c[i] = *v; c[0].x -= hx; c[0].y -= hy; c[1].x += hx; c[1].y -= hy; c[2].x += hx; c[2].y += hy; c[3].x -= hx; c[3].y += hy; emit_triangle(&c[0], &c[1], &c[2]); emit_triangle(&c[0], &c[2], &c[3]); } static GLuint compile(GLenum type, const char *source) { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, NULL); glCompileShader(shader); return shader; } static void setup(void) { if (program != 0) return; program = glCreateProgram(); glAttachShader(program, compile(GL_VERTEX_SHADER, "attribute vec2 position; attribute vec4 color; varying vec4 v_color;\n" "void main() { v_color = color; gl_Position = vec4(position, 0.0, 1.0); }\n")); glAttachShader(program, compile(GL_FRAGMENT_SHADER, "precision mediump float; varying vec4 v_color;\n" "void main() { gl_FragColor = v_color; }\n")); glLinkProgram(program); attr_position = glGetAttribLocation(program, "position"); attr_color = glGetAttribLocation(program, "color"); glGenBuffers(1, &buffer); } void azgl_End(void) { RECORD(OP_END, 0, 0, 0, 0); const int n = num_verts; num_tris = 0; switch (begin_mode) { case GL_POINTS: for (int i = 0; i < n; ++i) emit_point(&verts[i]); break; case GL_LINES: for (int i = 0; i + 1 < n; i += 2) emit_line(&verts[i], &verts[i + 1]); break; case GL_LINE_STRIP: for (int i = 0; i + 1 < n; ++i) emit_line(&verts[i], &verts[i + 1]); break; case GL_LINE_LOOP: for (int i = 0; i + 1 < n; ++i) emit_line(&verts[i], &verts[i + 1]); if (n > 2) emit_line(&verts[n - 1], &verts[0]); break; case GL_TRIANGLES: for (int i = 0; i + 2 < n; i += 3) emit_triangle(&verts[i], &verts[i + 1], &verts[i + 2]); break; case GL_TRIANGLE_STRIP: case GL_QUAD_STRIP: // a quad strip covers the same triangles as a triangle strip for (int i = 0; i + 2 < n; ++i) { if (i % 2 == 0) emit_triangle(&verts[i], &verts[i + 1], &verts[i + 2]); else emit_triangle(&verts[i + 1], &verts[i], &verts[i + 2]); } break; case GL_TRIANGLE_FAN: case GL_POLYGON: // the game's polygons are convex for (int i = 1; i + 1 < n; ++i) emit_triangle(&verts[0], &verts[i], &verts[i + 1]); break; case GL_QUADS: for (int i = 0; i + 3 < n; i += 4) { emit_triangle(&verts[i], &verts[i + 1], &verts[i + 2]); emit_triangle(&verts[i], &verts[i + 2], &verts[i + 3]); } break; } num_verts = 0; if (num_tris == 0) return; setup(); glUseProgram(program); glBindBuffer(GL_ARRAY_BUFFER, buffer); glBufferData(GL_ARRAY_BUFFER, num_tris * sizeof(vertex_t), tris, GL_STREAM_DRAW); glEnableVertexAttribArray(attr_position); glEnableVertexAttribArray(attr_color); glVertexAttribPointer(attr_position, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)0); glVertexAttribPointer(attr_color, 4, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)(2 * sizeof(float))); glDrawArrays(GL_TRIANGLES, 0, num_tris); } /*===========================================================================*/ void azgl_CallList(GLuint list) { RECORD(OP_CALL, list, 0, 0, 0); if (!azgl_IsList(list)) return; const list_t *l = &lists[list]; for (int i = 0; i < l->count; ++i) { const call_t *c = &l->calls[i]; switch (c->op) { case OP_BEGIN: azgl_Begin((GLenum)c->v[0]); break; case OP_END: azgl_End(); break; case OP_VERTEX: azgl_Vertex2f(c->v[0], c->v[1]); break; case OP_COLOR: azgl_Color4f(c->v[0], c->v[1], c->v[2], c->v[3]); break; case OP_PUSH: azgl_PushMatrix(); break; case OP_POP: azgl_PopMatrix(); break; case OP_IDENTITY: azgl_LoadIdentity(); break; case OP_MODE: azgl_MatrixMode((GLenum)c->v[0]); break; case OP_TRANSLATE: azgl_Translate(c->v[0], c->v[1], c->v[2]); break; case OP_SCALE: azgl_Scale(c->v[0], c->v[1], c->v[2]); break; case OP_ROTATE: azgl_Rotate(c->v[0], c->v[1], c->v[2], c->v[3]); break; case OP_MULT: { float m[16]; for (int row = 0; row < 4; ++row) memcpy(&m[row * 4], l->calls[i + row].v, 4 * sizeof(float)); i += 3; azgl_MultMatrixf(m); break; } case OP_CALL: azgl_CallList((GLuint)c->v[0]); break; case OP_LINEWIDTH: azgl_LineWidth(c->v[0]); break; } } }