//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Asset:GLB; import :Texture; import std; import Crafter.Math; namespace fs = std::filesystem; // glTF 2.0 binary (.glb) loading: the node hierarchy, every mesh's // triangle primitives with their vertex streams and morph targets, skins, // materials, textures and decoded images, cameras, and the lights of // KHR_lights_punctual. The JSON and BIN chunks stay available, for what this // loader doesn't interpret (animations, VRMC_springBone, ...), and so do the // `extras` of the scene, nodes, meshes, materials and lights (Blender writes // custom properties there); parsing those is the caller's business, which // keeps the JSON library out of this interface and out of everything that // imports Crafter.Asset. // // Errors in the file come back as the error of the std::expected rather than // through Compression::Fatal: a GLB is user content, and a bad one shouldn't // take the program down. A file that requires an extension this loader // doesn't implement is an error too, as glTF asks, and so is one that would // decode to more than Load's decode limit: a few bytes of JSON can point a // thousand primitives or images at the same data. Running out of memory // short of the limit isn't an error in the file; it throws std::bad_alloc, // as it would anywhere else. export namespace Crafter { struct GlbNode { std::string name; std::int32_t parent = -1; std::vector children; // The local transform, as TRS unless the file gives a matrix. The // matrix is the 4x4 row-vector form (translation in row 3), which is // glTF's column-major array copied into the rows unchanged. Vector translation = Vector(0, 0, 0); Vector rotation = Vector(0, 0, 0, 1); Vector scale = Vector(1, 1, 1); bool hasMatrix = false; MatrixRowMajor matrix = MatrixRowMajor::Identity(); std::int32_t mesh = -1; std::int32_t skin = -1; std::int32_t camera = -1; // A light of KHR_lights_punctual: GlbAsset::lights. std::int32_t light = -1; // Its mesh's morph target weights, for this node: one per target, or // empty to keep the mesh's. std::vector weights; // The node's extras as JSON text, or empty. std::string extras; }; // A camera looks down its node's -Z, with +Y up. struct GlbCamera { std::string name; bool orthographic = false; // Perspective: the vertical field of view in radians, and the aspect // ratio, or 0 when the file leaves it to the viewport. float yfov = 0.0f; float aspectRatio = 0.0f; // Orthographic: half the view's width and height. float xmag = 0.0f; float ymag = 0.0f; float znear = 0.0f; // 0 for a perspective camera without a far plane. float zfar = 0.0f; }; enum class GlbLightType : std::uint8_t { Directional, Point, Spot, }; // A KHR_lights_punctual light. It sits at its node's origin; a // directional or spot light shines down the node's -Z. struct GlbLight { std::string name; GlbLightType type = GlbLightType::Point; std::array color = {1, 1, 1}; // Candela for a point or spot light, lux for a directional one. float intensity = 1.0f; // Where its light ends, or 0 when it goes on with distance's falloff. float range = 0.0f; // A spot light's cone, from its axis, in radians. float innerConeAngle = 0.0f; float outerConeAngle = std::numbers::pi_v / 4.0f; // The light's extras as JSON text, or empty. std::string extras; }; // What a morph target adds to each vertex of its primitive, times the // target's weight. Streams the target doesn't move stay empty. struct GlbMorphTarget { std::vector> positions; std::vector> normals; // Added to the tangents' xyz; the handedness in w stays. std::vector> tangents; }; // One triangle list. Streams the primitive doesn't have stay empty; // otherwise each has one entry per vertex. Strips and fans come back as // lists. Points and lines aren't read: their primitives come back with no // vertices, so the rest keep the file's numbering. struct GlbPrimitive { std::vector> positions; std::vector> normals; // xyz plus the bitangent's handedness in w. Where the file has none // and the primitive has normals, computed from the UV set its // material's normal texture samples (TEXCOORD_0 without one), as glTF // asks. std::vector> tangents; std::vector> uv0; // TEXCOORD_1: a second UV set, for the textures whose GlbTextureRef // says texCoord 1. std::vector> uv1; // Up to four joints a vertex, their weights summing to 1. Where the // file gives more (JOINTS_1 and WEIGHTS_1, ...), the four heaviest, // a joint named twice counted once; where it gives a vertex no weight // at all, all of it on the vertex's first joint, as three.js does. A // slot left over holds joint 0 with weight 0. std::vector> joints; std::vector> weights; // 0..n-1 when the primitive isn't indexed. std::vector indices; std::vector targets; std::int32_t material = -1; }; struct GlbMesh { std::string name; std::vector primitives; // The weights of the morph targets its primitives share, one per // target; 0 where the file gives none. A node can override them. std::vector weights; // The mesh's extras as JSON text, or empty. Blender and three.js // write the morph targets' names there, as targetNames. std::string extras; }; struct GlbSkin { std::string name; std::vector joints; // One per joint, in the 4x4 row-vector form like GlbNode::matrix; // identity when the file leaves them out. std::vector> inverseBindMatrices; std::int32_t skeleton = -1; }; enum class GlbAlphaMode : std::uint8_t { Opaque, Mask, Blend, }; struct GlbTextureRef { std::int32_t texture = -1; // The UV set the texture is sampled with: 0 for uv0, 1 for uv1. Every // primitive whose material samples a set has it. std::uint32_t texCoord = 0; // normalTexture's scale or occlusionTexture's strength. float scale = 1.0f; }; struct GlbMaterial { std::string name; std::array baseColorFactor = {1, 1, 1, 1}; float metallicFactor = 1.0f; float roughnessFactor = 1.0f; std::array emissiveFactor = {0, 0, 0}; GlbTextureRef baseColorTexture; GlbTextureRef metallicRoughnessTexture; GlbTextureRef normalTexture; GlbTextureRef occlusionTexture; GlbTextureRef emissiveTexture; GlbAlphaMode alphaMode = GlbAlphaMode::Opaque; float alphaCutoff = 0.5f; bool doubleSided = false; // KHR_materials_emissive_strength: emissiveFactor's multiplier. float emissiveStrength = 1.0f; // KHR_materials_transmission: how much light passes through the // surface, times the texture's red channel. float transmissionFactor = 0.0f; GlbTextureRef transmissionTexture; // KHR_materials_ior. float ior = 1.5f; // The material's extras as JSON text, or empty. std::string extras; }; // The OpenGL filter and wrap enums, as in the file. The filters are 0 // when the file leaves them to the renderer; the wrap modes default to // REPEAT, 10497, as glTF's do. struct GlbSampler { std::uint32_t magFilter = 0; std::uint32_t minFilter = 0; std::uint32_t wrapS = 10497; std::uint32_t wrapT = 10497; }; struct GlbTexture { std::int32_t image = -1; std::int32_t sampler = -1; }; struct GlbAsset { std::vector nodes; // The root nodes of the default scene. std::vector sceneRoots; std::vector meshes; std::vector skins; std::vector materials; std::vector textures; std::vector samplers; // One per image in the file, in RGBA8. An image in a format other // than PNG and JPEG (the WebP or KTX2 of EXT_texture_webp or // KHR_texture_basisu) isn't decoded and stays empty, sizeX 0; no // texture's image is one. std::vector>> images; std::vector cameras; std::vector lights; // The default scene's extras as JSON text, or empty. std::string extras; // The JSON chunk, verbatim. std::string json; // The BIN chunk, verbatim: the data of what the JSON describes and // this loader doesn't read (animations, ...). std::vector bin; // What Load decodes (vertices, indices and pixels) is held to // `decodeLimit` bytes; without one, to 1 GiB plus 64 bytes for each // byte of the file. static std::expected Load(const fs::path& path, std::optional decodeLimit = std::nullopt); static std::expected Load(std::span glb, std::optional decodeLimit = std::nullopt); }; }