From b4768a885b20ad13f95788cb4b895fb07b3358a3 Mon Sep 17 00:00:00 2001 From: var Date: Wed, 1 Apr 2026 00:13:07 -0500 Subject: [PATCH] Basic 3D rendering --- .gitignore | 1 + res/glsl/fragment.glsl | 13 ++ res/glsl/vertex.glsl | 17 +++ res/tex/texture.png | Bin 0 -> 8578 bytes src/camera.c | 53 +++++++ src/camera.h | 19 +++ src/game.c | 37 ++--- src/game.h | 19 +++ src/input.c | 36 +++++ src/main.c | 2 + src/render.c | 336 +++++++++++++++++++++++++++++++++++++++++ src/render.h | 8 + src/shape.c | 83 ++++++++++ src/shape.h | 37 +++++ 14 files changed, 638 insertions(+), 23 deletions(-) create mode 100644 res/glsl/fragment.glsl create mode 100644 res/glsl/vertex.glsl create mode 100644 res/tex/texture.png create mode 100644 src/camera.c create mode 100644 src/camera.h create mode 100644 src/render.c create mode 100644 src/render.h create mode 100644 src/shape.c create mode 100644 src/shape.h diff --git a/.gitignore b/.gitignore index 678b679..dd476ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ obj/* Game +*.xcf diff --git a/res/glsl/fragment.glsl b/res/glsl/fragment.glsl new file mode 100644 index 0000000..b1c49d2 --- /dev/null +++ b/res/glsl/fragment.glsl @@ -0,0 +1,13 @@ +#version 450 core + +in vec3 textureCoord; + +out vec4 color; + +uniform sampler2DArray textureSampler; + +void main() +{ + color = texture(textureSampler, textureCoord); + if (color.w < 0.1) discard; +} diff --git a/res/glsl/vertex.glsl b/res/glsl/vertex.glsl new file mode 100644 index 0000000..b9aa740 --- /dev/null +++ b/res/glsl/vertex.glsl @@ -0,0 +1,17 @@ +#version 450 core + +layout (location = 0) in vec3 position; +layout (location = 1) in vec2 texCoord; +layout (location = 2) in float texIndex; +layout (location = 3) in mat4 theModelMatrix; + +out vec3 textureCoord; + +uniform mat4 theViewMatrix; +uniform mat4 theProjMatrix; + +void main() +{ + gl_Position = theProjMatrix * theViewMatrix * theModelMatrix * vec4(position, 1.0); + textureCoord = vec3(1.0 - texCoord.x, 1.0 - texCoord.y, texIndex); +} diff --git a/res/tex/texture.png b/res/tex/texture.png new file mode 100644 index 0000000000000000000000000000000000000000..58c3ab21ade96dfe3de117844a49f80b06c59611 GIT binary patch literal 8578 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 H`t|<-qJ^Q_ literal 0 HcmV?d00001 diff --git a/src/camera.c b/src/camera.c new file mode 100644 index 0000000..9a80666 --- /dev/null +++ b/src/camera.c @@ -0,0 +1,53 @@ +#include +#include "camera.h" + +void Camera_Init(Camera* c) +{ + c->position[0] = -50.0f; + c->position[1] = 130.0f; + c->position[2] = 0.0f; + c->width[0] = 0.6f; + c->width[1] = 5.8f; + c->width[2] = 0.6f; + glm_vec3_zero(c->velocity); + glm_vec3_zero(c->look); + glm_vec3_zero(c->up); + glm_vec3_zero(c->front); + glm_vec3_zero(c->right); + glm_vec3_zero(c->rotation); +} + +void Camera_GetViewMatrix(Camera* c, mat4 m) +{ + glm_lookat(c->position, c->look, c->up, m); +} + +void Camera_UpdateVectors(Camera* c) +{ + if (c->rotation[0] > 180.0f) c->rotation[0] -= 360.0f; + else if (c->rotation[0] <= -180.0f) c->rotation[0] += 360.0f; + + c->rotation[1] = glm_clamp(c->rotation[1], -89.0f, 89.0f); + c->rotation[2] = glm_clamp(c->rotation[2], -30.0f, 30.0f); + + // calculate the front facing unit vector by yaw and pitch + c->front[0] = cos(glm_rad(c->rotation[0])) * cos(glm_rad(c->rotation[1])); + c->front[1] = sin(glm_rad(c->rotation[1])); + c->front[2] = sin(glm_rad(c->rotation[0])) * cos(glm_rad(c->rotation[1])); + + // the look vector is the position + front + glm_vec3_add(c->position, c->front, c->look); + + // reset the up vector + c->up[0] = 0.0f; + c->up[1] = 1.0f; + c->up[2] = 0.0f; + // cross product points to the right, perpendicular to both front and up + glm_vec3_cross(c->front, c->up, c->right); + // pitch up or down, around the axis of the cross product + glm_vec3_rotate(c->up, glm_rad(c->rotation[1]), c->right); + // roll around the axis of the front vector + glm_vec3_rotate(c->up, glm_rad(c->rotation[2]), c->front); + // recalculate right vector after rolling + glm_vec3_cross(c->front, c->up, c->right); +} diff --git a/src/camera.h b/src/camera.h new file mode 100644 index 0000000..a8eefeb --- /dev/null +++ b/src/camera.h @@ -0,0 +1,19 @@ +#pragma once + +#include "cglm/cglm.h" + +typedef struct +{ + vec3 position; + vec3 width; + vec3 velocity; + vec3 look; + vec3 up; + vec3 front; + vec3 right; + vec3 rotation; +} Camera; + +void Camera_Init(Camera* c); +void Camera_GetViewMatrix(Camera* c, mat4 m); +void Camera_UpdateVectors(Camera* c); diff --git a/src/game.c b/src/game.c index 78b230f..554ebb2 100644 --- a/src/game.c +++ b/src/game.c @@ -1,32 +1,15 @@ #include #include -#include "SDL2/SDL.h" #include "SDL2/SDL_timer.h" -#include "GL/glew.h" -#include "SDL2/SDL_opengl.h" #include "game.h" +#include "render.h" +#include "camera.h" GameState *Game_New() { GameState *gs = calloc(1, sizeof(GameState)); - SDL_Init(SDL_INIT_VIDEO); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - - gs->window = SDL_CreateWindow("Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1920, 1080, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);; - gs->glContext = SDL_GL_CreateContext(gs->window); - printf("Created OpenGL window.\n"); - - glewInit(); - glEnable(GL_DEPTH_TEST); - glEnable(GL_CULL_FACE); - glEnable(GL_TEXTURE_2D); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - printf("Initialized OpenGL.\n"); + if (!Render_Init(gs)) return NULL; gs->running = true; return gs; @@ -34,12 +17,20 @@ GameState *Game_New() void Game_Update(GameState *gs) { - SDL_Delay(100); + vec3 move; + glm_vec3_zero(move); + if (gs->input.w) move[0] = 0.3f; + else if (gs->input.s) move[0] = -0.3f; + if (gs->input.a) move[1] = 0.3f; + else if (gs->input.d) move[1] = -0.3f; + glm_vec3_add(gs->camera.position, move, gs->camera.position); + + Camera_UpdateVectors(&gs->camera); + SDL_Delay(5); } void Game_Destroy(GameState *gs) { - SDL_GL_DeleteContext(gs->glContext); - SDL_DestroyWindow(gs->window); + Render_Destroy(gs); free(gs); } diff --git a/src/game.h b/src/game.h index 52ecff9..fb09d17 100644 --- a/src/game.h +++ b/src/game.h @@ -1,12 +1,31 @@ #pragma once #include +#include "SDL2/SDL.h" +#include "cglm/cglm.h" +#include "camera.h" +#include "shape.h" + +typedef struct +{ + bool w; + bool a; + bool s; + bool d; +} InputState; typedef struct { bool running; + InputState input; SDL_Window *window; SDL_GLContext *glContext; + GLuint textureId; + int shaderProgramId; + Camera camera; + Shape *testShape; + mat4 projMatrix; + mat4 viewMatrix; } GameState; GameState *Game_New(); diff --git a/src/input.c b/src/input.c index 7a1bd02..2dc003a 100644 --- a/src/input.c +++ b/src/input.c @@ -1,5 +1,6 @@ #include "SDL2/SDL.h" #include "input.h" +#include "camera.h" #include "game.h" static void HandleKeyDown(GameState *gs, SDL_KeyCode sym) @@ -21,11 +22,40 @@ static void HandleKeyDown(GameState *gs, SDL_KeyCode sym) SDL_SetWindowFullscreen(gs->window, windowFlags); } break; + + case SDLK_w: gs->input.w = true; break; + case SDLK_a: gs->input.a = true; break; + case SDLK_s: gs->input.s = true; break; + case SDLK_d: gs->input.d = true; break; } } static void HandleKeyUp(GameState *gs, SDL_KeyCode sym) { + switch (sym) + { + case SDLK_w: gs->input.w = false; break; + case SDLK_a: gs->input.a = false; break; + case SDLK_s: gs->input.s = false; break; + case SDLK_d: gs->input.d = false; break; + } +} + +static void HandleMouseMotion(SDL_MouseMotionEvent e, GameState* gs) +{ + const float sensitivity = 0.4f; + Camera *camera = &gs->camera; + camera->rotation[0] += e.xrel * sensitivity; + camera->rotation[1] -= e.yrel * sensitivity; +} + +static void HandleMouseDown(SDL_MouseButtonEvent e, GameState* gs) +{ + switch (e.button) + { + case 1: + break; + } } void Input_Poll(GameState *gs) @@ -49,6 +79,12 @@ void Input_Poll(GameState *gs) case SDL_KEYUP: HandleKeyUp(gs, sdlEvent.key.keysym.sym); break; + case SDL_MOUSEMOTION: + HandleMouseMotion(sdlEvent.motion, gs); + break; + case SDL_MOUSEBUTTONDOWN: + HandleMouseDown(sdlEvent.button, gs); + break; } } } diff --git a/src/main.c b/src/main.c index ac3e631..ff6f9de 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include "SDL2/SDL.h" #include "game.h" #include "input.h" +#include "render.h" int main(int argc, char* argv[]) { @@ -18,6 +19,7 @@ int main(int argc, char* argv[]) { Input_Poll(gs); Game_Update(gs); + Render_Draw(gs); } printf("Shutting down...\n"); diff --git a/src/render.c b/src/render.c new file mode 100644 index 0000000..387c7c5 --- /dev/null +++ b/src/render.c @@ -0,0 +1,336 @@ +#include +#include +#include +#include "SDL2/SDL.h" +#include "GL/glew.h" +#include "SDL2/SDL_opengl.h" +#define STB_IMAGE_IMPLEMENTATION +#include "stb/stb_image.h" +#include "cglm/cglm.h" +#include "render.h" +#include "camera.h" +#include "game.h" +#include "shape.h" + +static void LoadTextureArray(GLuint texture, const char* filePath, int nCols, int nRows) +{ + const int nMipmaps = 1; + int nTiles = nCols * nRows; + int tWidth, tHeight, tChannels; + + unsigned char* textureBytes = stbi_load(filePath, &tWidth, &tHeight, &tChannels, 0); + + tWidth /= nCols; + tHeight /= nRows; + + glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, tWidth, tHeight, nTiles, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + + int tBytes = 4 * tWidth * tHeight; + unsigned char* buffer = malloc(tBytes * sizeof(unsigned char)); + + for (int z = 0; z < nTiles; z++) + { + for (int b = 0; b < tBytes; b++) + { + int bWidth = 4 * tWidth; // width of each tile in bytes + int i = ((z / nCols) * nCols * bWidth * tHeight) // size (in bytes) of all tiles above + + ((b / bWidth) * bWidth * nCols) // size of pixel rows above, within the current tile row + + ((z % nCols) * bWidth) // width of tiles to the left + + (b % bWidth); // width from the left side of the current tile + + buffer[b] = textureBytes[i]; + } + + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, z, tWidth, tHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE, buffer); + } + + free(buffer); + + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + + stbi_image_free(textureBytes); +} + +static void LoadTexture(GameState* gs) +{ + glGenTextures(1, &gs->textureId); + glActiveTexture(GL_TEXTURE0); + glUniform1i(glGetUniformLocation(gs->shaderProgramId, "textureSampler"), 0); + LoadTextureArray(gs->textureId, "res/tex/texture.png", 8, 4); + printf("Loaded the texture array.\n"); + glBindTexture(GL_TEXTURE_2D, 0); +} + +static char **ReadFileLines(const char *path, int *n) +{ + const int MAX_LINES = 128; + const int MAX_LINE_LENGTH = 256; + + FILE *file = fopen(path, "r"); + char line[MAX_LINE_LENGTH]; + char **lines = calloc(MAX_LINES, sizeof(char*)); + *n = 0; + + if (lines == NULL) return NULL; + + while (fgets(&(line[0]), MAX_LINE_LENGTH, file) != NULL && *n < MAX_LINES) + { + GLchar *l = strdup(&(line[0])); + if (l == NULL) return NULL; + lines[*n] = l; + (*n)++; + } + + fclose(file); + int s = sizeof(char*) * (*n); + lines = realloc(lines, s); + + return lines; +} + +static void FreeLines(char **lines, int n) +{ + for (int i = 0; i < n; i++) + { + free(lines[i]); + } + + free(lines); +} + +static int CompileShader(GLchar *sourcePath, GLenum shaderType, char ***outCode, int *outNumLines) +{ + GLint success; + *outCode = ReadFileLines(sourcePath, outNumLines); + GLuint shaderId = glCreateShader(shaderType); + glShaderSource(shaderId, *outNumLines, (void*)(*outCode), NULL); + glCompileShader(shaderId); + glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success); + + if (!success) + { + GLchar log[512]; + glGetShaderInfoLog(shaderId, 512, NULL, log); + printf("%s\n", log); + return -1; + } + + return shaderId; +} + +static int LoadShaders() +{ + const char *vertShaderFilePath = "./res/glsl/vertex.glsl"; + char **vertCode = NULL; + int vertNumLines; + int vertShaderId = CompileShader(vertShaderFilePath, GL_VERTEX_SHADER, &vertCode, &vertNumLines); + if (vertShaderId < 0) return vertShaderId; + + const char *fragShaderFilePath = "./res/glsl/fragment.glsl"; + char **fragCode = NULL; + int fragNumLines; + int fragShaderId = CompileShader(fragShaderFilePath, GL_FRAGMENT_SHADER, &fragCode, &fragNumLines); + if (fragShaderId < 0 ) return fragShaderId; + + GLint success; + GLuint shaderProgramId = glCreateProgram(); + glAttachShader(shaderProgramId, vertShaderId); + glAttachShader(shaderProgramId, fragShaderId); + glLinkProgram(shaderProgramId); + glGetProgramiv(shaderProgramId, GL_LINK_STATUS, &success); + + if (!success) + { + GLchar log[512]; + glGetProgramInfoLog(shaderProgramId, 512, NULL, log); + printf("%s", log); + return -1; + } + + glDeleteShader(vertShaderId); + glDeleteShader(fragShaderId); + FreeLines(vertCode, vertNumLines); + FreeLines(fragCode, fragNumLines); + + return shaderProgramId; +} + +static void InitShapeBuffers(Shape *shape) +{ + glBindVertexArray(shape->VAO); + + // vertex buffer: contains vertex and texture coords + glBindBuffer(GL_ARRAY_BUFFER, shape->VBO); + glBufferData(GL_ARRAY_BUFFER, shape->numVertices * sizeof(GLfloat), shape->vertices, GL_STATIC_DRAW); + GLsizei stride = 5 * sizeof(GLfloat); + + // vertex coordinates + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (GLvoid*)0); + + // texture coordinates + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(3 * sizeof(GLfloat))); + + // instance buffer: contains per-instance texture index and model transformation matrix + // therefore each instance of the same shape, in the same draw call, can have a different texture and transformation + glBindBuffer(GL_ARRAY_BUFFER, shape->IBO); + stride = SHAPE_INSTANCE_SIZE; + + // texture index: basically another tex coord in the third dimension because a texture array is used + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, stride, (GLvoid*)0); + glVertexAttribDivisor(2, 1); + + // model matrix: contains 16 floats, each vert attrib has space for 4 floats, therefore it spans 4 attrib locations + glEnableVertexAttribArray(3); + glEnableVertexAttribArray(4); + glEnableVertexAttribArray(5); + glEnableVertexAttribArray(6); + glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(1 * sizeof(GLfloat))); + glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(5 * sizeof(GLfloat))); + glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(9 * sizeof(GLfloat))); + glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(13 * sizeof(GLfloat))); + glVertexAttribDivisor(3, 1); + glVertexAttribDivisor(4, 1); + glVertexAttribDivisor(5, 1); + glVertexAttribDivisor(6, 1); + + // index buffer: contains vertex indices, reducing the data to be buffered on the GPU + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, shape->EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, shape->numIndices * sizeof(GLushort), shape->indices, GL_STATIC_DRAW); + + glBindVertexArray(0); +} + +bool Render_Init(GameState *gs) +{ + SDL_Init(SDL_INIT_VIDEO); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + + gs->window = SDL_CreateWindow("Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1920, 1080, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);; + gs->glContext = SDL_GL_CreateContext(gs->window); + printf("Created OpenGL window.\n"); + + glewInit(); + gs->shaderProgramId = LoadShaders(); + + if (gs->shaderProgramId < 0) + { + printf("Failed to load shaders.\n"); + } + + glEnable(GL_DEPTH_TEST); + glEnable(GL_CULL_FACE); + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + printf("Initialized OpenGL.\n"); + + Shape *shape = Shape_MakePyramid(3); + gs->testShape = shape; + glGenVertexArrays(1, &shape->VAO); + glGenBuffers(1, &shape->VBO); + glGenBuffers(1, &shape->IBO); + glGenBuffers(1, &shape->EBO); + + LoadTexture(gs); + Camera_Init(&gs->camera); + + InitShapeBuffers(gs->testShape); +} + +void Render_Destroy(GameState *gs) +{ + SDL_GL_DeleteContext(gs->glContext); + SDL_DestroyWindow(gs->window); +} + +// Sets the GL viewport while maintaining aspect ratio. +// Calculates the projection and view matrices. +static void SetViewport(GameState *gs) +{ + const float ratio = 0.5625f; // 1080/1920 + int wWidth, wHeight, offsetX, offsetY; + SDL_GL_GetDrawableSize(gs->window, &wWidth, &wHeight); + + if ((float)wHeight / (float)wWidth > ratio) + { + int temp = (int)floorf(ratio * (float)wWidth); + offsetX = 0; + offsetY = (wHeight - temp) / 2; + wHeight = temp; + } + else + { + int temp = (int)floorf((float)wHeight / ratio); + offsetX = (wWidth - temp) / 2; + offsetY = 0; + wWidth = temp; + } + + glViewport(offsetX, offsetY, wWidth, wHeight); + + glm_mat4_identity(gs->projMatrix); + glm_perspective(45.0f, (GLfloat)wWidth / (GLfloat)wHeight, 0.1f, 2000.0f, gs->projMatrix); + + glm_mat4_identity(gs->viewMatrix); + Camera_GetViewMatrix(&gs->camera, gs->viewMatrix); +} + +void Render_Draw(GameState *gs) +{ + glClearColor(0.4f, 0.6f, 0.8f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glLoadIdentity(); + SetViewport(gs); + + glUseProgram(gs->shaderProgramId); + GLint projLoc = glGetUniformLocation(gs->shaderProgramId, "theProjMatrix"); + glUniformMatrix4fv(projLoc, 1, GL_FALSE, (void*)(gs->projMatrix)); + GLint viewLoc = glGetUniformLocation(gs->shaderProgramId, "theViewMatrix"); + glUniformMatrix4fv(viewLoc, 1, GL_FALSE, (void*)(gs->viewMatrix)); + glBindTexture(GL_TEXTURE_2D, gs->textureId); + vec3 scale = { 0, 0, 0 }; + + Shape *shape = gs->testShape; + glBindVertexArray(shape->VAO); + + // apply transformations to each instance + for (int j = 0; j < shape->numInstances; j++) + { + ShapeInstance* instance = shape->instances + j; + mat4* matrix = (void*)(shape->instanceData + (j * 17) + 1); + mat4 tempMat; + glm_mat4_identity(tempMat); + + glm_translate(tempMat, instance->position); + glm_rotate_x(tempMat, instance->rotation[0], tempMat); + glm_rotate_y(tempMat, instance->rotation[1], tempMat); + glm_rotate_z(tempMat, instance->rotation[2], tempMat); + + scale[0] = instance->scale; + scale[1] = instance->scale; + scale[2] = instance->scale; + glm_scale(tempMat, scale); + + memcpy(matrix, tempMat, sizeof(mat4)); + } + + // re-buffer the instance data because transformations may have changed + glBindBuffer(GL_ARRAY_BUFFER, shape->IBO); + glBufferData(GL_ARRAY_BUFFER, shape->numInstances * SHAPE_INSTANCE_SIZE, shape->instanceData, GL_DYNAMIC_DRAW); + + glDrawElementsInstanced(GL_TRIANGLES, shape->numIndices, GL_UNSIGNED_SHORT, 0, shape->numInstances); + + SDL_GL_SwapWindow(gs->window); +} diff --git a/src/render.h b/src/render.h new file mode 100644 index 0000000..cdba0ae --- /dev/null +++ b/src/render.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include "game.h" + +bool Render_Init(GameState *gs); +void Render_Destroy(GameState *gs); +void Render_Draw(GameState *gs); diff --git a/src/shape.c b/src/shape.c new file mode 100644 index 0000000..5c93668 --- /dev/null +++ b/src/shape.c @@ -0,0 +1,83 @@ +#include +#include +#include "GL/glew.h" +#include "cglm/cglm.h" +#include "shape.h" + +Shape *Shape_MakePyramid(int numInstances) +{ + GLfloat vertices[] = + { + -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, -1.0f, 1.0f, 0.0f, 1.0f, + + 1.0f, -1.0f, -1.0f, 1.0f, 0.0f, + -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.5f, 1.0f, + + 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.5f, 1.0f, + + -1.0f, -1.0f, 1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.5f, 1.0f, + + -1.0f, -1.0f, -1.0f, 1.0f, 0.0f, + -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.5f, 1.0f, + }; + + GLushort indices[] = + { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + 9, 10, 11, + 12, 13, 14, + 15, 16, 17 + }; + + size_t numBytesVertices = sizeof(vertices); + size_t numBytesIndices = sizeof(indices); + + Shape *shape = malloc(sizeof(Shape) + numBytesVertices + numBytesIndices); + void *tempPtr = (void*)(shape + 1); + shape->vertices = tempPtr; + shape->numVertices = numBytesVertices / sizeof(vertices[0]); + shape->indices = tempPtr + numBytesVertices; + shape->numIndices = numBytesIndices / sizeof(indices[0]); + + memcpy(shape->vertices, vertices, numBytesVertices); + memcpy(shape->indices, indices, numBytesIndices); + + shape->instances = calloc(numInstances, sizeof(ShapeInstance) + SHAPE_INSTANCE_SIZE); + shape->instanceData = (void*)(shape->instances + numInstances); + shape->numInstances = numInstances; + + for (int i = 0; i < numInstances; i++) + { + ShapeInstance *instance = shape->instances + i; + instance->position[0] = -4.0f; + instance->position[0] = 0.0f; + instance->position[0] = (5.0f * i) - 2.0f; + instance->scale = 1.0f; + instance->collisionRadius = 1.0f; + glm_vec3_zero(instance->velocity); + glm_vec3_zero(instance->rotation); + float *textureId = shape->instanceData + (i * 17); + *textureId = 0; + mat4 *matrix = (void*)(textureId + 1); + } + + return shape; +} + +void *Shape_Destroy(Shape *shape) +{ + free(shape); +} diff --git a/src/shape.h b/src/shape.h new file mode 100644 index 0000000..6291584 --- /dev/null +++ b/src/shape.h @@ -0,0 +1,37 @@ +#pragma once + +#include "GL/glew.h" +#include "cglm/cglm.h" + +enum +{ + SHAPE_INSTANCE_SIZE = sizeof(float) + sizeof(mat4) +}; + +typedef struct +{ + vec3 position; + vec3 rotation; + vec3 velocity; + float scale; + float collisionRadius; + +} ShapeInstance; + +typedef struct +{ + GLfloat *vertices; + int numVertices; + GLushort *indices; + int numIndices; + ShapeInstance *instances; + float *instanceData; + int numInstances; + + GLuint VAO; + GLuint VBO; + GLuint IBO; + GLuint EBO; +} Shape; + +Shape *Shape_MakePyramid(int numInstances);