From 0f69c929ddd655b5baa29e39db7c41e38a693c9f Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 6 Feb 2022 15:18:27 -0500 Subject: [PATCH] Add capability to draw a basic "mesh" - Added basic stuff for creating a "mesh" out of line segments. Currently shows a test rendering with three different colored line segments. Seems to work so far. --- build_windows.bat | 1 + l_system_3d.c | 90 ++++++++++++++++- l_system_3d.h | 11 +++ l_system_mesh.c | 203 ++++++++++++++++++++++++++++++++++++++ l_system_mesh.h | 61 ++++++++++++ mesh_fragment_shader.frag | 15 +++ mesh_vertex_shader.vert | 29 ++++++ shared_uniforms.glsl | 7 ++ utilities.c | 62 +++++++++++- utilities.h | 10 +- 10 files changed, 482 insertions(+), 7 deletions(-) create mode 100644 l_system_mesh.c create mode 100644 l_system_mesh.h create mode 100644 mesh_fragment_shader.frag create mode 100644 mesh_vertex_shader.vert create mode 100644 shared_uniforms.glsl diff --git a/build_windows.bat b/build_windows.bat index 16d721f..b71e238 100644 --- a/build_windows.bat +++ b/build_windows.bat @@ -1,5 +1,6 @@ gcc -Wall -Werror -O3 -o l_system_3d ^ l_system_3d.c ^ + l_system_mesh.c ^ utilities.c ^ glad\src\glad.c ^ -I cglm\include ^ diff --git a/l_system_3d.c b/l_system_3d.c index 7db6474..fe624e2 100644 --- a/l_system_3d.c +++ b/l_system_3d.c @@ -5,8 +5,9 @@ #include #include -#include "l_system_3d.h" +#include "l_system_mesh.h" #include "utilities.h" +#include "l_system_3d.h" #define DEFAULT_WINDOW_WIDTH (800) #define DEFAULT_WINDOW_HEIGHT (600) @@ -24,11 +25,20 @@ ApplicationState* AllocateApplicationState(void) { void FreeApplicationState(ApplicationState *s) { if (!s) return; + if (s->mesh) DestroyLSystemMesh(s->mesh); + glDeleteBuffers(1, &(s->ubo)); if (s->window) glfwDestroyWindow(s->window); memset(s, 0, sizeof(*s)); free(s); } +// Recomputes the scene's projection matrix based on s->aspect_ratio. +static void UpdateProjectionMatrix(ApplicationState *s) { + glm_mat4_identity(s->shared_uniforms.projection); + glm_perspective(45.0, s->aspect_ratio, 0.01, 100.0, + s->shared_uniforms.projection); +} + static void FramebufferResizedCallback(GLFWwindow *window, int width, int height) { ApplicationState *s = (ApplicationState *) glfwGetWindowUserPointer(window); @@ -36,8 +46,10 @@ static void FramebufferResizedCallback(GLFWwindow *window, int width, s->window_height = height; s->aspect_ratio = ((float) s->window_width) / ((float) s->window_height); glViewport(0, 0, width, height); + UpdateProjectionMatrix(s); } +// Sets up the GLFW window. Returns 0 on error. static int SetupWindow(ApplicationState *s) { GLFWwindow *window = NULL; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); @@ -56,6 +68,40 @@ static int SetupWindow(ApplicationState *s) { return 1; } +// Allocates the ubo, but doesn't populate it with data yet. Returns 0 on +// error. +static int SetupUniformBuffer(ApplicationState *s) { + glGenBuffers(1, &(s->ubo)); + glBindBuffer(GL_UNIFORM_BUFFER, s->ubo); + glBufferData(GL_UNIFORM_BUFFER, sizeof(SharedUniforms), NULL, + GL_STATIC_DRAW); + glBindBufferRange(GL_UNIFORM_BUFFER, SHARED_UNIFORMS_BINDING, s->ubo, 0, + sizeof(SharedUniforms)); + glBindBuffer(GL_UNIFORM_BUFFER, 0); + return CheckGLErrors(); +} + +// This generates the vertices for the L-system, and updates the mesh. Returns +// 0 on error. +static int GenerateVertices(ApplicationState *s) { + // TODO: Actually store the vertices in s. + MeshVertex tmp_verts[] = { + {{0, 0, 0}, 0, {1, 1, 1}, 0, {0, 0, 0}, 0, {1, 0, 0, 1}}, + {{1, 1, 1}, 0, {0, 0, 0}, 0, {0, 0, 0}, 0, {1, 0, 0, 1}}, + // Green line from 0, 0, 0 -> 0, 1, 1 + {{0, 0, 0}, 0, {0, 1, 1}, 0, {0, 0, 0}, 0, {0, 1, 0, 1}}, + {{0, 1, 1}, 0, {0, 0, 0}, 0, {0, 0, 0}, 0, {0, 1, 0, 1}}, + // Blue line from 0, 0, 0, -> 1, 1, 0 + {{0, 0, 0}, 0, {1, 1, 0}, 0, {0, 0, 0}, 0, {0, 0, 1, 1}}, + {{1, 1, 0}, 0, {0, 0, 0}, 0, {0, 0, 0}, 0, {0, 0, 1, 1}}, + }; + if (!SetMeshVertices(s->mesh, tmp_verts, 6)) { + printf("Failed setting vertices.\n"); + return 0; + } + return 1; +} + static int ProcessInputs(ApplicationState *s) { if (glfwGetKey(s->window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { glfwSetWindowShouldClose(s->window, 1); @@ -63,17 +109,39 @@ static int ProcessInputs(ApplicationState *s) { return 1; } +static void UpdateCamera(ApplicationState *s) { + vec3 position, target, up; + float tmp; + // TODO (eventually): Change camera based on user input; allow flying around. + glm_mat4_identity(s->shared_uniforms.view); + glm_vec3_zero(position); + glm_vec3_zero(target); + glm_vec3_zero(up); + up[1] = 1.0; + tmp = glfwGetTime() / 4.0; + position[0] = sin(tmp) * 4.0; + position[2] = cos(tmp) * 4.0; + glm_lookat(position, target, up, s->shared_uniforms.view); + glm_vec4(position, 0, s->shared_uniforms.camera_position); +} + static int RunMainLoop(ApplicationState *s) { glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); - glClearColor(0.3f, 0.05f, 0.5f, 1.0f); + glClearColor(0, 0, 0, 1.0); while (!glfwWindowShouldClose(s->window)) { if (!ProcessInputs(s)) { printf("Error processing inputs.\n"); return 0; } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + UpdateCamera(s); + glBindBuffer(GL_UNIFORM_BUFFER, s->ubo); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(SharedUniforms), + (void *) &(s->shared_uniforms)); + if (!DrawMesh(s->mesh)) return 0; + glfwSwapBuffers(s->window); glfwPollEvents(); if (!CheckGLErrors()) { @@ -108,12 +176,30 @@ int main(int argc, char **argv) { } glViewport(0, 0, s->window_width, s->window_height); glfwSetFramebufferSizeCallback(s->window, FramebufferResizedCallback); + if (!SetupUniformBuffer(s)) { + printf("Failed setting up uniform buffer.\n"); + to_return = 1; + goto cleanup; + } + UpdateProjectionMatrix(s); + s->mesh = CreateLSystemMesh(); + if (!s->mesh) { + printf("Failed initializing L-system mesh.\n"); + to_return = 1; + goto cleanup; + } if (!CheckGLErrors()) { printf("OpenGL errors detected during initialization.\n"); to_return = 1; goto cleanup; } + + if (!GenerateVertices(s)) { + printf("Failed generating vertices.\n"); + to_return = 1; + goto cleanup; + } if (!RunMainLoop(s)) { printf("Application ended with an error.\n"); to_return = 1; diff --git a/l_system_3d.h b/l_system_3d.h index 45c6313..a7b9fdd 100644 --- a/l_system_3d.h +++ b/l_system_3d.h @@ -2,12 +2,23 @@ #include #include +// Uniforms shared with all shaders. Must match the layout in +// shared_uniforms.glsl, and every field must be padded to four floats. +typedef struct { + mat4 projection; + mat4 view; + vec4 camera_position; +} SharedUniforms; + // Maintains global data about the running program. typedef struct { GLFWwindow *window; int window_width; int window_height; float aspect_ratio; + LSystemMesh *mesh; + GLuint ubo; + SharedUniforms shared_uniforms; } ApplicationState; // Allocates an ApplicationState struct and initializes its values to 0. diff --git a/l_system_mesh.c b/l_system_mesh.c new file mode 100644 index 0000000..f91d0e1 --- /dev/null +++ b/l_system_mesh.c @@ -0,0 +1,203 @@ +#include +#include +#include +#include +#include +#include +#include "utilities.h" +#include "l_system_mesh.h" + +// Loads and compiles a shader from the given file path. Returns 0 on error. +static GLuint LoadShader(const char *path, GLenum shader_type) { + GLuint to_return = 0; + GLint compile_result = 0; + GLchar shader_log[512]; + char *shader_src_orig = NULL; + char *shared_uniform_code = NULL; + char *final_src = NULL; + + // First do some preprocessing to insert the common uniform definitions in + // place of the special comment in the main shader source. + shader_src_orig = ReadFullFile(path); + if (!shader_src_orig) return 0; + shared_uniform_code = ReadFullFile("./shared_uniforms.glsl"); + if (!shared_uniform_code) { + free(shader_src_orig); + return 0; + } + final_src = StringReplace(shader_src_orig, "//INCLUDE_SHARED_UNIFORMS\n", + shared_uniform_code); + if (!final_src) { + printf("Failed preprocessing shader source code.\n"); + free(shader_src_orig); + free(shared_uniform_code); + return 0; + } + free(shader_src_orig); + free(shared_uniform_code); + shader_src_orig = NULL; + shared_uniform_code = NULL; + + to_return = glCreateShader(shader_type); + glShaderSource(to_return, 1, (const char**) &final_src, NULL); + glCompileShader(to_return); + free(final_src); + final_src = NULL; + + // Check compilation success. + memset(shader_log, 0, sizeof(shader_log)); + glGetShaderiv(to_return, GL_COMPILE_STATUS, &compile_result); + if (compile_result != GL_TRUE) { + glGetShaderInfoLog(to_return, sizeof(shader_log) - 1, NULL, + shader_log); + printf("Shader %s compile error:\n%s\n", path, shader_log); + glDeleteShader(to_return); + return 0; + } + if (!CheckGLErrors()) { + glDeleteShader(to_return); + return 0; + } + return to_return; +} + +// Links and returns the shader program from the given source file names. +static GLuint CreateShaderProgram(const char *vertex_src_file, + const char *fragment_src_file) { + GLchar link_log[512]; + GLint link_result = 0; + GLuint vertex_shader, fragment_shader, to_return; + + vertex_shader = LoadShader(vertex_src_file, GL_VERTEX_SHADER); + if (!vertex_shader) { + printf("Couldn't load vertex shader.\n"); + return 0; + } + fragment_shader = LoadShader(fragment_src_file, GL_FRAGMENT_SHADER); + if (!fragment_shader) { + printf("Couldn't load fragment shader.\n"); + glDeleteShader(vertex_shader); + return 0; + } + to_return = glCreateProgram(); + glAttachShader(to_return, vertex_shader); + glAttachShader(to_return, fragment_shader); + glLinkProgram(to_return); + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + glGetProgramiv(to_return, GL_LINK_STATUS, &link_result); + memset(link_log, 0, sizeof(link_log)); + if (link_result != GL_TRUE) { + glGetProgramInfoLog(to_return, sizeof(link_log) - 1, NULL, link_log); + printf("GL program link error:\n%s\n", link_log); + glDeleteProgram(to_return); + return 0; + } + glUseProgram(to_return); + if (!CheckGLErrors()) { + glDeleteProgram(to_return); + return 0; + } + return to_return; +} + +// Sets *index to the index of the named uniform in s->shader_program. Returns +// 0 and prints a message on error. +static int UniformIndex(GLuint program, const char *name, GLint *index) { + *index = glGetUniformLocation(program, name); + if (*index < 0) { + printf("Failed getting location of uniform %s.\n", name); + return 0; + } + return 1; +} + +// Loads the shaders for the model, and looks up uniform indices. Returns 0 on +// error. +static int SetupShaderProgram(LSystemMesh *m) { + GLuint p; + GLuint block_index; + p = CreateShaderProgram("./mesh_vertex_shader.vert", + "./mesh_fragment_shader.frag"); + if (!p) return 0; + m->shader_program = p; + if (!UniformIndex(p, "model", &(m->model_uniform_index))) return 0; + if (!UniformIndex(p, "normal", &(m->normal_uniform_index))) return 0; + block_index = glGetUniformBlockIndex(p, "SharedUniforms"); + if (block_index == GL_INVALID_INDEX) { + printf("Failed getting index of shared uniform block.\n"); + CheckGLErrors(); + return 0; + } + glUniformBlockBinding(p, block_index, SHARED_UNIFORMS_BINDING); + return CheckGLErrors(); +} + +LSystemMesh* CreateLSystemMesh(void) { + LSystemMesh *m = NULL; + + m = (LSystemMesh *) calloc(1, sizeof(*m)); + if (!m) { + printf("Failed allocating CPU-side mesh struct.\n"); + return NULL; + } + + glGenVertexArrays(1, &(m->vao)); + glBindVertexArray(m->vao); + glGenBuffers(1, &(m->vbo)); + glBindBuffer(GL_ARRAY_BUFFER, m->vbo); + // Setting up the location, direction, orientation, and color attributes + // (respectively). + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(MeshVertex), + (void *) offsetof(MeshVertex, location)); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(MeshVertex), + (void *) offsetof(MeshVertex, direction)); + glEnableVertexAttribArray(1); + glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(MeshVertex), + (void *) offsetof(MeshVertex, orientation)); + glEnableVertexAttribArray(2); + glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(MeshVertex), + (void *) offsetof(MeshVertex, color)); + glEnableVertexAttribArray(3); + if (!CheckGLErrors()) { + printf("Failed setting up mesh object.\n"); + DestroyLSystemMesh(m); + return NULL; + } + if (!SetupShaderProgram(m)) { + DestroyLSystemMesh(m); + return NULL; + } + glm_mat4_identity(m->model); + glm_mat3_identity(m->normal); + return m; +} + +void DestroyLSystemMesh(LSystemMesh *m) { + if (!m) return; + glDeleteBuffers(1, &(m->vbo)); + glDeleteVertexArrays(1, &(m->vao)); + glDeleteProgram(m->shader_program); + memset(m, 0, sizeof(*m)); + free(m); +} + +int SetMeshVertices(LSystemMesh *m, MeshVertex *vertices, uint32_t count) { + glBindVertexArray(m->vao); + glBindBuffer(GL_ARRAY_BUFFER, m->vbo); + glBufferData(GL_ARRAY_BUFFER, count * sizeof(MeshVertex), vertices, + GL_STATIC_DRAW); + m->vertex_count = count; + return CheckGLErrors(); +} + +int DrawMesh(LSystemMesh *m) { + glUseProgram(m->shader_program); + glBindVertexArray(m->vao); + glUniformMatrix4fv(m->model_uniform_index, 1, GL_FALSE, (float *) m->model); + glUniformMatrix3fv(m->normal_uniform_index, 1, GL_FALSE, + (float *) m->normal); + glDrawArrays(GL_LINES, 0, m->vertex_count); + return CheckGLErrors(); +} diff --git a/l_system_mesh.h b/l_system_mesh.h new file mode 100644 index 0000000..41c4f64 --- /dev/null +++ b/l_system_mesh.h @@ -0,0 +1,61 @@ +// Holds information about the L-system's mesh that we render. +#ifndef L_SYSTEM_MESH_H +#define L_SYSTEM_MESH_H +#include +#include +#include + +// The binding point for the shared uniform block. +#define SHARED_UNIFORMS_BINDING (0) + +// Defines a single vertex within the mesh. +typedef struct { + float location[3]; + float pad1; + // Points in the direction of the next vertex. May be 0 if this is at the + // end of a sequence of lines. + float direction[3]; + float pad2; + // Points "downward"; must be orthogonal to the direction. Basically just + // gives a consistent way to orient geometry. + float orientation[3]; + float pad3; + // The color of the vertex. + float color[4]; +} MeshVertex; + +// Holds information about a full mesh to render. +typedef struct { + // We copy the vertices into the buffer only when SetMeshVertices is called. + uint32_t vertex_count; + // OpenGL stuff needed for drawing this mesh. + GLuint shader_program; + GLuint vao; + GLuint vbo; + // The model and normal matrices used when drawing this mesh. + // TODO: Try to just make these identity matrices? Or perhaps use these to + // scale the model to fit in a 1x1x1 cube and sit on the floor? + mat4 model; + mat3 normal; + GLint model_uniform_index; + GLint normal_uniform_index; +} LSystemMesh; + +// Allocates and initializes a new L-system mesh. Returns NULL on error. The +// mesh is initially empty. Destroy the returned mesh using DestroyLSystemMesh +// when it's no longer needed. +LSystemMesh* CreateLSystemMesh(void); + +// Destroys the given mesh, freeing associated memory. The given mesh pointer +// is no longer valid after this returns. +void DestroyLSystemMesh(LSystemMesh *m); + +// Updates the vertices to render in the mesh. Returns 0 on error. The list of +// vertices should specify *lines*; i.e. this should be a list of pairs of +// vertices. +int SetMeshVertices(LSystemMesh *m, MeshVertex *vertices, uint32_t count); + +// Draws the mesh. Returns 0 on error, including any GL errors if they occur. +int DrawMesh(LSystemMesh *m); + +#endif // L_SYSTEM_MESH_H diff --git a/mesh_fragment_shader.frag b/mesh_fragment_shader.frag new file mode 100644 index 0000000..6129ccc --- /dev/null +++ b/mesh_fragment_shader.frag @@ -0,0 +1,15 @@ +#version 330 core + +in VS_OUT { + vec3 frag_position; + vec4 color; +} fs_in; + +out vec4 frag_color; + +// This will be replaced with the content of shared_uniforms.glsl +//INCLUDE_SHARED_UNIFORMS + +void main() { + frag_color = fs_in.color; +} diff --git a/mesh_vertex_shader.vert b/mesh_vertex_shader.vert new file mode 100644 index 0000000..a336e58 --- /dev/null +++ b/mesh_vertex_shader.vert @@ -0,0 +1,29 @@ +#version 330 core +layout (location = 0) in vec3 position_in; +layout (location = 1) in vec3 direction_in; +layout (location = 2) in vec3 orientation_in; +layout (location = 3) in vec4 color_in; + +uniform mat4 model; +uniform mat3 normal; + +out VS_OUT { + vec3 frag_position; + vec4 color; +} vs_out; + +// This will be replaced with the contents of shared_uniforms.glsl +//INCLUDE_SHARED_UNIFORMS + +void main() { + gl_Position = shared_uniforms.projection * shared_uniforms.view * model * + vec4(position_in, 1.0); + vs_out.color = color_in; + + // Prevent optimizing out the normal uniform. + if (normal[0].x < -1) { + vs_out.color[1] = 1; + } + + vs_out.frag_position = vec3(model * vec4(position_in, 1.0)); +} diff --git a/shared_uniforms.glsl b/shared_uniforms.glsl new file mode 100644 index 0000000..3b7474e --- /dev/null +++ b/shared_uniforms.glsl @@ -0,0 +1,7 @@ +// There are no shared uniforms so far. This file will contain them later. +layout(std140) uniform SharedUniforms { + mat4 projection; + mat4 view; + vec4 camera_position; +} shared_uniforms; + diff --git a/utilities.c b/utilities.c index 4d9fd85..22fa4f0 100644 --- a/utilities.c +++ b/utilities.c @@ -49,9 +49,9 @@ int CheckGLErrors(void) { return 0; } -uint8_t* ReadFullFile(const char *path) { +char* ReadFullFile(const char *path) { long size = 0; - uint8_t *to_return = NULL; + char *to_return = NULL; FILE *f = fopen(path, "rb"); if (!f) { printf("Failed opening %s: %s\n", path, strerror(errno)); @@ -79,7 +79,7 @@ uint8_t* ReadFullFile(const char *path) { return NULL; } // Use size + 1 to null-terminate the data. - to_return = (uint8_t *) calloc(1, size + 1); + to_return = (char *) calloc(1, size + 1); if (!to_return) { printf("Failed allocating buffer to hold contents of %s.\n", path); fclose(f); @@ -95,3 +95,59 @@ uint8_t* ReadFullFile(const char *path) { return to_return; } +char* StringReplace(const char *input, const char *match, const char *r) { + char *input_pos = NULL; + const char *prev_input_pos = NULL; + char *output_pos = NULL; + char *to_return = NULL; + int occurrences = 0; + int new_length = 0; + int unchanged_length = 0; + int original_length = strlen(input); + int match_length = strlen(match); + int sub_length = strlen(r); + // Start by counting the number of occurrences so we can preallocate an + // output buffer of the correct size. + input_pos = strstr(input, match); + while (input_pos != NULL) { + occurrences++; + input_pos += match_length; + input_pos = strstr(input_pos, match); + } + if (occurrences == 0) return strdup(input); + + // Allocate the new string's memory, including space for the null char + new_length = original_length - (occurrences * match_length) + + (occurrences * sub_length); + if (new_length < 0) return NULL; + to_return = (char *) calloc(new_length + 1, sizeof(char)); + if (!to_return) return NULL; + if (new_length == 0) return to_return; + + // Build the string with replacements. + output_pos = to_return; + prev_input_pos = input; + input_pos = strstr(input, match); + while (input_pos != NULL) { + // Copy the part of the string before the replacement to the output. + unchanged_length = ((uintptr_t) input_pos) - ((uintptr_t) prev_input_pos); + memcpy(output_pos, prev_input_pos, unchanged_length); + output_pos += unchanged_length; + // Copy the replacement to the output. + memcpy(output_pos, r, sub_length); + output_pos += sub_length; + // Advance past the matched string in the input and find the next + // occurrence. + input_pos += match_length; + prev_input_pos = input_pos; + input_pos = strstr(input_pos, match); + } + + // Finally, copy any remaining input past the final output. + unchanged_length = original_length - (((uintptr_t) prev_input_pos) - + ((uintptr_t) input)); + memcpy(output_pos, prev_input_pos, unchanged_length); + + return to_return; +} + diff --git a/utilities.h b/utilities.h index eba2a7e..517772c 100644 --- a/utilities.h +++ b/utilities.h @@ -3,7 +3,6 @@ #ifdef __cplusplus extern "C" { #endif -#include // Returns 0 if any OpenGL errors are detected. Otherwise, prints the errors // and returns nonzero. @@ -12,9 +11,16 @@ int CheckGLErrors(void); // Reads the file with the entire given name to a NULL-terminated buffer of // bytes. Returns NULL on error. The caller is responsible for freeing the // returned buffer when it's no longer needed. -uint8_t* ReadFullFile(const char *path); +char* ReadFullFile(const char *path); + +// Replaces all instances of the string match in input with r. Returns a new +// null-terminated string, or NULL on error. The returned string must be freed +// by the caller when no longer needed. Even if no replacements are made, this +// will return a new copy of the input string. +char* StringReplace(const char *input, const char *match, const char *r); #ifdef __cplusplus } // extern "C" #endif #endif // OPENGL_TUTORIAL_UTILITIES_H +