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}