ADD: Asset Loading

This commit is contained in:
sebastian 2026-07-30 14:23:19 +02:00
parent 6e37fda356
commit eeb7c9c00f
25 changed files with 1178 additions and 3 deletions

View File

@ -60,6 +60,13 @@ CPMAddPackage(
GIT_TAG v1.91.5 GIT_TAG v1.91.5
DOWNLOAD_ONLY YES DOWNLOAD_ONLY YES
) )
CPMAddPackage(
NAME spdlog
GITHUB_REPOSITORY gabime/spdlog
GIT_TAG v1.15.3
)
add_library(imgui STATIC add_library(imgui STATIC
${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp
@ -103,9 +110,33 @@ add_library(Engine STATIC
engine/src/utils/openglWrapper/shader/UniformValue.h engine/src/utils/openglWrapper/shader/UniformValue.h
engine/src/layer/Scene.cpp engine/src/layer/Scene.cpp
engine/src/layer/Scene.h engine/src/layer/Scene.h
engine/src/loader/AssetLoader.cpp
engine/src/loader/AssetLoader.h
engine/src/loader/assets/AssetTypes.h
engine/src/loader/assets/AssetRequests.h
engine/src/loader/assets/AssetRequests.cpp
engine/src/loader/assets/RawAssetData.h
engine/src/loader/assets/AssetFormatters.h
engine/src/loader/textures/TextureUploader.cpp
engine/src/loader/textures/TextureUploader.h
engine/src/loader/textures/TextureImporter.cpp
engine/src/loader/textures/TextureImporter.h
engine/src/loader/textures/Texture2D.cpp
engine/src/loader/textures/Texture2D.h
engine/src/loader/models/ModelImporter.cpp
engine/src/loader/models/ModelImporter.h
engine/src/loader/models/ModelUploader.cpp
engine/src/loader/models/ModelUploader.h
engine/src/loader/models/Material.cpp
engine/src/loader/models/Material.h
engine/src/loader/assets/LoadedAssets.h
engine/src/loader/models/Model.cpp
engine/src/loader/models/Model.h
engine/src/loader/models/MeshSection.cpp
engine/src/loader/models/MeshSection.h
) )
target_include_directories(Engine PUBLIC engine/src) target_include_directories(Engine PUBLIC engine/src)
target_link_libraries(Engine PUBLIC OpenGL::GL glfw glad glm::glm imgui) target_link_libraries(Engine PUBLIC OpenGL::GL glfw glad glm::glm imgui spdlog::spdlog tinyobjloader stb_image)
# --- Executable --- # --- Executable ---
add_executable(ColorRace add_executable(ColorRace
@ -151,6 +182,4 @@ add_executable(ColorRace
target_link_libraries(ColorRace PRIVATE target_link_libraries(ColorRace PRIVATE
Engine Engine
tinyobjloader
stb_image
) )

View File

@ -0,0 +1,89 @@
//
// Created by sebastian on 30.07.26.
//
#include "AssetLoader.h"
#include "spdlog/spdlog.h"
#include "textures/TextureImporter.h"
void engine::AssetLoader::scheduleAsset(const AssetRequest &request) {
{
std::lock_guard<std::mutex> lock(pendingMutex);
pendingQueue.push(request);
++total;
}
pendingCondition.notify_one();
std::visit([]<typename T>(const T& req) -> void {
using Type = std::decay_t<T>;
if constexpr (std::is_same_v<Type, TextureRequest>) {
spdlog::debug("Scheduled Texture Request: {} -> {}", req.name, req.path);
} else if constexpr (std::is_same_v<Type, ModelRequest>) {
spdlog::debug("Scheduled Model Request: {} -> {}", req.name, req.path);
}
}, request);
}
void engine::AssetLoader::start() {
if (!running) {
running = true;
loadingThread = std::thread(&AssetLoader::loadingThreadFunc, this);
}
}
void engine::AssetLoader::stop() {
running = false;
pendingCondition.notify_all();
if (loadingThread.joinable()) {
loadingThread.join();
}
}
engine::LoadingProgress engine::AssetLoader::getProgress() const {
return {total.load(), loaded.load()};
}
void engine::AssetLoader::loadingThreadFunc() {
while (running) {
AssetRequest request;
{
std::unique_lock lock(pendingMutex);
pendingCondition.wait(lock, [this] {
return !pendingQueue.empty() || !running;
});
if (!running && pendingQueue.empty()) {
return;
}
request = pendingQueue.front();
pendingQueue.pop();
}
IntermediateAsset result = std::visit([]<typename T0>(T0& req) -> IntermediateAsset {
using T = std::decay_t<T0>;
if constexpr (std::is_same_v<T, TextureRequest>) {
return processTextureRequest(req);
} else if constexpr (std::is_same_v<T, ModelRequest>) {
return processModelRequest(req);
}
}, request);
{
std::lock_guard lock(readyMutex);
readyQueue.push(std::move(result));
}
}
}
engine::RawTextureData engine::AssetLoader::processTextureRequest(const TextureRequest &request) {
spdlog::debug("Processing Texture Request: {} -> {}", request.name, request.path);
RawTextureData textureData = TextureImporter::import(request.path, request.flipY);
textureData.name = request.name;
return textureData;
}
engine::RawModelData engine::AssetLoader::processModelRequest(const ModelRequest &request) {
spdlog::debug("Processing Model Request: {} -> {}", request.name, request.path);
}

View File

@ -0,0 +1,62 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_ASSETLOADER_H
#define COLORRACE_ASSETLOADER_H
#include <condition_variable>
#include <format>
#include <mutex>
#include <queue>
#include <string>
#include <unordered_map>
#include <variant>
#include <vector>
#include "assets/AssetRequests.h"
#include "assets/RawAssetData.h"
namespace engine {
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 scheduleAsset(const AssetRequest& request);
void start();
void stop();
void processUploadQueue(int maxPerFrame);
LoadingProgress getProgress() const;
private:
void loadingThreadFunc();
static RawTextureData processTextureRequest(const TextureRequest& request);
static RawModelData processModelRequest(const ModelRequest& request);
std::queue<AssetRequest> pendingQueue;
std::mutex pendingMutex;
std::condition_variable pendingCondition;
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 //COLORRACE_ASSETLOADER_H

View File

@ -0,0 +1,59 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_ASSETFORMATTERS_H
#define COLORRACE_ASSETFORMATTERS_H
#pragma once
#include "AssetRequests.h"
#include "AssetTypes.h"
#include <format>
template<>
struct std::formatter<engine::AssetRequest>
{
constexpr auto parse(
std::format_parse_context& ctx)
{
return ctx.begin();
}
auto format(
const engine::AssetRequest& request,
std::format_context& ctx) const
{
return std::format_to(
ctx.out(),
"{}",
engine::assetRequestToString(request)
);
}
};
template<>
struct std::formatter<engine::TextureType>
{
constexpr auto parse(
std::format_parse_context& ctx)
{
return ctx.begin();
}
auto format(
engine::TextureType type,
std::format_context& ctx) const
{
return std::format_to(
ctx.out(),
"{}",
engine::toString(type)
);
}
};
#endif //COLORRACE_ASSETFORMATTERS_H

View File

@ -0,0 +1,43 @@
//
// Created by sebastian on 30.07.26.
//
// AssetRequests.cpp
#include "AssetRequests.h"
#include <format>
#include <type_traits>
namespace engine {
std::string assetRequestToString(
const AssetRequest& request)
{
return std::visit(
[](auto const& req)
{
using Type = std::decay_t<decltype(req)>;
if constexpr(std::is_same_v<Type, TextureRequest>)
{
return std::format(
"TextureRequest{{name='{}', path='{}'}}",
req.name,
req.path
);
}
else
{
return std::format(
"ModelRequest{{name='{}', path='{}'}}",
req.name,
req.path
);
}
},
request);
}
}

View File

@ -0,0 +1,39 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_ASSEZREQUESTS_H
#define COLORRACE_ASSEZREQUESTS_H
#pragma once
#include <string>
#include <variant>
namespace engine {
struct TextureRequest
{
std::string name;
std::string path;
bool flipY = true;
};
struct ModelRequest
{
std::string name;
std::string path;
};
using AssetRequest =
std::variant<
TextureRequest,
ModelRequest
>;
std::string assetRequestToString(const AssetRequest& request);
}
#endif //COLORRACE_ASSEZREQUESTS_H

View File

@ -0,0 +1,75 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_ASSETTYPES_H
#define COLORRACE_ASSETTYPES_H
#pragma once
#include <cstdint>
#include <string_view>
namespace engine {
enum class AssetType : uint8_t
{
Texture,
Model
};
enum class TextureType : uint8_t
{
Diffuse,
Specular,
Normal,
Emissive,
Opacity,
Count
};
enum class ModelSplitPolicy : uint8_t
{
Combined,
ByMaterial,
ByObject
};
constexpr std::string_view toString(TextureType type)
{
switch(type)
{
case TextureType::Diffuse: return "Diffuse";
case TextureType::Specular: return "Specular";
case TextureType::Normal: return "Normal";
case TextureType::Emissive: return "Emissive";
case TextureType::Opacity: return "Opacity";
case TextureType::Count: break;
}
return "Unknown";
}
constexpr std::string_view toString(ModelSplitPolicy policy)
{
switch(policy)
{
case ModelSplitPolicy::Combined:
return "Combined";
case ModelSplitPolicy::ByMaterial:
return "ByMaterial";
case ModelSplitPolicy::ByObject:
return "ByObject";
}
return "Unknown";
}
}
#endif //COLORRACE_ASSETTYPES_H

View File

@ -0,0 +1,30 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_LOADEDASSETS_H
#define COLORRACE_LOADEDASSETS_H
// engine/src/loader/assets/LoadedAssets.h
#pragma once
#include <memory>
#include <string>
#include <variant>
#include "loader/textures/Texture2D.h"
#include "loader/models/Model.h"
namespace engine {
struct LoadedTexture {
std::string name;
std::shared_ptr<Texture2D> texture;
};
struct LoadedModel {
std::string name;
std::shared_ptr<Model> model;
};
using LoadedAsset = std::variant<LoadedTexture, LoadedModel>;
}
#endif //COLORRACE_LOADEDASSETS_H

View File

@ -0,0 +1,81 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_RAWASSETDATA_H
#define COLORRACE_RAWASSETDATA_H
#pragma once
#include "AssetTypes.h"
#include <string>
#include <vector>
#include <unordered_map>
#include <variant>
namespace engine {
struct RawTextureData
{
std::string name;
std::vector<unsigned char> pixels;
int width;
int height;
int channels;
};
struct RawMaterialData
{
std::string name;
std::unordered_map<TextureType, std::string> texturePaths;
float shininess;
float opacity;
};
struct RawMeshSection
{
std::vector<float> vertices;
std::vector<float> normals;
std::vector<float> textureCoords;
std::vector<int> indices;
std::string materialName;
std::string name;
};
struct RawObjectData
{
std::string name;
std::vector<RawMeshSection> subModels;
};
struct RawModelData
{
std::string name;
std::vector<RawObjectData> objects;
ModelSplitPolicy splitPolicy;
};
using IntermediateAsset =
std::variant<
RawTextureData,
RawModelData
>;
}
#endif //COLORRACE_RAWASSETDATA_H

View File

@ -0,0 +1,5 @@
//
// Created by sebastian on 30.07.26.
//
#include "Material.h"

View File

@ -0,0 +1,41 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_MATERIAL_H
#define COLORRACE_MATERIAL_H
#include <array>
#include <memory>
#include "loader/assets/AssetTypes.h"
#include "loader/textures/Texture2D.h"
namespace engine {
class Material {
public:
Material() = default;
Material(std::array<std::shared_ptr<Texture2D>, static_cast<size_t>(TextureType::Count)> textures,
float shininess, float opacity)
: m_textures(std::move(textures)), m_shininess(shininess), m_opacity(opacity) {}
void bind() const {
for (size_t i = 0; i < m_textures.size(); ++i) {
if (m_textures[i]) m_textures[i]->bind(static_cast<unsigned int>(i));
}
}
[[nodiscard]] float getShininess() const { return m_shininess; }
[[nodiscard]] float getOpacity() const { return m_opacity; }
private:
std::array<std::shared_ptr<Texture2D>, static_cast<size_t>(TextureType::Count)> m_textures;
float m_shininess = 1.0f;
float m_opacity = 1.0f;
};
}
#endif //COLORRACE_MATERIAL_H

View File

@ -0,0 +1,5 @@
//
// Created by sebastian on 30.07.26.
//
#include "MeshSection.h"

View File

@ -0,0 +1,28 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_MESHSECTION_H
#define COLORRACE_MESHSECTION_H
// engine/src/renderer/model/MeshSection.h
#pragma once
#include <string>
#include <memory>
#include "Material.h"
namespace engine {
struct MeshSection {
std::string name;
unsigned int indexOffset; // in Indizes, nicht Bytes
unsigned int indexCount;
std::shared_ptr<Material> material;
};
}
#endif //COLORRACE_MESHSECTION_H

View File

@ -0,0 +1,52 @@
//
// Created by sebastian on 30.07.26.
//
#include "Model.h"
#include "spdlog/spdlog.h"
using namespace engine;
Model::Model(std::unique_ptr<Vao> vao, int vertexCount, bool isIndexed) : m_vao(std::move(vao)), m_vertexCount(vertexCount), m_isIndexed(isIndexed) {}
Model::Model(std::unique_ptr<Vao> vao, int vertexCount) : Model(std::move(vao), vertexCount, false) {}
void Model::bind() const {
m_vao->bind();
}
void Model::unbind() const {
m_vao->unbind();
}
Model::Model(std::unique_ptr<Vao> vao, int vertexCount, std::vector<MeshSection> sections)
: m_vao(std::move(vao)), m_vertexCount(vertexCount), m_isIndexed(true), m_sections(std::move(sections)) {}
void Model::draw() const {
bind();
if (m_sections.empty()) {
// Legacy-Pfad: unverändert wie bisher, z.B. für CubeFactory-Primitive
if (m_isIndexed) glDrawElements(GL_TRIANGLES, m_vertexCount, GL_UNSIGNED_INT, nullptr);
else glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
} else {
for (const auto& section : m_sections) {
section.material->bind();
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(section.indexCount), GL_UNSIGNED_INT,
reinterpret_cast<const void*>(section.indexOffset * sizeof(unsigned int)));
}
}
unbind();
}
void Model::drawSection(const std::string& name) const {
auto it = std::ranges::find_if(m_sections,
[&](const MeshSection& s) { return s.name == name; });
if (it == m_sections.end()) {
spdlog::warn("MeshSection '{}' nicht gefunden", name);
return;
}
it->material->bind();
bind();
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(it->indexCount), GL_UNSIGNED_INT,
reinterpret_cast<const void*>(it->indexOffset * sizeof(unsigned int)));
unbind();
}

View File

@ -0,0 +1,37 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_MODEL_H
#define COLORRACE_MODEL_H
#include <memory>
#include "MeshSection.h"
#include "utils/openglWrapper/openglObjects/Vao.h"
namespace engine {
class Model {
public:
Model(std::unique_ptr<Vao> vao, int vertexCount, bool isIndexed);
Model(std::unique_ptr<Vao> vao, int vertexCount);
Model(std::unique_ptr<Vao> vao, int vertexCount, std::vector<MeshSection> sections); // neu
void bind() const;
void unbind() const;
void draw() const;
void drawSection(const std::string& name) const; // neu
[[nodiscard]] int getVertexCount() const { return m_vertexCount; }
[[nodiscard]] bool isIndexed() const { return m_isIndexed; }
[[nodiscard]] bool hasSections() const { return !m_sections.empty(); }
private:
std::unique_ptr<Vao> m_vao;
int m_vertexCount;
bool m_isIndexed;
std::vector<MeshSection> m_sections; // leer bei den alten Konstruktoren
};
}
#endif //COLORRACE_MODEL_H

View File

@ -0,0 +1,104 @@
//
// Created by sebastian on 30.07.26.
//
#include "ModelImporter.h"
#define TINYOBJLOADER_IMPLEMENTATION
#include <filesystem>
#include <unordered_set>
#include "tiny_obj_loader.h"
#include "loader/textures/TextureImporter.h"
engine::RawModelBundle engine::ModelImporter::importFromFile(const std::string& objPath) {
tinyobj::ObjReaderConfig config;
config.mtl_search_path = std::filesystem::path(objPath).parent_path().string();
config.triangulate = true; // GL braucht Dreiecke, keine n-Ecke
tinyobj::ObjReader reader;
if (!reader.ParseFromFile(objPath, config)) {
throw std::runtime_error("OBJ-Import fehlgeschlagen '" + objPath + "': " + reader.Error());
}
const auto& attrib = reader.GetAttrib();
const auto& shapes = reader.GetShapes();
const auto& tinyMaterials = reader.GetMaterials();
RawModelBundle bundle;
bundle.model.name = std::filesystem::path(objPath).stem().string();
// 1. Materialien übersetzen
for (const auto& mat : tinyMaterials) {
RawMaterialData material;
material.name = mat.name;
if (!mat.diffuse_texname.empty()) material.texturePaths[TextureType::Diffuse] = mat.diffuse_texname;
if (!mat.specular_texname.empty()) material.texturePaths[TextureType::Specular] = mat.specular_texname;
if (!mat.bump_texname.empty()) material.texturePaths[TextureType::Normal] = mat.bump_texname;
if (!mat.emissive_texname.empty()) material.texturePaths[TextureType::Emissive] = mat.emissive_texname;
if (!mat.alpha_texname.empty()) material.texturePaths[TextureType::Opacity] = mat.alpha_texname;
material.shininess = mat.shininess;
material.opacity = mat.dissolve; // 'd' in der .mtl
bundle.materials.push_back(std::move(material));
}
// 2. Referenzierte Texturen dekodieren (dedupliziert innerhalb dieses Imports)
std::unordered_set<std::string> seen;
auto mtlDir = std::filesystem::path(objPath).parent_path();
for (const auto& material : bundle.materials) {
for (const auto& [type, relPath] : material.texturePaths) {
if (seen.insert(relPath).second) {
bundle.textures.push_back(TextureImporter::import((mtlDir / relPath).string()));
}
}
}
// 3. Shapes -> Objekte, Faces nach Material gruppieren
for (const auto& shape : shapes) {
RawObjectData object;
object.name = shape.name.empty() ? "default" : shape.name;
std::unordered_map<int, RawMeshSection> sectionsByMaterial;
size_t indexOffset = 0;
for (size_t f = 0; f < shape.mesh.num_face_vertices.size(); ++f) {
int faceVertexCount = shape.mesh.num_face_vertices[f];
int materialId = shape.mesh.material_ids[f]; // -1 = kein Material zugewiesen
auto& section = sectionsByMaterial[materialId];
if (materialId >= 0) section.materialName = tinyMaterials[materialId].name;
for (int v = 0; v < faceVertexCount; ++v) {
tinyobj::index_t idx = shape.mesh.indices[indexOffset + v];
section.vertices.insert(section.vertices.end(), {
attrib.vertices[3*idx.vertex_index + 0],
attrib.vertices[3*idx.vertex_index + 1],
attrib.vertices[3*idx.vertex_index + 2]
});
if (idx.normal_index >= 0) {
section.normals.insert(section.normals.end(), {
attrib.normals[3*idx.normal_index + 0],
attrib.normals[3*idx.normal_index + 1],
attrib.normals[3*idx.normal_index + 2]
});
}
if (idx.texcoord_index >= 0) {
section.textureCoords.insert(section.textureCoords.end(), {
attrib.texcoords[2*idx.texcoord_index + 0],
attrib.texcoords[2*idx.texcoord_index + 1]
});
}
section.indices.push_back(static_cast<int>(section.indices.size()));
}
indexOffset += faceVertexCount;
}
for (auto& [materialId, section] : sectionsByMaterial) {
object.subModels.push_back(std::move(section));
}
bundle.model.objects.push_back(std::move(object));
}
return bundle;
}

View File

@ -0,0 +1,24 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_MODELIMPORTER_H
#define COLORRACE_MODELIMPORTER_H
#include "loader/assets/RawAssetData.h"
namespace engine {
struct RawModelBundle {
RawModelData model;
std::vector<RawMaterialData> materials;
std::vector<RawTextureData> textures;
};
class ModelImporter {
public:
static RawModelBundle importFromFile(const std::string& objPath);
};
}
#endif //COLORRACE_MODELIMPORTER_H

View File

@ -0,0 +1,148 @@
//
// Created by sebastian on 30.07.26.
//
#include "ModelUploader.h"
#include <array>
#include "Material.h"
#include "loader/textures/TextureUploader.h"
#include "spdlog/spdlog.h"
std::vector<engine::LoadedAsset> engine::ModelUploader::upload(const engine::RawModelBundle &bundle, engine::ModelSplitPolicy policy) {
std::vector<engine::LoadedAsset> results;
auto uploadedTextures = uploadTextures(bundle.textures, results);
auto materials = buildMaterials(bundle.materials, uploadedTextures);
switch (policy) {
case ModelSplitPolicy::ByObject:
for (const auto& object : bundle.model.objects) {
auto model = std::make_shared<Model>(uploadSections(object.subModels, materials));
results.push_back(LoadedModel{bundle.model.name + "." + object.name, model});
}
break;
case ModelSplitPolicy::Combined:
results.push_back(LoadedModel{
bundle.model.name,
std::make_shared<Model>(buildCombinedByObject(bundle.model.objects, materials))
});
break;
case ModelSplitPolicy::ByMaterial:
results.push_back(LoadedModel{
bundle.model.name,
std::make_shared<Model>(buildCombinedByMaterial(bundle.model.objects, materials))
});
break;
}
return results;
}
std::unordered_map<std::string, std::shared_ptr<engine::Texture2D>> engine::ModelUploader::uploadTextures(
const std::vector<RawTextureData> &textures, std::vector<LoadedAsset> &results) {
std::unordered_map<std::string, std::shared_ptr<engine::Texture2D>> uploadedTextures;
for (const auto& rawTexture : textures) {
auto texture = std::make_shared<engine::Texture2D>(TextureUploader::upload(rawTexture));
uploadedTextures[rawTexture.name] = texture;
results.emplace_back(LoadedTexture{rawTexture.name, texture});
}
return uploadedTextures;
}
engine::ModelUploader::MaterialMap engine::ModelUploader::buildMaterials(const std::vector<RawMaterialData> &materials, const std::unordered_map<std::string, std::shared_ptr<Texture2D> > &textures) {
MaterialMap result;
for (const auto& raw : materials) {
std::array<std::shared_ptr<Texture2D>, static_cast<size_t>(TextureType::Count)> resolved;
for (const auto& [type, texName] : raw.texturePaths) {
if (auto it = textures.find(texName); it != textures.end()) {
resolved[static_cast<size_t>(type)] = it->second;
} else {
spdlog::warn("Textur '{}' für Material '{}' nicht gefunden", texName, raw.name);
}
}
result[raw.name] = std::make_shared<Material>(resolved, raw.shininess, raw.opacity);
}
result[""] = std::make_shared<Material>(); // Fallback für Faces ohne usemtl
return result;
}
engine::Model engine::ModelUploader::uploadSections(const std::vector<RawMeshSection> &sections, const MaterialMap &materials) {
std::vector<float> vertices, normals, texCoords;
std::vector<unsigned int> indices;
std::vector<MeshSection> meshSections;
unsigned int vertexOffset = 0;
for (const auto& section : sections) {
MeshSection meshSection;
meshSection.name = section.materialName.empty() ? section.name : section.materialName;
meshSection.indexOffset = static_cast<unsigned int>(indices.size());
meshSection.indexCount = static_cast<unsigned int>(section.indices.size());
auto it = materials.find(section.materialName);
meshSection.material = (it != materials.end()) ? it->second : materials.at("");
vertices.insert(vertices.end(), section.vertices.begin(), section.vertices.end());
normals.insert(normals.end(), section.normals.begin(), section.normals.end());
texCoords.insert(texCoords.end(), section.textureCoords.begin(), section.textureCoords.end());
for (int localIndex : section.indices) indices.push_back(vertexOffset + static_cast<unsigned int>(localIndex));
vertexOffset += static_cast<unsigned int>(section.vertices.size() / 3);
meshSections.push_back(std::move(meshSection));
}
auto vao = Vao::create();
vao->bind();
vao->initDataFeed(vertices.data(), vertices.size() * sizeof(float), GL_STATIC_DRAW, /* Attribute für Location 0, 3 Komponenten */ makeAttributeList(std::make_unique<Vec3Attribute>(0)));
vao->initDataFeed(normals.data(), normals.size() * sizeof(float), GL_STATIC_DRAW, /* Attribute für Location 1, 3 Komponenten */ makeAttributeList(std::make_unique<Vec3Attribute>(1)));
vao->initDataFeed(texCoords.data(), texCoords.size() * sizeof(float), GL_STATIC_DRAW, /* Attribute für Location 2, 2 Komponenten */ makeAttributeList(std::make_unique<Vec2Attribute>(2)));
vao->createIndexBuffer(indices.data(), indices.size());
vao->unbind();
return Model(std::move(vao), static_cast<int>(indices.size()), std::move(meshSections));
}
engine::Model engine::ModelUploader::buildCombinedByObject(const std::vector<RawObjectData> &objects, const MaterialMap &materials) {
std::vector<RawMeshSection> all;
for (const auto& object : objects) {
for (auto section : object.subModels) {
section.materialName = object.name;
all.push_back(std::move(section));
}
}
return uploadSections(all, materials);
}
engine::Model engine::ModelUploader::buildCombinedByMaterial(const std::vector<RawObjectData> &objects, const MaterialMap &materials) {
std::unordered_map<std::string, RawMeshSection> merged;
for (const auto& object : objects) {
for (auto section : object.subModels) {
auto& target = merged[section.materialName];
target.materialName = section.materialName;
unsigned int offset = static_cast<unsigned int>(target.vertices.size() / 3);
target.vertices.insert(target.vertices.end(), section.vertices.begin(), section.vertices.end());
target.normals.insert(target.normals.end(), section.normals.begin(), section.normals.end());
target.textureCoords.insert(target.textureCoords.end(), section.textureCoords.begin(), section.textureCoords.end());
for (int idx : section.indices) target.indices.push_back(static_cast<int>(offset) + idx);
}
}
std::vector<RawMeshSection> all;
for (auto& [name, section] : merged) all.push_back(std::move(section));
return uploadSections(all, materials);
}
std::vector<std::unique_ptr<engine::Attribute>> engine::ModelUploader::makeAttributeList(std::unique_ptr<Attribute> attr) {
std::vector<std::unique_ptr<engine::Attribute>> result;
result.push_back(std::move(attr));
return result;
};

View File

@ -0,0 +1,33 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_MODELUPLOADER_H
#define COLORRACE_MODELUPLOADER_H
#include <vector>
#include "ModelImporter.h"
#include "loader/assets/LoadedAssets.h"
namespace engine {
class Material;
class ModelUploader {
public:
static std::vector<engine::LoadedAsset> upload(const engine::RawModelBundle& bundle, engine::ModelSplitPolicy policy);
private:
using MaterialMap = std::unordered_map<std::string, std::shared_ptr<Material>>;
static std::unordered_map<std::string, std::shared_ptr<Texture2D>> uploadTextures(const std::vector<RawTextureData>& textures, std::vector<LoadedAsset>& results);
static MaterialMap buildMaterials(const std::vector<RawMaterialData>& materials, const std::unordered_map<std::string, std::shared_ptr<Texture2D>>& textures);
static Model uploadSections(const std::vector<RawMeshSection>& sections, const MaterialMap& materials);
static Model buildCombinedByObject(const std::vector<RawObjectData> &objects, const MaterialMap& materials);
static Model buildCombinedByMaterial(const std::vector<RawObjectData> &objects, const MaterialMap& materials);
static std::vector<std::unique_ptr<Attribute>> makeAttributeList(std::unique_ptr<Attribute> attr);
};
}
#endif //COLORRACE_MODELUPLOADER_H

View File

@ -0,0 +1,46 @@
//
// Created by sebastian on 30.07.26.
//
#include "Texture2D.h"
#include "glad/glad.h"
namespace engine {
Texture2D::~Texture2D() {release();}
Texture2D::Texture2D(Texture2D&& other) noexcept
: m_id(other.m_id), m_width(other.m_width), m_height(other.m_height), m_channels(other.m_channels) {
other.m_id = 0; // Ownership übertragen: das Original darf beim Zerstören nichts mehr löschen
}
Texture2D& Texture2D::operator=(Texture2D&& other) noexcept {
if (this != &other) {
release();
m_id = other.m_id;
m_width = other.m_width;
m_height = other.m_height;
m_channels = other.m_channels;
other.m_id = 0;
}
return *this;
}
void Texture2D::release() {
if (m_id != 0) {
glDeleteTextures(1, &m_id);
m_id = 0;
}
}
void Texture2D::bind(unsigned int unit) const {
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, m_id);
}
void Texture2D::unbind(unsigned int unit) const {
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, 0);
}
}

View File

@ -0,0 +1,38 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_TEXTURE2D_H
#define COLORRACE_TEXTURE2D_H
#include "../../../../cmake-build-debug/_deps/glfw-src/src/internal.h"
namespace engine {
class Texture2D {
public:
Texture2D() = default;
~Texture2D();
Texture2D(const Texture2D&) = delete;
Texture2D& operator=(const Texture2D&) = delete;
Texture2D(Texture2D&& other) noexcept;
Texture2D& operator=(Texture2D&& other) noexcept;
void bind(unsigned int unit = 0) const;
void unbind(unsigned int unit = 0) const;
private:
GLuint m_id = 0;
int m_width = 0;
int m_height = 0;
int m_channels = 0;
void release();
friend class TextureUploader;
Texture2D(GLuint id, int width, int height, int channels) : m_id(id), m_width(width), m_height(height), m_channels(channels) {};
};
}
#endif //COLORRACE_TEXTURE2D_H

View File

@ -0,0 +1,45 @@
//
// Created by sebastian on 30.07.26.
//
#include "TextureImporter.h"
#include <stb_image.h>
engine::RawTextureData engine::TextureImporter::import(const std::filesystem::path &path, bool flipY) {
stbi_set_flip_vertically_on_load(flipY);
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* pixels = stbi_load(path.string().c_str(), &width, &height, &channels, 0);
if (!pixels) {
throw std::runtime_error(
std::string("Failed to load texture: ")
+ path.string()
+ " ("
+ stbi_failure_reason()
+ ")");
}
RawTextureData data;
data.name = path.stem().string();
data.width = width;
data.height = height;
data.channels = channels;
const auto size =
static_cast<size_t>(width)
* height
* channels;
data.pixels.assign(
pixels,
pixels + size);
stbi_image_free(pixels);
return data;
}

View File

@ -0,0 +1,20 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_TEXTUREIMPORTER_H
#define COLORRACE_TEXTUREIMPORTER_H
#include <filesystem>
#include "loader/assets/RawAssetData.h"
namespace engine {
class TextureImporter {
public:
static RawTextureData import(const std::filesystem::path& path, bool flipY = true);
};
}
#endif //COLORRACE_TEXTUREIMPORTER_H

View File

@ -0,0 +1,23 @@
//
// Created by sebastian on 30.07.26.
//
#include "TextureUploader.h"
#include "glad/glad.h"
engine::Texture2D engine::TextureUploader::upload(const RawTextureData &data) {
GLuint id;
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
GLenum format = data.channels == 4 ? GL_RGBA : data.channels == 3 ? GL_RGB : GL_RED;
glTexImage2D(GL_TEXTURE_2D, 0, format, data.width, data.height, 0, format, GL_UNSIGNED_BYTE, data.pixels.data());
glGenerateMipmap(GL_TEXTURE_2D);
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);
return {id, data.width, data.height, data.channels}; // privater Ctor, TextureUploader ist friend
}

View File

@ -0,0 +1,19 @@
//
// Created by sebastian on 30.07.26.
//
#ifndef COLORRACE_TEXTUREUPLOADER_H
#define COLORRACE_TEXTUREUPLOADER_H
#include "Texture2D.h"
#include "loader/assets/RawAssetData.h"
namespace engine {
class TextureUploader {
public:
static Texture2D upload(const RawTextureData& data);
};
}
#endif //COLORRACE_TEXTUREUPLOADER_H