Add binary glTF (.glb) support

This commit is contained in:
Lars Mueller 2024-09-05 17:16:55 +02:00
parent 486dc3288d
commit 1859470b14
8 changed files with 184 additions and 42 deletions

View File

@ -274,7 +274,7 @@ Accepted formats are:
images: .png, .jpg, .tga, (deprecated:) .bmp
sounds: .ogg vorbis
models: .x, .b3d, .obj, .gltf (Minetest 5.10 or newer)
models: .x, .b3d, .obj, .gltf, .glb (Minetest 5.10 or newer)
Other formats won't be sent to the client (e.g. you can store .blend files
in a folder for convenience, without the risk that such files are transferred)
@ -302,6 +302,9 @@ The glTF model file format for now only serves as a
more modern alternative to the other static model file formats;
it unlocks no special rendering features.
Binary glTF (`.glb`) files are supported and recommended over `.gltf` files
due to their space savings.
This means that many glTF features are not supported *yet*, including:
* Animation

View File

@ -18,6 +18,14 @@ do
register_entity("blender_cube", cube_textures)
register_entity("blender_cube_scaled", cube_textures)
register_entity("blender_cube_matrix_transform", cube_textures)
minetest.register_entity("gltf:blender_cube_glb", {
initial_properties = {
visual = "mesh",
mesh = "gltf_blender_cube.glb",
textures = cube_textures,
backface_culling = true,
},
})
end
register_entity("snow_man", {"gltf_snow_man.png"})
register_entity("spider", {"gltf_spider.png"})

Binary file not shown.

View File

@ -14,8 +14,6 @@
#include "vector3d.h"
#include "os.h"
#include "tiniergltf.hpp"
#include <array>
#include <cstddef>
#include <cstring>
@ -303,13 +301,11 @@ std::array<f32, N> SelfType::getNormalizedValues(
return values;
}
/**
* The most basic portion of the code base. This tells irllicht if this file has a .gltf extension.
*/
bool SelfType::isALoadableFileExtension(
const io::path& filename) const
{
return core::hasFileExtension(filename, "gltf");
return core::hasFileExtension(filename, "gltf") ||
core::hasFileExtension(filename, "glb");
}
/**
@ -663,6 +659,7 @@ void SelfType::MeshExtractor::copyTCoords(
*/
std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
{
const bool isGlb = core::hasFileExtension(file->getFileName(), "glb");
auto size = file->getSize();
if (size < 0) // this can happen if `ftell` fails
return std::nullopt;
@ -671,15 +668,11 @@ std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
return std::nullopt;
// We probably don't need this, but add it just to be sure.
buf[size] = '\0';
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value json;
JSONCPP_STRING err;
if (!reader->parse(buf.get(), buf.get() + size, &json, &err)) {
return std::nullopt;
}
try {
return tiniergltf::GlTF(json);
if (isGlb)
return tiniergltf::readGlb(buf.get(), size);
else
return tiniergltf::readGlTF(buf.get(), size);
} catch (const std::runtime_error &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
return std::nullopt;

View File

@ -1,6 +1,9 @@
#pragma once
#include <json/json.h>
#include "util/base64.h"
#include <cstdint>
#include <functional>
#include <stack>
#include <string>
@ -13,7 +16,6 @@
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
#include "util/base64.h"
namespace tiniergltf {
@ -460,7 +462,8 @@ struct Buffer {
std::optional<std::string> name;
std::string data;
Buffer(const Json::Value &o,
const std::function<std::string(const std::string &uri)> &resolveURI)
const std::function<std::string(const std::string &uri)> &resolveURI,
std::optional<std::string> &&glbData = std::nullopt)
: byteLength(as<std::size_t>(o["byteLength"]))
{
check(o.isObject());
@ -468,6 +471,13 @@ struct Buffer {
if (o.isMember("name")) {
name = as<std::string>(o["name"]);
}
if (glbData.has_value()) {
check(!o.isMember("uri"));
data = *std::move(glbData);
// GLB allows padding, which need not be reflected in the JSON
check(byteLength + 3 >= data.size());
check(data.size() >= byteLength);
} else {
check(o.isMember("uri"));
bool dataURI = false;
const std::string uri = as<std::string>(o["uri"]);
@ -486,6 +496,7 @@ struct Buffer {
if (!dataURI)
data = resolveURI(uri);
check(data.size() >= byteLength);
}
data.resize(byteLength);
}
};
@ -1093,6 +1104,12 @@ struct Texture {
};
template<> Texture as(const Json::Value &o) { return o; }
using UriResolver = std::function<std::string(const std::string &uri)>;
static std::string uriError(const std::string &uri) {
// only base64 data URI support by default
throw std::runtime_error("unsupported URI: " + uri);
}
struct GlTF {
std::optional<std::vector<Accessor>> accessors;
std::optional<std::vector<Animation>> animations;
@ -1111,12 +1128,10 @@ struct GlTF {
std::optional<std::vector<Scene>> scenes;
std::optional<std::vector<Skin>> skins;
std::optional<std::vector<Texture>> textures;
static std::string uriError(const std::string &uri) {
// only base64 data URI support by default
throw std::runtime_error("unsupported URI: " + uri);
}
GlTF(const Json::Value &o,
const std::function<std::string(const std::string &uri)> &resolveURI = uriError)
const UriResolver &resolveUri = uriError,
std::optional<std::string> &&glbData = std::nullopt)
: asset(as<Asset>(o["asset"]))
{
check(o.isObject());
@ -1138,7 +1153,8 @@ struct GlTF {
std::vector<Buffer> bufs;
bufs.reserve(b.size());
for (Json::ArrayIndex i = 0; i < b.size(); ++i) {
bufs.emplace_back(b[i], resolveURI);
bufs.emplace_back(b[i], resolveUri,
i == 0 ? std::move(glbData) : std::nullopt);
}
check(bufs.size() >= 1);
buffers = std::move(bufs);
@ -1354,4 +1370,123 @@ struct GlTF {
}
};
// std::span is C++ 20, so we roll our own little struct here.
template <typename T>
struct Span {
T *ptr;
uint32_t len;
bool empty() const {
return len == 0;
}
T *end() const {
return ptr + len;
}
template <typename U>
Span<U> cast() const {
return {(U *) ptr, len};
}
};
static Json::Value readJson(Span<const char> span) {
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value json;
JSONCPP_STRING err;
if (!reader->parse(span.ptr, span.end(), &json, &err))
throw std::runtime_error("invalid JSON");
return json;
}
inline GlTF readGlb(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
struct Chunk {
uint32_t type;
Span<const uint8_t> span;
};
struct Stream {
Span<const uint8_t> span;
bool eof() const {
return span.empty();
}
void advance(uint32_t n) {
span.len -= n;
span.ptr += n;
}
uint32_t readUint32() {
if (span.len < 4)
throw std::runtime_error("premature EOF");
uint32_t res = 0;
for (int i = 0; i < 4; ++i)
res += span.ptr[i] << (i * 8);
advance(4);
return res;
}
Chunk readChunk() {
const auto chunkLen = readUint32();
if (chunkLen % 4 != 0)
throw std::runtime_error("chunk length must be multiple of 4");
const auto chunkType = readUint32();
auto chunkPtr = span.ptr;
if (span.len < chunkLen)
throw std::runtime_error("premature EOF");
advance(chunkLen);
return {chunkType, {chunkPtr, chunkLen}};
}
};
constexpr uint32_t MAGIC_glTF = 0x46546C67;
constexpr uint32_t MAGIC_JSON = 0x4E4F534A;
constexpr uint32_t MAGIC_BIN = 0x004E4942;
if (len > std::numeric_limits<uint32_t>::max())
throw std::runtime_error("too large");
Stream is{{(const uint8_t *) data, static_cast<uint32_t>(len)}};
const auto magic = is.readUint32();
if (magic != MAGIC_glTF) // glTF
throw std::runtime_error("wrong magic number");
const auto version = is.readUint32();
if (version != 2)
throw std::runtime_error("wrong version");
const auto length = is.readUint32();
if (length != len)
throw std::runtime_error("wrong length");
const auto json = is.readChunk();
if (json.type != MAGIC_JSON)
throw std::runtime_error("expected JSON chunk");
std::optional<std::string> buffer;
if (!is.eof()) {
const auto chunk = is.readChunk();
if (chunk.type == MAGIC_BIN)
buffer = std::string((const char *) chunk.span.ptr, chunk.span.len);
else if (chunk.type == MAGIC_JSON)
throw std::runtime_error("unexpected chunk");
// Ignore all other chunks. We still want to validate that
// 1. These chunks are valid;
// 2. These chunks are *not* JSON or BIN chunks
while (!is.eof()) {
const auto type = is.readChunk().type;
if (type == MAGIC_JSON || type == MAGIC_BIN)
throw std::runtime_error("unexpected chunk");
}
}
return GlTF(readJson(json.span.cast<const char>()), resolveUri, std::move(buffer));
}
inline GlTF readGlTF(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
if (len > std::numeric_limits<uint32_t>::max())
throw std::runtime_error("too large");
return GlTF(readJson({data, static_cast<uint32_t>(len)}), resolveUri);
}
}

View File

@ -827,7 +827,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
}
const char *model_ext[] = {
".x", ".b3d", ".obj", ".gltf",
".x", ".b3d", ".obj", ".gltf", ".glb",
NULL
};
name = removeStringEnd(filename, model_ext);

View File

@ -2465,7 +2465,7 @@ bool Server::addMediaFile(const std::string &filename,
const char *supported_ext[] = {
".png", ".jpg", ".bmp", ".tga",
".ogg",
".x", ".b3d", ".obj", ".gltf",
".x", ".b3d", ".obj", ".gltf", ".glb",
// Custom translation file format
".tr",
NULL

View File

@ -82,7 +82,10 @@ SECTION("minimal triangle") {
}
SECTION("blender cube") {
const auto mesh = loadMesh(model_stem + "blender_cube.gltf");
const auto path = GENERATE(
model_stem + "blender_cube.gltf",
model_stem + "blender_cube.glb");
const auto mesh = loadMesh(path);
REQUIRE(mesh != nullptr);
REQUIRE(mesh->getMeshBufferCount() == 1);
SECTION("vertex coordinates are correct") {