Blender to XNA: Skinned Model Renders Black

Started by
18 comments, last by oxiyoxi 11 years, 6 months ago
Hello all,

I am trying to import a skinned model into the Skinned Sample from Microsoft. The model was built in Blender 2.63a with an animation, a material, and a texture. The model renders perfectly with Blender, but once exported, the model loses all reference to the texture. The model renders black in XNA, and VS doesn't complain about the missing texture in the folder. Anyone know why Blender is not exporting the texture or at least a reference to it? I tried recreating the material and the texture, but to no avail.
Advertisement
What effect are you using to render the model? If it's not a BasicEffect, can we see the shader code?

Also, does VS give you any warnings about the loaded assets?

if you put the associated texture into the <Projectname>Content folder of your project, do you get a warning that it's being compiled twice?

Hazard Pay :: FPS/RTS in SharpDX (gathering dust, retained for... historical purposes)
DeviantArt :: Because right-brain needs love too (also pretty neglected these days)

The shader is the skinned model sample shader, yes, VS would warn me of a missing asset, and no it's not warning me that it's being compiled twice. I am fairly sure Blender is not exporting the texture or any reference to it.
I'm just assuming but I'm 99% sure you're exporting to .fbx since it's a common format for XNA animations and the sample uses this format, right?

Usually the compiler would give an error in the model processing phase of the build when it looks for a texture that is not found. Otherwise, the content processor you're using has a way to handle missing textures and just puts a default one in its place.

Coincidentally, I was fixing up a third-party model I exported from Blender yesterday in trying to get its textures to load too. I didn't export the animations but it was an .fbx format. When done incorrectly, exporting as .fbx would cause the model's texture to have "untitled" in its file path, so you will end up with nothing. You need to check the image list in the UV image editor. Select your mesh, go into Edit Mode and press A to highlight all the vertices. In the UV editor window, choose the image you want to associate the mesh with. Then save the file and export.

There's a chance you also have some unused image datablocks named "untitled" possibly followed with a number, which you will see in the list of images in the UV editor. Shift-click to unlink and remove these, save, and re-open the blend file. It was kind of tricky trying to figure this out myself, I was also struggling for hours figuring out why the model's texture wasn't showing.

Edit: Yep, I checked my original model again and you will get references to "untitled" in the exported file (search for this string in the file with quotes included). You gotta remove any references to those image files as I've mentioned before.

New game in progress: Project SeedWorld

My development blog: Electronic Meteor

Turns out that is not quite the problem. When I open the .fbx in notepad, I find absolutely no reference to a texture whatsoever. I haven't checked about unnecessary links yet, though.

EDIT: I saved, closed, reopened, and exported and now there is a reference to textures... And it does point to the correct file... I don't just get it... Lemme provide a link to the fbx and blend files:

https://dl.dropbox.com/u/10337688/SteveIdle01.blend
https://dl.dropbox.com/u/10337688/SteveIdle01.fbx
What do you see when you load the .fbx with the correct texture names? Does the new exported file work correctly? I checked the .fbx, and do see a texture name in RelativeFilename of the texture object, and it has a connection with the texture to the HumanMesh model.

New game in progress: Project SeedWorld

My development blog: Electronic Meteor

Still black...
You may want to post the content processing code for the model to see if that's the possible problem.

New game in progress: Project SeedWorld

My development blog: Electronic Meteor

From SkinnedModelProcessor (Modified):

[spoiler]

#region File Description
//-----------------------------------------------------------------------------
// SkinnedModelProcessor.cs
//
// Microsoft XNA Community Game Platform
// Copyright © Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using SkinnedModel;
#endregion

namespace SkinnedModelPipeline
{
/// <summary>
/// Custom processor extends the builtin framework ModelProcessor class,
/// adding animation support.
/// </summary>
[ContentProcessor(DisplayName = "Skinned Model")]
public class SkinnedModelProcessor : ModelProcessor
{
/// <summary>
/// The main Process method converts an intermediate format content pipeline
/// NodeContent tree to a ModelContent object with embedded animation data.
/// </summary>
public override ModelContent Process(NodeContent input,
ContentProcessorContext context)
{
RotateAll(input, 90, 0, 180, 2);

ValidateMesh(input, context, null);
// Find the skeleton.
BoneContent skeleton = MeshHelper.FindSkeleton(input);

if (skeleton == null)
throw new InvalidContentException("Input skeleton not found.");

// We don't want to have to worry about different parts of the model being
// in different local coordinate systems, so let's just bake everything.
FlattenTransforms(input, skeleton);

// Read the bind pose and skeleton hierarchy data.
IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);

if (bones.Count > SkinnedEffect.MaxBones)
{
throw new InvalidContentException(string.Format(
"Skeleton has {0} bones, but the maximum supported is {1}.",
bones.Count, SkinnedEffect.MaxBones));
}

List<Matrix> bindPose = new List<Matrix>();
List<Matrix> inverseBindPose = new List<Matrix>();
List<int> skeletonHierarchy = new List<int>();
Dictionary<string, int> boneIndices = new Dictionary<string, int>();

foreach (BoneContent bone in bones)
{
bindPose.Add(bone.Transform);
inverseBindPose.Add(Matrix.Invert(bone.AbsoluteTransform));
skeletonHierarchy.Add(bones.IndexOf(bone.Parent as BoneContent));
boneIndices.Add(bone.Name, boneIndices.Count);
}

// Convert animation data to our runtime format.
Dictionary<string, AnimationClip> animationClips;
animationClips = ProcessAnimations(skeleton.Animations, bones);

// Chain to the base ModelProcessor class so it can convert the model data.
ModelContent model = base.Process(input, context);

// Store our custom animation data in the Tag property of the model.
model.Tag = new SkinningData(animationClips, bindPose,
inverseBindPose, skeletonHierarchy,
boneIndices);

return model;
}

public static void RotateAll(NodeContent node, float degX, float degY, float degZ, float scaleFactor)
{
Matrix rotate = Matrix.Identity *
Matrix.CreateRotationX(MathHelper.ToRadians(degX)) *
Matrix.CreateRotationY(MathHelper.ToRadians(degY)) *
Matrix.CreateRotationZ(MathHelper.ToRadians(degZ));

Matrix transform = Matrix.Identity * Matrix.CreateScale(scaleFactor) * rotate;
MeshHelper.TransformScene(node, transform);
}

/// <summary>
/// Converts an intermediate format content pipeline AnimationContentDictionary
/// object to our runtime AnimationClip format.
/// </summary>
static Dictionary<string, AnimationClip> ProcessAnimations(
AnimationContentDictionary animations, IList<BoneContent> bones)
{
// Build up a table mapping bone names to indices.
Dictionary<string, int> boneMap = new Dictionary<string, int>();

for (int i = 0; i < bones.Count; i++)
{
string boneName = bones.Name;
if (!string.IsNullOrEmpty(boneName))
boneMap.Add(boneName, i);
}

// Convert each animation in turn.
Dictionary<string, AnimationClip> animationClips;
animationClips = new Dictionary<string, AnimationClip>();

foreach (KeyValuePair<string, AnimationContent> animation in animations)
{
AnimationClip processed = ProcessAnimation(animation.Value, boneMap);

animationClips.Add(animation.Key, processed);
}

if (animationClips.Count == 0)
{
throw new InvalidContentException(
"Input file does not contain any animations.");
}

return animationClips;
}


/// <summary>
/// Converts an intermediate format content pipeline AnimationContent
/// object to our runtime AnimationClip format.
/// </summary>
static AnimationClip ProcessAnimation(AnimationContent animation,
Dictionary<string, int> boneMap)
{
List<Keyframe> keyframes = new List<Keyframe>();

// For each input animation channel.
foreach (KeyValuePair<string, AnimationChannel> channel in
animation.Channels)
{
// Look up what bone this channel is controlling.
int boneIndex;

if (!boneMap.TryGetValue(channel.Key, out boneIndex))
{
throw new InvalidContentException(string.Format(
"Found animation for bone '{0}', " +
"which is not part of the skeleton.", channel.Key));
}

// Convert the keyframe data.
foreach (AnimationKeyframe keyframe in channel.Value)
{
keyframes.Add(new Keyframe(boneIndex, keyframe.Time,
keyframe.Transform));
}
}

// Sort the merged keyframes by time.
keyframes.Sort(CompareKeyframeTimes);

if (keyframes.Count == 0)
throw new InvalidContentException("Animation has no keyframes.");

if (animation.Duration <= TimeSpan.Zero)
throw new InvalidContentException("Animation has a zero duration.");

return new AnimationClip(animation.Duration, keyframes);
}


/// <summary>
/// Comparison function for sorting keyframes into ascending time order.
/// </summary>
static int CompareKeyframeTimes(Keyframe a, Keyframe b)
{
return a.Time.CompareTo(b.Time);
}


/// <summary>
/// Makes sure this mesh contains the kind of data we know how to animate.
/// </summary>
static void ValidateMesh(NodeContent node, ContentProcessorContext context,
string parentBoneName)
{
MeshContent mesh = node as MeshContent;

if (mesh != null)
{
// Validate the mesh.
if (parentBoneName != null)
{
context.Logger.LogWarning(null, null,
"Mesh {0} is a child of bone {1}. SkinnedModelProcessor " +
"does not correctly handle meshes that are children of bones.",
mesh.Name, parentBoneName);
}

if (!MeshHasSkinning(mesh))
{
context.Logger.LogWarning(null, null,
"Mesh {0} has no skinning information, so it has been deleted.",
mesh.Name);

mesh.Parent.Children.Remove(mesh);
return;
}
}
else if (node is BoneContent)
{
// If this is a bone, remember that we are now looking inside it.
parentBoneName = node.Name;
}

// Recurse (iterating over a copy of the child collection,
// because validating children may delete some of them).
foreach (NodeContent child in new List<NodeContent>(node.Children))
ValidateMesh(child, context, parentBoneName);
}


/// <summary>
/// Checks whether a mesh contains skininng information.
/// </summary>
static bool MeshHasSkinning(MeshContent mesh)
{
foreach (GeometryContent geometry in mesh.Geometry)
{
if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights()))
return false;
}

return true;
}


/// <summary>
/// Bakes unwanted transforms into the model geometry,
/// so everything ends up in the same coordinate system.
/// </summary>
static void FlattenTransforms(NodeContent node, BoneContent skeleton)
{
foreach (NodeContent child in node.Children)
{
// Don't process the skeleton, because that is special.
if (child == skeleton)
continue;

// Bake the local transform into the actual geometry.
MeshHelper.TransformScene(child, child.Transform);

// Having baked it, we can now set the local
// coordinate system back to identity.
child.Transform = Matrix.Identity;

// Recurse.
FlattenTransforms(child, skeleton);
}
}


/// <summary>
/// Force all the materials to use our skinned model effect.
/// </summary>
[DefaultValue(MaterialProcessorDefaultEffect.SkinnedEffect)]
public override MaterialProcessorDefaultEffect DefaultEffect
{
get { return MaterialProcessorDefaultEffect.SkinnedEffect; }
set { }
}
}
}


[/spoiler]
Hi Drakken255 try removing the TEXCOORD from the effect file and compile just with the material if it succeed then it is with blender. I know some in directx and not XNA.

This topic is closed to new replies.

Advertisement