1 /* Copyright (C) 2018 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 	 */
ColladaToPSA(const char * input,OutputCB & output,std::string & xmlErrors)61 	static void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors)
62 	{
63 		CommonConvert converter(input, xmlErrors);
64 
65 		if (converter.GetInstance().GetType() == FCDEntityInstance::CONTROLLER)
66 		{
67 			FCDControllerInstance& controllerInstance = static_cast<FCDControllerInstance&>(converter.GetInstance());
68 
69 			FixSkeletonRoots(controllerInstance);
70 
71 			assert(converter.GetInstance().GetEntity()->GetType() == FCDEntity::CONTROLLER); // assume this is always true?
72 			FCDController* controller = static_cast<FCDController*>(converter.GetInstance().GetEntity());
73 
74 			FCDSkinController* skin = controller->GetSkinController();
75 			REQUIRE(skin != NULL, "is skin controller");
76 
77 			const Skeleton& skeleton = FindSkeleton(controllerInstance);
78 
79 			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 			float timeStart = 0, timeEnd = 0;
84 			GetAnimationRange(converter.GetDocument(), skeleton, controllerInstance, timeStart, timeEnd);
85 			// To catch broken animations / skeletons.xml:
86 			REQUIRE(timeEnd > timeStart, "animation end frame must come after start frame");
87 
88 			// Count frames; don't include the last keyframe
89 			size_t frameCount = (size_t)((timeEnd - timeStart) / frameLength - 0.5f);
90 			REQUIRE(frameCount > 0, "animation must have frames");
91 			// (TODO: sort out the timing/looping problems)
92 
93 			size_t boneCount = skeleton.GetBoneCount();
94 
95 			std::vector<BoneTransform> boneTransforms;
96 
97 			for (size_t frame = 0; frame < frameCount; ++frame)
98 			{
99 				float time = timeStart + frameLength * frame;
100 
101 				BoneTransform boneDefault  = { { 0, 0, 0 }, { 0, 0, 0, 1 } };
102 				std::vector<BoneTransform> frameBoneTransforms (boneCount, boneDefault);
103 
104 				// Move the model into the new animated pose
105 				// (We can't tell exactly which nodes should be animated, so
106 				// just update the entire world recursively)
107 				EvaluateAnimations(converter.GetRoot(), time);
108 
109 				// Convert the pose into the form require by the game
110 				for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i)
111 				{
112 					FCDSceneNode* joint = controllerInstance.GetJoint(i);
113 
114 					int boneId = skeleton.GetRealBoneID(joint->GetName().c_str());
115 					if (boneId < 0)
116 						continue; // not a recognised bone - ignore it, same as before
117 
118 					FMMatrix44 worldTransform = joint->CalculateWorldTransform();
119 
120 					HMatrix matrix;
121 					memcpy(matrix, worldTransform.Transposed().m, sizeof(matrix));
122 
123 					AffineParts parts;
124 					decomp_affine(matrix, &parts);
125 
126 					BoneTransform b = {
127 						{ parts.t.x, parts.t.y, parts.t.z },
128 						{ parts.q.x, parts.q.y, parts.q.z, parts.q.w }
129 					};
130 
131 					frameBoneTransforms[boneId] = b;
132 				}
133 
134 				// Push frameBoneTransforms onto the back of boneTransforms
135 				copy(frameBoneTransforms.begin(), frameBoneTransforms.end(),
136 					std::inserter(boneTransforms, boneTransforms.end()));
137 			}
138 
139 			// Convert into game's coordinate space
140 			TransformVertices(boneTransforms, skin->GetBindShapeTransform(), converter.IsYUp(), converter.IsXSI());
141 
142 			// Write out the file
143 			WritePSA(output, frameCount, boneCount, boneTransforms);
144 		}
145 		else
146 		{
147 			throw ColladaException("Unrecognised object type");
148 		}
149 	}
150 
151 	/**
152 	 * Writes the animation data in the PSA format.
153 	 */
WritePSA(OutputCB & output,size_t frameCount,size_t boneCount,const std::vector<BoneTransform> & boneTransforms)154 	static void WritePSA(OutputCB& output, size_t frameCount, size_t boneCount, const std::vector<BoneTransform>& boneTransforms)
155 	{
156 		output("PSSA", 4);  // magic number
157 		write(output, (uint32)1); // version number
158 		write(output, (uint32)(
159 			4 + 0 + // name
160 			4 + // frameLength
161 			4 + 4 + // numBones, numFrames
162 			7*4*boneCount*frameCount // boneStates
163 			)); // data size
164 
165 		// Name
166 		write(output, (uint32)0);
167 
168 		// Frame length
169 		write(output, 1000.f/30.f);
170 
171 		write(output, (uint32)boneCount);
172 		write(output, (uint32)frameCount);
173 
174 		for (size_t i = 0; i < boneCount*frameCount; ++i)
175 		{
176 			output((char*)&boneTransforms[i], 7*4);
177 		}
178 	}
179 
TransformVertices(std::vector<BoneTransform> & bones,const FMMatrix44 & transform,bool yUp,bool isXSI)180 	static void TransformVertices(std::vector<BoneTransform>& bones,
181 		const FMMatrix44& transform, bool yUp, bool isXSI)
182 	{
183 		// HACK: we want to handle scaling in XSI because that makes it easy
184 		// for artists to adjust the models to the right size. But this way
185 		// doesn't work in Max, and I can't see how to make it do so, so this
186 		// is only applied to models from XSI.
187 		if (isXSI)
188 		{
189 			TransformBones(bones, DecomposeToScaleMatrix(transform), yUp);
190 		}
191 		else
192 		{
193 			TransformBones(bones, FMMatrix44_Identity, yUp);
194 		}
195 	}
196 
GetAnimationRange(const FColladaDocument & doc,const Skeleton & skeleton,const FCDControllerInstance & controllerInstance,float & timeStart,float & timeEnd)197 	static void GetAnimationRange(const FColladaDocument& doc, const Skeleton& skeleton,
198 		const FCDControllerInstance& controllerInstance,
199 		float& timeStart, float& timeEnd)
200 	{
201 		// FCollada tools export <extra> info in the scene to specify the start
202 		// and end times.
203 		// If that isn't available, we have to search for the earliest and latest
204 		// keyframes on any of the bones.
205 		if (doc.GetDocument()->HasStartTime() && doc.GetDocument()->HasEndTime())
206 		{
207 			timeStart = doc.GetDocument()->GetStartTime();
208 			timeEnd = doc.GetDocument()->GetEndTime();
209 			return;
210 		}
211 
212 		// XSI exports relevant information in
213 		// <extra><technique profile="XSI"><SI_Scene><xsi_param sid="start">
214 		// (and 'end' and 'frameRate') so use those
215 		if (GetAnimationRange_XSI(doc, timeStart, timeEnd))
216 			return;
217 
218 		timeStart = std::numeric_limits<float>::max();
219 		timeEnd = -std::numeric_limits<float>::max();
220 		for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i)
221 		{
222 			const FCDSceneNode* joint = controllerInstance.GetJoint(i);
223 			REQUIRE(joint != NULL, "joint exists");
224 
225 			int boneId = skeleton.GetBoneID(joint->GetName().c_str());
226 			if (boneId < 0)
227 			{
228 				// unrecognised joint - it's probably just a prop point
229 				// or something, so ignore it
230 				continue;
231 			}
232 
233 			// Skip unanimated joints
234 			if (joint->GetTransformCount() == 0)
235 				continue;
236 
237 			for (size_t j = 0; j < joint->GetTransformCount(); ++j)
238 			{
239 				const FCDTransform* transform = joint->GetTransform(j);
240 
241 				if (! transform->IsAnimated())
242 					continue;
243 
244 				// Iterate over all curves to find the earliest and latest keys
245 				const FCDAnimated* anim = transform->GetAnimated();
246 				const FCDAnimationCurveListList& curvesList = anim->GetCurves();
247 				for (size_t k = 0; k < curvesList.size(); ++k)
248 				{
249 					const FCDAnimationCurveTrackList& curves = curvesList[k];
250 					for (size_t l = 0; l < curves.size(); ++l)
251 					{
252 						const FCDAnimationCurve* curve = curves[l];
253 						timeStart = std::min(timeStart, curve->GetKeys()[0]->input);
254 						timeEnd = std::max(timeEnd, curve->GetKeys()[curve->GetKeyCount()-1]->input);
255 					}
256 				}
257 			}
258 		}
259 	}
260 
GetAnimationRange_XSI(const FColladaDocument & doc,float & timeStart,float & timeEnd)261 	static bool GetAnimationRange_XSI(const FColladaDocument& doc, float& timeStart, float& timeEnd)
262 	{
263 		FCDExtra* extra = doc.GetExtra();
264 		if (! extra) return false;
265 
266 		FCDEType* type = extra->GetDefaultType();
267 		if (! type) return false;
268 
269 		FCDETechnique* technique = type->FindTechnique("XSI");
270 		if (! technique) return false;
271 
272 		FCDENode* scene = technique->FindChildNode("SI_Scene");
273 		if (! scene) return false;
274 
275 		float start = FLT_MAX, end = -FLT_MAX, framerate = 0.f;
276 
277 		FCDENodeList paramNodes;
278 		scene->FindChildrenNodes("xsi_param", paramNodes);
279 		for (FCDENodeList::iterator it = paramNodes.begin(); it != paramNodes.end(); ++it)
280 		{
281 			if ((*it)->ReadAttribute("sid") == "start")
282 				start = FUStringConversion::ToFloat((*it)->GetContent());
283 			else if ((*it)->ReadAttribute("sid") == "end")
284 				end = FUStringConversion::ToFloat((*it)->GetContent());
285 			else if ((*it)->ReadAttribute("sid") == "frameRate")
286 				framerate = FUStringConversion::ToFloat((*it)->GetContent());
287 		}
288 
289 		if (framerate != 0.f && start != FLT_MAX && end != -FLT_MAX)
290 		{
291 			timeStart = start / framerate;
292 			timeEnd = end / framerate;
293 			return true;
294 		}
295 
296 		return false;
297 	}
298 
EvaluateAnimations(FCDSceneNode & node,float time)299 	static void EvaluateAnimations(FCDSceneNode& node, float time)
300 	{
301 		for (size_t i = 0; i < node.GetTransformCount(); ++i)
302 		{
303 			FCDTransform* transform = node.GetTransform(i);
304 			FCDAnimated* anim = transform->GetAnimated();
305 			if (anim)
306 				anim->Evaluate(time);
307 		}
308 
309 		for (size_t i = 0; i < node.GetChildrenCount(); ++i)
310 			EvaluateAnimations(*node.GetChild(i), time);
311 	}
312 
313 };
314 
315 
316 // The above stuff is just in a class since I don't like having to bother
317 // with forward declarations of functions - but provide the plain function
318 // interface here:
319 
ColladaToPSA(const char * input,OutputCB & output,std::string & xmlErrors)320 void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors)
321 {
322 	PSAConvert::ColladaToPSA(input, output, xmlErrors);
323 }
324