// Traditional OpenGL "Immediate Mode" (glBegin/glEnd) is slow for many objects // because every function call creates CPU overhead. Instead, we: // 1. Calculate all vertex data (Position, Color, UV) on the CPU. // 2. Store them in a contiguous memory buffer (Batch). // 3. Send the entire buffer to the GPU in a single call (glDrawArrays). #include "all.h" #include #include #include #include #define NONSTD_IMPLEMENTATION #include "libraries/nonstd.h" Game game = {0}; // A packed structure representing a single vertex. // The GPU needs to know the "stride" (size) of this struct to jump between vertices. typedef struct { float x, y, z; // Position float r, g, b; // Color float u, v; // Texture Coordinates (UV) } Vertex; // Batching using nonstd.h dynamic arrays. // This allows us to push an arbitrary number of vertices without manual reallocs. typedef array(Vertex) Batch; Batch textured_batch = {0}; // Grouping textured faces to minimize state changes Batch colored_batch = {0}; // Grouping solid color faces // Rodrigues' rotation formula. // Since we are batching, we can't use glRotatef (which modifies the global matrix). // We must rotate every vertex manually on the CPU before pushing it to the buffer. Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) { float rad = angle * M_PI / 180.0f; float c = cosf(rad); float s = sinf(rad); float len = sqrtf(axis.x*axis.x + axis.y*axis.y + axis.z*axis.z); if (len > 0) { axis.x /= len; axis.y /= len; axis.z /= len; } float x = axis.x, y = axis.y, z = axis.z; float dot = v.x*x + v.y*y + v.z*z; Vec3 cross = { y*v.z - z*v.y, z*v.x - x*v.z, x*v.y - y*v.x }; return (Vec3){ v.x*c + cross.x*s + x*dot*(1-c), v.y*c + cross.y*s + y*dot*(1-c), v.z*c + cross.z*s + z*dot*(1-c) }; } void init_texture(void) { Image img = read_ppm_image("textures/test.ppm"); if (!img.pixels) return; glGenTextures(1, &game.texture_id); glBindTexture(GL_TEXTURE_2D, game.texture_id); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.width, img.height, 0, GL_RGB, GL_UNSIGNED_BYTE, img.pixels); FREE(img.pixels); } void draw_text(float x, float y, void* font, const char* string, Vec3 color) { glColor3f(color.x, color.y, color.z); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, 1, 0, 1, -1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); glRasterPos2f(x, y); for (const char* c = string; *c != '\0'; c++) { glutBitmapCharacter(font, *c); } glPopMatrix(); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); } void batch_push_vertex(Batch* batch, Vertex v) { array_push(*batch, v); } // Takes object-space coordinates and transforms them into world-space. // Vertices are then sorted into either the textured or colored batch. void batch_push_cube(Vec3 pos, float size, float angle, Vec3 axis) { float h = size / 2.0f; Vec3 corners[8] = { {-h, -h, h}, { h, -h, h}, { h, h, h}, {-h, h, h}, // Front {-h, -h, -h}, { h, -h, -h}, { h, h, -h}, {-h, h, -h} // Back }; // Transform vertices (Rotation -> Translation) for (int i = 0; i < 8; i++) { if (angle != 0.0f) corners[i] = vec3_rotate(corners[i], angle, axis); corners[i].x += pos.x; corners[i].y += pos.y; corners[i].z += pos.z; } // Pushing Front and Top faces to the TEXTURED batch Vec3 white = {1,1,1}; batch_push_vertex(&textured_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, white.x, white.y, white.z, 0, 0}); batch_push_vertex(&textured_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, white.x, white.y, white.z, 1, 0}); batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1}); batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 0, 1}); batch_push_vertex(&textured_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, white.x, white.y, white.z, 0, 0}); batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 1, 0}); batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1}); batch_push_vertex(&textured_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, white.x, white.y, white.z, 0, 1}); // Pushing remaining faces to the COLORED batch Vec3 yellow = {1,1,0}; batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, yellow.x, yellow.y, yellow.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, yellow.x, yellow.y, yellow.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, yellow.x, yellow.y, yellow.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, yellow.x, yellow.y, yellow.z, 0, 0}); Vec3 magenta = {1,0,1}; batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, magenta.x, magenta.y, magenta.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, magenta.x, magenta.y, magenta.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, magenta.x, magenta.y, magenta.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, magenta.x, magenta.y, magenta.z, 0, 0}); Vec3 red = {1,0,0}; batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, red.x, red.y, red.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, red.x, red.y, red.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, red.x, red.y, red.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, red.x, red.y, red.z, 0, 0}); Vec3 cyan = {0,1,1}; batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, cyan.x, cyan.y, cyan.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, cyan.x, cyan.y, cyan.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, cyan.x, cyan.y, cyan.z, 0, 0}); batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, cyan.x, cyan.y, cyan.z, 0, 0}); } // The main render call that talks to the GPU. void batch_render() { // Enable client-side arrays (tells OpenGL we are passing memory pointers) glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); // 1. Render all TEXTURED geometry if (textured_batch.length > 0) { glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, game.texture_id); Vertex* v = textured_batch.data; // Pointer math: specify (size, type, stride, offset) glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x); glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r); glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u); glDrawArrays(GL_QUADS, 0, textured_batch.length); glDisable(GL_TEXTURE_2D); } // 2. Render all COLORED geometry if (colored_batch.length > 0) { Vertex* v = colored_batch.data; glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x); glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r); glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u); glDrawArrays(GL_QUADS, 0, colored_batch.length); } glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); // Clear length (re-use capacity) for the next frame array_clear(textured_batch); array_clear(colored_batch); } void render_display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // Render Batch of Cubes for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { float x = 0.1f + i * 0.1f; float y = 0.1f + j * 0.1f; float angle = game.cube_angle + (i + j) * 10.0f; batch_push_cube((Vec3){x, y, 0.0f}, 0.05f, angle, (Vec3){1.0f, 1.0f, 1.0f}); } } batch_render(); // Render UI Text char fps_text[32]; snprintf(fps_text, sizeof fps_text, "FPS: %d", game.target_fps); draw_text(0.01f, 0.95f, GLUT_BITMAP_HELVETICA_18, fps_text, COLOR_GREEN); glutSwapBuffers(); } void timer(int value) { (void)value; int current_time = glutGet(GLUT_ELAPSED_TIME); float delta_time = (current_time - game.last_time) / 1000.0f; game.last_time = current_time; // Rotate 120 degrees per second game.cube_angle += 120.0f * delta_time; if (game.cube_angle > 360) game.cube_angle -= 360; glutPostRedisplay(); glutTimerFunc(1000 / game.target_fps, timer, 0); } void init_display(void) { glClearColor(0.4, 0.4, 1.0, 0.0); glEnable(GL_DEPTH_TEST); glMatrixMode(GL_PROJECTION); glLoadIdentity(); int w = game.width; int h = game.height; if (h == 0) h = 1; float aspect = (float)w / (float)h; if (w >= h) { glOrtho(0.5 - 0.5 * aspect, 0.5 + 0.5 * aspect, 0.0, 1.0, -1.0, 1.0); } else { glOrtho(0.0, 1.0, 0.5 - 0.5 / aspect, 0.5 + 0.5 / aspect, -1.0, 1.0); } } void handle_reshape(int w, int h) { if (h == 0) h = 1; glViewport (0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); float aspect = (float)w / (float)h; if (w >= h) { glOrtho(0.5 - 0.5 * aspect, 0.5 + 0.5 * aspect, 0.0, 1.0, -1.0, 1.0); } else { glOrtho(0.0, 1.0, 0.5 - 0.5 / aspect, 0.5 + 0.5 / aspect, -1.0, 1.0); } glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } int main(int argc, char** argv) { static struct option long_options[] = { {"fps", required_argument, 0, 'f'}, {"width", required_argument, 0, 'w'}, {"height", required_argument, 0, 'h'}, {0, 0, 0, 0} }; game.width = 800; game.height = 600; game.target_fps = 60; int opt; int option_index = 0; while ((opt = getopt_long_only(argc, argv, "m:f:w:h:", long_options, &option_index)) != -1) { switch (opt) { case 'f': game.target_fps = atoi(optarg); break; case 'w': game.width = atoi(optarg); break; case 'h': game.height = atoi(optarg); break; } } glutInit(&argc, argv); int x = (glutGet(GLUT_SCREEN_WIDTH) - game.width) / 2; int y = (glutGet(GLUT_SCREEN_HEIGHT) - game.height) / 2; glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutInitWindowSize(game.width, game.height); glutInitWindowPosition(x, y); glutCreateWindow("floating"); init_display(); init_texture(); game.last_time = glutGet(GLUT_ELAPSED_TIME); glutDisplayFunc(render_display); glutReshapeFunc(handle_reshape); glutTimerFunc(1000 / game.target_fps, timer, 0); glutMouseFunc(handle_mouse_event); glutKeyboardFunc(handle_keyboard_event); glutMainLoop(); return 0; }