-
-
- (0)
-
premium membership
-
Posts
12
|
Re: Still trying to get skinned models to work in RobotGame.
|
pixelminerXNA:Yup, still at it.
I did figure out it's possible to pass through bone information to a
shader by expanding GameModel's RenderingCustomEffectEventArgs then
adjusting calls to it accordingly. However, I was still getting a
"blank" model so, just to see if vertex information is being passed in,
I commented out the skinning portion of the vertex shader. That got
the model to show up and even follow where the player model should be
after I passed in the world matrix for the object but the model comes
in face down on the ground instead of standing up on its feet.(Maybe I
have rotation on the geometry inside 3DS Max?)
Anyway, the guns, which are floating still, seems to be attached to where the hands would be and animated perfectly fine. I guess something must be getting messed up in translation or maybe the "boneTransforms" I'm passing in has to be formatted in some way to make it work right with the skinning example shader. If anybody has any ideas about why this might be I'd love to hear it...
Oh, I also meant to ask you if you're running your model through the content parser from the skinning sample? You have to do that in order for the bone information to be constructed in a way that the skinnedModel.fx can process. All of the code that processes the bones for the shader is in SkinnedModelProcessor.cs and you can probably just build that assembly, then reference it from your project.
It also appears the reason my model seemed to have 2 meshes is because it sortof does. It has a continuous mesh that isn't animating, and separate mesh objects that make up the armor that do animate. So it appears they don't handle multiple bones influencing a single mesh, and that is why their mechs are broken up into pieces that are attached to a single bone. Which is probably what you already figured out and why you were trying to use the skinning example, but it flew over my head until now. The model I'm using is a minitrooper I got from here.
Anyway, I'm a little bummed that their code doesn't handle a continuous mesh and our only option is the skinning shader. It's not a huge deal but the shader only handles 59 bones and the content has to be processed by the pipeline from their sample. I guess it will all be good when you get it integrated to the robot game. ;)
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Still trying to get skinned models to work in RobotGame.
|
McLovin: If you look in GameLevel.cs around line 945 in the function CreatePlayer, you will see:
Matrix rot = Matrix.CreateRotationX(MathHelper.ToRadians(-90.0f));
player.SetRootAxis(rot);
Aha! I knew there had to be something strange like this in there somewhere.
McLovin:Once I get this thing working consistently, I'll post the code again.
Nice! I'll be looking forward to it!
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Still trying to get skinned models to work in RobotGame.
|
McLovin:Oh, I also meant to ask you if you're running your model through the content parser from the skinning sample?
I tried to. Once I realized it was the shader that was actually doing the "skinning", I decided I should find out if the bones data from RobotGame will be able to drive the animation hence all the bone data problems I've been having the last couple of days. I'm at a point now where I have to try and figure out what is different between the bone data generated by RobotGame and the one the skinning example processor passes in to the shader so I can somehow match the format and get the skinned model to "skin" using the bones from RobotGame.
McLovin:It also appears the reason my model seemed to have 2 meshes is because it sortof does. It has a continuous mesh that isn't animating, and separate mesh objects that make up the armor that do animate.
This may mean that skinning is not yet working on your machine. Objects attached directly to bones as children are probably getting transformed during RobotGame's own animation process while the skin is hanging out by itself. I was thinking about attaching some boxes to my skinned model where the bones are so I can see if the bones are at least animating right. I think you may have just answered the question for me. I may still go ahead and do this anyway so I can see the bones too.
McLovin:the content has to be processed by the pipeline from their sample.
I don't think this is necessarily true because at the end of the day bone matrixes should be bone matrixes no matter what. I guess the trick is figuring out where in RobotGame's rendering pipe to siphon off the bone data so when it's passed in to the shader it drives the skinned mesh properly.
At least that's my theory at the moment anyway... :p
|
|
-
-
- (0)
-
premium membership
-
Posts
12
|
|
Here's my latest version with (hopefully) fewer bugs:
| using System; |
| using System.Collections.Generic; |
| using System.Text; |
| using System.IO; |
| using System.Xml; |
| using System.Xml.Serialization; |
| using Microsoft.Xna.Framework; |
| using Microsoft.Xna.Framework.Content.Pipeline; |
| using Microsoft.Xna.Framework.Content.Pipeline.Graphics; |
| using RobotGameData.GameObject; |
| |
| namespace AnimationBuilder |
| { |
| class Program |
| { |
| static AnimationSequence GetSequence(AnimationContent animContent) |
| { |
| AnimationSequence seq = new AnimationSequence(); |
| |
| seq.Duration = (float)animContent.Duration.TotalSeconds; |
| seq.KeyFrameSequenceCount = animContent.Channels.Count; |
| if (seq.KeyFrameSequenceCount == 0) |
| return null; |
| |
| seq.KeyFrameSequences = new List<KeyFrameSequence>(); |
| |
| foreach (KeyValuePair<string, AnimationChannel> ackvp in animContent.Channels) |
| { |
| KeyFrameSequence keyframeseq = new KeyFrameSequence(); |
| keyframeseq.BoneName = ackvp.Key; |
| keyframeseq.Rotation = new List<Quaternion>(); |
| keyframeseq.HasRotation = true; |
| keyframeseq.Translation = new List<Vector3>(); |
| keyframeseq.HasTranslation = true; |
| keyframeseq.HasTime = false; |
| keyframeseq.Time = new List<float>(); |
| |
| float lastTime = 0.0f, delta = 0.0f, lastDelta = -1.0f; |
| |
| AnimationChannel channel = ackvp.Value; |
| keyframeseq.KeyCount = channel.Count; |
| keyframeseq.Duration = seq.Duration; |
| for (int idx = 0; idx < channel.Count; idx++) |
| { |
| AnimationKeyframe keyframe = channel[idx]; |
| if (keyframe == null) |
| continue; |
| Quaternion q = Quaternion.CreateFromRotationMatrix(keyframe.Transform); |
| q.Normalize(); |
|
| |
| keyframeseq.Rotation.Add(q); |
| keyframeseq.Translation.Add(keyframe.Transform.Translation); |
| keyframeseq.Time.Add((float)keyframe.Time.TotalSeconds); |
| |
| if (idx == 0) |
| lastTime = (float)keyframe.Time.TotalSeconds; |
| else |
| { |
| delta = (float)keyframe.Time.TotalSeconds - lastTime; |
| lastTime = (float)keyframe.Time.TotalSeconds; |
| if (lastDelta < 0.0f) |
| lastDelta = delta; |
| else if (Math.Abs(delta - lastDelta) > 0.001f) |
| keyframeseq.HasTime = true; |
| } |
| } |
| |
| // check for fixed translation |
| if (keyframeseq.Translation.Count > 0) |
| { |
| keyframeseq.FixedTranslation = true; |
| Vector3 lastTranslation = keyframeseq.Translation[0]; |
| foreach (Vector3 translation in keyframeseq.Translation) |
| { |
| if (lastTranslation != translation) |
| { |
| keyframeseq.FixedTranslation = false; |
| break; |
| } |
| } |
| if (keyframeseq.FixedTranslation) |
| { |
| keyframeseq.Translation.Clear(); |
| keyframeseq.Translation.Add(lastTranslation); |
| } |
| } |
| |
| // check for fixed rotation |
| if (keyframeseq.Rotation.Count > 0) |
| { |
| keyframeseq.FixedRotation = true; |
| Quaternion lastRotation = keyframeseq.Rotation[0]; |
| foreach (Quaternion rotation in keyframeseq.Rotation) |
| { |
| if (lastRotation != rotation) |
| { |
| keyframeseq.FixedRotation = false; |
| break; |
| } |
| } |
| if (keyframeseq.FixedRotation) |
| { |
| keyframeseq.Rotation.Clear(); |
| keyframeseq.Rotation.Add(lastRotation); |
| } |
| } |
| |
| // if we didn't need to save the times (constant key interval), dump them |
| if (!keyframeseq.HasTime) |
| { |
| keyframeseq.Time = null; |
| keyframeseq.KeyInterval = delta; |
| } |
| seq.KeyFrameSequences.Add(keyframeseq); |
| } |
| |
| return seq; |
| } |
| |
| //---------------------------------------------------------------------------- |
| static Dictionary<string, AnimationSequence> LoadAnimationSequence(NodeContent content) |
| { |
| Dictionary<string, AnimationSequence> sequences = new Dictionary<string, AnimationSequence>(); |
| |
| if (content.Animations.Count > 0) |
| { |
| foreach (KeyValuePair<string, AnimationContent> kvp in content.Animations) |
| { |
| sequences.Add(kvp.Key, GetSequence(kvp.Value)); |
| } |
| } |
| else if (content.Children.Count > 0) |
| { |
| foreach (NodeContent child in content.Children) |
| { |
| if (child is BoneContent) |
| { |
| foreach (KeyValuePair<string, AnimationContent> kvp in child.Animations) |
| { |
| sequences.Add(kvp.Key, GetSequence(kvp.Value)); |
| } |
| } |
| } |
| } |
| // else nada |
| |
| return sequences; |
| } |
| |
| //---------------------------------------------------------------------------- |
| static void Main(string[ args) |
| { |
| FbxImporter importer = new FbxImporter(); |
| |
| if (args.Length < 1) |
| { |
| Console.WriteLine("{0} <input.fbx>", AppDomain.CurrentDomain.FriendlyName); // overkill? |
| return; |
| } |
| |
| NodeContent node = null; |
| try |
| { |
| Console.WriteLine("Importing {0}...", args[0]); |
| node = importer.Import(args[0], null); |
| } |
| catch (FileLoadException ex) |
| { |
| Console.WriteLine(ex.ToString()); |
| return; |
| } |
| |
| Console.WriteLine("Parsing Animations..."); |
| Dictionary<string, AnimationSequence> sequences = LoadAnimationSequence(node); |
| if (sequences == null || sequences.Count == 0) |
| { |
| Console.WriteLine("Failed to parse {0}", args[0]); |
| return; |
| } |
| |
| try |
| { |
| foreach (KeyValuePair<string, AnimationSequence> kvp in sequences) |
| { |
| string filename = kvp.Key + ".Animation"; |
| if (File.Exists(filename)) |
| File.Delete(filename); |
| |
| Stream stream = File.OpenWrite(filename); |
| XmlTextWriter writer = new XmlTextWriter(stream, Encoding.UTF8); |
| writer.Formatting = Formatting.Indented; |
| writer.Indentation = 3; |
| XmlSerializer serializer = new XmlSerializer(typeof(AnimationSequence)); |
| |
| Console.WriteLine("writing {0}...", filename); |
| serializer.Serialize(writer, kvp.Value); |
| } |
| } |
| catch (Exception ex) |
| { |
| Console.WriteLine(ex.ToString()); |
| return; |
| } |
| } |
| } |
| } |
| |
It's just a C# console app that takes in the name of the fbx file and writes out the animation(s) it finds. It will overwrite animations that are named the same in the file. (Like if you have Take 001 multiple times). It relies on an assembly from the RobotGame, so you'll have to download that project and build it first before trying to build this one. Anyway, I hope someone finds it useful.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Animation Stripper Code
|
McLovin:Here's my latest version with (hopefully) fewer bugs:
Sweetness! I can't wait to try it out! Gotta get my skinning thing to work first though...
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
I have to put skinning stuff on hold for now...
|
Got a presentation I have to get ready by Monday. But, since McLovin was so kind as to share code with us I figured I should do my part as well. So here goes.
When trying to get a custom player model in to RobotGame, one runs in to the "can't find normal and specular maps" problem because the locations of those maps are passed in through custom tags on the model's FBX files which as far as I can tell is not possible to do without custom tagging model in some strange way I couldn't get my 3D app of choice to do. So, one of the first things I had to do was figure out a way to pass in that information. Turns out another sample game does just that, the Shipgame. So I merged in some stuff from it and it went something like this. first the new model processor:
| #region Using Statements |
| using System; |
| using System.Collections.Generic; |
| using Microsoft.Xna.Framework; |
| using Microsoft.Xna.Framework.Audio; |
| using Microsoft.Xna.Framework.Graphics; |
| using Microsoft.Xna.Framework.Input; |
| using Microsoft.Xna.Framework.Storage; |
| using Microsoft.Xna.Framework.Content; |
| using Microsoft.Xna.Framework.Content.Pipeline.Processors; |
| using Microsoft.Xna.Framework.Content.Pipeline.Graphics; |
| using Microsoft.Xna.Framework.Content.Pipeline; |
| using System.IO; |
| #endregion |
| |
| namespace ShaderEffectPipeline |
| { |
| /// <summary> |
| /// The NewShaderModelProcessor is used to change the material/effect applied |
| /// to a model. |
| /// </summary> |
| [ContentProcessor(DisplayName = "New Shader Effect Model Processor")] |
| public class NewShaderModelProcessor : ModelProcessor |
| { |
| #region Fields |
| |
| public const string TextureMapKey = "Texture"; |
| public const string NormalMapKey = "Bump0"; |
| public const string SpecularMapKey = "Specular0"; |
| |
| String directory; |
|
| #endregion |
| |
| public override ModelContent Process(NodeContent input, |
| ContentProcessorContext context) |
| { |
| if (input == null) |
| { |
| throw new ArgumentNullException("input"); |
| } |
| directory = Path.GetDirectoryName(input.Identity.SourceFilename); |
| |
| PreprocessSceneHierarchy(input); |
| return base.Process(input, context); |
| } |
| |
| /// <summary> |
| /// Recursively calls MeshHelper.CalculateTangentFrames for every MeshContent |
| /// object in the NodeContent scene. This function could be changed to add |
| /// more per vertex data as needed. |
| /// </summary> |
| /// <param name="input">A node in the scene. The function should be called |
| /// with the root of the scene.</param> |
| private void PreprocessSceneHierarchy(NodeContent input) |
| { |
| MeshContent mesh = input as MeshContent; |
| if (mesh != null) |
| { |
| MeshHelper.CalculateTangentFrames(mesh, |
| VertexChannelNames.TextureCoordinate(0), |
| VertexChannelNames.Tangent(0), |
| VertexChannelNames.Binormal(0)); |
| |
| foreach (GeometryContent geometry in mesh.Geometry) |
| { |
| if (false == geometry.Material.Textures.ContainsKey(TextureMapKey)) |
| geometry.Material.Textures.Add(TextureMapKey, |
| new ExternalReference<TextureContent>( |
| "Shaders/null_color.tga")); |
| |
| if (false == geometry.Material.Textures.ContainsKey(NormalMapKey)) |
| geometry.Material.Textures.Add(NormalMapKey, |
| new ExternalReference<TextureContent>( |
| "Shaders/null_normal.tga")); |
| |
| if (false == geometry.Material.Textures.ContainsKey(SpecularMapKey)) |
| geometry.Material.Textures.Add(SpecularMapKey, |
| new ExternalReference<TextureContent>( |
| "Shaders/null_specular.tga")); |
| } |
| |
| } |
| |
| foreach (NodeContent child in input.Children) |
| { |
| PreprocessSceneHierarchy(child); |
| } |
| } |
| |
| // acceptableVertexChannelNames are the inputs that the normal map effect |
| // expects. The NewShaderModelProcessor overrides ProcessVertexChannel |
| // to remove all vertex channels which don't have one of these four |
| // names. |
| static IList<string> acceptableVertexChannelNames = |
| new string[ |
| { |
| VertexChannelNames.TextureCoordinate(0), |
| VertexChannelNames.Normal(0), |
| VertexChannelNames.Binormal(0), |
| VertexChannelNames.Tangent(0) |
| }; |
| |
| /// <summary> |
| /// As an optimization, ProcessVertexChannel is overriden to remove data which |
| /// is not used by the vertex shader. |
| /// </summary> |
| /// <param name="geometry">the geometry object which contains the |
| /// vertex channel</param> |
| /// <param name="vertexChannelIndex">the index of the vertex channel |
| /// to operate on</param> |
| /// <param name="context">the context that the processor is operating |
| /// under. in most cases, this parameter isn't necessary; but could |
| /// be used to log a warning that a channel had been removed.</param> |
| protected override void ProcessVertexChannel(GeometryContent geometry, |
| int vertexChannelIndex, ContentProcessorContext context) |
| { |
| String vertexChannelName = |
| geometry.Vertices.Channels[vertexChannelIndex].Name; |
| |
| // if this vertex channel has an acceptable names, process it as normal. |
| if (acceptableVertexChannelNames.Contains(vertexChannelName)) |
| { |
| base.ProcessVertexChannel(geometry, vertexChannelIndex, context); |
| } |
| // otherwise, remove it from the vertex channels; it's just extra data |
| // we don't need. |
| else |
| { |
| geometry.Vertices.Channels.Remove(vertexChannelName); |
| } |
| } |
| |
| protected override MaterialContent ConvertMaterial(MaterialContent material, |
| ContentProcessorContext context) |
| { |
| EffectMaterialContent shaderMaterial = new EffectMaterialContent(); |
| shaderMaterial.Effect = new ExternalReference<EffectContent> |
| ("Shaders/NewShaderModelEffect.fx"); |
| |
| // copy the textures in the original material to the new normal mapping |
| // material. this way the diffuse texture is preserved. The |
| // PreprocessSceneHierarchy function has already added the normal map |
| // texture to the Textures collection, so that will be copied as well. |
| foreach (KeyValuePair<String, ExternalReference<TextureContent>> texture |
| in material.Textures) |
| { |
| shaderMaterial.Textures.Add(texture.Key, texture.Value); |
| } |
| |
| // and convert the material using the ShaderMaterialProcessor, |
| // who has something special in store for the normal map. |
| //return context.Convert<MaterialContent, MaterialContent> (shaderMaterial, typeof(ShaderMaterialProcessor).Name); |
| return context.Convert<MaterialContent, MaterialContent> (shaderMaterial, typeof(MaterialProcessor).Name); |
| } |
| } |
| } |
The key parts are the following:
| public const string TextureMapKey = "Texture"; |
| public const string NormalMapKey = "Bump0"; |
| public const string SpecularMapKey = "Specular0"; |
and
| foreach (GeometryContent geometry in mesh.Geometry) |
| { |
| if (false == geometry.Material.Textures.ContainsKey(TextureMapKey)) |
| geometry.Material.Textures.Add(TextureMapKey, |
| new ExternalReference<TextureContent>( |
| "Shaders/null_color.tga")); |
| |
| if (false == geometry.Material.Textures.ContainsKey(NormalMapKey)) |
| geometry.Material.Textures.Add(NormalMapKey, |
| new ExternalReference<TextureContent>( |
| "Shaders/null_normal.tga")); |
| |
| if (false == geometry.Material.Textures.ContainsKey(SpecularMapKey)) |
| geometry.Material.Textures.Add(SpecularMapKey, |
| new ExternalReference<TextureContent>( |
| "Shaders/null_specular.tga")); |
| } |
and
| EffectMaterialContent shaderMaterial = new EffectMaterialContent(); |
| shaderMaterial.Effect = new ExternalReference<EffectContent> |
| ("Shaders/NewShaderModelEffect.fx"); |
Note that the path to your version of the shader may differ from mine which happens to be "Shaders\NewShaderModelEffect.fx".
Then my goal was to put normal maps on the level geometry so in GameWorld.cs:
| public override void Initialize() |
| { |
| //Add normal mapping to world model. |
| RenderingCustomEffect += new EventHandler<RenderingCustomEffectEventArgs>(OnEffectProcess); |
| |
| base.Initialize(); |
| } |
Then make sure it has a custom effect event handler:
| /// <summary> |
| /// set all effect parameters and process the model's effect. |
| /// </summary> |
| void OnEffectProcess(object sender, GameModel.RenderingCustomEffectEventArgs e) |
| { |
| RenderLighting lighting = e.RenderTracer.Lighting; |
| |
| e.Effect.Parameters["LightColor"].SetValue( |
| lighting.diffuseColor.ToVector4()); |
| |
| e.Effect.Parameters["AmbientLightColor"].SetValue( |
| lighting.ambientColor.ToVector4()); |
| |
| e.Effect.Parameters["Shininess"].SetValue(1.0f); |
| |
| e.Effect.Parameters["SpecularPower"].SetValue(12.0f); |
| |
| e.Effect.Parameters["EnvironmentMap"].SetValue(TextureCubeMap); |
| |
| e.Effect.Parameters["World"].SetValue(e.World); |
| |
| e.Effect.Parameters["View"].SetValue(e.RenderTracer.View); |
| |
| e.Effect.Parameters["Projection"].SetValue(e.RenderTracer.Projection); |
| |
| e.Effect.Parameters["LightPosition"].SetValue( |
| Vector3.Negate(lighting.direction) * 1000); |
| } |
Then we need to edit the .fx file. I created a new copy and put in the following:
| float4x4 World; |
| float4x4 View; |
| float4x4 Projection; |
| |
| // ***** Light properties ***** |
| float3 LightPosition; |
| float4 LightColor; |
| float4 AmbientLightColor; |
| // **************************** |
| |
| // ***** material properties ***** |
| // output from phong specular will be scaled by this amount |
| float Shininess; |
| |
| // specular exponent from phong lighting model. controls the "tightness" of |
| // specular highlights. |
| float SpecularPower; |
| // ******************************* |
| |
| // NormalMap texture sampler |
| texture2D Bump0; |
| sampler2D NormalMapSampler = sampler_state |
| { |
| Texture = <Bump0>; |
| MinFilter = linear; |
| MagFilter = linear; |
| MipFilter = linear; |
| }; |
| |
| // SpecularMap texture sampler |
| texture Specular0; |
| sampler2D SpecularMapSampler = sampler_state |
| { |
| Texture = <Specular0>; |
| MinFilter = linear; |
| MagFilter = linear; |
| MipFilter = linear; |
| }; |
| |
| // Diffuse texture sampler |
| texture2D Texture; |
| sampler2D DiffuseTextureSampler = sampler_state |
| { |
| Texture = <Texture>; |
| MinFilter = linear; |
| MagFilter = linear; |
| MipFilter = linear; |
| }; |
| |
| // Environment-Cube Map |
| texture EnvironmentMap; |
| samplerCUBE EnvironmentMapSampler = sampler_state |
| { |
| Texture = <EnvironmentMap>; |
| }; |
| |
| struct VS_INPUT |
| { |
| float4 position : POSITION0; |
| float2 texCoord : TEXCOORD0; |
| float3 normal : NORMAL0; |
| float3 binormal : BINORMAL0; |
| float3 tangent : TANGENT0; |
| }; |
| |
| // output from the vertex shader, and input to the pixel shader. |
| // lightDirection and viewDirection are in world space. |
| // NOTE: even though the tangentToWorld matrix is only marked |
| // with TEXCOORD3, it will actually take TEXCOORD3, 4, and 5. |
| struct VS_OUTPUT |
| { |
| float4 position : POSITION0; |
| float2 texCoord : TEXCOORD0; |
| float3 lightDirection : TEXCOORD1; |
| float3 viewDirection : TEXCOORD2; |
| float3x3 tangentToWorld : TEXCOORD3; |
| }; |
| |
| VS_OUTPUT VertexShader( VS_INPUT input ) |
| { |
| VS_OUTPUT output; |
| |
| // transform the position into projection space |
| float4 worldSpacePos = mul(input.position, World); |
| output.position = mul(worldSpacePos, View); |
| output.position = mul(output.position, Projection); |
| |
| // calculate the light direction ( from the surface to the light ), which is not |
| // normalized and is in world space |
| output.lightDirection = LightPosition - worldSpacePos; |
| |
| // similarly, calculate the view direction, from the eye to the surface. not |
| // normalized, in world space. |
| float3 eyePosition = mul(-View._m30_m31_m32, transpose(View)); |
| output.viewDirection = worldSpacePos - eyePosition; |
| |
| // calculate tangent space to world space matrix using the world space tangent, |
| // binormal, and normal as basis vectors. the pixel shader will normalize these |
| // in case the world matrix has scaling. |
| output.tangentToWorld[0] = mul(input.tangent, World); |
| output.tangentToWorld[1] = mul(input.binormal, World); |
| output.tangentToWorld[2] = mul(input.normal, World); |
| |
| // pass the texture coordinate through without additional processing |
| output.texCoord = input.texCoord; |
| |
| return output; |
| } |
| |
| float4 PixelShader( VS_OUTPUT input ) : COLOR0 |
| { |
| // look up the normal from the normal map, and transform from tangent space |
| // into world space using the matrix created above. normalize the result |
| // in case the matrix contains scaling. |
| float3 normalFromMap = tex2D(NormalMapSampler, input.texCoord); |
| normalFromMap = mul(normalFromMap, input.tangentToWorld); |
| normalFromMap = normalize(normalFromMap); |
| |
| // clean up our inputs a bit |
| input.viewDirection = normalize(input.viewDirection); |
| input.lightDirection = normalize(input.lightDirection); |
| |
| // use the normal we looked up to do phong diffuse style lighting. |
| float dotLight = max(dot(normalFromMap, input.lightDirection), 0); |
| float4 diffuse = LightColor * dotLight; |
| |
| float3 reflectCoord = reflect( normalFromMap, input.viewDirection); |
| float4 environmentColor = texCUBE( EnvironmentMapSampler, reflectCoord); |
| |
| // use phong to calculate specular highlights: reflect the incoming light |
| // vector off the normal, and use a dot product to see how "similar" |
| // the reflected vector is to the view vector. |
| float3 reflectedLight = reflect(input.lightDirection, normalFromMap); |
| float dotView = max(dot(reflectedLight, input.viewDirection), 0); |
| |
| float4 specularColor = tex2D( SpecularMapSampler, input.texCoord) * |
| float4(2, 2, 2, 255); |
| |
| float3 specular = specularColor.rgb * pow(dotView, specularColor.a); |
| |
| float4 diffuseTexture = tex2D(DiffuseTextureSampler, input.texCoord); |
| |
| AmbientLightColor.rgb = AmbientLightColor.rgb * environmentColor.rgb; |
| |
| // return the combined result. |
| return float4( (diffuse.rgb + AmbientLightColor.rgb) * diffuseTexture.rgb + |
| specular.rgb, diffuseTexture.a); |
| } |
| |
| Technique NormalMapping |
| { |
| Pass Go |
| { |
| VertexShader = compile vs_1_1 VertexShader(); |
| PixelShader = compile ps_2_a PixelShader(); |
| } |
| } |
Make sure the new level geometry is set to be processed using the new model processor. Then in the 3D app of your choice make sure that the specular map and normal maps are set in a standard shader/material. In 3DS Max, my app of choice, I set the "Diffuse Color", "Specular Color", and "Bump" map slots with the proper textures then strip the path name so the final app doesn't try to look for the textures in something like "C:\whatever". Rebuild and voila! Your level geometry should have normal maps working on them albeit a bit glossy.
This same trick works for units and player models as well by making sure which ever class the object happens to be have custom effect event handlers and the model gets processed with the new model processor. I replaced the tanks in my version of RobotGame with a model which has normal maps for instance.
Well, that's it from me for now. I might have left something out so if you find it not working for you drop a line and I'll be sure to respond. I should be getting back to getting skinning working again next week. :)
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Is this thread still being moderated?
|
I posted some code earlier today and it still haven't showed up yet. What gives?
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Animation Stripper Code
|
McLovin:Here's my latest version with (hopefully) fewer bugs:
McLovin, how are you building the animation builder app? Are you starting a new console program project within the RobotGame solution then using the command line to fire up the app after it compiles? Or is it a part of RobotGameProcessors? I'd like to go ahead and build your code but I'm not 100% on how I should go about doing it.
Thanks!
|
|
-
-
- (736)
-
premium membership
Team XNA
-
Posts
717
|
Re: Is this thread still being moderated?
|
pixelminerXNA:I posted some code earlier today and it still haven't showed up yet. What gives?
Moderation on the forums decided your code needed review before posting - just got a chance to look at it. Posted now.
Sean Jenkin | an XNA alumni, now hanging out at MSDN and TechNet...
|
|
-
-
- (0)
-
premium membership
-
Posts
12
|
Re: Animation Stripper Code
|
pixelminerXNA:
McLovin:Here's my latest version with (hopefully) fewer bugs:
McLovin, how are you building the animation builder app? Are you starting a new console program project within the RobotGame solution then using the command line to fire up the app after it compiles? Or is it a part of RobotGameProcessors? I'd like to go ahead and build your code but I'm not 100% on how I should go about doing it.
Thanks!
yes, just start a new console app then add the reference to RobotGameData.dll (from robotgame), plus the XNA ones and you should be good.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Is this thread still being moderated?
|
Jenkmeister:Moderation on the forums decided your code needed review before posting - just got a chance to look at it. Posted now.
Thanks Jenkmeister!
Just for future reference, is there some guide lines about what might get a posting flagged? Was my post too long for instance?
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Animation Stripper Code
|
McLovin:yes, just start a new console app then add the reference to RobotGameData.dll (from robotgame), plus the XNA ones and you should be good.
Coolness! I'll go ahead and give that a try. :D
|
|
-
-
- (736)
-
premium membership
Team XNA
-
Posts
717
|
Re: Is this thread still being moderated?
|
pixelminerXNA: Jenkmeister:Moderation on the forums decided your code needed review before posting - just got a chance to look at it. Posted now.
Thanks Jenkmeister!
Just for future reference, is there some guide lines about what might get a posting flagged? Was my post too long for instance?
It was a long post with a lot of code, so that might be it. However, the automatic moderation of the posts is a mystery to all ;-)
Sean Jenkin | an XNA alumni, now hanging out at MSDN and TechNet...
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Animation Stripper Code
|
McLovin!
Dude! I got a player model to play back a custom animation using your animation processor. I did have to tweak a few things to make it work though. Here is what ended up working for me:
| for (int idx = 0; idx < channel.Count; idx++) |
| { |
| AnimationKeyframe keyframe = channel[idx]; |
| if (keyframe == null) |
| continue; |
| Quaternion q = Quaternion.CreateFromRotationMatrix(keyframe.Transform); |
| q.Normalize(); |
| |
| if (keyframeseq.BoneName == "Root") |
| { |
| Matrix m = Matrix.CreateFromQuaternion(q); |
| //It's weird, even though XNA is Y-up, I guess the data in the FBX file is still Z-up at this point. |
| //So, I still need to use Z as the up axis when performing the rotation and translation to put the model |
| //where it needs to be. |
| Matrix r = Matrix.CreateRotationZ(MathHelper.ToRadians(90.0f)); |
| m = Matrix.Multiply(m, r); |
| q = Quaternion.CreateFromRotationMatrix(m); |
| q.Normalize(); |
| keyframeseq.Rotation.Add(q); |
| |
| //I have to figure out a way to get the Z transform of the root bone from the importer somehow. |
| Vector3 moveUp = new Vector3(0.0f, 0.0f, 1.86f); |
| Vector3 movedTrans = Vector3.Add(keyframe.Transform.Translation, moveUp); |
| keyframeseq.Translation.Add(movedTrans); |
| } |
| else |
| { |
| keyframeseq.Rotation.Add(q); |
| keyframeseq.Translation.Add(keyframe.Transform.Translation); |
| } |
So when I fired up your original code as is and applied an animation it exported to a player model, it came in sunken in to the ground and rotated -90 degrees in the up axis, that being Y in the game since XNA is Y-up. By tweaking the code as above, the player model now comes in on its feet facing away from the screen as it should!
I think what was happening is that the animation keys in the FBX files are relative to it's position in the hierarchy. And since keys for Root would have to be relative to itself being the top of the hierarchy, it does not record the offset from the ground but instead just how much it moved relative to its world location hence the model ending up buried half way in the ground to where the model's root is located. I think this is a hack the guys who created RobotGame may have in the exporter they are not sharing with us. Instead of applying the bone animation in the game with the root transform of the bone applied, they instead opted to bake the offsets in to the animation file and just play back the bone animations as is. I guess it's one less calculation to do at run-time.
And, there you have it. It's now possible to apply custom animations to player models too. Thanks, McLovin! :)
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Animation Stripper Code
|
Almost forgot, I also changed some lines in main() so it uses the file name of the FBX file as the file name of the exported animation. Like so:
| foreach (KeyValuePair<string, AnimationSequence> kvp in sequences) |
| { |
| //string filename = kvp.Key + ".Animation"; |
| string filename = args[0]; |
| filename = filename.Remove(filename.Length - 4); |
| filename = filename + ".Animation"; |
| if (File.Exists(filename)) |
| File.Delete(filename); |
| |
| Stream stream = File.OpenWrite(filename); |
| XmlTextWriter writer = new XmlTextWriter(stream, Encoding.UTF8); |
| writer.Formatting = Formatting.Indented; |
| writer.Indentation = 3; |
| XmlSerializer serializer = new XmlSerializer(typeof(AnimationSequence)); |
| |
| Console.WriteLine("writing {0}...", filename); |
| serializer.Serialize(writer, kvp.Value); |
| } |
I did this because, as far as I know, no off-the-shelf FBX exporter supports exporting multiple animations clips with different names inside a single FBX file and I'm not about to go write a custom one. The FBX exporter I use which is the "official" one from Autodesk doesn't even support it. So, I think it would be better to just have multiple FBX files with a single animation in them each. This way it's possible to just batch process them at the command line without having to do things like opening up each FBX in a text editor and copying and pasting huge chunks of ASCII back and forth. And since it's possible to export out .FBX files with just the animation, no space needs to be wasted exporting and processing the geometry information over and over again. But, I guess this would mean Z offsets of each model would need to be passed in or calculated in-game.
Now to go off and do some custom animations to replace all the stock ones! Oh, wait... I gotta get skinning working first... :p
BTW, the latest versions of Autodesk's FBX exporters hides much of the geometry portion of its UI making it not possible to just export out animations when you start it up for the first time. Just press the "Edit..." button in the lower left hand corner of the FBX export window and chose "Edit presets...". You'll be able to turn those options back on again.
|
|
-
-
- (0)
-
premium membership
-
Posts
12
|
Re: Animation Stripper Code
|
pixelminerXNA:Now to go off and do some custom animations to replace all the stock ones! Oh, wait... I gotta get skinning working first... :p
Glad to hear you got the code working with minimal work. I've actually kind of given up on using the engine as it is for skinned models, but I have been playing around with the Animation Component Library for XNA. You can read about it and download it on codeplex here. Seems like it has been out for quite some time and so far it seems like it has some potential. I'm not sure if I'd integrate it with the RobotGame or just start from scratch though.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
Re: Animation Stripper Code
|
McLovin:I've actually kind of given up on using the engine as it is for skinned models
Yeah. I'll have to agree with you here. At first I thought it might be a simple matter of massaging the bone matrixes to the proper format but after giving the code a more serious look I'm coming to the realization that to make skinned animation work in RobotGame will mean a total overhaul of the game object and animation system from the ground up. I was actually going through RobotGame to learn XNA in preparation for Torque X 3D builder's release. And seeing how Torque X 3D builder's release is literally a month or two away, I think I'll put implementing skinning in RobotGame on the back burner and try my hand at integrating the XNA animation library you mentioned in to the simple engine of mine which ironically has skinning animation working in it already. Then once Torque X 3D builder ships I'll get going on making my game with the knowledge I gained from digging through RobotGame. I also think there are parts of RobotGame which would make a good edition to the engine I'm rolling from scratch as well. The collision system comes to mind for instance.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
After giving up once and thinking I won't bother, I bit the bullet and worked through the skinning example and it finally works! Take a look:

The model is a space marine from a book on XNA and he's hopping up and down! The guns are crossed because the bind pose for the space marine is different then the Mark robot the gun idle animation is based on.
Implementing the functionality turned out to be not as difficult as I thought. The real problem was getting the set up of the model properly so the model won't break apart once it's running inside the game. I'll go post more about this tonight with the little bit of extra code it took to make it work.
Since it's finally working, it'll now be possible to start looking at supporting other features of RobotGame like animation blending and attaching weapons so it works with skinned models!
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
Okay, let's get it working.
First, copy the "SkinnedModel" files over to RobotGame. I chose to coppy AnimationClip.cs, AnimationPlayer.cs, Keyframe.cs, SkinningData.cs, and TypeReaders.cs in to RobotGameData\Object\Animation. Luckily, there are no name clashes with the pre-existing animation system. Add the files in to RobotGame then change the name space to RobotGameData.GameObject.
| namespace RobotGameData.GameObject |
Then add the following to GameAnimateModel.cs in the Fields region.
| //For skin anims |
| bool iAmSkinned = false; |
| AnimationPlayer animationPlayer; |
| SkinningData skinningData; |
Then expose the animation player by adding the following to the Properties region.
| public AnimationPlayer AnimationPlayer |
| { |
| get { return animationPlayer; } |
| } |
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
Continuing with GameAnimateModel.cs, add calls to the constructors to call a method to initialize the skin animation data.
| /// <summary> |
| /// Constructor. |
| /// </summary> |
| /// <param name="resource">model resource</param> |
| public GameAnimateModel(GameResourceModel resource) |
| : base(resource) |
| { |
| InitSkinAnim(); |
| } |
| |
| /// <summary> |
| /// Constructor. |
| /// </summary> |
| /// <param name="fileName">model file name</param> |
| public GameAnimateModel(string fileName) |
| : base(fileName) |
| { |
| InitSkinAnim(); |
| } |
Then, add the method itself just underneath.
| public void InitSkinAnim() |
| { |
| if (this.ModelData.model.Tag != null) |
| { |
| iAmSkinned = true; |
| skinningData = this.ModelData.model.Tag as SkinningData; |
| if (skinningData == null) throw new InvalidOperationException("This model does not contain a SkinningData tag."); |
| |
| // Create an animation player, and start decoding an animation clip. |
| animationPlayer = new AnimationPlayer(skinningData); |
| AnimationClip clip = skinningData.AnimationClips["Take 001"]; |
| animationPlayer.StartClip(clip); |
| } |
| } |
Then, at the end of the OnUpdate method after the call to the base OnUpdate, add the following.
| base.OnUpdate(gameTime); |
| |
| //Skinned animation stuff. |
| if (iAmSkinned == true) |
| { |
| animationPlayer.Update(gameTime.ElapsedGameTime, true, Matrix.Identity); |
| } |
.And that'll do it for GameAnimateModel. Moving on.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
Okay. Now open AnimationPlayer.cs and add the following to the Fields region.
| //This is to make the model stand up. |
| Matrix skinnedModelRootRotFix = Matrix.CreateRotationX(MathHelper.ToRadians(90.0f)); |
Then in UpdateWorldTransforms add a multiplication of the fixer matrix to the end of the root bone value.
| public void UpdateWorldTransforms(Matrix rootTransform) |
| { |
| // Root bone. |
| worldTransforms[0] = boneTransforms[0] * rootTransform * skinnedModelRootRotFix; |
| |
(BTW, I'm guessing this is something which will not be necessary when the non-skinning animation system's bone animation system is evnetually bypassed for the skinned animation system's bones.)
Then, pass the animated bones in to the shader in GamePlayer.cs through OnEffectProcess, the custom effect processor method.
| |
| e.Effect.Parameters["Bones"].SetValue(this.AnimationPlayer.GetSkinTransforms()); |
Now on to the shader.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
So, the animated model shader in the skinning example doesn't take in to account the world matrix which means the model animates in place but will not move off the spot it's standing on. The following shader fixes that.
(Note that Light1Color, Light1Direction, and AmbientColor is being passed in through the OnEffectProcess method.)
| //----------------------------------------------------------------------------- |
| // SkinnedModelEffect.fx |
| //----------------------------------------------------------------------------- |
| // Maximum number of bone matrices we can render using shader 2.0 in a single pass. |
| // If you change this, update SkinnedModelProcessor.cs to match. |
| #define MaxBones 59 |
| |
| // Input parameters. |
| float4x4 World; |
| float4x4 View; |
| float4x4 Projection; |
| |
| float4x4 Bones[MaxBones]; |
| |
| float3 Light1Color; |
| float3 Light1Direction; |
| float3 AmbientColor; |
| |
| texture Texture; |
| |
| sampler Sampler = sampler_state |
| { |
| Texture = (Texture); |
| MinFilter = Linear; |
| MagFilter = Linear; |
| MipFilter = Linear; |
| }; |
| |
| // Vertex shader input structure. |
| struct VS_INPUT |
| { |
| float4 Position : POSITION0; |
| float3 Normal : NORMAL0; |
| float2 TexCoord : TEXCOORD0; |
| float4 BoneIndices : BLENDINDICES0; |
| float4 BoneWeights : BLENDWEIGHT0; |
| }; |
| |
| // Vertex shader output structure. |
| struct VS_OUTPUT |
| { |
| float4 Position : POSITION0; |
| float3 Lighting : COLOR0; |
| float2 TexCoord : TEXCOORD0; |
| }; |
| |
| // Vertex shader program. |
| VS_OUTPUT VertexShader(VS_INPUT input) |
| { |
| VS_OUTPUT output; |
| |
| // Blend between the weighted bone matrices. |
| float4x4 skinTransform = 0; |
| |
| skinTransform += Bones[input.BoneIndices.x] * input.BoneWeights.x; |
| skinTransform += Bones[input.BoneIndices.y] * input.BoneWeights.y; |
| skinTransform += Bones[input.BoneIndices.z] * input.BoneWeights.z; |
| skinTransform += Bones[input.BoneIndices.w] * input.BoneWeights.w; |
| |
| // Skin the vertex position. |
| float4 position = mul(input.Position, skinTransform); |
| |
| //Worldspace the vertex position. |
| position = mul(position, World); |
| |
| output.Position = mul(mul(position, View), Projection); |
| |
| // Skin the vertex normal, then compute lighting. |
| float3 normal = normalize(mul(input.Normal, skinTransform)); |
| |
| float3 light1 = max(dot(normal, Light1Direction), 0) * Light1Color; |
| |
| output.Lighting = light1 + AmbientColor; |
| |
| output.TexCoord = input.TexCoord; |
| |
| return output; |
| } |
| |
| // Pixel shader input structure. |
| struct PS_INPUT |
| { |
| float3 Lighting : COLOR0; |
| float2 TexCoord : TEXCOORD0; |
| }; |
| |
| // Pixel shader program. |
| float4 PixelShader(PS_INPUT input) : COLOR0 |
| { |
| float4 color = tex2D(Sampler, input.TexCoord); |
| color.rgb *= input.Lighting; |
| |
| return color; |
| } |
| |
| technique SkinnedModelTechnique |
| { |
| pass SkinnedModelPass |
| { |
| VertexShader = compile vs_2_0 VertexShader(); |
| PixelShader = compile ps_2_0 PixelShader(); |
| } |
| } |
| |
The shader as a "shader" is actually pretty slim. I will add normal mapping to it ASAP. I'm calling mine SkinnedModelEffect.fx. You can use what ever you'd like but you have to make sure that the proper name is given at the end of the model processor in ConvertMaterial. (Note my shaders are in a "shaders" folder inside the Content folder. Yours may vary.)
| // Store a reference to our skinned mesh effect. |
| string effectPath = Path.GetFullPath("Shaders/SkinnedModelEffect.fx"); |
Oh, yeah. That reminds me, the model processors.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
You'll want to make put a copy of SkinnedModelProcessor.cs and TypeWriters.cs to RobotGame's processors. Personally, I have my files named SkinnedModelProcessor.cs and SkinnedModelDataWriter.cs and have them in the ShaderEffectPipeline folder under RobotGameProcessors. Make sure to change the namespace.
| namespace ShaderEffectPipeline |
Then, as I mentioned above, make sure the model processor is pointing at the right shader.
| // Store a reference to our skinned mesh effect. |
| string effectPath = Path.GetFullPath("Shaders/SkinnedModelEffect.fx"); |
Then make sure the player model file, in my case Mark.FBX, is set to process with the right model processor.
And I think that does it for code but we're not done just yet. One last thing.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
And the last thing of course is the actual model file itself.
My model comes out of Max. And in Max the model has to be facing down positive Y. And since I'm using Character Studio Bipeds as my skeleton, this means there is a 90 degree rotation in the Z rotation channel of the root node. (This might be a -90 degree. What ever makes the biped face down positive Y.)
I'm not sure what this would mean to where the model needs to be facing in other 3D packages. I think the smart thing to do would be import dude.fbx in to your 3D package of choice and use that as a guide.
And that's it. I wouldn't be surprised if I forgot something so holler if something isn't working right.
|
|
-
-
- (0)
-
premium membership
-
Posts
187
|
|
If your model does not come in centered in the game this means that there may be transforms in the character geometry in your 3D app. It's important to make sure the geometry that's being skinned is at (0, 0, 0), has no rotation, and is scaled at 100%, or 1.000 depending on the 3D app, before skinning begins.
In max, I had to create a box, make sure it's at 0,0,0, not rotated, and scaled 100% in xyz, collapse it in to a Editable Poly, blow away the faces making up the box, then attach the character's geometry in to it. Once I did and skin'ed the model, the model came in centered between the guns in the game. I had to re-skin the model three times before I figured this out so make sure you have the character geometry properly set up before you working on skin weights because it will all become invalid if the geometry's transform needs to be zeroed out after skinning weights have been applied.
|
|
|