From c55242ba80962ed35e4582be86b705efae9cd86b Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 22 Feb 2026 17:03:25 +1100 Subject: [PATCH] set up rendering + basic lighting, added imgui, added camera --- alloc.cpp | 11 +- assets.cpp | 387 +- assets/cube_with_vertexcolors.m3d | Bin 0 -> 223 bytes assets/yoda.m3d | Bin 0 -> 15862 bytes build.sh | 13 +- external/include/imgui/imconfig.h | 147 + external/include/imgui/imgui.cpp | 18183 ++++++++++++++++ external/include/imgui/imgui.h | 4203 ++++ external/include/imgui/imgui_demo.cpp | 10942 ++++++++++ external/include/imgui/imgui_draw.cpp | 6739 ++++++ external/include/imgui/imgui_impl_opengl3.cpp | 1066 + external/include/imgui/imgui_impl_opengl3.h | 68 + .../include/imgui/imgui_impl_opengl3_loader.h | 958 + external/include/imgui/imgui_impl_sdl3.cpp | 887 + external/include/imgui/imgui_impl_sdl3.h | 54 + external/include/imgui/imgui_internal.h | 3991 ++++ external/include/imgui/imgui_main.cpp | 8 + external/include/imgui/imgui_tables.cpp | 4573 ++++ external/include/imgui/imgui_widgets.cpp | 10817 +++++++++ external/include/imgui/imstb_rectpack.h | 627 + external/include/imgui/imstb_textedit.h | 1527 ++ external/include/imgui/imstb_truetype.h | 5085 +++++ external/include/m3d.h | 6574 ++++++ imgui.ini | 8 + main.cpp | 385 +- math.cpp | 60 + shaders/pbr.glsl | 72 +- shaders/vert.glsl | 1 - util.cpp | 21 + 29 files changed, 77283 insertions(+), 124 deletions(-) create mode 100644 assets/cube_with_vertexcolors.m3d create mode 100644 assets/yoda.m3d create mode 100644 external/include/imgui/imconfig.h create mode 100644 external/include/imgui/imgui.cpp create mode 100644 external/include/imgui/imgui.h create mode 100644 external/include/imgui/imgui_demo.cpp create mode 100644 external/include/imgui/imgui_draw.cpp create mode 100644 external/include/imgui/imgui_impl_opengl3.cpp create mode 100644 external/include/imgui/imgui_impl_opengl3.h create mode 100644 external/include/imgui/imgui_impl_opengl3_loader.h create mode 100644 external/include/imgui/imgui_impl_sdl3.cpp create mode 100644 external/include/imgui/imgui_impl_sdl3.h create mode 100644 external/include/imgui/imgui_internal.h create mode 100644 external/include/imgui/imgui_main.cpp create mode 100644 external/include/imgui/imgui_tables.cpp create mode 100644 external/include/imgui/imgui_widgets.cpp create mode 100644 external/include/imgui/imstb_rectpack.h create mode 100644 external/include/imgui/imstb_textedit.h create mode 100644 external/include/imgui/imstb_truetype.h create mode 100644 external/include/m3d.h create mode 100644 imgui.ini create mode 100644 math.cpp diff --git a/alloc.cpp b/alloc.cpp index 7bc28ec..fe89900 100644 --- a/alloc.cpp +++ b/alloc.cpp @@ -106,12 +106,17 @@ End(TempArena *temp_arena) { if(temp_arena->arena) { - temp_arena->start_pool->used = temp_arena->start_pos; + ArenaPool *start_pool = temp_arena->start_pool; + + start_pool->used = temp_arena->start_pos; + memset(start_pool->buffer+start_pool->used, 0, start_pool->length-start_pool->used); + ArenaPool *pool = temp_arena->start_pool->next; while(pool) { pool->used = 0; pool = pool->next; + memset(pool->buffer, 0, pool->length); } } } @@ -170,7 +175,7 @@ Alloc(Arena* arena) template Array Alloc(Arena* arena, u64 length) { - Array array; + Array array = {}; array.ptr = AllocAlign(arena, sizeof(T)*length, DEFAULT_ALIGNMENT); array.length = length; @@ -265,7 +270,7 @@ AllocPtrArray(u64 length) template Array Alloc(u64 length) { - Array array; + Array array = {}; array.ptr = (T *)Malloc(sizeof(T)*length); array.length = length; diff --git a/assets.cpp b/assets.cpp index 9511649..0c7b883 100644 --- a/assets.cpp +++ b/assets.cpp @@ -59,6 +59,42 @@ struct Model Model g_models[MODEL_MAX]; +ModelBuffers +CreateModelBuffers(Array vertices, Array indices) +{ + ModelBuffers buffers = CreateModelBuffers(); + + glBindVertexArray(buffers.vertex_array); + + glBindBuffer(GL_ARRAY_BUFFER, buffers.vertex_buffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex)*vertices.length, vertices.ptr, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers.index_buffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(u32)*indices.length, indices.ptr, GL_STATIC_DRAW); + + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, color)); + + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, tangent)); + + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, pos)); + + glEnableVertexAttribArray(3); + glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, normal)); + + glEnableVertexAttribArray(4); + glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, uv[0])); + + glEnableVertexAttribArray(5); + glVertexAttribPointer(5, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)(offsetof(Vertex, uv[1]))); + + glBindVertexArray(0); + + return buffers; +} + Array OpenFile(String8 file_path) { @@ -307,6 +343,65 @@ LoadImageToTexture(cgltf_image *asset_image, String8 texture_path) return texture_id; } +TextureID +LoadImageToTexture(u8 *data, i32 width, i32 height, i32 channels) +{ + assert(width > 0 && height > 0 && channels > 0); + + Array temp_buffer = {}; + if(channels == 1) + { + u64 length = width*height*channels; + temp_buffer = Alloc(length*4); + for(u64 i = 0; i < length; i += 1) + { + temp_buffer[i*3 + 0] = data[i]; + temp_buffer[i*3 + 1] = data[i]; + temp_buffer[i*3 + 2] = data[i]; + temp_buffer[i*3 + 3] = 255; + } + } + else if(channels == 3) + { + temp_buffer = Alloc(width*height*4); + u64 pixels = width*height; + for(u64 i = 0; i < pixels; i += 1) + { + temp_buffer[i*4 + 0] = data[i*3 + 0]; + temp_buffer[i*4 + 1] = data[i*3 + 1]; + temp_buffer[i*4 + 2] = data[i*3 + 2]; + temp_buffer[i*4 + 3] = 255; + } + } + + if(channels < 4) + { + data = temp_buffer.ptr; + channels = 4; + } + + ImageBuffer image_buffer = { + .data = { + .ptr = data, + .length = (u64)(width*height*channels), + }, + .w = (u32)width, + .h = (u32)height, + .ch = (u32)channels, + }; + + assert(image_buffer.data.length > 0); + + TextureID texture_id = CreateTexture(image_buffer); + + if(temp_buffer) + { + Free(&temp_buffer); + } + + return texture_id; +} + TextureID FindTexture(cgltf_texture *texture, cgltf_data *data, Model *model) { @@ -502,9 +597,9 @@ LoadGLTF(Arena* arena, Model* model_result, String8 file_name) } } - model.materials[i].buffer_id = CreateBuffer(&material_set); + model.materials[i].buffer_id = CreateBuffer(&material_set, true); model.materials[i].shader_state = shader_state; - model.materials[i].shader_state_buffer_id = CreateBuffer(&shader_state); + model.materials[i].shader_state_buffer_id = CreateBuffer(&shader_state, true); } u64 mesh_index = 0, point_index = 0; @@ -727,35 +822,7 @@ LoadGLTF(Arena* arena, Model* model_result, String8 file_name) Free(&file_data); - model.buffers = CreateModelBuffers(); - - glBindVertexArray(model.buffers.vertex_array); - - glBindBuffer(GL_ARRAY_BUFFER, model.buffers.vertex_buffer); - glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex)*vertices.length, vertices.ptr, GL_STATIC_DRAW); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, model.buffers.index_buffer); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(u32)*indices.length, indices.ptr, GL_STATIC_DRAW); - - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, color)); - - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, tangent)); - - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, pos)); - - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, normal)); - - glEnableVertexAttribArray(4); - glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, uv[0])); - - glEnableVertexAttribArray(5); - glVertexAttribPointer(5, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)(offsetof(Vertex, uv[1]))); - - glBindVertexArray(0); + model.buffers = CreateModelBuffers(vertices, indices); End(&temp_arena); @@ -764,6 +831,262 @@ LoadGLTF(Arena* arena, Model* model_result, String8 file_name) cgltf_free(data); } - return result == cgltf_result_success; } + +const char * +M3DPropToStr(u8 type) +{ + const char *result = "Unknown"; + switch(type) + { + case m3dp_Kd: result = "Diffuse"; break; + case m3dp_Ka: result = "Ambient"; break; + case m3dp_Ks: result = "Specular Color"; break; + case m3dp_Ns: result = "Specular Exponent"; break; + case m3dp_Ke: result = "Emissive"; break; + case m3dp_Tf: result = "Transmission"; break; + case m3dp_Km: result = "Bump Strength"; break; + case m3dp_d: result = "Alpha"; break; + case m3dp_il: result = "Illumination"; break; + case m3dp_Pr: result = "Roughness"; break; + case m3dp_Pm: result = "Metallic"; break; + case m3dp_Ps: result = "Sheen"; break; + case m3dp_Ni: result = "Refraction"; break; + case m3dp_Nt: result = "Face Thickness"; break; + case m3dp_map_Kd: result = "Diffuse Texture"; break; + case m3dp_map_Ka: result = "Ambient Texture"; break; + case m3dp_map_Ks: result = "Specular Texture"; break; + case m3dp_map_Ns: result = "Specular Exponent Texture"; break; + case m3dp_map_Ke: result = "Emissive Texture"; break; + case m3dp_map_Tf: result = "Transmission Texture"; break; + case m3dp_map_Km: result = "Bump Map"; break; + case m3dp_map_D: result = "Alpha Map"; break; + case m3dp_map_N: result = "Normal Map"; break; + case m3dp_map_Pr: result = "Roughness Map"; break; + case m3dp_map_Pm: result = "Metallic Map"; break; + case m3dp_map_Ps: result = "Sheen Map"; break; + case m3dp_map_Ni: result = "Refraction Map"; break; + case m3dp_map_Nt: result = "Thickness Map"; break; + default: break; + } + + return result; +} + +bool +LoadM3D(Arena* arena, Model* model_result, String8 file_name) +{ + bool result = false; + + Array file_data = OpenFile(file_name); + assert(file_data); + + m3d_t *m3d = m3d_load(file_data.ptr, NULL, NULL, NULL); + assert(m3d); + + if(m3d->numface) + { + Model model = { + textures: Alloc(arena, m3d->numtexture), + materials: Alloc(arena, m3d->nummaterial), + }; + + for(u64 i = 0; i < m3d->numtexture; i += 1) + { + model.textures[i] = LoadImageToTexture(m3d->texture[i].d, m3d->texture[i].w, m3d->texture[i].h, m3d->texture[i].f); + } + + for(u64 i = 0; i < m3d->nummaterial; i += 1) + { + MaterialSet material_set = {}; + ShaderModelState shader_state = {}; + + for(u64 j = 0; j < m3d->material[i].numprop; j += 1) + { + auto prop = m3d->material[i].prop + j; + TextureID *textures = model.materials[i].textures; + + switch(prop->type) + { + case m3dp_Kd: material_set.maps[MMI_Albedo].color = U32ToVec4(prop->value.color); break; + case m3dp_Ka: material_set.maps[MMI_Occlusion].color = U32ToVec4(prop->value.color); break; + case m3dp_Ks: material_set.maps[MMI_Metallic].color = U32ToVec4(prop->value.color); break; + case m3dp_Ns: material_set.maps[MMI_Roughness].value = prop->value.fnum; break; + case m3dp_d: break; // Alpha + case m3dp_map_Kd: + { + textures[MMI_Albedo] = model.textures[prop->value.textureid]; + shader_state.albedo_texture = true; + } break; + case m3dp_map_Ka: + { + textures[MMI_Occlusion] = model.textures[prop->value.textureid]; + shader_state.occlusion_texture = true; + } break; + case m3dp_map_Ks: + { + textures[MMI_Metallic] = model.textures[prop->value.textureid]; + textures[MMI_Roughness] = model.textures[prop->value.textureid]; // Maybe incorrect.. + shader_state.metallic_roughness_texture = true; + } break; + case m3dp_map_D: + { + // Alpha texture + } break; + default: break; // Logf("Unsupported property: %s", M3DPropToStr(prop->type)); break; + } + + model.materials[i].buffer_id = CreateBuffer(&material_set, true); + model.materials[i].shader_state = shader_state; + model.materials[i].shader_state_buffer_id = CreateBuffer(&shader_state, true); + } + } + + u64 mesh_count = 0; + u64 last = -2U; + for(u64 i = 0; i < m3d->numface; i += 1) + { + if(m3d->face[i].materialid != last) + { + last = m3d->face[i].materialid; + mesh_count += 1; + } + } + + model.meshes = Alloc(arena, mesh_count); + + u64 vertex_count = 0; + u64 mesh_index = 0; + if(mesh_count > 1) + { + last = -2U; + + for(u64 i = 0; i < m3d->numface; i += 1) + { + if(last == -2U) + { + model.meshes[mesh_index].material_index = m3d->face[i].materialid; + model.meshes[mesh_index].start = 0; + model.meshes[mesh_index].index_start = 0; + + last = m3d->face[i].materialid; + } + else if(m3d->face[i].materialid != last) + { + model.meshes[mesh_index].length = i*3-vertex_count; + model.meshes[mesh_index].index_length = i*3-vertex_count; + + mesh_index += 1; + vertex_count += model.meshes[mesh_index].length; + + model.meshes[mesh_index].start = vertex_count; + model.meshes[mesh_index].index_start = vertex_count; + model.meshes[mesh_index].material_index = m3d->face[i].materialid; + + last = m3d->face[i].materialid; + } + if(i == m3d->numface-1) + { + model.meshes[mesh_index].length = i*3 - vertex_count; + model.meshes[mesh_index].index_length = i*3 - vertex_count; + } + } + } + else + { + model.meshes[0].start = 0; + model.meshes[0].index_start = 0; + model.meshes[0].length = m3d->numface*3; + model.meshes[0].index_length = m3d->numface*3; + model.meshes[0].material_index = m3d->face[0].materialid; + } + + TempArena temp_arena = Begin(arena); + + Array vertices = Alloc(temp_arena, m3d->numface*3); + Array indices = Alloc(temp_arena, m3d->numface*3); + Array positions = Alloc(temp_arena, m3d->numface); + Array position_indices = Alloc(temp_arena, m3d->numface); + + for(u64 i = 0; i < m3d->numface; i += 1) + { + u64 vi = i*3; + + memcpy(&vertices[vi+0].pos, &m3d->vertex[m3d->face[i].vertex[0]], sizeof(Vec3)); + memcpy(&vertices[vi+1].pos, &m3d->vertex[m3d->face[i].vertex[1]], sizeof(Vec3)); + memcpy(&vertices[vi+2].pos, &m3d->vertex[m3d->face[i].vertex[2]], sizeof(Vec3)); + + memcpy(&vertices[vi+0].normal, &m3d->vertex[m3d->face[i].normal[0]], sizeof(Vec3)); + memcpy(&vertices[vi+1].normal, &m3d->vertex[m3d->face[i].normal[1]], sizeof(Vec3)); + memcpy(&vertices[vi+2].normal, &m3d->vertex[m3d->face[i].normal[2]], sizeof(Vec3)); + + vertices[vi+0].color = U32ToVec4(m3d->vertex[m3d->face[i].vertex[0]].color); + vertices[vi+1].color = U32ToVec4(m3d->vertex[m3d->face[i].vertex[1]].color); + vertices[vi+2].color = U32ToVec4(m3d->vertex[m3d->face[i].vertex[2]].color); + + if(m3d->numtmap) + { + memcpy(&vertices[vi+0].uv[0], &m3d->tmap[m3d->face[i].texcoord[0]], sizeof(Vec2)); + memcpy(&vertices[vi+1].uv[0], &m3d->tmap[m3d->face[i].texcoord[1]], sizeof(Vec2)); + memcpy(&vertices[vi+2].uv[0], &m3d->tmap[m3d->face[i].texcoord[2]], sizeof(Vec2)); + } + + indices[vi+0] = vi+0; + indices[vi+1] = vi+1; + indices[vi+2] = vi+2; + + positions[i] = (vertices[vi+0].pos + vertices[vi+1].pos + vertices[vi+2].pos) / 3.0f; + position_indices[i] = i; + } + + for(u64 i = 0; i < indices.length; i += 3) + { + u32 i0 = indices[i+0]; + u32 i1 = indices[i+1]; + u32 i2 = indices[i+2]; + + Vec3 edge1 = vertices[i1].pos - vertices[i0].pos; + Vec3 edge2 = vertices[i2].pos - vertices[i0].pos; + + Vec2 delta_uv1 = vertices[i1].uv[0] - vertices[i0].uv[0]; + Vec2 delta_uv2 = vertices[i2].uv[0] - vertices[i0].uv[0]; + + f32 dividend = delta_uv1.x*delta_uv2.y - delta_uv2.x*delta_uv1.y; + f32 fc = 1.0f/dividend; + + Vec3 tangent = Vec3( + fc * (delta_uv2.y * edge1.x - delta_uv1.y * edge2.x), + fc * (delta_uv2.y * edge1.y - delta_uv1.y * edge2.y), + fc * (delta_uv2.y * edge1.z - delta_uv1.y * edge2.z) + ); + + tangent = Normalize(tangent); + + f32 handedness = ((delta_uv1.y*delta_uv2.x - delta_uv2.y*delta_uv1.x) < 0.0f) ? -1.0f : +1.0f; + + Vec3 t = tangent * handedness; + + vertices[i0].tangent = Vec4(t, handedness); + vertices[i1].tangent = Vec4(t, handedness); + vertices[i2].tangent = Vec4(t, handedness); + } + + model.buffers = CreateModelBuffers(vertices, indices); + + End(&temp_arena); + + *model_result = model; + + result = true; + } + else + { + Logf("No faces in M3D model"); + } + + m3d_free(m3d); + Free(&file_data); + + return result; +} diff --git a/assets/cube_with_vertexcolors.m3d b/assets/cube_with_vertexcolors.m3d new file mode 100644 index 0000000000000000000000000000000000000000..a10dee65ba453115ee57cea1b7c95b67ab3becff GIT binary patch literal 223 zcmV<503iP}L`_fM0001ZUh{BubkSe{fd+eq^E?dB`8oMT3eKfTsSIvKsi_Q(#l@Mq z1q=+%zK#JPbu0`z|9!cP{!1`a{;%e;_+On{@n4+b_T literal 0 HcmV?d00001 diff --git a/assets/yoda.m3d b/assets/yoda.m3d new file mode 100644 index 0000000000000000000000000000000000000000..de34cb5f974fecca0aee620970e993cb347d7287 GIT binary patch literal 15862 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