main.c
raw
1// Traditional OpenGL "Immediate Mode" (glBegin/glEnd) is slow for many objects
2// because every function call creates CPU overhead. Instead, we:
3// 1. Calculate all vertex data (Position, Color, UV) on the CPU.
4// 2. Store them in a contiguous memory buffer (Batch).
5// 3. Send the entire buffer to the GPU in a single call (glDrawArrays).
6
7#include "all.h"
8
9#include <stdio.h>
10#include <stdlib.h>
11#include <getopt.h>
12#include <math.h>
13
14#define NONSTD_IMPLEMENTATION
15#include "libraries/nonstd.h"
16
17Game game = {0};
18
19// A packed structure representing a single vertex.
20// The GPU needs to know the "stride" (size) of this struct to jump between vertices.
21typedef struct {
22 float x, y, z; // Position
23 float r, g, b; // Color
24 float u, v; // Texture Coordinates (UV)
25} Vertex;
26
27// Batching using nonstd.h dynamic arrays.
28// This allows us to push an arbitrary number of vertices without manual reallocs.
29typedef array(Vertex) Batch;
30
31Batch textured_batch = {0}; // Grouping textured faces to minimize state changes
32Batch colored_batch = {0}; // Grouping solid color faces
33
34// Rodrigues' rotation formula.
35// Since we are batching, we can't use glRotatef (which modifies the global matrix).
36// We must rotate every vertex manually on the CPU before pushing it to the buffer.
37Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) {
38 float rad = angle * M_PI / 180.0f;
39 float c = cosf(rad);
40 float s = sinf(rad);
41
42 float len = sqrtf(axis.x*axis.x + axis.y*axis.y + axis.z*axis.z);
43 if (len > 0) { axis.x /= len; axis.y /= len; axis.z /= len; }
44
45 float x = axis.x, y = axis.y, z = axis.z;
46 float dot = v.x*x + v.y*y + v.z*z;
47 Vec3 cross = {
48 y*v.z - z*v.y,
49 z*v.x - x*v.z,
50 x*v.y - y*v.x
51 };
52
53 return (Vec3){
54 v.x*c + cross.x*s + x*dot*(1-c),
55 v.y*c + cross.y*s + y*dot*(1-c),
56 v.z*c + cross.z*s + z*dot*(1-c)
57 };
58}
59
60void init_texture(void) {
61 Image img = read_ppm_image("textures/test.ppm");
62 if (!img.pixels) return;
63
64 glGenTextures(1, &game.texture_id);
65 glBindTexture(GL_TEXTURE_2D, game.texture_id);
66
67 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
68 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
69
70 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.width, img.height, 0, GL_RGB, GL_UNSIGNED_BYTE, img.pixels);
71
72 FREE(img.pixels);
73}
74
75void draw_text(float x, float y, void* font, const char* string, Vec3 color) {
76 glColor3f(color.x, color.y, color.z);
77
78 glMatrixMode(GL_PROJECTION);
79 glPushMatrix();
80 glLoadIdentity();
81 glOrtho(0, 1, 0, 1, -1, 1);
82
83 glMatrixMode(GL_MODELVIEW);
84 glPushMatrix();
85 glLoadIdentity();
86
87 glRasterPos2f(x, y);
88 for (const char* c = string; *c != '\0'; c++) {
89 glutBitmapCharacter(font, *c);
90 }
91
92 glPopMatrix();
93 glMatrixMode(GL_PROJECTION);
94 glPopMatrix();
95 glMatrixMode(GL_MODELVIEW);
96}
97
98void batch_push_vertex(Batch* batch, Vertex v) {
99 array_push(*batch, v);
100}
101
102// Takes object-space coordinates and transforms them into world-space.
103// Vertices are then sorted into either the textured or colored batch.
104void batch_push_cube(Vec3 pos, float size, float angle, Vec3 axis) {
105 float h = size / 2.0f;
106 Vec3 corners[8] = {
107 {-h, -h, h}, { h, -h, h}, { h, h, h}, {-h, h, h}, // Front
108 {-h, -h, -h}, { h, -h, -h}, { h, h, -h}, {-h, h, -h} // Back
109 };
110
111 // Transform vertices (Rotation -> Translation)
112 for (int i = 0; i < 8; i++) {
113 if (angle != 0.0f) corners[i] = vec3_rotate(corners[i], angle, axis);
114 corners[i].x += pos.x;
115 corners[i].y += pos.y;
116 corners[i].z += pos.z;
117 }
118
119 // Pushing Front and Top faces to the TEXTURED batch
120 Vec3 white = {1,1,1};
121 batch_push_vertex(&textured_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, white.x, white.y, white.z, 0, 0});
122 batch_push_vertex(&textured_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, white.x, white.y, white.z, 1, 0});
123 batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1});
124 batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 0, 1});
125
126 batch_push_vertex(&textured_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, white.x, white.y, white.z, 0, 0});
127 batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 1, 0});
128 batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1});
129 batch_push_vertex(&textured_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, white.x, white.y, white.z, 0, 1});
130
131 // Pushing remaining faces to the COLORED batch
132 Vec3 yellow = {1,1,0};
133 batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, yellow.x, yellow.y, yellow.z, 0, 0});
134 batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, yellow.x, yellow.y, yellow.z, 0, 0});
135 batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, yellow.x, yellow.y, yellow.z, 0, 0});
136 batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, yellow.x, yellow.y, yellow.z, 0, 0});
137
138 Vec3 magenta = {1,0,1};
139 batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, magenta.x, magenta.y, magenta.z, 0, 0});
140 batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, magenta.x, magenta.y, magenta.z, 0, 0});
141 batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, magenta.x, magenta.y, magenta.z, 0, 0});
142 batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, magenta.x, magenta.y, magenta.z, 0, 0});
143
144 Vec3 red = {1,0,0};
145 batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, red.x, red.y, red.z, 0, 0});
146 batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, red.x, red.y, red.z, 0, 0});
147 batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, red.x, red.y, red.z, 0, 0});
148 batch_push_vertex(&colored_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, red.x, red.y, red.z, 0, 0});
149
150 Vec3 cyan = {0,1,1};
151 batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, cyan.x, cyan.y, cyan.z, 0, 0});
152 batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, cyan.x, cyan.y, cyan.z, 0, 0});
153 batch_push_vertex(&colored_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, cyan.x, cyan.y, cyan.z, 0, 0});
154 batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, cyan.x, cyan.y, cyan.z, 0, 0});
155}
156
157// The main render call that talks to the GPU.
158void batch_render() {
159 // Enable client-side arrays (tells OpenGL we are passing memory pointers)
160 glEnableClientState(GL_VERTEX_ARRAY);
161 glEnableClientState(GL_COLOR_ARRAY);
162 glEnableClientState(GL_TEXTURE_COORD_ARRAY);
163
164 // 1. Render all TEXTURED geometry
165 if (textured_batch.length > 0) {
166 glEnable(GL_TEXTURE_2D);
167 glBindTexture(GL_TEXTURE_2D, game.texture_id);
168
169 Vertex* v = textured_batch.data;
170 // Pointer math: specify (size, type, stride, offset)
171 glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x);
172 glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r);
173 glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u);
174
175 glDrawArrays(GL_QUADS, 0, textured_batch.length);
176 glDisable(GL_TEXTURE_2D);
177 }
178
179 // 2. Render all COLORED geometry
180 if (colored_batch.length > 0) {
181 Vertex* v = colored_batch.data;
182 glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x);
183 glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r);
184 glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u);
185
186 glDrawArrays(GL_QUADS, 0, colored_batch.length);
187 }
188
189 glDisableClientState(GL_VERTEX_ARRAY);
190 glDisableClientState(GL_COLOR_ARRAY);
191 glDisableClientState(GL_TEXTURE_COORD_ARRAY);
192
193 // Clear length (re-use capacity) for the next frame
194 array_clear(textured_batch);
195 array_clear(colored_batch);
196}
197
198
199void render_display(void) {
200 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
201
202 glMatrixMode(GL_MODELVIEW);
203 glLoadIdentity();
204
205 // Render Batch of Cubes
206 for (int i = 0; i < 10; i++) {
207 for (int j = 0; j < 10; j++) {
208 float x = 0.1f + i * 0.1f;
209 float y = 0.1f + j * 0.1f;
210 float angle = game.cube_angle + (i + j) * 10.0f;
211 batch_push_cube((Vec3){x, y, 0.0f}, 0.05f, angle, (Vec3){1.0f, 1.0f, 1.0f});
212 }
213 }
214 batch_render();
215
216 // Render UI Text
217 char fps_text[32];
218 snprintf(fps_text, sizeof fps_text, "FPS: %d", game.target_fps);
219 draw_text(0.01f, 0.95f, GLUT_BITMAP_HELVETICA_18, fps_text, COLOR_GREEN);
220
221 glutSwapBuffers();
222}
223
224void timer(int value) {
225 (void)value;
226
227 int current_time = glutGet(GLUT_ELAPSED_TIME);
228 float delta_time = (current_time - game.last_time) / 1000.0f;
229 game.last_time = current_time;
230
231 // Rotate 120 degrees per second
232 game.cube_angle += 120.0f * delta_time;
233 if (game.cube_angle > 360) game.cube_angle -= 360;
234
235 glutPostRedisplay();
236 glutTimerFunc(1000 / game.target_fps, timer, 0);
237}
238
239void init_display(void) {
240 glClearColor(0.4, 0.4, 1.0, 0.0);
241 glEnable(GL_DEPTH_TEST);
242 glMatrixMode(GL_PROJECTION);
243 glLoadIdentity();
244
245 int w = game.width;
246 int h = game.height;
247 if (h == 0) h = 1;
248 float aspect = (float)w / (float)h;
249 if (w >= h) {
250 glOrtho(0.5 - 0.5 * aspect, 0.5 + 0.5 * aspect, 0.0, 1.0, -1.0, 1.0);
251 } else {
252 glOrtho(0.0, 1.0, 0.5 - 0.5 / aspect, 0.5 + 0.5 / aspect, -1.0, 1.0);
253 }
254}
255
256void handle_reshape(int w, int h) {
257 if (h == 0) h = 1;
258 glViewport (0, 0, (GLsizei) w, (GLsizei) h);
259 glMatrixMode(GL_PROJECTION);
260 glLoadIdentity();
261
262 float aspect = (float)w / (float)h;
263 if (w >= h) {
264 glOrtho(0.5 - 0.5 * aspect, 0.5 + 0.5 * aspect, 0.0, 1.0, -1.0, 1.0);
265 } else {
266 glOrtho(0.0, 1.0, 0.5 - 0.5 / aspect, 0.5 + 0.5 / aspect, -1.0, 1.0);
267 }
268
269 glMatrixMode(GL_MODELVIEW);
270 glLoadIdentity();
271}
272
273int main(int argc, char** argv) {
274 static struct option long_options[] = {
275 {"fps", required_argument, 0, 'f'},
276 {"width", required_argument, 0, 'w'},
277 {"height", required_argument, 0, 'h'},
278 {0, 0, 0, 0}
279 };
280
281 game.width = 800;
282 game.height = 600;
283 game.target_fps = 60;
284
285 int opt;
286 int option_index = 0;
287 while ((opt = getopt_long_only(argc, argv, "m:f:w:h:", long_options, &option_index)) != -1) {
288 switch (opt) {
289 case 'f':
290 game.target_fps = atoi(optarg);
291 break;
292 case 'w':
293 game.width = atoi(optarg);
294 break;
295 case 'h':
296 game.height = atoi(optarg);
297 break;
298 }
299 }
300
301 glutInit(&argc, argv);
302
303 int x = (glutGet(GLUT_SCREEN_WIDTH) - game.width) / 2;
304 int y = (glutGet(GLUT_SCREEN_HEIGHT) - game.height) / 2;
305
306 glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
307 glutInitWindowSize(game.width, game.height);
308 glutInitWindowPosition(x, y);
309 glutCreateWindow("floating");
310
311 init_display();
312 init_texture();
313
314 game.last_time = glutGet(GLUT_ELAPSED_TIME);
315
316 glutDisplayFunc(render_display);
317 glutReshapeFunc(handle_reshape);
318 glutTimerFunc(1000 / game.target_fps, timer, 0);
319 glutMouseFunc(handle_mouse_event);
320 glutKeyboardFunc(handle_keyboard_event);
321
322 glutMainLoop();
323 return 0;
324}