Skip to content

Commit

Permalink
[Data/GltfFormat] Loading glTF and GLB files is now supported
Browse files Browse the repository at this point in the history
- The loader recovers all basic geometry information (vertices, indices & render mode), as well as materials (Cook-Torrance, metalness/roughness model) & related textures
  - Animation and morphing, among other features, aren't processed
  - No extension is explicitly supported

- Added a glTF unit test, along with a basic glTF test file
  • Loading branch information
Razakhel committed Dec 14, 2023
1 parent b859a7e commit 25bc6ea
Show file tree
Hide file tree
Showing 14 changed files with 867 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,8 @@ else ()
target_compile_definitions(RaZ PUBLIC RAZ_NO_LUA)
endif ()

target_link_libraries(RaZ PRIVATE fastgltf simdjson)

# Compiling RaZ's sources
target_sources(RaZ PRIVATE ${RAZ_FILES})

Expand Down
25 changes: 25 additions & 0 deletions include/RaZ/Data/GltfFormat.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#ifndef RAZ_GLTFFORMAT_HPP
#define RAZ_GLTFFORMAT_HPP

#include <utility>

namespace Raz {

class FilePath;
class Mesh;
class MeshRenderer;

namespace GltfFormat {

/// Loads a mesh from a glTF or GLB file.
/// \param filePath File from which to load the mesh.
/// \return Pair containing respectively the mesh's data (vertices & indices) and rendering information (materials, textures, ...).
std::pair<Mesh, MeshRenderer> load(const FilePath& filePath);

} // namespace GltfFormat

} // namespace Raz

#endif // RAZ_GLTFFORMAT_HPP
1 change: 1 addition & 0 deletions include/RaZ/RaZ.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "Data/BvhSystem.hpp"
#include "Data/Color.hpp"
#include "Data/FbxFormat.hpp"
#include "Data/GltfFormat.hpp"
#include "Data/Graph.hpp"
#include "Data/Image.hpp"
#include "Data/ImageFormat.hpp"
Expand Down
2 changes: 1 addition & 1 deletion src/RaZ/Data/FbxLoad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ std::pair<Mesh, MeshRenderer> load(const FilePath& filePath) {
// TODO: small hack to avoid segfaulting when mesh count > material count, but clearly wrong; find another way
submeshRenderer.setMaterialIndex(std::min(meshIndex, scene->GetMaterialCount() - 1));
else
Logger::error("[FBX] Materials can't be mapped to anything other than the whole submesh.");
Logger::error("[FbxLoad] Materials can't be mapped to anything other than the whole submesh.");
}

mesh.addSubmesh(std::move(submesh));
Expand Down
312 changes: 312 additions & 0 deletions src/RaZ/Data/GltfLoad.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
#include "RaZ/Data/GltfFormat.hpp"
#include "RaZ/Data/Image.hpp"
#include "RaZ/Data/ImageFormat.hpp"
#include "RaZ/Data/Mesh.hpp"
#include "RaZ/Render/MeshRenderer.hpp"
#include "RaZ/Utils/FilePath.hpp"
#include "RaZ/Utils/FileUtils.hpp"
#include "RaZ/Utils/Logger.hpp"

#include <fastgltf/parser.hpp>

namespace Raz::GltfFormat {

namespace {

template <typename T>
void loadVertexData(const fastgltf::Accessor& accessor,
const std::vector<fastgltf::Buffer>& buffers,
const std::vector<fastgltf::BufferView>& bufferViews,
std::vector<Vertex>& vertices,
void (*callback)(Vertex&, const T*)) {
assert("Error: Loading vertex data requires the accessor to reference a buffer view." && accessor.bufferViewIndex.has_value());

const fastgltf::BufferView& bufferView = bufferViews[*accessor.bufferViewIndex];
const fastgltf::DataSource& bufferData = buffers[bufferView.bufferIndex].data;

if (!std::holds_alternative<fastgltf::sources::Vector>(bufferData))
throw std::runtime_error("Error: Cannot load glTF data from sources other than vectors.");

const std::size_t dataOffset = bufferView.byteOffset + accessor.byteOffset;
const uint8_t* const vertexData = std::get<fastgltf::sources::Vector>(bufferData).bytes.data() + dataOffset;
const std::size_t dataStride = bufferView.byteStride.value_or(fastgltf::getElementByteSize(accessor.type, accessor.componentType));

for (std::size_t vertIndex = 0; vertIndex < vertices.size(); ++vertIndex) {
const auto* data = reinterpret_cast<const T*>(vertexData + vertIndex * dataStride);
callback(vertices[vertIndex], data);
}
}

void loadVertices(const fastgltf::Primitive& primitive,
const std::vector<fastgltf::Buffer>& buffers,
const std::vector<fastgltf::BufferView>& bufferViews,
const std::vector<fastgltf::Accessor>& accessors,
std::vector<Vertex>& vertices) {
Logger::debug("[GltfLoad] Loading vertices...");

const auto positionIt = primitive.findAttribute("POSITION");

if (positionIt == primitive.attributes.end())
throw std::invalid_argument("Error: Required 'POSITION' attribute not found in the glTF file.");

const fastgltf::Accessor& positionAccessor = accessors[positionIt->second];

if (!positionAccessor.bufferViewIndex.has_value())
return;

vertices.resize(positionAccessor.count);

loadVertexData<float>(positionAccessor, buffers, bufferViews, vertices, [] (Vertex& vert, const float* data) {
vert.position = Vec3f(data[0], data[1], data[2]);
});

// The tangent's input W component (data[3]) is either 1 or -1 and represents the handedness
// See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview
constexpr std::array<std::pair<std::string_view, void (*)(Vertex&, const float*)>, 3> attributes = {{
{ "TEXCOORD_0", [] (Vertex& vert, const float* data) { vert.texcoords = Vec2f(data[0], data[1]); } },
{ "NORMAL", [] (Vertex& vert, const float* data) { vert.normal = Vec3f(data[0], data[1], data[2]); } },
{ "TANGENT", [] (Vertex& vert, const float* data) { vert.tangent = Vec3f(data[0], data[1], data[2]) * data[3]; } }
}};

for (auto&& [attribName, callback] : attributes) {
const auto attribIter = primitive.findAttribute(attribName);

if (attribIter == primitive.attributes.end())
continue;

const fastgltf::Accessor& attribAccessor = accessors[attribIter->second];

if (attribAccessor.bufferViewIndex.has_value())
loadVertexData(attribAccessor, buffers, bufferViews, vertices, callback);
}

Logger::debug("[GltfLoad] Loaded vertices");
}

void loadIndices(const fastgltf::Accessor& indicesAccessor,
const std::vector<fastgltf::Buffer>& buffers,
const std::vector<fastgltf::BufferView>& bufferViews,
std::vector<unsigned int>& indices) {
Logger::debug("[GltfLoad] Loading indices...");

if (!indicesAccessor.bufferViewIndex.has_value())
throw std::invalid_argument("Error: Missing glTF buffer to load indices from.");

indices.resize(indicesAccessor.count);

const fastgltf::BufferView& indicesView = bufferViews[*indicesAccessor.bufferViewIndex];
const fastgltf::Buffer& indicesBuffer = buffers[indicesView.bufferIndex];

if (!std::holds_alternative<fastgltf::sources::Vector>(indicesBuffer.data))
throw std::runtime_error("Error: Cannot load glTF data from sources other than vectors.");

const std::size_t dataOffset = indicesView.byteOffset + indicesAccessor.byteOffset;
const uint8_t* const indicesData = std::get<fastgltf::sources::Vector>(indicesBuffer.data).bytes.data() + dataOffset;
const std::size_t dataSize = fastgltf::getElementByteSize(indicesAccessor.type, indicesAccessor.componentType);
const std::size_t dataStride = indicesView.byteStride.value_or(dataSize);

for (std::size_t i = 0; i < indices.size(); ++i) {
// The indices must be of an unsigned integer type, but its size is unspecified
// See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_indices
switch (dataSize) {
case 1:
indices[i] = *reinterpret_cast<const uint8_t*>(indicesData + i * dataStride);
break;

case 2:
indices[i] = *reinterpret_cast<const uint16_t*>(indicesData + i * dataStride);
break;

case 4:
indices[i] = *reinterpret_cast<const uint32_t*>(indicesData + i * dataStride);
break;

default:
throw std::invalid_argument("Error: Unexpected indices data size (" + std::to_string(dataSize) + ").");
}
}

Logger::debug("[GltfLoad] Loaded indices");
}

std::pair<Mesh, MeshRenderer> loadMeshes(const std::vector<fastgltf::Mesh>& meshes,
const std::vector<fastgltf::Buffer>& buffers,
const std::vector<fastgltf::BufferView>& bufferViews,
const std::vector<fastgltf::Accessor>& accessors) {
Logger::debug("[GltfLoad] Loading " + std::to_string(meshes.size()) + " meshes...");

Mesh loadedMesh;
MeshRenderer loadedMeshRenderer;

for (const fastgltf::Mesh& mesh : meshes) {
for (const fastgltf::Primitive& primitive : mesh.primitives) {
if (!primitive.indicesAccessor.has_value())
throw std::invalid_argument("Error: The glTF file requires having indexed geometry.");

Submesh& submesh = loadedMesh.addSubmesh();
SubmeshRenderer& submeshRenderer = loadedMeshRenderer.addSubmeshRenderer();

loadVertices(primitive, buffers, bufferViews, accessors, submesh.getVertices());
loadIndices(accessors[*primitive.indicesAccessor], buffers, bufferViews, submesh.getTriangleIndices());

submeshRenderer.load(submesh, (primitive.type == fastgltf::PrimitiveType::Triangles ? RenderMode::TRIANGLE : RenderMode::POINT));
submeshRenderer.setMaterialIndex(primitive.materialIndex.value_or(0));
}
}

Logger::debug("[GltfLoad] Loaded meshes");

return { std::move(loadedMesh), std::move(loadedMeshRenderer) };
}

std::vector<Image> loadImages(const std::vector<fastgltf::Image>& images, const FilePath& rootFilePath) {
Logger::debug("[GltfLoad] Loading " + std::to_string(images.size()) + " images...");

std::vector<Image> loadedImages;
loadedImages.reserve(images.size());

for (const fastgltf::Image& img : images) {
if (!std::holds_alternative<fastgltf::sources::URI>(img.data)) {
Logger::error("[GltfLoad] Images can only be loaded from a file path for now.");
continue;
}

const auto& imgPath = std::get<fastgltf::sources::URI>(img.data).uri.path();
loadedImages.emplace_back(ImageFormat::load(rootFilePath + imgPath));
}

Logger::debug("[GltfLoad] Loaded images");

return loadedImages;
}

Image extractAmbientOcclusionImage(const Image& occlusionImg) {
Image ambientImg(occlusionImg.getWidth(), occlusionImg.getHeight(), ImageColorspace::GRAY, occlusionImg.getDataType());

for (std::size_t i = 0; i < occlusionImg.getWidth() * occlusionImg.getHeight(); ++i) {
const std::size_t finalIndex = i * occlusionImg.getChannelCount();

// The occlusion is located in the red (1st) channel
// See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_material_occlusiontexture
if (occlusionImg.getDataType() == ImageDataType::BYTE)
static_cast<uint8_t*>(ambientImg.getDataPtr())[i] = static_cast<const uint8_t*>(occlusionImg.getDataPtr())[finalIndex];
else
static_cast<float*>(ambientImg.getDataPtr())[i] = static_cast<const float*>(occlusionImg.getDataPtr())[finalIndex];
}

return ambientImg;
}

std::pair<Image, Image> extractMetalnessRoughnessImages(const Image& metalRoughImg) {
Image metalnessImg(metalRoughImg.getWidth(), metalRoughImg.getHeight(), ImageColorspace::GRAY, metalRoughImg.getDataType());
Image roughnessImg(metalRoughImg.getWidth(), metalRoughImg.getHeight(), ImageColorspace::GRAY, metalRoughImg.getDataType());

for (std::size_t i = 0; i < metalRoughImg.getWidth() * metalRoughImg.getHeight(); ++i) {
const std::size_t finalIndex = i * metalRoughImg.getChannelCount();

// The metalness & roughness are located respectively in the blue (3rd) & green (2nd) channels
// See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_material_pbrmetallicroughness_metallicroughnesstexture
if (metalRoughImg.getDataType() == ImageDataType::BYTE) {
static_cast<uint8_t*>(metalnessImg.getDataPtr())[i] = static_cast<const uint8_t*>(metalRoughImg.getDataPtr())[finalIndex + 2];
static_cast<uint8_t*>(roughnessImg.getDataPtr())[i] = static_cast<const uint8_t*>(metalRoughImg.getDataPtr())[finalIndex + 1];
} else {
static_cast<float*>(metalnessImg.getDataPtr())[i] = static_cast<const float*>(metalRoughImg.getDataPtr())[finalIndex + 2];
static_cast<float*>(roughnessImg.getDataPtr())[i] = static_cast<const float*>(metalRoughImg.getDataPtr())[finalIndex + 1];
}
}

return { std::move(metalnessImg), std::move(roughnessImg) };
}

void loadMaterials(const std::vector<fastgltf::Material>& materials, const std::vector<Image>& images, MeshRenderer& meshRenderer) {
Logger::debug("[GltfLoad] Loading " + std::to_string(materials.size()) + " materials...");

meshRenderer.getMaterials().clear();

for (const fastgltf::Material& mat : materials) {
Material& loadedMat = meshRenderer.addMaterial();
RenderShaderProgram& matProgram = loadedMat.getProgram();

matProgram.setAttribute(Vec3f(mat.pbrData.baseColorFactor[0],
mat.pbrData.baseColorFactor[1],
mat.pbrData.baseColorFactor[2]), MaterialAttribute::BaseColor);
matProgram.setAttribute(Vec3f(mat.emissiveFactor[0],
mat.emissiveFactor[1],
mat.emissiveFactor[2]) * mat.emissiveStrength.value_or(1.f), MaterialAttribute::Emissive);
matProgram.setAttribute(mat.pbrData.metallicFactor, MaterialAttribute::Metallic);
matProgram.setAttribute(mat.pbrData.roughnessFactor, MaterialAttribute::Roughness);

if (mat.pbrData.baseColorTexture)
matProgram.setTexture(Texture2D::create(images[mat.pbrData.baseColorTexture->textureIndex]), MaterialTexture::BaseColor);

if (mat.emissiveTexture)
matProgram.setTexture(Texture2D::create(images[mat.emissiveTexture->textureIndex]), MaterialTexture::Emissive);

if (mat.occlusionTexture) { // Ambient occlusion
const Image ambientOcclusionImg = extractAmbientOcclusionImage(images[mat.occlusionTexture->textureIndex]);
matProgram.setTexture(Texture2D::create(ambientOcclusionImg), MaterialTexture::Ambient);
}

if (mat.normalTexture)
matProgram.setTexture(Texture2D::create(images[mat.normalTexture->textureIndex]), MaterialTexture::Normal);

if (mat.pbrData.metallicRoughnessTexture) {
const auto [metalnessImg, roughnessImg] = extractMetalnessRoughnessImages(images[mat.pbrData.metallicRoughnessTexture->textureIndex]);
matProgram.setTexture(Texture2D::create(metalnessImg), MaterialTexture::Metallic);
matProgram.setTexture(Texture2D::create(roughnessImg), MaterialTexture::Roughness);
}

loadedMat.loadType(MaterialType::COOK_TORRANCE);
}

Logger::debug("[GltfLoad] Loaded materials");
}

} // namespace

std::pair<Mesh, MeshRenderer> load(const FilePath& filePath) {
Logger::debug("[GltfLoad] Loading glTF file ('" + filePath + "')...");

if (!FileUtils::isReadable(filePath))
throw std::invalid_argument("Error: The glTF file '" + filePath + "' either does not exist or cannot be opened.");

fastgltf::GltfDataBuffer data;

if (!data.loadFromFile(filePath.getPath()))
throw std::invalid_argument("Error: Could not load the glTF file.");

const FilePath parentPath = filePath.recoverPathToFile();
fastgltf::Expected<fastgltf::Asset> asset(fastgltf::Error::None);

fastgltf::Parser parser;

switch (fastgltf::determineGltfFileType(&data)) {
case fastgltf::GltfType::glTF:
asset = parser.loadGLTF(&data, parentPath.getPath(), fastgltf::Options::LoadExternalBuffers);
break;

case fastgltf::GltfType::GLB:
asset = parser.loadBinaryGLTF(&data, parentPath.getPath(), fastgltf::Options::LoadGLBBuffers);
break;

default:
throw std::invalid_argument("Error: Failed to determine glTF container.");
}

if (asset.error() != fastgltf::Error::None)
throw std::invalid_argument("Error: Failed to load glTF: " + fastgltf::getErrorMessage(asset.error()));

auto [mesh, meshRenderer] = loadMeshes(asset->meshes, asset->buffers, asset->bufferViews, asset->accessors);

const std::vector<Image> images = loadImages(asset->images, parentPath);
loadMaterials(asset->materials, images, meshRenderer);

Logger::debug("[GltfLoad] Loaded glTF file (" + std::to_string(mesh.getSubmeshes().size()) + " submesh(es), "
+ std::to_string(mesh.recoverVertexCount()) + " vertices, "
+ std::to_string(mesh.recoverTriangleCount()) + " triangles, "
+ std::to_string(meshRenderer.getMaterials().size()) + " material(s))");

return { std::move(mesh), std::move(meshRenderer) };
}

} // namespace Raz::GltfFormat
5 changes: 4 additions & 1 deletion src/RaZ/Data/MeshFormat.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "RaZ/Data/FbxFormat.hpp"
#include "RaZ/Data/GltfFormat.hpp"
#include "RaZ/Data/Mesh.hpp"
#include "RaZ/Data/MeshFormat.hpp"
#include "RaZ/Data/ObjFormat.hpp"
Expand All @@ -12,7 +13,9 @@ namespace Raz::MeshFormat {
std::pair<Mesh, MeshRenderer> load(const FilePath& filePath) {
const std::string fileExt = StrUtils::toLowercaseCopy(filePath.recoverExtension().toUtf8());

if (fileExt == "obj") {
if (fileExt == "gltf" || fileExt == "glb") {
return GltfFormat::load(filePath);
} else if (fileExt == "obj") {
return ObjFormat::load(filePath);
} else if (fileExt == "off") {
Mesh mesh = OffFormat::load(filePath);
Expand Down
3 changes: 2 additions & 1 deletion src/RaZ/Data/ObjLoad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,8 @@ std::pair<Mesh, MeshRenderer> load(const FilePath& filePath) {

Logger::debug("[ObjLoad] Loaded OBJ file (" + std::to_string(mesh.getSubmeshes().size()) + " submesh(es), "
+ std::to_string(mesh.recoverVertexCount()) + " vertices, "
+ std::to_string(mesh.recoverTriangleCount()) + " triangles)");
+ std::to_string(mesh.recoverTriangleCount()) + " triangles, "
+ std::to_string(meshRenderer.getMaterials().size()) + " material(s))");

return { std::move(mesh), std::move(meshRenderer) };
}
Expand Down
Loading

0 comments on commit 25bc6ea

Please sign in to comment.