mirror of
https://github.com/minetest/minetest.git
synced 2025-01-08 22:37:32 +01:00
Add binary glTF (.glb) support
This commit is contained in:
parent
7e4919c6ed
commit
521e678d39
@ -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, (since version 5.10:) .gltf, .glb
|
||||
|
||||
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
|
||||
|
@ -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"})
|
||||
|
BIN
games/devtest/mods/gltf/models/gltf_blender_cube.glb
Normal file
BIN
games/devtest/mods/gltf/models/gltf_blender_cube.glb
Normal file
Binary file not shown.
@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -662,6 +658,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;
|
||||
@ -670,15 +667,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;
|
||||
|
@ -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,24 +471,32 @@ struct Buffer {
|
||||
if (o.isMember("name")) {
|
||||
name = as<std::string>(o["name"]);
|
||||
}
|
||||
check(o.isMember("uri"));
|
||||
bool dataURI = false;
|
||||
const std::string uri = as<std::string>(o["uri"]);
|
||||
for (auto &prefix : std::array<std::string, 2> {
|
||||
"data:application/octet-stream;base64,",
|
||||
"data:application/gltf-buffer;base64,"
|
||||
}) {
|
||||
if (std::string_view(uri).substr(0, prefix.length()) == prefix) {
|
||||
auto view = std::string_view(uri).substr(prefix.length());
|
||||
check(base64_is_valid(view));
|
||||
data = base64_decode(view);
|
||||
dataURI = true;
|
||||
break;
|
||||
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"]);
|
||||
for (auto &prefix : std::array<std::string, 2> {
|
||||
"data:application/octet-stream;base64,",
|
||||
"data:application/gltf-buffer;base64,"
|
||||
}) {
|
||||
if (std::string_view(uri).substr(0, prefix.length()) == prefix) {
|
||||
auto view = std::string_view(uri).substr(prefix.length());
|
||||
check(base64_is_valid(view));
|
||||
data = base64_decode(view);
|
||||
dataURI = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dataURI)
|
||||
data = resolveURI(uri);
|
||||
check(data.size() >= byteLength);
|
||||
}
|
||||
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 inline 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(std::string("invalid JSON: ") + err);
|
||||
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)
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -2519,7 +2519,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
|
||||
|
@ -87,7 +87,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);
|
||||
REQUIRE(mesh->getMeshBufferCount() == 1);
|
||||
SECTION("vertex coordinates are correct") {
|
||||
|
Loading…
Reference in New Issue
Block a user