Added support for glTF 2.0 model format

dev
Tomasz Kapuściński 2022-03-12 23:31:32 +01:00
parent fd3c2af358
commit 829c5fb42f
12 changed files with 778 additions and 14 deletions

View File

@ -133,6 +133,8 @@ add_library(colobotbase STATIC
graphics/model/model_input.h
graphics/model/model_io_exception.h
graphics/model/model_io_structs.h
graphics/model/model_gltf.cpp
graphics/model/model_gltf.h
graphics/model/model_manager.cpp
graphics/model/model_manager.h
graphics/model/model_mesh.cpp

View File

@ -55,9 +55,9 @@ CResourceManager::~CResourceManager()
}
}
std::string CResourceManager::CleanPath(const std::string& path)
std::string CResourceManager::CleanPath(const std::filesystem::path& path)
{
return boost::regex_replace(path, boost::regex("(.*)/\\.\\./"), "");
return boost::regex_replace(path.generic_u8string(), boost::regex("(.*)/\\.\\./"), "");
}
@ -137,7 +137,7 @@ std::unique_ptr<CSNDFileWrapper> CResourceManager::GetSNDFileHandler(const std::
}
bool CResourceManager::Exists(const std::string &filename)
bool CResourceManager::Exists(const std::filesystem::path& filename)
{
if (PHYSFS_isInit())
{

View File

@ -23,6 +23,7 @@
#include "common/resources/sdl_memory_wrapper.h"
#include "common/resources/sndfile_wrapper.h"
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
@ -33,7 +34,7 @@ public:
CResourceManager(const char *argv0);
~CResourceManager();
static std::string CleanPath(const std::string &path);
static std::string CleanPath(const std::filesystem::path& path);
//! Add a location to the search path
static bool AddLocation(const std::string &location, bool prepend = true, const std::string &mountPoint = "");
@ -52,7 +53,7 @@ public:
static std::unique_ptr<CSNDFileWrapper> GetSNDFileHandler(const std::string &filename);
//! Check if file exists
static bool Exists(const std::string &filename);
static bool Exists(const std::filesystem::path& filename);
//! Check if file exists and is a directory
static bool DirectoryExists(const std::string& directory);

View File

@ -67,7 +67,7 @@ struct Material
//! Alpha mode
AlphaMode alphaMode = AlphaMode::NONE;
//! Alpha threshold
float alphaThreshold = 0.0;
float alphaThreshold = 0.5;
// Cull face
CullFace cullFace = CullFace::BACK;
// Special tag

View File

@ -25,6 +25,7 @@
#include "common/stringutils.h"
#include "common/resources/inputstream.h"
#include "common/resources/resourcemanager.h"
#include "graphics/engine/engine.h"
@ -45,24 +46,59 @@ COldModelManager::~COldModelManager()
{
}
bool COldModelManager::LoadModel(const std::string& fileName, bool mirrored, int variant)
bool COldModelManager::LoadModel(const std::string& name, bool mirrored, int variant)
{
GetLogger()->Debug("Loading model '%s'\n", fileName.c_str());
CModel model;
try
{
std::filesystem::path path = "models/" + fileName;
auto extension = std::filesystem::path(name).extension().string();
ModelInput::Read(model, path);
if (!extension.empty())
{
GetLogger()->Debug("Loading model '%s'\n", name.c_str());
ModelInput::Read(model, "models/" + name);
if (model.GetMeshCount() == 0)
return false;
goto skip;
}
auto gltf_path = "models/" + name + ".gltf";
if (CResourceManager::Exists(gltf_path))
{
GetLogger()->Debug("Loading model '%s'\n", (name + ".gltf").c_str());
ModelInput::Read(model, gltf_path);
if (model.GetMeshCount() > 0)
goto skip;
}
auto mod_path = "models/" + name + ".mod";
if (CResourceManager::Exists(mod_path))
{
GetLogger()->Debug("Loading model '%s'\n", (name + ".mod").c_str());
ModelInput::Read(model, mod_path);
if (model.GetMeshCount() > 0)
goto skip;
}
return false;
}
catch (const CModelIOException& e)
{
GetLogger()->Error("Loading model '%s' failed: %s\n", fileName.c_str(), e.what());
GetLogger()->Error("Loading model '%s' failed: %s\n", name.c_str(), e.what());
return false;
}
CModelMesh* mesh = model.GetMesh("main");
skip:
CModelMesh* mesh = model.GetMesh();
assert(mesh != nullptr);
ModelInfo modelInfo;
@ -72,7 +108,7 @@ bool COldModelManager::LoadModel(const std::string& fileName, bool mirrored, int
if (mirrored)
Mirror(modelInfo.triangles);
FileInfo fileInfo(fileName, mirrored, variant);
FileInfo fileInfo(name, mirrored, variant);
m_models[fileInfo] = modelInfo;
m_engine->AddBaseObjTriangles(modelInfo.baseObjRank, modelInfo.triangles);

View File

@ -30,6 +30,18 @@ int CModel::GetMeshCount() const
return m_meshes.size();
}
CModelMesh* CModel::GetMesh()
{
if (m_meshes.size() == 1)
{
return &m_meshes.begin()->second;
}
else
{
return GetMesh("main");
}
}
CModelMesh* CModel::GetMesh(const std::string& name)
{
auto it = m_meshes.find(name);

View File

@ -42,6 +42,8 @@ class CModel
public:
//! Returns mesh count
int GetMeshCount() const;
//! Returns the only mesh or mesh with name "main"
CModelMesh* GetMesh();
//! Return a mesh with given \a name
CModelMesh* GetMesh(const std::string& name);
//! Return a mesh with given \a name

View File

@ -0,0 +1,653 @@
/*
* This file is part of the Colobot: Gold Edition source code
* Copyright (C) 2001-2022, Daniel Roux, EPSITEC SA & TerranovaTeam
* http://epsitec.ch; http://colobot.info; http://github.com/colobot
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://gnu.org/licenses
*/
#include "graphics/model/model_mod.h"
#include "common/ioutils.h"
#include "common/resources/inputstream.h"
#include "graphics/model/model_io_exception.h"
#include "graphics/model/model_io_structs.h"
#include <iostream>
#include <vector>
#include <nlohmann/json.hpp>
using namespace IOUtils;
using json = nlohmann::json;
namespace Gfx::ModelIO
{
struct BufferView
{
int buffer = 0;
size_t offset = 0;
size_t length = 0;
};
struct Accessor
{
int bufferView = 0;
size_t byteOffset = 0;
int componentType = 0;
int count = 0;
std::string type = "";
};
struct Texture
{
int sampler = -1;
int source = 0;
};
struct Image
{
std::string uri;
};
struct Sampler
{
};
class GLTFLoader
{
public:
void Load(const std::filesystem::path& path);
CModel GetModel()
{
return m_model;
}
private:
void ReadBuffers();
void ReadBufferViews();
void ReadMaterials();
void ReadAccessors();
void ReadSamplers();
void ReadImages();
void ReadTextures();
void ReadMeshes();
std::vector<glm::vec3> ReadPositions(int index);
std::vector<glm::vec3> ReadNormals(int index);
std::vector<glm::vec2> ReadUVs(int index);
std::vector<glm::u8vec4> ReadColors(int index);
std::vector<unsigned> ReadIndices(int index);
CModel m_model;
std::filesystem::path m_directory;
json m_root;
std::vector<std::vector<char>> m_buffers;
std::vector<BufferView> m_bufferViews;
std::vector<Material> m_materials;
std::vector<Accessor> m_accessors;
std::vector<Texture> m_textures;
std::vector<Image> m_images;
std::vector<Sampler> m_samplers;
};
void ReadGLTFModel(CModel& model, const std::filesystem::path& path)
{
GLTFLoader loader;
loader.Load(path);
model = loader.GetModel();
}
void GLTFLoader::Load(const std::filesystem::path& path)
{
m_directory = path.parent_path();
CInputStream stream(path);
stream >> m_root;
ReadBuffers();
ReadBufferViews();
ReadAccessors();
ReadSamplers();
ReadImages();
ReadTextures();
ReadMaterials();
ReadMeshes();
}
void GLTFLoader::ReadBuffers()
{
m_buffers.clear();
for (const auto& node : m_root["buffers"])
{
size_t length = node["byteLength"].get<int>();
std::vector<char> buffer(length);
if (node.contains("uri"))
{
auto uri = m_directory / node["uri"].get<std::string>();
CInputStream stream(uri);
stream.read(buffer.data(), buffer.size());
}
else
{
std::cerr << "Base64 not yet supported";
}
m_buffers.emplace_back(std::move(buffer));
}
}
void GLTFLoader::ReadBufferViews()
{
m_bufferViews.clear();
for (const auto& node : m_root["bufferViews"])
{
BufferView bufferView{};
bufferView.buffer = node["buffer"].get<int>();
bufferView.offset = node["byteOffset"].get<int>();
bufferView.length = node["byteLength"].get<int>();
m_bufferViews.push_back(bufferView);
}
}
void GLTFLoader::ReadMaterials()
{
m_materials.clear();
for (const auto& material : m_root["materials"])
{
Material mat;
if (material.contains("doubleSided"))
{
mat.cullFace = material["doubleSided"].get<bool>() ? CullFace::NONE : CullFace::BACK;
}
if (material.contains("extras"))
{
const auto& extras = material["extras"];
if (extras.contains("dirt"))
{
int dirt = extras["dirt"].get<int>();
std::string texName = std::string("dirty0") + char('0' + dirt) + ".png";
mat.detailTexture = texName;
}
if (extras.contains("tag"))
{
mat.tag = extras["tag"].get<std::string>();
}
if (extras.contains("energy"))
{
if (extras["energy"].get<int>() != 0)
mat.tag = "energy";
}
if (extras.contains("tracker_1"))
{
if (extras["tracker_1"].get<int>() != 0)
mat.tag = "tracker_right";
}
if (extras.contains("tracker_2"))
{
if (extras["tracker_2"].get<int>() != 0)
mat.tag = "tracker_left";
}
if (extras.contains("transparency"))
{
if (extras["transparency"].get<int>() != 0)
{
mat.alphaMode = AlphaMode::MASK;
mat.alphaThreshold = 0.5f;
}
}
}
if (material.contains("pbrMetallicRoughness"))
{
const auto& pbr = material["pbrMetallicRoughness"];
if (pbr.contains("baseColorFactor"))
{
const auto& color = pbr["baseColorFactor"];
mat.albedoColor = {
color[0].get<float>(),
color[1].get<float>(),
color[2].get<float>(),
color[3].get<float>()
};
}
if (pbr.contains("baseColorTexture"))
{
const auto& tex = pbr["baseColorTexture"];
if (tex.contains("index"))
{
int index = tex["index"].get<int>();
const auto& texture = m_textures[index];
const auto& image = m_images[texture.source];
mat.albedoTexture = image.uri;
}
}
if (pbr.contains("metallicFactor"))
{
mat.metalness = pbr["metallicFactor"].get<float>();
}
if (pbr.contains("roughnessFactor"))
{
mat.roughness = pbr["roughnessFactor"].get<float>();
}
if (pbr.contains("emissiveFactor"))
{
const auto& color = pbr["emissiveFactor"];
mat.emissiveColor = {
color[0].get<float>(),
color[1].get<float>(),
color[2].get<float>(),
0.0
};
}
if (pbr.contains("emissiveTextue"))
{
const auto& tex = pbr["emissiveTextue"];
if (tex.contains("index"))
{
int index = tex["index"].get<int>();
const auto& texture = m_textures[index];
const auto& image = m_images[texture.source];
mat.emissiveTexture = image.uri;
}
}
}
if (material.contains("alphaMode"))
{
auto mode = material["alphaMode"].get<std::string>();
if (mode == "OPAQUE")
mat.alphaMode = AlphaMode::NONE;
else if (mode == "MASK")
mat.alphaMode = AlphaMode::MASK;
else if (mode == "BLEND")
mat.alphaMode = AlphaMode::BLEND;
}
if (material.contains("alphaCutoff"))
{
mat.alphaThreshold = material["alphaCutoff"].get<float>();
}
m_materials.push_back(mat);
}
}
void GLTFLoader::ReadAccessors()
{
m_accessors.clear();
for (const auto& node : m_root["accessors"])
{
Accessor accessor{};
accessor.bufferView = node["bufferView"].get<int>();
if (node.contains("byteOffset"))
accessor.byteOffset = node["byteOffset"].get<int>();
else
accessor.byteOffset = 0;
accessor.count = node["count"].get<int>();
accessor.componentType = node["componentType"].get<int>();
accessor.type = node["type"].get<std::string>();
m_accessors.push_back(accessor);
}
}
void GLTFLoader::ReadSamplers()
{
m_samplers.clear();
for (const auto& node : m_root["samplers"])
{
Sampler sampler{};
m_samplers.push_back(sampler);
}
}
void GLTFLoader::ReadImages()
{
m_images.clear();
for (const auto& node : m_root["images"])
{
Image image{};
if (node.contains("uri"))
{
image.uri = node["uri"].get<std::string>();
}
m_images.push_back(image);
}
}
void GLTFLoader::ReadTextures()
{
m_textures.clear();
for (const auto& node : m_root["textures"])
{
Texture texture{};
if (node.contains("sampler"))
texture.sampler = node["sampler"].get<int>();
texture.source = node["source"].get<int>();
m_textures.push_back(texture);
}
}
void GLTFLoader::ReadMeshes()
{
m_model = {};
for (const auto& node : m_root["meshes"])
{
auto name = node["name"].get<std::string>();
CModelMesh mesh;
for (const auto& primitive : node["primitives"])
{
const auto& material = m_materials[primitive["material"].get<int>()];
auto& part = mesh.AddPart(material);
std::vector<glm::vec3> positions;
std::vector<glm::vec3> normals;
std::vector<glm::vec2> uvs;
std::vector<glm::vec2> uvs2;
std::vector<glm::u8vec4> colors;
std::vector<unsigned> indices;
if (primitive.contains("attributes"))
{
const auto& attributes = primitive["attributes"];
positions = ReadPositions(attributes["POSITION"].get<int>());
if (positions.empty()) continue;
if (attributes.contains("NORMAL"))
{
normals = ReadNormals(attributes["NORMAL"].get<int>());
}
else
{
normals.resize(positions.size());
std::fill(normals.begin(), normals.end(), glm::vec3(0.0, 1.0, 0.0));
}
if (attributes.contains("TEXCOORD_0"))
{
uvs = ReadUVs(attributes["TEXCOORD_0"].get<int>());
}
else
{
uvs.resize(positions.size());
std::fill(uvs.begin(), uvs.end(), glm::vec2(0.0, 0.0));
}
if (attributes.contains("TEXCOORD_1"))
{
uvs2 = ReadUVs(attributes["TEXCOORD_1"].get<int>());
}
else
{
uvs2.resize(positions.size());
std::fill(uvs2.begin(), uvs2.end(), glm::vec2(0.0, 0.0));
}
if (attributes.contains("COLOR_0"))
{
colors = ReadColors(attributes["COLOR_0"].get<int>());
}
else
{
colors.resize(positions.size());
std::fill(colors.begin(), colors.end(), glm::u8vec4(255, 255, 255, 255));
}
if (primitive.contains("indices"))
{
indices = ReadIndices(primitive["indices"].get<int>());
}
// Combine vertex attributes into vertices
std::vector<Vertex3D> vertices(positions.size());
for (size_t i = 0; i < positions.size(); i++)
{
vertices[i].position = positions[i] * glm::vec3(1.0, 1.0, -1.0);
vertices[i].normal = normals[i] * glm::vec3(1.0, 1.0, -1.0);
vertices[i].uv = uvs[i];
vertices[i].uv2 = uvs2[i];
vertices[i].color = colors[i];
}
// No indices, reverse triangle vertices
if (indices.empty())
{
for (size_t i = 0; i < vertices.size() - 2; i += 3)
std::swap(vertices[i], vertices[i + 1]);
}
// Indices present, reverse triangle indices order
else
{
for (size_t i = 0; i < indices.size() - 2; i += 3)
std::swap(indices[i], indices[i + 1]);
}
for (const auto& vertex : vertices)
part.AddVertex(vertex);
for (const auto& index : indices)
part.AddIndex(index);
}
}
if (mesh.GetPartCount() > 0)
m_model.AddMesh(name, std::move(mesh));
}
}
std::vector<glm::vec3> GLTFLoader::ReadPositions(int index)
{
const auto& accessor = m_accessors[index];
std::vector<glm::vec3> positions(accessor.count);
const auto& bufferView = m_bufferViews[accessor.bufferView];
const auto& buffer = m_buffers[bufferView.buffer];
auto data = reinterpret_cast<const glm::vec3*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
positions[i] = data[i];
return positions;
}
std::vector<glm::vec3> GLTFLoader::ReadNormals(int index)
{
const auto& accessor = m_accessors[index];
std::vector<glm::vec3> normals(accessor.count);
const auto& bufferView = m_bufferViews[accessor.bufferView];
const auto& buffer = m_buffers[bufferView.buffer];
auto data = reinterpret_cast<const glm::vec3*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
normals[i] = data[i];
return normals;
}
std::vector<glm::vec2> GLTFLoader::ReadUVs(int index)
{
const auto& accessor = m_accessors[index];
std::vector<glm::vec2> uvs(accessor.count);
const auto& bufferView = m_bufferViews[accessor.bufferView];
const auto& buffer = m_buffers[bufferView.buffer];
if (accessor.componentType == 5126)
{
auto data = reinterpret_cast<const glm::vec2*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
uvs[i] = data[i];
}
else
{
std::cerr << "Invalid UV type: " << accessor.componentType << '\n';
}
return uvs;
}
std::vector<glm::u8vec4> GLTFLoader::ReadColors(int index)
{
const auto& accessor = m_accessors[index];
std::vector<glm::u8vec4> colors(accessor.count);
const auto& bufferView = m_bufferViews[accessor.bufferView];
const auto& buffer = m_buffers[bufferView.buffer];
if (accessor.componentType == 5121)
{
auto data = reinterpret_cast<const glm::u8vec4*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
colors[i] = data[i];
}
else if (accessor.componentType == 5123)
{
auto data = reinterpret_cast<const glm::u16vec4*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
colors[i] = glm::u8vec4(data[i] / glm::u16vec4(256));
}
else if (accessor.componentType == 5126)
{
auto data = reinterpret_cast<const glm::vec4*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
colors[i] = glm::u8vec4(data[i] * 255.0f);
}
else
{
std::cerr << "Invalid color type: " << accessor.componentType << '\n';
}
return colors;
}
std::vector<unsigned> GLTFLoader::ReadIndices(int index)
{
const auto& accessor = m_accessors[index];
std::vector<unsigned> indices(accessor.count);
const auto& bufferView = m_bufferViews[accessor.bufferView];
const auto& buffer = m_buffers[bufferView.buffer];
// Unsigned byte components
if (accessor.componentType == 5121)
{
auto data = reinterpret_cast<const uint8_t*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
indices[i] = data[i];
}
// Unsigned short components
else if (accessor.componentType == 5123)
{
auto data = reinterpret_cast<const uint16_t*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
indices[i] = data[i];
}
// Unsigned int components
else if (accessor.componentType == 5125)
{
auto data = reinterpret_cast<const uint32_t*>(buffer.data() + bufferView.offset);
for (size_t i = 0; i < accessor.count; i++)
indices[i] = data[i];
}
return indices;
}
}

View File

@ -0,0 +1,36 @@
/*
* This file is part of the Colobot: Gold Edition source code
* Copyright (C) 2001-2022, Daniel Roux, EPSITEC SA & TerranovaTeam
* http://epsitec.ch; http://colobot.info; http://github.com/colobot
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://gnu.org/licenses
*/
#pragma once
#include "graphics/model/model.h"
#include <filesystem>
#include <istream>
/**
* \namespace ModelInput
* \brief Namespace with functions to read model files
*/
namespace Gfx::ModelIO
{
void ReadGLTFModel(CModel& model, const std::filesystem::path& path);
}

View File

@ -19,6 +19,7 @@
#include "graphics/model/model_input.h"
#include "graphics/model/model_gltf.h"
#include "graphics/model/model_mod.h"
#include "graphics/model/model_txt.h"
@ -39,6 +40,10 @@ void ModelInput::Read(CModel& model, const std::filesystem::path& path)
{
ModelIO::ReadTextModel(model, path);
}
else if (extension == ".gltf")
{
ModelIO::ReadGLTFModel(model, path);
}
else
{
throw CModelIOException(std::string("Unknown model format: ") + extension.string());

View File

@ -119,6 +119,21 @@ const CModelPart& CModelMesh::GetPart(size_t index) const
return m_parts[index];
}
CModelPart& CModelMesh::AddPart(const Material& material)
{
for (auto& part : m_parts)
{
if (part.GetMaterial() == material)
{
return part;
}
}
m_parts.push_back(CModelPart(material));
return m_parts.back();
}
const glm::vec3& CModelMesh::GetPosition() const
{
return m_position;

View File

@ -80,6 +80,8 @@ public:
size_t GetPartCount() const;
//! Returns a part with given index
const CModelPart& GetPart(size_t index) const;
//! Adds a new part with given material or returns an existing one
CModelPart& AddPart(const Material& material);
//! Returns the mesh position
const glm::vec3& GetPosition() const;