diff --git a/.gitmodules b/.gitmodules
index 1e5c26deddea8cb725aae8c84513d1dcd18e4cfb..cc3bf1fcd2e1eb8117cbcc7222b04f7041fea520 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -22,9 +22,6 @@
 [submodule "modules/gui/lib/imgui"]
 	path = modules/gui/lib/imgui
 	url = https://github.com/ocornut/imgui.git
-[submodule "lib/VulkanMemoryAllocator"]
-	path = lib/VulkanMemoryAllocator
-	url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
 [submodule "lib/VulkanMemoryAllocator-Hpp"]
 	path = lib/VulkanMemoryAllocator-Hpp
 	url = https://github.com/malte-v/VulkanMemoryAllocator-Hpp.git
diff --git a/include/vkcv/Logger.hpp b/include/vkcv/Logger.hpp
index 1ae0f211e1a3255d624cf78985b0797e9d90c634..bb60561e80baadfcac4956223d9313893547068f 100644
--- a/include/vkcv/Logger.hpp
+++ b/include/vkcv/Logger.hpp
@@ -53,9 +53,10 @@ namespace vkcv {
     VKCV_DEBUG_MESSAGE_LEN,                \
     __VA_ARGS__                            \
   );                                       \
+  auto output = getLogOutput(level);       \
   if (level != vkcv::LogLevel::RAW_INFO) { \
     fprintf(                               \
-      getLogOutput(level),                 \
+      output,                              \
       "[%s]: %s [%s, line %d: %s]\n",      \
       vkcv::getLogName(level),             \
       output_message,                      \
@@ -65,12 +66,13 @@ namespace vkcv {
     );                                     \
   } else {                                 \
     fprintf(                               \
-      getLogOutput(level),                 \
+      output,                              \
       "[%s]: %s\n",                        \
       vkcv::getLogName(level),             \
       output_message                       \
     );                                     \
   }                                        \
+  fflush(output);                          \
 }
 
 #else
diff --git a/modules/asset_loader/CMakeLists.txt b/modules/asset_loader/CMakeLists.txt
index c5a1fd0eb9620d3a95af1c52a9e7456337047c7b..870c16279b1578224a966a4a123a465413333555 100644
--- a/modules/asset_loader/CMakeLists.txt
+++ b/modules/asset_loader/CMakeLists.txt
@@ -31,10 +31,10 @@ include(config/FX_GLTF.cmake)
 include(config/STB.cmake)
 
 # link the required libraries to the module
-target_link_libraries(vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv)
+target_link_libraries(vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv ${vkcv_libraries})
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(vkcv_asset_loader SYSTEM BEFORE PRIVATE ${vkcv_asset_loader_includes})
+target_include_directories(vkcv_asset_loader SYSTEM BEFORE PRIVATE ${vkcv_asset_loader_includes} ${vkcv_includes})
 
 # add the own include directory for public headers
 target_include_directories(vkcv_asset_loader BEFORE PUBLIC ${vkcv_asset_loader_include})
diff --git a/modules/asset_loader/include/vkcv/asset/asset_loader.hpp b/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
index 471870fb1e5af3d3c448a66611d9754db9597f85..25d7d5b36d00bad8587fbe082f8ebd041d84fde6 100644
--- a/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
+++ b/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
@@ -11,17 +11,7 @@
 #include <cstdint>
 #include <filesystem>
 
-/** These macros define limits of the following structs. Implementations can
- * test against these limits when performing sanity checks. The main constraint
- * expressed is that of the data type: Material indices are identified by a
- * uint8_t in the VertexGroup struct, so there can't be more than UINT8_MAX
- * materials in the mesh. Should these limits be too narrow, the data type has
- * to be changed, but the current ones should be generous enough for most use
- * cases. */
-#define MAX_MATERIALS_PER_MESH UINT8_MAX
-#define MAX_VERTICES_PER_VERTEX_GROUP UINT32_MAX
-
-/** LOADING MESHES
+/* LOADING MESHES
  * The description of meshes is a hierarchy of structures with the Mesh at the
  * top.
  *
@@ -46,53 +36,89 @@
 
 namespace vkcv::asset {
 
-/** This enum matches modes in fx-gltf, the library returns a standard mode
- * (TRIANGLES) if no mode is given in the file. */
+/**
+ * These return codes are limited to the asset loader module. If unified return
+ * codes are defined for the vkcv framework, these will be used instead.
+ */
+#define ASSET_ERROR 0
+#define ASSET_SUCCESS 1
+
+/**
+ * This enum matches modes in fx-gltf, the library returns a standard mode
+ * (TRIANGLES) if no mode is given in the file.
+ */
 enum class PrimitiveMode : uint8_t {
-	POINTS=0, LINES, LINELOOP, LINESTRIP, TRIANGLES, TRIANGLESTRIP,
-	TRIANGLEFAN
+	POINTS = 0,
+	LINES = 1,
+	LINELOOP = 2,
+	LINESTRIP = 3,
+	TRIANGLES = 4,
+	TRIANGLESTRIP = 5,
+	TRIANGLEFAN = 6
 };
 
-/** The indices in the index buffer can be of different bit width. */
-enum class IndexType : uint8_t { UNDEFINED=0, UINT8=1, UINT16=2, UINT32=3 };
-
-typedef struct {
-	// TODO define struct for samplers (low priority)
-	// NOTE: glTF defines samplers based on OpenGL, which can not be
-	// directly translated to Vulkan. Specifically, OpenGL (and glTF)
-	// define a different set of Min/Mag-filters than Vulkan.
-} Sampler;
-
-/** struct for defining the loaded texture */
-typedef struct {
-	int sampler;		// index into the sampler array of the Scene
-	uint8_t channels;	// number of channels
-	uint16_t w, h;		// width and height of the texture
-	std::vector<uint8_t> data;	// binary data of the decoded texture
-} Texture;
+/**
+ * The indices in the index buffer can be of different bit width.
+ */
+enum class IndexType : uint8_t {
+	UNDEFINED=0,
+	UINT8=1,
+	UINT16=2,
+	UINT32=3
+};
 
-/** The asset loader module only supports the PBR-MetallicRoughness model for
- * materials.*/
-typedef struct {
-	uint16_t textureMask;	// bit mask with active texture targets
-	// Indices into the Array.textures array
-	int baseColor, metalRough, normal, occlusion, emissive;
-	// Scaling factors for each texture target
-	struct { float r, g, b, a; } baseColorFactor;
-	float metallicFactor, roughnessFactor;
-	float normalScale;
-	float occlusionStrength;
-	struct { float r, g, b; } emissiveFactor;
-} Material;
+/**
+ * This struct defines a sampler for a texture object. All values here can
+ * directly be passed to VkSamplerCreateInfo.
+ * NOTE that glTF defines samplers based on OpenGL, which can not be directly
+ * translated to Vulkan. The vkcv::asset::Sampler struct defined here adheres
+ * to the Vulkan spec, having alerady translated the flags from glTF to Vulkan.
+ * Since glTF does not specify border sampling for more than two dimensions,
+ * the addressModeW is hardcoded to a default: VK_SAMPLER_ADDRESS_MODE_REPEAT.
+ */
+struct Sampler {
+	int minFilter, magFilter;
+	int mipmapMode;
+	float minLOD, maxLOD;
+	int addressModeU, addressModeV, addressModeW;
+};
 
-/** Flags for the bit-mask in the Material struct. To check if a material has a
+/**
+ * This struct describes a (partially) loaded texture.
+ * The data member is not populated after calling probeScene() but only when
+ * calling loadMesh(), loadScene() or loadTexture(). Note that textures are
+ * currently always loaded with 4 channels as RGBA, even if the image has just
+ * RGB or is grayscale. In the case where the glTF-file does not provide a URI
+ * but references a buffer view for the raw data, the path member will be empty
+ * even though the rest is initialized properly.
+ * NOTE: Loading textures without URI is untested.
+ */
+struct Texture {
+	std::filesystem::path path;	// URI to the encoded texture data
+	int sampler;			// index into the sampler array of the Scene
+	
+	union { int width; int w; };
+	union { int height; int h; };
+	int channels;
+	
+	std::vector<uint8_t> data;	// binary data of the decoded texture
+};
+
+/**
+ * Flags for the bit-mask in the Material struct. To check if a material has a
  * certain texture target, you can use the hasTexture() function below, passing
- * the material struct and the enum. */
+ * the material struct and the enum.
+ */
 enum class PBRTextureTarget {
-	baseColor=1, metalRough=2, normal=4, occlusion=8, emissive=16
+	baseColor=1,
+	metalRough=2,
+	normal=4,
+	occlusion=8,
+	emissive=16
 };
 
-/** This macro translates the index of an enum in the defined order to an
+/**
+ * This macro translates the index of an enum in the defined order to an
  * integer with a single bit set in the corresponding place. It is used for
  * working with the bitmask of texture targets ("textureMask") in the Material
  * struct:
@@ -103,100 +129,196 @@ enum class PBRTextureTarget {
  * contact with bit-level operations. */
 #define bitflag(ENUM) (0x1u << ((unsigned)(ENUM)))
 
-/** To signal that a certain texture target is active in a Material struct, its
- * bit is set in the textureMask. You can use this function to check that:
- * Material mat = ...;
- * if (materialHasTexture(&mat, baseColor)) {...} */
-bool materialHasTexture(const Material *const m, const PBRTextureTarget t);
+/**
+ * The asset loader module only supports the PBR-MetallicRoughness model for
+ * materials.
+ */
+struct Material {
+	uint16_t textureMask;	// bit mask with active texture targets
+	
+	// Indices into the Scene.textures vector
+	int baseColor, metalRough, normal, occlusion, emissive;
+	
+	// Scaling factors for each texture target
+	struct { float r, g, b, a; } baseColorFactor;
+	float metallicFactor, roughnessFactor;
+	float normalScale;
+	float occlusionStrength;
+	struct { float r, g, b; } emissiveFactor;
+
+	/**
+	 * To signal that a certain texture target is active in this Material
+	 * struct, its bit is set in the textureMask. You can use this function
+	 * to check that:
+	 * if (myMaterial.hasTexture(baseColor)) {...}
+	 *
+	 * @param t The target to query for
+	 * @return Boolean to signal whether the texture target is active in
+	 * the material.
+	 */
+	bool hasTexture(PBRTextureTarget target) const;
+};
 
-/** With these enums, 0 is reserved to signal uninitialized or invalid data. */
+/* With these enums, 0 is reserved to signal uninitialized or invalid data. */
 enum class PrimitiveType : uint32_t {
     UNDEFINED = 0,
     POSITION = 1,
     NORMAL = 2,
     TEXCOORD_0 = 3,
     TEXCOORD_1 = 4,
-    TANGENT = 5
+    TANGENT = 5,
+    COLOR_0 = 6,
+    COLOR_1 = 7,
+    JOINTS_0 = 8,
+    WEIGHTS_0 = 9
 };
 
-/** These integer values are used the same way in OpenGL, Vulkan and glTF. This
+/**
+ * These integer values are used the same way in OpenGL, Vulkan and glTF. This
  * enum is not needed for translation, it's only for the programmers
  * convenience (easier to read in if/switch statements etc). While this enum
  * exists in (almost) the same definition in the fx-gltf library, we want to
- * avoid exposing that dependency, thus it is re-defined here. */
+ * avoid exposing that dependency, thus it is re-defined here.
+ */
 enum class ComponentType : uint16_t {
-    NONE = 0, INT8 = 5120, UINT8 = 5121, INT16 = 5122, UINT16 = 5123,
-    UINT32 = 5125, FLOAT32 = 5126
+    NONE = 0,
+    INT8 = 5120,
+    UINT8 = 5121,
+    INT16 = 5122,
+    UINT16 = 5123,
+    UINT32 = 5125,
+    FLOAT32 = 5126
 };
 
-/** This struct describes one vertex attribute of a vertex buffer. */
-typedef struct {
+/**
+ * This struct describes one vertex attribute of a vertex buffer.
+ */
+struct VertexAttribute {
     PrimitiveType type;			// POSITION, NORMAL, ...
+    
     uint32_t offset;			// offset in bytes
     uint32_t length;			// length of ... in bytes
     uint32_t stride;			// stride in bytes
-    ComponentType componentType;		// eg. 5126 for float
-    uint8_t  componentCount;	// eg. 3 for vec3
-} VertexAttribute;
+    
+    ComponentType componentType;	// eg. 5126 for float
+    uint8_t  componentCount;		// eg. 3 for vec3
+};
 
-/** This struct represents one (possibly the only) part of a mesh. There is
+/**
+ * This struct represents one (possibly the only) part of a mesh. There is
  * always one vertexBuffer and zero or one indexBuffer (indexed rendering is
  * common but not always used). If there is no index buffer, this is indicated
  * by indexBuffer.data being empty. Each vertex buffer can have one or more
- * vertex attributes. */
-typedef struct {
+ * vertex attributes.
+ */
+struct VertexGroup {
 	enum PrimitiveMode mode;	// draw as points, lines or triangle?
-	size_t numIndices, numVertices;
+	size_t numIndices;
+	size_t numVertices;
+	
 	struct {
 		enum IndexType type;	// data type of the indices
 		std::vector<uint8_t> data; // binary data of the index buffer
 	} indexBuffer;
+	
 	struct {
 		std::vector<uint8_t> data; // binary data of the vertex buffer
 		std::vector<VertexAttribute> attributes; // description of one
 	} vertexBuffer;
+	
 	struct { float x, y, z; } min;	// bounding box lower left
 	struct { float x, y, z; } max;	// bounding box upper right
+	
 	int materialIndex;		// index to one of the materials
-} VertexGroup;
+};
 
-/** This struct represents a single mesh as it was loaded from a glTF file. It
+/**
+ * This struct represents a single mesh as it was loaded from a glTF file. It
  * consists of at least one VertexGroup, which then references other resources
- * such as Materials. */
-typedef struct {
+ * such as Materials.
+ */
+struct Mesh {
 	std::string name;
 	std::array<float, 16> modelMatrix;
 	std::vector<int> vertexGroups;
-} Mesh;
+};
 
-/** The scene struct is simply a collection of objects in the scene as well as
+/**
+ * The scene struct is simply a collection of objects in the scene as well as
  * the resources used by those objects.
- * For now the only type of object are the meshes and they are represented in a
- * flat array.
- * Note that parent-child relations are not yet possible. */
-typedef struct {
+ * Note that parent-child relations are not yet possible.
+ */
+struct Scene {
 	std::vector<Mesh> meshes;
 	std::vector<VertexGroup> vertexGroups;
 	std::vector<Material> materials;
 	std::vector<Texture> textures;
 	std::vector<Sampler> samplers;
-} Scene;
+	std::vector<std::string> uris;
+};
 
 /**
- * Load every mesh from the glTF file, as well as materials and textures.
+ * Parse the given glTF file and create a shallow description of the content.
+ * Only the meta-data of the objects in the scene is loaded, not the binary
+ * content. The rationale is to provide a means of probing the content of a
+ * glTF file without the costly process of loading and decoding large amounts
+ * of data. The returned Scene struct can be used to search for specific meshes
+ * in the scene, that can then be loaded on their own using the loadMesh()
+ * function. Note that the Scene struct received as output argument will be
+ * overwritten by this function.
+ * After this function completes, the returned Scene struct is completely
+ * initialized and all information is final, except for the missing binary
+ * data. This means that indices to vectors will remain valid even when the
+ * shallow scene struct is filled with data by loadMesh().
+ * Note that for URIs only (local) filesystem paths are supported, no
+ * URLs using network protocols etc.
  *
- * @param path must be the path to a glTF or glb file.
+ * @param path	must be the path to a glTF- or glb-file.
+ * @param scene	is a reference to a Scene struct that will be filled with the
+ * 	meta-data of all objects described in the glTF file.
+ * @return ASSET_ERROR on failure, otherwise ASSET_SUCCESS
+ */
+int probeScene(const std::filesystem::path &path, Scene &scene);
+
+/**
+ * This function loads a single mesh from the given file and adds it to the
+ * given scene. The scene must already be initialized (via probeScene()).
+ * The mesh_index refers to the Scenes meshes array and identifies the mesh to
+ * load. To find the mesh you want, iterate over the probed scene and check the
+ * meshes details (eg. name).
+ * Besides the mesh, this function will also add any associated data to the
+ * Scene struct such as Materials and Textures required by the Mesh.
+ *
+ * @param path	must be the path to a glTF- or glb-file.
+ * @param scene	is the scene struct to which the results will be written.
+ * @return ASSET_ERROR on failure, otherwise ASSET_SUCCESS
+ */
+int loadMesh(Scene &scene, int mesh_index);
+
+/**
+ * Load every mesh from the glTF file, as well as materials, textures and other
+ * associated objects.
+ *
+ * @param path	must be the path to a glTF- or glb-file.
  * @param scene is a reference to a Scene struct that will be filled with the
  * 	content of the glTF file being loaded.
- * */
-int loadScene(const std::string &path, Scene &scene);
-
-struct TextureData {
-    int width;
-    int height;
-    int componentCount;
-    std::vector<char*> data;
-};
-TextureData loadTexture(const std::filesystem::path& path);
+ * @return ASSET_ERROR on failure, otherwise ASSET_SUCCESS
+ */
+int loadScene(const std::filesystem::path &path, Scene &scene);
+
+/**
+ * Simply loads a single image at the given path and returns a Texture
+ * struct describing it. This is for special use cases only (eg.
+ * loading a font atlas) and not meant to be used for regular assets.
+ * The sampler is set to -1, signalling that this Texture was loaded
+ * outside the context of a glTF-file.
+ * If there was an error loading or decoding the image, the returned struct
+ * will be cleared to all 0 with path and data being empty; make sure to always
+ * check that !data.empty() before using the struct.
+ *
+ * @param path	must be the path to an image file.
+ * @return	Texture struct describing the loaded image.
+ */
+Texture loadTexture(const std::filesystem::path& path);
 
-}
+}	// end namespace vkcv::asset
diff --git a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
index e3d3072543bd33e1f5a67ae7dac61d229005947a..571d965a400de7197b6fb46f163c4099a5b353f1 100644
--- a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
+++ b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
@@ -1,387 +1,784 @@
 #include "vkcv/asset/asset_loader.hpp"
 #include <iostream>
 #include <string.h>	// memcpy(3)
+#include <set>
 #include <stdlib.h>	// calloc(3)
+#include <vulkan/vulkan.hpp>
 #include <fx/gltf.h>
 #include <stb_image.h>
 #include <vkcv/Logger.hpp>
 #include <algorithm>
 
+
 namespace vkcv::asset {
 
-/**
-* convert the accessor type from the fx-gltf library to an unsigned int
-* @param type
-* @return unsigned integer representation
-*/
-// TODO Return proper error code (we need to define those as macros or enums,
-// will discuss during the next core meeting if that should happen on the scope
-// of the vkcv framework or just this module)
-uint8_t convertTypeToInt(const fx::gltf::Accessor::Type type) {
-	switch (type) {
-	case fx::gltf::Accessor::Type::None :
-		return 0;
-	case fx::gltf::Accessor::Type::Scalar :
-		return 1;
-	case fx::gltf::Accessor::Type::Vec2 :
-		return 2;
-	case fx::gltf::Accessor::Type::Vec3 :
-		return 3;
-	case fx::gltf::Accessor::Type::Vec4 :
-		return 4;
-	default: return 10; // TODO add cases for matrices (or maybe change the type in the struct itself)
+	/**
+	 * This function unrolls nested exceptions via recursion and prints them
+	 * @param e	The exception being thrown
+	 * @param path	The path to the file that was responsible for the exception
+	 */
+	static void recurseExceptionPrint(const std::exception& e, const std::string &path) {
+		vkcv_log(LogLevel::ERROR, "Loading file %s: %s", path.c_str(), e.what());
+		
+		try {
+			std::rethrow_if_nested(e);
+		} catch (const std::exception& nested) {
+			recurseExceptionPrint(nested, path);
+		}
 	}
-}
 
-/**
- * This function unrolls nested exceptions via recursion and prints them
- * @param e error code
- * @param path path to file that is responsible for error
- */
-void print_what (const std::exception& e, const std::string &path) {
-	vkcv_log(LogLevel::ERROR, "Loading file %s: %s",
-			 path.c_str(), e.what());
+	/**
+	 * Returns the component count for an accessor type of the fx-gltf library.
+	 * @param type	The accessor type
+	 * @return	An unsigned integer count
+	 */
+	static uint32_t getComponentCount(const fx::gltf::Accessor::Type type) {
+		switch (type) {
+		case fx::gltf::Accessor::Type::Scalar:
+			return 1;
+		case fx::gltf::Accessor::Type::Vec2:
+			return 2;
+		case fx::gltf::Accessor::Type::Vec3:
+			return 3;
+		case fx::gltf::Accessor::Type::Vec4:
+		case fx::gltf::Accessor::Type::Mat2:
+			return 4;
+		case fx::gltf::Accessor::Type::Mat3:
+			return 9;
+		case fx::gltf::Accessor::Type::Mat4:
+			return 16;
+		case fx::gltf::Accessor::Type::None:
+		default:
+			return 0;
+		}
+	}
 	
-	try {
-		std::rethrow_if_nested(e);
-	} catch (const std::exception& nested) {
-		print_what(nested, path);
+	static uint32_t getComponentSize(ComponentType type) {
+		switch (type) {
+			case ComponentType::INT8:
+			case ComponentType::UINT8:
+				return 1;
+			case ComponentType::INT16:
+			case ComponentType::UINT16:
+				return 2;
+			case ComponentType::UINT32:
+			case ComponentType::FLOAT32:
+				return 4;
+			default:
+				return 0;
+		}
 	}
-}
 
-/** Translate the component type used in the index accessor of fx-gltf to our
- * enum for index type. The reason we have defined an incompatible enum that
- * needs translation is that only a subset of component types is valid for
- * indices and we want to catch these incompatibilities here. */
-enum IndexType getIndexType(const enum fx::gltf::Accessor::ComponentType &t)
-{
-	switch (t) {
-	case fx::gltf::Accessor::ComponentType::UnsignedByte:
-		return IndexType::UINT8;
-	case fx::gltf::Accessor::ComponentType::UnsignedShort:
-		return IndexType::UINT16;
-	case fx::gltf::Accessor::ComponentType::UnsignedInt:
-		return IndexType::UINT32;
-	default:
-		vkcv_log(LogLevel::ERROR, "Index type not supported: %u", static_cast<uint16_t>(t));
-		return IndexType::UNDEFINED;
+	/**
+	 * Translate the component type used in the index accessor of fx-gltf to our
+	 * enum for index type. The reason we have defined an incompatible enum that
+	 * needs translation is that only a subset of component types is valid for
+	 * indices and we want to catch these incompatibilities here.
+	 * @param t	The component type
+	 * @return 	The vkcv::IndexType enum representation
+	 */
+	enum IndexType getIndexType(const enum fx::gltf::Accessor::ComponentType &type) {
+		switch (type) {
+		case fx::gltf::Accessor::ComponentType::UnsignedByte:
+			return IndexType::UINT8;
+		case fx::gltf::Accessor::ComponentType::UnsignedShort:
+			return IndexType::UINT16;
+		case fx::gltf::Accessor::ComponentType::UnsignedInt:
+			return IndexType::UINT32;
+		default:
+			vkcv_log(LogLevel::ERROR, "Index type not supported: %u", static_cast<uint16_t>(type));
+			return IndexType::UNDEFINED;
+		}
 	}
-}
-
-/**
- * This function computes the modelMatrix out of the data given in the gltf file. It also checks, whether a modelMatrix was given.
- * @param translation possible translation vector (default 0,0,0)
- * @param scale possible scale vector (default 1,1,1)
- * @param rotation possible rotation, given in quaternion (default 0,0,0,1)
- * @param matrix possible modelmatrix (default identity)
- * @return model Matrix as an array of floats
- */
-std::array<float, 16> computeModelMatrix(std::array<float, 3> translation, std::array<float, 3> scale, std::array<float, 4> rotation, std::array<float, 16> matrix){
-    std::array<float, 16> modelMatrix = {1,0,0,0,
-                                         0,1,0,0,
-                                         0,0,1,0,
-                                         0,0,0,1};
-    if (matrix != modelMatrix){
-        return matrix;
-    } else {
-        // translation
-        modelMatrix[3] = translation[0];
-        modelMatrix[7] = translation[1];
-        modelMatrix[11] = translation[2];
-        // rotation and scale
-        auto a = rotation[0];
-        auto q1 = rotation[1];
-        auto q2 = rotation[2];
-        auto q3 = rotation[3];
-
-        modelMatrix[0] = (2 * (a * a + q1 * q1) - 1) * scale[0];
-        modelMatrix[1] = (2 * (q1 * q2 - a * q3)) * scale[1];
-        modelMatrix[2] = (2 * (q1 * q3 + a * q2)) * scale[2];
-
-        modelMatrix[4] = (2 * (q1 * q2 + a * q3)) * scale[0];
-        modelMatrix[5] = (2 * (a * a + q2 * q2) - 1) * scale[1];
-        modelMatrix[6] = (2 * (q2 * q3 - a * q1)) * scale[2];
-
-        modelMatrix[8] = (2 * (q1 * q3 - a * q2)) * scale[0];
-        modelMatrix[9] = (2 * (q2 * q3 + a * q1)) * scale[1];
-        modelMatrix[10] = (2 * (a * a + q3 * q3) - 1) * scale[2];
-
-        // flip y, because GLTF uses y up, but vulkan -y up
-        modelMatrix[5] *= -1;
-
-        return modelMatrix;
-    }
-
-}
-
-bool materialHasTexture(const Material *const m, const PBRTextureTarget t)
-{
-	return m->textureMask & bitflag(t);
-}
-
-int loadScene(const std::string &path, Scene &scene){
-    fx::gltf::Document sceneObjects;
-
-    try {
-        if (path.rfind(".glb", (path.length()-4)) != std::string::npos) {
-            sceneObjects = fx::gltf::LoadFromBinary(path);
-        } else {
-            sceneObjects = fx::gltf::LoadFromText(path);
-        }
-    } catch (const std::system_error &err) {
-        print_what(err, path);
-        return 0;
-    } catch (const std::exception &e) {
-        print_what(e, path);
-        return 0;
-    }
-    size_t pos = path.find_last_of("/");
-    auto dir = path.substr(0, pos);
-
-    // file has to contain at least one mesh
-    if (sceneObjects.meshes.size() == 0) return 0;
-
-    fx::gltf::Accessor posAccessor;
-    std::vector<VertexAttribute> vertexAttributes;
-    std::vector<Material> materials;
-    std::vector<Texture> textures;
-    std::vector<Sampler> samplers;
-    std::vector<Mesh> meshes;
-    std::vector<VertexGroup> vertexGroups;
-    int groupCount = 0;
-
-    Mesh mesh = {};
-
-    for(int i = 0; i < sceneObjects.meshes.size(); i++){
-        std::vector<int> vertexGroupsIndices;
-        fx::gltf::Mesh const &objectMesh = sceneObjects.meshes[i];
-
-        for(int j = 0; j < objectMesh.primitives.size(); j++){
-            fx::gltf::Primitive const &objectPrimitive = objectMesh.primitives[j];
-            vertexAttributes.clear();
-            vertexAttributes.reserve(objectPrimitive.attributes.size());
-
-            for (auto const & attrib : objectPrimitive.attributes) {
-
-                fx::gltf::Accessor accessor =  sceneObjects.accessors[attrib.second];
-                VertexAttribute attribute;
-
-                if (attrib.first == "POSITION") {
-                    attribute.type = PrimitiveType::POSITION;
-                    posAccessor = accessor;
-                } else if (attrib.first == "NORMAL") {
-                    attribute.type = PrimitiveType::NORMAL;
-                } else if (attrib.first == "TEXCOORD_0") {
-                    attribute.type = PrimitiveType::TEXCOORD_0;
-                }
-                else if (attrib.first == "TEXCOORD_1") {
-                    attribute.type = PrimitiveType::TEXCOORD_1;
-                } else if (attrib.first == "TANGENT") {
-                    attribute.type = PrimitiveType::TANGENT;
-                } else {
-                    return 0;
-                }
-
-                attribute.offset = sceneObjects.bufferViews[accessor.bufferView].byteOffset;
-                attribute.length = sceneObjects.bufferViews[accessor.bufferView].byteLength;
-                attribute.stride = sceneObjects.bufferViews[accessor.bufferView].byteStride;
-		        attribute.componentType = static_cast<ComponentType>(accessor.componentType);
-
-                if (convertTypeToInt(accessor.type) != 10) {
-                    attribute.componentCount = convertTypeToInt(accessor.type);
-                } else {
-                    return 0;
-                }
-
-                vertexAttributes.push_back(attribute);
-            }
-
-            IndexType indexType;
-            std::vector<uint8_t> indexBufferData = {};
-            if (objectPrimitive.indices >= 0){ // if there is no index buffer, -1 is returned from fx-gltf
-                const fx::gltf::Accessor &indexAccessor = sceneObjects.accessors[objectPrimitive.indices];
-                const fx::gltf::BufferView &indexBufferView = sceneObjects.bufferViews[indexAccessor.bufferView];
-                const fx::gltf::Buffer &indexBuffer = sceneObjects.buffers[indexBufferView.buffer];
-
-                indexBufferData.resize(indexBufferView.byteLength);
-                {
-                    const size_t off = indexBufferView.byteOffset;
-                    const void *const ptr = ((char*)indexBuffer.data.data()) + off;
-                    if (!memcpy(indexBufferData.data(), ptr, indexBufferView.byteLength)) {
-                        vkcv_log(LogLevel::ERROR, "Copying index buffer data");
-                        return 0;
-                    }
-                }
-
-                indexType = getIndexType(indexAccessor.componentType);
-                if (indexType == IndexType::UNDEFINED){
-                    vkcv_log(LogLevel::ERROR, "Index Type undefined.");
-                    return 0;
-                }
-            }
-
-            const fx::gltf::BufferView&	vertexBufferView	= sceneObjects.bufferViews[posAccessor.bufferView];
-            const fx::gltf::Buffer&		vertexBuffer		= sceneObjects.buffers[vertexBufferView.buffer];
-
-            // only copy relevant part of vertex data
-            uint32_t relevantBufferOffset = std::numeric_limits<uint32_t>::max();
-            uint32_t relevantBufferEnd = 0;
-            for (const auto &attribute : vertexAttributes) {
-                relevantBufferOffset = std::min(attribute.offset, relevantBufferOffset);
-                const uint32_t attributeEnd = attribute.offset + attribute.length;
-                relevantBufferEnd = std::max(relevantBufferEnd, attributeEnd);    // TODO: need to incorporate stride?
-            }
-            const uint32_t relevantBufferSize = relevantBufferEnd - relevantBufferOffset;
-
-            // FIXME: This only works when all vertex attributes are in one buffer
-            std::vector<uint8_t> vertexBufferData;
-            vertexBufferData.resize(relevantBufferSize);
-            {
-                const void *const ptr = ((char*)vertexBuffer.data.data()) + relevantBufferOffset;
-                if (!memcpy(vertexBufferData.data(), ptr, relevantBufferSize)) {
-                    vkcv_log(LogLevel::ERROR, "Copying vertex buffer data");
-                    return 0;
-                }
-            }
-
-            // make vertex attributes relative to copied section
-            for (auto &attribute : vertexAttributes) {
-                attribute.offset -= relevantBufferOffset;
-            }
-
-            const size_t numVertexGroups = objectMesh.primitives.size();
-            vertexGroups.reserve(numVertexGroups);
-
-            vertexGroups.push_back({
-                static_cast<PrimitiveMode>(objectPrimitive.mode),
-                sceneObjects.accessors[objectPrimitive.indices].count,
-                posAccessor.count,
-                {indexType, indexBufferData},
-                {vertexBufferData, vertexAttributes},
-                {posAccessor.min[0], posAccessor.min[1], posAccessor.min[2]},
-                {posAccessor.max[0], posAccessor.max[1], posAccessor.max[2]},
-                static_cast<uint8_t>(objectPrimitive.material)
-            });
-
-            vertexGroupsIndices.push_back(groupCount);
-            groupCount++;
-        }
-
-        mesh.name = sceneObjects.meshes[i].name;
-        mesh.vertexGroups = vertexGroupsIndices;
 
-        meshes.push_back(mesh);
-    }
-
-    for(int m = 0; m < sceneObjects.nodes.size(); m++) {
-        meshes[sceneObjects.nodes[m].mesh].modelMatrix = computeModelMatrix(sceneObjects.nodes[m].translation,
-                                                                            sceneObjects.nodes[m].scale,
-                                                                            sceneObjects.nodes[m].rotation,
-                                                                            sceneObjects.nodes[m].matrix);
-    }
-
-    if (sceneObjects.textures.size() > 0){
-        textures.reserve(sceneObjects.textures.size());
-
-        for(int k = 0; k < sceneObjects.textures.size(); k++){
-            const fx::gltf::Texture &tex = sceneObjects.textures[k];
-            const fx::gltf::Image &img = sceneObjects.images[tex.source];
-            std::string img_uri = dir + "/" + img.uri;
-            int w, h, c;
-            uint8_t *data = stbi_load(img_uri.c_str(), &w, &h, &c, 4);
-            c = 4;	// FIXME hardcoded to always have RGBA channel layout
-            if (!data) {
-                vkcv_log(LogLevel::ERROR, "Loading texture image data.")
-                return 0;
-            }
-            const size_t byteLen = w * h * c;
-
-            std::vector<uint8_t> imgdata;
-            imgdata.resize(byteLen);
-            if (!memcpy(imgdata.data(), data, byteLen)) {
-                vkcv_log(LogLevel::ERROR, "Copying texture image data")
-                free(data);
-                return 0;
-            }
-            free(data);
+	/**
+	 * This function fills the array of vertex attributes of a VertexGroup (usually
+	 * part of a vkcv::asset::Mesh) object based on the description of attributes
+	 * for a fx::gltf::Primitive.
+	 *
+	 * @param src	The description of attribute objects from the fx-gltf library
+	 * @param gltf	The main glTF document
+	 * @param dst	The array of vertex attributes stored in an asset::Mesh object
+	 * @return	ASSET_ERROR when at least one VertexAttribute could not be
+	 * 		constructed properly, otherwise ASSET_SUCCESS
+	 */
+	static int loadVertexAttributes(const fx::gltf::Attributes &src,
+									const std::vector<fx::gltf::Accessor> &accessors,
+									const std::vector<fx::gltf::BufferView> &bufferViews,
+									std::vector<VertexAttribute> &dst) {
+		for (const auto &attrib : src) {
+			VertexAttribute att;
+			
+			if (attrib.first == "POSITION") {
+				att.type = PrimitiveType::POSITION;
+			} else if (attrib.first == "NORMAL") {
+				att.type = PrimitiveType::NORMAL;
+			} else if (attrib.first == "TANGENT") {
+				att.type = PrimitiveType::TANGENT;
+			} else if (attrib.first == "TEXCOORD_0") {
+				att.type = PrimitiveType::TEXCOORD_0;
+			} else if (attrib.first == "TEXCOORD_1") {
+				att.type = PrimitiveType::TEXCOORD_1;
+			} else if (attrib.first == "COLOR_0") {
+				att.type = PrimitiveType::COLOR_0;
+			} else if (attrib.first == "COLOR_1") {
+				att.type = PrimitiveType::COLOR_1;
+			} else if (attrib.first == "JOINTS_0") {
+				att.type = PrimitiveType::JOINTS_0;
+			} else if (attrib.first == "WEIGHTS_0") {
+				att.type = PrimitiveType::WEIGHTS_0;
+			} else {
+				att.type = PrimitiveType::UNDEFINED;
+			}
+			
+			if (att.type != PrimitiveType::UNDEFINED) {
+				const fx::gltf::Accessor &accessor = accessors[attrib.second];
+				const fx::gltf::BufferView &buf = bufferViews[accessor.bufferView];
+				
+				att.offset = buf.byteOffset;
+				att.length = buf.byteLength;
+				att.stride = buf.byteStride;
+				att.componentType = static_cast<ComponentType>(accessor.componentType);
+				att.componentCount = getComponentCount(accessor.type);
+				
+				/* Assume tightly packed stride as not explicitly provided */
+				if (att.stride == 0) {
+					att.stride = att.componentCount * getComponentSize(att.componentType);
+				}
+			}
+			
+			if ((att.type == PrimitiveType::UNDEFINED) ||
+				(att.componentCount == 0)) {
+				return ASSET_ERROR;
+			}
+			
+			dst.push_back(att);
+		}
+		
+		return ASSET_SUCCESS;
+	}
 
-            textures.push_back({
-                0,
-                static_cast<uint8_t>(c),
-                static_cast<uint16_t>(w),
-                static_cast<uint16_t>(h),
-                imgdata
-            });
+	/**
+	 * This function calculates the modelMatrix out of the data given in the gltf file.
+	 * It also checks, whether a modelMatrix was given.
+	 *
+	 * @param translation possible translation vector (default 0,0,0)
+	 * @param scale possible scale vector (default 1,1,1)
+	 * @param rotation possible rotation, given in quaternion (default 0,0,0,1)
+	 * @param matrix possible modelmatrix (default identity)
+	 * @return model Matrix as an array of floats
+	 */
+	static std::array<float, 16> calculateModelMatrix(const std::array<float, 3>& translation,
+													  const std::array<float, 3>& scale,
+													  const std::array<float, 4>& rotation,
+													  const std::array<float, 16>& matrix){
+		std::array<float, 16> modelMatrix = {
+				1,0,0,0,
+				0,1,0,0,
+				0,0,1,0,
+				0,0,0,1
+		};
+		
+		if (matrix != modelMatrix){
+			return matrix;
+		} else {
+			// translation
+			modelMatrix[3] = translation[0];
+			modelMatrix[7] = translation[1];
+			modelMatrix[11] = translation[2];
+			
+			// rotation and scale
+			auto a = rotation[0];
+			auto q1 = rotation[1];
+			auto q2 = rotation[2];
+			auto q3 = rotation[3];
+	
+			modelMatrix[0] = (2 * (a * a + q1 * q1) - 1) * scale[0];
+			modelMatrix[1] = (2 * (q1 * q2 - a * q3)) * scale[1];
+			modelMatrix[2] = (2 * (q1 * q3 + a * q2)) * scale[2];
+	
+			modelMatrix[4] = (2 * (q1 * q2 + a * q3)) * scale[0];
+			modelMatrix[5] = (2 * (a * a + q2 * q2) - 1) * scale[1];
+			modelMatrix[6] = (2 * (q2 * q3 - a * q1)) * scale[2];
+	
+			modelMatrix[8] = (2 * (q1 * q3 - a * q2)) * scale[0];
+			modelMatrix[9] = (2 * (q2 * q3 + a * q1)) * scale[1];
+			modelMatrix[10] = (2 * (a * a + q3 * q3) - 1) * scale[2];
+	
+			// flip y, because GLTF uses y up, but vulkan -y up
+			modelMatrix[5] *= -1;
+	
+			return modelMatrix;
+		}
+	}
 
-        }
-    }
+	bool Material::hasTexture(const PBRTextureTarget target) const {
+		return textureMask & bitflag(target);
+	}
 
-    if (sceneObjects.materials.size() > 0){
-        materials.reserve(sceneObjects.materials.size());
+	/**
+	 * This function translates a given fx-gltf-sampler-wrapping-mode-enum to its vulkan sampler-adress-mode counterpart.
+	 * @param mode: wrapping mode of a sampler given as fx-gltf-enum
+	 * @return int vulkan-enum representing the same wrapping mode
+	 */
+	static int translateSamplerMode(const fx::gltf::Sampler::WrappingMode mode) {
+		switch (mode) {
+		case fx::gltf::Sampler::WrappingMode::ClampToEdge:
+			return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+		case fx::gltf::Sampler::WrappingMode::MirroredRepeat:
+			return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
+		case fx::gltf::Sampler::WrappingMode::Repeat:
+		default:
+			return VK_SAMPLER_ADDRESS_MODE_REPEAT;
+		}
+	}
 
-        for (int l = 0; l < sceneObjects.materials.size(); l++){
-            fx::gltf::Material material = sceneObjects.materials[l];
-	    // TODO I think we shouldn't set the index for a texture target if
-	    // it isn't defined. So we need to test first if there is a normal
-	    // texture before assigning material.normalTexture.index.
-	    // About the bitmask: If a normal texture is there, modify the
-	    // materials textureMask like this:
-	    // 		mat.textureMask |= bitflag(asset::normal);
-            materials.push_back({
-               0,
-               material.pbrMetallicRoughness.baseColorTexture.index,
-               material.pbrMetallicRoughness.metallicRoughnessTexture.index,
-               material.normalTexture.index,
-               material.occlusionTexture.index,
-               material.emissiveTexture.index,
-               {
-                   material.pbrMetallicRoughness.baseColorFactor[0],
-                   material.pbrMetallicRoughness.baseColorFactor[1],
-                   material.pbrMetallicRoughness.baseColorFactor[2],
-                   material.pbrMetallicRoughness.baseColorFactor[3]
-               },
-               material.pbrMetallicRoughness.metallicFactor,
-               material.pbrMetallicRoughness.roughnessFactor,
-               material.normalTexture.scale,
-               material.occlusionTexture.strength,
-               {
-                   material.emissiveFactor[0],
-                   material.emissiveFactor[1],
-                   material.emissiveFactor[2]
-               }
+	/**
+	 * If the glTF doesn't define samplers, we use the defaults defined by fx-gltf.
+	 * The following are details about the glTF/OpenGL to Vulkan translation.
+	 * magFilter (VkFilter?):
+	 * 	GL_NEAREST -> VK_FILTER_NEAREST
+	 * 	GL_LINEAR -> VK_FILTER_LINEAR
+	 * minFilter (VkFilter?):
+	 * mipmapMode (VkSamplerMipmapMode?):
+	 * Vulkans minFilter and mipmapMode combined correspond to OpenGLs
+	 * GL_minFilter_MIPMAP_mipmapMode:
+	 * 	GL_NEAREST_MIPMAP_NEAREST:
+	 * 		minFilter=VK_FILTER_NEAREST
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 	GL_LINEAR_MIPMAP_NEAREST:
+	 * 		minFilter=VK_FILTER_LINEAR
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 	GL_NEAREST_MIPMAP_LINEAR:
+	 * 		minFilter=VK_FILTER_NEAREST
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR
+	 * 	GL_LINEAR_MIPMAP_LINEAR:
+	 * 		minFilter=VK_FILTER_LINEAR
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR
+	 * The modes of GL_LINEAR and GL_NEAREST have to be emulated using
+	 * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST with specific minLOD and maxLOD:
+	 * 	GL_LINEAR:
+	 * 		minFilter=VK_FILTER_LINEAR
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 		minLOD=0, maxLOD=0.25
+	 * 	GL_NEAREST:
+	 * 		minFilter=VK_FILTER_NEAREST
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 		minLOD=0, maxLOD=0.25
+	 * Setting maxLOD=0 causes magnification to always be performed (using
+	 * the defined magFilter), this may be valid if the min- and magFilter
+	 * are equal, otherwise it won't be the expected behaviour from OpenGL
+	 * and glTF; instead using maxLod=0.25 allows the minFilter to be
+	 * performed while still always rounding to the base level.
+	 * With other modes, minLOD and maxLOD default to:
+	 * 	minLOD=0
+	 * 	maxLOD=VK_LOD_CLAMP_NONE
+	 * wrapping:
+	 * gltf has wrapS, wrapT with {clampToEdge, MirroredRepeat, Repeat} while
+	 * Vulkan has addressModeU, addressModeV, addressModeW with values
+	 * VK_SAMPLER_ADDRESS_MODE_{REPEAT,MIRRORED_REPEAT,CLAMP_TO_EDGE,
+	 * 			    CAMP_TO_BORDER,MIRROR_CLAMP_TO_EDGE}
+	 * Translation from glTF to Vulkan is straight forward for the 3 existing
+	 * modes, default is repeat, the other modes aren't available.
+	 */
+	static vkcv::asset::Sampler loadSampler(const fx::gltf::Sampler &src) {
+		Sampler dst;
+		
+		dst.minLOD = 0;
+		dst.maxLOD = VK_LOD_CLAMP_NONE;
+	
+		switch (src.minFilter) {
+		case fx::gltf::Sampler::MinFilter::None:
+		case fx::gltf::Sampler::MinFilter::Nearest:
+			dst.minFilter = VK_FILTER_NEAREST;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			dst.maxLOD = 0.25;
+			break;
+		case fx::gltf::Sampler::MinFilter::Linear:
+			dst.minFilter = VK_FILTER_LINEAR;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			dst.maxLOD = 0.25;
+			break;
+		case fx::gltf::Sampler::MinFilter::NearestMipMapNearest:
+			dst.minFilter = VK_FILTER_NEAREST;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			break;
+		case fx::gltf::Sampler::MinFilter::LinearMipMapNearest:
+			dst.minFilter = VK_FILTER_LINEAR;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			break;
+		case fx::gltf::Sampler::MinFilter::NearestMipMapLinear:
+			dst.minFilter = VK_FILTER_NEAREST;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+			break;
+		case fx::gltf::Sampler::MinFilter::LinearMipMapLinear:
+			dst.minFilter = VK_FILTER_LINEAR;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+			break;
+		default:
+			break;
+		}
+	
+		switch (src.magFilter) {
+		case fx::gltf::Sampler::MagFilter::None:
+		case fx::gltf::Sampler::MagFilter::Nearest:
+			dst.magFilter = VK_FILTER_NEAREST;
+			break;
+		case fx::gltf::Sampler::MagFilter::Linear:
+			dst.magFilter = VK_FILTER_LINEAR;
+			break;
+		default:
+			break;
+		}
+	
+		dst.addressModeU = translateSamplerMode(src.wrapS);
+		dst.addressModeV = translateSamplerMode(src.wrapT);
+		
+		// There is no information about wrapping for a third axis in glTF and
+		// we have to hardcode this value.
+		dst.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+		
+		return dst;
+	}
 
-            });
-        }
-    }
+	/**
+	 * Initializes vertex groups of a Mesh, including copying the data to
+	 * index- and vertex-buffers.
+	 */
+	static int loadVertexGroups(const fx::gltf::Mesh &objectMesh,
+								const fx::gltf::Document &sceneObjects,
+								Scene &scene, Mesh &mesh) {
+		mesh.vertexGroups.reserve(objectMesh.primitives.size());
+	
+		for (const auto &objectPrimitive : objectMesh.primitives) {
+			VertexGroup vertexGroup;
+			
+			vertexGroup.vertexBuffer.attributes.reserve(
+					objectPrimitive.attributes.size()
+			);
+	
+			if (ASSET_SUCCESS != loadVertexAttributes(
+					objectPrimitive.attributes,
+					sceneObjects.accessors,
+					sceneObjects.bufferViews,
+					vertexGroup.vertexBuffer.attributes)) {
+				vkcv_log(LogLevel::ERROR, "Failed to get vertex attributes of '%s'",
+						 mesh.name.c_str());
+				return ASSET_ERROR;
+			}
+			
+			// The accessor for the position attribute is used for
+			// 1) getting the vertex buffer view which is only needed to get
+			//    the vertex buffer
+			// 2) getting the vertex count for the VertexGroup
+			// 3) getting the min/max of the bounding box for the VertexGroup
+			fx::gltf::Accessor posAccessor;
+			bool noPosition = true;
+			
+			for (auto const& attrib : objectPrimitive.attributes) {
+				if (attrib.first == "POSITION") {
+					posAccessor = sceneObjects.accessors[attrib.second];
+					noPosition = false;
+					break;
+				}
+			}
+			
+			if (noPosition) {
+				vkcv_log(LogLevel::ERROR, "Position attribute not found from '%s'",
+						 mesh.name.c_str());
+				return ASSET_ERROR;
+			}
+	
+			const fx::gltf::Accessor& indexAccessor = sceneObjects.accessors[objectPrimitive.indices];
+			
+			int indexBufferURI;
+			if (objectPrimitive.indices >= 0) { // if there is no index buffer, -1 is returned from fx-gltf
+				const fx::gltf::BufferView& indexBufferView = sceneObjects.bufferViews[indexAccessor.bufferView];
+				const fx::gltf::Buffer& indexBuffer = sceneObjects.buffers[indexBufferView.buffer];
+				
+				// Because the buffers are already preloaded into the memory by the gltf-library,
+				// it makes no sense to load them later on manually again into memory.
+				vertexGroup.indexBuffer.data.resize(indexBufferView.byteLength);
+				memcpy(vertexGroup.indexBuffer.data.data(),
+					   indexBuffer.data.data() + indexBufferView.byteOffset,
+					   indexBufferView.byteLength);
+			} else {
+				indexBufferURI = -1;
+			}
+			
+			vertexGroup.indexBuffer.type = getIndexType(indexAccessor.componentType);
+			
+			if (IndexType::UNDEFINED == vertexGroup.indexBuffer.type) {
+				vkcv_log(LogLevel::ERROR, "Index Type undefined or not supported.");
+				return ASSET_ERROR;
+			}
+	
+			if (posAccessor.bufferView >= sceneObjects.bufferViews.size()) {
+				vkcv_log(LogLevel::ERROR, "Access to bufferView out of bounds: %lu",
+						posAccessor.bufferView);
+				return ASSET_ERROR;
+			}
+			const fx::gltf::BufferView& vertexBufferView = sceneObjects.bufferViews[posAccessor.bufferView];
+			if (vertexBufferView.buffer >= sceneObjects.buffers.size()) {
+				vkcv_log(LogLevel::ERROR, "Access to buffer out of bounds: %lu",
+						vertexBufferView.buffer);
+				return ASSET_ERROR;
+			}
+			const fx::gltf::Buffer& vertexBuffer = sceneObjects.buffers[vertexBufferView.buffer];
+			
+			// only copy relevant part of vertex data
+			uint32_t relevantBufferOffset = std::numeric_limits<uint32_t>::max();
+			uint32_t relevantBufferEnd = 0;
+			
+			for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
+				relevantBufferOffset = std::min(attribute.offset, relevantBufferOffset);
+				relevantBufferEnd = std::max(relevantBufferEnd, attribute.offset + attribute.length);
+			}
+			
+			const uint32_t relevantBufferSize = relevantBufferEnd - relevantBufferOffset;
+			
+			vertexGroup.vertexBuffer.data.resize(relevantBufferSize);
+			memcpy(vertexGroup.vertexBuffer.data.data(),
+				   vertexBuffer.data.data() + relevantBufferOffset,
+				   relevantBufferSize);
+			
+			// make vertex attributes relative to copied section
+			for (auto& attribute : vertexGroup.vertexBuffer.attributes) {
+				attribute.offset -= relevantBufferOffset;
+			}
+			
+			vertexGroup.mode = static_cast<PrimitiveMode>(objectPrimitive.mode);
+			vertexGroup.numIndices = sceneObjects.accessors[objectPrimitive.indices].count;
+			vertexGroup.numVertices = posAccessor.count;
+			
+			memcpy(&(vertexGroup.min), posAccessor.min.data(), sizeof(vertexGroup.min));
+			memcpy(&(vertexGroup.max), posAccessor.max.data(), sizeof(vertexGroup.max));
+			
+			vertexGroup.materialIndex = static_cast<uint8_t>(objectPrimitive.material);
+			
+			mesh.vertexGroups.push_back(static_cast<int>(scene.vertexGroups.size()));
+			scene.vertexGroups.push_back(vertexGroup);
+		}
+		
+		return ASSET_SUCCESS;
+	}
 
-    scene = {
-            meshes,
-            vertexGroups,
-            materials,
-            textures,
-            samplers
-    };
+	/**
+	 * Returns an integer with specific bits set corresponding to the
+	 * textures that appear in the given material. This mask is used in the
+	 * vkcv::asset::Material struct and can be tested via the hasTexture
+	 * method.
+	 */
+	static uint16_t generateTextureMask(fx::gltf::Material &material) {
+		uint16_t textureMask = 0;
+		
+		if (material.pbrMetallicRoughness.baseColorTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::baseColor);
+		}
+		if (material.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::metalRough);
+		}
+		if (material.normalTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::normal);
+		}
+		if (material.occlusionTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::occlusion);
+		}
+		if (material.emissiveTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::emissive);
+		}
+		
+		return textureMask;
+	}
 
-    return 1;
-}
+	int probeScene(const std::filesystem::path& path, Scene& scene) {
+		fx::gltf::Document sceneObjects;
+	
+		try {
+			if (path.extension() == ".glb") {
+				sceneObjects = fx::gltf::LoadFromBinary(path.string());
+			} else {
+				sceneObjects = fx::gltf::LoadFromText(path.string());
+			}
+		} catch (const std::system_error& err) {
+			recurseExceptionPrint(err, path.string());
+			return ASSET_ERROR;
+		} catch (const std::exception& e) {
+			recurseExceptionPrint(e, path.string());
+			return ASSET_ERROR;
+		}
+		
+		const auto directory = path.parent_path();
+		
+		scene.meshes.clear();
+		scene.vertexGroups.clear();
+		scene.materials.clear();
+		scene.textures.clear();
+		scene.samplers.clear();
+	
+		// file has to contain at least one mesh
+		if (sceneObjects.meshes.empty()) {
+			vkcv_log(LogLevel::ERROR, "No meshes found! (%s)", path.c_str());
+			return ASSET_ERROR;
+		} else {
+			scene.meshes.reserve(sceneObjects.meshes.size());
+			
+			for (size_t i = 0; i < sceneObjects.meshes.size(); i++) {
+				Mesh mesh;
+				mesh.name = sceneObjects.meshes[i].name;
+				
+				if (loadVertexGroups(sceneObjects.meshes[i], sceneObjects, scene, mesh) != ASSET_SUCCESS) {
+					vkcv_log(LogLevel::ERROR, "Failed to load vertex groups of '%s'! (%s)",
+							 mesh.name.c_str(), path.c_str());
+					return ASSET_ERROR;
+				}
+				
+				scene.meshes.push_back(mesh);
+			}
+			
+			// This only works if the node has a mesh and it only loads the meshes and ignores cameras and lights
+			for (const auto& node : sceneObjects.nodes) {
+				if ((node.mesh >= 0) && (node.mesh < scene.meshes.size())) {
+					scene.meshes[node.mesh].modelMatrix = calculateModelMatrix(
+							node.translation,
+							node.scale,
+							node.rotation,
+							node.matrix
+					);
+				}
+			}
+		}
+		
+		if (sceneObjects.samplers.empty()) {
+			vkcv_log(LogLevel::WARNING, "No samplers found! (%s)", path.c_str());
+		} else {
+			scene.samplers.reserve(sceneObjects.samplers.size());
+			
+			for (const auto &samplerObject : sceneObjects.samplers) {
+				scene.samplers.push_back(loadSampler(samplerObject));
+			}
+		}
+		
+		if (sceneObjects.textures.empty()) {
+			vkcv_log(LogLevel::WARNING, "No textures found! (%s)", path.c_str());
+		} else {
+			scene.textures.reserve(sceneObjects.textures.size());
+			
+			for (const auto& textureObject : sceneObjects.textures) {
+				Texture texture;
+				
+				if (textureObject.sampler < 0) {
+					texture.sampler = -1;
+				} else
+				if (static_cast<size_t>(textureObject.sampler) >= scene.samplers.size()) {
+					vkcv_log(LogLevel::ERROR, "Sampler of texture '%s' missing (%s) %d",
+							 textureObject.name.c_str(), path.c_str());
+					return ASSET_ERROR;
+				} else {
+					texture.sampler = textureObject.sampler;
+				}
+				
+				if ((textureObject.source < 0) ||
+					(static_cast<size_t>(textureObject.source) >= sceneObjects.images.size())) {
+					vkcv_log(LogLevel::ERROR, "Failed to load texture '%s' (%s)",
+							 textureObject.name.c_str(), path.c_str());
+					return ASSET_ERROR;
+				}
+				
+				const auto& image = sceneObjects.images[textureObject.source];
+				
+				if (image.uri.empty()) {
+					const fx::gltf::BufferView bufferView = sceneObjects.bufferViews[image.bufferView];
+					
+					texture.path.clear();
+					texture.data.resize(bufferView.byteLength);
+					memcpy(texture.data.data(),
+						   sceneObjects.buffers[bufferView.buffer].data.data() + bufferView.byteOffset,
+						   bufferView.byteLength);
+				} else {
+					texture.path = directory / image.uri;
+				}
+				
+				scene.textures.push_back(texture);
+			}
+		}
+		
+		if (sceneObjects.materials.empty()) {
+			vkcv_log(LogLevel::WARNING, "No materials found! (%s)", path.c_str());
+		} else {
+			scene.materials.reserve(sceneObjects.materials.size());
+			
+			for (auto material : sceneObjects.materials) {
+				scene.materials.push_back({
+						generateTextureMask(material),
+						material.pbrMetallicRoughness.baseColorTexture.index,
+						material.pbrMetallicRoughness.metallicRoughnessTexture.index,
+						material.normalTexture.index,
+						material.occlusionTexture.index,
+						material.emissiveTexture.index,
+						{
+								material.pbrMetallicRoughness.baseColorFactor[0],
+								material.pbrMetallicRoughness.baseColorFactor[1],
+								material.pbrMetallicRoughness.baseColorFactor[2],
+								material.pbrMetallicRoughness.baseColorFactor[3]
+						},
+						material.pbrMetallicRoughness.metallicFactor,
+						material.pbrMetallicRoughness.roughnessFactor,
+						material.normalTexture.scale,
+						material.occlusionTexture.strength,
+						{
+								material.emissiveFactor[0],
+								material.emissiveFactor[1],
+								material.emissiveFactor[2]
+						}
+				});
+			}
+		}
+	
+		return ASSET_SUCCESS;
+	}
+	
+	/**
+	 * Loads and decodes the textures data based on the textures file path.
+	 * The path member is the only one that has to be initialized before
+	 * calling this function, the others (width, height, channels, data)
+	 * are set by this function and the sampler is of no concern here.
+	 */
+	static int loadTextureData(Texture& texture) {
+		if ((texture.width > 0) && (texture.height > 0) && (texture.channels > 0) &&
+			(!texture.data.empty())) {
+			return ASSET_SUCCESS; // Texture data was loaded already!
+		}
+		
+		uint8_t* data;
+		
+		if (texture.path.empty()) {
+			data = stbi_load_from_memory(
+					reinterpret_cast<uint8_t*>(texture.data.data()),
+					static_cast<int>(texture.data.size()),
+					&texture.width,
+					&texture.height,
+					&texture.channels, 4
+			);
+		} else {
+			data = stbi_load(
+					texture.path.string().c_str(),
+					&texture.width,
+					&texture.height,
+					&texture.channels, 4
+			);
+		}
+		
+		if (!data) {
+			vkcv_log(LogLevel::ERROR, "Texture could not be loaded from '%s'",
+					 texture.path.c_str());
+			
+			texture.width = 0;
+			texture.height = 0;
+			texture.channels = 0;
+			return ASSET_ERROR;
+		}
+		
+		texture.data.resize(texture.width * texture.height * 4);
+		memcpy(texture.data.data(), data, texture.data.size());
+		stbi_image_free(data);
+	
+		return ASSET_SUCCESS;
+	}
 
-TextureData loadTexture(const std::filesystem::path& path) {
-    TextureData texture;
-    
-    uint8_t* data = stbi_load(path.string().c_str(), &texture.width, &texture.height, &texture.componentCount, 4);
-    
-    if (!data) {
-		vkcv_log(LogLevel::ERROR, "Texture could not be loaded from '%s'", path.c_str());
-    	
-    	texture.width = 0;
-    	texture.height = 0;
-    	texture.componentCount = 0;
-    	return texture;
-    }
-    
-    texture.data.resize(texture.width * texture.height * 4);
-    memcpy(texture.data.data(), data, texture.data.size());
-    return texture;
-}
+	int loadMesh(Scene &scene, int index) {
+		if ((index < 0) || (static_cast<size_t>(index) >= scene.meshes.size())) {
+			vkcv_log(LogLevel::ERROR, "Mesh index out of range: %d", index);
+			return ASSET_ERROR;
+		}
+		
+		const Mesh &mesh = scene.meshes[index];
+		
+		for (const auto& vg : mesh.vertexGroups) {
+			const VertexGroup &vertexGroup = scene.vertexGroups[vg];
+			const Material& material = scene.materials[vertexGroup.materialIndex];
+			
+			if (material.hasTexture(PBRTextureTarget::baseColor)) {
+				const int result = loadTextureData(scene.textures[material.baseColor]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading baseColor texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::metalRough)) {
+				const int result = loadTextureData(scene.textures[material.metalRough]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading metalRough texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::normal)) {
+				const int result = loadTextureData(scene.textures[material.normal]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading normal texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::occlusion)) {
+				const int result = loadTextureData(scene.textures[material.occlusion]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading occlusion texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::emissive)) {
+				const int result = loadTextureData(scene.textures[material.emissive]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading emissive texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+		}
+	
+		return ASSET_SUCCESS;
+	}
+	
+	int loadScene(const std::filesystem::path &path, Scene &scene) {
+		int result = probeScene(path, scene);
+		
+		if (result != ASSET_SUCCESS) {
+			vkcv_log(LogLevel::ERROR, "Loading scene failed '%s'",
+					 path.c_str());
+			return result;
+		}
+		
+		for (size_t i = 0; i < scene.meshes.size(); i++) {
+			result = loadMesh(scene, static_cast<int>(i));
+			
+			if (result != ASSET_SUCCESS) {
+				vkcv_log(LogLevel::ERROR, "Loading mesh with index %d failed '%s'",
+						 static_cast<int>(i), path.c_str());
+				return result;
+			}
+		}
+		
+		return ASSET_SUCCESS;
+	}
+	
+	Texture loadTexture(const std::filesystem::path& path) {
+		Texture texture;
+		texture.path = path;
+		texture.sampler = -1;
+		if (loadTextureData(texture) != ASSET_SUCCESS) {
+			texture.path.clear();
+			texture.w = texture.h = texture.channels = 0;
+			texture.data.clear();
+		}
+		return texture;
+	}
 
 }
diff --git a/projects/mesh_shader/.gitignore b/projects/mesh_shader/.gitignore
index 7e24fd7b853bfb0a29d8b30879ef1cb95ad141c0..54601c357bf3fb97b914a6e657c042a5c6a985d7 100644
--- a/projects/mesh_shader/.gitignore
+++ b/projects/mesh_shader/.gitignore
@@ -1 +1 @@
-first_triangle
\ No newline at end of file
+mesh_shader
diff --git a/src/vkcv/BufferManager.cpp b/src/vkcv/BufferManager.cpp
index cfa233290b89702f196ed97c706254e002a0551b..1998198513b18d061446201f178ccd96cb7d5b6a 100644
--- a/src/vkcv/BufferManager.cpp
+++ b/src/vkcv/BufferManager.cpp
@@ -49,7 +49,7 @@ namespace vkcv {
 				usageFlags = vk::BufferUsageFlagBits::eIndexBuffer;
 				break;
 			default:
-				// TODO: maybe an issue
+				vkcv_log(LogLevel::WARNING, "Unknown buffer type");
 				break;
 		}
 		
@@ -81,6 +81,10 @@ namespace vkcv {
 				break;
 		}
 		
+		if (type == BufferType::STAGING) {
+			memoryUsage = vma::MemoryUsage::eCpuToGpu;
+		}
+		
 		auto bufferAllocation = allocator.createBuffer(
 				vk::BufferCreateInfo(createFlags, size, usageFlags),
 				vma::AllocationCreateInfo(