From b71aa63b71f67b3fa7fe3ea8464a99153796e242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Tue, 21 Apr 2026 08:05:57 +0200 Subject: [PATCH] ADD: Async Asset Loading, closes #18 --- CMakeLists.txt | 7 + src/engine/renderer/loader/OBJLoader.cpp | 70 ++++++++ src/engine/renderer/loader/OBJLoader.h | 4 +- src/engine/renderer/loader/TextureLoader.cpp | 72 ++++++++ src/engine/renderer/loader/TextureLoader.h | 7 + .../renderer/loader/async/AssetLoader.cpp | 156 ++++++++++++++++++ .../renderer/loader/async/AssetLoader.h | 92 +++++++++++ .../renderer/loader/async/RawModelData.h | 16 ++ .../renderer/loader/async/RawModelStageData.h | 16 ++ .../loader/async/RawStagedModelData.h | 15 ++ .../renderer/loader/async/RawSubModelData.h | 19 +++ .../renderer/loader/async/RawTextureData.h | 29 ++++ 12 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 src/engine/renderer/loader/async/AssetLoader.cpp create mode 100644 src/engine/renderer/loader/async/AssetLoader.h create mode 100644 src/engine/renderer/loader/async/RawModelData.h create mode 100644 src/engine/renderer/loader/async/RawModelStageData.h create mode 100644 src/engine/renderer/loader/async/RawStagedModelData.h create mode 100644 src/engine/renderer/loader/async/RawSubModelData.h create mode 100644 src/engine/renderer/loader/async/RawTextureData.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3199a26..da784f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -292,6 +292,13 @@ if(BUILD_GAME) src/engine/renderer/config/MinimapConfig.h src/engine/core/Types.h src/engine/core/gui/text/UiTheme.h + src/engine/renderer/loader/async/RawSubModelData.h + src/engine/renderer/loader/async/RawModelData.h + src/engine/renderer/loader/async/RawTextureData.h + src/engine/renderer/loader/async/RawModelStageData.h + src/engine/renderer/loader/async/RawStagedModelData.h + src/engine/renderer/loader/async/AssetLoader.cpp + src/engine/renderer/loader/async/AssetLoader.h ) target_compile_options(Dicewars_Siedler PRIVATE -Wall diff --git a/src/engine/renderer/loader/OBJLoader.cpp b/src/engine/renderer/loader/OBJLoader.cpp index 87e12a6..473eeb9 100644 --- a/src/engine/renderer/loader/OBJLoader.cpp +++ b/src/engine/renderer/loader/OBJLoader.cpp @@ -9,7 +9,9 @@ #include #include +#include "TextureLoader.h" #include "tiny_obj_loader.h" +#include "async/RawModelData.h" std::shared_ptr OBJLoader::loadModel(const std::string &modelPath, const std::string& texturePath, Loader &loader) { tinyobj::attrib_t attrib; @@ -88,3 +90,71 @@ std::shared_ptr OBJLoader::loadModel(const std::string &modelPath return std::make_shared(subModels); } + +RawModelData OBJLoader::loadModel(const std::string &modelPath) { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + std::string baseDir = std::filesystem::path(modelPath).parent_path().string(); + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, modelPath.c_str(), baseDir.c_str()); + + if (!warn.empty()) { + std::cout << "OBJ Loader warning: " << warn << std::endl; + } + if (!err.empty()) { + std::cerr << "OBJ Loader error: " << err << std::endl; + } + if (!ret) { + throw std::runtime_error("Failed to load OBJ"); + } + + std::vector subModels; + for (const auto& shape : shapes) { + std::vector vertices; + std::vector normals; + std::vector uvs; + std::vector indices; + int indexOffset = 0; + for (const auto& index : shape.mesh.indices) { + // Vertex-Position + vertices.push_back(attrib.vertices[3*index.vertex_index + 0]); + vertices.push_back(attrib.vertices[3*index.vertex_index + 1]); + vertices.push_back(attrib.vertices[3*index.vertex_index + 2]); + + // UV-Koordinaten + if (!attrib.texcoords.empty()) { + uvs.push_back(attrib.texcoords[2*index.texcoord_index + 0]); + uvs.push_back(1.0f - attrib.texcoords[2*index.texcoord_index + 1]); + } else { + uvs.push_back(0.0f); + uvs.push_back(0.0f); + } + + normals.push_back(attrib.normals[3*index.normal_index + 0]); + normals.push_back(attrib.normals[3*index.normal_index + 1]); + normals.push_back(attrib.normals[3*index.normal_index + 2]); + + // Index + indices.push_back(indexOffset); + indexOffset++; + } + + std::vector textures; + if (!materials.empty() && !shape.mesh.material_ids.empty()) { + int matID = shape.mesh.material_ids[0]; + if (matID >= 0 && matID < static_cast(materials.size())) { + tinyobj::material_t &mat = materials[matID]; + if (!mat.diffuse_texname.empty()) { + std::string texFile = mat.diffuse_texname; + std::filesystem::path fullTexPath = std::filesystem::path(baseDir) / std::filesystem::path(texFile); + textures.push_back(TextureLoader::loadTextureData(fullTexPath.string(), false, TextureType::Diffuse)); + } + } + } + + RawSubModelData subModelData = {vertices, normals, uvs, indices, textures}; + subModels.push_back(subModelData); + } + return {"", subModels}; +} diff --git a/src/engine/renderer/loader/OBJLoader.h b/src/engine/renderer/loader/OBJLoader.h index 5f0d4c2..face3a7 100644 --- a/src/engine/renderer/loader/OBJLoader.h +++ b/src/engine/renderer/loader/OBJLoader.h @@ -10,10 +10,12 @@ #include "../model/TexturedModel.h" +struct RawModelData; + class OBJLoader { public: static std::shared_ptr loadModel(const std::string &modelPath, const std::string &texturePath, Loader &loader); - + static RawModelData loadModel(const std::string &modelPath); }; diff --git a/src/engine/renderer/loader/TextureLoader.cpp b/src/engine/renderer/loader/TextureLoader.cpp index 00d3045..ba45e1c 100644 --- a/src/engine/renderer/loader/TextureLoader.cpp +++ b/src/engine/renderer/loader/TextureLoader.cpp @@ -8,6 +8,7 @@ #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" +#include "async/RawTextureData.h" Texture2D TextureLoader::loadTexture(const std::string &path, bool flipVertically) { Texture2D texture; @@ -62,6 +63,77 @@ Texture2D TextureLoader::loadTexture(const std::string &path, bool flipVerticall return texture; } +RawTextureData TextureLoader::loadRawTextureData(const std::string &path, bool flipVertically) { + RawTextureData raw; + + stbi_set_flip_vertically_on_load(flipVertically); + unsigned char* data = stbi_load(path.c_str(), &raw.width, &raw.height, &raw.channels, 0); + + if (!data) + throw std::runtime_error("Failed to load texture: " + path); + + raw.pixels.assign(data, data + raw.width * raw.height * raw.channels); + stbi_image_free(data); // sofort freigeben, Daten sind im vector + + return raw; +} + +TextureData TextureLoader::loadTextureData(const std::string &path, bool flipVertically, TextureType textureType) { + const RawTextureData raw = loadRawTextureData(path, flipVertically); + TextureData textureData; + textureData.type = textureType; + textureData.textureData = raw; + return textureData; +} + +Texture2D TextureLoader::uploadToGPU(RawTextureData &rawTextureData) { + Texture2D texture; + texture.pixels = rawTextureData.pixels.data(); + texture.width = rawTextureData.width; + texture.height = rawTextureData.height; + texture.channels = rawTextureData.channels; + + GLenum format = GL_RGB; + if (texture.channels == 4) + format = GL_RGBA; + else if (texture.channels == 3) + format = GL_RGB; + else if (texture.channels == 1) + format = GL_RED; + + glGenTextures(1, &texture.id); + glBindTexture(GL_TEXTURE_2D, texture.id); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glTexImage2D( + GL_TEXTURE_2D, + 0, + format, + texture.width, + texture.height, + 0, + format, + GL_UNSIGNED_BYTE, + texture.pixels + ); + + glGenerateMipmap(GL_TEXTURE_2D); + + glBindTexture(GL_TEXTURE_2D, 0); + + return texture; +} + +Texture2D TextureLoader::uploadToGPU(TextureData &textureData) { + return uploadToGPU(textureData.textureData); +} + + void TextureLoader::free(Texture2D &texture) { if (texture.id != 0) { glDeleteTextures(1, &texture.id); diff --git a/src/engine/renderer/loader/TextureLoader.h b/src/engine/renderer/loader/TextureLoader.h index 5f259c5..eeeb67a 100644 --- a/src/engine/renderer/loader/TextureLoader.h +++ b/src/engine/renderer/loader/TextureLoader.h @@ -9,10 +9,17 @@ #include #include "Texture2D.h" +#include "async/RawTextureData.h" + +struct RawTextureData; class TextureLoader { public: static Texture2D loadTexture(const std::string& path, bool flipVertically= true); + static RawTextureData loadRawTextureData(const std::string &path, bool flipVertically = true); + static TextureData loadTextureData(const std::string &path, bool flipVertically = true, TextureType textureType); + static Texture2D uploadToGPU(RawTextureData& rawTextureData); + static Texture2D uploadToGPU(TextureData& textureData); static void free(Texture2D& texture); }; diff --git a/src/engine/renderer/loader/async/AssetLoader.cpp b/src/engine/renderer/loader/async/AssetLoader.cpp new file mode 100644 index 0000000..36fd4c2 --- /dev/null +++ b/src/engine/renderer/loader/async/AssetLoader.cpp @@ -0,0 +1,156 @@ +// +// Created by sebastian on 20.04.26. +// + +#include "AssetLoader.h" + +#include + +#include "json.hpp" +#include "../AssetManager.h" +#include "../OBJLoader.h" +#include "../TextureLoader.h" + +namespace fs = std::filesystem; + +void AssetLoader::scheduleTexture(const std::string &name, const std::string &path) { + { + std::lock_guard lock(pendingMutex); + pendingQueue.push(TextureRequest{name, path}); + ++total; + } + pendingCV.notify_one(); +} + +void AssetLoader::scheduleModel(const std::string &name, const std::string &objPath) { + { + std::lock_guard lock(pendingMutex); + pendingQueue.push(ModelRequest{name, objPath}); + ++total; + } + pendingCV.notify_one(); +} + +void AssetLoader::scheduleAsset(const std::string &name, const std::string &assetPath) { + { + std::lock_guard lock(pendingMutex); + pendingQueue.push(StagedModelRequest{name, assetPath}); + ++total; + } + pendingCV.notify_one(); +} + +void AssetLoader::start() { + running = true; + loadingThread = std::thread(&AssetLoader::loadingThreadFunc, this); +} + +void AssetLoader::stop() { + running = false; + pendingCV.notify_all(); + if (loadingThread.joinable()) { + loadingThread.join(); + } +} + +std::vector AssetLoader::processUploadQueue(int maxPerFrame) { + std::vector results; + std::lock_guard lock(readyMutex); + for (int i = 0; i < maxPerFrame && !readyQueue.empty(); ++i) { + results.push_back(std::move(readyQueue.front())); + readyQueue.pop(); + } + return results; +} + +LoadingProgress AssetLoader::getProgress() const { + return {total.load(), loaded.load()}; +} + +void AssetLoader::loadingThreadFunc() { + while (running) { + AssetRequest request; + { + std::unique_lock lock(pendingMutex); + pendingCV.wait(lock, [this] { + return !pendingQueue.empty() || !running; + }); + + if (!running && pendingQueue.empty()) { + return; + } + + request = pendingQueue.front(); + pendingQueue.pop(); + } + + IntermediateAsset result = std::visit([](auto& req) -> IntermediateAsset { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return processTextureRequest(req); + } else if constexpr (std::is_same_v) { + return processModelRequest(req); + } else { + return processStagedModelRequest(req); + } + }, request); + + { + std::lock_guard lock(readyMutex); + readyQueue.push(std::move(result)); + ++loaded; + } + } +} + +RawTextureData AssetLoader::processTextureRequest(const TextureRequest &request) { + RawTextureData textureData = TextureLoader::loadRawTextureData(request.path, request.flipVertically); + textureData.name = request.name; + return textureData; +} + +RawModelData AssetLoader::processModelRequest(const ModelRequest &request) { + RawModelData modelData = OBJLoader::loadModel(request.objPath); + modelData.name = request.name; + return modelData; +} + +RawStagedModelData AssetLoader::processStagedModelRequest(const StagedModelRequest &request) { + nlohmann::json assetConfiguration = loadJson(request.assetPath); + + std::vector modelStages; + std::string baseDir = std::filesystem::path(request.assetPath).parent_path().string(); + + for (const auto& modelStage: assetConfiguration["modelStages"]) { + auto name = modelStage["name"].get(); + auto objPath = modelStage["filename"].get(); + const auto conditionKey = modelStage["conditionKey"].get(); + const float min = modelStage["minValue"].get(); + const float max = modelStage["maxValue"].get(); + + std::filesystem::path fullObjPath = std::filesystem::path(baseDir) / std::filesystem::path(objPath); + + ModelRequest modelRequest = {name, fullObjPath.string()}; + RawModelData modelData = processModelRequest(modelRequest); + + RawModelStageData modelStageData = {name, conditionKey, min, max, modelData}; + modelStages.push_back(modelStageData); + } + + RawStagedModelData rawStagedModelData = {request.name, modelStages}; + return rawStagedModelData; +} + +nlohmann::json AssetLoader::loadJson(const std::string &path) { + if (fs::exists(path)) { + std::ifstream filestream(path); + nlohmann::json j; + filestream >> j; + return j; + } + return nlohmann::json::object(); +} + + + + diff --git a/src/engine/renderer/loader/async/AssetLoader.h b/src/engine/renderer/loader/async/AssetLoader.h new file mode 100644 index 0000000..76a5d3e --- /dev/null +++ b/src/engine/renderer/loader/async/AssetLoader.h @@ -0,0 +1,92 @@ +// +// Created by sebastian on 20.04.26. +// + +#ifndef ASSETLOADER_H +#define ASSETLOADER_H +#include +#include +#include +#include + +#include "json.hpp" +#include "RawModelData.h" +#include "RawStagedModelData.h" +#include "RawTextureData.h" + +enum class AssetType { + Texture, + Model, + StagedModel +}; + +struct TextureRequest { + std::string name; + std::string path; + bool flipVertically = true; +}; + +struct ModelRequest { + std::string name; + std::string objPath; +}; + +struct StagedModelRequest { + std::string name; + std::string assetPath; +}; + +using AssetRequest = std::variant; +using IntermediateAsset = std::variant; + +struct LoadingProgress { + int total; + int loaded; + float fraction() const { + return total > 0 ? static_cast(loaded) / total : 1.f; + } + bool isDone() const { + return loaded >= total; + } +}; + +class AssetLoader { +public: + void scheduleTexture(const std::string& name, const std::string& path); + void scheduleModel(const std::string& name, const std::string& objPath); + void scheduleAsset(const std::string& name, const std::string& assetPath); + + void start(); + void stop(); + + std::vector processUploadQueue(int maxPerFrame); + + LoadingProgress getProgress() const; + +private: + void loadingThreadFunc(); + + static RawTextureData processTextureRequest(const TextureRequest& request); + static RawModelData processModelRequest(const ModelRequest& request); + static RawStagedModelData processStagedModelRequest(const StagedModelRequest& request); + static nlohmann::json loadJson(const std::string& path); + + // Render-Thread schreibt, Loading Thread liest + std::queue pendingQueue; + std::mutex pendingMutex; + std::condition_variable pendingCV; + + //Loading Thread schreibt, Render-Thread liest + std::queue readyQueue; + std::mutex readyMutex; + + std::atomic total{0}; + std::atomic loaded{0}; + + std::thread loadingThread; + std::atomic running{false}; +}; + + + +#endif //ASSETLOADER_H diff --git a/src/engine/renderer/loader/async/RawModelData.h b/src/engine/renderer/loader/async/RawModelData.h new file mode 100644 index 0000000..ea24532 --- /dev/null +++ b/src/engine/renderer/loader/async/RawModelData.h @@ -0,0 +1,16 @@ +// +// Created by sebastian on 20.04.26. +// + +#ifndef RAWMODELDATA_H +#define RAWMODELDATA_H +#include +#include + +#include "RawSubModelData.h" + +struct RawModelData { + std::string name; + std::vector subModels; +}; +#endif //RAWMODELDATA_H diff --git a/src/engine/renderer/loader/async/RawModelStageData.h b/src/engine/renderer/loader/async/RawModelStageData.h new file mode 100644 index 0000000..fca270f --- /dev/null +++ b/src/engine/renderer/loader/async/RawModelStageData.h @@ -0,0 +1,16 @@ +// +// Created by sebastian on 20.04.26. +// + +#ifndef RAWMODELSTAGEDATA_H +#define RAWMODELSTAGEDATA_H +#include "RawModelData.h" + +struct RawModelStageData { + std::string stageName; + std::string conditionKey; + float minValue; + float maxValue; + RawModelData modelData; +}; +#endif //RAWMODELSTAGEDATA_H diff --git a/src/engine/renderer/loader/async/RawStagedModelData.h b/src/engine/renderer/loader/async/RawStagedModelData.h new file mode 100644 index 0000000..b513f98 --- /dev/null +++ b/src/engine/renderer/loader/async/RawStagedModelData.h @@ -0,0 +1,15 @@ +// +// Created by sebastian on 20.04.26. +// + +#ifndef RAWSTAGEDMODELDATA_H +#define RAWSTAGEDMODELDATA_H +#include + +#include "RawModelStageData.h" + +struct RawStagedModelData { + std::string name; + std::vector stages; +}; +#endif //RAWSTAGEDMODELDATA_H diff --git a/src/engine/renderer/loader/async/RawSubModelData.h b/src/engine/renderer/loader/async/RawSubModelData.h new file mode 100644 index 0000000..2caf061 --- /dev/null +++ b/src/engine/renderer/loader/async/RawSubModelData.h @@ -0,0 +1,19 @@ +// +// Created by sebastian on 20.04.26. +// + +#ifndef RAWSUBMODELDATA_H +#define RAWSUBMODELDATA_H +#include + +#include "RawTextureData.h" + +struct RawSubModelData { + std::vector vertices; + std::vector normals; + std::vector textureCoords; + std::vector indices; + + std::vector textures; +}; +#endif //RAWSUBMODELDATA_H diff --git a/src/engine/renderer/loader/async/RawTextureData.h b/src/engine/renderer/loader/async/RawTextureData.h new file mode 100644 index 0000000..5e7efa2 --- /dev/null +++ b/src/engine/renderer/loader/async/RawTextureData.h @@ -0,0 +1,29 @@ +// +// Created by sebastian on 20.04.26. +// + +#ifndef RAWTEXTUREDATA_H +#define RAWTEXTUREDATA_H +#include + +enum class TextureType { + Diffuse, + Specular, + Normal, + Emissive, + Opacity +}; + +struct RawTextureData { + std::string name; + std::vector pixels; + int width, height, channels; +}; + +struct TextureData { + TextureType type; + RawTextureData textureData; +}; + + +#endif //RAWTEXTUREDATA_H