FBX file-ok használata

Az FBX egy 3D-s modellek tárolására szolgáló file formátum, amit sok szerkesztő program tud kezelni. Ami számunkra érdekesebb, hogy ezeket az XNA segítségével kényelmesen tudjuk betölteni és megjeleníteni.

FBX exportálható például blender-ből vagy 3ds Max-ből.

Előfeltétel

Innen letölthető az előző gyakorlaton készített per-pixel lighting-os program lecsupaszított változata.

Közös feladat

Írjunk olyan programot, ami betölt egy FBX modellt, és kirajzolja azt a múlt órán készített shader segítségével!

A project Content könyvtárába másoljuk be az fbx-columns.zip tartalmát. Ez a .fbx file-on kívül a használt textúrát is tartalmazza. A .fbx file-t hozzá kell adni a project-hez, de a textúrákat nem szükséges.

A file-t a Content.Load metódussal tölthetjük be egy Model objektumba.

Ezért adjunk hozzá egy új adattagot a Game osztályunkhoz:

Model myModel;

A Game.LoadContent-ben töltsük be a modelt az FBX file-ból:

protected override void LoadContent()
{
    // ...
    myModel = Content.Load<Model>("columns");
}

Így létrehozva a Model alapértelmezett Effect-eket fog használni a kirajzolás során. Mi nem ezt szeretnénk, hanem az elkészült per-pixel lighting-os shader-ünket akarjuk használni. Ehhez a Model minden egyes ModelMesh-ének minden ModelMeshPart-jának le kell cserélni az Effect-jét a sajátunkra. Erre használható a következő eljárás:

public static void RemapModel(Model model, Effect effect)
{
    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
            part.Effect = effect;
        }
    }
}

Itt nem jön létre új Effect, minden part ugyanarra a példányra fog hivatkozni, ezért az effect minden változása hat majd minden ModelMeshPart-ra a Model-ben.

Adjuk hozzá ezt az eljárást a Game osztályunkhoz, és javítsuk az előző LoadContent-et:

protected override void LoadContent()
{
    // ...
    myModel = Content.Load<Model>("columns");
    RemapModel(myModel, effect);
}

Természetesen már régebben be kellett töltenünk ehhez az effect-et.

A kirajzoláshoz elég három sort hozzáadni a Draw metódushoz:

protected override void Draw(GameTime gameTime)
{
    // törlés
    // ...
    // effect paraméterek
    // ...

    foreach (ModelMesh mesh in myModel.Meshes)
    {
        mesh.Draw();
    }            

    // ...
    
    base.Draw(gameTime);
}

Megjegyzés: blender-ből exportált modellek esetén figyelni kell, hogy ott az Y helyett a Z tengely mutat felfele ezért a world mátrixot egy Matrix.CreateRotationX(-MathHelper.PiOver2) -vel érdemes kezdeni.

Önálló feladat

Adj hozzá még egy Model-t a programhoz, és ez mozogjon az előzőtől függetlenül (pl. ezt)!

Tippek:


Textúrák visszahozása

Azzal hogy lecseréltük a Model Effect-jeit elveszítettük a betöltött textúrákat is. Ahhoz hogy ezeket használhassuk saját Effect-tel kicsit trükközni kell.

(Hogy látható legyen a probléma, a perpixel.fx 73, 74 sorából ki kell venni a sor végi kommentet a textúraolvasás elöl.)

Írjuk át a RemapModel eljárást úgy, hogy a textúrákat elmenti egy Dictionary-be mielőtt lecserélne az Effect-et! A ModelMeshPart pontosan anyagonként csoportosítja egy mesh részeit, ezért a Dictionaty-t hozzuk létre ModelMeshPart-ok és Texture-k között.

Kell egy úgy adattag a főosztályunkba:

Dictionary<ModelMeshPart, Texture> textures =
    new Dictionary<ModelMeshPart,Texture>();

És ezt tölti fel a RemapModel:

public void RemapModel(Model model, Effect effect)
{
    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
            textures.Add(part, ((BasicEffect)part.Effect).Texture);
            part.Effect = effect;
        }
    }
}

Fontos! Ez a változat már nem static.

A következő probléma, hogy az Effect-ünknek part-onként kéne megadni a textúrát, de csak a ModelMesh-nek van Draw metódusa. Az egyes part-okat mi is kirajzoltathatjuk, ehhez hozzunk létre egy külön metódust:

public void DrawPart(ModelMesh mesh, ModelMeshPart part)
{
    part.Effect.Parameters["tex"].SetValue(textures[part]);
    part.Effect.Begin();

    foreach (EffectPass pass in part.Effect.CurrentTechnique.Passes)
    {
        pass.Begin();
        
        graphics.GraphicsDevice.VertexDeclaration = part.VertexDeclaration;
        graphics.GraphicsDevice.Vertices[0].SetSource(
            mesh.VertexBuffer,
            part.StreamOffset,
            part.VertexStride
        );                

        graphics.GraphicsDevice.DrawIndexedPrimitives(
            PrimitiveType.TriangleList,
            part.BaseVertex, 0,
            part.NumVertices,
            part.StartIndex,
            part.PrimitiveCount
        );
        pass.End();
    }

    part.Effect.End();
}

Itt először a textures segítségével átadjuk a textúrát a shader-ünknek, majd az IndexBuffer-eknél tanult módok rajzoljuk ki az aktuális darabot. A part és a mesh közösen tartalmaz minden paramétert, ami a kirajzoláshoz kell.

A Draw függvényben módosítanunk kell a kirajzolást:

protected override void Draw(GameTime gameTime)
{
    // törlés
    // ...
    // effect paraméterek
    // ...

    foreach (ModelMesh mesh in myModel.Meshes)
    {
        graphics.GraphicsDevice.Indices = mesh.IndexBuffer;
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
            DrawPart(mesh, part);
        }
    }

    // ...

    base.Draw(gameTime);
}

Mivel az index buffer egy ModelMesh-en belül azonos, azt elég a DrawPart-on kívül beállítani. (Igazából a vertex buffer is ilyen, de annak a megadásakor más-más az offset minden part esetén.)

Önálló feladat

Azok a részek, mikhez nincs textúra rendelve, feketén jelennek meg, mert egy null textúrából vett értékkel számolja a shader a színüket. Írd át a programot úgy, hogy ezek a részek az anyag színnel legyenek árnyalva.

Tippek: