Implement mesh and material loading in glTF loader

This commit is contained in:
2022-10-17 21:28:17 +02:00
parent 18df6dae18
commit fbb6890f71
13 changed files with 361 additions and 105 deletions

View File

@@ -8,7 +8,7 @@ AlwaysBreakTemplateDeclarations: 'true'
BreakBeforeBraces: Mozilla
BinPackParameters: 'false'
BinPackArguments: 'false'
ColumnLimit: '120'
ColumnLimit: '100'
ConstructorInitializerAllOnOneLineOrOnePerLine: 'true'
IndentWidth: '4'
PointerAlignment: Right

View File

@@ -24,13 +24,13 @@ target_compile_definitions(fever_engine PRIVATE SPDLOG_FMT_EXTERNAL)
target_link_libraries(
fever_engine PUBLIC
glad
glm
glfw
EnTT::EnTT
fmt
pthread
spdlog
fx-gltf::fx-gltf
nlohmann_json::nlohmann_json
stb
sail
)

View File

@@ -13,35 +13,37 @@
#include <GLFW/glfw3.h>
#include <array>
#include <chrono>
#include <filesystem>
#include <fx/gltf.h>
#include <glad/gl.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>
#include <thread>
#include <utility>
struct MyRes
{
MyRes(std::filesystem::path const &path, int)
{
std::cout << "Path: " << std::filesystem::weakly_canonical(path) << std::endl;
};
int x = 0;
};
using namespace entt::literals;
Controller::Controller()
: m_gameWindow(std::make_shared<Window>()),
m_postProcessFrameBuffer(
m_gameWindow->dimensions().first, m_gameWindow->dimensions().second, postProcessingProgram)
m_postProcessFrameBuffer(m_gameWindow->dimensions().first,
m_gameWindow->dimensions().second,
postProcessingProgram)
{
gltf_loader<fx::gltf::Document>()("Lantern/glTF-Binary/Lantern.glb");
GltfLoader loader;
entt::resource_cache<Gltf, GltfLoader> gltf_resource_cache;
entt::resource<Gltf> gltf_document =
gltf_resource_cache
.load("Lantern/glTF-Binary/Lantern.glb"_hs, "Lantern/glTF-Binary/Lantern.glb")
.first->second;
fx::gltf::ReadQuotas read_quotas{.MaxFileSize = 512 * 1024 * 1024, .MaxBufferByteLength = 512 * 1024 * 1024};
fx::gltf::ReadQuotas read_quotas{.MaxFileSize = 512 * 1024 * 1024,
.MaxBufferByteLength = 512 * 1024 * 1024};
// auto gltf_path = std::filesystem::path("Lantern/glTF/Lantern.gltf");
auto gltf_path = std::filesystem::path("WaterBottle/glTF/WaterBottle.gltf");
// auto gltf_path = std::filesystem::path("WaterBottle/glTF/WaterBottle.gltf");
auto gltf_path = std::filesystem::path("ABeautifulGame.glb");
auto gltf = [&]() {
if (gltf_path.extension() == ".gltf") {
return fx::gltf::LoadFromText(gltf_path, read_quotas);
@@ -73,7 +75,7 @@ Controller::Controller()
std::vector<Model> models;
for (auto const &mesh : gltf.meshes) {
std::vector<Mesh> meshes;
std::vector<Mesh_> meshes;
for (auto const &primitive : mesh.primitives) {
auto const &material = gltf.materials.at(primitive.material);
auto baseColorTexture = material.pbrMetallicRoughness.baseColorTexture.index;
@@ -110,7 +112,7 @@ Controller::Controller()
primitive_textures.emplace_back(m_textures.at(normalTexture));
}
meshes.emplace_back(Mesh({primitive, gltf, locations}, primitive_textures));
meshes.emplace_back(Mesh_({primitive, gltf, locations}, primitive_textures));
}
models.emplace_back(Model(mesh.name, std::move(meshes)));
}
@@ -122,16 +124,18 @@ Controller::Controller()
continue;
}
ModelEntity entity(
Entity::Prototype(node.name, {}, {}, 1.0F), m_models[static_cast<unsigned>(node.mesh)], defaultProgram);
ModelEntity entity(Entity::Prototype(node.name, {}, {}, 1.0F),
m_models[static_cast<unsigned>(node.mesh)],
defaultProgram);
if (!node.translation.empty()) {
entity.setPosition(glm::vec3(node.translation[0], node.translation[1], node.translation[2]));
entity.setPosition(
glm::vec3(node.translation[0], node.translation[1], node.translation[2]));
}
if (!node.rotation.empty()) {
entity.setRotation(
glm::eulerAngles(glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2])));
entity.setRotation(glm::eulerAngles(
glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2])));
}
if (!node.scale.empty()) {
@@ -139,13 +143,17 @@ Controller::Controller()
}
entities.push_back(std::move(entity));
}
for (auto const &node : gltf.nodes) {
for (auto const &child : node.children) {
if (!node.translation.empty()) {
entities[child].translate(glm::vec3(node.translation[0], node.translation[1], node.translation[2]));
entities[child].translate(
glm::vec3(node.translation[0], node.translation[1], node.translation[2]));
}
}
}
m_entities = std::move(entities);
}
@@ -155,11 +163,13 @@ void Controller::run()
m_camera->translate(glm::vec3(0., .5, 2.));
DirectionalLight directional_light(
DirectionalLight::Prototype("", glm::vec3(-0.2, -1.0, -0.3), glm::vec3(1.0f), 5.f), &defaultProgram);
DirectionalLight::Prototype("", glm::vec3(-0.2, -1.0, -0.3), glm::vec3(1.0f), 5.f),
&defaultProgram);
directional_light.setActive(true);
PointLight point_light(PointLight::Prototype("", "", glm::vec3(4.0, 1.0, 6.0), glm::vec3(1.0F), 3.0F),
&defaultProgram);
PointLight point_light(
PointLight::Prototype("", "", glm::vec3(4.0, 1.0, 6.0), glm::vec3(1.0F), 3.0F),
&defaultProgram);
point_light.setActive(true);
Log::logger().info("Startup complete. Enter game loop.");
@@ -222,7 +232,10 @@ void Controller::limit_framerate()
double frameTime = 1 / (double)MAX_FPS;
if (frameTime > lastTime) {
Helper::sleep(static_cast<unsigned>(frameTime - lastTime) * 1000000);
static constexpr auto MICROSECONDS_PER_SECOND = 1'000'000;
auto sleep_time_us =
static_cast<unsigned>((frameTime - lastTime) * MICROSECONDS_PER_SECOND);
std::this_thread::sleep_for(std::chrono::microseconds(sleep_time_us));
}
m_deltaTime = glfwGetTime() - startingTime;

View File

@@ -3,26 +3,13 @@
#include <algorithm>
#ifdef __linux__
#include <unistd.h>
#endif
#ifdef _WIN32
#include <windows.h>
#endif
void Helper::sleep(uint32_t us)
{
#ifdef __linux__
usleep(us);
#endif
#ifdef _WIN32
// I don't know why I have to divide by 2000 and not 1000 but this way it works even on Windows
Sleep(us / 2000);
#endif
}
void Helper::gl_debug_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
const GLchar *message, const void *userParam)
void Helper::gl_debug_callback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar *message,
const void *userParam)
{
(void)length;
(void)userParam;
@@ -128,7 +115,11 @@ void Helper::gl_debug_callback(GLenum source, GLenum type, GLuint id, GLenum sev
"Type: {}\n"
"ID: {}\n"
"Severity: {}\n",
_message, _source, _type, id, _severity);
_message,
_source,
_type,
id,
_severity);
}
Helper::Timer::Timer(const std::string &name) : m_name(name)

View File

@@ -6,9 +6,12 @@
#include <string>
namespace Helper {
void sleep(uint32_t us);
void gl_debug_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message,
void gl_debug_callback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar *message,
const void *userParam);
class Timer

View File

@@ -5,12 +5,12 @@
#include "resources/Texture.h"
#include <utility>
Mesh::Mesh(VertexArray vertexArray, std::vector<std::reference_wrapper<const Texture>> textures)
Mesh_::Mesh_(VertexArray vertexArray, std::vector<std::reference_wrapper<const Texture>> textures)
: m_vertexArray(std::move(vertexArray)), m_textures(std::move(textures))
{
}
void Mesh::draw(ShaderProgram const &shaderProgram) const
void Mesh_::draw(ShaderProgram const &shaderProgram) const
{
// Bind all textures in order to its texture unit
std::size_t textureNum = 0;
@@ -31,7 +31,7 @@ void Mesh::draw(ShaderProgram const &shaderProgram) const
}
}
void Mesh::drawWithoutTextures() const
void Mesh_::drawWithoutTextures() const
{
m_vertexArray.bind();
@@ -41,7 +41,7 @@ void Mesh::drawWithoutTextures() const
VertexArray::unbind();
}
auto Mesh::getVertexArray() -> VertexArray *
auto Mesh_::getVertexArray() -> VertexArray *
{
return &m_vertexArray;
}

View File

@@ -7,10 +7,10 @@
class ShaderProgram;
class Mesh
class Mesh_
{
public:
Mesh(VertexArray vertexArray, std::vector<std::reference_wrapper<const Texture>> textures);
Mesh_(VertexArray vertexArray, std::vector<std::reference_wrapper<const Texture>> textures);
void draw(ShaderProgram const &shaderProgram) const;
void drawWithoutTextures() const;

View File

@@ -1,10 +1,35 @@
#include "gltf_loader.h"
#include "util/Log.h"
static void load_texture(fx::gltf::Texture const &texture,
fx::gltf::Document const &gltf,
std::filesystem::path const &document_path,
Image::ColorFormat colorFormat,
entt::resource_cache<Image> &image_cache)
template <typename T>
static auto create_vertex_attribute_data(std::span<uint8_t const> vertex_attribute_data)
-> VertexAttributeData
{
T attribute_data;
attribute_data.reserve(vertex_attribute_data.size_bytes());
std::memcpy(
attribute_data.data(), vertex_attribute_data.data(), vertex_attribute_data.size_bytes());
return VertexAttributeData{.values = std::move(attribute_data)};
}
template <typename T>
static auto create_indices(std::span<uint8_t const> gltf_index_data) -> Indices
{
T index_data;
index_data.reserve(gltf_index_data.size_bytes());
std::memcpy(index_data.data(), gltf_index_data.data(), gltf_index_data.size_bytes());
return Indices{.values = std::move(index_data)};
}
static auto load_texture(fx::gltf::Texture const &texture,
fx::gltf::Document const &gltf,
std::filesystem::path const &document_path,
Image::ColorFormat colorFormat,
entt::resource_cache<Image> &image_cache) -> entt::resource<Image>
{
auto const &gltf_image = gltf.images.at(texture.source);
auto const base_directory = document_path.parent_path();
@@ -13,53 +38,194 @@ static void load_texture(fx::gltf::Texture const &texture,
auto const &image_buffer_view = gltf.bufferViews.at(gltf_image.bufferView);
auto const &image_buffer = gltf.buffers.at(image_buffer_view.buffer);
std::string const image_name = document_path.string() + ".image." + std::to_string(texture.source);
entt::hashed_string image_hash(image_name.c_str());
std::string const image_name =
document_path.string() + ".image." + std::to_string(texture.source);
entt::hashed_string const image_hash(image_name.c_str());
image_cache.load(
image_hash,
std::span{image_buffer.data}.subspan(image_buffer_view.byteOffset, image_buffer_view.byteLength),
colorFormat);
} else {
auto const image_path = base_directory / gltf_image.uri;
std::size_t const image_size = std::filesystem::file_size(image_path);
auto image_ifstream = std::ifstream(image_path, std::ios::binary);
std::vector<uint8_t> image_data;
image_data.reserve(image_size);
std::copy(std::istreambuf_iterator<char>(image_ifstream),
std::istreambuf_iterator<char>(),
std::back_inserter(image_data));
entt::hashed_string image_hash(image_path.c_str());
image_cache.load(image_hash, image_data, colorFormat);
return image_cache
.load(image_hash,
std::span{image_buffer.data}.subspan(image_buffer_view.byteOffset,
image_buffer_view.byteLength),
colorFormat)
.first->second;
}
auto const image_path = base_directory / gltf_image.uri;
std::size_t const image_size = std::filesystem::file_size(image_path);
auto image_ifstream = std::ifstream(image_path, std::ios::binary);
std::vector<uint8_t> image_data;
image_data.reserve(image_size);
std::copy(std::istreambuf_iterator<char>(image_ifstream),
std::istreambuf_iterator<char>(),
std::back_inserter(image_data));
entt::hashed_string const image_hash(image_path.c_str());
return image_cache.load(image_hash, image_data, colorFormat).first->second;
}
static void load_material(fx::gltf::Material const &material,
fx::gltf::Document const &gltf,
std::filesystem::path const &document_path,
entt::resource_cache<Image> &image_cache)
static auto load_material(fx::gltf::Material const &material,
fx::gltf::Document const &gltf,
std::filesystem::path const &document_path,
entt::resource_cache<Material> &material_cache,
entt::resource_cache<Image> &image_cache) -> entt::resource<Material>
{
auto base_color_texture_id = material.pbrMetallicRoughness.baseColorTexture.index;
auto normal_texture_id = material.normalTexture.index;
std::optional<entt::resource<Image>> base_color_image;
if (base_color_texture_id != -1) {
auto const &base_color_texture = gltf.textures.at(base_color_texture_id);
load_texture(base_color_texture, gltf, document_path, Image::ColorFormat::SRGB, image_cache);
base_color_image = load_texture(
base_color_texture, gltf, document_path, Image::ColorFormat::SRGB, image_cache);
}
std::optional<entt::resource<Image>> normal_map_image;
if (normal_texture_id != -1) {
auto const &normal_texture = gltf.textures.at(normal_texture_id);
load_texture(normal_texture, gltf, document_path, Image::ColorFormat::RGB, image_cache);
normal_map_image =
load_texture(normal_texture, gltf, document_path, Image::ColorFormat::RGB, image_cache);
}
entt::hashed_string material_hash(material.name.c_str());
return material_cache
.load(material_hash,
Material{.base_color_texture = base_color_image,
.normal_map_texture = normal_map_image})
.first->second;
}
auto gltf_loader::operator()(std::filesystem::path const &document_path) -> result_type
static auto load_attribute(std::string_view attribute_name,
uint32_t attribute_id,
fx::gltf::Document const &gltf) -> std::optional<VertexAttributeData>
{
fx::gltf::ReadQuotas const read_quotas{.MaxFileSize = MAX_SIZE, .MaxBufferByteLength = MAX_SIZE};
auto const &attribute_accessor = gltf.accessors.at(attribute_id);
if (attribute_accessor.componentType != fx::gltf::Accessor::ComponentType::Float) {
Log::logger().critical("Only float attributes supported!");
std::terminate();
}
auto vertex_attribute_id = [attribute_name]() -> std::optional<std::size_t> {
if (attribute_name == "POSITION") {
return 0;
}
if (attribute_name == "TEXCOORD_0") {
return 1;
}
if (attribute_name == "NORMAL") {
return 2;
}
if (attribute_name == "TANGENT") {
return 3;
}
return {};
}();
// Skip unsupported attribute
if (!vertex_attribute_id.has_value()) {
return {};
}
auto const &buffer_view = gltf.bufferViews.at(attribute_accessor.bufferView);
auto const &buffer = gltf.buffers.at(buffer_view.buffer);
std::span<uint8_t const> buffer_span(buffer.data);
std::span<uint8_t const> vertex_attribute_data_span =
buffer_span.subspan(buffer_view.byteOffset, buffer_view.byteLength);
auto vertex_attribute_data = [vertex_attribute_data_span, &attribute_accessor]() {
if (attribute_accessor.type == fx::gltf::Accessor::Type::Scalar) {
return create_vertex_attribute_data<VertexAttributeData::Scalar>(
vertex_attribute_data_span);
}
if (attribute_accessor.type == fx::gltf::Accessor::Type::Vec2) {
return create_vertex_attribute_data<VertexAttributeData::Vec2>(
vertex_attribute_data_span);
}
if (attribute_accessor.type == fx::gltf::Accessor::Type::Vec3) {
return create_vertex_attribute_data<VertexAttributeData::Vec3>(
vertex_attribute_data_span);
}
if (attribute_accessor.type == fx::gltf::Accessor::Type::Vec4) {
return create_vertex_attribute_data<VertexAttributeData::Vec4>(
vertex_attribute_data_span);
}
Log::logger().critical("Unsupported vertex attribute type!");
std::terminate();
}();
return std::move(vertex_attribute_data);
}
auto load_gltf_primitive(fx::gltf::Primitive const &gltf_primitive,
fx::gltf::Document const &gltf,
std::filesystem::path const &document_path,
unsigned primitive_id,
entt::resource_cache<Material> &material_cache,
entt::resource_cache<Mesh> &mesh_cache) -> GltfPrimitive
{
// Load attributes
std::map<Mesh::VertexAttributeId, VertexAttributeData> attributes;
for (auto const &attribute : gltf_primitive.attributes) {
auto vertex_attribute_data = load_attribute(attribute.first, attribute.second, gltf);
if (!vertex_attribute_data.has_value()) {
continue;
}
attributes.emplace(attribute.second, std::move(vertex_attribute_data.value()));
}
// Load indices
auto const &indices_accessor = gltf.accessors.at(gltf_primitive.indices);
auto const &indices_buffer_view = gltf.bufferViews.at(indices_accessor.bufferView);
auto const &indices_buffer = gltf.buffers.at(indices_buffer_view.buffer);
std::span<uint8_t const> indices_buffer_span(indices_buffer.data);
std::span<uint8_t const> indices_data_span =
indices_buffer_span.subspan(indices_buffer_view.byteOffset, indices_buffer_view.byteLength);
auto indices = [indices_data_span, &indices_accessor]() {
if (indices_accessor.componentType == fx::gltf::Accessor::ComponentType::UnsignedByte) {
return create_indices<Indices::UnsignedByte>(indices_data_span);
}
if (indices_accessor.componentType == fx::gltf::Accessor::ComponentType::UnsignedShort) {
return create_indices<Indices::UnsignedShort>(indices_data_span);
}
if (indices_accessor.componentType == fx::gltf::Accessor::ComponentType::UnsignedInt) {
return create_indices<Indices::UnsignedInt>(indices_data_span);
}
Log::logger().critical("Unsupported indices type!");
std::terminate();
}();
std::string const mesh_name =
document_path.string() + ".primitive." + std::to_string(primitive_id);
entt::hashed_string const mesh_hash(mesh_name.c_str());
entt::resource<Mesh> mesh =
mesh_cache.load(mesh_hash, Mesh{.attributes = attributes, .indices = indices})
.first->second;
// Get material by name
auto const &gltf_material = gltf.materials.at(gltf_primitive.material);
entt::hashed_string material_hash(gltf_material.name.c_str());
entt::resource<Material> material = material_cache[material_hash];
return GltfPrimitive{.mesh = mesh, .material = material};
}
auto GltfLoader::operator()(std::filesystem::path const &document_path) -> result_type
{
fx::gltf::ReadQuotas const read_quotas{.MaxFileSize = MAX_SIZE,
.MaxBufferByteLength = MAX_SIZE};
fx::gltf::Document gltf = [&]() {
if (document_path.extension() == ".gltf") {
@@ -72,12 +238,39 @@ auto gltf_loader::operator()(std::filesystem::path const &document_path) -> resu
// Load here all the rest...
auto const base_directory = document_path.parent_path();
// Load textures
for (auto const &material : gltf.materials) {
load_material(material, gltf, document_path, image_cache);
// Load materials
std::vector<entt::resource<Material>> materials;
for (auto const &gltf_material : gltf.materials) {
entt::resource<Material> material =
load_material(gltf_material, gltf, document_path, material_cache, image_cache);
materials.push_back(material);
}
// Load meshes
std::vector<entt::resource<GltfMesh>> gltf_meshes;
gltf_meshes.reserve(gltf.meshes.size());
return std::make_shared<fx::gltf::Document>(std::move(gltf));
}
for (auto const &gltf_mesh : gltf.meshes) {
// Load primitives
unsigned primitive_count = 0;
std::vector<GltfPrimitive> primitives;
primitives.reserve(gltf_mesh.primitives.size());
for (auto const &gltf_primitive : gltf_mesh.primitives) {
primitives.push_back(load_gltf_primitive(
gltf_primitive, gltf, document_path, primitive_count, material_cache, mesh_cache));
}
entt::hashed_string gltf_mesh_hash(gltf_mesh.name.c_str());
entt::resource<GltfMesh> gltf_mesh_resource =
gltf_mesh_cache.load(gltf_mesh_hash, GltfMesh{.primitives = std::move(primitives)})
.first->second;
gltf_meshes.push_back(gltf_mesh_resource);
}
return std::make_shared<Gltf>(Gltf{.materials = std::move(materials),
.meshes = std::move(gltf_meshes),
.document = std::move(gltf)});
}

View File

@@ -2,6 +2,7 @@
#include "image.h"
#include "material.h"
#include "mesh.h"
#include <entt/entt.hpp>
#include <filesystem>
@@ -9,16 +10,33 @@
static constexpr auto MAX_SIZE = 512 * 1024 * 1024;
struct GltfPrimitive
{
entt::resource<Mesh> mesh;
entt::resource<Material> material;
};
struct GltfMesh
{
std::vector<GltfPrimitive> primitives;
};
// Move to own file.
struct Gltf
{
std::vector<entt::resource<Material>> materials;
std::vector<entt::resource<GltfMesh>> meshes;
fx::gltf::Document document;
};
struct gltf_loader final
struct GltfLoader final
{
using result_type = std::shared_ptr<fx::gltf::Document>;
using result_type = std::shared_ptr<Gltf>;
auto operator()(std::filesystem::path const &document_path) -> result_type;
entt::resource_cache<Image> image_cache;
entt::resource_cache<Material> material_cache;
entt::resource_cache<Mesh> mesh_cache;
entt::resource_cache<GltfMesh> gltf_mesh_cache;
};

View File

@@ -3,9 +3,10 @@
#include "image.h"
#include <entt/entt.hpp>
#include <optional>
struct Material
{
entt::resource<Image> base_color_texture;
entt::resource<Image> normal_map_texture;
std::optional<entt::resource<Image>> base_color_texture;
std::optional<entt::resource<Image>> normal_map_texture;
};

37
src/mesh.h Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
#include <map>
#include <variant>
#include <vector>
struct VertexAttributeData
{
using Scalar = std::vector<float>;
using Vec2 = std::vector<std::array<float, 2>>;
using Vec3 = std::vector<std::array<float, 3>>;
using Vec4 = std::vector<std::array<float, 4>>;
// Possible improvement:
// Instead of copying the data into a vector, create a span that
// points into the original data. For that, the glTF file has to
// persist until the data is uploaded to the GPU.
std::variant<Scalar, Vec2, Vec3, Vec4> values;
};
struct Indices
{
using UnsignedByte = std::vector<uint8_t>;
using UnsignedShort = std::vector<uint16_t>;
using UnsignedInt = std::vector<uint32_t>;
std::variant<UnsignedByte, UnsignedShort, UnsignedInt> values;
};
struct Mesh
{
using VertexAttributeId = std::size_t;
std::map<VertexAttributeId, VertexAttributeData> attributes;
Indices indices;
};

View File

@@ -2,7 +2,7 @@
#include "Texture.h"
#include "../util/Log.h"
Model::Model(std::string_view name, std::vector<Mesh> meshes) : m_meshes(std::move(meshes)), m_name(name)
Model::Model(std::string_view name, std::vector<Mesh_> meshes) : m_meshes(std::move(meshes)), m_name(name)
{
Log::logger().trace(R"(Loaded model "{}".)", name);
}
@@ -23,7 +23,7 @@ void Model::drawWithoutTextures() const
}
}
auto Model::getMesh(unsigned int index) -> Mesh *
auto Model::getMesh(unsigned int index) -> Mesh_ *
{
return &m_meshes[index];
}

View File

@@ -9,15 +9,15 @@
class Model
{
public:
Model(std::string_view name, std::vector<Mesh> meshes);
Model(std::string_view name, std::vector<Mesh_> meshes);
void draw(ShaderProgram const &shaderProgram) const;
void drawWithoutTextures() const;
auto getMesh(unsigned int index) -> Mesh *; // TODO...
auto getMesh(unsigned int index) -> Mesh_ *; // TODO...
private:
std::vector<Mesh> m_meshes;
std::vector<Mesh_> m_meshes;
std::vector<ResourceId> m_textures;
std::string m_name;