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
|
|
@ -143,16 +143,57 @@ export namespace Crafter {
|
|||
file.read(reinterpret_cast<char*>(pixels), sizeX * sizeY * sizeof(T));
|
||||
}
|
||||
|
||||
// Decodes anything stb_image reads (PNG, JPEG, ...) to 8-bit RGBA, or
|
||||
// to normalized floats when TT is a floating-point component type.
|
||||
template <typename TT>
|
||||
static TextureAsset<T> LoadPNG(fs::path path) {
|
||||
TextureAsset<T> tex;
|
||||
|
||||
std::filesystem::path abs = std::filesystem::absolute(path);
|
||||
|
||||
int sizeX;
|
||||
int sizeY;
|
||||
|
||||
unsigned char* data = stbi_load(abs.string().c_str(), &sizeX, &sizeY, nullptr, 4);
|
||||
if (data == nullptr) {
|
||||
Compression::Fatal(std::format("LoadPNG: {}: {}", abs.string(), stbi_failure_reason()));
|
||||
}
|
||||
if (!FitsSize(sizeX, sizeY)) {
|
||||
stbi_image_free(data);
|
||||
Compression::Fatal(std::format("LoadPNG: {}: {}x{} is more than a texture's 65535 a side", abs.string(), sizeX, sizeY));
|
||||
}
|
||||
return FromRGBA8<TT>(data, sizeX, sizeY);
|
||||
}
|
||||
|
||||
// LoadPNG from an image that is already in memory, e.g. one embedded
|
||||
// in a GLB. An image that doesn't decode comes back as an error value
|
||||
// rather than a Fatal: the caller is reading a larger asset and can
|
||||
// say which part was bad.
|
||||
template <typename TT>
|
||||
static std::expected<TextureAsset<T>, std::string> LoadPNGFromMemory(std::span<const std::byte> encoded) {
|
||||
int sizeX;
|
||||
int sizeY;
|
||||
|
||||
unsigned char* data = stbi_load_from_memory(reinterpret_cast<const stbi_uc*>(encoded.data()),
|
||||
static_cast<int>(encoded.size()), &sizeX, &sizeY, nullptr, 4);
|
||||
if (data == nullptr) {
|
||||
return std::unexpected(std::string(stbi_failure_reason()));
|
||||
}
|
||||
if (!FitsSize(sizeX, sizeY)) {
|
||||
stbi_image_free(data);
|
||||
return std::unexpected(std::format("{}x{} is more than a texture's 65535 a side", sizeX, sizeY));
|
||||
}
|
||||
return FromRGBA8<TT>(data, sizeX, sizeY);
|
||||
}
|
||||
|
||||
private:
|
||||
// sizeX and sizeY hold 16 bits.
|
||||
static bool FitsSize(int sizeX, int sizeY) {
|
||||
return sizeX <= std::numeric_limits<std::uint16_t>::max() && sizeY <= std::numeric_limits<std::uint16_t>::max();
|
||||
}
|
||||
|
||||
// Converts stb's RGBA8 buffer, classifies its opacity and frees it.
|
||||
template <typename TT>
|
||||
static TextureAsset<T> FromRGBA8(unsigned char* data, int sizeX, int sizeY) {
|
||||
TextureAsset<T> tex;
|
||||
|
||||
tex.sizeX = sizeX;
|
||||
tex.sizeY = sizeY;
|
||||
|
|
|
|||
Loading…
Reference in a new issue