I have some functions and structs for loading and drawing an image with OpenGL. The two main places I would like feedback are: 1. I've tried to separate the OpenGL-specific code into the implementation so the caller doesn't have to know what rendering API I'm using. Can I do a better job of separating the OpenGL-specific code from the code necessary to specify the image? 2. I'm not happy with my `convert_pixels_to_render_coordinates` function. It works, it's just klunky. Passing 7 floats is just begging for mis-use. Is there any way I can simplify things, or make it easier to use? I show my usage at the end. image.h ```c #ifndef IMAGE_H #define IMAGE_H struct rectangle { float x, y, width, height; }; void rectangle_vertices(const struct rectangle *rectangle, float *vertices); /* * Calculate the y_start and height of an image in * render coordinates by "glueing" two points on * the image to two points on the coordinate system */ void convert_pixels_to_render_coordinates( float image_height, float px_0, float px_1, float coord_0, float coord_1, float *y_offset, float *height); struct image { struct rectangle rect; void *render_data; }; void set_position(struct image *image, float new_x, float new_y); void load_image(struct image *image, const char *pathname); void render_image(struct image *image); #endif ``` image.c ```c include "image.h" #include "gl_header.h" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include <stdbool.h> void rectangle_vertices(const struct rectangle *rectangle, float *vertices) { vertices[0] = rectangle->x; vertices[1] = rectangle->y; vertices[2] = rectangle->x + rectangle->width; vertices[3] = rectangle->y; vertices[4] = rectangle->x + rectangle->width; vertices[5] = rectangle->y - rectangle->height; vertices[6] = rectangle->x; vertices[7] = rectangle->y - rectangle->height; } void convert_pixels_to_render_coordinates( float image_dim, float px_0, float px_1, float coord_0, float coord_1, float *offset, float *length) { const float px_delta = px_1 - px_0; const float coord_delta = coord_0 - coord_1; /* Get the size of the image in render coordinates by solving the equation px_delta *length ----------- = ------------- image_dim coord_delta */ *length = image_dim * coord_delta / px_delta; /* Solving this equation gives us the offset to our first point: image_dim offset ----------- = -------- px_0 *length And then we need to apply the coord offset */ *offset = coord_0 + (px_0 * (*length) / image_dim); } static const struct rectangle FULL_UV_COORDS = {0.f, 0.f, 1.f, 1.f}; struct image_impl { GLuint texture; GLuint buffers[2]; }; #define VERTEX_BUFFER buffers[0] #define UV_BUFFER buffers[1] void load_image(struct image *image, const char *pathname) { stbi_set_flip_vertically_on_load(true); image->render_data = malloc(sizeof(struct image_impl)); if (!image->render_data) { fprintf(stderr, "Out of memory\n"); exit(1); } struct image_impl *impl = (struct image_impl *)image->render_data; glGenBuffers(2, impl->buffers); int width_px, height_px, channels; unsigned char *pixels = stbi_load(pathname, &width_px, &height_px, &channels, 0); set_position(image, image->rect.x, image->rect.y); const struct rectangle *texture_rect = &FULL_UV_COORDS; GLfloat note_uv[8] = {}; rectangle_vertices(texture_rect, note_uv); glBindBuffer(GL_ARRAY_BUFFER, impl->UV_BUFFER); glBufferData(GL_ARRAY_BUFFER, sizeof(note_uv), note_uv, GL_STATIC_DRAW); glGenTextures(1, &impl->texture); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, impl->texture); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 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_RGBA, width_px, height_px, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); glGenerateMipmap(GL_TEXTURE_2D); stbi_image_free(pixels); } void render_image(struct image *image) { struct image_impl *impl = (struct image_impl *)image->render_data; glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, impl->texture); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, impl->VERTEX_BUFFER); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, impl->UV_BUFFER); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); glDisableVertexAttribArray(0); glDisableVertexAttribArray(1); } void set_position(struct image *image, float new_x, float new_y) { image->rect.x = new_x; image->rect.y = new_y; struct image_impl *impl = (struct image_impl *)image->render_data; GLfloat note_vertices[8] = {}; rectangle_vertices(&image->rect, note_vertices); glBindBuffer(GL_ARRAY_BUFFER, impl->VERTEX_BUFFER); glBufferData(GL_ARRAY_BUFFER, sizeof(note_vertices), note_vertices, GL_STATIC_DRAW); } #undef VERTEX_BUFFER #undef UV_BUFFER ``` Usage: ```c convert_pixels_to_render_coordinates( 256, 0.f, 74.f, .5f, .45f, &state->bass_clef.rect.y, &state->bass_clef.rect.height); state->bass_clef.rect.x = -1.f; state->bass_clef.rect.width = .1f; load_image(&state->bass_clef, "./res/bass-clef.png"); ``` And to try to explain where all these numbers are coming from, here's a (marked) image that I'm trying to load with this: [![example bass clef with markings][1]][1] The two red dots are the y-coordinates (0.f, 74.f) where the clef needs to intersect lines on the staff. The .5f and .45f are the y-values in OpenGL coordinates [-1..1] of the lines on the staff those dots need to touch. From this we calculate the y offset and height of the image rectangle for the clef. [1]: https://i.sstatic.net/IP9N6.png