Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/pr-changelogs/3321.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Caveats
* existing scripts for GLTF/GLB models using the `s3ocompat = true` flag will
see the Y and Z axes swapped for animations, adjust them or set the flag to false.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ The list of supported key-values is following:
| `radius` | `float` | The radius of the model, by default calculated based on the model dimensions |
| `fliptextures` | `boolean` | Whether to flip the supplied `tex1` / `tex2` (if supported by the texture types). `False` by default |
| `invertteamcolor` | `boolean` | Whether to inverse the teamcolor in `tex1` (if supported by the texture types). `False` by default |
| `s3ocompat` | `boolean` | Transform the model such that its animation and worldspace position matches those of s3o (left-hand coordinate system). `False` by default. Very experimental |
| `s3ocompat` | `boolean` | Select the source-facing convention used by models imported through the S3O Blender workflow. It changes model facing, but both modes load piece data and animation axes into the engine coordinate frame. `False` by default |

> [!NOTE]
> You can export any additional attributes not listed above, there's no harm in that
Expand Down Expand Up @@ -98,12 +98,27 @@ In order to load the GLTF model, make sure it resides somewhere in `Objects3d` d

### S3O Compat

If you export your existing s3o model as GLTF and reuse the existing animation, made for s3o model and you see your units are walking "backwards", make sure to instruct the importer to try to rotate the model in s3o compatible way (define `s3ocompat` to `true`).
If you import an existing S3O into Blender and export it as GLTF, define
`s3ocompat` as `true`. This selects the same source-facing conversion as the S3O
Blender tools: Blender `(x, y, z)` becomes engine `(-x, z, y)`.

By default, GLTF is imported in the same (right handed) coordinate system as Collada / `.dae` and thus require changes to the animation scripts.
The default convention preserves the historical GLTF facing conversion
`(x, z, -y)`. Both conventions are baked into vertices, piece offsets, node rest
rotations, normals, and tangents while the model loads. The resulting hierarchy is
always in the engine model coordinate frame.

BOS and LUS piece animation axes are therefore the same as for S3O. Do not remap
script axes based on whether the model uses `.s3o`, `.gltf`, or `.glb`, and do not
use BARScriptCompiler's deprecated GLTF axis-remapping flags or macros.

`midpos`, `mins`, and `maxs` metadata values are interpreted directly in engine
model coordinates. They are not converted from Blender's Z-up source frame.

![image](blender-gltf-9.png)

### Nodes hierarchy

The importer will create a few empty top level pieces to accomodate for rotations and other nuances of the importer. This is usually completely transparent to the game devs. This might be optimized later on.
The importer creates an empty scene piece above the exported node hierarchy. The
piece has an identity transform; coordinate conversion is applied to the imported
piece data rather than hidden in this ancestor. Additional empty nodes exported by
Blender remain ordinary model pieces.
47 changes: 47 additions & 0 deletions rts/Rendering/Models/GLTFModelAxisConversion.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* This file is part of the Recoil engine (GPL v2 or later), see LICENSE.html */

#pragma once

#include "System/Transform.hpp"

namespace gltfmodel {

enum class SourceConvention {
Default,
S3OCompatible,
};

constexpr SourceConvention GetSourceConvention(bool s3oCompatible)
{
return s3oCompatible ? SourceConvention::S3OCompatible : SourceConvention::Default;
}

// Recoil's GLTF pipeline keeps Blender's Z-up coordinates in the file.
// Convert those coordinates into the Y-up engine model frame. The two
// conventions differ only in the source model's forward direction.
constexpr float3 ToEngineSpace(const float3& v, SourceConvention convention)
{
if (convention == SourceConvention::S3OCompatible)
return float3{-v.x, v.z, v.y};

return float3{v.x, v.z, -v.y};
}

// For a proper basis rotation qBasis, q' = qBasis * q * inverse(qBasis).
// Quaternion conjugation rotates the imaginary part and preserves the real
// part, so the exact signed swizzle avoids needless floating-point noise.
constexpr CQuaternion ToEngineSpace(const CQuaternion& q, SourceConvention convention)
{
const float3 imaginary = ToEngineSpace(float3{q.x, q.y, q.z}, convention);
return CQuaternion{imaginary, q.r};
}

constexpr Transform ToEngineSpace(const Transform& transform, SourceConvention convention)
{
return Transform{
ToEngineSpace(transform.r, convention),
ToEngineSpace(transform.t, convention),
transform.s,
};
}
}
93 changes: 64 additions & 29 deletions rts/Rendering/Models/GLTFParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "GLTFParser.h"
#include "3DModel.hpp"
#include "3DModelLog.h"
#include "GLTFModelAxisConversion.hpp"
#include "ModelUtils.h"

#include "Lua/LuaParser.h"
Expand Down Expand Up @@ -42,19 +43,34 @@ namespace Impl {
return Transform{ r, t, s.x };
}

Transform GetNodeTransform(const fastgltf::Node& node) {
return fastgltf::visit_exhaustive(fastgltf::visitor{
Transform GetNodeTransform(
const fastgltf::Node& node,
gltfmodel::SourceConvention sourceConvention
) {
const Transform sourceTransform = fastgltf::visit_exhaustive(fastgltf::visitor{
[](const fastgltf::math::fmat4x4& matrix) {
return Impl::MatrixToTransform(matrix);
},
[](const fastgltf::TRS& trs) {
return Impl::TRStoTransform(trs);
}
}, node.transform);

return gltfmodel::ToEngineSpace(sourceTransform, sourceConvention);
}

template<typename PrimContainer>
void ReadGeometryData(const fastgltf::Asset& asset, const PrimContainer& primitives, std::vector<SVertexData>& verts, std::vector<uint32_t>& indcs, size_t nodeIdx, const fastgltf::Skin* skinPtr = nullptr) {
void ReadGeometryData(
const fastgltf::Asset& asset,
const PrimContainer& primitives,
std::vector<SVertexData>& verts,
std::vector<uint32_t>& indcs,
size_t nodeIdx,
gltfmodel::SourceConvention sourceConvention,
const fastgltf::Skin* skinPtr = nullptr
) {
const size_t firstVertSize = verts.size();

for (const auto& prim : primitives) {
const size_t prevVertSize = verts.size();
const size_t prevIndcSize = indcs.size();
Expand Down Expand Up @@ -197,6 +213,15 @@ namespace Impl {

if (!seenTangents)
ModelUtils::CalculateTangents(verts, indcs);

}

for (size_t i = firstVertSize; i < verts.size(); ++i) {
auto& vert = verts[i];
vert.pos = gltfmodel::ToEngineSpace(vert.pos, sourceConvention);
vert.normal = gltfmodel::ToEngineSpace(vert.normal, sourceConvention);
vert.sTangent = gltfmodel::ToEngineSpace(vert.sTangent, sourceConvention);
vert.tTangent = gltfmodel::ToEngineSpace(vert.tTangent, sourceConvention);
}
}

Expand Down Expand Up @@ -312,7 +337,7 @@ namespace Impl {
// TODO guess the texture?
}

auto GetModelTransforms(const fastgltf::Asset& asset, std::size_t sceneIndex, const Transform& sceneTransform = Transform{}) {
auto GetModelTransforms(const fastgltf::Asset& asset, std::size_t sceneIndex, gltfmodel::SourceConvention sourceConvention) {
auto& scene = asset.scenes[sceneIndex];

spring::unordered_map<size_t, Transform> transforms(asset.nodes.size());
Expand All @@ -322,7 +347,7 @@ namespace Impl {

const auto& [it, noDup] = transforms.emplace(
nodeIdx,
parentTransform * GetNodeTransform(node)
parentTransform * GetNodeTransform(node, sourceConvention)
);
assert(noDup);

Expand All @@ -332,7 +357,7 @@ namespace Impl {
};

for (auto& node : scene.nodeIndices) {
function(function, node, sceneTransform);
function(function, node, Transform{});
}

return transforms;
Expand Down Expand Up @@ -444,14 +469,16 @@ void CGLTFParser::Load(S3DModel& model, const std::string& modelFilePath)
model.mins = DEF_MIN_SIZE;
model.maxs = DEF_MAX_SIZE;

// GLTF model MUST be exported with Z axis UP. We will rotate it here by ourselves
const auto initTransform = (optionalModelParams.s3oCompat.value_or(false)) ?
Transform(CQuaternion(0, -math::HALFSQRT2, -math::HALFSQRT2, 0)): // Rotate so xyz ==> (-x,z, y)
Transform(CQuaternion( math::HALFSQRT2, 0, 0, -math::HALFSQRT2)); // Rotate so xyz ==> ( x,z,-y)
// GLTF source coordinates are converted into the engine model frame at the
// parser boundary. Keeping the synthetic scene root unrotated makes script
// animation axes identical to those of S3O pieces.
const auto sourceConvention = gltfmodel::GetSourceConvention(
optionalModelParams.s3oCompat.value_or(false)
);

const auto defaultSceneIdx = asset.defaultScene.value_or(0);

auto* rootPiece = AllocRootEmptyPiece(&model, initTransform, asset, defaultSceneIdx);
auto* rootPiece = AllocRootEmptyPiece(&model, asset, defaultSceneIdx, sourceConvention);
model.FlattenPieceTree(rootPiece);

spring::unordered_map<size_t, size_t> nodeIdxToPieceIdx;
Expand All @@ -465,7 +492,7 @@ void CGLTFParser::Load(S3DModel& model, const std::string& modelFilePath)

// conceptually almost the same as AllocRootEmptyPiece + SetPieceMatrices
// except this one doesn't ignore nodes with skinned meshes
const auto modelTransforms = Impl::GetModelTransforms(asset, defaultSceneIdx, initTransform);
const auto modelTransforms = Impl::GetModelTransforms(asset, defaultSceneIdx, sourceConvention);

std::vector<Skinning::SkinnedMesh> allSkinnedMeshes;

Expand All @@ -481,7 +508,7 @@ void CGLTFParser::Load(S3DModel& model, const std::string& modelFilePath)
const auto& mesh = asset.meshes[*node.meshIndex];
auto& skinnedMesh = allSkinnedMeshes.emplace_back();

Impl::ReadGeometryData(asset, mesh.primitives, skinnedMesh.verts, skinnedMesh.indcs, ni, &skin);
Impl::ReadGeometryData(asset, mesh.primitives, skinnedMesh.verts, skinnedMesh.indcs, ni, sourceConvention, &skin);
Impl::TransformSkinsToModelSpace(skinnedMesh.verts, ni, modelTransforms);
Impl::ReplaceNodeIndexWithPieceIndex(skinnedMesh.verts, nodeIdxToPieceIdx);
}
Expand Down Expand Up @@ -544,7 +571,12 @@ GLTFPiece* CGLTFParser::AllocPiece()
return &piecePool[numPoolPieces++];
}

GLTFPiece* CGLTFParser::AllocRootEmptyPiece(S3DModel* model, const Transform& parentTransform, const fastgltf::Asset& asset, size_t sceneIndex)
GLTFPiece* CGLTFParser::AllocRootEmptyPiece(
S3DModel* model,
const fastgltf::Asset& asset,
size_t sceneIndex,
gltfmodel::SourceConvention sourceConvention
)
{
const auto& scene = asset.scenes[sceneIndex];

Expand All @@ -553,21 +585,17 @@ GLTFPiece* CGLTFParser::AllocRootEmptyPiece(S3DModel* model, const Transform& pa

piece->SetParentModel(model);

auto bakedTransform = parentTransform;
// only rotation is allowed because of Spring-isms
bakedTransform.t = float3{};
bakedTransform.s = 1.0f;
piece->SetBakedTransform(bakedTransform); // bakedRotAngles are not read or supported for GLTF
piece->offset = parentTransform.t;
piece->scale = parentTransform.s;
piece->SetBakedTransform(Transform{});
piece->offset = float3{};
piece->scale = 1.0f;


piece->parent = nullptr;
piece->name = scene.name;
piece->children.reserve(scene.nodeIndices.size());

for (const auto childNodeIndex : scene.nodeIndices) {
auto* childPiece = LoadPiece(model, piece, asset, childNodeIndex);
auto* childPiece = LoadPiece(model, piece, asset, childNodeIndex, sourceConvention);
if (childPiece)
piece->children.push_back(childPiece);
}
Expand All @@ -576,7 +604,13 @@ GLTFPiece* CGLTFParser::AllocRootEmptyPiece(S3DModel* model, const Transform& pa
}


GLTFPiece* CGLTFParser::LoadPiece(S3DModel* model, GLTFPiece* parentPiece, const fastgltf::Asset& asset, size_t nodeIndex)
GLTFPiece* CGLTFParser::LoadPiece(
S3DModel* model,
GLTFPiece* parentPiece,
const fastgltf::Asset& asset,
size_t nodeIndex,
gltfmodel::SourceConvention sourceConvention
)
{
const auto& node = asset.nodes[nodeIndex];

Expand All @@ -593,19 +627,20 @@ GLTFPiece* CGLTFParser::LoadPiece(S3DModel* model, GLTFPiece* parentPiece, const
piece->children.reserve(node.children.size());
piece->nodeIndex = nodeIndex;

Transform pieceTransform = Impl::GetNodeTransform(node);
Transform pieceTransform = Impl::GetNodeTransform(node, sourceConvention);

auto bakedTransform = pieceTransform;
// by idiotic Spring convention bakedTransform should only contain rotation
// Piece translation and scale are stored separately; bakedTransform contains
// only the artist-authored rest rotation expressed in engine axes.
bakedTransform.t = float3{};
bakedTransform.s = 1.0f;

piece->SetBakedTransform(bakedTransform); // bakedRotAngles are not read or supported for GLTF
piece->SetBakedTransform(bakedTransform);
piece->offset = pieceTransform.t;
piece->scale = pieceTransform.s;

for (const auto childNodeIndex : node.children) {
auto* childPiece = LoadPiece(model, piece, asset, childNodeIndex);
auto* childPiece = LoadPiece(model, piece, asset, childNodeIndex, sourceConvention);
if (childPiece)
piece->children.push_back(childPiece);
}
Expand All @@ -616,7 +651,7 @@ GLTFPiece* CGLTFParser::LoadPiece(S3DModel* model, GLTFPiece* parentPiece, const
auto& verts = piece->GetVerticesVec();
auto& indcs = piece->GetIndicesVec();
const auto& mesh = asset.meshes[*node.meshIndex];
Impl::ReadGeometryData(asset, mesh.primitives, verts, indcs, nodeIndex, nullptr);
Impl::ReadGeometryData(asset, mesh.primitives, verts, indcs, nodeIndex, sourceConvention, nullptr);

return piece;
}
}
19 changes: 16 additions & 3 deletions rts/Rendering/Models/GLTFParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ namespace fastgltf {
class Asset;
}

struct Transform;
namespace gltfmodel {
enum class SourceConvention;
}

struct GLTFPiece : public S3DModelPiece {
static constexpr size_t INVALID_NODE_INDEX = size_t(-1);
Expand All @@ -30,8 +32,19 @@ class CGLTFParser: public IModelParser
void Load(S3DModel& model, const std::string& name) override;
private:
GLTFPiece* AllocPiece();
GLTFPiece* AllocRootEmptyPiece(S3DModel* model, const Transform& parentTransform, const fastgltf::Asset& asset, size_t sceneIndex);
GLTFPiece* LoadPiece(S3DModel* model, GLTFPiece* parentPiece, const fastgltf::Asset& asset, size_t nodeIndex);
GLTFPiece* AllocRootEmptyPiece(
S3DModel* model,
const fastgltf::Asset& asset,
size_t sceneIndex,
gltfmodel::SourceConvention sourceConvention
);
GLTFPiece* LoadPiece(
S3DModel* model,
GLTFPiece* parentPiece,
const fastgltf::Asset& asset,
size_t nodeIndex,
gltfmodel::SourceConvention sourceConvention
);

std::vector<GLTFPiece> piecePool;
spring::mutex poolMutex;
Expand Down