From 08ba782bd5fd1d0392b70e675db99702ff349b7f Mon Sep 17 00:00:00 2001 From: George Papadopoulos Date: Thu, 26 Mar 2015 00:43:41 +0200 Subject: [PATCH] [FBX] add support for multiple animations (by using LocalStart/LocalStop in takes) + change key type from uint to int (fixes bugs from negative int_t becoming a junk uint_t value ) + detect and ignore channels with no keys in the specified take start/stop window + add test model with multiple animations --- code/FBXConverter.cpp | 158 ++++++++++++++---- code/FBXDocument.h | 10 +- code/FBXParser.cpp | 106 ++++++++++++ code/FBXParser.h | 3 + code/FBXProperties.cpp | 7 +- .../2013_BINARY/multiple_animations_test.fbx | Bin 0 -> 34576 bytes 6 files changed, 243 insertions(+), 41 deletions(-) create mode 100644 test/models-nonbsd/FBX/2013_BINARY/multiple_animations_test.fbx diff --git a/code/FBXConverter.cpp b/code/FBXConverter.cpp index 7bd15736f..1698d6d93 100644 --- a/code/FBXConverter.cpp +++ b/code/FBXConverter.cpp @@ -1968,9 +1968,12 @@ private: // strip AnimationStack:: prefix std::string name = st.Name(); - if(name.substr(0,16) == "AnimationStack::") { + if (name.substr(0, 16) == "AnimationStack::") { name = name.substr(16); } + else if (name.substr(0, 11) == "AnimStack::") { + name = name.substr(11); + } anim->mName.Set(name); @@ -2014,12 +2017,18 @@ private: double min_time = 1e10; double max_time = -1e10; + int64_t start_time = st.LocalStart(); + int64_t stop_time = st.LocalStop(); + double start_timeF = CONVERT_FBX_TIME(start_time); + double stop_timeF = CONVERT_FBX_TIME(stop_time); + try { BOOST_FOREACH(const NodeMap::value_type& kv, node_map) { GenerateNodeAnimations(node_anims, kv.first, kv.second, layer_map, + start_time, stop_time, max_time, min_time); } @@ -2043,9 +2052,27 @@ private: return; } + //adjust relative timing for animation + { + double start_fps = start_timeF * anim_fps; + + for (unsigned int c = 0; c < anim->mNumChannels; c++) + { + aiNodeAnim* channel = anim->mChannels[c]; + for (uint32_t i = 0; i < channel->mNumPositionKeys; i++) + channel->mPositionKeys[i].mTime -= start_fps; + for (uint32_t i = 0; i < channel->mNumRotationKeys; i++) + channel->mRotationKeys[i].mTime -= start_fps; + for (uint32_t i = 0; i < channel->mNumScalingKeys; i++) + channel->mScalingKeys[i].mTime -= start_fps; + } + + max_time -= min_time; + } + // for some mysterious reason, mDuration is simply the maximum key -- the // validator always assumes animations to start at zero. - anim->mDuration = max_time /*- min_time */; + anim->mDuration = (stop_timeF - start_timeF) * anim_fps; anim->mTicksPerSecond = anim_fps; } @@ -2055,6 +2082,7 @@ private: const std::string& fixed_name, const std::vector& curves, const LayerMap& layer_map, + int64_t start, int64_t stop, double& max_time, double& min_time) { @@ -2147,13 +2175,19 @@ private: aiNodeAnim* const nd = GenerateSimpleNodeAnim(fixed_name, target, chain, node_property_map.end(), layer_map, + start, stop, max_time, min_time, true // input is TRS order, assimp is SRT ); ai_assert(nd); - node_anims.push_back(nd); + if (nd->mNumPositionKeys == 0 && nd->mNumRotationKeys == 0 && nd->mNumScalingKeys == 0) { + delete nd; + } + else { + node_anims.push_back(nd); + } return; } @@ -2185,6 +2219,7 @@ private: target, (*chain[i]).second, layer_map, + start, stop, max_time, min_time); @@ -2200,6 +2235,7 @@ private: target, (*chain[i]).second, layer_map, + start, stop, max_time, min_time); @@ -2212,12 +2248,18 @@ private: target, (*chain[i]).second, layer_map, + start, stop, max_time, min_time, true); ai_assert(inv); - node_anims.push_back(inv); + if (inv->mNumPositionKeys == 0 && inv->mNumRotationKeys == 0 && inv->mNumScalingKeys == 0) { + delete inv; + } + else { + node_anims.push_back(inv); + } ai_assert(TransformationComp_RotationPivotInverse > i); flags |= bit << (TransformationComp_RotationPivotInverse - i); @@ -2230,12 +2272,18 @@ private: target, (*chain[i]).second, layer_map, + start, stop, max_time, min_time, true); ai_assert(inv); - node_anims.push_back(inv); + if (inv->mNumPositionKeys == 0 && inv->mNumRotationKeys == 0 && inv->mNumScalingKeys == 0) { + delete inv; + } + else { + node_anims.push_back(inv); + } ai_assert(TransformationComp_RotationPivotInverse > i); flags |= bit << (TransformationComp_RotationPivotInverse - i); @@ -2249,6 +2297,7 @@ private: target, (*chain[i]).second, layer_map, + start, stop, max_time, min_time); @@ -2259,7 +2308,12 @@ private: } ai_assert(na); - node_anims.push_back(na); + if (na->mNumPositionKeys == 0 && na->mNumRotationKeys == 0 && na->mNumScalingKeys == 0) { + delete na; + } + else { + node_anims.push_back(na); + } continue; } } @@ -2320,13 +2374,14 @@ private: const Model& target, const std::vector& curves, const LayerMap& layer_map, + int64_t start, int64_t stop, double& max_time, double& min_time) { ScopeGuard na(new aiNodeAnim()); na->mNodeName.Set(name); - ConvertRotationKeys(na, curves, layer_map, max_time,min_time, target.RotationOrder()); + ConvertRotationKeys(na, curves, layer_map, start, stop, max_time, min_time, target.RotationOrder()); // dummy scaling key na->mScalingKeys = new aiVectorKey[1]; @@ -2351,13 +2406,14 @@ private: const Model& /*target*/, const std::vector& curves, const LayerMap& layer_map, + int64_t start, int64_t stop, double& max_time, double& min_time) { ScopeGuard na(new aiNodeAnim()); na->mNodeName.Set(name); - ConvertScaleKeys(na, curves, layer_map, max_time,min_time); + ConvertScaleKeys(na, curves, layer_map, start, stop, max_time, min_time); // dummy rotation key na->mRotationKeys = new aiQuatKey[1]; @@ -2382,6 +2438,7 @@ private: const Model& /*target*/, const std::vector& curves, const LayerMap& layer_map, + int64_t start, int64_t stop, double& max_time, double& min_time, bool inverse = false) @@ -2389,7 +2446,7 @@ private: ScopeGuard na(new aiNodeAnim()); na->mNodeName.Set(name); - ConvertTranslationKeys(na, curves, layer_map, max_time,min_time); + ConvertTranslationKeys(na, curves, layer_map, start, stop, max_time, min_time); if (inverse) { for (unsigned int i = 0; i < na->mNumPositionKeys; ++i) { @@ -2422,6 +2479,7 @@ private: NodeMap::const_iterator chain[TransformationComp_MAXIMUM], NodeMap::const_iterator iter_end, const LayerMap& layer_map, + int64_t start, int64_t stop, double& max_time, double& min_time, bool reverse_order = false) @@ -2443,21 +2501,21 @@ private: KeyFrameListList rotation; if(chain[TransformationComp_Scaling] != iter_end) { - scaling = GetKeyframeList((*chain[TransformationComp_Scaling]).second); + scaling = GetKeyframeList((*chain[TransformationComp_Scaling]).second, start, stop); } else { def_scale = PropertyGet(props,"Lcl Scaling",aiVector3D(1.f,1.f,1.f)); } if(chain[TransformationComp_Translation] != iter_end) { - translation = GetKeyframeList((*chain[TransformationComp_Translation]).second); + translation = GetKeyframeList((*chain[TransformationComp_Translation]).second, start, stop); } else { def_translate = PropertyGet(props,"Lcl Translation",aiVector3D(0.f,0.f,0.f)); } if(chain[TransformationComp_Rotation] != iter_end) { - rotation = GetKeyframeList((*chain[TransformationComp_Rotation]).second); + rotation = GetKeyframeList((*chain[TransformationComp_Rotation]).second, start, stop); } else { def_rot = EulerToQuaternion(PropertyGet(props,"Lcl Rotation",aiVector3D(0.f,0.f,0.f)), @@ -2475,17 +2533,20 @@ private: aiVectorKey* out_scale = new aiVectorKey[times.size()]; aiVectorKey* out_translation = new aiVectorKey[times.size()]; - ConvertTransformOrder_TRStoSRT(out_quat, out_scale, out_translation, - scaling, - translation, - rotation, - times, - max_time, - min_time, - target.RotationOrder(), - def_scale, - def_translate, - def_rot); + if (times.size()) + { + ConvertTransformOrder_TRStoSRT(out_quat, out_scale, out_translation, + scaling, + translation, + rotation, + times, + max_time, + min_time, + target.RotationOrder(), + def_scale, + def_translate, + def_rot); + } // XXX remove duplicates / redundant keys which this operation did // likely produce if not all three channels were equally dense. @@ -2507,6 +2568,7 @@ private: if(chain[TransformationComp_Scaling] != iter_end) { ConvertScaleKeys(na, (*chain[TransformationComp_Scaling]).second, layer_map, + start, stop, max_time, min_time); } @@ -2522,6 +2584,7 @@ private: if(chain[TransformationComp_Rotation] != iter_end) { ConvertRotationKeys(na, (*chain[TransformationComp_Rotation]).second, layer_map, + start, stop, max_time, min_time, target.RotationOrder()); @@ -2539,6 +2602,7 @@ private: if(chain[TransformationComp_Translation] != iter_end) { ConvertTranslationKeys(na, (*chain[TransformationComp_Translation]).second, layer_map, + start, stop, max_time, min_time); } @@ -2558,17 +2622,21 @@ private: // key (time), value, mapto (component index) - typedef boost::tuple< const KeyTimeList*, const KeyValueList*, unsigned int > KeyFrameList; + typedef boost::tuple, boost::shared_ptr, unsigned int > KeyFrameList; typedef std::vector KeyFrameListList; // ------------------------------------------------------------------------------------------------ - KeyFrameListList GetKeyframeList(const std::vector& nodes) + KeyFrameListList GetKeyframeList(const std::vector& nodes, int64_t start, int64_t stop) { KeyFrameListList inputs; inputs.reserve(nodes.size()*3); + //give some breathing room for rounding errors + int64_t adj_start = start - 10000; + int64_t adj_stop = stop + 10000; + BOOST_FOREACH(const AnimationCurveNode* node, nodes) { ai_assert(node); @@ -2593,7 +2661,23 @@ private: const AnimationCurve* const curve = kv.second; ai_assert(curve->GetKeys().size() == curve->GetValues().size() && curve->GetKeys().size()); - inputs.push_back(boost::make_tuple(&curve->GetKeys(), &curve->GetValues(), mapto)); + //get values within the start/stop time window + boost::shared_ptr Keys(new KeyTimeList()); + boost::shared_ptr Values(new KeyValueList()); + const int count = curve->GetKeys().size(); + Keys->reserve(count); + Values->reserve(count); + for (int n = 0; n < count; n++) + { + int64_t k = curve->GetKeys().at(n); + if (k >= adj_start && k <= adj_stop) + { + Keys->push_back(k); + Values->push_back(curve->GetValues().at(n)); + } + } + + inputs.push_back(boost::make_tuple(Keys, Values, mapto)); } } return inputs; // pray for NRVO :-) @@ -2623,7 +2707,7 @@ private: const size_t count = inputs.size(); while(true) { - uint64_t min_tick = std::numeric_limits::max(); + int64_t min_tick = std::numeric_limits::max(); for (size_t i = 0; i < count; ++i) { const KeyFrameList& kfl = inputs[i]; @@ -2632,7 +2716,7 @@ private: } } - if (min_tick == std::numeric_limits::max()) { + if (min_tick == std::numeric_limits::max()) { break; } keys.push_back(min_tick); @@ -2832,6 +2916,7 @@ private: // ------------------------------------------------------------------------------------------------ void ConvertScaleKeys(aiNodeAnim* na, const std::vector& nodes, const LayerMap& /*layers*/, + int64_t start, int64_t stop, double& maxTime, double& minTime) { @@ -2841,36 +2926,40 @@ private: // layers should be multiplied with each other). There is a FBX // property in the layer to specify the behaviour, though. - const KeyFrameListList& inputs = GetKeyframeList(nodes); + const KeyFrameListList& inputs = GetKeyframeList(nodes, start, stop); const KeyTimeList& keys = GetKeyTimeList(inputs); na->mNumScalingKeys = static_cast(keys.size()); na->mScalingKeys = new aiVectorKey[keys.size()]; - InterpolateKeys(na->mScalingKeys, keys, inputs, true, maxTime, minTime); + if (keys.size() > 0) + InterpolateKeys(na->mScalingKeys, keys, inputs, true, maxTime, minTime); } // ------------------------------------------------------------------------------------------------ void ConvertTranslationKeys(aiNodeAnim* na, const std::vector& nodes, const LayerMap& /*layers*/, + int64_t start, int64_t stop, double& maxTime, double& minTime) { ai_assert(nodes.size()); // XXX see notes in ConvertScaleKeys() - const KeyFrameListList& inputs = GetKeyframeList(nodes); + const KeyFrameListList& inputs = GetKeyframeList(nodes, start, stop); const KeyTimeList& keys = GetKeyTimeList(inputs); na->mNumPositionKeys = static_cast(keys.size()); na->mPositionKeys = new aiVectorKey[keys.size()]; - InterpolateKeys(na->mPositionKeys, keys, inputs, false, maxTime, minTime); + if (keys.size() > 0) + InterpolateKeys(na->mPositionKeys, keys, inputs, false, maxTime, minTime); } // ------------------------------------------------------------------------------------------------ void ConvertRotationKeys(aiNodeAnim* na, const std::vector& nodes, const LayerMap& /*layers*/, + int64_t start, int64_t stop, double& maxTime, double& minTime, Model::RotOrder order) @@ -2878,12 +2967,13 @@ private: ai_assert(nodes.size()); // XXX see notes in ConvertScaleKeys() - const std::vector< KeyFrameList >& inputs = GetKeyframeList(nodes); + const std::vector< KeyFrameList >& inputs = GetKeyframeList(nodes, start, stop); const KeyTimeList& keys = GetKeyTimeList(inputs); na->mNumRotationKeys = static_cast(keys.size()); na->mRotationKeys = new aiQuatKey[keys.size()]; - InterpolateKeys(na->mRotationKeys, keys, inputs, false, maxTime, minTime, order); + if (keys.size() > 0) + InterpolateKeys(na->mRotationKeys, keys, inputs, false, maxTime, minTime, order); } diff --git a/code/FBXDocument.h b/code/FBXDocument.h index b4099550b..9f1849132 100644 --- a/code/FBXDocument.h +++ b/code/FBXDocument.h @@ -871,7 +871,7 @@ private: std::vector mappings; }; -typedef std::vector KeyTimeList; +typedef std::vector KeyTimeList; typedef std::vector KeyValueList; /** Represents a FBX animation curve (i.e. a 1-dimensional set of keyframes and values therefor) */ @@ -1015,10 +1015,10 @@ public: public: - fbx_simple_property(LocalStart, uint64_t, 0L) - fbx_simple_property(LocalStop, uint64_t, 0L) - fbx_simple_property(ReferenceStart, uint64_t, 0L) - fbx_simple_property(ReferenceStop, uint64_t, 0L) + fbx_simple_property(LocalStart, int64_t, 0L) + fbx_simple_property(LocalStop, int64_t, 0L) + fbx_simple_property(ReferenceStart, int64_t, 0L) + fbx_simple_property(ReferenceStop, int64_t, 0L) diff --git a/code/FBXParser.cpp b/code/FBXParser.cpp index 62f8d9c0c..a9fe97a5e 100644 --- a/code/FBXParser.cpp +++ b/code/FBXParser.cpp @@ -431,6 +431,43 @@ int ParseTokenAsInt(const Token& t, const char*& err_out) } +// ------------------------------------------------------------------------------------------------ +int64_t ParseTokenAsInt64(const Token& t, const char*& err_out) +{ + err_out = NULL; + + if (t.Type() != TokenType_DATA) { + err_out = "expected TOK_DATA token"; + return 0L; + } + + if (t.IsBinary()) + { + const char* data = t.begin(); + if (data[0] != 'L') { + err_out = "failed to parse Int64, unexpected data type"; + return 0L; + } + + BE_NCONST int64_t id = SafeParse(data + 1, t.end()); + AI_SWAP8(id); + return id; + } + + // XXX: should use size_t here + unsigned int length = static_cast(t.end() - t.begin()); + ai_assert(length > 0); + + const char* out; + const int64_t id = strtoul10_64(t.begin(), &out, &length); + if (out > t.end()) { + err_out = "failed to parse Int64 (text)"; + return 0L; + } + + return id; +} + // ------------------------------------------------------------------------------------------------ std::string ParseTokenAsString(const Token& t, const char*& err_out) { @@ -1062,6 +1099,63 @@ void ParseVectorDataArray(std::vector& out, const Element& el) } } +// ------------------------------------------------------------------------------------------------ +// read an array of int64_ts +void ParseVectorDataArray(std::vector& out, const Element& el) +{ + out.clear(); + const TokenList& tok = el.Tokens(); + if (tok.empty()) { + ParseError("unexpected empty element", &el); + } + + if (tok[0]->IsBinary()) { + const char* data = tok[0]->begin(), *end = tok[0]->end(); + + char type; + uint32_t count; + ReadBinaryDataArrayHead(data, end, type, count, el); + + if (!count) { + return; + } + + if (type != 'l') { + ParseError("expected long array (binary)", &el); + } + + std::vector buff; + ReadBinaryDataArray(type, count, data, end, buff, el); + + ai_assert(data == end); + ai_assert(buff.size() == count * 8); + + out.reserve(count); + + const int64_t* ip = reinterpret_cast(&buff[0]); + for (unsigned int i = 0; i < count; ++i, ++ip) { + BE_NCONST int64_t val = *ip; + AI_SWAP8(val); + out.push_back(val); + } + + return; + } + + const size_t dim = ParseTokenAsDim(*tok[0]); + + // see notes in ParseVectorDataArray() + out.reserve(dim); + + const Scope& scope = GetRequiredScope(el); + const Element& a = GetRequiredElement(scope, "a", &el); + + for (TokenList::const_iterator it = a.Tokens().begin(), end = a.Tokens().end(); it != end;) { + const int64_t ival = ParseTokenAsInt64(**it++); + + out.push_back(ival); + } +} // ------------------------------------------------------------------------------------------------ aiMatrix4x4 ReadMatrix(const Element& element) @@ -1205,6 +1299,18 @@ int ParseTokenAsInt(const Token& t) +// ------------------------------------------------------------------------------------------------ +// wrapper around ParseTokenAsInt64() with ParseError handling +int64_t ParseTokenAsInt64(const Token& t) +{ + const char* err; + const int64_t i = ParseTokenAsInt64(t, err); + if (err) { + ParseError(err, t); + } + return i; +} + } // !FBX } // !Assimp diff --git a/code/FBXParser.h b/code/FBXParser.h index e6fa25d22..150b6267a 100644 --- a/code/FBXParser.h +++ b/code/FBXParser.h @@ -206,6 +206,7 @@ size_t ParseTokenAsDim(const Token& t, const char*& err_out); float ParseTokenAsFloat(const Token& t, const char*& err_out); int ParseTokenAsInt(const Token& t, const char*& err_out); +int64_t ParseTokenAsInt64(const Token& t, const char*& err_out); std::string ParseTokenAsString(const Token& t, const char*& err_out); @@ -214,6 +215,7 @@ uint64_t ParseTokenAsID(const Token& t); size_t ParseTokenAsDim(const Token& t); float ParseTokenAsFloat(const Token& t); int ParseTokenAsInt(const Token& t); +int64_t ParseTokenAsInt64(const Token& t); std::string ParseTokenAsString(const Token& t); /* read data arrays */ @@ -224,6 +226,7 @@ void ParseVectorDataArray(std::vector& out, const Element& el); void ParseVectorDataArray(std::vector& out, const Element& el); void ParseVectorDataArray(std::vector& out, const Element& el); void ParseVectorDataArray(std::vector& out, const Element& e); +void ParseVectorDataArray(std::vector& out, const Element& el); diff --git a/code/FBXProperties.cpp b/code/FBXProperties.cpp index 13b354442..5676d9d5e 100644 --- a/code/FBXProperties.cpp +++ b/code/FBXProperties.cpp @@ -88,9 +88,12 @@ Property* ReadTypedProperty(const Element& element) else if (!strcmp(cs, "int") || !strcmp(cs, "Int") || !strcmp(cs, "enum") || !strcmp(cs, "Enum")) { return new TypedProperty(ParseTokenAsInt(*tok[4])); } - else if (!strcmp(cs,"ULongLong")) { + else if (!strcmp(cs, "ULongLong")) { return new TypedProperty(ParseTokenAsID(*tok[4])); } + else if (!strcmp(cs, "KTime")) { + return new TypedProperty(ParseTokenAsInt64(*tok[4])); + } else if (!strcmp(cs,"Vector3D") || !strcmp(cs,"ColorRGB") || !strcmp(cs,"Vector") || @@ -105,7 +108,7 @@ Property* ReadTypedProperty(const Element& element) ParseTokenAsFloat(*tok[6])) ); } - else if (!strcmp(cs,"double") || !strcmp(cs,"Number") || !strcmp(cs,"KTime") || !strcmp(cs,"Float") || !strcmp(cs,"FieldOfView")) { + else if (!strcmp(cs,"double") || !strcmp(cs,"Number") || !strcmp(cs,"Float") || !strcmp(cs,"FieldOfView")) { return new TypedProperty(ParseTokenAsFloat(*tok[4])); } return NULL; diff --git a/test/models-nonbsd/FBX/2013_BINARY/multiple_animations_test.fbx b/test/models-nonbsd/FBX/2013_BINARY/multiple_animations_test.fbx new file mode 100644 index 0000000000000000000000000000000000000000..7999e690c566d3c4a77c58f9cb46305b55bc7ecf GIT binary patch literal 34576 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 DHi)xP literal 0 HcmV?d00001