#include <cstddef>
#include "asset.h"
#include "core/logger.h"
#include "core/math_rtm.h"
#define CGLTF_IMPLEMENTATION
#include <cgltf.h>
#include <mikktspace.h>
#include <stb_image.h>
#include <filesystem>
#include <vector>
// clang-format off
// both are right handed
// my: right-handed | +X fwd, -Y rgt (+Y lft), +Z up
// gltf: right-handed | +X rgt, +Y up, +Z fwd
// maps gltf axes 1:1 directly into my space
// I think this is pretty common? Unless theres a better solutio?
// For entire level/world geometry exports so global coordinates don't rotate
// props/characters will import facing backwards (looking down -X)
static const mat4 GLTF_TO_ENGINE_PURE = {
0.0f, 1.0f, 0.0f, 0.0f, // col 0: gltf X (rgt) -> my Y (lft)
0.0f, 0.0f, 1.0f, 0.0f, // col 1: gltf Y (up) -> my Z (up)
1.0f, 0.0f, 0.0f, 0.0f, // col 2: gltf Z (fwd) -> my X (fwd)
0.0f, 0.0f, 0.0f, 1.0f
};
// bakes a 180-degree yaw rot (mind both the flips) into the conversion matrix
// aligns assets modeled facing the front-camera in blender to automatically
// face my +X fwd dir ootb preserves winding order (+det)
static const mat4 GLTF_TO_ENGINE_FLIPPED = {
0.0f, -1.0f, 0.0f, 0.0f, // col 0: gltf X (rgt) -> my -Y (flip rgt)
0.0f, 0.0f, 1.0f, 0.0f, // col 1: gltf Y (up) -> my Z
-1.0f, 0.0f, 0.0f, 0.0f, // col 2: gltf Z (fwd) -> my -X (flip fwd)
0.0f, 0.0f, 0.0f, 1.0f
};
// #define GLTF_TO_ENGINE m4_identity()
#define GLTF_TO_ENGINE GLTF_TO_ENGINE_FLIPPED
// #define GLTF_TO_ENGINE GLTF_TO_ENGINE_PURE
// clang-format on
struct MikkUserData {
const std::vector<vec3> *positions;
const std::vector<vec3> *normals;
const std::vector<vec2> *uvs;
const std::vector<u32> *indices;
std::vector<vec4> *tangents_out;
};
static int mikk_get_num_faces(const SMikkTSpaceContext *ctx) {
auto *d = (MikkUserData *)ctx->m_pUserData;
return (int)(d->indices->size() / 3);
}
static int mikk_get_num_vertices_of_face(const SMikkTSpaceContext *, int) {
return 3;
}
static void mikk_get_position(const SMikkTSpaceContext *ctx, f32 pos[], int face, int vert) {
auto *d = (MikkUserData *)ctx->m_pUserData;
u32 idx = (*d->indices)[face * 3 + vert];
pos[0] = (*d->positions)[idx].x;
pos[1] = (*d->positions)[idx].y;
pos[2] = (*d->positions)[idx].z;
}
static void mikk_get_normal(const SMikkTSpaceContext *ctx, f32 nrm[], int face, int vert) {
auto *d = (MikkUserData *)ctx->m_pUserData;
u32 idx = (*d->indices)[face * 3 + vert];
nrm[0] = (*d->normals)[idx].x;
nrm[1] = (*d->normals)[idx].y;
nrm[2] = (*d->normals)[idx].z;
}
static void mikk_get_texcoord(const SMikkTSpaceContext *ctx, f32 uv[], int face, int vert) {
auto *d = (MikkUserData *)ctx->m_pUserData;
u32 idx = (*d->indices)[face * 3 + vert];
uv[0] = (*d->uvs)[idx].x;
uv[1] = (*d->uvs)[idx].y;
}
static void
mikk_set_ts_space_basic(const SMikkTSpaceContext *ctx, const f32 tangent[], const f32 sign, int face, int vert) {
auto *d = (MikkUserData *)ctx->m_pUserData;
u32 idx = (*d->indices)[face * 3 + vert];
(*d->tangents_out)[idx] = vec4(tangent[0], tangent[1], tangent[2], sign);
}
// helpers
static u32 add_texture(AssetScene &out, const cgltf_texture_view &view, const std::string &gltf_dir, bool srgb) {
if (!view.texture) {
return ASSET_INVALID;
}
cgltf_image *img = nullptr;
AssetTexture tex{};
tex.is_srgb = srgb;
if (view.texture->has_basisu) {
img = view.texture->basisu_image;
tex.is_ktx = true;
} else {
img = view.texture->image;
}
if (!img) {
return ASSET_INVALID;
}
if (img->name) {
tex.name = img->name;
}
if (img->uri) {
tex.path = gltf_dir + "/" + img->uri;
} else if (img->buffer_view) {
tex.blob_size = img->buffer_view->size;
tex.blob = new u8[tex.blob_size];
const u8 *src = (const u8 *)img->buffer_view->buffer->data + img->buffer_view->offset;
memcpy(tex.blob, src, tex.blob_size);
}
u32 index = (u32)out.textures.size();
out.textures.push_back(tex);
return index;
}
bool load_gltf_asset(const char *path, AssetScene &out) {
cgltf_options options{};
cgltf_data *data = nullptr;
if (cgltf_parse_file(&options, path, &data) != cgltf_result_success) {
VEL_ERROR("Failed to parse glTF: {}", path);
return false;
}
if (cgltf_load_buffers(&options, data, path) != cgltf_result_success) {
VEL_ERROR("Failed to load glTF buffers: {}", path);
cgltf_free(data);
return false;
}
out = {};
std::filesystem::path gltf_path(path);
std::string gltf_dir = gltf_path.parent_path().generic_string();
// materials
out.materials.resize(data->materials_count);
for (cgltf_size i = 0; i < data->materials_count; ++i) {
const cgltf_material &m = data->materials[i];
AssetMaterial &mat = out.materials[i];
if (m.name) {
mat.name = m.name;
}
mat.double_sided = m.double_sided;
if (m.has_pbr_metallic_roughness) {
const auto &pbr = m.pbr_metallic_roughness;
mat.base_color = vec4(
pbr.base_color_factor[0], pbr.base_color_factor[1], pbr.base_color_factor[2], pbr.base_color_factor[3]
);
mat.metallic = (f32)pbr.metallic_factor;
mat.roughness = (f32)pbr.roughness_factor;
mat.base_color_tex = add_texture(out, pbr.base_color_texture, gltf_dir, true);
mat.mr_tex = add_texture(out, pbr.metallic_roughness_texture, gltf_dir, false);
}
mat.normal_tex = add_texture(out, m.normal_texture, gltf_dir, false);
mat.emissive_tex = add_texture(out, m.emissive_texture, gltf_dir, true);
mat.emissive = vec3((f32)m.emissive_factor[0], (f32)m.emissive_factor[1], (f32)m.emissive_factor[2]);
}
// meshes
out.meshes.resize(data->meshes_count);
out.mesh_names.resize(data->meshes_count);
for (cgltf_size mi = 0; mi < data->meshes_count; ++mi) {
const cgltf_mesh &src_mesh = data->meshes[mi];
AssetMesh &mesh = out.meshes[mi];
mesh.first_submesh = (u32)out.mesh_submeshes.size();
if (src_mesh.name) {
out.mesh_names[mi] = src_mesh.name;
}
for (cgltf_size pi = 0; pi < src_mesh.primitives_count; ++pi) {
const cgltf_primitive &prim = src_mesh.primitives[pi];
if (prim.type != cgltf_primitive_type_triangles) {
continue;
}
std::vector<vec3> positions;
std::vector<vec3> normals;
std::vector<vec2> uvs;
std::vector<vec4> tangents;
std::vector<f32> float_data;
for (cgltf_size ai = 0; ai < prim.attributes_count; ++ai) {
const cgltf_attribute &attr = prim.attributes[ai];
cgltf_accessor *accessor = attr.data;
float_data.resize(accessor->count * cgltf_num_components(accessor->type));
cgltf_accessor_unpack_floats(accessor, float_data.data(), float_data.size());
switch (attr.type) {
case cgltf_attribute_type_position: {
positions.resize(accessor->count);
memcpy(positions.data(), float_data.data(), float_data.size() * sizeof(f32));
} break;
case cgltf_attribute_type_normal: {
normals.resize(accessor->count);
memcpy(normals.data(), float_data.data(), float_data.size() * sizeof(f32));
} break;
case cgltf_attribute_type_texcoord: {
if (attr.index == 0) {
uvs.resize(accessor->count);
memcpy(uvs.data(), float_data.data(), float_data.size() * sizeof(f32));
}
} break;
case cgltf_attribute_type_tangent: {
tangents.resize(accessor->count);
memcpy(tangents.data(), float_data.data(), float_data.size() * sizeof(f32));
// gltf tangents are VEC4 (xyz + sign w), but some
// files store VEC3 (xyz only, no w). In that case
// the w component remains 0.0 from resize()
u32 comps = cgltf_num_components(accessor->type);
if (comps < 4) {
for (auto &t : tangents) {
t.w = 1.0f;
}
}
} break;
default:
break;
}
}
if (positions.empty()) {
continue;
}
// indices
std::vector<u32> indices;
if (prim.indices) {
indices.resize(prim.indices->count);
for (cgltf_size i = 0; i < prim.indices->count; ++i) {
indices[i] = (u32)cgltf_accessor_read_index(prim.indices, i);
}
} else {
indices.resize(positions.size());
for (u32 i = 0; i < positions.size(); ++i) {
indices[i] = i;
}
}
// coordinate conversion
for (usize i = 0; i < positions.size(); ++i) {
vec4 p = transform_point(positions[i], GLTF_TO_ENGINE);
positions[i] = {p.x, p.y, p.z};
if (i < normals.size()) {
normals[i] = transform_direction(normals[i], GLTF_TO_ENGINE).normalized();
}
}
if (!tangents.empty()) {
for (usize i = 0; i < tangents.size(); ++i) {
vec3 t = vec3(tangents[i].x, tangents[i].y, tangents[i].z);
t = transform_direction(t, GLTF_TO_ENGINE).normalized();
tangents[i].x = t.x;
tangents[i].y = t.y;
tangents[i].z = t.z;
}
}
// generate tangents
if (tangents.empty() && !normals.empty() && !uvs.empty()) {
tangents.resize(positions.size(), vec4(0, 0, 0, 1));
MikkUserData user{};
user.positions = &positions;
user.normals = &normals;
user.uvs = &uvs;
user.indices = &indices;
user.tangents_out = &tangents;
SMikkTSpaceInterface iface{};
iface.m_getNumFaces = mikk_get_num_faces;
iface.m_getNumVerticesOfFace = mikk_get_num_vertices_of_face;
iface.m_getPosition = mikk_get_position;
iface.m_getNormal = mikk_get_normal;
iface.m_getTexCoord = mikk_get_texcoord;
iface.m_setTSpaceBasic = mikk_set_ts_space_basic;
SMikkTSpaceContext ctx{};
ctx.m_pInterface = &iface;
ctx.m_pUserData = &user;
genTangSpaceDefault(&ctx);
for (usize i = 0; i < tangents.size(); ++i) {
tangents[i].w *= -1.0f;
}
}
// ---------------------------------------------
// out
AssetGeometry geom;
geom.first_vertex = (u32)out.vertices.size();
geom.vertex_count = (u32)positions.size();
geom.first_index = (u32)out.indices.size();
geom.index_count = (u32)indices.size();
for (usize i = 0; i < positions.size(); ++i) {
Vertex v{};
v.pos = positions[i];
v.normal = (i < normals.size()) ? normals[i] : vec3(0, 0, 1);
v.uv = (i < uvs.size()) ? uvs[i] : vec2(0);
v.tangent = (i < tangents.size()) ? tangents[i] : vec4(1, 0, 0, 1);
out.vertices.push_back(v);
}
out.indices.insert(out.indices.end(), indices.begin(), indices.end());
u32 geom_index = (u32)out.geometries.size();
out.geometries.push_back(geom);
AssetSubmesh submesh{};
submesh.geometry = geom_index;
submesh.material = prim.material ? (u32)(prim.material - data->materials) : ASSET_INVALID;
// Compute submesh bounds from converted vertex positions
{
vec3 min_pos{FLT_MAX, FLT_MAX, FLT_MAX};
vec3 max_pos{-FLT_MAX, -FLT_MAX, -FLT_MAX};
for (u32 idx : indices) {
const vec3 &p = positions[idx];
min_pos.x = std::min(min_pos.x, p.x);
min_pos.y = std::min(min_pos.y, p.y);
min_pos.z = std::min(min_pos.z, p.z);
max_pos.x = std::max(max_pos.x, p.x);
max_pos.y = std::max(max_pos.y, p.y);
max_pos.z = std::max(max_pos.z, p.z);
}
submesh.aabb_min = min_pos;
submesh.aabb_max = max_pos;
vec3 center = (min_pos + max_pos) * 0.5f;
f32 radius = (max_pos - min_pos).length() * 0.5f;
submesh.sphere = vec4(center.x, center.y, center.z, radius);
}
if (prim.material && prim.material->name) {
out.submesh_names.push_back(prim.material->name);
} else {
out.submesh_names.emplace_back();
}
u32 submesh_index = (u32)out.submeshes.size();
out.submeshes.push_back(submesh);
out.mesh_submeshes.push_back(submesh_index);
}
mesh.submesh_count = (u32)out.mesh_submeshes.size() - mesh.first_submesh;
}
// nodes
out.nodes.resize(data->nodes_count);
out.node_names.resize(data->nodes_count);
for (cgltf_size ni = 0; ni < data->nodes_count; ++ni) {
const cgltf_node &src = data->nodes[ni];
AssetNode &dst = out.nodes[ni];
if (src.name) {
out.node_names[ni] = src.name;
}
cgltf_node_transform_local(&src, dst.local.m);
dst.local = inverse(GLTF_TO_ENGINE) * dst.local * GLTF_TO_ENGINE;
if (src.parent) {
dst.parent = (int)(src.parent - data->nodes);
}
if (src.mesh) {
dst.mesh = (u32)(src.mesh - data->meshes);
}
if (src.camera) {
dst.camera = (i32)(src.camera - data->cameras);
}
if (src.light) {
dst.light = (i32)(src.light - data->lights);
}
dst.first_child = (u32)out.child_indices.size();
dst.child_count = (u32)src.children_count;
for (cgltf_size ci = 0; ci < src.children_count; ++ci) {
out.child_indices.push_back((u32)(src.children[ci] - data->nodes));
}
dst.first_instance = (u32)out.instance_locals.size();
dst.instance_count = 0;
if (src.has_mesh_gpu_instancing) {
const cgltf_mesh_gpu_instancing &inst = src.mesh_gpu_instancing;
const cgltf_accessor *transl = nullptr;
const cgltf_accessor *rot = nullptr;
const cgltf_accessor *scale = nullptr;
for (cgltf_size ai = 0; ai < inst.attributes_count; ++ai) {
const cgltf_attribute &attr = inst.attributes[ai];
if (strcmp(attr.name, "TRANSLATION") == 0) {
transl = attr.data;
} else if (strcmp(attr.name, "ROTATION") == 0) {
rot = attr.data;
} else if (strcmp(attr.name, "SCALE") == 0) {
scale = attr.data;
}
}
cgltf_size count = 0;
if (transl) {
count = transl->count;
} else if (rot) {
count = rot->count;
} else if (scale) {
count = scale->count;
}
dst.instance_count = (u32)count;
// gltf -> engine frame conversion (a rotation), applied to TRS directly.
// Exact for uniform/identity scale; non-uniform scale is approximated by
// the TRS decomposition (a TRS cannot represent a rotated non-uniform scale).
const quat frame = mat4_to_trs(GLTF_TO_ENGINE).rotation;
const quat frame_inv = rtm::quat_conjugate(frame.q);
out.instance_locals.reserve(out.instance_locals.size() + count);
for (cgltf_size ii = 0; ii < count; ++ii) {
TRS trs;
if (transl) {
f32 v[3];
cgltf_accessor_read_float(transl, ii, v, 3);
trs.translation = vec3(v[0], v[1], v[2]);
}
if (rot) {
f32 v[4];
cgltf_accessor_read_float(rot, ii, v, 4);
trs.rotation = quat(rtm::quat_normalize(rtm::vector_set(v[0], v[1], v[2], v[3])));
}
if (scale) {
f32 v[3];
cgltf_accessor_read_float(scale, ii, v, 3);
trs.scale = vec3(v[0], v[1], v[2]);
}
trs.translation = transform_direction(trs.translation, GLTF_TO_ENGINE);
trs.rotation = quat(rtm::quat_mul(frame_inv, rtm::quat_mul(trs.rotation.q, frame)));
out.instance_locals.push_back(trs);
}
}
}
// camers
out.cameras.resize(data->cameras_count);
for (cgltf_size ci = 0; ci < data->cameras_count; ++ci) {
const cgltf_camera &src = data->cameras[ci];
AssetCamera &dst = out.cameras[ci];
if (src.type == cgltf_camera_type_perspective) {
dst.type = AssetCamera::Perspective;
dst.yfov = src.data.perspective.yfov;
dst.znear = src.data.perspective.znear;
dst.zfar = src.data.perspective.has_zfar ? src.data.perspective.zfar : 1000.0f;
dst.aspect_ratio = src.data.perspective.has_aspect_ratio ? src.data.perspective.aspect_ratio : 0.0f;
} else {
dst.type = AssetCamera::Orthographic;
dst.yfov = 0.0f;
dst.znear = src.data.orthographic.znear;
dst.zfar = src.data.orthographic.zfar;
}
}
// lights (KHR_lights_punctual)
out.lights.resize(data->lights_count);
for (cgltf_size li = 0; li < data->lights_count; ++li) {
const cgltf_light &src = data->lights[li];
AssetLight &dst = out.lights[li];
if (src.type == cgltf_light_type_directional) {
dst.type = AssetLight::Directional;
} else if (src.type == cgltf_light_type_point) {
dst.type = AssetLight::Point;
dst.range = src.range;
}
dst.color.x = src.color[0];
dst.color.y = src.color[1];
dst.color.z = src.color[2];
dst.intensity = src.intensity;
}
// root nodes
const cgltf_scene *scene = data->scene ? data->scene : &data->scenes[0];
for (cgltf_size i = 0; i < scene->nodes_count; ++i) {
out.root_nodes.push_back((u32)(scene->nodes[i] - data->nodes));
}
VEL_INFO(
"Loaded glTF: {} meshes, {} submeshes, {} materials, {} textures, {} cameras, {} lights",
out.meshes.size(),
out.submeshes.size(),
out.materials.size(),
out.textures.size(),
out.cameras.size(),
out.lights.size()
);
cgltf_free(data);
return true;
}