ADD: Async Asset Loading, closes #18
All checks were successful
Tests / test (push) Successful in 2m37s

This commit is contained in:
Sebastian Böckelmann 2026-04-21 08:05:57 +02:00
parent 0894c5615e
commit b71aa63b71
12 changed files with 502 additions and 1 deletions

View File

@ -292,6 +292,13 @@ if(BUILD_GAME)
src/engine/renderer/config/MinimapConfig.h src/engine/renderer/config/MinimapConfig.h
src/engine/core/Types.h src/engine/core/Types.h
src/engine/core/gui/text/UiTheme.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 target_compile_options(Dicewars_Siedler PRIVATE
-Wall -Wall

View File

@ -9,7 +9,9 @@
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>
#include "TextureLoader.h"
#include "tiny_obj_loader.h" #include "tiny_obj_loader.h"
#include "async/RawModelData.h"
std::shared_ptr<TexturedModel> OBJLoader::loadModel(const std::string &modelPath, const std::string& texturePath, Loader &loader) { std::shared_ptr<TexturedModel> OBJLoader::loadModel(const std::string &modelPath, const std::string& texturePath, Loader &loader) {
tinyobj::attrib_t attrib; tinyobj::attrib_t attrib;
@ -88,3 +90,71 @@ std::shared_ptr<TexturedModel> OBJLoader::loadModel(const std::string &modelPath
return std::make_shared<TexturedModel>(subModels); return std::make_shared<TexturedModel>(subModels);
} }
RawModelData OBJLoader::loadModel(const std::string &modelPath) {
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> 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<RawSubModelData> subModels;
for (const auto& shape : shapes) {
std::vector<float> vertices;
std::vector<float> normals;
std::vector<float> uvs;
std::vector<int> 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<TextureData> textures;
if (!materials.empty() && !shape.mesh.material_ids.empty()) {
int matID = shape.mesh.material_ids[0];
if (matID >= 0 && matID < static_cast<int>(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};
}

View File

@ -10,10 +10,12 @@
#include "../model/TexturedModel.h" #include "../model/TexturedModel.h"
struct RawModelData;
class OBJLoader { class OBJLoader {
public: public:
static std::shared_ptr<TexturedModel> loadModel(const std::string &modelPath, const std::string &texturePath, Loader &loader); static std::shared_ptr<TexturedModel> loadModel(const std::string &modelPath, const std::string &texturePath, Loader &loader);
static RawModelData loadModel(const std::string &modelPath);
}; };

View File

@ -8,6 +8,7 @@
#define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h" #include "stb_image.h"
#include "async/RawTextureData.h"
Texture2D TextureLoader::loadTexture(const std::string &path, bool flipVertically) { Texture2D TextureLoader::loadTexture(const std::string &path, bool flipVertically) {
Texture2D texture; Texture2D texture;
@ -62,6 +63,77 @@ Texture2D TextureLoader::loadTexture(const std::string &path, bool flipVerticall
return texture; 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) { void TextureLoader::free(Texture2D &texture) {
if (texture.id != 0) { if (texture.id != 0) {
glDeleteTextures(1, &texture.id); glDeleteTextures(1, &texture.id);

View File

@ -9,10 +9,17 @@
#include <string> #include <string>
#include "Texture2D.h" #include "Texture2D.h"
#include "async/RawTextureData.h"
struct RawTextureData;
class TextureLoader { class TextureLoader {
public: public:
static Texture2D loadTexture(const std::string& path, bool flipVertically= true); 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); static void free(Texture2D& texture);
}; };

View File

@ -0,0 +1,156 @@
//
// Created by sebastian on 20.04.26.
//
#include "AssetLoader.h"
#include <fstream>
#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<IntermediateAsset> AssetLoader::processUploadQueue(int maxPerFrame) {
std::vector<IntermediateAsset> 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<decltype(req)>;
if constexpr (std::is_same_v<T, TextureRequest>) {
return processTextureRequest(req);
} else if constexpr (std::is_same_v<T, ModelRequest>) {
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<RawModelStageData> modelStages;
std::string baseDir = std::filesystem::path(request.assetPath).parent_path().string();
for (const auto& modelStage: assetConfiguration["modelStages"]) {
auto name = modelStage["name"].get<std::string>();
auto objPath = modelStage["filename"].get<std::string>();
const auto conditionKey = modelStage["conditionKey"].get<std::string>();
const float min = modelStage["minValue"].get<float>();
const float max = modelStage["maxValue"].get<float>();
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();
}

View File

@ -0,0 +1,92 @@
//
// Created by sebastian on 20.04.26.
//
#ifndef ASSETLOADER_H
#define ASSETLOADER_H
#include <condition_variable>
#include <queue>
#include <string>
#include <variant>
#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<TextureRequest, ModelRequest, StagedModelRequest>;
using IntermediateAsset = std::variant<RawTextureData, RawModelData, RawStagedModelData>;
struct LoadingProgress {
int total;
int loaded;
float fraction() const {
return total > 0 ? static_cast<float>(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<IntermediateAsset> 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<AssetRequest> pendingQueue;
std::mutex pendingMutex;
std::condition_variable pendingCV;
//Loading Thread schreibt, Render-Thread liest
std::queue<IntermediateAsset> readyQueue;
std::mutex readyMutex;
std::atomic<int> total{0};
std::atomic<int> loaded{0};
std::thread loadingThread;
std::atomic<bool> running{false};
};
#endif //ASSETLOADER_H

View File

@ -0,0 +1,16 @@
//
// Created by sebastian on 20.04.26.
//
#ifndef RAWMODELDATA_H
#define RAWMODELDATA_H
#include <string>
#include <vector>
#include "RawSubModelData.h"
struct RawModelData {
std::string name;
std::vector<RawSubModelData> subModels;
};
#endif //RAWMODELDATA_H

View File

@ -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

View File

@ -0,0 +1,15 @@
//
// Created by sebastian on 20.04.26.
//
#ifndef RAWSTAGEDMODELDATA_H
#define RAWSTAGEDMODELDATA_H
#include <vector>
#include "RawModelStageData.h"
struct RawStagedModelData {
std::string name;
std::vector<RawModelStageData> stages;
};
#endif //RAWSTAGEDMODELDATA_H

View File

@ -0,0 +1,19 @@
//
// Created by sebastian on 20.04.26.
//
#ifndef RAWSUBMODELDATA_H
#define RAWSUBMODELDATA_H
#include <vector>
#include "RawTextureData.h"
struct RawSubModelData {
std::vector<float> vertices;
std::vector<float> normals;
std::vector<float> textureCoords;
std::vector<int> indices;
std::vector<TextureData> textures;
};
#endif //RAWSUBMODELDATA_H

View File

@ -0,0 +1,29 @@
//
// Created by sebastian on 20.04.26.
//
#ifndef RAWTEXTUREDATA_H
#define RAWTEXTUREDATA_H
#include <vector>
enum class TextureType {
Diffuse,
Specular,
Normal,
Emissive,
Opacity
};
struct RawTextureData {
std::string name;
std::vector<unsigned char> pixels;
int width, height, channels;
};
struct TextureData {
TextureType type;
RawTextureData textureData;
};
#endif //RAWTEXTUREDATA_H