One of the issues I had to resolve during the development of our next game was slow loading times on WP8. After some investigation I figure that about half of the time was spent on loading models with skinning information.
I use the code from the CPU Skinning sample. The sample demonstrate how to efficiently do animations on mobile devices which means all other aspects are left as simple as possible so you can adapt it to your needs easily. So, it comes as no surprise that the code depends on automatic serialization (reflection) which is not very efficient. Since we are going to talk about content loading on XNA / MonoGame this post apply to traditional GPU-skinning as well.
Most of the CPU circles were wasted on serializing the list of Keyframes in AnimationClip. To resolve this we can write our own serializer. If you think this doesn't worth doing then take a look at the numbers below...
Platform |
Reader |
Loading Time |
XNA |
automatic serialization |
03,826 sec |
custom AnimationClipReader |
01,970 sec |
MonoGame |
automatic serialization |
14,263 sec |
custom AnimationClipReader |
07,284 sec |
(Lumia 620). You can clearly see a drop by ~50% (Twice as Fast!).
The produced .xnb are also a bit smaller.
he first step is to write a new ContentTypeWriter. Open the CpuSkinningPipelineExtensions project and add a new file named AnimationClipWriter.cs. Copy-paste the following code.
using
CpuSkinningDataTypes;
using
Microsoft.Xna.Framework.Content.Pipeline;
using
Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using
System;
using
System.Collections.Generic;
namespace
CpuSkinningPipelineExtensions
{
/// <summary>
/// Writes out a KeyframeContent object to an XNB file to be read in as
/// a Keyframe.
/// </summary>
[ContentTypeWriter]
class
AnimationClipWriter : ContentTypeWriter<AnimationClip>
{
protected
override
void
Write(ContentWriter output, AnimationClip value)
{
WriteDuration(output, value.Duration);
WriteKeyframes(output, value.Keyframes);
}
private
void
WriteDuration(ContentWriter output, TimeSpan duration)
{
output.Write(duration.Ticks);
}
private
void
WriteKeyframes(ContentWriter output, IList<Keyframe> keyframes)
{
Int32 count = keyframes.Count;
output.Write((Int32)count);
for
(
int
i = 0; i < count; i++)
{
Keyframe keyframe = keyframes[i];
output.Write(keyframe.Bone);
output.Write(keyframe.Time.Ticks);
output.Write(keyframe.Transform);
}
return
;
}
public
override
string
GetRuntimeType(TargetPlatform targetPlatform)
{
return
"CpuSkinningDataTypes.AnimationClip, CpuSkinningDataTypes"
;
}
public
override
string
GetRuntimeReader(TargetPlatform targetPlatform)
{
return
"CpuSkinningDataTypes.AnimationClipReader, CpuSkinningDataTypes"
;
}
}
}
At this point you should rebuild the Content to get the new .XNB.
Next, Open the CpuSkinningDataTypes project and add a new file named AnimationClipReader.cs. Copy-paste the following code.
using
System.Collections.Generic;
using
System.Collections.ObjectModel;
using
Microsoft.Xna.Framework.Content;
using
Microsoft.Xna.Framework.Graphics;
using
Microsoft.Xna.Framework;
using
System;
namespace
CpuSkinningDataTypes
{
/// <summary>
/// A custom reader to read Keyframe.
/// </summary>
public
class
AnimationClipReader : ContentTypeReader<AnimationClip>
{
protected
override
AnimationClip Read(ContentReader input, AnimationClip existingInstance)
{
AnimationClip animationClip = existingInstance;
if
(existingInstance ==
null
)
{
TimeSpan duration = ReadDuration(input);
List<Keyframe> keyframes = ReadKeyframes(input,
null
);
animationClip =
new
AnimationClip(duration, keyframes);
}
else
{
animationClip.Duration = ReadDuration(input);
ReadKeyframes(input, animationClip.Keyframes);
}
return
animationClip;
}
private
TimeSpan ReadDuration(ContentReader input)
{
return
new
TimeSpan(input.ReadInt64());
}
private
List<Keyframe> ReadKeyframes(ContentReader input, List<Keyframe> existingInstance)
{
List<Keyframe> keyframes = existingInstance;
int
count = input.ReadInt32();
if
(keyframes ==
null
)
keyframes =
new
List<Keyframe>(count);
for
(
int
i = 0; i < count; i++)
{
Keyframe keyframe =
new
Keyframe();
keyframe.Bone = input.ReadInt32();
keyframe.Time =
new
TimeSpan(input.ReadInt64());
keyframe.Transform = input.ReadMatrix();
if
(existingInstance ==
null
)
keyframes.Add(keyframe);
else
keyframes[i] = keyframe;
}
return
keyframes;
}
}
}
At this point you must make a few minor changes to AnimationClip & Keyframe classes.
Open AnimationClip.cs and change the access modifier of Duration to internal protected.
public
TimeSpan Duration {
get
;
internal
protected
set
; }
Now, open Keyframe.cs and replace all private modifiers to internal.
public
class
Keyframe
{
public
int
Bone {
get
;
internal
set
; }
public
TimeSpan Time {
get
;
internal
set
; }
public
Matrix Transform {
get
;
internal
set
; }
internal
Keyframe() {}
}
That's it!
If you want to know more about how content serialization works,
see: XNA custom content writer/reader part 1: Introduction.
The .zip file below has some extra changes to correctly reload the model after Resuming under WP8/MonoGame. If you need these changes, make sure to copy both the CpuSkinnedModelWriter.cs / CpuSkinnedModelReader.cs to your project and then rebuild your content.
Code
CPUSkinning - 01 - Loader.zip (7.18 mb)