diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..974848b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,82 @@ +name: Build meson project + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + name: ${{ matrix.os }} / ${{ matrix.compiler }} (${{ matrix.build_type }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + build_type: [release] + compiler: [gcc, clang] + + exclude: + - os: windows-latest # Issues with meson for whatever reason + compiler: clang + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Meson dependencies + uses: actions/cache@v4 + with: + path: | + ${{ github.workspace }}/subprojects/*/ + key: ${{ runner.os }}-meson-${{ hashFiles('**/meson.build', '**/meson_options.txt', '**/subprojects/*.wrap') }} + restore-keys: | + ${{ runner.os }}-meson- + + - name: Set up C++ environment + uses: aminya/setup-cpp@v1 + with: + ninja: true + meson: true + cmake: true + compiler: ${{ matrix.compiler }} + + - name: Install System Dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libglu1-mesa-dev libgl1-mesa-dev + + - name: Set reusable workflow outputs + id: set_outputs + shell: bash + run: | + echo "build-dir=${{ github.workspace }}/build" >> $GITHUB_OUTPUT + + - name: Configure Meson + run: meson setup ${{ steps.set_outputs.outputs.build-dir }} --buildtype=${{ matrix.build_type }} + + - name: Build + run: meson compile -C ${{ steps.set_outputs.outputs.build-dir }} + + + - name: Prepare Artifacts (Linux) + if: github.event_name == 'push' && runner.os == 'Linux' + run: | + cp ${{ steps.set_outputs.outputs.build-dir }}/lowlevelgame ${{ github.workspace }}/lowlevelgame + + - name: Prepare Artifacts (Windows) + if: github.event_name == 'push' && runner.os == 'Windows' + run: | + cp ${{ steps.set_outputs.outputs.build-dir }}/lowlevelgame.exe ${{ github.workspace }}/lowlevelgame.exe + + - name: Upload Build Artifacts + if: github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: build-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }} + path: | + ${{ github.workspace }}/lowlevelgame + ${{ github.workspace }}/lowlevelgame.exe + ${{ github.workspace }}/resources/ diff --git a/debug.ps1 b/debug.ps1 index 204b3aa..5d4df2a 100644 --- a/debug.ps1 +++ b/debug.ps1 @@ -1,2 +1,3 @@ meson setup build/debug meson compile -C build/debug +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/meson.build b/meson.build index f9e2a44..b3b8467 100644 --- a/meson.build +++ b/meson.build @@ -27,7 +27,11 @@ if not assimp_dep.found() assimp_dep = cmake.subproject('assimp', options : assimp_opt).dependency('assimp') endif -dependencies = [sdl2_dep, glew_dep, glm_dep, imgui_dep, assimp_dep] + +spdlog_options = ['default_library=static', 'compile_library=true', 'werror=false', 'tests=disabled', 'external_fmt=disabled', 'std_format=disabled'] +spdlog_dep = dependency('spdlog', default_options: spdlog_options) + +dependencies = [sdl2_dep, glew_dep, glm_dep, imgui_dep, assimp_dep, spdlog_dep] if host_machine.system() == 'windows' sdl2_main_dep = dependency('sdl2main') @@ -37,28 +41,45 @@ endif sources = [ 'src/main.cpp', 'src/engine/run.cpp', - 'src/engine/logging.cpp', - 'src/engine/loader/shader/shader_program.cpp', - 'src/engine/loader/scene.cpp', - 'src/engine/loader/texture.cpp', - 'src/engine/loader/generic.cpp', - 'src/engine/manager/texture.cpp', - 'src/engine/manager/scene.cpp', + 'src/engine/util/logging.cpp', + 'src/engine/util/file.cpp', + 'src/engine/resources/shader.cpp', + 'src/engine/resources/texture.cpp', + 'src/engine/resources/scene.cpp', + 'src/engine/resources/mesh.cpp', 'src/engine/render/overlay.cpp', 'src/engine/render/frame_buffer.cpp', + 'src/engine/resources/resource_manager.cpp', 'src/game/game.cpp', - 'src/game/camera.cpp', + 'src/game/camera_utils.cpp', + 'src/game/player.cpp', 'src/game/skybox.cpp', 'src/game/gui.cpp', ] + +bin2h = executable('bin2h', 'tools/bin2h.cpp') +static_binaries = [ + ['resources/static/error.png', 'error_png'], + ['resources/static/error.obj', 'error_obj'], + ['resources/static/error_shader.vert', 'error_shader_vert'], + ['resources/static/error_shader.frag', 'error_shader_frag'], +] +foreach binary : static_binaries + sources += custom_target( + 'embed_binary_' + binary[1], + input: binary[0], + output: binary[1] + '.h', + command: [bin2h, '@INPUT@', '@OUTPUT@'], + ) +endforeach + + exe = executable( 'lowlevelgame', sources, - # win_subsystem: 'windows', # Supresses stdout on Windows include_directories : include_directories('src', 'include'), dependencies : dependencies, cpp_args : ['-std=c++23'] ) - test('basic', exe) diff --git a/release.ps1 b/release.ps1 index 70f8cce..eea94ce 100644 --- a/release.ps1 +++ b/release.ps1 @@ -1,2 +1,3 @@ meson setup build/release --buildtype release --optimization=3 meson compile -C build/release +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/resources/assets/shaders/frag.frag b/resources/assets/shaders/frag.frag index 4179461..83c0a32 100644 --- a/resources/assets/shaders/frag.frag +++ b/resources/assets/shaders/frag.frag @@ -6,9 +6,11 @@ in vec3 Normal; in vec3 FragPos; struct Material { - sampler2D texture_diffuse; - sampler2D texture_specular; - float shininess; + sampler2D albedo_tex; + sampler2D normal_tex; + sampler2D roughness_tex; + sampler2D metallic_tex; + sampler2D ambientOcclusion_tex; }; struct DirLight { @@ -61,7 +63,7 @@ vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir); void main() { // TODO: Transparency blending - if (texture(material.texture_diffuse, TexCoord).a < 0.5) + if (texture(material.albedo_tex, TexCoord).a < 0.5) discard; vec3 norm = normalize(Normal); @@ -84,11 +86,11 @@ vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir) float diff = max(dot(normal, lightDir), 0.0); // specular shading vec3 reflectDir = reflect(-lightDir, normal); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 1); // material.shininess); // combine results - vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse, TexCoord)); - vec3 diffuse = light.diffuse * diff * vec3(texture(material.texture_diffuse, TexCoord)); - vec3 specular = light.specular * spec * vec3(texture(material.texture_specular, TexCoord)); + vec3 ambient = light.ambient * vec3(texture(material.albedo_tex, TexCoord)); + vec3 diffuse = light.diffuse * diff * vec3(texture(material.albedo_tex, TexCoord)); + vec3 specular = light.specular * spec * vec3(texture(material.roughness_tex, TexCoord)); return (ambient + diffuse + specular); } @@ -100,14 +102,14 @@ vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) float diff = max(dot(normal, lightDir), 0.0); // specular shading vec3 reflectDir = reflect(-lightDir, normal); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 1); // material.shininess); // attenuation float distance = length(light.position - fragPos); float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); // combine results - vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse, TexCoord)); - vec3 diffuse = light.diffuse * diff * vec3(texture(material.texture_diffuse, TexCoord)); - vec3 specular = light.specular * spec * vec3(texture(material.texture_specular, TexCoord)); + vec3 ambient = light.ambient * vec3(texture(material.albedo_tex, TexCoord)); + vec3 diffuse = light.diffuse * diff * vec3(texture(material.albedo_tex, TexCoord)); + vec3 specular = light.specular * spec * vec3(texture(material.roughness_tex, TexCoord)); ambient *= attenuation; diffuse *= attenuation; specular *= attenuation; @@ -122,7 +124,7 @@ vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) float diff = max(dot(normal, lightDir), 0.0); // specular shading vec3 reflectDir = reflect(-lightDir, normal); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 1); // material.shininess); // attenuation float distance = length(light.position - fragPos); float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); @@ -131,9 +133,9 @@ vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) float epsilon = light.cutOff - light.outerCutOff; float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); // combine results - vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse, TexCoord)); - vec3 diffuse = light.diffuse * diff * vec3(texture(material.texture_diffuse, TexCoord)); - vec3 specular = light.specular * spec * vec3(texture(material.texture_specular, TexCoord)); + vec3 ambient = light.ambient * vec3(texture(material.albedo_tex, TexCoord)); + vec3 diffuse = light.diffuse * diff * vec3(texture(material.albedo_tex, TexCoord)); + vec3 specular = light.specular * spec * vec3(texture(material.roughness_tex, TexCoord)); ambient *= attenuation * intensity; diffuse *= attenuation * intensity; specular *= attenuation * intensity; diff --git a/resources/assets/models/error.obj b/resources/static/error.obj similarity index 100% rename from resources/assets/models/error.obj rename to resources/static/error.obj diff --git a/resources/assets/textures/error.png b/resources/static/error.png similarity index 100% rename from resources/assets/textures/error.png rename to resources/static/error.png diff --git a/resources/static/error_shader.frag b/resources/static/error_shader.frag new file mode 100644 index 0000000..413832b --- /dev/null +++ b/resources/static/error_shader.frag @@ -0,0 +1,16 @@ +#version 410 core +out vec4 oFragColor; + +float hash(float x) { + return fract(sin(x) * 78657.11909); +} + +void main() +{ + float id = float(gl_PrimitiveID); + float r = hash(id); + float g = hash(id+1); + float b = hash(id+2); + vec3 col = (vec3(r, g, b) - 0.5) * 0.75 + 0.5; + oFragColor = vec4(col, 1.0); +} diff --git a/resources/static/error_shader.vert b/resources/static/error_shader.vert new file mode 100644 index 0000000..096d7b9 --- /dev/null +++ b/resources/static/error_shader.vert @@ -0,0 +1,14 @@ +#version 410 core + +in vec3 iPos; + +layout(std140) uniform Matrices +{ + mat4 projection; + mat4 view; +}; +uniform mat4 model; + +void main() { + gl_Position = projection * view * vec4(vec3(model * vec4(iPos, 1.0)), 1.0); +} diff --git a/src/engine/game.h b/src/engine/game.h index adf2c3b..e5ad6d2 100644 --- a/src/engine/game.h +++ b/src/engine/game.h @@ -1,16 +1,12 @@ -#ifndef GAME_H -#define GAME_H -#include "engine/run.h" +#pragma once #include +bool setupGame(); +void shutdownGame(); -bool setupGame(StatePackage &statePackage, SDL_Window *sdlWindow, SDL_GLContext glContext); -void shutdownGame(StatePackage &statePackage); +bool renderUpdate(double deltaTime); +bool fixedUpdate(double deltaTime); -bool renderUpdate(double deltaTime, StatePackage &statePackage); -bool fixedUpdate(double deltaTime, StatePackage &statePackage); +bool handleEvent(const SDL_Event &event); -bool handleEvent(const SDL_Event &event, StatePackage &statePackage); - -#endif //GAME_H diff --git a/src/engine/loader/generic.cpp b/src/engine/loader/generic.cpp deleted file mode 100644 index c3dcbd8..0000000 --- a/src/engine/loader/generic.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "generic.h" -#include -#include - -namespace Engine::Loader { - std::expected readTextFile(const std::string &filePath) { - std::ifstream file(filePath); - if (!file.is_open()) - return UNEXPECTED_REF("Failed to open file: " + filePath); - - std::string fileContents((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - return fileContents; - } - std::expected readTextFile(const char* filePath) { - std::ifstream file(filePath); - if (!file.is_open()) - return UNEXPECTED_REF("Failed to open file: " + std::string(filePath)); - - std::string fileContents((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - return fileContents; - } -} diff --git a/src/engine/loader/generic.h b/src/engine/loader/generic.h deleted file mode 100644 index 7ba1e5b..0000000 --- a/src/engine/loader/generic.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef GENERIC_H -#define GENERIC_H -#include -#include - -namespace Engine::Loader { - std::expected readTextFile(const char* filePath); - std::expected readTextFile(const std::string &filePath); -} - -#endif //GENERIC_H diff --git a/src/engine/loader/scene.cpp b/src/engine/loader/scene.cpp deleted file mode 100644 index 377cc0e..0000000 --- a/src/engine/loader/scene.cpp +++ /dev/null @@ -1,307 +0,0 @@ -#include "scene.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "shader/graphics_shader.h" -#include "engine/manager/texture.h" - -#ifndef NDEBUG -#include -#endif - - -#define UNPACK_MAT4(aiMat) { \ - aiMat.a1, aiMat.a2, aiMat.a3, aiMat.a4, \ - aiMat.b1, aiMat.b2, aiMat.b3, aiMat.b4, \ - aiMat.c1, aiMat.c2, aiMat.c3, aiMat.c4, \ - aiMat.d1, aiMat.d2, aiMat.d3, aiMat.d4 \ -} -#define UNPACK_VEC2(aiVec) {aiVec.x, aiVec.y} -#define UNPACK_VEC3(aiVec) {aiVec.x, aiVec.y, aiVec.z} -#define UNPACK_VEC4(aiVec) {aiVec.x, aiVec.y, aiVec.z, aiVec.w} -#define UNPACK_RGBA(aiColor) {aiColor.r, aiColor.g, aiColor.b, aiColor.a} - - -namespace Engine::Loader { -#pragma region Loading - std::expected processNode(const aiNode *loadedNode); - std::expected processMesh(const aiMesh *loadedMesh); - std::expected processMaterial(const aiMaterial *loadedMaterial); - - std::expected loadScene(const std::string &path) { -#ifndef NDEBUG - const auto start = std::chrono::high_resolution_clock::now(); -#endif - Assimp::Importer importer; - const aiScene* loadedNode = importer.ReadFile(path.c_str(), - aiProcess_Triangulate - | aiProcess_FlipUVs - // TODO: Add more post processing flags if needed - ); - if (!loadedNode || loadedNode->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !loadedNode->mRootNode) - return std::unexpected(std::string("Failed to load scene: ") + importer.GetErrorString()); - - if (loadedNode->mNumTextures > 0) - logWarn("Embedded textures are not supported"); - if (loadedNode->mNumLights > 0) - logWarn("Lights are not supported"); - if (loadedNode->mNumCameras > 0) - logWarn("Cameras are not supported"); - if (loadedNode->mNumAnimations > 0) - logWarn("Animations are not supported"); - - // Load the node tree - std::expected rootNode = processNode(loadedNode->mRootNode); - if (!rootNode.has_value()) - return std::unexpected(FW_UNEXP(rootNode, "Failed to load node tree")); - - std::vector meshes; - std::vector materials; - - // Load all the meshes - meshes.reserve(loadedNode->mNumMeshes); - for (unsigned int i = 0; i < loadedNode->mNumMeshes; i++) { - std::expected mesh = processMesh(loadedNode->mMeshes[i]); - if (!mesh.has_value()) - return std::unexpected(FW_UNEXP(mesh, "Failed to load mesh "+std::to_string(i)+)); - meshes.push_back(std::move(mesh.value())); - } - - // Load all the materials - materials.reserve(loadedNode->mNumMaterials); - for (unsigned int i = 0; i < loadedNode->mNumMaterials; i++) { - std::expected material = processMaterial(loadedNode->mMaterials[i]); - if (!material.has_value()) - return std::unexpected(FW_UNEXP(material, "Failed to load material "+std::to_string(i)+)); - materials.push_back(material.value()); - } - -#ifndef NDEBUG - logDebug("Loaded scene \"%s\" in %d ms", path.c_str(), - std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count()); -#else - logDebug("Loaded scene \"%s\"", path.c_str()); -#endif - - return Scene{ - rootNode.value(), - std::move(meshes), - materials, - }; - } - - std::expected processNode(const aiNode *loadedNode) { // NOLINT(*-no-recursion) - Node resultNode; - resultNode.transform = UNPACK_MAT4(loadedNode->mTransformation); - - resultNode.children.reserve(loadedNode->mNumChildren); - for (unsigned int i = 0; i < loadedNode->mNumChildren; i++) { - std::expected result = processNode(loadedNode->mChildren[i]); - if (!result.has_value()) - return std::unexpected(FW_UNEXP(result, "Failed to load child node "+std::to_string(i)+)); - resultNode.children.push_back(result.value()); - } - - return resultNode; - } - - std::expected processMesh(const aiMesh *loadedMesh) { - std::vector vertices; - std::vector indices; - const unsigned int materialIndex = loadedMesh->mMaterialIndex; - - for (unsigned int i = 0; i < loadedMesh->mNumVertices; i++) { - MeshVertex vertex{}; - vertex.Position = UNPACK_VEC3(loadedMesh->mVertices[i]); - - if (loadedMesh->HasNormals()) - vertex.Normal = UNPACK_VEC3(loadedMesh->mNormals[i]); - - // 0 since we only support a single set of texture coordinates atm (the first one) - if (loadedMesh->HasTextureCoords(0)) - vertex.TexCoords = UNPACK_VEC2(loadedMesh->mTextureCoords[0][i]); - - // 0 since we only support a single vertex color atm (the first one) - if (loadedMesh->HasVertexColors(0)) - vertex.Color = UNPACK_RGBA(loadedMesh->mColors[0][i]); - - vertices.push_back(vertex); - } - - for (unsigned int i = 0; i < loadedMesh->mNumFaces; i++) { - const aiFace &face = loadedMesh->mFaces[i]; - for (unsigned int j = 0; j < face.mNumIndices; j++) - indices.push_back(face.mIndices[j]); - } - - return Mesh{std::move(vertices), std::move(indices), materialIndex}; - } - - std::expected processMaterial(const aiMaterial *loadedMaterial) { - Material resultMaterial; - - aiString path; - if (loadedMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS) - resultMaterial.diffusePath = path.C_Str(); - // else - // logWarn("Material %s does not have a diffuse texture", loadedMaterial->GetName().C_Str()); - if (loadedMaterial->GetTexture(aiTextureType_SPECULAR, 0, &path) == AI_SUCCESS) - resultMaterial.specularPath = path.C_Str(); - // else - // logWarn("Material %s does not have a specular texture", loadedMaterial->GetName().C_Str()); - - // TODO: This does not make sense in the context of our renderer - if (loadedMaterial->Get(AI_MATKEY_REFLECTIVITY, resultMaterial.shininess) != AI_SUCCESS) { - // logWarn("Material %s does not have shininess", loadedMaterial->GetName().C_Str()); - resultMaterial.shininess = 32.0f; - } - - return resultMaterial; - } -#pragma endregion - - -#pragma region Scene Rendering - void Mesh::bindGlMesh() const { - glBindVertexArray(VAO); - } - - std::expected Scene::Draw(Manager::TextureManager &textureManager, const GraphicsShader &shader, const glm::mat4 &modelTransform) const { - // TODO: Only do unique per-scene stuff here, and don't double-use the shader - shader.use(); - for (const Mesh &mesh: meshes) { - auto matRet = materials[mesh.materialIndex].PopulateShader(shader, textureManager); - if (!matRet.has_value()) - return std::unexpected(FW_UNEXP(matRet, "Failed to populate shader with material")); - - const auto model = rootNode.transform * modelTransform; - shader.setMat4("model", model); - shader.setMat3("mTransposed", glm::mat3(glm::transpose(glm::inverse(model)))); - - mesh.bindGlMesh(); - glDrawElements(GL_TRIANGLES, mesh.indices.size(), GL_UNSIGNED_INT, nullptr); - } - return {}; - } - - std::expected Material::PopulateShader(const GraphicsShader &shader, Manager::TextureManager &textureManager) const { - shader.setFloat("material.shininess", shininess); - shader.setInt("material.test", textureManager.errorTexture); - - std::expected diffuse = textureManager.getTexture(diffusePath); - if (!diffuse.has_value()) - return std::unexpected(FW_UNEXP(diffuse, "Failed to load diffuse texture")); - shader.setInt("material.texture_diffuse", 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, diffuse.value_or(textureManager.errorTexture)); - - std::expected specular = textureManager.getTexture(specularPath); - if (!specular.has_value()) - return std::unexpected(FW_UNEXP(specular, "Failed to load specular texture")); - shader.setInt("material.texture_specular", 1); - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, specular.value_or(textureManager.errorTexture)); - - return {}; - } -#pragma endregion - - -#pragma region Constructing & memory safety stuff - Scene::Scene( - const Node &rootNode, - std::vector &&meshes, - const std::vector &materials - ) noexcept : rootNode(rootNode), meshes(std::move(meshes)), materials(materials) {} - - Scene::Scene(Scene &&other) noexcept { - rootNode = std::move(other.rootNode); - meshes = std::move(other.meshes); - materials = std::move(other.materials); - } - Scene &Scene::operator=(Scene &&other) noexcept { - if (this != &other) { - rootNode = std::move(other.rootNode); - meshes = std::move(other.meshes); - materials = std::move(other.materials); - } - return *this; - } - - Mesh::Mesh( - std::vector &&vertices, - std::vector &&indices, - const unsigned int materialIndex - ) : vertices(std::move(vertices)), indices(std::move(indices)), materialIndex(materialIndex) { - setupGlMesh(); - } - - void Mesh::setupGlMesh() { - glGenVertexArrays(1, &VAO); - glGenBuffers(1, &VBO); - glGenBuffers(1, &EBO); - - glBindVertexArray(VAO); - - glBindBuffer(GL_ARRAY_BUFFER, VBO); - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(MeshVertex), &vertices[0], GL_STATIC_DRAW); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); - - // Set all the properties of the vertices -#define ENABLE_F_VERTEX_ATTRIB(index, member) \ - glEnableVertexAttribArray(index); \ - glVertexAttribPointer(index, \ - sizeof(MeshVertex::member) / sizeof(float), GL_FLOAT, \ - GL_FALSE, \ - sizeof(MeshVertex), \ - reinterpret_cast(offsetof(MeshVertex, member))) - - ENABLE_F_VERTEX_ATTRIB(0, Position); - ENABLE_F_VERTEX_ATTRIB(1, Normal); - ENABLE_F_VERTEX_ATTRIB(2, TexCoords); - ENABLE_F_VERTEX_ATTRIB(3, Color); -#undef ENABLE_F_VERTEX_ATTRIB - } - - - Mesh::~Mesh() { - glDeleteVertexArrays(1, &VAO); - glDeleteBuffers(1, &VBO); - glDeleteBuffers(1, &EBO); - } - - Mesh::Mesh(Mesh &&other) noexcept { - BUFFERS_MV_FROM_TO(other, this); - - vertices = std::move(other.vertices); - indices = std::move(other.indices); - materialIndex = other.materialIndex; - } - Mesh &Mesh::operator=(Mesh &&other) noexcept { - if (this != &other) { - glDeleteVertexArrays(1, &VAO); - glDeleteBuffers(1, &VBO); - glDeleteBuffers(1, &EBO); - - BUFFERS_MV_FROM_TO(other, this); - - vertices = std::move(other.vertices); - indices = std::move(other.indices); - materialIndex = other.materialIndex; - } - return *this; - } - -#pragma endregion -}; - diff --git a/src/engine/loader/scene.h b/src/engine/loader/scene.h deleted file mode 100644 index a6df0b1..0000000 --- a/src/engine/loader/scene.h +++ /dev/null @@ -1,112 +0,0 @@ -#ifndef SCENE_H -#define SCENE_H - -#include -#include -#include -#include - -namespace Engine { - class GraphicsShader; - namespace Manager { - class TextureManager; - } -} - - -// TODO: Bones and animations -// TODO: We should stop using paths as IDs... maybe pull a minecraft and use some sort of namespace path hybrid? - -namespace Engine::Loader { - // TODO: Somehow couple materials and shaders. Perhaps materials should hold shared pointers to their shaders? - // Materials are just data associated with a shader. They don't necessarily need to be tied to Mesh objects in any way - struct Material { - std::string diffusePath; - std::string specularPath; - float shininess; - - std::expected PopulateShader(const GraphicsShader &shader, Manager::TextureManager &textureManager) const; - }; - - struct MeshVertex { - glm::vec3 Position; - glm::vec3 Normal; - // Yes, texture coordinates can be 3D - // We only support a single set of texture coordinates atm - glm::vec2 TexCoords; - // We only support a single vertex color atm - glm::vec4 Color; - }; - /*! - * A mesh is a piece of geometry with a single material. - * It manages its own OpenGL buffers. - */ - class Mesh { - public: - - // TODO: We don't technically need to store the vertices and indices in the mesh object, since they're in the OpenGL buffers - // The geometry will still need to be stored for the collision system, so it might be best to split the mesh into a data version and a renderable version? - std::vector vertices; - std::vector indices; - unsigned int materialIndex; - - // TODO: Moving vertices when constructing the mesh, only to move the entire mesh when making a scene seems a bit wasteful - // A bit of a weird constructor, but moving in the vectors allows us to avoid copying them - Mesh( - std::vector&& vertices, - std::vector&& indices, - unsigned int materialIndex - ); - ~Mesh(); - - // Non-copyable - Mesh(const Mesh&) = delete; - Mesh& operator=(const Mesh&) = delete; - // Moveable - Mesh(Mesh&& other) noexcept; - Mesh& operator=(Mesh&& other) noexcept; - - void bindGlMesh() const; - - private: - unsigned int VAO{}, VBO{}, EBO{}; - /*! - * Sets up the OpenGL buffers for this mesh. - * @note Leaves the VAO bound. - */ - void setupGlMesh(); - }; - - struct Node { - std::vector children; - glm::mat4x4 transform; - - std::vector meshIndices; - }; - - struct Scene { - Node rootNode; - std::vector meshes; - std::vector materials; - - Scene( - const Node &rootNode, - std::vector&& meshes, - const std::vector &materials - ) noexcept; - - // Non-copyable - Scene(const Scene&) = delete; - Scene& operator=(const Scene&) = delete; - // Moveable - Scene(Scene&& other) noexcept; - Scene& operator=(Scene&& other) noexcept; - - std::expected Draw(Manager::TextureManager &textureManager, const GraphicsShader &shader, const glm::mat4 &modelTransform) const; - }; - - std::expected loadScene(const std::string &path); -}; - - -#endif diff --git a/src/engine/loader/shader/compute_shader.h b/src/engine/loader/shader/compute_shader.h deleted file mode 100644 index 7453799..0000000 --- a/src/engine/loader/shader/compute_shader.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef COMPUTE_SHADER_H -#define COMPUTE_SHADER_H -#include -#include - -#include "shader_program.h" - -namespace Engine { - class ComputeShader : public ShaderProgram { - public: - explicit ComputeShader(const std::string &computeFilePath) - : ShaderProgram({ - {computeFilePath, GL_COMPUTE_SHADER} - }) {}; - }; -} - -#endif diff --git a/src/engine/loader/shader/graphics_shader.h b/src/engine/loader/shader/graphics_shader.h deleted file mode 100644 index ede79ba..0000000 --- a/src/engine/loader/shader/graphics_shader.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef GRAPHICS_SHADER_H -#define GRAPHICS_SHADER_H -#include -#include -#include - -#include "shader_program.h" - -namespace Engine { - class GraphicsShader : public ShaderProgram { - public: - GraphicsShader(const std::string &vertexFilePath, const std::string &fragmentFilePath) - : ShaderProgram({ - {vertexFilePath, GL_VERTEX_SHADER}, - {fragmentFilePath, GL_FRAGMENT_SHADER} - }) {}; - GraphicsShader(const std::string &vertexFilePath, const std::string &geometryFilePath, const std::string &fragmentFilePath) - : ShaderProgram({ - {vertexFilePath, GL_VERTEX_SHADER}, - {geometryFilePath, GL_GEOMETRY_SHADER}, - {fragmentFilePath, GL_FRAGMENT_SHADER} - }) {}; - /*! - * @brief Construct a shader program from multiple files - * @param filePaths A vector of pairs of file paths and shader types (GL_VERTEX_SHADER, GL_FRAGMENT_SHADER...) - */ - explicit GraphicsShader(const std::vector> &filePaths) - : ShaderProgram(filePaths) {}; - }; -} - -#endif diff --git a/src/engine/loader/shader/shader_program.cpp b/src/engine/loader/shader/shader_program.cpp deleted file mode 100644 index 5ec8e09..0000000 --- a/src/engine/loader/shader/shader_program.cpp +++ /dev/null @@ -1,231 +0,0 @@ -#include - -#include -#include - -#include "engine/logging.h" -#include "engine/loader/generic.h" - -#include "shader_program.h" - - -namespace Engine { - ShaderProgram::ShaderProgram(const std::vector> &filePaths) : programID(0) { - std::vector shaderIDs; - shaderIDs.reserve(filePaths.size()); - - // Load all shaders - for (const auto &[filePath, shaderType] : filePaths) { - const std::expected shaderID = loadShader(filePath, shaderType); - if (!shaderID.has_value()) { - for (const auto &id : shaderIDs) - glDeleteShader(id); - throw std::runtime_error("Failed to compile shader " + filePath + ": " NL_INDENT + shaderID.error()); - } - shaderIDs.push_back(shaderID.value()); - } - - const unsigned int progID = glCreateProgram(); - // Attach all shaders - for (const auto &shaderID : shaderIDs) { - glAttachShader(progID, shaderID); - glDeleteShader(shaderID); // Flagged for deletion when no longer attached to anything - } - - glLinkProgram(progID); - - int result = GL_FALSE; - glGetProgramiv(progID, GL_LINK_STATUS, &result); - if (result == GL_TRUE) { - for (const auto &shaderID : shaderIDs) - glDetachShader(progID, shaderID); // Detach and delete shaders, we only need what is linked in the program now - programID = progID; - return; - } - - int infoLogLength = 0; - glGetProgramiv(progID, GL_INFO_LOG_LENGTH, &infoLogLength); - if (infoLogLength == 0) { - glDeleteProgram(progID); // Automatically detaches shaders, we don't need to loop through them - throw std::runtime_error("Program linking failed: No info log available"); - } - - std::vector infoLog(infoLogLength); - glGetProgramInfoLog(progID, infoLogLength, nullptr, infoLog.data()); - glDeleteProgram(progID); // Automatically detaches shaders, we don't need to loop through them - throw std::runtime_error("Program linking failed: " NL_INDENT + std::string(infoLog.data())); - } - - ShaderProgram::~ShaderProgram() { - glDeleteProgram(programID); - } - - ShaderProgram::ShaderProgram(ShaderProgram &&other) noexcept { - programID = other.programID; - other.programID = 0; - } - ShaderProgram &ShaderProgram::operator=(ShaderProgram &&other) noexcept { - if (this != &other) { - glDeleteProgram(programID); - programID = other.programID; - other.programID = 0; - } - return *this; - } - - std::expected ShaderProgram::loadShader( - const std::string &filePath, - const unsigned int shaderType - ) { - std::expected shaderSrc = Loader::readTextFile(filePath); - if (!shaderSrc.has_value()) - return std::unexpected(FW_UNEXP(shaderSrc, "Failed to read shader file")); - shaderSrc = preprocessSource(shaderSrc.value()); - if (!shaderSrc.has_value()) - return std::unexpected(FW_UNEXP(shaderSrc, std::string("Failed to preprocess shader source"))); - - const unsigned int shaderID = glCreateShader(shaderType); - const char *shaderSource = shaderSrc.value().c_str(); - glShaderSource(shaderID, 1, &shaderSource, nullptr); - glCompileShader(shaderID); - - int result = GL_FALSE; - glGetShaderiv(shaderID, GL_COMPILE_STATUS, &result); - if (result == GL_TRUE) - return shaderID; - - int infoLogLength = 0; - glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLogLength); - if (infoLogLength == 0) { - glDeleteShader(shaderID); // Prevent leak if we failed to compile - return UNEXPECTED_REF("Shader compilation failed: No info log available"); - } - - std::vector infoLog(infoLogLength); - glGetShaderInfoLog(shaderID, infoLogLength, nullptr, infoLog.data()); - glDeleteShader(shaderID); // Prevent leak if we failed to compile - return UNEXPECTED_REF("Shader compilation failed: " + std::string(infoLog.data())); - } - - std::expected ShaderProgram::preprocessSource(std::string shaderSrc) { - constexpr auto includeDirective = "#include"; - constexpr auto includeDirectiveLength = strlen(includeDirective); - - std::vector processedFiles; // Avoid infinite recursion and unnecessary file reads - - std::string::size_type cursor = 0; - std::string::size_type findStart = cursor; - while ((findStart = shaderSrc.find(includeDirective, findStart)) != std::string::npos) { - cursor = findStart; - // Exit if we are in the middle of a line (ignoring leading whitespace) - while (shaderSrc[cursor - 1] == ' ' || shaderSrc[cursor - 1] == '\t') - cursor--; - if (shaderSrc[findStart - 1] != '\n') { - logWarn("Invalid include directive at " + std::to_string(findStart) + ". Expected start of line:" + shaderSrc.substr(findStart-10, 40)); - findStart++; // So we won't find the same include directive again - continue; - } - cursor = findStart; - -#define IN_BOUNDS cursor < shaderSrc.size() - - cursor += includeDirectiveLength; - if (shaderSrc[cursor] != ' ' && shaderSrc[cursor] != '\t') { - logWarn("Invalid include directive. Expected whitespace after directive"); - continue; - } - cursor++; - while (IN_BOUNDS && shaderSrc[cursor] == ' ' || shaderSrc[cursor] == '\t') - cursor++; - - if (shaderSrc[cursor] != '"') { - logWarn("Invalid include directive. Expected opening quote"); - continue; - } - cursor++; - const auto pathStart = cursor; - while (IN_BOUNDS && shaderSrc[cursor] != '"' && shaderSrc[cursor] != '\n') - cursor++; - if (shaderSrc[cursor] != '"') { - logWarn("Invalid include directive. Expected closing quote"); - continue; - } - const auto includePath = shaderSrc.substr(pathStart, cursor - pathStart); - cursor++; - - if (std::ranges::find(processedFiles, includePath) != processedFiles.end()) { - shaderSrc.replace(findStart, cursor - findStart, "// ignoring #include " + includePath + " (already included)"); - continue; - } - logDebug("Processing include file \"%s\"", includePath.c_str()); - processedFiles.push_back(includePath); - std::expected includeSrc = Loader::readTextFile(includePath); - if (!includeSrc.has_value()) - return std::unexpected(FW_UNEXP(includeSrc, - "Failed to read include: " + shaderSrc.substr(findStart, cursor - findStart))); - // Replace the include directive with the actual source - shaderSrc.replace(findStart, cursor - findStart, includeSrc.value()); - } -#undef IN_BOUNDS - - return shaderSrc; - } - - void ShaderProgram::use() const { - glUseProgram(programID); - } - - int ShaderProgram::getUniformLoc(const std::string &name) const { - return glGetUniformLocation(programID, name.c_str()); - } - - void ShaderProgram::setBool(const std::string &name, const bool value) const { - glUniform1i(getUniformLoc(name), static_cast(value)); - } - void ShaderProgram::setInt(const std::string &name, const int value) const { - glUniform1i(getUniformLoc(name), value); - } - void ShaderProgram::setFloat(const std::string &name, const float value) const { - glUniform1f(getUniformLoc(name), value); - } - - void ShaderProgram::setVec2(const std::string &name, const glm::vec2 &value) const { - glUniform2fv(getUniformLoc(name), 1, &value[0]); - } - void ShaderProgram::setVec3(const std::string &name, const glm::vec3 &value) const { - glUniform3fv(getUniformLoc(name), 1, &value[0]); - } - void ShaderProgram::setVec4(const std::string &name, const glm::vec4 &value) const { - glUniform4fv(getUniformLoc(name), 1, &value[0]); - } - - void ShaderProgram::setVec2(const std::string &name, const float x, const float y) const { - glUniform2f(getUniformLoc(name), x, y); - } - void ShaderProgram::setVec3(const std::string &name, const float x, const float y, const float z) const { - glUniform3f(getUniformLoc(name), x, y, z); - } - void ShaderProgram::setVec4(const std::string &name, const float x, const float y, const float z, const float w) const { - glUniform4f(getUniformLoc(name), x, y, z, w); - } - - void ShaderProgram::setMat2(const std::string &name, const glm::mat2 &mat) const { - glUniformMatrix2fv(getUniformLoc(name), 1, GL_FALSE, &mat[0][0]); - } - void ShaderProgram::setMat3(const std::string &name, const glm::mat3 &mat) const { - glUniformMatrix3fv(getUniformLoc(name), 1, GL_FALSE, &mat[0][0]); - } - void ShaderProgram::setMat4(const std::string &name, const glm::mat4 &mat) const { - glUniformMatrix4fv(getUniformLoc(name), 1, GL_FALSE, &mat[0][0]); - } - - std::expected ShaderProgram::bindUniformBlock(const std::string &name, const unsigned int bindingPoint) const { - const unsigned int blockIndex = glGetUniformBlockIndex(programID, name.c_str()); - if (blockIndex == GL_INVALID_INDEX) - return UNEXPECTED_REF("Failed to get uniform block index"); - - glUniformBlockBinding(programID, blockIndex, bindingPoint); - return {}; - } - -} diff --git a/src/engine/loader/shader/shader_program.h b/src/engine/loader/shader/shader_program.h deleted file mode 100644 index b325e79..0000000 --- a/src/engine/loader/shader/shader_program.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef SHADER_PROGRAM_H -#define SHADER_PROGRAM_H - -#include -#include -#include -#include - -namespace Engine { - class ShaderProgram { - protected: // ShaderProgram should never be instantiated directly, only through derived classes - explicit ShaderProgram(const std::vector> &filePaths); - public: - unsigned int programID; - - ~ShaderProgram(); - - // Non-copyable - ShaderProgram(const ShaderProgram&) = delete; - ShaderProgram& operator=(const ShaderProgram&) = delete; - // Moveable - ShaderProgram(ShaderProgram&& other) noexcept; - ShaderProgram& operator=(ShaderProgram&& other) noexcept; - - void use() const; - [[nodiscard]] int getUniformLoc(const std::string &name) const; - - private: - static std::expected loadShader( - const std::string &filePath, - unsigned int shaderType - ); - static std::expected preprocessSource(std::string shaderSrc); - - public: - void setBool(const std::string &name, bool value) const; - void setInt(const std::string &name, int value) const; - void setFloat(const std::string &name, float value) const; - - void setVec2(const std::string &name, const glm::vec2 &value) const; - void setVec3(const std::string &name, const glm::vec3 &value) const; - void setVec4(const std::string &name, const glm::vec4 &value) const; - void setVec2(const std::string &name, float x, float y) const; - void setVec3(const std::string &name, float x, float y, float z) const; - void setVec4(const std::string &name, float x, float y, float z, float w) const; - - void setMat2(const std::string &name, const glm::mat2 &mat) const; - void setMat3(const std::string &name, const glm::mat3 &mat) const; - void setMat4(const std::string &name, const glm::mat4 &mat) const; - - std::expected bindUniformBlock(const std::string &name, unsigned int bindingPoint) const; - }; -} - -#endif diff --git a/src/engine/loader/texture.cpp b/src/engine/loader/texture.cpp deleted file mode 100644 index e0cb838..0000000 --- a/src/engine/loader/texture.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#define STB_IMAGE_IMPLEMENTATION -#include -#include -#include - -#include - -#include "texture.h" - -namespace Engine::Loader { - struct ImageData { - int width, height, channelCount; - stbi_uc *imgData; - }; - /*! - * Internal helper function to load an image from a file. - * @attention You as the caller are responsible for freeing the allocated memory using `stbi_image_free`. - */ - std::expected loadImage(const char *filePath) { - int width, height, channelCount; - stbi_uc *imgData = stbi_load(filePath, &width, &height, &channelCount, 0); - if (!imgData) { - stbi_image_free(imgData); - logError("Failed to load texture \"%s\": %s", filePath, stbi_failure_reason()); - return std::unexpected(FILE_REF + std::string("Failed to load texture: \"") + filePath + "\": " + stbi_failure_reason()); - } - - return ImageData{width, height, channelCount, imgData}; - } - - GLint getChannelCount(const int channelCount) { - switch (channelCount) { - case 1: return GL_RED; - case 3: return GL_RGB; - case 4: return GL_RGBA; - default: { - logWarn("Unknown channel count %d, defaulting to single channel", channelCount); - return GL_RED; // Least likely to segfault :+1: - } - } - } - - std::expected loadTexture(const char *filePath) { - std::expected imgData = loadImage(filePath); - if (!imgData) - return std::unexpected(FW_UNEXP(imgData, "Failed to load texture")); - - logDebug("Loaded texture \"%s\" with dimensions %dx%d", filePath, imgData->width, imgData->height); - const GLint format = getChannelCount(imgData->channelCount); - - unsigned int textureID; - glGenTextures(1, &textureID); - - glBindTexture(GL_TEXTURE_2D, textureID); - glTexImage2D(GL_TEXTURE_2D, 0, format, imgData->width, imgData->height, 0, format, GL_UNSIGNED_BYTE, imgData->imgData); - glGenerateMipmap(GL_TEXTURE_2D); - stbi_image_free(imgData->imgData); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // TODO: GL_CLAMP_TO_EDGE to better support alpha textures? - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - return textureID; - } - - std::expected loadCubeMap(const std::string &filePath) { - unsigned int textureID; - glGenTextures(1, &textureID); - glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); - - const std::pmr::unordered_map dirMap = { - {GL_TEXTURE_CUBE_MAP_POSITIVE_X, "right"}, - {GL_TEXTURE_CUBE_MAP_NEGATIVE_X, "left"}, - {GL_TEXTURE_CUBE_MAP_POSITIVE_Y, "top"}, - {GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, "bottom"}, - {GL_TEXTURE_CUBE_MAP_POSITIVE_Z, "front"}, - {GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, "back"} - }; - - const auto extension_index = filePath.find_last_of('.'); - for (int i = 0; i < 6; i++) { - const GLint faceDir = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; - - const auto path = filePath.substr(0, extension_index) + "_" + dirMap.at(faceDir) + filePath.substr(extension_index); - - std::expected imgData = loadImage(path.c_str()); - if (!imgData) { - glDeleteTextures(1, &textureID); - return std::unexpected(FW_UNEXP(imgData, "Failed to load cubemap texture")); - } - - const GLint format = getChannelCount(imgData->channelCount); - glTexImage2D(faceDir, - 0, format, imgData->width, imgData->height, 0, format, GL_UNSIGNED_BYTE, imgData->imgData); - } - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - return textureID; - } -} \ No newline at end of file diff --git a/src/engine/loader/texture.h b/src/engine/loader/texture.h deleted file mode 100644 index 5d70aba..0000000 --- a/src/engine/loader/texture.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef TEXTURE_H -#define TEXTURE_H -#include -#include - -namespace Engine::Loader { - /*! - * Load a texture from a file. - * @param filePath The path to the file. - * @return The texture ID if successful, or an error message if not. - * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. - */ - std::expected loadTexture(const char* filePath); - - // TODO: Allow loading cubemap from equirectangular projection - /*! - * Loads a cubemap texture from a set of files. - * @param filePath The path to the file. The different directions are inserted before the file extension with an underscore. - * @return The texture ID if successful, or an error message if not. - * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. - * @note The file name suffixes are: `_right`, `_left`, `_top`, `_bottom`, `_front`, `_back`. - */ - std::expected loadCubeMap(const std::string &filePath); -} - -#endif //TEXTURE_H diff --git a/src/engine/logging.cpp b/src/engine/logging.cpp deleted file mode 100644 index 5e6192e..0000000 --- a/src/engine/logging.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "logging.h" - -#include - -std::string glErrorString(const GLenum errorCode) { - static const std::unordered_map map = { - {GL_NO_ERROR, "No error"}, - {GL_INVALID_ENUM, "Invalid enum"}, - {GL_INVALID_VALUE, "Invalid value"}, - {GL_INVALID_OPERATION, "Invalid operation"}, - {GL_STACK_OVERFLOW, "Stack overflow"}, - {GL_STACK_UNDERFLOW, "Stack underflow"}, - {GL_OUT_OF_MEMORY, "Out of memory"}, - {GL_INVALID_FRAMEBUFFER_OPERATION, "Invalid framebuffer operation"}, - {GL_CONTEXT_LOST, "Context lost"}, - {GL_TABLE_TOO_LARGE, "Table too large"} - }; - - const auto err = map.find(errorCode); - return err != map.end() ? err->second : "Unknown error: " + std::to_string(errorCode); -} - -GLenum glLogErrors_(const char *file, const int line) { - GLenum errorCode; - while ((errorCode = glGetError()) != GL_NO_ERROR) { - logError("%s:%d OpenGL error: (%d) %s", file, line, errorCode, glErrorString(errorCode).c_str()); - } - return errorCode; -} - -GLenum glLogErrorsExtra_(const char *file, const int line, const char *extra) { - GLenum errorCode; - while ((errorCode = glGetError()) != GL_NO_ERROR) { - logError("%s:%d OpenGL error %s: (%d) %s", file, line, extra, errorCode, glErrorString(errorCode).c_str()); - } - return errorCode; -} -GLenum glLogErrorsExtra_(const char *file, const int line, const std::string &extra) { - return glLogErrorsExtra_(file, line, extra.c_str()); -} - -void GLAPIENTRY MessageCallback( - GLenum source, - GLenum type, - GLuint id, - GLenum severity, - GLsizei length, - const GLchar* message, - const void* userParam -) { - std::unordered_map sourceMap = { - {GL_DEBUG_SOURCE_API, "API"}, - {GL_DEBUG_SOURCE_WINDOW_SYSTEM, "Window system"}, - {GL_DEBUG_SOURCE_SHADER_COMPILER, "Shader compiler"}, - {GL_DEBUG_SOURCE_THIRD_PARTY, "Third party"}, - {GL_DEBUG_SOURCE_APPLICATION, "Application"}, - {GL_DEBUG_SOURCE_OTHER, "Other"} - }; - std::unordered_map typeMap = { - {GL_DEBUG_TYPE_ERROR, "Error"}, - {GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR, "Deprecated behavior"}, - {GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, "Undefined behavior"}, - {GL_DEBUG_TYPE_PORTABILITY, "Non-portable"}, - {GL_DEBUG_TYPE_PERFORMANCE, "Performance"}, - {GL_DEBUG_TYPE_MARKER, "Marker"}, - {GL_DEBUG_TYPE_PUSH_GROUP, "Push group"}, - {GL_DEBUG_TYPE_POP_GROUP, "Pop group"}, - {GL_DEBUG_TYPE_OTHER, "Other"} - }; - std::unordered_map severityMap = { - {GL_DEBUG_SEVERITY_HIGH, "High"}, - {GL_DEBUG_SEVERITY_MEDIUM, "Medium"}, - {GL_DEBUG_SEVERITY_LOW, "Low"}, - {GL_DEBUG_SEVERITY_NOTIFICATION, "Notification"} - }; - std::unordered_map severitySDLMap = { - {GL_DEBUG_SEVERITY_HIGH, SDL_LOG_PRIORITY_CRITICAL}, // Real errors or really dangerous undefined behavior - {GL_DEBUG_SEVERITY_MEDIUM, SDL_LOG_PRIORITY_ERROR}, // Undefined behavior or major performance issues - {GL_DEBUG_SEVERITY_LOW, SDL_LOG_PRIORITY_WARN}, // Redundant state change or unimportant undefined behavior - {GL_DEBUG_SEVERITY_NOTIFICATION, SDL_LOG_PRIORITY_VERBOSE} - }; - - const auto err = severitySDLMap.find(severity); - const auto logPriority = err != severitySDLMap.end() ? err->second : SDL_LOG_PRIORITY_CRITICAL; - -#define KEY_OR_UNKNOWN(map, key) (map.find(key) != map.end() ? map[key] : "Unknown") - logRaw(logPriority, - "OpenGL %s [%s] (%d) %s %s", - KEY_OR_UNKNOWN(sourceMap, source), - KEY_OR_UNKNOWN(typeMap, type), - id, - KEY_OR_UNKNOWN(severityMap, severity), - message - ); -#undef KEY_OR_UNKNOWN -} diff --git a/src/engine/logging.h b/src/engine/logging.h deleted file mode 100644 index c416740..0000000 --- a/src/engine/logging.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef LOGGING_H -#define LOGGING_H -#include -#include -// ReSharper disable once CppUnusedIncludeDirective // Used in macros -#include - -std::string glErrorString(GLenum errorCode); - -GLenum glLogErrors_(const char *file, int line); -GLenum glLogErrorsExtra_(const char *file, int line, const char *extra); -GLenum glLogErrorsExtra_(const char *file, int line, const std::string &extra); - -#ifndef NDEBUG -#define glLogErrors() glLogErrors_(__FILE__, __LINE__) // glGetError is a bit slow -#define glLogErrorsExtra(extra) glLogErrorsExtra_(__FILE__, __LINE__, extra) -#else -#define glLogErrors() -#define glLogErrorsExtra(extra) -#endif - -#define STRINGIFY_HELPER(x) #x -#define STRINGIFY(x) STRINGIFY_HELPER(x) -#define INDENT4 " " -#define NL_INDENT "\n" INDENT4 - -#define FILE_REF std::string(__FILE__ ":" STRINGIFY(__LINE__) " ") -#define FW_UNEXP(unexpected, thisTime) (FILE_REF + thisTime + NL_INDENT + unexpected.error()) - -#define UNEXPECTED_REF(...) std::unexpected(FILE_REF + __VA_ARGS__) - - -#define logRaw(...) SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) -#define logSeverity(severity, fmt, ...) SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, severity, \ - (FILE_REF + fmt).c_str(), ##__VA_ARGS__) -#define logError(fmt, ...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, \ - (FILE_REF + fmt).c_str(), ##__VA_ARGS__) -#define logWarn(fmt, ...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, \ - (FILE_REF + fmt).c_str(), ##__VA_ARGS__) -#define logInfo(fmt, ...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, \ - (FILE_REF + fmt).c_str(), ##__VA_ARGS__) -#define logDebug(fmt, ...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, \ - (FILE_REF + fmt).c_str(), ##__VA_ARGS__) -#define logVerbose(fmt, ...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, \ - (FILE_REF + fmt).c_str(), ##__VA_ARGS__) - -void GLAPIENTRY MessageCallback( - GLenum source, - GLenum type, - GLuint id, - GLenum severity, - GLsizei length, - const GLchar* message, - const void* userParam -); - -#endif //LOGGING_H diff --git a/src/engine/manager/scene.cpp b/src/engine/manager/scene.cpp deleted file mode 100644 index c60068f..0000000 --- a/src/engine/manager/scene.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "engine/manager/scene.h" - -#include - - -namespace Engine::Manager { - SceneManager::SceneManager() - // This is so cursed... - : errorScene([] { - std::expected errorScn = Loader::loadScene(ERROR_MESH_PATH); - if (!errorScn.has_value()) - throw std::runtime_error(FW_UNEXP(errorScn, "Failed to load error model")); - return std::make_shared(std::move(errorScn.value())); - }()) {} - - std::expected, std::string> SceneManager::getScene(const std::string &scenePath) { - if (scenes.contains(scenePath)) - return scenes[scenePath].lock(); - - std::expected scene = Loader::loadScene(scenePath); - if (!scene.has_value()) { - scenes[scenePath] = errorScene; // Only error once, then use the error model - return std::unexpected(FW_UNEXP(scene, "Failed to load uncached model")); - } - auto ptr = std::make_shared(std::move(scene.value())); - scenes[scenePath] = ptr; - return ptr; - } -} \ No newline at end of file diff --git a/src/engine/manager/scene.h b/src/engine/manager/scene.h deleted file mode 100644 index be222e4..0000000 --- a/src/engine/manager/scene.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef MANAGER_MESH_H -#define MANAGER_MESH_H - -#include -#include -#include -#include -#include - -#define ERROR_MESH_PATH "resources/assets/models/error.obj" - -struct aiNode; - -namespace Engine::Manager { - // TODO: Hot reloading - // TODO: Maybe a tad *too* ready to unload unused assets? - class SceneManager { - private: - std::unordered_map> scenes; - public: - std::shared_ptr errorScene; - - SceneManager(); - - std::expected, std::string> getScene(const std::string &scenePath); - }; - -} - -#endif diff --git a/src/engine/manager/texture.cpp b/src/engine/manager/texture.cpp deleted file mode 100644 index 9444f2e..0000000 --- a/src/engine/manager/texture.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "engine/manager/texture.h" - -#include -#include - -#include "engine/loader/texture.h" - -#include - - -namespace Engine::Manager { - TextureManager::TextureManager() { - const auto errorTxt = Loader::loadTexture(ERROR_TEXTURE_PATH); - if (!errorTxt.has_value()) - throw std::runtime_error("Failed to load error texture: " + errorTxt.error()); - errorTexture = errorTxt.value(); - } - - TextureManager::~TextureManager() { - clear(); - glDeleteTextures(1, &errorTexture); - } - - void TextureManager::clear() { - for (const auto &[path, textureID] : textures) - glDeleteTextures(1, &textureID); - textures.clear(); - } - - std::expected TextureManager::getTexture(const std::string &texturePath, const TextureType type) { - if (texturePath.empty()) - return errorTexture; - - if (textures.contains(texturePath)) - return textures[texturePath]; - - std::expected texture; - switch (type) { - case TextureType::TEXTURE_2D: texture = Loader::loadTexture(texturePath.c_str()); break; - case TextureType::CUBEMAP: texture = Loader::loadCubeMap(texturePath); break; - default: - return std::unexpected("Unknown texture type"); - } - if (!texture.has_value()) { - textures[texturePath] = errorTexture; // Only error once, then use the error texture - return std::unexpected(FW_UNEXP(texture, "Failed to load uncached texture")); - } - return textures[texturePath] = texture.value(); - } - - bool TextureManager::unloadTexture(const std::string &texturePath) { - if (!textures.contains(texturePath)) - return false; - glDeleteTextures(1, &textures[texturePath]); - textures.erase(texturePath); - return true; - } - - -} diff --git a/src/engine/manager/texture.h b/src/engine/manager/texture.h deleted file mode 100644 index 88d5ae8..0000000 --- a/src/engine/manager/texture.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef MANAGER_TEXTURE_H -#define MANAGER_TEXTURE_H - -#include -#include -#include - -#define ERROR_TEXTURE_PATH "resources/assets/textures/error.png" - -namespace Engine { - enum class TextureType { - TEXTURE_2D, - CUBEMAP - }; -} - -namespace Engine::Manager { - /*! - * Class that stores and manages OpenGL texture IDs to avoid loading the same texture multiple times - */ - class TextureManager { - private: - std::unordered_map textures; - // TODO: Hot reloading - // TODO: Implement a reference counting system like we have for scenees - - public: - unsigned int errorTexture; - - TextureManager(); - ~TextureManager(); - - /*! - * @brief Get the OpenGL texture ID associated with a texture path, loading it if necessary - * @param texturePath The path to the texture - * @param type The type of texture to load. Defaults to a 2D texture - * @return The texture ID or an error message - */ - std::expected getTexture(const std::string &texturePath, TextureType type = TextureType::TEXTURE_2D); - - /*! - * @brief Unload a texture - * @param texturePath The path to the texture - * @return Whether the texture was successfully unloaded - */ - bool unloadTexture(const std::string &texturePath); - - /*! - * @brief Unload all textures (except the error texture) - */ - void clear(); - }; -} - -#endif diff --git a/src/engine/render/frame_buffer.h b/src/engine/render/frame_buffer.h index 5598e51..408ddc5 100644 --- a/src/engine/render/frame_buffer.h +++ b/src/engine/render/frame_buffer.h @@ -1,6 +1,5 @@ -#ifndef FRAME_BUFFER_H -#define FRAME_BUFFER_H -#include +#pragma once +#include class FrameBuffer { @@ -12,7 +11,7 @@ class FrameBuffer { unsigned int ColorTextureID{}; unsigned int DepthStencilTextureID{}; - [[nodiscard]] WindowSize getSize() const { return {width, height}; } + [[nodiscard]] Size2Di getSize() const { return {width, height}; } FrameBuffer(int width, int height); ~FrameBuffer(); @@ -35,4 +34,3 @@ class FrameBuffer { }; -#endif diff --git a/src/engine/render/overlay.cpp b/src/engine/render/overlay.cpp index 8190ffd..ea16660 100644 --- a/src/engine/render/overlay.cpp +++ b/src/engine/render/overlay.cpp @@ -1,47 +1,57 @@ #include "overlay.h" -#include +#include +#include +#include "engine/util/geometry.h" + + +ScreenOverlay::ScreenOverlay() { + shader = engineState->resourceManager.loadShader({ + {Resource::ShaderType::VERTEX, "resources/assets/shaders/overlay.vert"}, + {Resource::ShaderType::FRAGMENT, "resources/assets/shaders/overlay.frag"} + }); -ScreenOverlay::ScreenOverlay() : shader("resources/assets/shaders/overlay.vert", "resources/assets/shaders/overlay.frag") { glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); - glGenBuffers(1, &EBO); - + glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(ScreenSpaceQuadVertices), ScreenSpaceQuadVertices.data(), GL_STATIC_DRAW); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(ScreenSpaceQuadIndices), ScreenSpaceQuadIndices.data(), GL_STATIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr); glEnableVertexAttribArray(0); } void ScreenOverlay::draw(const unsigned int texture) const { - shader.use(); - shader.setInt("screenTexture", 0); + shader->use(); + shader->setInt("screenTexture", 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glBindVertexArray(VAO); - glDrawElements(GL_TRIANGLES, sizeof(ScreenSpaceQuadIndices) / sizeof(unsigned int), GL_UNSIGNED_INT, nullptr); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } ScreenOverlay::~ScreenOverlay() { glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); - glDeleteBuffers(1, &EBO); } ScreenOverlay &ScreenOverlay::operator=(ScreenOverlay &&other) noexcept { if (this != &other) { glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); - glDeleteBuffers(1, &EBO); - BUFFERS_MV_FROM_TO(other, this); + this->VAO = other.VAO; + this->VBO = other.VBO; + other.VAO = 0; + other.VBO = 0; shader = std::move(other.shader); } return *this; } ScreenOverlay::ScreenOverlay(ScreenOverlay &&other) noexcept: shader(std::move(other.shader)) { - BUFFERS_MV_FROM_TO(other, this); + this->VAO = other.VAO; + this->VBO = other.VBO; + other.VAO = 0; + other.VBO = 0; } diff --git a/src/engine/render/overlay.h b/src/engine/render/overlay.h index 0b737bf..5fda18a 100644 --- a/src/engine/render/overlay.h +++ b/src/engine/render/overlay.h @@ -1,12 +1,10 @@ -#ifndef ScreenOverlay_H -#define ScreenOverlay_H - -#include +#pragma once +#include class ScreenOverlay { private: - Engine::GraphicsShader shader; - unsigned int VAO{}, VBO{}, EBO{}; + std::shared_ptr shader; + unsigned int VAO{}, VBO{}; public: ScreenOverlay(); @@ -23,4 +21,3 @@ class ScreenOverlay { }; -#endif diff --git a/src/engine/resources/material.h b/src/engine/resources/material.h new file mode 100644 index 0000000..8fb2880 --- /dev/null +++ b/src/engine/resources/material.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include "shader.h" +#include "texture.h" + +namespace Resource +{ + /*! A fairly light wrapper around a shader containing PBR material data to pass to it. */ + struct PBRMaterial { + std::shared_ptr shader; + + std::shared_ptr albedo{}; + std::shared_ptr normal{}; + std::shared_ptr roughness{}; + std::shared_ptr metallic{}; + std::shared_ptr ambientOcclusion{}; + }; +} diff --git a/src/engine/resources/mesh.cpp b/src/engine/resources/mesh.cpp new file mode 100644 index 0000000..28a9212 --- /dev/null +++ b/src/engine/resources/mesh.cpp @@ -0,0 +1,118 @@ +#include "mesh.h" + +#include +#include + +#include + +namespace Resource { + Mesh::~Mesh() { + glDeleteVertexArrays(1, &VAO); + glDeleteBuffers(1, &VBO); + glDeleteBuffers(1, &EBO); + } + + void Mesh::rebuildGl() { + // where the "re" in rebuild comes in :3. delete on 0 is no-op + glDeleteVertexArrays(1, &VAO); + glDeleteBuffers(1, &VBO); + glDeleteBuffers(1, &EBO); + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, + static_cast(vertices.size() * sizeof(MeshVertex)), + vertices.data(), GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, + static_cast(indices.size() * sizeof(unsigned int)), + indices.data(), GL_STATIC_DRAW); + +#define ENABLE_F_VERTEX_ATTRIB(index, member) \ + glEnableVertexAttribArray(index); \ + glVertexAttribPointer(index, \ + sizeof(MeshVertex::member) / sizeof(float), GL_FLOAT, \ + GL_FALSE, \ + sizeof(MeshVertex), \ + reinterpret_cast(offsetof(MeshVertex, member))) + // Set all the properties of the vertices + ENABLE_F_VERTEX_ATTRIB(0, Position); + ENABLE_F_VERTEX_ATTRIB(1, Normal); + ENABLE_F_VERTEX_ATTRIB(2, TexCoords); +#undef ENABLE_F_VERTEX_ATTRIB + } + +#pragma region Move Semantics + Mesh& Mesh::operator=(Mesh&& other) noexcept { + if (this != &other) { + glDeleteVertexArrays(1, &VAO); + glDeleteBuffers(1, &VBO); + glDeleteBuffers(1, &EBO); + + VAO = other.VAO; + VBO = other.VBO; + EBO = other.EBO; + vertices = std::move(other.vertices); + indices = std::move(other.indices); + material = std::move(other.material); + + other.VAO = 0; + other.VBO = 0; + other.EBO = 0; + } + return *this; + } + Mesh::Mesh(Mesh&& other) noexcept { + VAO = other.VAO; + VBO = other.VBO; + EBO = other.EBO; + vertices = std::move(other.vertices); + indices = std::move(other.indices); + material = std::move(other.material); + + other.VAO = 0; + other.VBO = 0; + other.EBO = 0; + } +#pragma endregion + + void Mesh::bindBuffers() const { + glBindVertexArray(VAO); + } + + Expected Mesh::Draw(const glm::mat4& modelTransform) const { + const std::shared_ptr shader = material->shader; + if (!shader) + return std::unexpected(ERROR("Mesh material has no shader")); + shader->use(); + shader->setMat4("model", modelTransform); + shader->setMat3("mTransposed", glm::mat3(glm::transpose(glm::inverse(modelTransform)))); + // Populate shader material uniforms + { + Engine::ResourceManager& resourceManager = engineState->resourceManager; + unsigned int workingIndex = 0; +#define BIND_TEX(key, value) \ + shader->setInt("material." key, static_cast(workingIndex)); \ + glActiveTexture(GL_TEXTURE0 + workingIndex); \ + glBindTexture(GL_TEXTURE_2D, value ? value->textureID : resourceManager.errorTexture->textureID); \ + workingIndex++ + + BIND_TEX("albedo_tex", material->albedo); + BIND_TEX("normal_tex", material->normal); + BIND_TEX("roughness_tex", material->roughness); + BIND_TEX("metallic_tex", material->metallic); + BIND_TEX("ambientOcclusion_tex", material->ambientOcclusion); +#undef BIND_TEX + } + + bindBuffers(); + assert(!indices.empty() && indices.size() < std::numeric_limits::max()); + glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, nullptr); + return {}; + } + +} diff --git a/src/engine/resources/mesh.h b/src/engine/resources/mesh.h new file mode 100644 index 0000000..5ec983c --- /dev/null +++ b/src/engine/resources/mesh.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "material.h" + + +namespace Resource { + struct MeshVertex { + glm::vec3 Position = glm::vec3(0.0f); + glm::vec3 Normal = glm::vec3(0.0f); + // We only support a single set of texture coordinates atm + glm::vec2 TexCoords = glm::vec2(0.0f); + }; + + /*! + * A mesh is a piece of geometry with a single material. + * It manages its own OpenGL buffers. + */ + class Mesh { + public: + std::vector vertices; + std::vector indices; + std::shared_ptr material; + + unsigned int VAO{}, VBO{}, EBO{}; + public: + Mesh() = default; + ~Mesh(); + std::string name; + + /*! Binds the mesh's VAO. */ + void bindBuffers() const; + /*! (Re)creates the OpenGL buffers for this mesh based on its current data. */ + void rebuildGl(); + + Expected Draw(const glm::mat4& modelTransform) const; + + // Non-copyable + Mesh(const Mesh&) = delete; + Mesh& operator=(const Mesh&) = delete; + // Moveable + Mesh(Mesh&& other) noexcept; + Mesh& operator=(Mesh&& other) noexcept; + }; +} diff --git a/src/engine/resources/resource_manager.cpp b/src/engine/resources/resource_manager.cpp new file mode 100644 index 0000000..623f816 --- /dev/null +++ b/src/engine/resources/resource_manager.cpp @@ -0,0 +1,190 @@ +#include "resource_manager.h" + +#include +#include + +// Generated header files for embedded resources +#include +#include +#include +#include + + +namespace Engine { + ResourceManager::ResourceManager() { + // Init error resources to an almost valid state + errorShader = std::make_shared(0); + errorTexture = std::make_shared(0); + errorScene = std::make_shared(); + } + + Expected ResourceManager::populateErrorResources() + { + // TEXTURE + const auto tmpTex = Resource::Loading::loadTexture(BIN_ERROR_PNG.data(), BIN_ERROR_PNG.size()); + if (!tmpTex.has_value()) + return std::unexpected(FW_ERROR(tmpTex.error(), "Failed to load error texture")); + errorTexture = std::make_shared(tmpTex.value()); + // CUBEMAP + const auto tmpCubemap = Resource::Loading::loadCubemapSingle(BIN_ERROR_PNG.data(), BIN_ERROR_PNG.size()); + if (!tmpCubemap.has_value()) + return std::unexpected(FW_ERROR(tmpCubemap.error(), "Failed to load error cubemap")); + errorCubemap = std::make_shared(tmpCubemap.value()); + + // SHADER + const auto vertShaderID = Resource::Loading::loadGLShaderSource( + std::string(BIN_ERROR_SHADER_VERT.begin(), BIN_ERROR_SHADER_VERT.end()), + Resource::ShaderType::VERTEX + ); + if (!vertShaderID.has_value()) + return std::unexpected(FW_ERROR(vertShaderID.error(), "Failed to load error vertex shader")); + const auto fragShaderID = Resource::Loading::loadGLShaderSource( + std::string(BIN_ERROR_SHADER_FRAG.begin(), BIN_ERROR_SHADER_FRAG.end()), + Resource::ShaderType::FRAGMENT + ); + if (!fragShaderID.has_value()) + return std::unexpected(FW_ERROR(fragShaderID.error(), "Failed to load error fragment shader")); + errorShader = std::make_shared(std::vector{ + vertShaderID.value(), + fragShaderID.value() + }); + + // SCENE + std::expected tmpScene = Resource::Loading::loadScene(BIN_ERROR_OBJ.data(), BIN_ERROR_OBJ.size()); + if (!tmpScene.has_value()) + return std::unexpected(FW_ERROR(tmpScene.error(), "Failed to load error scene")); + errorScene = std::make_shared(std::move(tmpScene.value())); + + return {}; + } + + std::shared_ptr + ResourceManager::loadScene(const std::string& scenePath) + { + SPDLOG_DEBUG("Loading scene: {}", scenePath); + if (errorScene == nullptr) + throw std::runtime_error("Error scene is uninitialised. Refusing to proceed."); + + if (scenes.contains(scenePath)) { + if (scenes[scenePath].expired()) + scenes.erase(scenePath); + else + return scenes[scenePath].lock(); + } + + std::expected scene = Resource::Loading::loadScene(scenePath); + if (!scene.has_value()) { + scenes[scenePath] = errorScene; // Only error once, then use the error scene + reportError(FW_ERROR(scene.error(), "Failed to load uncached scene")); + return errorScene; + } + auto ptr = std::make_shared(std::move(scene.value())); + scenes[scenePath] = ptr; + return ptr; + } + + std::shared_ptr + ResourceManager::loadTexture(const std::string& texturePath) + { + SPDLOG_DEBUG("Loading texture: {}", texturePath); + if (errorTexture == nullptr || errorTexture->textureID == 0) + throw std::runtime_error("Error texture is uninitialised or invalid. Refusing to proceed."); + + if (textures.contains(texturePath)) { + if (textures[texturePath].expired()) + textures.erase(texturePath); + else + return textures[texturePath].lock(); + } + + std::expected textureID = Resource::Loading::loadTexture(texturePath.c_str()); + if (!textureID.has_value()) { + textures[texturePath] = errorTexture; // Only error once, then use the error texture + reportError(FW_ERROR(textureID.error(), "Failed to load uncached texture")); + return errorTexture; + } + + auto ptr = std::make_shared(textureID.value()); + textures[texturePath] = ptr; + return ptr; + } + + std::shared_ptr + ResourceManager::loadCubemap(const std::string& cubemapPath) + { + SPDLOG_DEBUG("Loading cubemap: {}", cubemapPath); + if (errorCubemap == nullptr || errorCubemap->textureID == 0) + throw std::runtime_error("Error cubemap is uninitialised or invalid. Refusing to proceed."); + + // TODO: This could conflict with a texture with the same name... + // I need to figure out a better solution for identifying resources + if (textures.contains(cubemapPath)) { + if (textures[cubemapPath].expired()) + textures.erase(cubemapPath); + else + return textures[cubemapPath].lock(); + } + + std::expected cubemapID = Resource::Loading::loadCubemap(cubemapPath); + if (!cubemapID.has_value()) { + textures[cubemapPath] = errorCubemap; // Only error once, then use the error cubemap + reportError(FW_ERROR(cubemapID.error(), "Failed to load uncached cubemap")); + return errorCubemap; + } + + auto ptr = std::make_shared(cubemapID.value()); + textures[cubemapPath] = ptr; + return ptr; + } + + std::shared_ptr + ResourceManager::loadShader(const std::map& shaders) + { + SPDLOG_DEBUG("Loading shaders: {}", fmt::join(shaders, " & ")); + if (errorShader == nullptr || errorShader.get()->programID == 0) + throw std::runtime_error("Error shader is uninitialised or invalid. Refusing to proceed."); + + std::string jointPath; // Used as an identifier for this specific combo of shaders + for (const auto& [type, path] : shaders) + jointPath += path + std::to_string(type); + + if (this->shaders.contains(jointPath)) { + if (this->shaders[jointPath].expired()) + this->shaders.erase(jointPath); + else + return this->shaders[jointPath].lock(); + } + + std::vector shaderIDs; + shaderIDs.reserve(shaders.size()); + for (const auto& [type, path] : shaders) { + std::expected shaderID = Resource::Loading::loadGLShaderFile(path, type); + if (!shaderID.has_value()) { + this->shaders[jointPath] = errorShader; // Only error once, then use the error shader + reportError(FW_ERROR(shaderID.error(), "Failed to load uncached shader")); + return errorShader; + } + shaderIDs.push_back(shaderID.value()); + } + + auto ptr = std::make_shared(shaderIDs); + this->shaders[jointPath] = ptr; + return ptr; + } + + + std::shared_ptr ResourceManager::loadShader(std::string computePath) + { return loadShader({ + {Resource::ShaderType::COMPUTE, computePath} }); } + + std::shared_ptr ResourceManager::loadShader(std::string vertexPath, std::string fragmentPath) + { return loadShader({ + {Resource::ShaderType::VERTEX, vertexPath}, + {Resource::ShaderType::FRAGMENT, fragmentPath}}); } + + std::shared_ptr ResourceManager::loadShader(std::string vertexPath, std::string geometryPath, std::string fragmentPath) + { return loadShader({ + {Resource::ShaderType::VERTEX, vertexPath}, + {Resource::ShaderType::GEOMETRY, geometryPath}, + {Resource::ShaderType::FRAGMENT, fragmentPath}}); } +} diff --git a/src/engine/resources/resource_manager.h b/src/engine/resources/resource_manager.h new file mode 100644 index 0000000..7459e15 --- /dev/null +++ b/src/engine/resources/resource_manager.h @@ -0,0 +1,57 @@ +#pragma once +#include +#include +#include +#include + +#include "engine/resources/scene.h" +#include "engine/resources/shader.h" +#include "engine/resources/texture.h" + +namespace Engine { + class ResourceManager { + // TODO: Hot reloading + private: + std::unordered_map> shaders{}; + std::unordered_map> textures{}; + std::unordered_map> scenes{}; + public: + std::shared_ptr errorShader; + std::shared_ptr errorTexture; + std::shared_ptr errorCubemap; + std::shared_ptr errorScene; + + /*! + * @brief Creates a resource manager. + * @note The constructor will not load any resources and the manager will be in a largely INVALID state. + * You must call \ref populateErrorResources() "populateErrorResources()" to load the error resources. + */ + ResourceManager(); + /*! + * @brief Loads the error resources into the resource manager. + * @note This can not be called before the constructor, as it requires an already constructed resource manager to construct some resources. + */ + [[nodiscard]] Expected populateErrorResources(); + + public: + // Shader + [[nodiscard]] std::shared_ptr + loadShader(const std::map& shaders); + [[nodiscard]] std::shared_ptr + loadShader(std::string computePath); + [[nodiscard]] std::shared_ptr + loadShader(std::string vertexPath, std::string fragmentPath); + [[nodiscard]] std::shared_ptr + loadShader(std::string vertexPath, std::string geometryPath, std::string fragmentPath); + + // Texture + [[nodiscard]] std::shared_ptr + loadTexture(const std::string &texturePath); + [[nodiscard]] std::shared_ptr + loadCubemap(const std::string &cubemapPath); + + // Scene + [[nodiscard]] std::shared_ptr + loadScene(const std::string &scenePath); + }; +} diff --git a/src/engine/resources/scene.cpp b/src/engine/resources/scene.cpp new file mode 100644 index 0000000..8d3807c --- /dev/null +++ b/src/engine/resources/scene.cpp @@ -0,0 +1,198 @@ +#include "scene.h" + +#include +#include +#include +#include +#include + +#include "engine/resources/mesh.h" + +// TODO: Put more consideration into this depending on our needs (for example mesh sorting?) +// This is just a super simple first pass set of flags where much consideration hasn't been put in +constexpr auto ASSIMP_FLAGS = ( + aiProcess_Triangulate + | aiProcess_FlipUVs + | aiProcess_OptimizeMeshes + | aiProcess_OptimizeGraph + ); + + +#define UNPACK_MAT4(aiMat) { \ + aiMat.a1, aiMat.a2, aiMat.a3, aiMat.a4, \ + aiMat.b1, aiMat.b2, aiMat.b3, aiMat.b4, \ + aiMat.c1, aiMat.c2, aiMat.c3, aiMat.c4, \ + aiMat.d1, aiMat.d2, aiMat.d3, aiMat.d4 \ +} +#define UNPACK_VEC2(aiVec) {aiVec.x, aiVec.y} +#define UNPACK_VEC3(aiVec) {aiVec.x, aiVec.y, aiVec.z} +#define UNPACK_VEC4(aiVec) {aiVec.x, aiVec.y, aiVec.z, aiVec.w} +#define UNPACK_RGBA(aiColor) {aiColor.r, aiColor.g, aiColor.b, aiColor.a} + +namespace Resource +{ + Expected Scene::DrawNode(const Node& node, const glm::mat4& parentTransform) const { // NOLINT(*-no-recursion) + const glm::mat4 transform = parentTransform * node.transform; + for (const unsigned int meshIndex : node.meshIndices) { + if (meshIndex >= meshes.size()) + return std::unexpected(ERROR("Mesh index out of bounds")); + Expected result = meshes[meshIndex].Draw(transform); + if (!result.has_value()) + return std::unexpected(FW_ERROR(result.error(), "Failed to draw mesh")); + } + + for (const Node& child : node.children) { + Expected result = DrawNode(child, transform); + if (!result.has_value()) + return std::unexpected(FW_ERROR(result.error(), "Failed to draw child node")); + } + + return {}; + } + + Expected Scene::Draw(const glm::mat4& transform) const { + return DrawNode(root, transform); + } +} + +namespace Resource::Loading { + Expected loadScene(const std::string &path) + { + Assimp::Importer importer; + const aiScene* loadedScene = importer.ReadFile(path.c_str(), ASSIMP_FLAGS); + if (!loadedScene || loadedScene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !loadedScene->mRootNode) + return std::unexpected(ERROR(std::string("Failed to load scene file: ") + importer.GetErrorString())); + + Expected scene = loadScene(*loadedScene); + if (!scene.has_value()) + return std::unexpected(FW_ERROR(scene.error(), "Failed to load scene from file")); + return scene; + } + Expected loadScene(const unsigned char* data, const int size) + { + Assimp::Importer importer; + const aiScene* loadedScene = importer.ReadFileFromMemory(data, size, ASSIMP_FLAGS); + if (!loadedScene || loadedScene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !loadedScene->mRootNode) + return std::unexpected(ERROR(std::string("Failed to load scene data: ") + importer.GetErrorString())); + + Expected scene = loadScene(*loadedScene); + if (!scene.has_value()) + return std::unexpected(FW_ERROR(scene.error(), "Failed to load scene from data")); + return scene; + } + + Expected processNode(const aiNode* loadedNode); + Expected processMaterial(const aiMaterial* loadedMaterial); + Expected processMesh(const aiMesh* loadedMesh, const std::shared_ptr& material); + + Expected loadScene(const aiScene &scene) { + Scene resultScene; + // Materials + resultScene.materials.reserve(scene.mNumMaterials); + for (unsigned int i = 0; i < scene.mNumMaterials; i++) { + Expected material = processMaterial(scene.mMaterials[i]); + if (!material.has_value()) + return std::unexpected(FW_ERROR(material.error(), "Failed to load material "+std::to_string(i))); + resultScene.materials.push_back(std::move(material.value())); + } + // Meshes + resultScene.meshes.reserve(scene.mNumMeshes); + for (unsigned int i = 0; i < scene.mNumMeshes; i++) { + unsigned int matIndex = scene.mMeshes[i]->mMaterialIndex; + if (matIndex >= resultScene.materials.size()) + return std::unexpected(ERROR( + "Encountered invalid mesh material index " + std::to_string(scene.mMeshes[i]->mMaterialIndex))); + Expected mesh = processMesh( + scene.mMeshes[i], + std::make_shared(resultScene.materials[matIndex]) + ); + if (!mesh.has_value()) + return std::unexpected(FW_ERROR(mesh.error(), "Failed to load mesh "+std::to_string(i))); + resultScene.meshes.push_back(std::move(mesh.value())); + } + // Nodes + auto rootNode = processNode(scene.mRootNode); + if (!rootNode.has_value()) + return std::unexpected(FW_ERROR(rootNode.error(), "Failed to load scene root node")); + resultScene.root = rootNode.value(); + + return resultScene; + } + + Expected processNode(const aiNode *loadedNode) { // NOLINT(*-no-recursion) + Node resultNode; + resultNode.transform = UNPACK_MAT4(loadedNode->mTransformation); + + resultNode.meshIndices.reserve(loadedNode->mNumMeshes); + for (unsigned int i = 0; i < loadedNode->mNumMeshes; i++) { + resultNode.meshIndices.push_back(loadedNode->mMeshes[i]); + } + + resultNode.children.reserve(loadedNode->mNumChildren); + for (unsigned int i = 0; i < loadedNode->mNumChildren; i++) { + Expected result = processNode(loadedNode->mChildren[i]); + if (!result.has_value()) + return std::unexpected(FW_ERROR(result.error(), "Failed to load child node " + std::to_string(i))); + resultNode.children.push_back(std::move(result.value())); + } + + return resultNode; + } + + Expected processMaterial(const aiMaterial *loadedMaterial) { + PBRMaterial resultMaterial{ + // TODO: Don't hardcode this + .shader = engineState->resourceManager.loadShader( + "resources/assets/shaders/vert.vert", + "resources/assets/shaders/frag.frag" + ) + }; + SPDLOG_TRACE("Loading material \"{}\"", loadedMaterial->GetName().C_Str()); + + aiString path; + if (loadedMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS) { + resultMaterial.albedo = engineState->resourceManager.loadTexture(path.C_Str()); + } + if (loadedMaterial->GetTexture(aiTextureType_NORMALS, 0, &path) == AI_SUCCESS) { + resultMaterial.normal = engineState->resourceManager.loadTexture(path.C_Str()); + } + if (loadedMaterial->GetTexture(aiTextureType_SHININESS, 0, &path) == AI_SUCCESS) { + resultMaterial.roughness = engineState->resourceManager.loadTexture(path.C_Str()); + } + if (loadedMaterial->GetTexture(aiTextureType_REFLECTION, 0, &path) == AI_SUCCESS) { + resultMaterial.metallic = engineState->resourceManager.loadTexture(path.C_Str()); + } + if (loadedMaterial->GetTexture(aiTextureType_AMBIENT_OCCLUSION, 0, &path) == AI_SUCCESS) { + resultMaterial.ambientOcclusion = engineState->resourceManager.loadTexture(path.C_Str()); + } + + return resultMaterial; + } + + Expected processMesh(const aiMesh *loadedMesh, const std::shared_ptr& material) { + Mesh resultMesh; + resultMesh.material = material; + + resultMesh.vertices.reserve(loadedMesh->mNumVertices); + for (unsigned int i = 0; i < loadedMesh->mNumVertices; i++) { + MeshVertex vertex{}; + vertex.Position = UNPACK_VEC3(loadedMesh->mVertices[i]); + vertex.Normal = UNPACK_VEC3(loadedMesh->mNormals[i]); + if (loadedMesh->mTextureCoords[0]) { // We only support a single set of texture coordinates atm + vertex.TexCoords = UNPACK_VEC2(loadedMesh->mTextureCoords[0][i]); + } + + resultMesh.vertices.push_back(vertex); + } + resultMesh.indices.reserve(loadedMesh->mNumFaces * 3); // 3 indices per face, assuming all faces are triangles + for (unsigned int i = 0; i < loadedMesh->mNumFaces; i++) { + const aiFace &face = loadedMesh->mFaces[i]; + for (unsigned int j = 0; j < face.mNumIndices; j++) + resultMesh.indices.push_back(face.mIndices[j]); + } + + resultMesh.rebuildGl(); + + return resultMesh; + } +} diff --git a/src/engine/resources/scene.h b/src/engine/resources/scene.h new file mode 100644 index 0000000..58a3554 --- /dev/null +++ b/src/engine/resources/scene.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include + +#include "engine/resources/material.h" +#include "engine/resources/mesh.h" +#include "engine/util/error.h" + + +struct aiScene; + +namespace Resource +{ + struct Node { + std::vector children; + glm::mat4x4 transform; + + std::vector meshIndices; + }; + + class Scene { + public: + Node root = {}; + std::vector meshes; + std::vector materials; + + Expected Draw(const glm::mat4& transform = glm::mat4(1.0)) const; + + private: + Expected DrawNode(const Node& node, const glm::mat4& parentTransform) const; + + public: + // // Non-copyable + // Scene(const Scene&) = delete; + // Scene& operator=(const Scene&) = delete; + // // Moveable + // Scene(Scene&& other) noexcept; + // Scene& operator=(Scene&& other) noexcept; + }; +} + +namespace Resource::Loading +{ + [[nodiscard]] Expected loadScene(const std::string& path); + [[nodiscard]] Expected loadScene(const unsigned char* data, int size); + [[nodiscard]] Expected loadScene(const aiScene& scene); +} diff --git a/src/engine/resources/shader.cpp b/src/engine/resources/shader.cpp new file mode 100644 index 0000000..e84e656 --- /dev/null +++ b/src/engine/resources/shader.cpp @@ -0,0 +1,241 @@ +#include "shader.h" + +#include +#include + +#include "engine/util/file.h" + +namespace Resource { + Shader::Shader(const unsigned int shaderProgramID) { + programID = shaderProgramID; + } + + Shader::Shader(const std::vector& shaders) { + const unsigned int progID = glCreateProgram(); + // Attach all shaders + for (const auto &shaderID : shaders) { + glAttachShader(progID, shaderID); + glDeleteShader(shaderID); // Flagged for deletion when no longer attached to anything + } + + glLinkProgram(progID); + + int result = GL_FALSE; + glGetProgramiv(progID, GL_LINK_STATUS, &result); + if (result == GL_TRUE) { + for (const auto &shaderID : shaders) + // Detach shaders, we only need what is linked in the program now + // Shaders flagged for deletion will be deleted when no longer attached to anything + glDetachShader(progID, shaderID); + programID = progID; + return; + } + + // Linking failed + int infoLogLength = 0; + glGetProgramiv(progID, GL_INFO_LOG_LENGTH, &infoLogLength); + if (infoLogLength == 0) { + glDeleteProgram(progID); // Automatically detaches shaders, we don't need to loop through them + throw std::runtime_error("Program linking failed. No info log available"); + } + + std::vector infoLog(infoLogLength); + glGetProgramInfoLog(progID, infoLogLength, nullptr, infoLog.data()); + glDeleteProgram(progID); // Automatically detaches shaders, we don't need to loop through them + throw std::runtime_error("Program linking failed with: " + std::string(infoLog.data())); + } + + Shader::~Shader() { + glDeleteProgram(programID); + } + + Shader::Shader(Shader &&other) noexcept { + programID = other.programID; + other.programID = 0; + } + Shader &Shader::operator=(Shader &&other) noexcept { + if (this != &other) { + glDeleteProgram(programID); + programID = other.programID; + other.programID = 0; + } + return *this; + } + + void Shader::use() const { + glUseProgram(programID); + } + + int Shader::getUniformLocation(const std::string& name) const { + return glGetUniformLocation(programID, name.c_str()); + } + + Expected Shader::bindUniformBlock(const std::string& name, const unsigned int bindingPoint) const { + const unsigned int blockIndex = glGetUniformBlockIndex(programID, name.c_str()); + if (blockIndex == GL_INVALID_INDEX) + return std::unexpected(ERROR("Failed to get uniform block index for " + name)); + glUniformBlockBinding(programID, blockIndex, bindingPoint); + return {}; + } + +#pragma region Setters + void Shader::setBool(const std::string &name, const bool value) const { + glUniform1i(getUniformLocation(name), static_cast(value)); + } + void Shader::setInt(const std::string &name, const int value) const { + glUniform1i(getUniformLocation(name), value); + } + void Shader::setFloat(const std::string &name, const float value) const { + glUniform1f(getUniformLocation(name), value); + } + + void Shader::setVec2(const std::string &name, const glm::vec2 &value) const { + glUniform2fv(getUniformLocation(name), 1, &value[0]); + } + void Shader::setVec3(const std::string &name, const glm::vec3 &value) const { + glUniform3fv(getUniformLocation(name), 1, &value[0]); + } + void Shader::setVec4(const std::string &name, const glm::vec4 &value) const { + glUniform4fv(getUniformLocation(name), 1, &value[0]); + } + + void Shader::setVec2(const std::string &name, const float x, const float y) const { + glUniform2f(getUniformLocation(name), x, y); + } + void Shader::setVec3(const std::string &name, const float x, const float y, const float z) const { + glUniform3f(getUniformLocation(name), x, y, z); + } + void Shader::setVec4(const std::string &name, const float x, const float y, const float z, const float w) const { + glUniform4f(getUniformLocation(name), x, y, z, w); + } + + void Shader::setMat2(const std::string &name, const glm::mat2 &mat) const { + glUniformMatrix2fv(getUniformLocation(name), 1, GL_FALSE, &mat[0][0]); + } + void Shader::setMat3(const std::string &name, const glm::mat3 &mat) const { + glUniformMatrix3fv(getUniformLocation(name), 1, GL_FALSE, &mat[0][0]); + } + void Shader::setMat4(const std::string &name, const glm::mat4 &mat) const { + glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, &mat[0][0]); + } +#pragma endregion +} + +namespace Resource::Loading +{ + [[nodiscard]] std::expected preprocessShaderSource(std::string shaderSrc); + [[nodiscard]] std::expected compileSingleShader(const char* shaderSource, unsigned int shaderType); + + std::expected loadGLShaderFile( + const std::string& filePath, + const ShaderType shaderType + ) { + std::expected shaderSrc = readTextFile(filePath); + if (!shaderSrc.has_value()) + return std::unexpected(FW_ERROR(shaderSrc.error(), "Failed to read shader file")); + shaderSrc = preprocessShaderSource(shaderSrc.value()); + if (!shaderSrc.has_value()) + return std::unexpected(FW_ERROR(shaderSrc.error(), std::string("Failed to preprocess shader source"))); + return compileSingleShader(shaderSrc.value().c_str(), shaderType); + } + + std::expected loadGLShaderSource(const std::string& shaderSrc, const ShaderType shaderType) { + std::expected shaderSource = preprocessShaderSource(shaderSrc); + if (!shaderSource.has_value()) + return std::unexpected(FW_ERROR(shaderSource.error(), std::string("Failed to preprocess shader source"))); + return compileSingleShader(shaderSource.value().c_str(), shaderType); + } + + std::expected compileSingleShader( + const char* shaderSource, + const unsigned int shaderType + ) { + const unsigned int shaderID = glCreateShader(shaderType); + glShaderSource(shaderID, 1, &shaderSource, nullptr); + glCompileShader(shaderID); + + int result = GL_FALSE; + glGetShaderiv(shaderID, GL_COMPILE_STATUS, &result); + if (result == GL_TRUE) + return shaderID; + + // Compilation failed + int infoLogLength = 0; + glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLogLength); + if (infoLogLength == 0) { + glDeleteShader(shaderID); // Prevent leak if we failed to compile + return std::unexpected(ERROR("Shader compilation failed: No info log available")); + } + std::vector infoLog(infoLogLength); + glGetShaderInfoLog(shaderID, infoLogLength, nullptr, infoLog.data()); + glDeleteShader(shaderID); // Prevent leak if we failed to compile + return std::unexpected(ERROR("Shader compilation failed: " + std::string(infoLog.data()))); + } + + std::expected preprocessShaderSource(std::string shaderSrc) { + constexpr auto includeDirective = "#include"; + const auto includeDirectiveLength = strlen(includeDirective); + + std::vector processedFiles; // Avoid infinite recursion and unnecessary file reads + + std::string::size_type cursor = 0; + std::string::size_type findStart = cursor; + while ((findStart = shaderSrc.find(includeDirective, findStart)) != std::string::npos) { + cursor = findStart; + // Exit if we are in the middle of a line (ignoring leading whitespace) + while (shaderSrc[cursor - 1] == ' ' || shaderSrc[cursor - 1] == '\t') + cursor--; + if (shaderSrc[findStart - 1] != '\n') { + spdlog::warn( + "Invalid include directive at " + std::to_string(findStart) + ". " + "Expected start of line:" + shaderSrc.substr(findStart-10, 40)); + findStart++; // So we won't find the same include directive again + continue; + } + cursor = findStart; + +#define IN_BOUNDS cursor < shaderSrc.size() + + cursor += includeDirectiveLength; + if (shaderSrc[cursor] != ' ' && shaderSrc[cursor] != '\t') { + spdlog::warn("Invalid include directive. Expected whitespace after directive"); + continue; + } + cursor++; + while (IN_BOUNDS && (shaderSrc[cursor] == ' ' || shaderSrc[cursor] == '\t')) + cursor++; + + if (shaderSrc[cursor] != '"') { + spdlog::warn("Invalid include directive. Expected opening quote"); + continue; + } + cursor++; + const auto pathStart = cursor; + while (IN_BOUNDS && shaderSrc[cursor] != '"' && shaderSrc[cursor] != '\n') + cursor++; + if (shaderSrc[cursor] != '"') { + spdlog::warn("Invalid include directive. Expected closing quote"); + continue; + } + const auto includePath = shaderSrc.substr(pathStart, cursor - pathStart); + cursor++; + + if (std::find(processedFiles.begin(), processedFiles.end(), includePath) != processedFiles.end()) { + shaderSrc.replace(findStart, cursor - findStart, "// ignoring #include " + includePath + " (already included)"); + continue; + } + spdlog::debug("Processing include file \"%s\"", includePath.c_str()); + processedFiles.push_back(includePath); + auto includeSrc = readTextFile(includePath); + if (!includeSrc.has_value()) + return std::unexpected(FW_ERROR( + includeSrc.error(), + "Failed to read include: " + shaderSrc.substr(findStart, cursor - findStart))); + // Replace the include directive with the actual source + shaderSrc.replace(findStart, cursor - findStart, includeSrc.value()); + } +#undef IN_BOUNDS + + return shaderSrc; + } +} diff --git a/src/engine/resources/shader.h b/src/engine/resources/shader.h new file mode 100644 index 0000000..2b52b5e --- /dev/null +++ b/src/engine/resources/shader.h @@ -0,0 +1,100 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "engine/util/error.h" + +namespace Engine +{ + class ResourceManager; +} + +namespace Resource +{ + enum ShaderType + { + FRAGMENT = GL_FRAGMENT_SHADER, + GEOMETRY = GL_GEOMETRY_SHADER, + VERTEX = GL_VERTEX_SHADER, + COMPUTE = GL_COMPUTE_SHADER, + TESS_CONTROL = GL_TESS_CONTROL_SHADER, + TESS_EVALUATION = GL_TESS_EVALUATION_SHADER, + }; + + class Shader { + private: + friend class Engine::ResourceManager; + /*! + * OpenGL ID of the linked shader program. + */ + unsigned int programID; + public: + /*! + * Creates a shader program from a set of shader stages. + * @param shaders The shader stages to link into a program. + * @warning Passed stages will be deleted when we go out of scope. Do not use them elsewhere. + */ + explicit Shader(const std::vector &shaders); + /*! + * Creates a shader object around an existing shader program ID. + * @param shaderProgramID The ID of the shader program to use. + * @note There is little to no reason to use this constructor outside the engine itself. + */ + explicit Shader(unsigned int shaderProgramID); + ~Shader(); + + // Non-copyable + Shader(const Shader&) = delete; + Shader& operator=(const Shader&) = delete; + // Moveable + Shader(Shader&& other) noexcept; + Shader& operator=(Shader&& other) noexcept; + + void use() const; + // TODO: Can error, but would mean all the setter functions also have to return expected + [[nodiscard]] int getUniformLocation(const std::string &name) const; + [[nodiscard]] Expected bindUniformBlock(const std::string &name, unsigned int bindingPoint) const; + + void setBool(const std::string &name, bool value) const; + void setInt(const std::string &name, int value) const; + void setFloat(const std::string &name, float value) const; + + void setVec2(const std::string &name, const glm::vec2 &value) const; + void setVec2(const std::string &name, float x, float y) const; + void setVec3(const std::string &name, const glm::vec3 &value) const; + void setVec3(const std::string &name, float x, float y, float z) const; + void setVec4(const std::string &name, const glm::vec4 &value) const; + void setVec4(const std::string &name, float x, float y, float z, float w) const; + + void setMat2(const std::string &name, const glm::mat2 &mat) const; + void setMat3(const std::string &name, const glm::mat3 &mat) const; + void setMat4(const std::string &name, const glm::mat4 &mat) const; + }; +} + +namespace Resource::Loading +{ + /*! + * Loads a GLSL shader from a file and compiles it. + * @param filePath The path to the GLSL shader file. + * @param shaderType The type of shader to load (for example vertex or fragment). + * @return The shader ID if successful, or an error. + * @note Sources will be preprocessed. + * @note A shader here is not a complete shader *program*, but a single shader stage. + * Pass the resulting shader ID(s) to the Shader constructor to link them into a program. + */ + [[nodiscard]] std::expected loadGLShaderFile(const std::string& filePath, ShaderType shaderType); + /*! + * Loads a GLSL shader from a string and compiles it. + * @param shaderSrc The GLSL shader source code. + * @param shaderType The type of shader to load (for example vertex or fragment). + * @return The shader ID if successful, or an error. + * @note Sources will be preprocessed. + * @note A shader here is not a complete shader *program*, but a single shader stage. + * Pass the resulting shader ID(s) to the Shader constructor to link them into a program. + */ + [[nodiscard]] std::expected loadGLShaderSource(const std::string& shaderSrc, ShaderType shaderType); +} diff --git a/src/engine/resources/texture.cpp b/src/engine/resources/texture.cpp new file mode 100644 index 0000000..6b7fea8 --- /dev/null +++ b/src/engine/resources/texture.cpp @@ -0,0 +1,262 @@ +#include "texture.h" + +#define STB_IMAGE_IMPLEMENTATION +#include + +#include "engine/util/error.h" +#include "engine/util/logging.h" + +namespace Resource { + ManagedTexture::ManagedTexture(const unsigned int textureID): textureID(textureID) {} + ManagedTexture::~ManagedTexture() { + glDeleteTextures(1, &textureID); + } +} + +namespace Resource::Loading { + struct ImageData { + int width, height, channelCount; + stbi_uc *imgData; + }; + /*! + * Internal helper function to load an image from a file. + * @attention You as the caller are responsible for freeing the allocated memory using `stbi_image_free`. + */ + Expected loadImage(const char *filePath) { + int width, height, channelCount; + stbi_uc *imgData = stbi_load(filePath, &width, &height, &channelCount, 0); + if (!imgData) { + stbi_image_free(imgData); + return std::unexpected(ERROR( + std::string("Failed to load texture: \"") + filePath + "\": " + stbi_failure_reason())); + } + return ImageData{width, height, channelCount, imgData}; + } + Expected loadImageMemory(const unsigned char *data, const int size) { + int width, height, channelCount; + stbi_uc *imgData = stbi_load_from_memory(data, size, &width, &height, &channelCount, 0); + if (!imgData) { + stbi_image_free(imgData); + return std::unexpected(ERROR( + std::string("Failed to load texture from memory: ") + stbi_failure_reason())); + } + return ImageData{width, height, channelCount, imgData}; + } + + GLint getGLChannels(const int channelCount) { + switch (channelCount) { + case 1: return GL_RED; + case 3: return GL_RGB; + case 4: return GL_RGBA; + default: { + SPDLOG_WARN("Unknown channel count %d, defaulting to single channel", channelCount); + return GL_RED; // Least likely to segfault :+1: + } + } + } + + std::expected loadTexture(const ImageData& imgData) { + const GLint format = getGLChannels(imgData.channelCount); + + unsigned int textureID; + glGenTextures(1, &textureID); + + glBindTexture(GL_TEXTURE_2D, textureID); + glTexImage2D(GL_TEXTURE_2D, 0, format, imgData.width, imgData.height, 0, format, GL_UNSIGNED_BYTE, imgData.imgData); + glGenerateMipmap(GL_TEXTURE_2D); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // TODO: GL_CLAMP_TO_EDGE to better support alpha textures? + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + return textureID; + } + + std::expected loadTexture(const char* filePath) + { + Expected imgData = loadImage(filePath); + if (!imgData) + return std::unexpected(FW_ERROR(imgData.error(), "Failed to load texture")); + const std::expected texture = loadTexture(imgData.value()); + SPDLOG_TRACE("Loaded texture \"{}\" with dimensions {}x{}", filePath, imgData->width, imgData->height); + stbi_image_free(imgData->imgData); + return texture; + } + + std::expected loadTexture(const unsigned char* data, const int size) + { + Expected imgData = loadImageMemory(data, size); + if (!imgData) + return std::unexpected(FW_ERROR(imgData.error(), "Failed to load texture from memory")); + const std::expected texture = loadTexture(imgData.value()); + SPDLOG_TRACE("Loaded texture from memory with dimensions {}x{}", imgData->width, imgData->height); + stbi_image_free(imgData->imgData); + return texture; + } + + constexpr std::array cubemapFaces = {"right", "left", "top", "bottom", "front", "back"}; + + std::expected loadCubemap(const std::string& filePath) { + unsigned int textureID; + glGenTextures(1, &textureID); + glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); + + const auto extension_index = filePath.find_last_of('.'); + const auto pathPrefix = filePath.substr(0, extension_index) + "_"; + + ImageData firstData = {}; + for (int i = 0; i < 6; i++) { + const std::string path = pathPrefix + cubemapFaces[i] + filePath.substr(extension_index); + + Expected imgData = loadImage(path.c_str()); + if (!imgData) { + glDeleteTextures(1, &textureID); + return std::unexpected(FW_ERROR(imgData.error(), "Failed to load cubemap texture")); + } + if (imgData->width != imgData->height) { + glDeleteTextures(1, &textureID); + stbi_image_free(imgData->imgData); + return std::unexpected(ERROR("Cubemap texture must be square")); + } + if (i == 0) + firstData = imgData.value(); + else if (imgData->width != firstData.width || imgData->channelCount != firstData.channelCount) { + glDeleteTextures(1, &textureID); + stbi_image_free(imgData->imgData); + return std::unexpected(ERROR("Cubemap texture faces must have the same dimensions and channel counts")); + } + SPDLOG_DEBUG("Loaded cubemap %s texture \"%s\" with dimensions %dx%d", + cubemapFaces[i].c_str(), path.c_str(), imgData->width, imgData->height); + + const GLint faceDir = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + assert(faceDir >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && faceDir <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z); + + const GLint format = getGLChannels(imgData->channelCount); + glTexImage2D(faceDir, + 0, format, imgData->width, imgData->height, 0, format, GL_UNSIGNED_BYTE, imgData->imgData); + stbi_image_free(imgData->imgData); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + return textureID; + } + std::expected loadCubemap(const std::array& data, const std::array& sizes) + { + unsigned int textureID; + glGenTextures(1, &textureID); + glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); + + ImageData firstData = {}; + for (int i = 0; i < 6; i++) { + Expected imgData = loadImageMemory(data[i], sizes[i]); + if (!imgData) { + glDeleteTextures(1, &textureID); + return std::unexpected(FW_ERROR(imgData.error(), "Failed to load cubemap texture")); + } + if (imgData->width != imgData->height) { + glDeleteTextures(1, &textureID); + stbi_image_free(imgData->imgData); + return std::unexpected(ERROR("Cubemap texture must be square")); + } + if (i == 0) + firstData = imgData.value(); + else if (imgData->width != firstData.width || imgData->channelCount != firstData.channelCount) { + glDeleteTextures(1, &textureID); + stbi_image_free(imgData->imgData); + return std::unexpected(ERROR("Cubemap texture faces must have the same dimensions and channel counts")); + } + SPDLOG_DEBUG("Loaded cubemap %d texture with dimensions %dx%d", + cubemapFaces[i].c_str(), imgData->width, imgData->height); + + const GLint faceDir = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + assert(faceDir >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && faceDir <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z); + + const GLint format = getGLChannels(imgData->channelCount); + glTexImage2D(faceDir, + 0, format, imgData->width, imgData->height, 0, format, GL_UNSIGNED_BYTE, imgData->imgData); + stbi_image_free(imgData->imgData); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + return textureID; + } + + std::expected loadCubemapSingle(const std::string& filePath) + { + unsigned int textureID; + glGenTextures(1, &textureID); + glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); + + Expected imgData = loadImage(filePath.c_str()); + if (!imgData) { + glDeleteTextures(1, &textureID); + return std::unexpected(FW_ERROR(imgData.error(), "Failed to load cubemap texture")); + } + if (imgData->width != imgData->height) { + glDeleteTextures(1, &textureID); + stbi_image_free(imgData->imgData); + return std::unexpected(ERROR("Cubemap texture must be square")); + } + + const GLint format = getGLChannels(imgData->channelCount); + for (int i = 0; i < 6; i++) { + const GLint faceDir = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + assert(faceDir >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && faceDir <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z); + glTexImage2D(faceDir, + 0, format, imgData->width, imgData->height, 0, format, GL_UNSIGNED_BYTE, imgData->imgData); + } + stbi_image_free(imgData->imgData); + + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + return textureID; + } + + std::expected loadCubemapSingle(const unsigned char* data, int size) + { + unsigned int textureID; + glGenTextures(1, &textureID); + glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); + + Expected imgData = loadImageMemory(data, size); + if (!imgData) { + glDeleteTextures(1, &textureID); + return std::unexpected(FW_ERROR(imgData.error(), "Failed to load cubemap texture")); + } + if (imgData->width != imgData->height) { + glDeleteTextures(1, &textureID); + stbi_image_free(imgData->imgData); + return std::unexpected(ERROR("Cubemap texture must be square")); + } + + const GLint format = getGLChannels(imgData->channelCount); + for (int i = 0; i < 6; i++) { + const GLint faceDir = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + assert(faceDir >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && faceDir <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z); + glTexImage2D(faceDir, + 0, format, imgData->width, imgData->height, 0, format, GL_UNSIGNED_BYTE, imgData->imgData); + } + stbi_image_free(imgData->imgData); + + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + return textureID; + } +} \ No newline at end of file diff --git a/src/engine/resources/texture.h b/src/engine/resources/texture.h new file mode 100644 index 0000000..a9b7cce --- /dev/null +++ b/src/engine/resources/texture.h @@ -0,0 +1,73 @@ +#pragma once +#include +#include + +#include "engine/util/error.h" + +namespace Resource +{ + /*! + * Class that stores an OpenGL texture ID and deletes it when it goes out of scope + */ + class ManagedTexture { + public: + unsigned int textureID; + explicit ManagedTexture(unsigned int textureID); + ~ManagedTexture(); + }; +} + +namespace Resource::Loading +{ + /*! + * Load a texture from a file. + * @param filePath The path to the file. + * @return The texture ID if successful, or an error if not. + * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. + */ + std::expected loadTexture(const char* filePath); + /*! + * Load a texture from memory. + * @param data Byte array of the image data. + * @param size The size of the byte array. + * @return The texture ID if successful, or an error if not. + * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. + */ + std::expected loadTexture(const unsigned char* data, int size); + + // TODO: Allow loading cubemap from equirectangular projection + /*! + * Loads a cubemap texture from a set of files. + * @param filePath The path to the file. The different directions are inserted before the file extension with an underscore. + * @return The texture ID if successful, or an error if not. + * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. + * @note The file name suffixes are: `_right`, `_left`, `_top`, `_bottom`, `_front`, `_back`. + */ + std::expected loadCubemap(const std::string& filePath); + /*! + * Loads a cubemap texture from a set of data. + * @param data The data for each side of the cubemap. + * @param sizes The sizes of each side of the cubemap. + * @return The texture ID if successful, or an error if not. + * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. + */ + std::expected loadCubemap( + const std::array& data, + const std::array& sizes); + + /*! + * Loads a cubemap texture from a single file to be used for all sides. + * @param filePath The path to the file. + * @return The texture ID if successful, or an error if not. + * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. + */ + std::expected loadCubemapSingle(const std::string& filePath); + /*! + * Loads a cubemap texture from a single byte array to be used for all sides. + * @param data Byte array of the image data. + * @param size The size of the byte array. + * @return The texture ID if successful, or an error if not. + * @attention If returned successfully, it is YOUR responsibility to free the memory allocated by opengl. + */ + std::expected loadCubemapSingle(const unsigned char* data, int size); +} diff --git a/src/engine/run.cpp b/src/engine/run.cpp index 33a4d54..3058445 100644 --- a/src/engine/run.cpp +++ b/src/engine/run.cpp @@ -1,35 +1,35 @@ +#include "run.h" + #include #include #include -#include "run.h" -#include "logging.h" +#include "engine/util/logging.h" #include "engine/game.h" +#include "engine/state.h" -Config config; -WindowSize windowSize; - -StatePackage statePackage = {&config, &windowSize}; +EngineState *engineState; int run() { -#ifndef NDEBUG +#pragma region Setup + setupLogging(); + SDL_LogSetOutputFunction(LogSDLCallback, nullptr); SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); -#endif if (0 > SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { - logError("Couldn't initialize SDL: %s", SDL_GetError()); + SPDLOG_ERROR("Couldn't initialize SDL: {}", SDL_GetError()); return 1; } SDL_Window *sdlWindow = SDL_CreateWindow( "LowLevelGame attempt 2 (million)", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, - windowSize.width, windowSize.height, + 1920/2, 1080/2, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE ); if (!sdlWindow) { - logError("Couldn't create window: %s", SDL_GetError()); + SPDLOG_ERROR("Couldn't create window: {}", SDL_GetError()); SDL_Quit(); return 1; } @@ -44,7 +44,7 @@ int run() SDL_GLContext glContext = SDL_GL_CreateContext(sdlWindow); if (!glContext) { - logError("Couldn't create OpenGL context: %s", SDL_GetError()); + SPDLOG_ERROR("Couldn't create OpenGL context: {}", SDL_GetError()); SDL_DestroyWindow(sdlWindow); SDL_Quit(); return -1; @@ -53,7 +53,8 @@ int run() const GLenum glewError = glewInit(); if (glewError != GLEW_OK) { - logError("Couldn't initialize GLEW: %s", glewGetErrorString(glewError)); + // SPDLOG_ERROR("Couldn't initialize GLEW: {}", glewGetErrorString(glewError)); + SPDLOG_ERROR(fmt::format("Couldn't initialize GLEW: {}", std::string(reinterpret_cast(glewGetErrorString(glewError))))); SDL_GL_DeleteContext(glContext); SDL_DestroyWindow(sdlWindow); SDL_Quit(); @@ -64,93 +65,99 @@ int run() int glCtxFlags; glGetIntegerv(GL_CONTEXT_FLAGS, &glCtxFlags); if (glCtxFlags & GL_CONTEXT_FLAG_DEBUG_BIT) { - logDebug("OpenGL debug output enabled"); + SPDLOG_DEBUG("OpenGL debug output enabled"); glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); - glDebugMessageCallback(MessageCallback, nullptr); + glDebugMessageCallback(LogGLCallback, nullptr); } else { - logWarn("OpenGL debug output not enabled"); + SPDLOG_WARN("OpenGL debug output not available"); } #endif - if (!setupGame(statePackage, sdlWindow, glContext)) { - logError("Setup failed"); + // Loaded enough to create the global state + engineState = new EngineState(sdlWindow, glContext); + Expected managerResult = engineState->resourceManager.populateErrorResources(); + if (!managerResult.has_value()) + throw std::runtime_error(stringifyError(FW_ERROR(managerResult.error(), + "Failed to load resource manager error resources"))); + + if (!setupGame()) { + SPDLOG_ERROR("Setup failed"); goto quitNoShutdown; } +#pragma endregion +#pragma region MainLoop { - double physicsAccumulator = 0.0; + double fixedAccumulator = 0.0; Uint64 frameStart = SDL_GetPerformanceCounter(); while (true) { #pragma region DeltaTime const Uint64 lastFrameStart = frameStart; frameStart = SDL_GetPerformanceCounter(); double deltaTime = static_cast(frameStart - lastFrameStart) / static_cast(SDL_GetPerformanceFrequency()); - if (config.deltaTimeLimit > 0 && deltaTime > config.deltaTimeLimit) // Things may get a bit weird if our deltaTime is like 10 seconds - deltaTime = config.deltaTimeLimit; + if (engineState->config.deltaTimeLimit > 0 && deltaTime > engineState->config.deltaTimeLimit) // Things may get a bit weird if our deltaTime is like 10 seconds + deltaTime = engineState->config.deltaTimeLimit; // TODO: Process inputs instead of sleeping, to mitigate some input lag - const double expectedDT = 1.0 / config.maxFPS; - if (config.limitFPS && !config.vsync && deltaTime < expectedDT) { + const double expectedDT = 1.0 / engineState->config.maxFPS; + if (engineState->config.limitFPS && !engineState->config.vsync && deltaTime < expectedDT) { SDL_Delay(static_cast((expectedDT - deltaTime) * 1000.0)); } #pragma endregion - if (!(*statePackage.isPaused)) { - physicsAccumulator += deltaTime; - physicsAccumulator = std::fmin(physicsAccumulator, 0.1); // Prevent spiral of death // TODO: Magic number? - const double desiredPhysicsDT = 1.0 / config.physicsTPS; - while (physicsAccumulator >= desiredPhysicsDT) { - if (!fixedUpdate(desiredPhysicsDT, statePackage)) { - logError("Physics update failed"); - goto quit; - } - physicsAccumulator -= desiredPhysicsDT; + fixedAccumulator += deltaTime; + fixedAccumulator = std::fmin(fixedAccumulator, 0.1); // Prevent spiral of death // TODO: Magic number? + const double desiredFixedDT = 1.0 / engineState->config.fixedTPS; + + while (fixedAccumulator >= desiredFixedDT) { + if (!fixedUpdate(desiredFixedDT)) { + SPDLOG_ERROR("Fixed update failed"); + goto quit; } + fixedAccumulator -= desiredFixedDT; } + // TODO: Allow recording demos? SDL_Event event; while (SDL_PollEvent(&event)) { - if (handleEvent(event, statePackage)) continue; + if (handleEvent(event)) continue; switch (event.type) { default: break; case SDL_QUIT: - logDebug("Received quit signal"); + SPDLOG_DEBUG("Received quit signal"); goto quit; case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_RESIZED) { - windowSize.width = event.window.data1; - windowSize.height = event.window.data2; - glViewport(0, 0, windowSize.width, windowSize.height); + int w, h; + SDL_GL_GetDrawableSize(sdlWindow, &w, &h); + glViewport(0, 0, w, h); } break; } } - if (*statePackage.shouldRedraw || !*statePackage.isPaused) { - const bool renderSuccess = renderUpdate(deltaTime, statePackage); - glLogErrors(); - if (!renderSuccess) { - logError("Render update failed"); - goto quit; - } - SDL_GL_SwapWindow(sdlWindow); - *statePackage.shouldRedraw = false; - } - else { - SDL_Delay(1); + const bool renderSuccess = renderUpdate(deltaTime); + glLogErrors(); + if (!renderSuccess) { + SPDLOG_ERROR("Render update failed"); + goto quit; } } } +#pragma endregion +#pragma region Shutdown quit: - shutdownGame(statePackage); + shutdownGame(); quitNoShutdown: - logDebug("Shutting down"); + delete engineState; + SPDLOG_DEBUG("Shutting down"); SDL_GL_DeleteContext(glContext); SDL_DestroyWindow(sdlWindow); SDL_Quit(); +#pragma endregion return 0; } diff --git a/src/engine/run.h b/src/engine/run.h index 91d94f1..cce2fae 100644 --- a/src/engine/run.h +++ b/src/engine/run.h @@ -1,31 +1,7 @@ -#ifndef RUN_H -#define RUN_H +#pragma once #define LLG_GL_VER_MAJOR 4 #define LLG_GL_VER_MINOR 6 -struct Config { - double deltaTimeLimit = 3.0; - bool limitFPS = true; - bool vsync = true; - int maxFPS = 100; - int physicsTPS = 60; -}; - -struct WindowSize { - int width = 1920 / 2; - int height = 1080 / 2; - - [[nodiscard]] float aspectRatio() const { return static_cast(width) / static_cast(height); } -}; - -struct StatePackage { - Config *config; - WindowSize *windowSize; - bool* isPaused; - bool* shouldRedraw; -}; - int run(); -#endif //RUN_H diff --git a/src/engine/state.h b/src/engine/state.h new file mode 100644 index 0000000..71ad56e --- /dev/null +++ b/src/engine/state.h @@ -0,0 +1,30 @@ +#pragma once +#include + +#include "engine/resources/resource_manager.h" + +struct EngineConfig { + double deltaTimeLimit = 3.0; + bool limitFPS = true; + bool vsync = true; + int maxFPS = 100; + int fixedTPS = 60; +}; + +struct EngineState { + EngineState(SDL_Window *sdlWindow, void *glContext) + : sdlWindow(sdlWindow), glContext(glContext) {} + + SDL_Window *sdlWindow; + void *glContext; + + EngineConfig config{}; + + Engine::ResourceManager resourceManager{}; +}; + +/*! + * @brief The global state of the engine + */ +extern EngineState *engineState; // TODO: Ref not ptr? + diff --git a/src/engine/typedefs.h b/src/engine/typedefs.h new file mode 100644 index 0000000..525b0e9 --- /dev/null +++ b/src/engine/typedefs.h @@ -0,0 +1,10 @@ +#pragma once + +typedef float Radians; +typedef float Degrees; + +struct Size2Di { + int width; + int height; +}; + diff --git a/src/engine/util/error.h b/src/engine/util/error.h new file mode 100644 index 0000000..7df49d5 --- /dev/null +++ b/src/engine/util/error.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include +#include + + +struct ErrorPos { + std::string file; + int line; +}; +struct Error { + std::string message; + std::optional pos; + std::shared_ptr cause; +}; + +template +using Expected = std::expected; + +/*! + * Creates a new error with a message. + * @param message The error message. + */ +#define ERROR(message) Error{message, ErrorPos{__FILE__, __LINE__}, nullptr} +/*! + * Creates a new error with a referenced cause. + * @param error The error that caused this error. + * @param message The error message. + */ +#define FW_ERROR(error, message) Error{message, ErrorPos{__FILE__, __LINE__}, std::make_shared(error)} + +/*! Transforms an error into a nicely formatted multiline string. */ +inline std::string stringifyError(Error error) { + std::string message = error.message; + while (error.cause) { + error = *error.cause; + message += "\n "; + if (error.pos.has_value() && !error.message.empty()) + message += "at " + error.pos->file + ":" + std::to_string(error.pos->line) + ": " + error.message; + else if (error.pos.has_value() && error.message.empty()) + message += "at " + error.pos->file + ":" + std::to_string(error.pos->line) + ""; + else if (!error.pos.has_value() && !error.message.empty()) + message += error.message; + } + return message; +} +/*! Logs an error to the console using spdlog with error severity. */ +inline void reportError(const Error& error) { + SPDLOG_ERROR(stringifyError(error)); +} diff --git a/src/engine/util/file.cpp b/src/engine/util/file.cpp new file mode 100644 index 0000000..55971d1 --- /dev/null +++ b/src/engine/util/file.cpp @@ -0,0 +1,12 @@ +#include "file.h" + +#include + +std::expected readTextFile(const std::string &filePath) { + std::ifstream file(filePath); + if (!file.is_open()) + return std::unexpected(ERROR("Failed to open file: " + filePath)); + + std::string fileContents((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return fileContents; +} diff --git a/src/engine/util/file.h b/src/engine/util/file.h new file mode 100644 index 0000000..71c2afb --- /dev/null +++ b/src/engine/util/file.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +#include "engine/util/error.h" + +std::expected readTextFile(const std::string &filePath); diff --git a/src/engine/util/geometry.h b/src/engine/util/geometry.h index 2f34ac1..1c61dab 100644 --- a/src/engine/util/geometry.h +++ b/src/engine/util/geometry.h @@ -1,16 +1,6 @@ -#ifndef SHAPE_H -#define SHAPE_H - +#pragma once #include -#define BUFFERS_MV_FROM_TO(from, to) \ - to->VAO = from.VAO; \ - to->VBO = from.VBO; \ - to->EBO = from.EBO; \ - from.VAO = 0; \ - from.VBO = 0; \ - from.EBO = 0; - enum CubeVertIndex { BOTTOM_LEFT_FRONT = 0, @@ -72,4 +62,3 @@ constexpr std::array ScreenSpaceQuadIndices = { 1, 3, 2, }; -#endif diff --git a/src/engine/util/logging.cpp b/src/engine/util/logging.cpp new file mode 100644 index 0000000..131e7db --- /dev/null +++ b/src/engine/util/logging.cpp @@ -0,0 +1,112 @@ +#include "logging.h" + +#include +#include + +std::string glErrorString(const GLenum errorCode) { + static const std::unordered_map map = { + {GL_NO_ERROR, "No error"}, + {GL_INVALID_ENUM, "Invalid enum"}, + {GL_INVALID_VALUE, "Invalid value"}, + {GL_INVALID_OPERATION, "Invalid operation"}, + {GL_STACK_OVERFLOW, "Stack overflow"}, + {GL_STACK_UNDERFLOW, "Stack underflow"}, + {GL_OUT_OF_MEMORY, "Out of memory"}, + {GL_INVALID_FRAMEBUFFER_OPERATION, "Invalid framebuffer operation"}, + {GL_CONTEXT_LOST, "Context lost"}, + {GL_TABLE_TOO_LARGE, "Table too large"} + }; + + const auto err = map.find(errorCode); + return err != map.end() ? err->second : "Unknown error: " + std::to_string(errorCode); +} + +GLenum glLogErrors_(const char *file, const int line) { + GLenum errorCode; + while ((errorCode = glGetError()) != GL_NO_ERROR) { + spdlog::get("opengl")->error("OpenGL error: ({}) {}", errorCode, glErrorString(errorCode)); + } + return errorCode; +} + +GLenum glLogErrorsExtra_(const char *file, const int line, const char *extra) { + GLenum errorCode; + while ((errorCode = glGetError()) != GL_NO_ERROR) { + spdlog::get("opengl")->error("OpenGL error {}: ({}) {}", extra, errorCode, glErrorString(errorCode)); + } + return errorCode; +} +GLenum glLogErrorsExtra_(const char *file, const int line, const std::string &extra) { + return glLogErrorsExtra_(file, line, extra.c_str()); +} + +void GLAPIENTRY LogGLCallback( + const GLenum source, + const GLenum type, + const GLuint id, + const GLenum severity, + const GLsizei length, + const GLchar* message, + const void* userParam +) { + std::unordered_map sourceMap = { + {GL_DEBUG_SOURCE_API, "API"}, + {GL_DEBUG_SOURCE_WINDOW_SYSTEM, "Window system"}, + {GL_DEBUG_SOURCE_SHADER_COMPILER, "Shader compiler"}, + {GL_DEBUG_SOURCE_THIRD_PARTY, "Third party"}, + {GL_DEBUG_SOURCE_APPLICATION, "Application"}, + {GL_DEBUG_SOURCE_OTHER, "Other"} + }; + std::unordered_map typeMap = { + {GL_DEBUG_TYPE_ERROR, "Error"}, + {GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR, "Deprecated behavior"}, + {GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, "Undefined behavior"}, + {GL_DEBUG_TYPE_PORTABILITY, "Non-portable"}, + {GL_DEBUG_TYPE_PERFORMANCE, "Performance"}, + {GL_DEBUG_TYPE_MARKER, "Marker"}, + {GL_DEBUG_TYPE_PUSH_GROUP, "Push group"}, + {GL_DEBUG_TYPE_POP_GROUP, "Pop group"}, + {GL_DEBUG_TYPE_OTHER, "Other"} + }; + std::unordered_map severityMap = { + {GL_DEBUG_SEVERITY_HIGH, spdlog::level::level_enum::critical}, // Real errors or really dangerous undefined behavior + {GL_DEBUG_SEVERITY_MEDIUM, spdlog::level::level_enum::err}, // Undefined behavior or major performance issues + {GL_DEBUG_SEVERITY_LOW, spdlog::level::level_enum::warn}, // Redundant state change or unimportant undefined behavior + {GL_DEBUG_SEVERITY_NOTIFICATION, spdlog::level::level_enum::trace} + }; + const auto errSeverity = severityMap.find(severity); + const auto logPriority = errSeverity != severityMap.end() ? errSeverity->second : spdlog::level::critical; + + spdlog::get("opengl")->log( + logPriority, + "[from:{}] [type:{}] (id:{}) : {}", + sourceMap.contains(source) ? sourceMap[source] : "Unknown", + typeMap.contains(type) ? typeMap[type] : "Unknown", + id, + message + ); +} + +void LogSDLCallback(void*, int category, SDL_LogPriority priority, const char *message) { + //TODO: Make better lmao + static const std::unordered_map priorityMap = { + {SDL_LOG_PRIORITY_VERBOSE, spdlog::level::trace}, + {SDL_LOG_PRIORITY_DEBUG, spdlog::level::debug}, + {SDL_LOG_PRIORITY_INFO, spdlog::level::info}, + {SDL_LOG_PRIORITY_WARN, spdlog::level::warn}, + {SDL_LOG_PRIORITY_ERROR, spdlog::level::err}, + {SDL_LOG_PRIORITY_CRITICAL, spdlog::level::critical} + }; + const auto err = priorityMap.find(priority); + const auto logLevel = err != priorityMap.end() ? err->second : spdlog::level::critical; + spdlog::get("opengl")->log(logLevel, "{}", message); +} + +void setupLogging() { + const auto logger = spdlog::stdout_color_mt("console"); + spdlog::set_default_logger(logger); + spdlog::set_level(spdlog::level::debug); + + const auto openglLogger = spdlog::stdout_color_mt("opengl"); + openglLogger->set_level(spdlog::level::debug); +} diff --git a/src/engine/util/logging.h b/src/engine/util/logging.h new file mode 100644 index 0000000..ad0e5f4 --- /dev/null +++ b/src/engine/util/logging.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include +#include + +void setupLogging(); + +#pragma region OpenGL error logging, for use when debug output is not available +std::string glErrorString(GLenum errorCode); +GLenum glLogErrors_(const char *file, int line); +GLenum glLogErrorsExtra_(const char *file, int line, const char *extra); +GLenum glLogErrorsExtra_(const char *file, int line, const std::string &extra); + +#ifndef NDEBUG +#define glLogErrors() glLogErrors_(__FILE__, __LINE__) // glGetError is a bit slow +#define glLogErrorsExtra(extra) glLogErrorsExtra_(__FILE__, __LINE__, extra) +#else +#define glLogErrors() +#define glLogErrorsExtra(extra) +#endif +#pragma endregion + +void GLAPIENTRY LogGLCallback( + GLenum source, + GLenum type, + GLuint id, + GLenum severity, + GLsizei length, + const GLchar* message, + const void* userParam +); +void LogSDLCallback(void* /*userdata*/, int category, SDL_LogPriority priority, const char *message); + + diff --git a/src/game/camera.cpp b/src/game/camera.cpp deleted file mode 100644 index 7c5a757..0000000 --- a/src/game/camera.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "camera.h" -#include -#include - -#include -#include - -Camera::Camera(const glm::vec3 position, const float yaw, const float pitch, const float roll, const float fov) -: position(position), fov(fov), yaw_(yaw), pitch_(pitch), roll_(roll) -{ - updateVectors(); -} - -void Camera::updateVectors() { - forward_ = glm::normalize(glm::vec3( - cos(glm::radians(yaw_)) * cos(glm::radians(pitch_)), - sin(glm::radians(pitch_)), - sin(glm::radians(yaw_)) * cos(glm::radians(pitch_)) - )); - right_ = glm::normalize(glm::cross(forward_, glm::vec3(0.0f, 1.0f, 0.0f))); - up_ = glm::normalize(glm::cross(right_, forward_)); -} - -glm::mat4 Camera::getViewMatrix() const { - return glm::lookAt(position, position + forward_, up_); -} - -void Camera::populateProjMatrixBuffer(const unsigned int uboID, const float aspectRatio) const { - glBindBuffer(GL_UNIFORM_BUFFER, uboID); - glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), - glm::value_ptr(getProjectionMatrix(aspectRatio))); - glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), - glm::value_ptr(getViewMatrix())); -} - -void CameraController::look(const SDL_MouseMotionEvent &event) const { - const auto xOffset = static_cast(event.xrel) * sensitivity; - const auto yOffset = static_cast(-event.yrel) * sensitivity; - - camera.setYaw(camera.yaw() + xOffset); - camera.setPitch(std::max(minPitch, std::min(maxPitch, camera.pitch() + yOffset))); -} - -void CameraController::zoom(const float offset) const { - camera.fov -= offset; - camera.fov = std::max(camera.fov, 0.1f); - camera.fov = std::min(camera.fov, 160.0f); - // for some reason, while using clamp it crashes on startup - // progState.fov = std::clamp(progState.fov, 1.0f, 45.0f); -} diff --git a/src/game/camera.h b/src/game/camera.h deleted file mode 100644 index 1619b3b..0000000 --- a/src/game/camera.h +++ /dev/null @@ -1,105 +0,0 @@ -#ifndef CAMERA_H -#define CAMERA_H -#include -#define GLM_FORCE_DEPTH_ZERO_TO_ONE // For reverse-z -#include -#include -#include - -typedef float Radians; -typedef float Degrees; - -auto constexpr DEFAULT_CAMERA_POSITION = glm::vec3(0.0f); -Degrees constexpr DEFAULT_FOV = 45.0f; -Radians constexpr DEFAULT_YAW = -90.0f; -Radians constexpr DEFAULT_PITCH = 0.0f; -Radians constexpr DEFAULT_ROLL = 0.0f; - - -class Camera { -public: - glm::vec3 position; - Degrees fov; - - float clipNear = 0.1f; - float clipFar = 100.0f; - - explicit Camera( - glm::vec3 position = DEFAULT_CAMERA_POSITION, - Radians yaw = DEFAULT_YAW, - Radians pitch = DEFAULT_PITCH, - Radians roll = DEFAULT_ROLL, - Degrees fov = DEFAULT_FOV - ); - - // TODO: Only ever have to update the matrices when the camera moves/zooms. - // Or maybe just simplify this entire monster of a class and eat the microscopical (potential) performance hit... - // I haven't actually even profiled so that seems most reasonable xD - /*! - * @brief Update the UBO holding the projection and view matrices. - * @param uboID The ID of the uniform buffer object. - * @param aspectRatio The aspect ratio of the window. Must be a floating point type (float, double). - */ - void populateProjMatrixBuffer(unsigned int uboID, float aspectRatio) const; - - [[nodiscard]] glm::mat4 getViewMatrix() const; - // Witchcraft to not accept ints and only floating point types for aspectRatio - template - [[nodiscard]] - std::enable_if_t, glm::mat4> - /*! - * @brief Get the projection matrix for the camera. - * @param aspectRatio The aspect ratio of the window. Must be a floating point type (float, double). - * @return The projection matrix. - */ - getProjectionMatrix(const FloatOnly aspectRatio) const { - // Swapped near and far for reverse-z - return glm::perspective(glm::radians(fov), aspectRatio, clipFar, clipNear); - } - -private: - bool anglesChanged = false; - Radians yaw_; - Radians pitch_; - Radians roll_; - - glm::vec3 forward_{}; - glm::vec3 up_{}; - glm::vec3 right_{}; - - void updateVectors(); - -public: - [[nodiscard]] Radians yaw() const { return yaw_; } - void setYaw(const Radians yaw) { yaw_ = yaw; anglesChanged = true; } - [[nodiscard]] Radians pitch() const { return pitch_; } - void setPitch(const Radians pitch) { pitch_ = pitch; anglesChanged = true; } - [[nodiscard]] Radians roll() const { return roll_; } - void setRoll(const Radians roll) { roll_ = roll; anglesChanged = true; } - -#define UPDATE_IF_CHANGED() if (anglesChanged) { updateVectors(); anglesChanged = false; } - glm::vec3 forward() { UPDATE_IF_CHANGED(); return forward_; } - glm::vec3 up() { UPDATE_IF_CHANGED(); return up_; } - glm::vec3 right() { UPDATE_IF_CHANGED(); return right_; } -#undef UPDATE_IF_CHANGED -}; - -class CameraController { -public: - Camera &camera; - - float sensitivity = 0.1f; - float maxPitch = 89.0f; - float minPitch = -89.0f; - - explicit CameraController(Camera &camera, const float sensitivity) - : camera(camera), sensitivity(sensitivity) {} - explicit CameraController(Camera &camera, const float sensitivity, const float maxPitch, const float minPitch) - : camera(camera), sensitivity(sensitivity), maxPitch(maxPitch), minPitch(minPitch) {} - - void look(const SDL_MouseMotionEvent &event) const; - void zoom(float offset) const; -}; - - -#endif //CAMERA_H diff --git a/src/game/camera_utils.cpp b/src/game/camera_utils.cpp new file mode 100644 index 0000000..9eed308 --- /dev/null +++ b/src/game/camera_utils.cpp @@ -0,0 +1,23 @@ +#include "camera_utils.h" + +#include + + +namespace CameraUtils { + glm::mat4 getViewMatrix(const Player &player) { + return glm::lookAt( + player.origin, + player.origin + player.getForward(), + player.getUp()); + } + + glm::mat4 getProjectionMatrix(const GameSettings &gameSettings, const float aspectRatio) { + return glm::perspective(glm::radians(gameSettings.baseFov), aspectRatio, + // Swapped NEAR and FAR for reverse-z + gameSettings.clipFar, gameSettings.clipNear); + } + glm::mat4 getProjectionMatrix(const GameSettings &gameSettings, const int width, const int height) { + return getProjectionMatrix(gameSettings, static_cast(width) / static_cast(height)); + } + +} diff --git a/src/game/camera_utils.h b/src/game/camera_utils.h new file mode 100644 index 0000000..27eccc9 --- /dev/null +++ b/src/game/camera_utils.h @@ -0,0 +1,14 @@ +#pragma once +#define GLM_FORCE_DEPTH_ZERO_TO_ONE // For reverse-z +#include + +#include "game/state.h" + +namespace CameraUtils { + [[nodiscard]] glm::mat4 getViewMatrix(const Player &player); + [[nodiscard]] glm::mat4 getProjectionMatrix(const GameSettings &gameSettings, float aspectRatio); + [[nodiscard]] glm::mat4 getProjectionMatrix(const GameSettings &gameSettings, int width, int height); + template + [[nodiscard]] glm::mat4 getProjectionMatrix(const GameState &gameState, T aspectRatio) = delete; +} + diff --git a/src/game/game.cpp b/src/game/game.cpp index a3505ce..2845588 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1,104 +1,117 @@ +#include "engine/game.h" + +#include #include +#include #include #include -#include #include -#include - -#include "engine/logging.h" -#include "engine/loader/shader/compute_shader.h" -#include "engine/game.h" -#include "engine/render/overlay.h" -#include "engine/render/frame_buffer.h" +#include -#include "camera.h" +#include "camera_utils.h" #include "gui.h" -#include "state.h" #include "skybox.h" +#include "state.h" +#include "engine/resources/resource_manager.h" +#include "engine/state.h" +#include "engine/render/frame_buffer.h" +#include "engine/util/logging.h" +GameState *gameState; -std::unique_ptr gameState; unsigned int uboMatrices; -#define LEVEL gameState->level -#define PLAYER LEVEL.player -#define CAMERA PLAYER.camera - std::unique_ptr frameBuffer; // This is TEMPORARY until I // TODO: Implement a concept of objects/levels/whatever -std::shared_ptr globalScene; +std::vector> scenes; +Skybox *skybox; +std::shared_ptr mainShader; -bool setupGame(StatePackage &statePackage, SDL_Window *sdlWindow, SDL_GLContext glContext) { - DebugGUI::init(*sdlWindow, glContext); - frameBuffer = std::make_unique(statePackage.windowSize->width, statePackage.windowSize->height); +bool setupGame() { + gameState = new GameState(); - glEnable(GL_CULL_FACE); + DebugGUI::init(); + int windowWidth, windowHeight; + SDL_GL_GetDrawableSize(engineState->sdlWindow, &windowWidth, &windowHeight); + frameBuffer = std::make_unique(windowWidth, windowHeight); + + if (gameState->settings.backfaceCulling) + glEnable(GL_CULL_FACE); + else + glDisable(GL_CULL_FACE); glCullFace(GL_BACK); + glFrontFace(GL_CCW); // Counter-clockwise winding order glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); // OpenGL's default NDC is [-1, 1], we want [0, 1] SDL_SetRelativeMouseMode(SDL_TRUE); - - gameState = std::make_unique(statePackage); - statePackage.isPaused = &gameState->isPaused; - statePackage.shouldRedraw = &gameState->shouldRedraw; - - LEVEL.shaders.emplace_back("resources/assets/shaders/vert.vert", "resources/assets/shaders/frag.frag"); - LEVEL.shaders[0].use(); - auto matricesBinding = LEVEL.shaders[0].bindUniformBlock("Matrices", 0); + mainShader = engineState->resourceManager.loadShader( + "resources/assets/shaders/vert.vert", "resources/assets/shaders/frag.frag");; + mainShader->use(); + auto matricesBinding = mainShader->bindUniformBlock("Matrices", 0); if (!matricesBinding.has_value()) - logError("Failed to bind matrices uniform block" NL_INDENT "%s", matricesBinding.error().c_str()); + throw std::runtime_error(stringifyError(FW_ERROR(matricesBinding.error(), "Failed to bind matrices uniform block"))); - LEVEL.shaders.emplace_back("resources/assets/shaders/sb_vert.vert", "resources/assets/shaders/sb_frag.frag"); - LEVEL.shaders[1].use(); - matricesBinding = LEVEL.shaders[1].bindUniformBlock("Matrices", 0); + // TODO: So super duper mega ultra scuffed and a remnant from when we initialised the skybox stuff manually each frame + const std::shared_ptr sbShader = engineState->resourceManager.loadShader( + "resources/assets/shaders/sb_vert.vert", "resources/assets/shaders/sb_frag.frag"); + sbShader->use(); + matricesBinding = sbShader->bindUniformBlock("Matrices", 0); if (!matricesBinding.has_value()) - logError("Failed to bind matrices uniform block" NL_INDENT "%s", matricesBinding.error().c_str()); + throw std::runtime_error(stringifyError(FW_ERROR(matricesBinding.error(), "Failed to bind matrices uniform block"))); + skybox = new Skybox(engineState->resourceManager.loadCubemap("resources/assets/textures/skybox/sky.png")); glGenBuffers(1, &uboMatrices); glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices); glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), nullptr, GL_STATIC_DRAW); glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4)); - auto scene = LEVEL.modelManager.getScene("resources/assets/models/map.obj"); - if (!scene.has_value()) - logError("Failed to load scene" NL_INDENT "%s", scene.error().c_str()); - globalScene = scene.value_or(LEVEL.modelManager.errorScene); + scenes.push_back(engineState->resourceManager.loadScene("resources/assets/models/map.obj")); return true; } -void shutdownGame(StatePackage &statePackage) { - gameState.reset(); +void shutdownGame() { + DebugGUI::shutdown(); + glDeleteBuffers(1, &uboMatrices); + delete gameState; + delete skybox; } -bool renderUpdate(const double deltaTime, StatePackage &statePackage) { - if (!(*statePackage.isPaused)) { - const Uint8* keyState = SDL_GetKeyboardState(nullptr); - auto inputDir = glm::vec3(0.0f, 0.0f, 0.0f); - if (keyState[SDL_SCANCODE_W]) - inputDir += CAMERA.forward(); - if (keyState[SDL_SCANCODE_S]) - inputDir -= CAMERA.forward(); - if (keyState[SDL_SCANCODE_A]) - inputDir -= CAMERA.right(); - if (keyState[SDL_SCANCODE_D]) - inputDir += CAMERA.right(); - if (keyState[SDL_SCANCODE_SPACE]) - inputDir += CAMERA.up(); - if (keyState[SDL_SCANCODE_LSHIFT]) - inputDir -= CAMERA.up(); - if (keyState[SDL_SCANCODE_F]) - SDL_SetRelativeMouseMode(static_cast(!SDL_GetRelativeMouseMode())); - - inputDir = glm::dot(inputDir, inputDir) > 0.0f ? glm::normalize(inputDir) : inputDir; // dot(v, v) is squared length - constexpr auto CAMERA_SPEED = 2.5f; - CAMERA.position += inputDir * CAMERA_SPEED * static_cast(deltaTime); - } +bool pausedRenderUpdate(double deltaTime); + +bool renderUpdate(const double deltaTime) { + if (gameState->isPaused) + return pausedRenderUpdate(deltaTime); + + int windowWidth, windowHeight; + SDL_GL_GetDrawableSize(engineState->sdlWindow, &windowWidth, &windowHeight); + + const Uint8* keyState = SDL_GetKeyboardState(nullptr); + auto inputDir = glm::vec3(0.0f, 0.0f, 0.0f); + if (keyState[SDL_SCANCODE_W]) + inputDir += gameState->playerState.getForward(); + if (keyState[SDL_SCANCODE_S]) + inputDir -= gameState->playerState.getForward(); + if (keyState[SDL_SCANCODE_A]) + inputDir -= gameState->playerState.getRight(); + if (keyState[SDL_SCANCODE_D]) + inputDir += gameState->playerState.getRight(); + if (keyState[SDL_SCANCODE_SPACE]) + inputDir += gameState->playerState.getUp(); + if (keyState[SDL_SCANCODE_LSHIFT]) + inputDir -= gameState->playerState.getUp(); + if (keyState[SDL_SCANCODE_F]) + SDL_SetRelativeMouseMode(static_cast( + !SDL_GetRelativeMouseMode())); + + inputDir = glm::dot(inputDir, inputDir) > 0.0f ? glm::normalize(inputDir) : inputDir; // dot(v, v) is squared length + constexpr auto CAMERA_SPEED = 2.5f; + gameState->playerState.origin += inputDir * CAMERA_SPEED * static_cast(deltaTime); frameBuffer->bind(); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_GREATER); - glClearColor(0.5f, 0.0f, 0.5f, 1.0f); + glClearColor(0.62, 0.56, 0.95, 1); glClearDepth(0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glPolygonMode(GL_FRONT_AND_BACK, gameState->settings.wireframe ? GL_LINE : GL_FILL); @@ -109,54 +122,55 @@ bool renderUpdate(const double deltaTime, StatePackage &statePackage) { else glEnable(GL_CULL_FACE); - CAMERA.populateProjMatrixBuffer(uboMatrices, statePackage.windowSize->aspectRatio()); - - LEVEL.shaders[0].use(); - - LEVEL.shaders[0].setVec3("dirLight.direction", -0.2f, -1.0f, -0.3f); - LEVEL.shaders[0].setVec3("dirLight.ambient", 0.5f, 0.5f, 0.5f); - LEVEL.shaders[0].setVec3("dirLight.diffuse", 0.4f, 0.4f, 0.4f); - LEVEL.shaders[0].setVec3("dirLight.specular", 0.5f, 0.5f, 0.5f); - - LEVEL.shaders[0].setVec3("pointLights[0].position", 1.2f, 1.0f, 2.0f); - LEVEL.shaders[0].setVec3("pointLights[0].ambient", 0.05f, 0.05f, 0.05f); - LEVEL.shaders[0].setVec3("pointLights[0].diffuse", 0.8f, 0.8f, 0.8f); - LEVEL.shaders[0].setVec3("pointLights[0].specular", 1.0f, 1.0f, 1.0f); - LEVEL.shaders[0].setFloat("pointLights[0].constant", 1.0f); - LEVEL.shaders[0].setFloat("pointLights[0].linear", 0.09f); - LEVEL.shaders[0].setFloat("pointLights[0].quadratic", 0.032f); - - LEVEL.shaders[0].setVec3("spotLight.position", CAMERA.position); - LEVEL.shaders[0].setVec3("spotLight.direction", CAMERA.forward()); - LEVEL.shaders[0].setVec3("spotLight.ambient", 0.0f, 0.0f, 0.0f); - LEVEL.shaders[0].setVec3("spotLight.diffuse", 1.0f, 1.0f, 1.0f); - LEVEL.shaders[0].setVec3("spotLight.specular", 1.0f, 1.0f, 1.0f); - LEVEL.shaders[0].setFloat("spotLight.constant", 1.0f); - LEVEL.shaders[0].setFloat("spotLight.linear", 0.09f); - LEVEL.shaders[0].setFloat("spotLight.quadratic", 0.032f); - LEVEL.shaders[0].setFloat("spotLight.cutOff", glm::cos(glm::radians(12.5f))); - LEVEL.shaders[0].setFloat("spotLight.outerCutOff", glm::cos(glm::radians(15.0f))); - - LEVEL.shaders[0].setVec3("viewPos", CAMERA.position); - - auto drawRet = globalScene->Draw(LEVEL.textureManager, LEVEL.shaders[0], glm::mat4(1.0f)); - if (!drawRet.has_value()) - logError("Failed to draw scene" NL_INDENT "%s", drawRet.error().c_str()); + // TODO: FIGURE THIS OUT WITH NEW STATE + glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), + glm::value_ptr(CameraUtils::getProjectionMatrix(gameState->settings, windowWidth, windowHeight))); + glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), + glm::value_ptr(CameraUtils::getViewMatrix(gameState->playerState))); + + mainShader->use(); + + mainShader->setVec3("dirLight.direction", -0.2f, -1.0f, -0.3f); + mainShader->setVec3("dirLight.ambient", 0.5f, 0.5f, 0.5f); + mainShader->setVec3("dirLight.diffuse", 0.4f, 0.4f, 0.4f); + mainShader->setVec3("dirLight.specular", 0.5f, 0.5f, 0.5f); + + mainShader->setVec3("pointLights[0].position", 1.2f, 1.0f, 2.0f); + mainShader->setVec3("pointLights[0].ambient", 0.05f, 0.05f, 0.05f); + mainShader->setVec3("pointLights[0].diffuse", 0.8f, 0.8f, 0.8f); + mainShader->setVec3("pointLights[0].specular", 1.0f, 1.0f, 1.0f); + mainShader->setFloat("pointLights[0].constant", 1.0f); + mainShader->setFloat("pointLights[0].linear", 0.09f); + mainShader->setFloat("pointLights[0].quadratic", 0.032f); + + mainShader->setVec3("spotLight.position", gameState->playerState.origin); + mainShader->setVec3("spotLight.direction", gameState->playerState.getForward()); + mainShader->setVec3("spotLight.ambient", 0.0f, 0.0f, 0.0f); + mainShader->setVec3("spotLight.diffuse", 1.0f, 1.0f, 1.0f); + mainShader->setVec3("spotLight.specular", 1.0f, 1.0f, 1.0f); + mainShader->setFloat("spotLight.constant", 1.0f); + mainShader->setFloat("spotLight.linear", 0.09f); + mainShader->setFloat("spotLight.quadratic", 0.032f); + mainShader->setFloat("spotLight.cutOff", glm::cos(glm::radians(12.5f))); + mainShader->setFloat("spotLight.outerCutOff", glm::cos(glm::radians(15.0f))); + + // TODO: Why is this not handled in the buffer + mainShader->setVec3("viewPos", gameState->playerState.origin); + + for (const auto &scene : scenes) { + auto drawRet = scene->Draw(); + if (!drawRet.has_value()) + reportError(FW_ERROR(drawRet.error(), "Failed to draw scene")); + } const glm::mat4 trans = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 1.0f, -2.0f)); - LEVEL.modelManager.errorScene->Draw(LEVEL.textureManager, LEVEL.shaders[0], trans); + engineState->resourceManager.loadScene("INVALID_SCENE")->Draw(trans); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); #pragma region Skybox - // We render the skybox manually, since we don't need any of the fancy scene stuff - LEVEL.shaders[1].use(); - - std::expected skyboxTex = LEVEL.textureManager.getTexture( - "resources/assets/textures/skybox/sky.png", Engine::TextureType::CUBEMAP); - if (!skyboxTex.has_value()) - logError("Failed to load skybox texture" NL_INDENT "%s", skyboxTex.error().c_str()); - LEVEL.skybox.draw(skyboxTex.value_or(LEVEL.textureManager.errorTexture), LEVEL.shaders[1]); + skybox->draw(); #pragma endregion #pragma region "Transfer color buffer to the default framebuffer before rendering overlays" @@ -164,7 +178,7 @@ bool renderUpdate(const double deltaTime, StatePackage &statePackage) { glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBlitFramebuffer( 0, 0, frameBuffer->getSize().width, frameBuffer->getSize().height, - 0, 0, statePackage.windowSize->width, statePackage.windowSize->height, + 0, 0, windowWidth, windowHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); // Make sure both read and write are set to the default framebuffer. Theoretically only binding read would do. glBindFramebuffer(GL_FRAMEBUFFER, 0); @@ -174,50 +188,71 @@ bool renderUpdate(const double deltaTime, StatePackage &statePackage) { glDepthFunc(GL_LESS); // Don't leak reverse z into other rendering, IDK what imgui or whatever might be doing glDisable(GL_DEPTH_TEST); - DebugGUI::renderStart(*gameState, statePackage, deltaTime); + DebugGUI::renderStart(deltaTime); ImGui::Begin("Preview", nullptr, ImGuiWindowFlags_AlwaysAutoResize); ImGui::Text("Color buffer"); ImGui::Image(frameBuffer->ColorTextureID, - ImVec2(static_cast(statePackage.windowSize->width) / 4, static_cast(statePackage.windowSize->height) / 4), + ImVec2(static_cast(windowWidth) / 4, static_cast(windowHeight) / 4), ImVec2(0, 1), ImVec2(1, 0)); ImGui::End(); DebugGUI::renderEnd(); #pragma endregion + SDL_GL_SwapWindow(engineState->sdlWindow); return true; } -bool fixedUpdate(const double deltaTime, StatePackage &statePackage) { +bool pausedRenderUpdate(const double deltaTime) { + return true; +} + +bool fixedUpdate(const double deltaTime) { + if (gameState->isPaused) return true; + return true; } -bool handleEvent(const SDL_Event &event, StatePackage &statePackage) { +bool handleEvent(const SDL_Event &event) { DebugGUI::handleEvent(event); switch (event.type) { default: break; case SDL_KEYDOWN: if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { - *statePackage.isPaused = !(*statePackage.isPaused); + gameState->isPaused ^= true; + SDL_SetRelativeMouseMode(static_cast(!gameState->isPaused)); + return true; + } + if (event.key.keysym.scancode == SDL_SCANCODE_P) { SDL_SetRelativeMouseMode(static_cast(!SDL_GetRelativeMouseMode())); - *statePackage.shouldRedraw = true; return true; } break; case SDL_MOUSEWHEEL: - if (!(*statePackage.isPaused)) - PLAYER.cController.zoom(static_cast(event.wheel.y) * 2.0f); + if (!gameState->isPaused) { + gameState->settings.baseFov -= static_cast(event.wheel.y) * 2.0f; + // TODO: Don't adjust this directly when zooming, instead change a modifier on top of it + gameState->settings.baseFov = std::max(gameState->settings.baseFov, 0.1f); + gameState->settings.baseFov = std::min(gameState->settings.baseFov, 160.0f); + } break; case SDL_MOUSEMOTION: - if (!(*statePackage.isPaused)) - PLAYER.cController.look(event.motion); + if (!gameState->isPaused && SDL_GetRelativeMouseMode()) { + const auto xOffset = static_cast(event.motion.xrel) * gameState->settings.sensitivity; + const auto yOffset = static_cast(-event.motion.yrel) * gameState->settings.sensitivity; + gameState->playerState.rotation.yaw += xOffset; + gameState->playerState.rotation.pitch = std::max(-89.0f, std::min(89.0f, + gameState->playerState.rotation.pitch + yOffset)); + } break; case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_RESIZED) { // TODO: Move framebuffer handling to the engine + int w, h; + SDL_GL_GetDrawableSize(engineState->sdlWindow, &w, &h); frameBuffer->resize(event.window.data1, event.window.data2); } break; diff --git a/src/game/gui.cpp b/src/game/gui.cpp index 9279c0e..5646ade 100644 --- a/src/game/gui.cpp +++ b/src/game/gui.cpp @@ -1,21 +1,21 @@ +#include "gui.h" + #include #include #include #include -#include +#include -#include "engine/run.h" -#include - -#include "gui.h" +#include "engine/state.h" +#include "game/state.h" namespace DebugGUI { - void init(SDL_Window &window, const SDL_GLContext glContext) { + void init() { IMGUI_CHECKVERSION(); ImGui::CreateContext(); - ImGui_ImplSDL2_InitForOpenGL(&window, glContext); + ImGui_ImplSDL2_InitForOpenGL(engineState->sdlWindow, engineState->glContext); ImGui_ImplOpenGL3_Init(); } void shutdown() { @@ -28,73 +28,74 @@ namespace DebugGUI { ImGui_ImplSDL2_ProcessEvent(&event); } - void drawFrame(GameState &gameState, StatePackage &statePackage, double deltaTime); + void drawFrame(double deltaTime); - void render(GameState &gameState, StatePackage &statePackage, const double deltaTime) { - renderStart(gameState, statePackage, deltaTime); + void render(const double deltaTime) { + renderStart(deltaTime); renderEnd(); } - void renderStart(GameState &gameState, StatePackage &statePackage, double deltaTime) { + void renderStart(const double deltaTime) { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); - drawFrame(gameState, statePackage, deltaTime); + drawFrame(deltaTime); } void renderEnd() { ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } - - void drawSettingsGUI(GameState &gameState, StatePackage &statePackage, double deltaTime); - void drawOverlay(GameState &gameState, StatePackage &statePackage, double deltaTime); - - void drawFrame(GameState &gameState, StatePackage &statePackage, double deltaTime) { - drawSettingsGUI(gameState, statePackage, deltaTime); - drawOverlay(gameState, statePackage, deltaTime); - - // TODO: fix menu not being visible on first pause - if (gameState.isPaused) { - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), - ImGuiCond_Always, {0.5f, 0.5f}); - ImGui::Begin("Paused", nullptr, - ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - if (ImGui::Button("Resume")) { // does not work for now - gameState.isPaused = false; - *statePackage.shouldRedraw = true; - } - ImGui::End(); - } + void drawSettingsGUI(double deltaTime); + void drawOverlay(double deltaTime); + + void drawFrame(double deltaTime) { + drawSettingsGUI(deltaTime); + drawOverlay(deltaTime); + + // // TODO: fix menu not being visible on first pause + // if (gameState.isPaused) { + // ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + // ImGuiCond_Always, {0.5f, 0.5f}); + // ImGui::Begin("Paused", nullptr, + // ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + // if (ImGui::Button("Resume")) { // does not work for now + // gameState.isPaused = false; + // *statePackage.shouldRedraw = true; + // } + // ImGui::End(); + // } // ImGui::ShowDemoWindow(); } - void drawSettingsGUI(GameState &gameState, StatePackage &statePackage, double deltaTime) { -#define ENGINE_CONFIG statePackage.config -#define GAME_SETTINGS gameState.settings - + void drawSettingsGUI(double deltaTime) { ImGui::Begin("Settings"); - ImGui::Checkbox("Limit FPS", &ENGINE_CONFIG->limitFPS); - if (ENGINE_CONFIG->limitFPS) { - ImGui::Checkbox("VSync", &ENGINE_CONFIG->vsync); - if (!ENGINE_CONFIG->vsync) - ImGui::SliderInt("Max FPS", &ENGINE_CONFIG->maxFPS, 1, 300); + ImGui::Checkbox("Limit FPS", &engineState->config.limitFPS); + if (engineState->config.limitFPS) { + ImGui::Checkbox("VSync", &engineState->config.vsync); + if (!engineState->config.vsync) + ImGui::DragInt("Max FPS", &engineState->config.maxFPS, 1, 1, 300); + } + // -1 is adaptive vsync, 1 is normal vsync as a fallback + // https://wiki.libsdl.org/SDL3/SDL_GL_SetSwapInterval#remarks + if (engineState->config.limitFPS && engineState->config.vsync) { + static bool failedSwap = false; + if (failedSwap) SDL_GL_SetSwapInterval(1); + else if (SDL_GL_SetSwapInterval(-1) == -1) { + failedSwap = true; + SDL_GL_SetSwapInterval(1); + } } - if (SDL_GL_SetSwapInterval(ENGINE_CONFIG->limitFPS && ENGINE_CONFIG->vsync ? -1 : 0) == -1) - SDL_GL_SetSwapInterval(1); + else SDL_GL_SetSwapInterval(0); if (ImGui::CollapsingHeader("Mouse")) { - ImGui::SliderFloat("Sensitivity", &GAME_SETTINGS.sensitivity, 0.01f, 1.0f); + ImGui::SliderFloat("Sensitivity", &gameState->settings.sensitivity, 0.01f, 1.0f); } - if (ImGui::Button("Capture Mouse")) { - SDL_SetRelativeMouseMode(SDL_TRUE); - ImGui::SetWindowCollapsed(true); - } - ImGui::Checkbox("Wireframe", &GAME_SETTINGS.wireframe); + ImGui::Checkbox("Wireframe", &gameState->settings.wireframe); ImGui::End(); } - void drawOverlay(GameState &gameState, StatePackage &statePackage, double deltaTime) { + void drawOverlay(double deltaTime) { constexpr auto flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | @@ -127,6 +128,33 @@ namespace DebugGUI { else if (fps < 120) col = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); ImGui::TextColored(col, "%.0f FPS (%.1f ms)", fps, deltaTime * 1000.0); + + // Macro mayhem to not have duplicate code :D + // Starting to feel like a JS dev +#define LOG_LEVELS(X) \ +X(spdlog::level::trace, "Trace") \ +X(spdlog::level::debug, "Debug") \ +X(spdlog::level::info, "Info") \ +X(spdlog::level::warn, "Warn") \ +X(spdlog::level::err, "Error") \ +X(spdlog::level::critical, "Critical") \ +X(spdlog::level::off, "Off") +#define DEFINE_LOG_NAME(level, name) name, +#define DEFINE_LOG_LEVEL(level, name) level, + constexpr spdlog::level::level_enum logLevels[] = { + LOG_LEVELS(DEFINE_LOG_LEVEL) + }; + constexpr const char* logLevelNames[] = { + LOG_LEVELS(DEFINE_LOG_NAME) + }; +#undef DEFINE_LOG_NAME +#undef DEFINE_LOG_LEVEL +#undef LOG_LEVELS + static auto currentLogLevel = spdlog::get_level(); + if (ImGui::Combo("Log Level", reinterpret_cast(¤tLogLevel), logLevelNames, IM_ARRAYSIZE(logLevelNames))) { + spdlog::set_level(logLevels[currentLogLevel]); + } + ImGui::End(); } } diff --git a/src/game/gui.h b/src/game/gui.h index 59f2714..38ab9c1 100644 --- a/src/game/gui.h +++ b/src/game/gui.h @@ -1,24 +1,21 @@ -#ifndef GUI_H -#define GUI_H +#pragma once +#include -#include -#include - -struct GameState; +struct EngineState; namespace DebugGUI { - void init(SDL_Window &window, SDL_GLContext glContext); + void init(); void shutdown(); /*! * @brief Renders the debug GUI * @note Should be called AFTER all game rendering occurs, so that the debug GUI is drawn on top of everything */ - void render(GameState &gameState, StatePackage &statePackage, double deltaTime); + void render(double deltaTime); /*! * @brief Starts the rendering process for the debug GUI */ - void renderStart(GameState &gameState, StatePackage &statePackage, double deltaTime); + void renderStart(double deltaTime); /*! * @brief Ends the rendering process for the debug GUI and renders it * @note Should be called AFTER all game rendering occurs @@ -29,4 +26,3 @@ namespace DebugGUI { } -#endif //GUI_H diff --git a/src/game/player.cpp b/src/game/player.cpp new file mode 100644 index 0000000..f4e43ee --- /dev/null +++ b/src/game/player.cpp @@ -0,0 +1,24 @@ +#include "player.h" + +#include +#include + + +glm::vec3 Player::getForward() const { + const auto xz = cos(glm::radians(this->rotation.pitch)); + return glm::normalize(glm::vec3( + cos(glm::radians(this->rotation.yaw)) * cos(glm::radians(this->rotation.pitch)), + sin(glm::radians(this->rotation.pitch)), + sin(glm::radians(this->rotation.yaw)) * cos(glm::radians(this->rotation.pitch)) + )); +} +glm::vec3 Player::getRight() const { + return glm::normalize(glm::cross(getForward(), glm::vec3(0.0f, 1.0f, 0.0f))); +} +glm::vec3 Player::getUp() const { + return glm::normalize(glm::cross(getRight(), getForward())); +} + + + + diff --git a/src/game/player.h b/src/game/player.h new file mode 100644 index 0000000..19ad0b3 --- /dev/null +++ b/src/game/player.h @@ -0,0 +1,21 @@ +#pragma once +#include + +#include "engine/typedefs.h" + +class Player { +public: + glm::vec3 origin{}; + + struct Rotation { + Degrees yaw = 0.0f; + Degrees pitch = 0.0f; + Degrees roll = 0.0f; + } rotation; + [[nodiscard]] glm::vec3 getForward() const; + [[nodiscard]] glm::vec3 getRight() const; + [[nodiscard]] glm::vec3 getUp() const; + + bool tick(); +}; + diff --git a/src/game/skybox.cpp b/src/game/skybox.cpp index 12ec25a..48f5e32 100644 --- a/src/game/skybox.cpp +++ b/src/game/skybox.cpp @@ -1,12 +1,28 @@ #include "skybox.h" #include -#include #include -#include +#include "engine/state.h" +#include "engine/util/geometry.h" -Skybox::Skybox() { + +void Skybox::draw() const +{ + glDepthFunc(GL_GEQUAL); + + shader->use(); + shader->setInt("skybox", 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap->textureID); + + glBindVertexArray(VAO); + glDrawElements(GL_TRIANGLES, sizeof(CubeIndicesInside) / sizeof(unsigned int), GL_UNSIGNED_INT, nullptr); + + glDepthFunc(GL_GREATER); // Our default depth function +} + +void Skybox::initGL() { glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); @@ -24,6 +40,22 @@ Skybox::Skybox() { glEnableVertexAttribArray(0); } +Skybox::Skybox() +{ + shader = engineState->resourceManager.loadShader( + "resources/assets/shaders/sb_vert.vert", "resources/assets/shaders/sb_frag.frag"); + this->cubemap = engineState->resourceManager.errorCubemap; + initGL(); +} + +Skybox::Skybox(const std::shared_ptr& cubemap) +{ + shader = engineState->resourceManager.loadShader( + "resources/assets/shaders/sb_vert.vert", "resources/assets/shaders/sb_frag.frag"); + this->cubemap = cubemap; + initGL(); +} + Skybox::~Skybox() { glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); @@ -34,24 +66,20 @@ Skybox &Skybox::operator=(Skybox &&other) noexcept { glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); - BUFFERS_MV_FROM_TO(other, this); + this->VAO = other.VAO; + this->VBO = other.VBO; + this->EBO = other.EBO; + other.VAO = 0; + other.VBO = 0; + other.EBO = 0; } return *this; } Skybox::Skybox(Skybox &&other) noexcept { - BUFFERS_MV_FROM_TO(other, this); -} - -std::expected Skybox::draw(const unsigned int cubemap, const Engine::GraphicsShader &shader) const { - glDepthFunc(GL_GEQUAL); - - shader.setInt("skybox", 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap); - - glBindVertexArray(VAO); - glDrawElements(GL_TRIANGLES, sizeof(CubeIndicesInside) / sizeof(unsigned int), GL_UNSIGNED_INT, nullptr); - - glDepthFunc(GL_GREATER); // Our default depth function - return {}; -} + this->VAO = other.VAO; + this->VBO = other.VBO; + this->EBO = other.EBO; + other.VAO = 0; + other.VBO = 0; + other.EBO = 0; +} \ No newline at end of file diff --git a/src/game/skybox.h b/src/game/skybox.h index 1b48fb9..9aeff3c 100644 --- a/src/game/skybox.h +++ b/src/game/skybox.h @@ -1,31 +1,22 @@ -#ifndef SKYBOX_H -#define SKYBOX_H -#include -#include -#include -#include -#include -#include -#include - - -namespace Engine { - class GraphicsShader; - namespace Manager { - class TextureManager; - } -} +#pragma once +#include +#include "engine/resources/texture.h" class Skybox { public: + std::shared_ptr cubemap; + std::shared_ptr shader; + Skybox(); + Skybox(const std::shared_ptr& cubemap); ~Skybox(); - std::expected draw(unsigned int cubemap, const Engine::GraphicsShader &shader) const; + + void draw() const; private: unsigned int VAO{}, VBO{}, EBO{}; - + void initGL(); public: // Non-copyable Skybox(const Skybox&) = delete; @@ -35,4 +26,3 @@ class Skybox { Skybox& operator=(Skybox&& other) noexcept; }; -#endif diff --git a/src/game/state.h b/src/game/state.h index caeb86f..56b1e25 100644 --- a/src/game/state.h +++ b/src/game/state.h @@ -1,51 +1,30 @@ -#ifndef STATE_H -#define STATE_H +#pragma once -#include -#include -#include -#include +#include "engine/typedefs.h" +#include "game/player.h" -#include "camera.h" -#include "skybox.h" - -struct Settings { - // Mouse +struct GameSettings { float sensitivity = 0.1f; - // Graphics + Degrees baseFov = 45.0f; + float clipNear = 0.1f; + float clipFar = 100.0f; + bool wireframe = false; - // TODO: Add multiple debug modes, like viewing polygons, normals, positions, albedo, disabling post-processing effects, etc + bool backfaceCulling = true; }; -struct PlayerState { - Camera camera; - CameraController cController; - - explicit PlayerState(const Settings settings): cController(camera, settings.sensitivity) {} +struct WorldState { }; -struct LevelState { - // TODO: Storing shaders in a random vector is odd. Should they have their own managers and be associated with each thing that needs them? - std::vector shaders; - Engine::Manager::TextureManager textureManager; - Engine::Manager::SceneManager modelManager; - - std::vector modelPaths; - - PlayerState player; - Skybox skybox; +// TODO: Make state (or rather parts of it, smartly) savable (including engine state!!!) +struct GameState { + GameSettings settings{}; - explicit LevelState(const Settings settings): player(settings) {} -}; + Player playerState{}; + WorldState worldState{}; -struct GameState { - Settings settings; - LevelState level; bool isPaused = false; - bool shouldRedraw = true; - - explicit GameState(StatePackage &statePackage): settings(), level(settings) {} }; +extern GameState *gameState; -#endif //STATE_H diff --git a/subprojects/spdlog.wrap b/subprojects/spdlog.wrap new file mode 100644 index 0000000..cc0e0e1 --- /dev/null +++ b/subprojects/spdlog.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = spdlog-1.15.1 +source_url = https://github.com/gabime/spdlog/archive/refs/tags/v1.15.1.tar.gz +source_filename = spdlog-1.15.1.tar.gz +source_hash = 25c843860f039a1600f232c6eb9e01e6627f7d030a2ae5e232bdd3c9205d26cc +patch_filename = spdlog_1.15.1-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.15.1-1/get_patch +patch_hash = 75a3787aeab73c3b7426197820d9cd92a4cdf0ea5c8bbb6701c0f55c66a0a6d0 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/spdlog_1.15.1-1/spdlog-1.15.1.tar.gz +wrapdb_version = 1.15.1-1 + +[provide] +spdlog = spdlog_dep diff --git a/tools/bin2h.cpp b/tools/bin2h.cpp new file mode 100644 index 0000000..531ffcd --- /dev/null +++ b/tools/bin2h.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include +#include + +std::string sanitizeVarName(const std::string& name) { + std::string result; + for (const unsigned char ch : name) { + if (std::isalnum(ch) || ch == '_') + result += static_cast(std::toupper(ch)); + else + result += '_'; + } + return "BIN_" + result; +} + +int main(int argc, char* argv[]) { + if (argc != 3) { + std::cerr << "Invalid number of arguments.\n" + << "Usage: " << argv[0] << " input.bin output.h\n"; + return 1; + } + + std::filesystem::path inputPath{argv[1]}; + std::filesystem::path outputPath{argv[2]}; + + std::ifstream inFile(inputPath, std::ios::binary | std::ios::ate); + if (!inFile) { + std::perror(("Error opening input file: " + inputPath.string()).c_str()); + return 1; + } + + // Get file size and allocate buffer + std::streamsize fileSize = inFile.tellg(); + inFile.seekg(0, std::ios::beg); + std::vector data(fileSize); + + if (!inFile.read(reinterpret_cast(data.data()), fileSize)) { + std::cerr << "Error reading input file: " << inputPath << "\n"; + return 1; + } + + std::ofstream outFile(outputPath); + if (!outFile) { + std::perror(("Error opening output file: " + outputPath.string()).c_str()); + return 1; + } + + std::string varName = sanitizeVarName(outputPath.stem().string()); + + outFile << "#pragma once\n" + << "#include \n" + << "constexpr std::array " + << varName << " = {\n "; + + // Write each byte as a hex literal, 12 bytes per line. + outFile << std::hex << std::uppercase << std::setfill('0'); + for (size_t i = 0; i < static_cast(fileSize); ++i) { + outFile << "0x" << std::setw(2) << static_cast(data[i]); + if (i != static_cast(fileSize) - 1) { + outFile << ", "; + } + if ((i + 1) % 12 == 0 && i != static_cast(fileSize) - 1) { + outFile << "\n "; + } + } + outFile << "\n};\n"; + + return 0; +}