Line data Source code
1 : /* Copyright (C) 2021 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "PSAConvert.h"
21 : #include "CommonConvert.h"
22 :
23 : #include "FCollada.h"
24 : #include "FCDocument/FCDocument.h"
25 : #include "FCDocument/FCDocumentTools.h"
26 : #include "FCDocument/FCDAnimated.h"
27 : #include "FCDocument/FCDAnimationCurve.h"
28 : #include "FCDocument/FCDAnimationKey.h"
29 : #include "FCDocument/FCDController.h"
30 : #include "FCDocument/FCDControllerInstance.h"
31 : #include "FCDocument/FCDExtra.h"
32 : #include "FCDocument/FCDGeometry.h"
33 : #include "FCDocument/FCDGeometryMesh.h"
34 : #include "FCDocument/FCDGeometryPolygons.h"
35 : #include "FCDocument/FCDGeometrySource.h"
36 : #include "FCDocument/FCDSceneNode.h"
37 :
38 : #include "StdSkeletons.h"
39 : #include "Decompose.h"
40 : #include "Maths.h"
41 : #include "GeomReindex.h"
42 :
43 : #include <cassert>
44 : #include <vector>
45 : #include <limits>
46 : #include <iterator>
47 : #include <algorithm>
48 :
49 : class PSAConvert
50 : {
51 : public:
52 : /**
53 : * Converts a COLLADA XML document into the PSA animation format.
54 : *
55 : * @param input XML document to parse
56 : * @param output callback for writing the PSA data; called lots of times
57 : * with small strings
58 : * @param xmlErrors output - errors reported by the XML parser
59 : * @throws ColladaException on failure
60 : */
61 0 : static void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors)
62 : {
63 0 : CommonConvert converter(input, xmlErrors);
64 :
65 0 : if (converter.GetInstance().GetType() == FCDEntityInstance::CONTROLLER)
66 : {
67 0 : FCDControllerInstance& controllerInstance = static_cast<FCDControllerInstance&>(converter.GetInstance());
68 :
69 0 : FixSkeletonRoots(controllerInstance);
70 :
71 0 : assert(converter.GetInstance().GetEntity()->GetType() == FCDEntity::CONTROLLER); // assume this is always true?
72 0 : FCDController* controller = static_cast<FCDController*>(converter.GetInstance().GetEntity());
73 :
74 0 : FCDSkinController* skin = controller->GetSkinController();
75 0 : REQUIRE(skin != NULL, "is skin controller");
76 :
77 0 : const Skeleton& skeleton = FindSkeleton(controllerInstance);
78 :
79 0 : float frameLength = 1.f / 30.f; // currently we always want to create PMDs at fixed 30fps
80 :
81 : // Find the extents of the animation:
82 :
83 0 : float timeStart = 0, timeEnd = 0;
84 0 : GetAnimationRange(converter.GetDocument(), skeleton, controllerInstance, timeStart, timeEnd);
85 : // To catch broken animations / skeletons.xml:
86 0 : REQUIRE(timeEnd > timeStart, "animation end frame must come after start frame");
87 :
88 : // Count frames; don't include the last keyframe
89 0 : size_t frameCount = (size_t)((timeEnd - timeStart) / frameLength - 0.5f);
90 0 : REQUIRE(frameCount > 0, "animation must have frames");
91 : // (TODO: sort out the timing/looping problems)
92 :
93 0 : const size_t boneCount = skeleton.GetBoneCount();
94 0 : if (boneCount > 64)
95 0 : Log(LOG_ERROR, "Skeleton has too many bones %zu/64", boneCount);
96 :
97 0 : std::vector<BoneTransform> boneTransforms;
98 :
99 0 : for (size_t frame = 0; frame < frameCount; ++frame)
100 : {
101 0 : float time = timeStart + frameLength * frame;
102 :
103 0 : BoneTransform boneDefault = { { 0, 0, 0 }, { 0, 0, 0, 1 } };
104 0 : std::vector<BoneTransform> frameBoneTransforms(boneCount, boneDefault);
105 :
106 : // Move the model into the new animated pose
107 : // (We can't tell exactly which nodes should be animated, so
108 : // just update the entire world recursively)
109 0 : EvaluateAnimations(converter.GetRoot(), time);
110 :
111 : // Convert the pose into the form require by the game
112 0 : for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i)
113 : {
114 0 : FCDSceneNode* joint = controllerInstance.GetJoint(i);
115 :
116 0 : int boneId = skeleton.GetRealBoneID(joint->GetName().c_str());
117 0 : if (boneId < 0)
118 0 : continue; // not a recognised bone - ignore it, same as before
119 :
120 0 : FMMatrix44 worldTransform = joint->CalculateWorldTransform();
121 :
122 : HMatrix matrix;
123 0 : memcpy(matrix, worldTransform.Transposed().m, sizeof(matrix));
124 :
125 : AffineParts parts;
126 0 : decomp_affine(matrix, &parts);
127 :
128 : BoneTransform b = {
129 0 : { parts.t.x, parts.t.y, parts.t.z },
130 0 : { parts.q.x, parts.q.y, parts.q.z, parts.q.w }
131 0 : };
132 :
133 0 : frameBoneTransforms[boneId] = b;
134 : }
135 :
136 : // Push frameBoneTransforms onto the back of boneTransforms
137 0 : copy(frameBoneTransforms.begin(), frameBoneTransforms.end(),
138 0 : std::inserter(boneTransforms, boneTransforms.end()));
139 : }
140 :
141 : // Convert into game's coordinate space
142 0 : TransformVertices(boneTransforms, skin->GetBindShapeTransform(), converter.IsYUp(), converter.IsXSI());
143 :
144 : // Write out the file
145 0 : WritePSA(output, frameCount, boneCount, boneTransforms);
146 : }
147 : else
148 : {
149 0 : throw ColladaException("Unrecognised object type");
150 : }
151 0 : }
152 :
153 : /**
154 : * Writes the animation data in the PSA format.
155 : */
156 0 : static void WritePSA(OutputCB& output, size_t frameCount, size_t boneCount, const std::vector<BoneTransform>& boneTransforms)
157 : {
158 0 : output("PSSA", 4); // magic number
159 0 : write(output, (uint32)1); // version number
160 0 : write(output, (uint32)(
161 : 4 + 0 + // name
162 : 4 + // frameLength
163 0 : 4 + 4 + // numBones, numFrames
164 : 7*4*boneCount*frameCount // boneStates
165 : )); // data size
166 :
167 : // Name
168 0 : write(output, (uint32)0);
169 :
170 : // Frame length
171 0 : write(output, 1000.f/30.f);
172 :
173 0 : write(output, (uint32)boneCount);
174 0 : write(output, (uint32)frameCount);
175 :
176 0 : for (size_t i = 0; i < boneCount*frameCount; ++i)
177 : {
178 0 : output((char*)&boneTransforms[i], 7*4);
179 : }
180 0 : }
181 :
182 0 : static void TransformVertices(std::vector<BoneTransform>& bones,
183 : const FMMatrix44& transform, bool yUp, bool isXSI)
184 : {
185 : // HACK: we want to handle scaling in XSI because that makes it easy
186 : // for artists to adjust the models to the right size. But this way
187 : // doesn't work in Max, and I can't see how to make it do so, so this
188 : // is only applied to models from XSI.
189 0 : if (isXSI)
190 : {
191 0 : TransformBones(bones, DecomposeToScaleMatrix(transform), yUp);
192 : }
193 : else
194 : {
195 0 : TransformBones(bones, FMMatrix44_Identity, yUp);
196 : }
197 0 : }
198 :
199 0 : static void GetAnimationRange(const FColladaDocument& doc, const Skeleton& skeleton,
200 : const FCDControllerInstance& controllerInstance,
201 : float& timeStart, float& timeEnd)
202 : {
203 : // FCollada tools export <extra> info in the scene to specify the start
204 : // and end times.
205 : // If that isn't available, we have to search for the earliest and latest
206 : // keyframes on any of the bones.
207 0 : if (doc.GetDocument()->HasStartTime() && doc.GetDocument()->HasEndTime())
208 : {
209 0 : timeStart = doc.GetDocument()->GetStartTime();
210 0 : timeEnd = doc.GetDocument()->GetEndTime();
211 0 : return;
212 : }
213 :
214 : // XSI exports relevant information in
215 : // <extra><technique profile="XSI"><SI_Scene><xsi_param sid="start">
216 : // (and 'end' and 'frameRate') so use those
217 0 : if (GetAnimationRange_XSI(doc, timeStart, timeEnd))
218 0 : return;
219 :
220 0 : timeStart = std::numeric_limits<float>::max();
221 0 : timeEnd = -std::numeric_limits<float>::max();
222 0 : for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i)
223 : {
224 0 : const FCDSceneNode* joint = controllerInstance.GetJoint(i);
225 0 : REQUIRE(joint != NULL, "joint exists");
226 :
227 0 : int boneId = skeleton.GetBoneID(joint->GetName().c_str());
228 0 : if (boneId < 0)
229 : {
230 : // unrecognised joint - it's probably just a prop point
231 : // or something, so ignore it
232 0 : continue;
233 : }
234 :
235 : // Skip unanimated joints
236 0 : if (joint->GetTransformCount() == 0)
237 0 : continue;
238 :
239 0 : for (size_t j = 0; j < joint->GetTransformCount(); ++j)
240 : {
241 0 : const FCDTransform* transform = joint->GetTransform(j);
242 :
243 0 : if (! transform->IsAnimated())
244 0 : continue;
245 :
246 : // Iterate over all curves to find the earliest and latest keys
247 0 : const FCDAnimated* anim = transform->GetAnimated();
248 0 : const FCDAnimationCurveListList& curvesList = anim->GetCurves();
249 0 : for (size_t k = 0; k < curvesList.size(); ++k)
250 : {
251 0 : const FCDAnimationCurveTrackList& curves = curvesList[k];
252 0 : for (size_t l = 0; l < curves.size(); ++l)
253 : {
254 0 : const FCDAnimationCurve* curve = curves[l];
255 0 : timeStart = std::min(timeStart, curve->GetKeys()[0]->input);
256 0 : timeEnd = std::max(timeEnd, curve->GetKeys()[curve->GetKeyCount()-1]->input);
257 : }
258 : }
259 : }
260 : }
261 : }
262 :
263 0 : static bool GetAnimationRange_XSI(const FColladaDocument& doc, float& timeStart, float& timeEnd)
264 : {
265 0 : FCDExtra* extra = doc.GetExtra();
266 0 : if (! extra) return false;
267 :
268 0 : FCDEType* type = extra->GetDefaultType();
269 0 : if (! type) return false;
270 :
271 0 : FCDETechnique* technique = type->FindTechnique("XSI");
272 0 : if (! technique) return false;
273 :
274 0 : FCDENode* scene = technique->FindChildNode("SI_Scene");
275 0 : if (! scene) return false;
276 :
277 0 : float start = FLT_MAX, end = -FLT_MAX, framerate = 0.f;
278 :
279 0 : FCDENodeList paramNodes;
280 0 : scene->FindChildrenNodes("xsi_param", paramNodes);
281 0 : for (FCDENodeList::iterator it = paramNodes.begin(); it != paramNodes.end(); ++it)
282 : {
283 0 : if ((*it)->ReadAttribute("sid") == "start")
284 0 : start = FUStringConversion::ToFloat((*it)->GetContent());
285 0 : else if ((*it)->ReadAttribute("sid") == "end")
286 0 : end = FUStringConversion::ToFloat((*it)->GetContent());
287 0 : else if ((*it)->ReadAttribute("sid") == "frameRate")
288 0 : framerate = FUStringConversion::ToFloat((*it)->GetContent());
289 : }
290 :
291 0 : if (framerate != 0.f && start != FLT_MAX && end != -FLT_MAX)
292 : {
293 0 : timeStart = start / framerate;
294 0 : timeEnd = end / framerate;
295 0 : return true;
296 : }
297 :
298 0 : return false;
299 : }
300 :
301 0 : static void EvaluateAnimations(FCDSceneNode& node, float time)
302 : {
303 0 : for (size_t i = 0; i < node.GetTransformCount(); ++i)
304 : {
305 0 : FCDTransform* transform = node.GetTransform(i);
306 0 : FCDAnimated* anim = transform->GetAnimated();
307 0 : if (anim)
308 0 : anim->Evaluate(time);
309 : }
310 :
311 0 : for (size_t i = 0; i < node.GetChildrenCount(); ++i)
312 0 : EvaluateAnimations(*node.GetChild(i), time);
313 0 : }
314 :
315 : };
316 :
317 :
318 : // The above stuff is just in a class since I don't like having to bother
319 : // with forward declarations of functions - but provide the plain function
320 : // interface here:
321 :
322 0 : void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors)
323 : {
324 0 : PSAConvert::ColladaToPSA(input, output, xmlErrors);
325 0 : }
|