托管D3D:使用frame层次
这篇文章假设我们的场景是从.x文件中导入。SDK中提供能支持导入此文件格式的类库。
原文出处:http://www.jkarlsson.com/Articles/loadframes.asp 转载请注明出处,模型及完整源代码请到原文处下载
Directx(.x) 文件
Code
xof 0303txt 0032
Frame Body
{
FrameTransformMatrix
{
1.000000,0.000000,0.000000,0.000000,
0.000000,1.000000,0.000000,0.000000,
0.000000,0.000000,1.000000,0.000000,
-1.401197,0.000000,0.279802,1.000000;;
}
Mesh
{
20;
-29.527559;0.000000;-49.212597;,
}
Frame Turret
{
FrameTransformMatrix
{
1.000000,0.000000,0.000000,0.000000,
0.000000,1.000000,0.000000,0.000000,
0.000000,0.000000,1.000000,0.000000,
1.435083,29.904209,-0.126913,1.000000;;
}
Mesh
{
116;
0.000000;0.000000;0.000000;
}
Frame Gun
{
FrameTransformMatrix
{
1.000000,0.000000,0.000000,0.000000,
0.000000,0.000000,-1.000000,0.000000,
0.000000,1.000000,0.000000,0.000000,
1.016502,4.063328,21.468393,1.000000;;
}
Mesh
{
116;
0.000000;0.000000;0.000000;,
}
}
}
}
整个.x文件比这个要多的多,但是以上是和框架这个议题相关的内容。让我们来看看它们的玄机。
Frame Body {}
它描述名为body的frame。再看看另两个frame,Turret和Gun,Gun嵌套于Turret中,而Turret嵌套与Body中,就这样组成了层次关系。
FrameTransformMatrix {}
当前frame的FrameTransformMatrix{}声明了frame的网格及其子frame的网格将以怎样的规则排列、移动、旋转、这样的话frame将得以与其它几何体以正确的方式放置。考虑以Turret为例,它应该被建模为一个圆柱并放置在车身表面。但如果不为它提供变换的方式,它将可能掉落于车身中。有了变换矩阵,我们就有了足够的信息去正确地放置Turret。
Mesh {}
包括了组成frame的顶点的坐标。和变换矩阵一起使用,我们就有了足够的信息将不同的frame放在一起正确地渲染。
载入Frame层次
让我们开始设计一个能够将对frame层次操作封装起来的类。我们由载入.x文件和在内存里创造实现frame的对象开始。内部存储的场景将被以一个AnimationRootFrame对象保存。它将frame层次保存在FrameHierarchy属性中。它也可以对frame结构完成其它的操作,但这里我们暂时忽略掉它。
我们需要额外写一些代码去提取.x文件,创造frame层次并把它存储在AnimationRootFrame对象中。我们使用静态方法Mesh.LoadHierarchyFromFile。这个方法需要我们通过编写类来实现部分对建立frame层次具有实际意义的操作。我们需要从AllocateHierarchy,Frame,MeshContainer这些抽象类中继承。这篇文章中,我们将重写的类命名为CustomAllocateHierarchy,CustomFrame,和CustomMeshContainer。
代码的第一部分。
Code
public class GraphicsObject
{
//.xfile的物理地址
protected string xFilePath;
//frame的容器
protected AnimationRootFrame rootFrame;
public GraphicsObject(string xFilePath, Device device)
{
this.xFilePath = xFilePath;
this.device = device;
}
// 从文件中导入frame层次
public virtual void Initialize()
{
this.rootFrame = Mesh.LoadHierarchyFromFile(this.xFilePath,
MeshFlags.Managed,
this.device,
new CustomAllocateHierarchy(this.xFilePath),
null);
}
}
静态方法Mesh.LoadHierarchyFromFile 接受一个.x文件 并且返回一个AnimationRootFrame对象,文件中包含了一个frame层次。第一个参数是文件的物理地址。第二个参数决定了网格将以怎样的方式被创建。第三个参数是对渲染设备的引用。第四个参数是对派生类的引用。忽略第五个参数。
Mesh.LoadHierarchyFromFile 期望引用一个从抽象方法AllocateHierarchy派生的对象。类中必须定义CreateFrame方法,将被每一个从.x文件中被检测到的frame调用。这些方法返回的都是抽象类,frame对应CreateFrame方法,MeshContainer对应CreateMeshContainer对象,所以我们将派生这些类。
The AllocateHierarchy Class
CustomAllocateHierarchy类的声明如下。
Code
public class CustomAllocateHierarchy : AllocateHierarchy
{
// 我们将存储文件路径
private string xFilePath;
public CustomAllocateHierarchy(string xFilePath) : base()
{
this.xFilePath = xFilePath;
}
public override Frame CreateFrame(string name)
{
return new CustomFrame(name);
}
public override MeshContainer CreateMeshContainer(string name,
MeshData meshData,
ExtendedMaterial[] materials,
EffectInstance[] effectInstances,
GraphicsStream adjacency,
SkinInformation skinInfo)
{
CustomMeshContainer mc = new CustomMeshContainer(this.xFilePath, name, meshData, materials,
effectInstances, adjacency, skinInfo);
mc.Initialize();
return mc;
}
}
我们的CustomAllocateHierarchy保存了将会载入的.x文件的路径。在待会创建网格对象时将会使用它。马上我们将看到,使用CreateFrame方法创造的CustomFrame类的实例,使用CreateMeshContainer方法创造了CustomMeshContainer类的实例。
Frame类
Code
ublic class CustomFrame : Frame
{
// 变换矩阵让我们可以对frame层次中单个的frame添加变换
protected Matrix customTransform = Matrix.Identity;
public CustomFrame() : base()
{
}
public CustomFrame(string name) : base()
{
this.Name = name;
}
public Matrix CustomTransform
{
get
{
return this.customTransform;
}
set
{
this.customTransform = value;
}
}
}
CustomFrame类为当前Frame定义了变换矩阵。它将用于定制frame层次中单独分支的变化。举个例子,对于坦克模型中的Turret。有了定义在.x文件中的Turretframe和其父frame的变换矩阵,我们就可以正确地渲染Turret。有了添加的定制的矩阵,我们还可以旋转Turrt和Gun。CustomTransform矩阵被初始化为标准矩阵。所以没有定义任何变换。我们将在以后的代码中看到它怎样被用在渲染中。
The MeshContainer Class
MeshContainer类的清单
Code
ublic class CustomMeshContainer : MeshContainer
{
private string xFilePath;
private Texture[] textures;
public CustomMeshContainer(string xFilePath, string name,
MeshData meshData, ExtendedMaterial[] materials,
EffectInstance[] effectInstances, GraphicsStream adjacency,
SkinInformation skinInfo) : base()
{
this.Name = name;
this.MeshData = meshData;
this.SetMaterials(materials);
this.SetEffectInstances(effectInstances);
this.SetAdjacency(adjacency);
this.SkinInformation = skinInfo;
this.textures = null;
this.xFilePath = xFilePath;
}
public void Initialize()
{
// Load the textures for the mesh.
ExtendedMaterial[] m = this.GetMaterials();
this.textures = new Texture[m.Length];
for(int i=0; i<m.Length; i++)
{
if(m[i].TextureFilename != null)
{
string path =
Path.Combine(Path.GetDirectoryName(this.xFilePath),
m[i].TextureFilename);
this.textures[i] = TextureLoader.FromFile(
this.MeshData.Mesh.Device, path);
}
}
}
public Texture[] Textures
{
get
{
return this.textures;
}
}
}
构造函数接受了相应的参数并将相应的属性赋值。我人为这比单独去设置每一个属性要简单。类公开了一个初始化方法,使用.x文件的信息载入网格的贴图。Initialize方法中的代码假设贴图存储在与.x文件相同的路径之中。这个类同时将textures公开为一个属性。这对我们接下来渲染场景是必要的。
以上是载入frame层次所需的全部代码,下面添加渲染代码。
渲染一个场景
我们由添加公共的渲染方法开始。在开始渲染之前,我们要给予所有对象的用户这样的可能性,是否单个frame的定制转换矩阵需要被调整以表现出动画效果。我选择用事件机制来实现。
这个事件的所有句柄将被在渲染对象开始之前通知。并且可能会调整对象相应的定制变换矩阵。
Code
// 被用于逐次渲染的事件
public event EventHandler SetupCustomTransform;
// 渲染frame层次
public bool Render()
{
// 允许事件的任何订阅者调整定制变换矩阵
if(this.SetupCustomTransform != null)
{
this.SetupCustomTransform(this, null);
}
// 开始渲染
// 使用初始矩阵作为所有定制变换的矩阵
this.RenderFrame(this.rootFrame.FrameHierarchy,
Matrix.Identity);
return true;
}
Render方法将会调用受保护的RenderFrame方法,这个方法将递归地调用并设置变换矩阵和渲染它们,这个方法接受将被渲染的frame和他们的父frame作为参数。为root frame传入标准矩阵。
Code
protected void RenderFrame(Frame frame, Matrix parentTransformationMatrix)
{
// 首先渲染所有的兄弟frame
if(frame.FrameSibling != null)
{
this.RenderFrame(frame.FrameSibling, parentTransformationMatrix);
}
// 合并所有相关的矩阵
// 1. 使用x-file中的指定矩阵.
// 2. 使用单个frame的矩阵.
// 3. 使用相关联的父frame的矩阵.
Matrix tm =
frame.TransformationMatrix *
((CustomFrame)frame).CustomTransform *
parentTransformationMatrix;
// 使用我们刚刚得到的矩阵继续渲染子fram
if(frame.FrameFirstChild != null)
{
this.RenderFrame(frame.FrameFirstChild, tm);
}
// TODO: 看看网格层次是否会调整
// 开始正式渲染.
if(frame.MeshContainer != null)
this.DoRender(frame, tm);
}
对层次中的每个frame,RenderFrame方法首先对于所有兄弟frame,使用父frame的变换矩阵,递归地调用它自己。然后使之和父frame,当前frame,当前的frame定制的变换矩阵相关联。然后递归地处理子frame,使用当前的矩阵做参数。最后调用DoRender完成真正的渲染。
Code
protected void DoRender(Frame frame, Matrix m)
{
this.device.Transform.World = m;
ExtendedMaterial[] em = frame.MeshContainer.GetMaterials();
Texture[] t = ((CustomMeshContainer)(frame.MeshContainer)).Textures;
for(int i=0; i < em.Length; i++)
{
this.device.Material = em[i].Material3D;
if(t[i] != null)
{
this.device.SetTexture(0, t[i]);
}
frame.MeshContainer.MeshData.Mesh.DrawSubset(i);
}
}
Retrieving Frames
假如我们有可能使用到层次中单个frame的变换,我们需要一个方式去从层次中找到它。我们添加GetFrame方法。
Code
// 在层次中找到调用者指定名字的frame
public CustomFrame GetFrame(string name)
{
return (CustomFrame)Frame.Find(this.rootFrame.FrameHierarchy, name);
}
使用这个类
让我们看看我们刚刚创造的类将怎样为我们专门为tank模型所创造的类所使用。我们声明tank的position,bearing,roll和pitch的属性,也包括Turret的角度。
我们重载GraphicsObject类的Initialize方法并为SetupCustomTransform事件注册句柄。Tank_SetupCustomTransform事件句柄寻找body frame并从bearing,pitch,roll,和position属性中计算它的CustomTransform。然后找到Turret frame并计算指定角度的CustomTransform。
Code
public class Tank : GraphicsObject
{
public override void Initialize()
{
base.Initialize();
this.SetupCustomTransform +=
new EventHandler(this.Tank_SetupCustomTransform);
}
public void Tank_SetupCustomTransform(object sender, EventArgs e)
{
// 提供整个坦克的变换
this.GetFrame("Body").CustomTransform =
Matrix.RotationYawPitchRoll(this.bearing, this.pitch, this.roll)*
Matrix.Translation(this.position);
// 仅提供Turret的变换
this.GetFrame("Turret").CustomTransform =
Matrix.RotationY(this.turretAngle);
}
}