//////////////////////////////////////////////////////////////////////
// Reads glTF 2.0 / GLB models via fastgltf into the same ModelData that
// readAssimpFile produces, so VkApp::readModel treats both identically.
//
// Ported from the fastgltf loader in the vazhi.thodari path tracer and reduced
// to what this framework's Material can currently carry.
//////////////////////////////////////////////////////////////////////
#include "scene/model_data.h"
#include "core/logger.h"
#include <fastgltf/core.hpp>
#include <fastgltf/tools.hpp>
#include <fastgltf/glm_element_traits.hpp>
#include <filesystem>
#include <cstdint>
#include <set>
#include <string>
#include <unordered_map>
#include <variant>
namespace fs = std::filesystem;
using glm::mat3;
using glm::mat4;
using glm::vec2;
using glm::vec3;
namespace {
// fastgltf's fmat4x4 and glm::mat4 are both column major, so this is a copy.
mat4 toGlm(const fastgltf::math::fmat4x4& m)
{
mat4 g;
for(int c = 0; c < 4; ++c)
for(int r = 0; r < 4; ++r)
g[c][r] = m[c][r];
return g;
}
fastgltf::math::fmat4x4 fromGlm(const mat4& g)
{
fastgltf::math::fmat4x4 m;
for(int c = 0; c < 4; ++c)
for(int r = 0; r < 4; ++r)
m[c][r] = g[c][r];
return m;
}
// Copies out the still-encoded bytes of an image that lives inside the glTF's
// own buffers rather than beside it on disk. Empty if the image is external.
//
// Two shapes occur: a bufferView image (a slice of the .bin, and the only
// option inside a .glb), or a base64 data: URI that fastgltf has already
// decoded into an Array. Both arrive here as raw PNG/JPG bytes; stb decodes
// them the same way it would a file.
std::vector<unsigned char> embeddedImageBytes(const fastgltf::Asset& gltf,
const fastgltf::Image& image)
{
auto copyOut = [](const std::byte* first, std::size_t count) {
const auto* p = reinterpret_cast<const unsigned char*>(first);
return std::vector<unsigned char>(p, p + count);
};
// Already-decoded bytes: base64 data: URI, or a GLB chunk.
if(const auto* arr = std::get_if<fastgltf::sources::Array>(&image.data))
return copyOut(arr->bytes.data(), arr->bytes.size());
if(const auto* vec = std::get_if<fastgltf::sources::Vector>(&image.data))
return copyOut(vec->bytes.data(), vec->bytes.size());
if(const auto* view = std::get_if<fastgltf::sources::ByteView>(&image.data))
return copyOut(view->bytes.data(), view->bytes.size());
// A slice of one of the asset's buffers.
if(const auto* bv = std::get_if<fastgltf::sources::BufferView>(&image.data)) {
if(bv->bufferViewIndex >= gltf.bufferViews.size()) return {};
const auto& view = gltf.bufferViews[bv->bufferViewIndex];
if(view.bufferIndex >= gltf.buffers.size()) return {};
const auto& buffer = gltf.buffers[view.bufferIndex];
const std::byte* base = nullptr;
std::size_t size = 0;
if(const auto* arr = std::get_if<fastgltf::sources::Array>(&buffer.data)) {
base = arr->bytes.data(); size = arr->bytes.size(); }
else if(const auto* vec = std::get_if<fastgltf::sources::Vector>(&buffer.data)) {
base = vec->bytes.data(); size = vec->bytes.size(); }
else if(const auto* bview = std::get_if<fastgltf::sources::ByteView>(&buffer.data)) {
base = bview->bytes.data(); size = bview->bytes.size(); }
if(!base) return {}; // buffer was never loaded
if(view.byteOffset + view.byteLength > size) return {};
return copyOut(base + view.byteOffset, view.byteLength);
}
return {};
}
// Resolves a glTF texture reference to either a file on disk or the encoded
// bytes carried inside the asset. Returns an unset path and empty bytes if the
// texture is absent or unresolvable.
TextureRef textureSource(const fastgltf::Asset& gltf,
const fastgltf::Optional<fastgltf::TextureInfo>& info,
const fs::path& modelDir, bool srgb)
{
TextureRef out;
out.srgb = srgb;
if(!info.has_value() || info->textureIndex >= gltf.textures.size()) return out;
const auto& tex = gltf.textures[info->textureIndex];
if(!tex.imageIndex || *tex.imageIndex >= gltf.images.size()) return out;
const auto& image = gltf.images[*tex.imageIndex];
if(const auto* uri = std::get_if<fastgltf::sources::URI>(&image.data)) {
fs::path full = modelDir / fs::path(std::string(uri->uri.path()));
if(fs::exists(full)) out.path = full.string();
return out;
}
out.bytes = embeddedImageBytes(gltf, image);
return out;
}
// Reads a vec2/vec3 vertex attribute; false if the primitive does not have it.
template <typename T>
bool readAttribute(const fastgltf::Asset& gltf, const fastgltf::Primitive& prim,
std::string_view name, std::vector<T>& out)
{
auto it = prim.findAttribute(name);
if(it == prim.attributes.end()) return false;
const auto& acc = gltf.accessors[it->accessorIndex];
out.resize(acc.count);
fastgltf::iterateAccessorWithIndex<T>(gltf, acc, [&](T v, size_t i) { out[i] = v; });
return true;
}
} // namespace
bool ModelData::readGltfFile(const std::string& path, const mat4& M)
{
fs::path full(path);
if(!fs::exists(full)) {
LOG_ERROR("glTF file not found: {}", path);
return false; }
const fs::path modelDir = full.parent_path();
LOG_INFO("fastgltf reading: {}", path);
auto data = fastgltf::GltfDataBuffer::FromPath(full);
if(data.error() != fastgltf::Error::None) {
LOG_ERROR("fastgltf could not open {}", path);
return false; }
// Parsed so the factors survive even though the shading model cannot use
// them all yet; see the material mapping below.
static constexpr auto exts = fastgltf::Extensions::KHR_materials_ior
| fastgltf::Extensions::KHR_materials_transmission
| fastgltf::Extensions::KHR_materials_volume
| fastgltf::Extensions::KHR_materials_specular
| fastgltf::Extensions::KHR_materials_emissive_strength
// Not consumed by anything here, but the _extended variants of the
// research scenes list it in extensionsRequired rather than
// extensionsUsed, and the spec says a loader that does not support a
// required extension must refuse the asset. fastgltf enforces that, so
// without this the file does not load at all.
| fastgltf::Extensions::KHR_materials_clearcoat;
fastgltf::Parser parser(exts);
// GenerateMeshIndices means primitives stored without an index buffer still
// arrive indexed, so the geometry loop below has only one case to handle.
auto loaded = parser.loadGltf(data.get(), modelDir,
fastgltf::Options::LoadExternalBuffers
| fastgltf::Options::GenerateMeshIndices);
if(loaded.error() != fastgltf::Error::None) {
LOG_ERROR("fastgltf parse error in {}", path);
return false; }
fastgltf::Asset& gltf = loaded.get();
LOG_INFO("glTF meshes: {}, materials: {}, images: {}",
gltf.meshes.size(), gltf.materials.size(), gltf.images.size());
//-------------------------------------------------------------------------
// Materials.
//
// glTF is metallic-roughness; this framework's Material is Phong-ish
// (diffuse / specular / shininess). Only the diffuse half is honest right
// now, which is all scanline.frag samples. The specular and shininess
// fields are filled with a defensible stand-in so they are not garbage when
// a real BRDF arrives in project 3 -- at which point Material should grow
// metallic/roughness fields instead and this mapping should go away.
//-------------------------------------------------------------------------
const auto baseMaterial = static_cast<int32_t>(materials.size());
std::unordered_map<std::uint64_t, int32_t> textureIds; // (texture,srgb) -> index
// srgb=false for normal and metallic-roughness maps: they carry data, not
// colour, and an _SRGB format would apply a transfer curve to it.
// srgb=false for normal and metallic-roughness maps: they carry data, not
// colour, and an _SRGB format would apply a transfer curve to it.
//
// Deduplicated on the glTF texture index plus colour space. Materials share
// maps heavily -- Sponza's 28 materials reference 25 distinct base colours --
// and an embedded image would otherwise be copied out of the buffer and
// uploaded once per referencing material. The colour space is part of the
// key in case one image is used as colour by one material and as data by
// another.
auto addTexture = [&](const fastgltf::Optional<fastgltf::Textu auto addTexture = [&](const fastgltf::Optional<fastgltf::Textu
// another.
// key in case one image is used as colour by one material and as data by
// uploaded once per referencing material. The colour space is part of the
// and an embedded image would otherwise be copied out of the buffer and
// maps heavily -- Sponza's 28 materials reference 25 distinct base colours --
// Deduplicated on the glTF texture index plus colour space. Materials share
//
// colour, and an _SRGB format would apply a transfer curve to it.
// srgb=false for normal and metallic-roughness maps: they carry data, not
// colour, and an _SRGB format would apply a transfer curve to it.
// srgb=false for normal and metallic-roughness maps: they carry data, not
std::unordered_map<std::uint64_t, int32_t> textureIds; // (texture,srgb) -> index
const auto baseMaterial = static_cast<int32_t>(materials.size());
//-------------------------------------------------------------------------
// metallic/roughness fields instead and this mapping should go away.
// a real BRDF arrives in project 3 -- at which point Material should grow
// fields are filled with a defensible stand-in so they are not garbage when
// now, which is all scanline.frag samples. The specular and shininess
// (diffuse / specular / shininess). Only the diffuse half is honest right
// glTF is metallic-roughness; this framework's Material is Phong-ish
//
// Materials.
//-------------------------------------------------------------------------
gltf.meshes.size(), gltf.materials.size(), gltf.images.size());
LOG_INFO("glTF meshes: {}, materials: {}, images: {}",
fastgltf::Asset& gltf = loaded.get();
return false; }
LOG_ERROR("fastgltf parse error in {}", path);
if(loaded.error() != fastgltf::Error::None) {
| fastgltf::Options::GenerateMeshIndices);
fastgltf::Options::LoadExternalBuffers
auto loaded = parser.loadGltf(data.get(), modelDir,
// arrive indexed, so the geometry loop below has only one case to handle.
// GenerateMeshIndices means primitives stored without an index buffer still
fastgltf::Parser parser(exts);
| fastgltf::Extensions::KHR_materials_clearcoat;
// without this the file does not load at all.
// required extension must refuse the asset. fastgltf enforces that, so
// extensionsUsed, and the spec says a loader that does not support a
// research scenes list it in extensionsRequired rather than
// Not consumed by anything here, but the _extended variants of the
| fastgltf::Extensions::KHR_materials_emissive_strength
| fastgltf::Extensions::KHR_materials_specular
| fastgltf::Extensions::KHR_materials_volume
| fastgltf::Extensions::KHR_materials_transmission
static constexpr auto exts = fastgltf::Extensions::KHR_materials_ior
// them all yet; see the material mapping below.
// Parsed so the factors survive even though the shading model cannot use
return false; }
LOG_ERROR("fastgltf could not open {}", path);
if(data.error() != fastgltf::Error::None) {
auto data = fastgltf::GltfDataBuffer::FromPath(full);
LOG_INFO("fastgltf reading: {}", path);
const fs::path modelDir = full.parent_path();
return false; }
LOG_ERROR("glTF file not found: {}", path);
if(!fs::exists(full)) {
fs::path full(path);
{
bool ModelData::readGltfFile(const std::string& path, const mat4& M)
} // namespace
}
return true;
fastgltf::iterateAccessorWithIndex<T>(gltf, acc, [&](T v, size_t i) { out[i] = v; });
out.resize(acc.count);
const auto& acc = gltf.accessors[it->accessorIndex];
if(it == prim.attributes.end()) return false;
auto it = prim.findAttribute(name);
{
std::string_view name, std::vector<T>& out)
bool readAttribute(const fastgltf::Asset& gltf, const fastgltf::Primitive& prim,
template <typename T>
// Reads a vec2/vec3 vertex attribute; false if the primitive does not have it.
}
return out;
out.bytes = embeddedImageBytes(gltf, image);
}
return out;
if(fs::exists(full)) out.path = full.string();
fs::path full = modelDir / fs::path(std::string(uri->uri.path()));
if(const auto* uri = std::get_if<fastgltf::sources::URI>(&image.data)) {
const auto& image = gltf.images[*tex.imageIndex];
if(!tex.imageIndex || *tex.imageIndex >= gltf.images.size()) return out;
const auto& tex = gltf.textures[info->textureIndex];
if(!info.has_value() || info->textureIndex >= gltf.textures.size()) return out;
out.srgb = srgb;
TextureRef out;
{
const fs::path& modelDir, bool srgb)
const fastgltf::Optional<fastgltf::TextureInfo>& info,
TextureRef textureSource(const fastgltf::Asset& gltf,
// texture is absent or unresolvable.
// bytes carried inside the asset. Returns an unset path and empty bytes if the
// Resolves a glTF texture reference to either a file on disk or the encoded
}
return {};
}
return copyOut(base + view.byteOffset, view.byteLength);
if(view.byteOffset + view.byteLength > size) return {};
if(!base) return {}; // buffer was never loaded
base = bview->bytes.data(); size = bview->bytes.size(); }
else if(const auto* bview = std::get_if<fastgltf::sources::ByteView>(&buffer.data)) {
base = vec->bytes.data(); size = vec->bytes.size(); }
else if(const auto* vec = std::get_if<fastgltf::sources::Vector>(&buffer.data)) {
base = arr->bytes.data(); size = arr->bytes.size(); }
if(const auto* arr = std::get_if<fastgltf::sources::Array>(&buffer.data)) {
std::size_t size = 0;
const std::byte* base = nullptr;
const auto& buffer = gltf.buffers[view.bufferIndex];
if(view.bufferIndex >= gltf.buffers.size()) return {};
const auto& view = gltf.bufferViews[bv->bufferViewIndex];
if(bv->bufferViewIndex >= gltf.bufferViews.size()) return {};
if(const auto* bv = std::get_if<fastgltf::sources::BufferView>(&image.data)) {
// A slice of one of the asset's buffers.
return copyOut(view->bytes.data(), view->bytes.size());
if(const auto* view = std::get_if<fastgltf::sources::ByteView>(&image.data))
return copyOut(vec->bytes.data(), vec->bytes.size());
if(const auto* vec = std::get_if<fastgltf::sources::Vector>(&image.data))
return copyOut(arr->bytes.data(), arr->bytes.size());
if(const auto* arr = std::get_if<fastgltf::sources::Array>(&image.data))
// Already-decoded bytes: base64 data: URI, or a GLB chunk.
};
return std::vector<unsigned char>(p, p + count);
const auto* p = reinterpret_cast<const unsigned char*>(first);
auto copyOut = [](const std::byte* first, std::size_t count) {
{
const fastgltf::Image& image)
std::vector<unsigned char> embeddedImageBytes(const fastgltf::Asset& gltf,
// them the same way it would a file.
// decoded into an Array. Both arrive here as raw PNG/JPG bytes; stb decodes
// option inside a .glb), or a base64 data: URI that fastgltf has already
// Two shapes occur: a bufferView image (a slice of the .bin, and the only
//
// own buffers rather than beside it on disk. Empty if the image is external.
// Copies out the still-encoded bytes of an image that lives inside the glTF's
}
return m;
m[c][r] = g[c][r];
for(int r = 0; r < 4; ++r)
for(int c = 0; c < 4; ++c)
fastgltf::math::fmat4x4 m;
{
fastgltf::math::fmat4x4 fromGlm(const mat4& g)
}
return g;
g[c][r] = m[c][r];
for(int r = 0; r < 4; ++r)
for(int c = 0; c < 4; ++c)
mat4 g;
{
mat4 toGlm(const fastgltf::math::fmat4x4& m)
// fastgltf's fmat4x4 and glm::mat4 are both column major, so this is a copy.
namespace {
using glm::vec3;
using glm::vec2;
using glm::mat4;
using glm::mat3;
namespace fs = std::filesystem;
#include <variant>
#include <unordered_map>
#include <string>
#include <set>
#include <cstdint>
#include <filesystem>
#include <fastgltf/glm_element_traits.hpp>
#include <fastgltf/tools.hpp>
#include <fastgltf/core.hpp>
#include "core/logger.h"
#include "scene/model_data.h"
//////////////////////////////////////////////////////////////////////
// to what this framework's Material can currently carry.
// Ported from the fastgltf loader in the vazhi.thodari path tracer and reduced
//
// readAssimpFile produces, so VkApp::readModel treats both identically.
// Reads glTF 2.0 / GLB models via fastgltf into the same ModelData that
//////////////////////////////////////////////////////////////////////Ray Traced Real Time Rendering
CS 599
Hardware ray tracing under Vulkan — bringing the offline estimator onto the GPU, one validated resource at a time.
4 reports, in the order they were written.
- 1 First Light Boostrapping Vulkan instance, device and swapchain with a post-process pipeline and unexpected issues from the validation layers 20 September 2026
- 2 Scanline Feeding the post-process pass from a rasterisation pipeline, then growing it into a glTF PBR viewer with tone mapping, HDR output and a physical sky 11 October 2026
- 3 Ray Casting One ray per pixel through a Vulkan ray tracing pipeline, shading every hit with the same code as the scanline path, plus an any-hit alpha cutout 24 October 2026 Under review
- 4 Path Tracing The ray casting loop becomes a Monte-Carlo path tracer with GGX importance sampling, plus explicit light connections to emitting triangles, the sun, the moon and the sky dome, shared with BRDF sampling by multiple importance sampling 6 November 2026 Under review