gltf.cpp

cross platform rendering playground

src/asset/gltf.cpp

19.16 KB
#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;
}