feat(glb): a loader for binary glTF, and images decoded from memory
- Crafter.Asset:GLB. GlbAsset::Load reads a .glb from a file or a buffer:
- nodes, with their parents;
- every mesh's primitives: positions, normals, UV0, tangents (computed
when absent), joints, weights (normalized), u32 indices;
- skins with their inverse-bind matrices;
- materials: PBR, emissive strength, transmission, ior, alpha mode;
- images decoded to RGBA8, and samplers;
- cameras and KHR_lights_punctual lights;
- the raw JSON, and the extras of the scene, nodes, materials and lights.
nlohmann/json 3.12.0 is vendored in lib/; only the implementation sees it.
- A broken file comes back as an error rather than a crash:
- bad magic, truncation, a sparse accessor;
- a chunk, view or accessor past its bounds, checked so that nothing
wraps round, with 32-bit size_t too;
- a stride outside 4..252, or one its elements overlap in;
- a reference to something that doesn't exist, or children that form a
cycle (found stepping through each node once, however deep);
- a path that isn't a file;
- an image larger than a texture's 65535 a side.
- TextureAsset::LoadPNGFromMemory decodes an image already in memory.
LoadPNG refuses images over 65535 a side instead of truncating their size
(in its float path, past the end of the pixels).
- Tests: GLB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dfd7f899cf
commit
c04b7a331a
7 changed files with 27170 additions and 6 deletions
191
interfaces/Crafter.Asset-GLB.cppm
Normal file
191
interfaces/Crafter.Asset-GLB.cppm
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
//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, skins, materials, textures
|
||||
// and decoded images, cameras, and the lights of KHR_lights_punctual. The JSON
|
||||
// chunk stays available as text, for the extensions this loader doesn't
|
||||
// interpret (VRMC_springBone, ...), and so do the `extras` of the scene,
|
||||
// nodes, 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.
|
||||
export namespace Crafter {
|
||||
struct GlbRGBA8 {
|
||||
std::uint8_t r, g, b, a;
|
||||
};
|
||||
|
||||
struct GlbNode {
|
||||
std::string name;
|
||||
std::int32_t parent = -1;
|
||||
std::vector<std::uint32_t> 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<float, 3, 0> translation = Vector<float, 3, 0>(0, 0, 0);
|
||||
Vector<float, 4, 0> rotation = Vector<float, 4, 0>(0, 0, 0, 1);
|
||||
Vector<float, 3, 0> scale = Vector<float, 3, 0>(1, 1, 1);
|
||||
bool hasMatrix = false;
|
||||
MatrixRowMajor<float, 4, 4, 1> matrix = MatrixRowMajor<float, 4, 4, 1>::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;
|
||||
// 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<float, 3> 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<float> / 4.0f;
|
||||
// The light's extras as JSON text, or empty.
|
||||
std::string extras;
|
||||
};
|
||||
|
||||
// One triangle list. Streams the primitive doesn't have stay empty;
|
||||
// otherwise each has one entry per vertex.
|
||||
struct GlbPrimitive {
|
||||
std::vector<Vector<float, 3, 3>> positions;
|
||||
std::vector<Vector<float, 3, 3>> normals;
|
||||
// xyz plus the bitangent's handedness in w. Computed from the UVs when
|
||||
// the file has none but has normals and TEXCOORD_0.
|
||||
std::vector<Vector<float, 4, 4>> tangents;
|
||||
std::vector<Vector<float, 2, 2>> uv0;
|
||||
std::vector<std::array<std::uint16_t, 4>> joints;
|
||||
// Normalized to sum to 1, which the file is meant to do and often
|
||||
// only nearly does.
|
||||
std::vector<std::array<float, 4>> weights;
|
||||
// 0..n-1 when the primitive isn't indexed.
|
||||
std::vector<std::uint32_t> indices;
|
||||
std::int32_t material = -1;
|
||||
};
|
||||
|
||||
struct GlbMesh {
|
||||
std::string name;
|
||||
std::vector<GlbPrimitive> primitives;
|
||||
};
|
||||
|
||||
struct GlbSkin {
|
||||
std::string name;
|
||||
std::vector<std::uint32_t> joints;
|
||||
// One per joint, in the 4x4 row-vector form like GlbNode::matrix;
|
||||
// identity when the file leaves them out.
|
||||
std::vector<MatrixRowMajor<float, 4, 4, 1>> inverseBindMatrices;
|
||||
std::int32_t skeleton = -1;
|
||||
};
|
||||
|
||||
enum class GlbAlphaMode : std::uint8_t {
|
||||
Opaque,
|
||||
Mask,
|
||||
Blend,
|
||||
};
|
||||
|
||||
struct GlbTextureRef {
|
||||
std::int32_t texture = -1;
|
||||
std::uint32_t texCoord = 0;
|
||||
// normalTexture's scale or occlusionTexture's strength.
|
||||
float scale = 1.0f;
|
||||
};
|
||||
|
||||
struct GlbMaterial {
|
||||
std::string name;
|
||||
std::array<float, 4> baseColorFactor = {1, 1, 1, 1};
|
||||
float metallicFactor = 1.0f;
|
||||
float roughnessFactor = 1.0f;
|
||||
std::array<float, 3> 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; 0 when unspecified.
|
||||
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<GlbNode> nodes;
|
||||
// The root nodes of the default scene.
|
||||
std::vector<std::uint32_t> sceneRoots;
|
||||
std::vector<GlbMesh> meshes;
|
||||
std::vector<GlbSkin> skins;
|
||||
std::vector<GlbMaterial> materials;
|
||||
std::vector<GlbTexture> textures;
|
||||
std::vector<GlbSampler> samplers;
|
||||
std::vector<TextureAsset<GlbRGBA8>> images;
|
||||
std::vector<GlbCamera> cameras;
|
||||
std::vector<GlbLight> lights;
|
||||
// The default scene's extras as JSON text, or empty.
|
||||
std::string extras;
|
||||
// The JSON chunk, verbatim.
|
||||
std::string json;
|
||||
|
||||
static std::expected<GlbAsset, std::string> Load(const fs::path& path);
|
||||
static std::expected<GlbAsset, std::string> Load(std::span<const std::byte> glb);
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue