First functional morph shape export.
- This does not support progressive morphs. Still evaluating whether that's required functionality. It's really just a little bit of more work. An example implementation is available in the SDK samples. - We're blithely ignoring NORMALs and TANGENTs. This almost certainly has to change.
This commit is contained in:
parent
b1076fa373
commit
77470e236e
133
src/Fbx2Raw.cpp
133
src/Fbx2Raw.cpp
|
@ -449,6 +449,91 @@ private:
|
|||
std::vector<Vec4f> vertexJointWeights;
|
||||
};
|
||||
|
||||
class FbxBlendShapesAccess
|
||||
{
|
||||
public:
|
||||
struct Shape {
|
||||
explicit Shape(const std::vector<FbxVector4> &offsets, FbxFloat defaultDeform, const std::vector<FbxAnimCurve *>animations) :
|
||||
offsets(offsets),
|
||||
defaultDeform(defaultDeform),
|
||||
animations(animations)
|
||||
{}
|
||||
const std::vector<FbxVector4> offsets;
|
||||
const FbxDouble defaultDeform;
|
||||
const std::vector<FbxAnimCurve *> animations;
|
||||
};
|
||||
|
||||
explicit FbxBlendShapesAccess(const FbxScene *scene, FbxMesh *mesh) :
|
||||
shapes(extractShapes(scene, mesh))
|
||||
{ }
|
||||
|
||||
size_t GetShapeCount() const { return shapes.size(); }
|
||||
FbxDouble GetDefaultDeform(size_t shapeIx) const {
|
||||
return shapes.at(shapeIx).defaultDeform;
|
||||
}
|
||||
FbxVector4 GetPosition(size_t shapeIx, size_t vertexIx) const {
|
||||
return shapes.at(shapeIx).offsets[vertexIx];
|
||||
}
|
||||
size_t GetAnimCount(size_t shapeIx) const { return shapes.at(shapeIx).animations.size(); }
|
||||
FbxAnimCurve * GetAnimation(size_t shapeIx, size_t animIx) const {
|
||||
return shapes.at(shapeIx).animations[animIx];
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Shape> extractShapes(const FbxScene *scene, FbxMesh *mesh) const {
|
||||
std::vector<Shape> results;
|
||||
// acquire the regular control points from the mesh
|
||||
const FbxVector4 *meshPoints = mesh->GetControlPoints();
|
||||
for (int shapeIx = 0; shapeIx < mesh->GetDeformerCount(FbxDeformer::eBlendShape); shapeIx++) {
|
||||
auto *blendShape = dynamic_cast<FbxBlendShape *>(mesh->GetDeformer(shapeIx, FbxDeformer::eBlendShape));
|
||||
if (blendShape == nullptr) {
|
||||
continue;
|
||||
}
|
||||
for (int channelIx = 0; channelIx < blendShape->GetBlendShapeChannelCount(); ++channelIx) {
|
||||
FbxBlendShapeChannel *channel = blendShape->GetBlendShapeChannel(channelIx);
|
||||
|
||||
if (channel->GetTargetShapeCount() != 1) {
|
||||
if (channel->GetTargetShapeCount() > 1) {
|
||||
fmt::print("Warning: Blend Shape %s is a progressive morph; this is not supported.\n", channel->GetName());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
FbxShape *fbxShape = channel->GetTargetShape(0);
|
||||
assert(fbxShape->GetControlPointsCount() == mesh->GetControlPointsCount());
|
||||
|
||||
// glTF morph target positions are *mutation vectors* to be added (by weight) to the regular mesh positions.
|
||||
// FBX blend shape control points, on the other hand are final positions.
|
||||
// So we must do a little subtracting.
|
||||
std::vector<FbxVector4> offsets;
|
||||
auto shapePoints = fbxShape->GetControlPoints();
|
||||
for (int pointIx = 0; pointIx < mesh->GetControlPointsCount(); pointIx ++) {
|
||||
offsets.push_back(shapePoints[pointIx] - meshPoints[pointIx]);
|
||||
}
|
||||
|
||||
size_t animationCount = static_cast<size_t>(scene->GetSrcObjectCount<FbxAnimStack>());
|
||||
std::vector<FbxAnimCurve *>animations(animationCount);
|
||||
for (int j = 0; j < animationCount; j++) {
|
||||
auto *pAnimStack = scene->GetSrcObject<FbxAnimStack>(j);
|
||||
auto *layer = pAnimStack->GetMember<FbxAnimLayer>(0);
|
||||
if (pAnimStack->GetMemberCount() > 1) {
|
||||
fmt::print("Warning: ignoring animation layers 1+ in stack %s", pAnimStack->GetName());
|
||||
}
|
||||
|
||||
FbxAnimCurve *curve = mesh->GetShapeChannel(shapeIx, channelIx, layer, true);
|
||||
// note that curve can be null here, which is fine
|
||||
animations[j] = curve;
|
||||
}
|
||||
|
||||
results.push_back(Shape(offsets, channel->DeformPercent * 0.01f, animations));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
const std::vector<Shape> shapes;
|
||||
};
|
||||
|
||||
static bool TriangleTexturePolarity(const Vec2f &uv0, const Vec2f &uv1, const Vec2f &uv2)
|
||||
{
|
||||
const Vec2f d0 = uv1 - uv0;
|
||||
|
@ -482,7 +567,7 @@ static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std:
|
|||
{
|
||||
FbxGeometryConverter meshConverter(pScene->GetFbxManager());
|
||||
meshConverter.Triangulate(pNode->GetNodeAttribute(), true);
|
||||
const FbxMesh *pMesh = pNode->GetMesh();
|
||||
FbxMesh *pMesh = pNode->GetMesh();
|
||||
|
||||
const char *meshName = (pNode->GetName()[0] != '\0') ? pNode->GetName() : pMesh->GetName();
|
||||
const int rawSurfaceIndex = raw.AddSurface(meshName, pNode->GetName());
|
||||
|
@ -496,6 +581,7 @@ static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std:
|
|||
const FbxLayerElementAccess<FbxVector2> uvLayer1(pMesh->GetElementUV(1), pMesh->GetElementUVCount());
|
||||
const FbxSkinningAccess skinning(pMesh, pScene, pNode);
|
||||
const FbxMaterialsAccess materials(pMesh, textureLocations);
|
||||
const FbxBlendShapesAccess blendShapes(pScene, pMesh);
|
||||
|
||||
if (verboseOutput) {
|
||||
fmt::printf(
|
||||
|
@ -541,6 +627,11 @@ static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std:
|
|||
rawSurface.jointGeometryMaxs.emplace_back(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
|
||||
rawSurface.defaultShapeDeforms.clear();
|
||||
for (int shapeIx = 0; shapeIx < blendShapes.GetShapeCount(); shapeIx ++) {
|
||||
rawSurface.defaultShapeDeforms.push_back(blendShapes.GetDefaultDeform(shapeIx));
|
||||
}
|
||||
|
||||
int polygonVertexIndex = 0;
|
||||
|
||||
for (int polygonIndex = 0; polygonIndex < pMesh->GetPolygonCount(); polygonIndex++) {
|
||||
|
@ -641,6 +732,12 @@ static void ReadMesh(RawModel &raw, FbxScene *pScene, FbxNode *pNode, const std:
|
|||
|
||||
rawSurface.bounds.AddPoint(vertex.position);
|
||||
|
||||
for (int shapeIx = 0; shapeIx < blendShapes.GetShapeCount(); shapeIx ++) {
|
||||
// extract this vertex' position in this blend shape, transform it as per above, and add it to the vector
|
||||
const Vec3f rawPos = toVec3(transform.MultNormalize(blendShapes.GetPosition(shapeIx, controlPointIndex)));
|
||||
vertex.shapePositions.push_back(rawPos);
|
||||
}
|
||||
|
||||
if (skinning.IsSkinned()) {
|
||||
const int jointIndices[FbxSkinningAccess::MAX_WEIGHTS] = {
|
||||
vertex.jointIndices[0],
|
||||
|
@ -846,12 +943,12 @@ static void ReadAnimations(RawModel &raw, FbxScene *pScene)
|
|||
{
|
||||
FbxTime::EMode eMode = FbxTime::eFrames24;
|
||||
const int animationCount = pScene->GetSrcObjectCount<FbxAnimStack>();
|
||||
for (int i = 0; i < animationCount; i++) {
|
||||
FbxAnimStack *pAnimStack = pScene->GetSrcObject<FbxAnimStack>(i);
|
||||
for (int animIx = 0; animIx < animationCount; animIx++) {
|
||||
FbxAnimStack *pAnimStack = pScene->GetSrcObject<FbxAnimStack>(animIx);
|
||||
FbxString animStackName = pAnimStack->GetName();
|
||||
|
||||
if (verboseOutput) {
|
||||
fmt::printf("animation %d: %s (%d%%)", i, (const char *) animStackName, 0);
|
||||
fmt::printf("animation %d: %s (%d%%)", animIx, (const char *) animStackName, 0);
|
||||
}
|
||||
|
||||
pScene->SetCurrentAnimationStack(pAnimStack);
|
||||
|
@ -884,10 +981,20 @@ static void ReadAnimations(RawModel &raw, FbxScene *pScene)
|
|||
bool hasTranslation = false;
|
||||
bool hasRotation = false;
|
||||
bool hasScale = false;
|
||||
bool hasMorphs = false;
|
||||
|
||||
RawChannel channel;
|
||||
channel.nodeIndex = raw.GetNodeByName(pNode->GetName());
|
||||
|
||||
std::vector<FbxAnimCurve *> shapeAnimCurves;
|
||||
FbxNodeAttribute *nodeAttr = pNode->GetNodeAttribute();
|
||||
if (nodeAttr != nullptr && nodeAttr->GetAttributeType() == FbxNodeAttribute::EType::eMesh) {
|
||||
// it's inelegant to recreate this same access class multiple times, but it's also dirt cheap...
|
||||
FbxBlendShapesAccess blendShapes(pScene, dynamic_cast<FbxMesh *>(nodeAttr));
|
||||
for (size_t shapeIx = 0; shapeIx < blendShapes.GetShapeCount(); shapeIx ++) {
|
||||
shapeAnimCurves.push_back(blendShapes.GetAnimation(shapeIx, animIx));
|
||||
}
|
||||
}
|
||||
for (FbxLongLong frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; frameIndex++) {
|
||||
FbxTime pTime;
|
||||
pTime.SetFrame(frameIndex, eMode);
|
||||
|
@ -915,9 +1022,15 @@ static void ReadAnimations(RawModel &raw, FbxScene *pScene)
|
|||
channel.translations.push_back(toVec3f(localTranslation));
|
||||
channel.rotations.push_back(toQuatf(localRotation));
|
||||
channel.scales.push_back(toVec3f(localScale));
|
||||
|
||||
for (auto curve : shapeAnimCurves) {
|
||||
float weight = curve->Evaluate(pTime) * 0.01; // [0, 100] in FBX, [0, 1] in glTF
|
||||
hasMorphs |= (fabs(weight) > epsilon);
|
||||
channel.weights.push_back(weight);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTranslation || hasRotation || hasScale) {
|
||||
if (hasTranslation || hasRotation || hasScale || hasMorphs) {
|
||||
if (!hasTranslation) {
|
||||
channel.translations.clear();
|
||||
}
|
||||
|
@ -927,16 +1040,20 @@ static void ReadAnimations(RawModel &raw, FbxScene *pScene)
|
|||
if (!hasScale) {
|
||||
channel.scales.clear();
|
||||
}
|
||||
if (!hasMorphs) {
|
||||
channel.weights.clear();
|
||||
}
|
||||
|
||||
animation.channels.emplace_back(channel);
|
||||
|
||||
totalSizeInBytes += channel.translations.size() * sizeof(channel.translations[0]) +
|
||||
channel.rotations.size() * sizeof(channel.rotations[0]) +
|
||||
channel.scales.size() * sizeof(channel.scales[0]);
|
||||
channel.scales.size() * sizeof(channel.scales[0]) +
|
||||
channel.weights.size() * sizeof(channel.weights[0]);
|
||||
}
|
||||
|
||||
if (verboseOutput) {
|
||||
fmt::printf("\ranimation %d: %s (%d%%)", i, (const char *) animStackName, nodeIndex * 100 / nodeCount);
|
||||
fmt::printf("\ranimation %d: %s (%d%%)", animIx, (const char *) animStackName, nodeIndex * 100 / nodeCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -944,7 +1061,7 @@ static void ReadAnimations(RawModel &raw, FbxScene *pScene)
|
|||
|
||||
if (verboseOutput) {
|
||||
fmt::printf(
|
||||
"\ranimation %d: %s (%d channels, %3.1f MB)\n", i, (const char *) animStackName,
|
||||
"\ranimation %d: %s (%d channels, %3.1f MB)\n", animIx, (const char *) animStackName,
|
||||
(int) animation.channels.size(), (float) totalSizeInBytes * 1e-6f);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -333,15 +333,15 @@ ModelData *Raw2Gltf(
|
|||
fmt::printf("Animation '%s' has %lu channels:\n", animation.name.c_str(), animation.channels.size());
|
||||
}
|
||||
|
||||
for (size_t j = 0; j < animation.channels.size(); j++) {
|
||||
const RawChannel &channel = animation.channels[j];
|
||||
for (size_t channelIx = 0; channelIx < animation.channels.size(); channelIx++) {
|
||||
const RawChannel &channel = animation.channels[channelIx];
|
||||
const RawNode &node = raw.GetNode(channel.nodeIndex);
|
||||
|
||||
if (verboseOutput) {
|
||||
fmt::printf(
|
||||
" Channel %lu (%s) has translations/rotations/scales: [%lu, %lu, %lu]\n",
|
||||
j, node.name.c_str(), channel.translations.size(),
|
||||
channel.rotations.size(), channel.scales.size());
|
||||
" Channel %lu (%s) has translations/rotations/scales/weights: [%lu, %lu, %lu, %lu]\n",
|
||||
channelIx, node.name.c_str(), channel.translations.size(), channel.rotations.size(),
|
||||
channel.scales.size(), channel.weights.size());
|
||||
}
|
||||
|
||||
NodeData &nDat = require(nodesByName, node.name);
|
||||
|
@ -354,6 +354,9 @@ ModelData *Raw2Gltf(
|
|||
if (!channel.scales.empty()) {
|
||||
aDat.AddNodeChannel(nDat, *gltf->AddAccessorAndView(buffer, GLT_VEC3F, channel.scales), "scale");
|
||||
}
|
||||
if (!channel.weights.empty()) {
|
||||
aDat.AddNodeChannel(nDat, *gltf->AddAccessorAndView(buffer, {CT_FLOAT, 1, "SCALAR"}, channel.weights), "weights");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -496,7 +499,7 @@ ModelData *Raw2Gltf(
|
|||
mesh = meshIter->second.get();
|
||||
|
||||
} else {
|
||||
auto meshPtr = gltf->meshes.hold(new MeshData(rawSurface.name));
|
||||
auto meshPtr = gltf->meshes.hold(new MeshData(rawSurface.name, rawSurface.defaultShapeDeforms));
|
||||
meshByNodeName[nodeName] = meshPtr;
|
||||
meshNode.SetMesh(meshPtr->ix);
|
||||
mesh = meshPtr.get();
|
||||
|
@ -565,6 +568,21 @@ ModelData *Raw2Gltf(
|
|||
|
||||
accessor->min = toStdVec(rawSurface.bounds.min);
|
||||
accessor->max = toStdVec(rawSurface.bounds.max);
|
||||
|
||||
for (int ii = 0; ii < rawSurface.defaultShapeDeforms.size(); ii ++) {
|
||||
auto bufferView = gltf->GetAlignedBufferView(buffer, BufferViewData::GL_ARRAY_BUFFER);
|
||||
std::vector<Vec3f> result;
|
||||
Bounds<float, 3> shapeBounds;
|
||||
for (int jj = 0; jj < surfaceModel.GetVertexCount(); jj ++) {
|
||||
const Vec3f &position = surfaceModel.GetVertex(jj).shapePositions[ii];
|
||||
shapeBounds.AddPoint(position);
|
||||
result.push_back(position);
|
||||
}
|
||||
accessor = gltf->AddAccessorWithView(*bufferView, GLT_VEC3F, result);
|
||||
accessor->min = toStdVec(shapeBounds.min);
|
||||
accessor->max = toStdVec(shapeBounds.max);
|
||||
primitive->AddTarget(*accessor);
|
||||
}
|
||||
}
|
||||
if ((surfaceModel.GetVertexAttributes() & RAW_VERTEX_ATTRIBUTE_NORMAL) != 0) {
|
||||
const AttributeDefinition<Vec3f> ATTR_NORMAL("NORMAL", &RawVertex::normal,
|
||||
|
|
|
@ -37,7 +37,8 @@ bool RawVertex::operator==(const RawVertex &other) const
|
|||
(uv1 == other.uv1) &&
|
||||
(jointIndices == other.jointIndices) &&
|
||||
(jointWeights == other.jointWeights) &&
|
||||
(polarityUv0 == other.polarityUv0);
|
||||
(polarityUv0 == other.polarityUv0) &&
|
||||
(shapePositions == other.shapePositions);
|
||||
}
|
||||
|
||||
size_t RawVertex::Difference(const RawVertex &other) const
|
||||
|
|
|
@ -46,6 +46,8 @@ struct RawVertex
|
|||
Vec4i jointIndices { 0, 0, 0, 0 };
|
||||
Vec4f jointWeights { 0.0f };
|
||||
|
||||
std::vector<Vec3f> shapePositions { };
|
||||
|
||||
bool polarityUv0;
|
||||
bool pad1;
|
||||
bool pad2;
|
||||
|
@ -166,6 +168,7 @@ struct RawSurface
|
|||
std::vector<Vec3f> jointGeometryMins;
|
||||
std::vector<Vec3f> jointGeometryMaxs;
|
||||
std::vector<Mat4f> inverseBindMatrices;
|
||||
std::vector<float> defaultShapeDeforms;
|
||||
bool discrete;
|
||||
bool skinRigid;
|
||||
};
|
||||
|
@ -176,6 +179,7 @@ struct RawChannel
|
|||
std::vector<Vec3f> translations;
|
||||
std::vector<Quatf> rotations;
|
||||
std::vector<Vec3f> scales;
|
||||
std::vector<float> weights;
|
||||
};
|
||||
|
||||
struct RawAnimation
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
#include "MeshData.h"
|
||||
#include "PrimitiveData.h"
|
||||
|
||||
MeshData::MeshData(std::string name)
|
||||
MeshData::MeshData(const std::string &name, const std::vector<float> &weights)
|
||||
: Holdable(),
|
||||
name(std::move(name))
|
||||
name(name),
|
||||
weights(weights)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -22,8 +23,12 @@ json MeshData::serialize() const
|
|||
for (const auto &primitive : primitives) {
|
||||
jsonPrimitivesArray.push_back(*primitive);
|
||||
}
|
||||
return {
|
||||
json result = {
|
||||
{ "name", name },
|
||||
{ "primitives", jsonPrimitivesArray }
|
||||
};
|
||||
if (!weights.empty()) {
|
||||
result["weights"] = weights;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
struct MeshData : Holdable
|
||||
{
|
||||
explicit MeshData(std::string name);
|
||||
MeshData(const std::string &name, const std::vector<float> &weights);
|
||||
|
||||
void AddPrimitive(std::shared_ptr<PrimitiveData> primitive)
|
||||
{
|
||||
|
@ -30,6 +30,7 @@ struct MeshData : Holdable
|
|||
json serialize() const override;
|
||||
|
||||
const std::string name;
|
||||
const std::vector<float> weights;
|
||||
std::vector<std::shared_ptr<PrimitiveData>> primitives;
|
||||
};
|
||||
|
||||
|
|
|
@ -39,6 +39,11 @@ void PrimitiveData::NoteDracoBuffer(const BufferViewData &data)
|
|||
dracoBufferView = data.ix;
|
||||
}
|
||||
|
||||
void PrimitiveData::AddTarget(const AccessorData &positions)
|
||||
{
|
||||
targetPositionAccessors.push_back(positions.ix);
|
||||
}
|
||||
|
||||
void to_json(json &j, const PrimitiveData &d) {
|
||||
j = {
|
||||
{ "material", d.material },
|
||||
|
@ -48,7 +53,15 @@ void to_json(json &j, const PrimitiveData &d) {
|
|||
if (d.indices >= 0) {
|
||||
j["indices"] = d.indices;
|
||||
}
|
||||
|
||||
if (!d.targetPositionAccessors.empty()) {
|
||||
json targets {};
|
||||
for (int ii = 0; ii < d.targetPositionAccessors.size(); ii ++) {
|
||||
targets.push_back({
|
||||
{ "POSITION", d.targetPositionAccessors[ii] }
|
||||
});
|
||||
}
|
||||
j["targets"] = targets;
|
||||
}
|
||||
if (!d.dracoAttributes.empty()) {
|
||||
j["extensions"] = {
|
||||
{ KHR_DRACO_MESH_COMPRESSION, {
|
||||
|
|
|
@ -31,6 +31,8 @@ struct PrimitiveData
|
|||
|
||||
void AddAttrib(std::string name, const AccessorData &accessor);
|
||||
|
||||
void AddTarget(const AccessorData &positions);
|
||||
|
||||
template<class T>
|
||||
void AddDracoAttrib(const AttributeDefinition<T> attribute, const std::vector<T> &attribArr)
|
||||
{
|
||||
|
@ -59,6 +61,8 @@ struct PrimitiveData
|
|||
const unsigned int material;
|
||||
const MeshMode mode;
|
||||
|
||||
std::vector<int> targetPositionAccessors { };
|
||||
|
||||
std::map<std::string, int> attributes;
|
||||
std::map<std::string, int> dracoAttributes;
|
||||
|
||||
|
|
Loading…
Reference in New Issue