From 77470e236e1e3953d243ed476e898659c293dca6 Mon Sep 17 00:00:00 2001 From: Par Winzell Date: Sun, 29 Oct 2017 01:59:51 -0700 Subject: [PATCH] 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. --- src/Fbx2Raw.cpp | 133 ++++++++++++++++++++++++++++++++++--- src/Raw2Gltf.cpp | 30 +++++++-- src/RawModel.cpp | 3 +- src/RawModel.h | 4 ++ src/glTF/MeshData.cpp | 11 ++- src/glTF/MeshData.h | 3 +- src/glTF/PrimitiveData.cpp | 15 ++++- src/glTF/PrimitiveData.h | 4 ++ 8 files changed, 183 insertions(+), 20 deletions(-) diff --git a/src/Fbx2Raw.cpp b/src/Fbx2Raw.cpp index d293c4e..4f9411a 100644 --- a/src/Fbx2Raw.cpp +++ b/src/Fbx2Raw.cpp @@ -449,6 +449,91 @@ private: std::vector vertexJointWeights; }; +class FbxBlendShapesAccess +{ +public: + struct Shape { + explicit Shape(const std::vector &offsets, FbxFloat defaultDeform, const std::vectoranimations) : + offsets(offsets), + defaultDeform(defaultDeform), + animations(animations) + {} + const std::vector offsets; + const FbxDouble defaultDeform; + const std::vector 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 extractShapes(const FbxScene *scene, FbxMesh *mesh) const { + std::vector 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(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 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(scene->GetSrcObjectCount()); + std::vectoranimations(animationCount); + for (int j = 0; j < animationCount; j++) { + auto *pAnimStack = scene->GetSrcObject(j); + auto *layer = pAnimStack->GetMember(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 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 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(); - for (int i = 0; i < animationCount; i++) { - FbxAnimStack *pAnimStack = pScene->GetSrcObject(i); + for (int animIx = 0; animIx < animationCount; animIx++) { + FbxAnimStack *pAnimStack = pScene->GetSrcObject(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 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(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); } } diff --git a/src/Raw2Gltf.cpp b/src/Raw2Gltf.cpp index d200378..5720fcf 100644 --- a/src/Raw2Gltf.cpp +++ b/src/Raw2Gltf.cpp @@ -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 result; + Bounds 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 ATTR_NORMAL("NORMAL", &RawVertex::normal, diff --git a/src/RawModel.cpp b/src/RawModel.cpp index 5b625db..44bba64 100644 --- a/src/RawModel.cpp +++ b/src/RawModel.cpp @@ -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 diff --git a/src/RawModel.h b/src/RawModel.h index 1dae913..3f0dc78 100644 --- a/src/RawModel.h +++ b/src/RawModel.h @@ -46,6 +46,8 @@ struct RawVertex Vec4i jointIndices { 0, 0, 0, 0 }; Vec4f jointWeights { 0.0f }; + std::vector shapePositions { }; + bool polarityUv0; bool pad1; bool pad2; @@ -166,6 +168,7 @@ struct RawSurface std::vector jointGeometryMins; std::vector jointGeometryMaxs; std::vector inverseBindMatrices; + std::vector defaultShapeDeforms; bool discrete; bool skinRigid; }; @@ -176,6 +179,7 @@ struct RawChannel std::vector translations; std::vector rotations; std::vector scales; + std::vector weights; }; struct RawAnimation diff --git a/src/glTF/MeshData.cpp b/src/glTF/MeshData.cpp index e7dae6a..a236c53 100644 --- a/src/glTF/MeshData.cpp +++ b/src/glTF/MeshData.cpp @@ -10,9 +10,10 @@ #include "MeshData.h" #include "PrimitiveData.h" -MeshData::MeshData(std::string name) +MeshData::MeshData(const std::string &name, const std::vector &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; } diff --git a/src/glTF/MeshData.h b/src/glTF/MeshData.h index 993a9bb..a88f0c4 100644 --- a/src/glTF/MeshData.h +++ b/src/glTF/MeshData.h @@ -20,7 +20,7 @@ struct MeshData : Holdable { - explicit MeshData(std::string name); + MeshData(const std::string &name, const std::vector &weights); void AddPrimitive(std::shared_ptr primitive) { @@ -30,6 +30,7 @@ struct MeshData : Holdable json serialize() const override; const std::string name; + const std::vector weights; std::vector> primitives; }; diff --git a/src/glTF/PrimitiveData.cpp b/src/glTF/PrimitiveData.cpp index 9d6b732..ef91426 100644 --- a/src/glTF/PrimitiveData.cpp +++ b/src/glTF/PrimitiveData.cpp @@ -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, { diff --git a/src/glTF/PrimitiveData.h b/src/glTF/PrimitiveData.h index 5d6ffb4..64053c2 100644 --- a/src/glTF/PrimitiveData.h +++ b/src/glTF/PrimitiveData.h @@ -31,6 +31,8 @@ struct PrimitiveData void AddAttrib(std::string name, const AccessorData &accessor); + void AddTarget(const AccessorData &positions); + template void AddDracoAttrib(const AttributeDefinition attribute, const std::vector &attribArr) { @@ -59,6 +61,8 @@ struct PrimitiveData const unsigned int material; const MeshMode mode; + std::vector targetPositionAccessors { }; + std::map attributes; std::map dracoAttributes;