第十五章 Models

第十五章 Models

本章,主要讲解如何在应用程序中使用3D模型。将会集成一个第三方库用于导入多种类型的文件格式,并编写一组类函数用于在程序运行过程中表示3D模型。在学习过程上,你将会得到更多关于创建DirectX应用程序的实际动手经验。

Motivation

到目前为止,已经编写了两个Direct3D应用程序,但是都是通过手动指定vertices和indices的方法,我们需要想想怎么使用一种更好的方法。显然,对于每一个渲染的object不能使用硬编码(hard-code)的方式(也就是手动的方法)。而是从一个美术设计师那里得到一些objects,这些objects通常是使用3D建模软件创建的,如Autodesk Maya或3D Studio Max。我们的任务主要是在程序运行时加载这些资源,并提取必要的数据渲染到屏幕上。当然,口头上说总是比实际作要容易。接下来几节将详细讲解一种使用3D模型的方法。

Model File Formats

现在市面上已经存在了大量的文件格式用于存储3D模型,这些格式大致上可以分为三个类别:authoring formats(专用格式),game/graphics engine formats(游戏引擎使用的格式),interchage formats(可交换的格式)。

一个authoring format由某个建模软件所特有。比如,Autodesk Maya有两种专用的文件格式,Maya ASCII(.ma)和Maya Binary(.mb)。3D Studio Max则使用.3DS文件格式。这种格式一般都会包含一些该建模软件特有的数据,而这些数据不适用于游戏引擎。例如,Autodesk Maya文件中存储了最终模型每一步的创建记录,但是在渲染引擎中只需要最终的结果,并不需要这些记录。

一个game/graphics engine文件格式只包含从模型资源中提取出来的必需数据,而不包含建模软件特有的数据。另外,这种格式通常会针对具体的引擎进行优化。如果一个游戏工作室开发的引擎是提供给工作室之外的人员使用(比如,对于一个需要不断修改的游戏),需要公开该格式的相关文档或提供其他的支持,以便用户自己创建的资源可以被正确的读取。例如,Blizzard的Starcraft II格式MDX3(.m3)已经公开发布了,并为3D Studio Max提供了一个导入导出的插件。

第三种类型的interchange formats,包含了一组独立与3D建模软件和游戏引擎的文件格式。这种格式的目的是支持在各个不同的应用程序之间交换资源,也就是希望使用一种格式用于多个应用程序中。COLLADA(Collaborative Design Activity)格式可能是目前应用最广泛的interchange format,被各种建模工具和游戏引擎所支持。

无论是哪一类文件格式,都以文本(比如,COLLADA格式使用一种基于XML样式的文本)或二进制形式存储。同一个asset以文本形式存储需要更多的硬盘空间,但是可能使用任意的文本编辑工具进行查看编辑。但是,不能仅仅因为一种文件格式可以以字符串的形式显示出来,就认为该格式是普通人都能看懂的。如果不仔细查看的话,一些难看的文本格式,会导致像查看一个二进制文件一样错误百出。通常情况下,比较好的方式是使用建模工具保存文件,并在必要的时候手动进行编辑。

还有一个问题需要考虑的是,并不是所有的格式都支持存储应用程序可能需要的数据。比如,Wavefront OBJ(.obj)格式是一个简单的并被广泛支持的文件格式,但是该格式无法存储动画数据。

在实际工作中,我们期望固定使用一种特定的authoring format。这种统一规定使得aasets的调试更容易(没错,assets中也会出现问题,不只是运行崩溃或产生错误的渲染结果)。如果无法统一使用一种格式,就需要把多种格式处理一种渲染引擎可以直接加载的统一表示。

The Content Pipeline

Content pipeline是处理模型asset的过程。在图形管线中,数据以primitives的形式输入并以pixels形式输出。在content管线中,由美术设计人员提供某种格式的assets文件作为数据的输出,然后输出一种在游戏中可以直接使用的assets文件。比如,把一个.obj格式的3D模型文件转换成游戏引擎使用的格式。但是与Direct3D图形管线不同的是,没有现成的content管线用于处理assets文件。需要自己创建一个这样的系统或者使用一个第三方的通用库。

通常情况下,在编译时通过content管线处理assets文件,并保存到当前所使用的游戏引擎格式的文件中。然后在运行时,加载具体游戏引擎格式的文件并使用相关的数据。另外,在运行时可以任意修改数据内容,但是也会带来相应的开销。一个游戏引擎通常不会支持同时在编译期和运行时调整assets,因为运行时与游戏的release版本相关太远。在本章,将会在运行时转换assets文件,但是把该系统应用于一个编译期content管线上是非常简单的。

一个content管线包括很多方面的内容,从assets文件格式转换的工作流程(可能包括一个GUI或者版本控制系统)到应用程序中使用的各种类型的content资源(比如3D模型,纹理,音频文件)。但是一个content管线的基础是对数据进行序列化和反序列化,也就是写文件和读文件。因此,content管线的核心是解析一种或多种格式的输入文件,然后产生一种游戏引擎可以直接使用的统一格式的输出文件。也许你可以格式化某一种authoring format,但是是本书并没有这样的前提假设,而是选择支持尽可能多的输入格式。但是,编写一个解析多种文件格式的工具超出了本书的范围,因此,我们将使用一个开源的assets解析库Open Asset Import Library,该开源库的链接为 http://assimp.sourceforge.net.

The Open Asset Import Library

Open Asset Import Library提供了一个C++调用方式的API,用于导入各种各样的3D模型格式,并以一种完全一致的方式表示这些格式。表格15.1列出了该库所支持的一小部分格式。在Open Asset Import Library(aasimp)网站上可以查看完整的支持列表。

第十五章 Models_第1张图片

表格15.1 Common 3D Model Formats the Open Asset Import Library Supports

要在示例工程中使用这个库,首先把assimp库的开发包解压到 external 目录中,并在Library工程中添加相应的Include Directory和Library Directory。然后在Game工程或Library工程中添加对assimp.lib库的引用(或者创建一个完成独立的工程,用于创建一个编译期content管线)。Open Asset Import Library是以动态链接库(.dll)的形式发布的,并带有一个导入库文件assimp.lib,真正的API函数实现都包含在DLL文件中。因此,在启动应用程序之前需要把DLL文件拷贝到应用程序的输出目录中(也就是可执行文件的目录)。只需要一步简单的post-build event,就可以完成。如果你在配置工程时遇到了困难,可以查看本书配套网站上提供的完整工程代码。

尽管使用Open Asset Import Library可以读取各种各样的模型枨,但是还是需要自己创建相关的类用于表示3D模型。以这种封装的方法,就可以去掉对assimp库的任何显式的运行时依赖。

What's in a Model?

那么3D模型中真正包含哪些数据呢?这个问题无法简单地回答,但是模型通常被描述为一组基本网格(mesh)。每一个mesh又由一组vertices,edges以及faces组成,用于描述一个3D object的形状。通过一组meshes定义一个模型,既可以把多个“子模型”看成一个个独立的模型,也可以作为一个整体。例如,可以通过一些独立的meshes描述一辆汽车,包括汽车底盘,车门以及4个轮子。其中车门可以由车轮和底盘变换得到,但是所有的objects在world space中有一个共用的坐标位置。此外,还可以针对每一个mesh使用不同的shader。比如,汽车底盘和车门可能会使用一种environment mapping effect,而车轮使用一种diffuse shader。
一个模型中还可以包含一种shader的输入数据,也可以称之为模型材质。如果材质表示一种effect的实例,该effect中包含了shader的变量值,那么模型材质(至少部分材质)中就存储了这些变量的原始值。例如,一个模型材质中包含了用于渲染的shader的名称,以及一列用于shader constants初始值的name/value键值对。还可以包含一系列纹理引用(一个纹理引用就是一个指向某个纹理的文件名)或者内置纹理数据(直接在模型文件中存储实际的纹理数据)。但是,相对于引用纹理内置纹理并不常用,因为重复的内置纹理数据会浪费大量的内存。
列表15.1中列出了Model类的声明代码,可以使用该类定义一种模型。

列表15.1 The Model.h Header File

#pragma once

#include "Common.h"

namespace Library
{
    class Game;
    class Mesh;
    class ModelMaterial;

    class Model
    {
    public:
        Model(Game& game, const std::string& filename, bool flipUVs = false);
        ~Model();

        Game& GetGame();
        bool HasMeshes() const;
        bool HasMaterials() const;

        const std::vector<Mesh*>& Meshes() const;
        const std::vector<ModelMaterial*>& Materials() const;

    private:
        Model(const Model& rhs);
        Model& operator=(const Model& rhs);

        Game& mGame;
        std::vector<Mesh*> mMeshes;
        std::vector<ModelMaterial*> mMaterials;
    };
}


在Model类的声明代码中,包含了用于存储Mesh和ModelMaterial objects的数组变量,以及一个成员变量对Game类的引用。需要的是,虽然在该类中包含了model material,但是模型材质主要用于描述模型中单个mesh的属性。因此多个meshes可以使用相同的模型材质。

Model类的构造函数中接受两个参数,一个是要读取的模型的文件名,另一个是判断是否翻转模型的垂直纹理坐标(主要是用于加载OpenGL风格的模型)的bool值。在该类的声明代码并没有依赖Open Asset Import Library。对于assimp库的调用主要在Model类的实现代码中,这样在使用Model接口时,就隐藏了对asset导入的具体实现。在全面讨论Model类的实现代码之前,先讲述与Model类相关的Mesh和ModelMaterial类。

Meshes

一个mesh至少包含一组verties,但是也可以包括indices,normals,tangents,binormals,textures coordinates以及vertex colors(其中,tangents,binormal主要用于法线纹理贴图中)。此外,一个mesh还可以引用一个模型材质。列表15.2列出了Mesh类的声明代码。

列表15.2 The Mesh.h Header File

#pragma once

#include "Common.h"

struct aiMesh;

namespace Library
{
    class Material;
    class ModelMaterial;

    class Mesh
    {
        friend class Model;

    public:
        Mesh(Model& model, ModelMaterial* material);
        ~Mesh();

        Model& GetModel();
        ModelMaterial* GetMaterial();
        const std::string& Name() const;

        const std::vector<XMFLOAT3>& Vertices() const;
        const std::vector<XMFLOAT3>& Normals() const;
        const std::vector<XMFLOAT3>& Tangents() const;
        const std::vector<XMFLOAT3>& BiNormals() const;
        const std::vector<std::vector<XMFLOAT3>*>& TextureCoordinates() const;
        const std::vector<std::vector<XMFLOAT4>*>& VertexColors() const;
        UINT FaceCount() const;
        const std::vector<UINT>& Indices() const;

        void CreateIndexBuffer(ID3D11Buffer** indexBuffer);

    private:
        Mesh(Model& model, aiMesh& mesh);
        Mesh(const Mesh& rhs);
        Mesh& operator=(const Mesh& rhs);

        Model& mModel;
        ModelMaterial* mMaterial;
        std::string mName;
        std::vector<XMFLOAT3> mVertices;
        std::vector<XMFLOAT3> mNormals;
        std::vector<XMFLOAT3> mTangents;
        std::vector<XMFLOAT3> mBiNormals;
        std::vector<std::vector<XMFLOAT3>*> mTextureCoordinates;
        std::vector<std::vector<XMFLOAT4>*> mVertexColors;
        UINT mFaceCount;
        std::vector<UINT> mIndices;
    };
}


Mesh类的声明代码中有一个aiMesh结构体的前向声明,并把该结构体类型用于Mesh类的私有构造的参数,该结构体是Open Asset Import Library中的mesh表示形式。之所以在构造函数中引用aiMesh对象,是因为Model类的实现代码中是在运行时使用Open Asset Import Library执行assets文件的转换。另外,该构造函数指定为私有的是为了保持Mesh类的接口不依赖于assimp库,意味着可以在别的地方引用Open Asset Import Library,而不会影响到应用程序的其他部分。如果要创建一个编译期的content管线,就需要重新引用assimp库。

在Mesh类还有一个Mesh::CrateIndexBuffer()函数。如果一个mesh中包含indices数据,就具备了创建一个index buffer的所有信息。但是这并不能用于创建vertex buffers,因为一个vertex buffer的定义依赖于用于渲染mesh的shader。

Model Materials

3D模型系统中的最后一种类型是ModelMaterial类。与models和meshes一样,可以使用各种各样的表示方法描述一个模型的shader相关的数据和输入数据。ModelMaterial类中只包含一个name变量以及纹理列表的变量。其中,变量name表示渲染一个mesh使用的effect名称。比如,使用BasicEffect.fx文件时,model material的名称就是BasicEffect。纹理列表中存储了纹理文件名,而且在纹理容器中的每一个纹理文件名都对应该纹理的用途(纹理类型)。例如,map容器中的类型变量可以标识纹理为diffuse color纹理或specular纹理。其中TextureType枚举值定义了所有支持的纹理类型。

列表15.3中列出了ModelMaterial类的声明代码,以及枚举类型TextureType的枚举常量值。

列表15.3 The ModelMaterial.h Header File

#pragma once

#include "Common.h"

struct aiMaterial;

namespace Library
{
    enum TextureType
    {
        TextureTypeDifffuse = 0,
        TextureTypeSpecularMap,
        TextureTypeAmbient,
        TextureTypeEmissive,
        TextureTypeHeightmap,
        TextureTypeNormalMap,
        TextureTypeSpecularPowerMap,
        TextureTypeDisplacementMap,
        TextureTypeLightMap,
        TextureTypeEnd
    };

    class ModelMaterial
    {
        friend class Model;

    public:
        ModelMaterial(Model& model);
        ~ModelMaterial();

        Model& GetModel();
        const std::string& Name() const;
        const std::map<TextureType, std::vector<std::wstring>*> Textures() const;

    private:		
        static void InitializeTextureTypeMappings();
        static std::map<TextureType, UINT> sTextureTypeMappings;

        ModelMaterial(Model& model, aiMaterial* material);
        ModelMaterial(const ModelMaterial& rhs);
        ModelMaterial& operator=(const ModelMaterial& rhs);

        Model& mModel;
        std::string mName;
        std::map<TextureType, std::vector<std::wstring>*> mTextures;		
    };
}



你可能感兴趣的:(Model,import,library,3D,asset,图形引擎)