2024-06-02 11:55:04 +00:00
|
|
|
#include "gltf_loader.h"
|
2023-10-02 11:54:37 +00:00
|
|
|
|
|
|
|
#include "log.h"
|
2024-06-02 11:55:04 +00:00
|
|
|
#include "files.h"
|
2023-10-02 11:54:37 +00:00
|
|
|
|
2024-06-02 11:55:04 +00:00
|
|
|
#include <mikktspace.h>
|
|
|
|
#include <weldmesh.h>
|
|
|
|
#include <tiny_gltf.h>
|
2023-11-28 12:50:55 +00:00
|
|
|
|
2024-06-02 11:55:04 +00:00
|
|
|
#include "component_mesh.h"
|
|
|
|
#include "component_transform.h"
|
|
|
|
#include "component_collider.h"
|
2023-11-28 12:50:55 +00:00
|
|
|
|
2024-02-17 00:22:22 +00:00
|
|
|
struct Color {
|
|
|
|
uint8_t r, g, b, a;
|
|
|
|
Color(const std::vector<double>& doubles)
|
|
|
|
{
|
2024-05-16 00:31:19 +00:00
|
|
|
assert(doubles.size() == 4 && "Invalid color doubles array");
|
2024-02-17 00:22:22 +00:00
|
|
|
r = static_cast<uint8_t>(lround(doubles[0] * 255.0));
|
|
|
|
g = static_cast<uint8_t>(lround(doubles[1] * 255.0));
|
|
|
|
b = static_cast<uint8_t>(lround(doubles[2] * 255.0));
|
|
|
|
a = static_cast<uint8_t>(lround(doubles[3] * 255.0));
|
|
|
|
}
|
|
|
|
bool operator==(const Color&) const = default;
|
|
|
|
};
|
|
|
|
|
|
|
|
namespace std {
|
2024-02-24 15:16:30 +00:00
|
|
|
template <>
|
|
|
|
struct std::hash<Color> {
|
2024-02-28 22:18:19 +00:00
|
|
|
std::size_t operator()(const Color& k) const
|
|
|
|
{
|
|
|
|
return static_cast<size_t>(k.r) << 24 | static_cast<size_t>(k.g) << 16 | static_cast<size_t>(k.b) << 8 | static_cast<size_t>(k.a);
|
|
|
|
}
|
2024-02-24 15:16:30 +00:00
|
|
|
};
|
2024-02-17 00:22:22 +00:00
|
|
|
} // namespace std
|
|
|
|
|
2023-11-28 12:50:55 +00:00
|
|
|
namespace tg = tinygltf;
|
|
|
|
|
2023-10-02 11:54:37 +00:00
|
|
|
namespace engine::util {
|
|
|
|
|
2024-02-05 16:02:14 +00:00
|
|
|
template <typename T>
|
|
|
|
struct Attribute {
|
|
|
|
const uint8_t* buffer;
|
|
|
|
size_t offset;
|
|
|
|
size_t stride;
|
|
|
|
const T& operator[](size_t i) { return *reinterpret_cast<const T*>(&buffer[offset + stride * i]); }
|
|
|
|
};
|
|
|
|
|
2023-12-10 20:57:47 +00:00
|
|
|
static void DecomposeTransform(glm::mat4 transform, glm::vec3& pos, glm::quat& rot, glm::vec3& scale)
|
|
|
|
{
|
|
|
|
// get position
|
|
|
|
pos.x = transform[3][0];
|
|
|
|
pos.y = transform[3][1];
|
|
|
|
pos.z = transform[3][2];
|
|
|
|
|
|
|
|
// remove position from matrix
|
|
|
|
transform[3][0] = 0.0f;
|
|
|
|
transform[3][1] = 0.0f;
|
|
|
|
transform[3][2] = 0.0f;
|
|
|
|
|
|
|
|
// get scale
|
|
|
|
scale.x = sqrtf(transform[0][0] * transform[0][0] + transform[0][1] * transform[0][1] + transform[0][2] * transform[0][2]);
|
|
|
|
scale.y = sqrtf(transform[1][0] * transform[1][0] + transform[1][1] * transform[1][1] + transform[1][2] * transform[1][2]);
|
|
|
|
scale.z = sqrtf(transform[2][0] * transform[2][0] + transform[2][1] * transform[2][1] + transform[2][2] * transform[2][2]);
|
|
|
|
|
|
|
|
// remove scaling from matrix
|
|
|
|
for (int row = 0; row < 3; row++) {
|
|
|
|
transform[0][row] /= scale.x;
|
|
|
|
transform[1][row] /= scale.y;
|
|
|
|
transform[2][row] /= scale.z;
|
|
|
|
}
|
|
|
|
|
|
|
|
// get rotation
|
|
|
|
rot = glm::quat_cast(transform);
|
|
|
|
}
|
|
|
|
|
|
|
|
static glm::mat4 MatFromDoubleArray(const std::vector<double>& arr)
|
|
|
|
{
|
|
|
|
glm::mat4 mat{};
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
2024-02-14 00:30:51 +00:00
|
|
|
mat[i][0] = static_cast<float>(arr[static_cast<size_t>(i) * 4 + 0]);
|
|
|
|
mat[i][1] = static_cast<float>(arr[static_cast<size_t>(i) * 4 + 1]);
|
|
|
|
mat[i][2] = static_cast<float>(arr[static_cast<size_t>(i) * 4 + 2]);
|
|
|
|
mat[i][3] = static_cast<float>(arr[static_cast<size_t>(i) * 4 + 3]);
|
2023-12-10 20:57:47 +00:00
|
|
|
}
|
|
|
|
return mat;
|
|
|
|
}
|
|
|
|
|
2023-10-02 11:54:37 +00:00
|
|
|
engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic)
|
|
|
|
{
|
|
|
|
|
2024-04-26 22:31:34 +00:00
|
|
|
LOG_INFO("Loading gltf file: {}", path);
|
|
|
|
|
2023-11-28 12:50:55 +00:00
|
|
|
tg::TinyGLTF loader;
|
|
|
|
tg::Model model;
|
|
|
|
std::string err, warn;
|
|
|
|
|
|
|
|
loader.SetParseStrictness(tg::ParseStrictness::Strict);
|
|
|
|
|
|
|
|
const bool success = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
|
|
|
|
|
|
|
if (!warn.empty()) {
|
|
|
|
LOG_WARN("glTF Loader: {}", warn);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!err.empty()) {
|
|
|
|
LOG_ERROR("glTF Loader: {}", err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!success) {
|
|
|
|
throw std::runtime_error("Failed to load glTF file!");
|
|
|
|
}
|
|
|
|
|
2024-03-19 11:32:51 +00:00
|
|
|
// check for required extensions
|
|
|
|
if (model.extensionsRequired.empty() == false) { // this loader doesn't support any extensions
|
|
|
|
for (const auto& ext : model.extensionsRequired) {
|
2024-03-27 00:28:31 +00:00
|
|
|
LOG_CRITICAL("Unsupported required extension: {}", ext);
|
2024-03-19 11:32:51 +00:00
|
|
|
}
|
|
|
|
throw std::runtime_error("One or more required extensions are unsupported. File: " + path);
|
|
|
|
}
|
|
|
|
|
2023-11-28 12:50:55 +00:00
|
|
|
// test model loading
|
|
|
|
|
|
|
|
if (model.scenes.size() < 1) {
|
|
|
|
throw std::runtime_error("Need at least 1 scene");
|
|
|
|
}
|
|
|
|
|
|
|
|
int scene_index = 0;
|
|
|
|
if (model.defaultScene != -1) scene_index = model.defaultScene;
|
|
|
|
|
2023-12-10 20:57:47 +00:00
|
|
|
const tg::Scene& s = model.scenes.at(scene_index);
|
2023-11-28 12:50:55 +00:00
|
|
|
|
2024-02-28 22:18:19 +00:00
|
|
|
/* Find which texture indices point to base color maps. */
|
2024-02-25 01:35:07 +00:00
|
|
|
/* This must be done for the Texture constructor to know to use srgb or not. */
|
2024-02-28 22:18:19 +00:00
|
|
|
std::vector<bool> tex_index_is_base_color(model.textures.size(), false);
|
2024-02-25 01:35:07 +00:00
|
|
|
for (const tg::Material& mat : model.materials) {
|
2024-02-28 22:18:19 +00:00
|
|
|
int texture_index = mat.pbrMetallicRoughness.baseColorTexture.index;
|
2024-02-25 01:35:07 +00:00
|
|
|
if (texture_index != -1) {
|
2024-02-28 22:18:19 +00:00
|
|
|
assert(texture_index < tex_index_is_base_color.size());
|
|
|
|
tex_index_is_base_color[texture_index] = true;
|
2024-02-25 01:35:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-03 00:01:01 +00:00
|
|
|
/* load all textures found in the model */
|
|
|
|
|
|
|
|
std::vector<std::shared_ptr<Texture>> textures{};
|
|
|
|
textures.reserve(model.textures.size());
|
|
|
|
|
2024-02-25 01:35:07 +00:00
|
|
|
size_t texture_idx = 0;
|
2024-02-03 00:01:01 +00:00
|
|
|
for (const tg::Texture& texture : model.textures) {
|
|
|
|
// find the image first
|
|
|
|
// use missing texture image by default
|
|
|
|
textures.emplace_back(scene.app()->GetResource<Texture>("builtin.white"));
|
2024-02-05 16:02:14 +00:00
|
|
|
|
2024-03-19 11:32:51 +00:00
|
|
|
if (texture.source == -1) {
|
|
|
|
LOG_ERROR("A gltf file specifies a texture with no source.");
|
|
|
|
continue;
|
|
|
|
}
|
2024-02-03 00:01:01 +00:00
|
|
|
|
|
|
|
gfx::SamplerInfo samplerInfo{};
|
|
|
|
// default to trilinear filtering even if mipmaps are not specified
|
|
|
|
samplerInfo.minify = gfx::Filter::kLinear;
|
|
|
|
samplerInfo.magnify = gfx::Filter::kLinear;
|
|
|
|
samplerInfo.mipmap = gfx::Filter::kLinear;
|
|
|
|
if (texture.sampler != -1) {
|
|
|
|
const tg::Sampler& sampler = model.samplers.at(texture.sampler);
|
|
|
|
switch (sampler.minFilter) {
|
2024-02-05 16:02:14 +00:00
|
|
|
case TINYGLTF_TEXTURE_FILTER_NEAREST:
|
|
|
|
case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR:
|
|
|
|
samplerInfo.minify = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.mipmap = gfx::Filter::kLinear;
|
|
|
|
break;
|
|
|
|
case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST:
|
|
|
|
samplerInfo.minify = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.mipmap = gfx::Filter::kNearest;
|
|
|
|
break;
|
|
|
|
case TINYGLTF_TEXTURE_FILTER_LINEAR:
|
|
|
|
case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR:
|
|
|
|
samplerInfo.minify = gfx::Filter::kLinear;
|
|
|
|
samplerInfo.mipmap = gfx::Filter::kLinear;
|
|
|
|
break;
|
|
|
|
case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST:
|
|
|
|
samplerInfo.minify = gfx::Filter::kLinear;
|
|
|
|
samplerInfo.mipmap = gfx::Filter::kNearest;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2024-02-03 00:01:01 +00:00
|
|
|
}
|
|
|
|
switch (sampler.magFilter) {
|
2024-02-05 16:02:14 +00:00
|
|
|
case TINYGLTF_TEXTURE_FILTER_NEAREST:
|
|
|
|
samplerInfo.magnify = gfx::Filter::kNearest;
|
|
|
|
break;
|
|
|
|
case TINYGLTF_TEXTURE_FILTER_LINEAR:
|
|
|
|
samplerInfo.magnify = gfx::Filter::kLinear;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2024-02-03 00:01:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// use aniso if min filter is LINEAR_MIPMAP_LINEAR
|
|
|
|
samplerInfo.anisotropic_filtering = (samplerInfo.minify == gfx::Filter::kLinear && samplerInfo.mipmap == gfx::Filter::kLinear);
|
|
|
|
|
|
|
|
const tg::Image& image = model.images.at(texture.source);
|
|
|
|
if (image.as_is == false && image.bits == 8 && image.component == 4 && image.pixel_type == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) {
|
|
|
|
// create texture on GPU
|
2024-02-28 22:18:19 +00:00
|
|
|
textures.back() = std::make_shared<Texture>(scene.app()->renderer(), image.image.data(), image.width, image.height, samplerInfo,
|
|
|
|
tex_index_is_base_color[texture_idx]);
|
2024-02-03 00:01:01 +00:00
|
|
|
}
|
2024-03-19 11:32:51 +00:00
|
|
|
else {
|
|
|
|
throw std::runtime_error("Texture found in gltf file is unsupported! Make sure texture is 8 bpp. File: " + path);
|
|
|
|
}
|
2024-02-25 01:35:07 +00:00
|
|
|
|
|
|
|
++texture_idx;
|
2024-02-03 00:01:01 +00:00
|
|
|
}
|
2023-11-28 12:50:55 +00:00
|
|
|
|
2024-02-03 00:01:01 +00:00
|
|
|
/* load all materials found in model */
|
|
|
|
|
2024-02-16 22:08:14 +00:00
|
|
|
// store some 1x1 colour textures as a hack to render solid colours
|
2024-02-17 00:22:22 +00:00
|
|
|
std::unordered_map<Color, std::shared_ptr<Texture>> colour_textures;
|
2024-02-28 22:18:19 +00:00
|
|
|
// same for metallic roughness:
|
|
|
|
std::unordered_map<Color, std::shared_ptr<Texture>> metal_rough_textures;
|
2024-02-16 22:08:14 +00:00
|
|
|
|
2024-02-03 00:01:01 +00:00
|
|
|
std::vector<std::shared_ptr<Material>> materials{};
|
|
|
|
materials.reserve(model.materials.size());
|
|
|
|
for (const tg::Material& material : model.materials) {
|
2024-02-13 01:45:14 +00:00
|
|
|
if (material.alphaMode != "OPAQUE") {
|
|
|
|
LOG_WARN("Material {} contains alphaMode {} which isn't supported yet", material.name, material.alphaMode);
|
|
|
|
}
|
|
|
|
if (material.doubleSided == true) {
|
|
|
|
LOG_WARN("Material {} specifies double-sided mesh rendering which isn't supported yet", material.name);
|
|
|
|
}
|
|
|
|
if (material.emissiveTexture.index != -1 || material.emissiveFactor[0] != 0.0 || material.emissiveFactor[1] != 0.0 ||
|
|
|
|
material.emissiveFactor[2] != 0.0) {
|
|
|
|
LOG_WARN("Material {} contains an emissive texture or non-zero emissive factor. Emission is currently unsupported.", material.name);
|
|
|
|
}
|
|
|
|
const auto& baseColorFactor4 = material.pbrMetallicRoughness.baseColorFactor;
|
|
|
|
if (baseColorFactor4[0] != 1.0 || baseColorFactor4[1] != 1.0 || baseColorFactor4[2] != 1.0 || baseColorFactor4[3] != 1.0) {
|
2024-02-17 00:22:22 +00:00
|
|
|
if (material.pbrMetallicRoughness.baseColorTexture.index != -1) {
|
2024-02-16 22:08:14 +00:00
|
|
|
LOG_WARN("Material {} contains a base color multiplier which isn't supported yet.", material.name);
|
2024-02-13 01:45:14 +00:00
|
|
|
}
|
|
|
|
}
|
2024-02-28 22:18:19 +00:00
|
|
|
if (material.pbrMetallicRoughness.metallicFactor != 1.0 || material.pbrMetallicRoughness.roughnessFactor != 1.0) {
|
|
|
|
if (material.pbrMetallicRoughness.metallicRoughnessTexture.index != -1) {
|
2024-04-13 11:11:36 +00:00
|
|
|
LOG_WARN("Material {} contains a metallic and/or roughness multiplier which isn't supported yet.", material.name);
|
2024-02-28 22:18:19 +00:00
|
|
|
}
|
2024-02-13 01:45:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
materials.emplace_back(std::make_shared<Material>(scene.app()->renderer(), scene.app()->GetResource<Shader>("builtin.fancy")));
|
2024-02-28 22:18:19 +00:00
|
|
|
|
|
|
|
// base color
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setAlbedoTexture(scene.app()->GetResource<Texture>("builtin.white"));
|
2024-02-13 01:45:14 +00:00
|
|
|
if (material.pbrMetallicRoughness.baseColorTexture.index != -1) {
|
|
|
|
if (material.pbrMetallicRoughness.baseColorTexture.texCoord == 0) {
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setAlbedoTexture(textures.at(material.pbrMetallicRoughness.baseColorTexture.index));
|
2024-02-14 00:30:51 +00:00
|
|
|
}
|
|
|
|
else {
|
2024-02-25 01:35:07 +00:00
|
|
|
LOG_WARN("Material {} base color texture specifies a UV channel other than zero which is unsupported.", material.name);
|
2024-02-13 01:45:14 +00:00
|
|
|
}
|
|
|
|
}
|
2024-02-17 00:22:22 +00:00
|
|
|
else if (baseColorFactor4[0] != 1.0 || baseColorFactor4[1] != 1.0 || baseColorFactor4[2] != 1.0 || baseColorFactor4[3] != 1.0) {
|
2024-03-27 00:28:31 +00:00
|
|
|
LOG_DEBUG("Creating a base-color texture...");
|
2024-02-17 00:22:22 +00:00
|
|
|
Color c(baseColorFactor4);
|
|
|
|
if (colour_textures.contains(c) == false) {
|
2024-02-28 22:18:19 +00:00
|
|
|
const uint8_t pixel[4] = {c.r, c.g, c.b, c.a};
|
2024-02-17 00:22:22 +00:00
|
|
|
gfx::SamplerInfo samplerInfo{};
|
|
|
|
samplerInfo.minify = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.magnify = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.mipmap = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.anisotropic_filtering = false;
|
|
|
|
colour_textures.emplace(std::make_pair(c, std::make_shared<Texture>(scene.app()->renderer(), pixel, 1, 1, samplerInfo, true)));
|
|
|
|
}
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setAlbedoTexture(colour_textures.at(c));
|
2024-02-17 00:22:22 +00:00
|
|
|
}
|
2024-02-13 01:45:14 +00:00
|
|
|
|
2024-03-19 11:32:51 +00:00
|
|
|
// occlusion roughness metallic
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setOcclusionRoughnessMetallicTexture(
|
2024-04-13 11:11:36 +00:00
|
|
|
scene.app()->GetResource<Texture>("builtin.white")); // default ao = 1.0, rough = 1.0, metal = 1.0
|
2024-02-28 22:18:19 +00:00
|
|
|
if (material.pbrMetallicRoughness.metallicRoughnessTexture.index != -1) {
|
|
|
|
if (material.pbrMetallicRoughness.metallicRoughnessTexture.texCoord == 0) {
|
2024-03-27 00:28:31 +00:00
|
|
|
LOG_DEBUG("Setting occlusion roughness metallic texture!");
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setOcclusionRoughnessMetallicTexture(textures.at(material.pbrMetallicRoughness.metallicRoughnessTexture.index));
|
2024-02-28 22:18:19 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
LOG_WARN("Material {} metallic roughness texture specifies a UV channel other than zero which is unsupported.", material.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2024-03-27 00:28:31 +00:00
|
|
|
LOG_DEBUG("Creating occlusion roughness metallic texture...");
|
2024-04-13 11:11:36 +00:00
|
|
|
const std::vector<double> mr_values{1.0f /* no AO */, material.pbrMetallicRoughness.roughnessFactor, material.pbrMetallicRoughness.metallicFactor,
|
|
|
|
1.0f};
|
2024-02-28 22:18:19 +00:00
|
|
|
Color mr(mr_values);
|
|
|
|
if (metal_rough_textures.contains(mr) == false) {
|
|
|
|
const uint8_t pixel[4] = {mr.r, mr.g, mr.b, mr.a};
|
|
|
|
gfx::SamplerInfo samplerInfo{};
|
|
|
|
samplerInfo.minify = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.magnify = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.mipmap = gfx::Filter::kNearest;
|
|
|
|
samplerInfo.anisotropic_filtering = false;
|
|
|
|
metal_rough_textures.emplace(std::make_pair(mr, std::make_shared<Texture>(scene.app()->renderer(), pixel, 1, 1, samplerInfo, false)));
|
|
|
|
}
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setOcclusionRoughnessMetallicTexture(metal_rough_textures.at(mr));
|
2024-02-28 22:18:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// occlusion texture
|
2024-03-19 11:32:51 +00:00
|
|
|
// if occlusion texture was same as metallic-roughness texture, this will already have been applied
|
2024-02-28 22:18:19 +00:00
|
|
|
if (material.occlusionTexture.index != -1) {
|
|
|
|
if (material.occlusionTexture.texCoord == 0) {
|
2024-03-19 11:32:51 +00:00
|
|
|
if (material.occlusionTexture.index != material.pbrMetallicRoughness.metallicRoughnessTexture.index) {
|
2024-04-13 11:11:36 +00:00
|
|
|
throw std::runtime_error(std::string("Material ") + material.name +
|
|
|
|
std::string(" has an ambient occlusion texture different to the metal-rough texture."));
|
2024-03-19 11:32:51 +00:00
|
|
|
}
|
2024-02-28 22:18:19 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
LOG_WARN("Material {} occlusion texture specifies a UV channel other than zero which is unsupported.", material.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// normal map
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setNormalTexture(scene.app()->GetResource<Texture>("builtin.normal"));
|
2024-02-13 01:45:14 +00:00
|
|
|
if (material.normalTexture.index != -1) {
|
|
|
|
if (material.normalTexture.texCoord == 0) {
|
2024-06-01 22:03:39 +00:00
|
|
|
materials.back()->setNormalTexture(textures.at(material.normalTexture.index));
|
2024-02-13 01:45:14 +00:00
|
|
|
}
|
|
|
|
else {
|
2024-02-25 01:35:07 +00:00
|
|
|
LOG_WARN("Material {} normal texture specifies a UV channel other than zero which is unsupported.", material.name);
|
2024-02-13 01:45:14 +00:00
|
|
|
}
|
|
|
|
}
|
2024-02-03 00:01:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* load all meshes found in model */
|
|
|
|
|
2024-02-05 16:02:14 +00:00
|
|
|
struct EnginePrimitive {
|
|
|
|
std::shared_ptr<Mesh> mesh;
|
|
|
|
std::shared_ptr<Material> material;
|
2024-03-03 23:22:23 +00:00
|
|
|
AABB aabb;
|
2024-02-05 16:02:14 +00:00
|
|
|
};
|
|
|
|
std::vector<std::vector<EnginePrimitive>> primitive_arrays{}; // sub-array is all primitives for a given mesh
|
|
|
|
primitive_arrays.reserve(model.meshes.size());
|
2024-02-03 00:01:01 +00:00
|
|
|
for (const tg::Mesh& mesh : model.meshes) {
|
2024-02-05 16:02:14 +00:00
|
|
|
auto& primitive_array = primitive_arrays.emplace_back();
|
|
|
|
for (const tg::Primitive& primitive : mesh.primitives) {
|
|
|
|
if (primitive.attributes.contains("POSITION")) {
|
|
|
|
const tg::Accessor& pos_accessor = model.accessors.at(primitive.attributes.at("POSITION"));
|
|
|
|
|
2024-02-24 15:16:30 +00:00
|
|
|
const size_t original_num_vertices = pos_accessor.count;
|
2024-02-05 16:02:14 +00:00
|
|
|
|
2024-02-16 22:08:14 +00:00
|
|
|
bool generate_tangents = false; // generating tangents creates a new index list and therefore all attribute accessors must be reassigned
|
|
|
|
|
2024-02-05 16:02:14 +00:00
|
|
|
// these checks are probably unneccesary assuming a valid glTF file
|
|
|
|
// if (pos_accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) throw std::runtime_error("Position att. must be float!");
|
|
|
|
// if (pos_accessor.type != 3) throw std::runtime_error("Position att. dim. must be 3!");
|
|
|
|
|
|
|
|
const tg::BufferView& pos_bufferview = model.bufferViews.at(pos_accessor.bufferView);
|
|
|
|
const tg::Buffer& pos_buffer = model.buffers.at(pos_bufferview.buffer);
|
|
|
|
|
|
|
|
Attribute<glm::vec3> positions{.buffer = pos_buffer.data.data(),
|
|
|
|
.offset = pos_accessor.byteOffset + pos_bufferview.byteOffset,
|
|
|
|
.stride = static_cast<size_t>(pos_accessor.ByteStride(pos_bufferview))};
|
|
|
|
|
|
|
|
Attribute<glm::vec3> normals{};
|
|
|
|
if (primitive.attributes.contains("NORMAL")) {
|
|
|
|
const tg::Accessor& norm_accessor = model.accessors.at(primitive.attributes.at("NORMAL"));
|
|
|
|
const tg::BufferView& norm_bufferview = model.bufferViews.at(norm_accessor.bufferView);
|
|
|
|
const tg::Buffer& norm_buffer = model.buffers.at(norm_bufferview.buffer);
|
|
|
|
normals.buffer = norm_buffer.data.data();
|
|
|
|
normals.offset = norm_accessor.byteOffset + norm_bufferview.byteOffset;
|
|
|
|
normals.stride = static_cast<size_t>(norm_accessor.ByteStride(norm_bufferview));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// TODO: generate flat normals
|
|
|
|
throw std::runtime_error(std::string("No normals found in primitive from ") + mesh.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
Attribute<glm::vec4> tangents{};
|
|
|
|
if (primitive.attributes.contains("TANGENT")) {
|
|
|
|
const tg::Accessor& tang_accessor = model.accessors.at(primitive.attributes.at("TANGENT"));
|
|
|
|
const tg::BufferView& tang_bufferview = model.bufferViews.at(tang_accessor.bufferView);
|
|
|
|
const tg::Buffer& tang_buffer = model.buffers.at(tang_bufferview.buffer);
|
|
|
|
tangents.buffer = tang_buffer.data.data();
|
|
|
|
tangents.offset = tang_accessor.byteOffset + tang_bufferview.byteOffset;
|
|
|
|
tangents.stride = static_cast<size_t>(tang_accessor.ByteStride(tang_bufferview));
|
|
|
|
}
|
|
|
|
else {
|
2024-02-16 22:08:14 +00:00
|
|
|
generate_tangents = true;
|
2024-02-05 16:02:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// UV0
|
|
|
|
Attribute<glm::vec2> uv0s{};
|
|
|
|
if (primitive.attributes.contains("TEXCOORD_0")) {
|
|
|
|
const tg::Accessor& uv0_accessor = model.accessors.at(primitive.attributes.at("TEXCOORD_0"));
|
|
|
|
const tg::BufferView& uv0_bufferview = model.bufferViews.at(uv0_accessor.bufferView);
|
|
|
|
const tg::Buffer& uv0_buffer = model.buffers.at(uv0_bufferview.buffer);
|
|
|
|
uv0s.buffer = uv0_buffer.data.data();
|
|
|
|
uv0s.offset = uv0_accessor.byteOffset + uv0_bufferview.byteOffset;
|
|
|
|
uv0s.stride = static_cast<size_t>(uv0_accessor.ByteStride(uv0_bufferview));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// TODO: Possibly create a shader variant that doesn't need UVs?
|
|
|
|
throw std::runtime_error(std::string("No TEXCOORD_0 found in primitive from ") + mesh.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Indices
|
|
|
|
const tg::Accessor& indices_accessor = model.accessors.at(primitive.indices);
|
|
|
|
const tg::BufferView& indices_bufferview = model.bufferViews.at(indices_accessor.bufferView);
|
|
|
|
const tg::Buffer& indices_buffer = model.buffers.at(indices_bufferview.buffer);
|
|
|
|
const uint8_t* const indices_data_start = indices_buffer.data.data() + indices_accessor.byteOffset + indices_bufferview.byteOffset;
|
|
|
|
|
|
|
|
const size_t num_indices = indices_accessor.count;
|
|
|
|
std::vector<uint32_t> indices;
|
|
|
|
indices.reserve(num_indices);
|
|
|
|
|
2024-04-26 22:31:34 +00:00
|
|
|
// TODO: natively support indices of these sizes instead of having to convert to uint32
|
2024-02-05 16:02:14 +00:00
|
|
|
if (indices_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) {
|
|
|
|
for (size_t i = 0; i < num_indices; ++i) {
|
|
|
|
indices.push_back(*reinterpret_cast<const uint8_t*>(&indices_data_start[i * 1]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (indices_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) {
|
|
|
|
for (size_t i = 0; i < num_indices; ++i) {
|
|
|
|
indices.push_back(*reinterpret_cast<const uint16_t*>(&indices_data_start[i * 2]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (indices_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) {
|
|
|
|
for (size_t i = 0; i < num_indices; ++i) {
|
|
|
|
indices.push_back(*reinterpret_cast<const uint32_t*>(&indices_data_start[i * 4]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
throw std::runtime_error(std::string("Invalid index buffer in primtive from: ") + mesh.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<Vertex> vertices;
|
2024-02-16 22:08:14 +00:00
|
|
|
|
|
|
|
if (generate_tangents) {
|
2024-03-27 00:28:31 +00:00
|
|
|
LOG_DEBUG("Generating tangents...");
|
|
|
|
LOG_TRACE("Tangent gen: vtx count before = {} idx count before = {}", original_num_vertices, num_indices);
|
2024-02-16 22:08:14 +00:00
|
|
|
// generate tangents if they're not in the file
|
2024-04-27 22:56:59 +00:00
|
|
|
// manually generating tangents directly with MikkTSpace instead of util::GenTangents() in order to directly access glTF vertex attributes
|
2024-02-16 22:08:14 +00:00
|
|
|
struct MeshData {
|
|
|
|
Attribute<glm::vec3>* positions;
|
|
|
|
Attribute<glm::vec3>* normals;
|
|
|
|
Attribute<glm::vec2>* uvs;
|
|
|
|
const uint32_t* indices;
|
|
|
|
size_t num_indices;
|
|
|
|
std::vector<Vertex>* new_vertices;
|
|
|
|
};
|
|
|
|
|
|
|
|
MeshData meshData{};
|
|
|
|
meshData.positions = &positions;
|
|
|
|
meshData.normals = &normals;
|
|
|
|
meshData.uvs = &uv0s;
|
|
|
|
meshData.indices = indices.data();
|
|
|
|
meshData.num_indices = num_indices;
|
|
|
|
meshData.new_vertices = &vertices;
|
|
|
|
vertices.resize(num_indices);
|
|
|
|
|
|
|
|
SMikkTSpaceInterface mts_interface{};
|
|
|
|
mts_interface.m_getNumFaces = [](const SMikkTSpaceContext* pContext) -> int {
|
|
|
|
const MeshData* meshData = static_cast<const MeshData*>(pContext->m_pUserData);
|
|
|
|
assert(meshData->num_indices % 3 == 0);
|
2024-02-24 15:16:30 +00:00
|
|
|
return static_cast<int>(meshData->num_indices / 3);
|
2024-02-16 22:08:14 +00:00
|
|
|
};
|
|
|
|
mts_interface.m_getNumVerticesOfFace = [](const SMikkTSpaceContext*, const int) -> int { return 3; };
|
|
|
|
mts_interface.m_getPosition = [](const SMikkTSpaceContext* pContext, float fvPosOut[], const int iFace, const int iVert) -> void {
|
|
|
|
const MeshData* const meshData = static_cast<const MeshData*>(pContext->m_pUserData);
|
2024-02-28 22:18:19 +00:00
|
|
|
const size_t i = static_cast<size_t>(iFace) * 3 + static_cast<size_t>(iVert);
|
2024-02-16 22:08:14 +00:00
|
|
|
assert(i < meshData->num_indices);
|
|
|
|
const size_t vertex_index = meshData->indices[i];
|
|
|
|
const glm::vec3 pos = meshData->positions->operator[](vertex_index);
|
|
|
|
fvPosOut[0] = pos.x;
|
|
|
|
fvPosOut[1] = pos.y;
|
|
|
|
fvPosOut[2] = pos.z;
|
|
|
|
};
|
|
|
|
mts_interface.m_getNormal = [](const SMikkTSpaceContext* pContext, float fvNormOut[], const int iFace, const int iVert) -> void {
|
|
|
|
const MeshData* const meshData = static_cast<const MeshData*>(pContext->m_pUserData);
|
2024-02-28 22:18:19 +00:00
|
|
|
const size_t i = static_cast<size_t>(iFace) * 3 + static_cast<size_t>(iVert);
|
2024-02-16 22:08:14 +00:00
|
|
|
assert(i < meshData->num_indices);
|
|
|
|
const size_t vertex_index = meshData->indices[i];
|
|
|
|
const glm::vec3 norm = meshData->normals->operator[](vertex_index);
|
|
|
|
fvNormOut[0] = norm.x;
|
|
|
|
fvNormOut[1] = norm.y;
|
|
|
|
fvNormOut[2] = norm.z;
|
|
|
|
};
|
|
|
|
mts_interface.m_getTexCoord = [](const SMikkTSpaceContext* pContext, float fvTexcOut[], const int iFace, const int iVert) -> void {
|
|
|
|
const MeshData* const meshData = static_cast<const MeshData*>(pContext->m_pUserData);
|
2024-02-28 22:18:19 +00:00
|
|
|
const size_t i = static_cast<size_t>(iFace) * 3 + static_cast<size_t>(iVert);
|
2024-02-16 22:08:14 +00:00
|
|
|
assert(i < meshData->num_indices);
|
|
|
|
const size_t vertex_index = meshData->indices[i];
|
|
|
|
const glm::vec2 uv = meshData->uvs->operator[](vertex_index);
|
|
|
|
fvTexcOut[0] = uv.x;
|
|
|
|
fvTexcOut[1] = uv.y;
|
|
|
|
};
|
|
|
|
mts_interface.m_setTSpaceBasic = [](const SMikkTSpaceContext* pContext, const float fvTangent[], const float fSign, const int iFace,
|
|
|
|
const int iVert) -> void {
|
|
|
|
MeshData* const meshData = static_cast<MeshData*>(pContext->m_pUserData);
|
2024-02-28 22:18:19 +00:00
|
|
|
const size_t i = static_cast<size_t>(iFace) * 3 + static_cast<size_t>(iVert);
|
2024-02-16 22:08:14 +00:00
|
|
|
assert(i < meshData->num_indices);
|
|
|
|
const size_t vertex_index = meshData->indices[i];
|
|
|
|
|
|
|
|
Vertex& new_v = meshData->new_vertices->operator[](i);
|
|
|
|
|
|
|
|
new_v.pos = meshData->positions->operator[](vertex_index);
|
|
|
|
new_v.norm = meshData->normals->operator[](vertex_index);
|
|
|
|
new_v.uv = meshData->uvs->operator[](vertex_index);
|
|
|
|
new_v.tangent.x = fvTangent[0];
|
|
|
|
new_v.tangent.y = fvTangent[1];
|
|
|
|
new_v.tangent.z = fvTangent[2];
|
|
|
|
new_v.tangent.w = fSign;
|
|
|
|
};
|
|
|
|
SMikkTSpaceContext mts_context{};
|
|
|
|
mts_context.m_pInterface = &mts_interface;
|
|
|
|
mts_context.m_pUserData = &meshData;
|
|
|
|
|
|
|
|
bool tan_result = genTangSpaceDefault(&mts_context);
|
|
|
|
if (tan_result == false) throw std::runtime_error("Failed to generate tangents!");
|
|
|
|
|
2024-02-24 15:16:30 +00:00
|
|
|
// vertices now contains new vertices (possibly with duplicates)
|
|
|
|
// use weldmesh to generate new index list without duplicates
|
|
|
|
|
|
|
|
std::vector<int> remap_table(num_indices); // initialised to zeros
|
|
|
|
std::vector<Vertex> vertex_data_out(num_indices); // initialised to zeros
|
|
|
|
|
|
|
|
const int num_unq_vertices = WeldMesh(remap_table.data(), reinterpret_cast<float*>(vertex_data_out.data()),
|
2024-06-01 22:03:39 +00:00
|
|
|
reinterpret_cast<float*>(vertices.data()), static_cast<int>(num_indices), Vertex::floatsPerVertex());
|
2024-02-24 15:16:30 +00:00
|
|
|
assert(num_unq_vertices >= 0);
|
|
|
|
|
|
|
|
// get new vertices into the vector
|
2024-04-13 11:11:36 +00:00
|
|
|
vertices.resize(static_cast<size_t>(num_unq_vertices));
|
|
|
|
for (size_t i = 0; i < static_cast<size_t>(num_unq_vertices); ++i) {
|
2024-02-24 15:16:30 +00:00
|
|
|
vertices[i] = vertex_data_out[i];
|
2024-02-16 22:08:14 +00:00
|
|
|
}
|
2024-02-24 15:16:30 +00:00
|
|
|
|
|
|
|
// get new indices into the vector
|
|
|
|
indices.resize(num_indices); // redundant, for clarity
|
|
|
|
for (size_t i = 0; i < num_indices; ++i) {
|
|
|
|
assert(remap_table[i] >= 0);
|
|
|
|
indices[i] = static_cast<uint32_t>(remap_table[i]);
|
|
|
|
}
|
|
|
|
|
2024-03-27 00:28:31 +00:00
|
|
|
LOG_TRACE("Tangent gen: vtx count after = {} idx count after = {}", vertices.size(), indices.size());
|
2024-02-16 22:08:14 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// combine vertices into one array
|
|
|
|
vertices.clear();
|
2024-02-24 15:16:30 +00:00
|
|
|
vertices.reserve(original_num_vertices);
|
|
|
|
for (size_t i = 0; i < original_num_vertices; ++i) {
|
2024-02-16 22:08:14 +00:00
|
|
|
Vertex v{.pos = positions[i], .norm = normals[i], .tangent = tangents[i], .uv = uv0s[i]};
|
|
|
|
vertices.push_back(v);
|
|
|
|
}
|
2024-02-05 16:02:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// generate mesh on GPU
|
|
|
|
std::shared_ptr<Mesh> engine_mesh = std::make_shared<Mesh>(scene.app()->renderer()->GetDevice(), vertices, indices);
|
|
|
|
|
|
|
|
// get material
|
|
|
|
std::shared_ptr<Material> engine_material = nullptr;
|
|
|
|
if (primitive.material != -1) {
|
|
|
|
engine_material = materials.at(primitive.material);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
engine_material = scene.app()->GetResource<Material>("builtin.default");
|
|
|
|
}
|
|
|
|
|
2024-03-03 23:22:23 +00:00
|
|
|
// get AABB
|
|
|
|
AABB box{};
|
2024-03-23 21:16:30 +00:00
|
|
|
box.min.x = static_cast<float>(pos_accessor.minValues.at(0));
|
|
|
|
box.min.y = static_cast<float>(pos_accessor.minValues.at(1));
|
|
|
|
box.min.z = static_cast<float>(pos_accessor.minValues.at(2));
|
|
|
|
box.max.x = static_cast<float>(pos_accessor.maxValues.at(0));
|
|
|
|
box.max.y = static_cast<float>(pos_accessor.maxValues.at(1));
|
|
|
|
box.max.z = static_cast<float>(pos_accessor.maxValues.at(2));
|
2024-03-03 23:22:23 +00:00
|
|
|
|
|
|
|
primitive_array.emplace_back(engine_mesh, engine_material, box);
|
2024-02-05 16:02:14 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// skip primitive's rendering
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
2023-12-10 20:57:47 +00:00
|
|
|
}
|
2024-02-05 16:02:14 +00:00
|
|
|
|
2024-02-16 22:08:14 +00:00
|
|
|
/* now create the entities and traverse the glTF scene hierarchy */
|
|
|
|
const std::filesystem::path filePath(path);
|
|
|
|
const std::string name = filePath.stem().string();
|
|
|
|
|
|
|
|
// glTF uses the Y-up convention so the parent object must be rotated to Z-up
|
|
|
|
const Entity parent = scene.CreateEntity(name, 0, glm::vec3{}, glm::quat{glm::one_over_root_two<float>(), glm::one_over_root_two<float>(), 0.0f, 0.0f});
|
2024-04-13 11:11:36 +00:00
|
|
|
scene.GetTransform(parent)->is_static = isStatic;
|
2024-02-14 00:30:51 +00:00
|
|
|
|
|
|
|
std::vector<Entity> entities(model.nodes.size(), 0);
|
|
|
|
std::function<void(Entity, const tg::Node&)> generateEntities = [&](Entity parent_entity, const tg::Node& node) -> void {
|
|
|
|
const Entity e = scene.CreateEntity(node.name.empty() ? "anode" : node.name, parent_entity);
|
|
|
|
|
|
|
|
// transform
|
|
|
|
auto t = scene.GetComponent<TransformComponent>(e);
|
|
|
|
t->position.x = 0.0f;
|
|
|
|
t->position.y = 0.0f;
|
|
|
|
t->position.z = 0.0f;
|
|
|
|
t->rotation.x = 0.0f;
|
|
|
|
t->rotation.y = 0.0f;
|
|
|
|
t->rotation.z = 0.0f;
|
|
|
|
t->rotation.w = 1.0f;
|
|
|
|
t->scale.x = 1.0f;
|
|
|
|
t->scale.y = 1.0f;
|
|
|
|
t->scale.z = 1.0f;
|
2024-03-27 00:28:31 +00:00
|
|
|
t->is_static = isStatic; // if file was imported as static, no imported entities can move!
|
2024-02-14 00:30:51 +00:00
|
|
|
|
|
|
|
if (node.matrix.size() == 16) {
|
|
|
|
const glm::mat4 matrix = MatFromDoubleArray(node.matrix);
|
|
|
|
DecomposeTransform(matrix, t->position, t->rotation, t->scale);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (node.translation.size() == 3) {
|
|
|
|
t->position.x = static_cast<float>(node.translation[0]);
|
|
|
|
t->position.y = static_cast<float>(node.translation[1]);
|
|
|
|
t->position.z = static_cast<float>(node.translation[2]);
|
|
|
|
}
|
|
|
|
if (node.rotation.size() == 4) {
|
|
|
|
t->rotation.x = static_cast<float>(node.rotation[0]);
|
|
|
|
t->rotation.y = static_cast<float>(node.rotation[1]);
|
|
|
|
t->rotation.z = static_cast<float>(node.rotation[2]);
|
|
|
|
t->rotation.w = static_cast<float>(node.rotation[3]);
|
|
|
|
}
|
|
|
|
if (node.scale.size() == 3) {
|
|
|
|
t->scale.x = static_cast<float>(node.scale[0]);
|
|
|
|
t->scale.y = static_cast<float>(node.scale[1]);
|
|
|
|
t->scale.z = static_cast<float>(node.scale[2]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ignoring cameras
|
|
|
|
// ignoring skin
|
|
|
|
// ignoring weights
|
|
|
|
|
|
|
|
if (node.mesh != -1) {
|
|
|
|
const auto& primitives = primitive_arrays.at(node.mesh);
|
2024-02-25 01:35:07 +00:00
|
|
|
if (primitives.size() == 1) {
|
|
|
|
auto meshren = scene.AddComponent<MeshRenderableComponent>(e);
|
|
|
|
meshren->mesh = primitives.front().mesh;
|
|
|
|
meshren->material = primitives.front().material;
|
2024-03-03 23:22:23 +00:00
|
|
|
auto collider = scene.AddComponent<ColliderComponent>(e);
|
|
|
|
collider->aabb = primitives.front().aabb;
|
2024-02-25 01:35:07 +00:00
|
|
|
}
|
|
|
|
else {
|
2024-04-26 22:31:34 +00:00
|
|
|
int i = 0;
|
2024-02-25 01:35:07 +00:00
|
|
|
for (const EnginePrimitive& prim : primitives) {
|
|
|
|
auto prim_entity = scene.CreateEntity(std::string("_mesh") + std::to_string(i), e);
|
2024-04-26 22:31:34 +00:00
|
|
|
scene.GetTransform(prim_entity)->is_static = isStatic;
|
2024-02-25 01:35:07 +00:00
|
|
|
auto meshren = scene.AddComponent<MeshRenderableComponent>(prim_entity);
|
|
|
|
meshren->mesh = prim.mesh;
|
|
|
|
meshren->material = prim.material;
|
2024-04-26 22:31:34 +00:00
|
|
|
auto collider = scene.AddComponent<ColliderComponent>(prim_entity);
|
2024-03-03 23:22:23 +00:00
|
|
|
collider->aabb = prim.aabb;
|
2024-02-25 01:35:07 +00:00
|
|
|
++i;
|
|
|
|
}
|
2024-02-14 00:30:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i : node.children) {
|
|
|
|
generateEntities(e, model.nodes.at(i));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
for (int i : s.nodes) {
|
|
|
|
generateEntities(parent, model.nodes.at(i));
|
|
|
|
}
|
2023-10-02 11:54:37 +00:00
|
|
|
|
2024-04-26 22:31:34 +00:00
|
|
|
LOG_DEBUG("Loaded glTF file: {}", path);
|
2024-02-05 16:02:14 +00:00
|
|
|
|
2023-12-10 20:57:47 +00:00
|
|
|
return parent;
|
2023-10-02 11:54:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace engine::util
|