mirror of
https://github.com/minetest/minetest.git
synced 2025-01-24 06:51:35 +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
|
images: .png, .jpg, .tga, (deprecated:) .bmp
|
||||||
sounds: .ogg vorbis
|
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
|
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)
|
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;
|
more modern alternative to the other static model file formats;
|
||||||
it unlocks no special rendering features.
|
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:
|
This means that many glTF features are not supported *yet*, including:
|
||||||
|
|
||||||
* Animation
|
* Animation
|
||||||
|
@ -18,6 +18,14 @@ do
|
|||||||
register_entity("blender_cube", cube_textures)
|
register_entity("blender_cube", cube_textures)
|
||||||
register_entity("blender_cube_scaled", cube_textures)
|
register_entity("blender_cube_scaled", cube_textures)
|
||||||
register_entity("blender_cube_matrix_transform", 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
|
end
|
||||||
register_entity("snow_man", {"gltf_snow_man.png"})
|
register_entity("snow_man", {"gltf_snow_man.png"})
|
||||||
register_entity("spider", {"gltf_spider.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 "vector3d.h"
|
||||||
#include "os.h"
|
#include "os.h"
|
||||||
|
|
||||||
#include "tiniergltf.hpp"
|
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -303,13 +301,11 @@ std::array<f32, N> SelfType::getNormalizedValues(
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The most basic portion of the code base. This tells irllicht if this file has a .gltf extension.
|
|
||||||
*/
|
|
||||||
bool SelfType::isALoadableFileExtension(
|
bool SelfType::isALoadableFileExtension(
|
||||||
const io::path& filename) const
|
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)
|
std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
|
||||||
{
|
{
|
||||||
|
const bool isGlb = core::hasFileExtension(file->getFileName(), "glb");
|
||||||
auto size = file->getSize();
|
auto size = file->getSize();
|
||||||
if (size < 0) // this can happen if `ftell` fails
|
if (size < 0) // this can happen if `ftell` fails
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@ -670,15 +667,11 @@ std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
// We probably don't need this, but add it just to be sure.
|
// We probably don't need this, but add it just to be sure.
|
||||||
buf[size] = '\0';
|
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 {
|
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) {
|
} catch (const std::runtime_error &e) {
|
||||||
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
|
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <json/json.h>
|
#include <json/json.h>
|
||||||
|
#include "util/base64.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <stack>
|
#include <stack>
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -13,7 +16,6 @@
|
|||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include "util/base64.h"
|
|
||||||
|
|
||||||
namespace tiniergltf {
|
namespace tiniergltf {
|
||||||
|
|
||||||
@ -460,7 +462,8 @@ struct Buffer {
|
|||||||
std::optional<std::string> name;
|
std::optional<std::string> name;
|
||||||
std::string data;
|
std::string data;
|
||||||
Buffer(const Json::Value &o,
|
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"]))
|
: byteLength(as<std::size_t>(o["byteLength"]))
|
||||||
{
|
{
|
||||||
check(o.isObject());
|
check(o.isObject());
|
||||||
@ -468,24 +471,32 @@ struct Buffer {
|
|||||||
if (o.isMember("name")) {
|
if (o.isMember("name")) {
|
||||||
name = as<std::string>(o["name"]);
|
name = as<std::string>(o["name"]);
|
||||||
}
|
}
|
||||||
check(o.isMember("uri"));
|
if (glbData.has_value()) {
|
||||||
bool dataURI = false;
|
check(!o.isMember("uri"));
|
||||||
const std::string uri = as<std::string>(o["uri"]);
|
data = *std::move(glbData);
|
||||||
for (auto &prefix : std::array<std::string, 2> {
|
// GLB allows padding, which need not be reflected in the JSON
|
||||||
"data:application/octet-stream;base64,",
|
check(byteLength + 3 >= data.size());
|
||||||
"data:application/gltf-buffer;base64,"
|
check(data.size() >= byteLength);
|
||||||
}) {
|
} else {
|
||||||
if (std::string_view(uri).substr(0, prefix.length()) == prefix) {
|
check(o.isMember("uri"));
|
||||||
auto view = std::string_view(uri).substr(prefix.length());
|
bool dataURI = false;
|
||||||
check(base64_is_valid(view));
|
const std::string uri = as<std::string>(o["uri"]);
|
||||||
data = base64_decode(view);
|
for (auto &prefix : std::array<std::string, 2> {
|
||||||
dataURI = true;
|
"data:application/octet-stream;base64,",
|
||||||
break;
|
"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);
|
data.resize(byteLength);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1093,6 +1104,12 @@ struct Texture {
|
|||||||
};
|
};
|
||||||
template<> Texture as(const Json::Value &o) { return o; }
|
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 {
|
struct GlTF {
|
||||||
std::optional<std::vector<Accessor>> accessors;
|
std::optional<std::vector<Accessor>> accessors;
|
||||||
std::optional<std::vector<Animation>> animations;
|
std::optional<std::vector<Animation>> animations;
|
||||||
@ -1111,12 +1128,10 @@ struct GlTF {
|
|||||||
std::optional<std::vector<Scene>> scenes;
|
std::optional<std::vector<Scene>> scenes;
|
||||||
std::optional<std::vector<Skin>> skins;
|
std::optional<std::vector<Skin>> skins;
|
||||||
std::optional<std::vector<Texture>> textures;
|
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,
|
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"]))
|
: asset(as<Asset>(o["asset"]))
|
||||||
{
|
{
|
||||||
check(o.isObject());
|
check(o.isObject());
|
||||||
@ -1138,7 +1153,8 @@ struct GlTF {
|
|||||||
std::vector<Buffer> bufs;
|
std::vector<Buffer> bufs;
|
||||||
bufs.reserve(b.size());
|
bufs.reserve(b.size());
|
||||||
for (Json::ArrayIndex i = 0; i < b.size(); ++i) {
|
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);
|
check(bufs.size() >= 1);
|
||||||
buffers = std::move(bufs);
|
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[] = {
|
const char *model_ext[] = {
|
||||||
".x", ".b3d", ".obj", ".gltf",
|
".x", ".b3d", ".obj", ".gltf", ".glb",
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
name = removeStringEnd(filename, model_ext);
|
name = removeStringEnd(filename, model_ext);
|
||||||
|
@ -2519,7 +2519,7 @@ bool Server::addMediaFile(const std::string &filename,
|
|||||||
const char *supported_ext[] = {
|
const char *supported_ext[] = {
|
||||||
".png", ".jpg", ".bmp", ".tga",
|
".png", ".jpg", ".bmp", ".tga",
|
||||||
".ogg",
|
".ogg",
|
||||||
".x", ".b3d", ".obj", ".gltf",
|
".x", ".b3d", ".obj", ".gltf", ".glb",
|
||||||
// Custom translation file format
|
// Custom translation file format
|
||||||
".tr",
|
".tr",
|
||||||
NULL
|
NULL
|
||||||
|
@ -87,7 +87,10 @@ SECTION("minimal triangle") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SECTION("blender cube") {
|
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);
|
||||||
REQUIRE(mesh->getMeshBufferCount() == 1);
|
REQUIRE(mesh->getMeshBufferCount() == 1);
|
||||||
SECTION("vertex coordinates are correct") {
|
SECTION("vertex coordinates are correct") {
|
||||||
|
Loading…
Reference in New Issue
Block a user