广州业余3D程序

沉着,简单,便捷,逐步重构
随笔 - 8, 文章 - 0, 评论 - 10, 引用 - 0
数据加载中……

(翻译)3.3 抽象渲染API(二)

  3.3 抽象渲染API(二)

 

3.3.8 资源管理

    渲染器的大多数内容是为资源管理设计和构建的。最低限度地,渲染器继承类必须指定图形硬件支持的各种对象的最大数目。这些数量由接口函数存取

 

    int GetMaxLights() const;

    int GetMaxColors() const; 

    int GetMaxTCoords() const; 

    int GetMaxVShaderImages() const; 

    int GetMaxPShaderImages() const; 

    int GetMaxStencilIndices() const; 

    int GetMaxUserClipPlanes() const;

 

    在固定函数管线里,光照的数量通常限制为8。然而,着色程序允许你拥有你想有多少就多少光照。换句话说,我实现的渲染器限制光照的数量为8,不是我想限制你为一个固定数量,但我不能想象你哪里需要这么多动态光照同时影响一个几何图元的情况。

    对于所有的渲染器,最大的颜色数量是2。顶点颜色典型地保存为单一的RGBA颜色数组,但图形硬件的发展导致了API为散射颜色提供主储存器以及为高光颜色提供次储存器。从实践的角度来看,我认为这是提供了两套顶点颜色,因此着色程序可以编写为用上这两套。通过图形API处理顶点颜色有潜在的缺陷需要注意。举个例子,Wild Magic颜色是保存为ColorRGB或者ColorRGBA,两种形式都拥有值在[0,1]的浮点通道。OpenGL和软件渲染器需要顶点数据的颜色通道在[0,1]。然而,Direct3D需要颜色打包为32位大小,8位每通道。你必须注意图形API在处理颜色时产生的副作用,例如,内部钳制超出范围的颜色值。如果你仅仅是使用顶点颜色来储存非颜色数量,你最好使用纹理坐标代替。

    GetMaxTCoords函数指定了多少套纹理坐标可以在顶点和像素程序里面使用。GetMaxPShaderImages指定了一个像素程序支持多少纹理图像单元。尽管你可能认为它们是一样的,但它们不必如此。举个例子,在NVIDIA Geforce 6800显卡上,纹理坐标集的数量是8,但纹理图像单元的数量是16。如果你使用9或者更多图像,你必须共享一些纹理坐标集。GetMaxVShaderImages函数指定一个顶点程序支持多少纹理图像单元。起初,顶点程序没有访问纹理采样器的权限——现在它们有了。

    GetMaxStencilIndices函数告诉你,你能拥有多少不同的模板值。如果你有一个8位的模板缓存,这个数量是256GetMaxUserClipPlanes函数返回你可以启用的用户自定义裁剪平面的数量。OpenGL报告是6或者更多,但通常仅仅是6。这个数量是除了六个平截头体面之外的,因此,大多数情况下,OpenGL渲染器允许12个裁剪平面,也就是你能提供6个。

 

    资源加载和释放

    渲染需要的资源可以在计算机的很多地方存在。场景的美术内容驻留在磁盘里(硬盘,CD-ROMDVD等)。它们被加载进系统内存,一个相对慢的过程。最终,一部分数据必须发送到图形硬件,驻留在显存。在许多桌面电脑上,你有AGP内存。从AGP内存到显存之间的传送比从系统内存到显存之间的传送要快。一般地,静态几何体缓存在显存里,这避免了经常通过总线发送数据到图形硬件的瓶颈。很少改变的动态几何体可以保存在AGP内存;主意是这样,当它在系统内存时,你可以尽可能快的修改数据,但从AGP内存到显存的传输时间比从系统内存的短。性能的关键是,你使你的数据在正确的形式,在正确的地方,并且在正确的时间当渲染需要时。

    渲染器的资源管理系统就是设计来帮你达到上面的目的。相关的接口函数有

 

    typedef void (Renderer::*ReleaseFunction)(Bindable*);

    typedef void (Renderer::*ReleasePassFunction)(int,Bindable*);

 

    void LoadAllResources( Spatial* );

    void ReleaseAllResources( Spatial* );

    void LoadResources( Geometry* );

    void ReleaseResources( Geometry* );

    void LoadResources( ShaderEffect* );

    void ReleaseResources( ShaderEffect* );

    void LoadVProgram( VertexProgram* );

    void ReleaseVProgram( Bindable* );

    void LoadPProgram( PixelProgram* );

    void ReleasePProgram( Bindable* );

    void LoadTexture( Texture* );

    void ReleaseTexture( Bindable* );

    void LoadVBuffer( iPass, const Attributes&, VertexBuffer* );

    void ReleaseVBuffer( iPass, Bindable* );

    void LoadIBuffer( IndexBuffer* );

    void ReleaseIBuffer( Bindable* );

 

    virtual void OnLoadVProgram( ResourceIdentifier*&, VertexProgram* ) = 0;

    virtual void OnReleaseVProgram( ResourceIdentifier* ) = 0;

    virtual void OnLoadPProgram( ResourceIdentifier*&, PixelProgram* ) = 0;

    virtual void OnReleasePProgram( ResourceIdentifier* ) = 0;

    virtual void OnLoadTexture( ResourceIdentifier*&, Texture* ) = 0;

    virtual void OnReleaseTexture( ResourceIdentifier* ) = 0;

    virtual void OnLoadVBuffer( ResourceIdentifier*&, const Attributes&, VertexBuffer* ) = 0;

    virtual void OnReleaseVBuffer( ResourceIdentifier* ) = 0;

    virtual void OnLoadIBuffer( ResourceIdentifier*&, IndexBuffer* ) = 0;

    virtual void OnReleaseIBuffer( ResourceIdentifier* ) = 0;

     

    重要的资源有顶点缓存,索引缓存,顶点程序,像素程序和纹理。所有这些由列在这里的类代表,同时,还列出了它们的基类:

 

    VertexBuffer    : Object, Bindable

    IndexBuffer     : Object, Bindable

    VertexProgram : Program : Object, Bindable

    PixelProgram   : Program : Object, Bindable

    Texture         : Bindable

 

    图形API允许你加载一个资源到显存里用作后续使用。每个API为你提供一个句柄,因此当你以后需要资源的时候,你仅仅需要将句柄传递给API。句柄的类型由API决定。我通过Bindable类将它隐藏。这个类也提供了渲染器和资源之间的双路通讯。如果资源被程序释放,是否显式或者由于资源被破坏,渲染器必须被通知到资源已经丢失,这样渲染器可以释放资源相关的内部数据。   

    每种资源都有Load*Release*函数。顶点缓存相关的函数有名为iPass的输入。这指的是多通渲染操作里的通道索引。每个通道可能有不同的顶点属性需求,因此顶点缓存的格式可能每通道都改变。这套loadrelease函数是由DrawApplyEffect函数内部使用。

    资源的loadrelease函数是由绘图系统自动调用的;这样,加载过程延迟到绘图过程才发生。一旦加载了,资源不会再一次被加载,除非你同时释放了它。在一个有大量数据需要加载的程序里,你很可能看见帧速的减慢。这不是想要的,因此接口也有高级loadrelease函数。这些函数设计来允许你在任何你喜欢的时间里加载和释放。举例子,当玩家在观看场景切换画面而分散注意力的时候,你可能会预加载整层游戏内容。这些接口允许你加载相关的资源,这些资源与一个单一的效果(着色程序和纹理),一个几何物体(顶点和索引缓存,附加在物体上的效果相关的资源),以及整个场景(场景里每个几何物体相关的资源)都有关。

    涉及到顶点程序加载器的资源加载的例子

 

    void Renderer::LoadVProgram( VertexProgram* pkVProgram )

    {

        ResourceIdentifier* pkId = pkVProgram->GetIndentifier( this );

        if( !pkId )

        {

            OnLoadVProgram( pkId, pkVProgram );

            pkVProgram->OnLoad( this, &Renderer::ReleaseVProgram, pkId );

        }

    }

 

    第一行代码涉及到Bindable类函数GetIdentifier的调用。输入是渲染器,因为一个资源可以绑定到多个渲染器。返回值是一个资源标识符的指针。这个标识符是晦涩的——你不用在意它是什么,除了指针是空还是非空。如果顶点程序在之前已经被渲染器加载,返回的指针会是非空,并且没有工作需要去做。可是,当第一次加载的时候,返回的值为空。在这种情况下,顶点程序必须被加载。在Wild Magic里,从磁盘加载到系统内存是平台无关的,但从系统内存加载到显存(或AGP内存)是平台相关的。为了从系统内存加载到显存,每个继承类实现虚函数OnLoadVProgram。最后的调用是BindableOnLoad函数。渲染器(指针)和标识符传递给这个函数,另外还有一个函数指针。每当资源准备删除的时候,这个函数就会被调用,比方说,在析构函数调用期间,给予渲染器一个机会去释放资源相关的数据。

    除了只是让你了解它们怎样比较,我不打算花精力在图形API的细节上,OnLoadVProgram的实现列在这里(没有部分错误处理的细节)。OpenGL的实现是

 

    void OpenGLRenderer::OnLoadVProgram( ResourceIdentifier*& rpkId, VertexProgram* pkVProgram )

    {

        VProgramID* pkResource = WM4_NEW VProgramID;

        rpkId = pkResource;

        const char* acProgramText = pkVProgram->GetProgramText().c_str();

        int iProgramLength = (int)strlen(acProgramText);

        glEnable( GL_VERTEX_PROGRAM_ARB );

        glGenProgramsARB( 1, &pkResource->ID );

        glBindProgramARB( GL_VERTEX_PROGRAM_ARB, pkResource->ID );

        glProgramStringARB( GL_VERTEX_PROGRAM_ARB, GL_PROGRAM_FORMAT_ASCII_ARB, iLength, acProgram );

        glDisable( GL_VERTEX_PROGRAM_ARB );

    }

 

    Direct3D的实现是

 

    void Dx9Renderer::OnLoadVProgram( ResourceIdentifier*& rpkId, VertexProgram* pkVProgram )

    {

        VProgramID* pkResource = WM4_NEW VProgramID;

        rpkId = pkResource;

        const char* acProgramText = pkVProgram->GetProgram().c_str();

        int iProgramLength = (int)strlen(acProgramText);

 

        LPD3DXBUFFER pkCompiledShader = 0;

        LPD3DXBUFFER pkErrors = 0;

        D3DXAssembleShader( acProgramText, iProgramLength, 0, 0, 0, &pkCompiledShader, &pkErrors );

        m_pqDevice->CreateVertexShader( (DWORD*)(pkCompiledShader->GetBufferPointer()), &pkResource->ID );

        if( pkCompiledShader )

        {

            pkCompiledShader->Release();

        }

        if( pkErrors )

        {

            pkErrors->Release();

        }

 

    }

 

    软件渲染器的实现是

 

    void SoftRenderer::OnLoadVProgram( ResourceIdentifier*& rpkId, VertexProgram* pkVProgram )

    {

        VProgramID* pkResource = WM4_NEW VProgramID;

        rpkId = pkResource;

        pkResource->OAttr = pkVProgram->GetOutputAttributes();

        stdext::hash_map<std::string,VProgram>::iterator pkIter = ms_pkVPrograms->find( pkVProgram->GetName() );

        pkResource->ID = pkIter->second;

    }

 

    释放资源的处理也相似。

 

    void Renderer::ReleaseVProgram( Bindable* pkVProgram )

    {

        ResourceIdentifier* pkId = pkVProgram->GetIdentifier( this );

        if( pkId )

        {

            OnReleaseVProgram( pkId );

            pkVProgram->OnRelease( this );

        }

    }

 

    如果GetIdentifier的返回值是空,那么资源当前还没有被加载进图形系统,因此没有什么释放。如果返回值非空,那么资源必须被释放,机制由平台决定。每个继承类实现虚函数OnReleaseVProgram。最后的调用是BindableOnRelease函数,这个函数仅仅删除资源和渲染器之间的绑定。

    另外的加载和释放函数也是差不多的,除了顶点缓存的加载。那个函数是

 

    void Renderer::LoadVBuffer( int iPass, const Attributes& rkIAttr, VertexBuffer* pkVBuffer )

    {

        // 查找一个在之前通道里用过的兼容的顶点缓存

        ResourceIdentifier* pkId;

        for( int i=0; i<=iPass; ++i )

        {

            piId = pkVBuffer->GetIdentifier( this, i );

            if( pkId )

            {

                if( rkIAttr.IsSubsetOf(*(Attributes*)pkId) )

                {

                    // 在显存里找到一个兼容的顶点缓存

                    return;

                }

            }

        }

 

        // 第一次遇到这个顶点缓存

        const Attribute& rkVBAttr = pkVBuffer->GetAttributes();

        assert( rkIAttr.GetPChannels()==3 && rkVBAttr.GetPChannels()==3 );

        if( rkIAttr.HasNormal() )

        {

            assert( riIAttr.GetNChannels()==3 && rkVBAttr.GetNChannels()==3 );

        }

 

        OnLoadVBuffer( pkId, rkIAttr, pkVBuffer );

        pkVBuffer->OnLoad( this, iPass, &Renderer::ReleaseVBuffer, pkId );

 

    }

 

    一个多通效果可能每个通道需要不同的顶点属性。在这种情况下,一个单一的顶点缓存是不足够的。有可能两个或多个通道共同使用单一的顶点缓存是,因此,加载函数的第一部分迭代顶点缓存和渲染器相关资源之间的绑定。如果找到一个兼容的资源,那个资源将用做内部顶点缓存。

 

    资源启用和禁用

    一旦资源加载了,为了使用,它需要由图形系统启用。启用的操作在每个绘制通道里完成。一旦物体绘制完毕,资源变成禁用状态。相关接口函数有

 

    void EnableVProgram( VertexProgram* );

    void DisableVProgram( VertexProgram* );

    void EnablePProgram( PixelProgram* );

    void DisablePProgram( PixelProgram* ); 

    void EnableTexture( Texture* );

    void DisableTexture( Texture* );

    ResourceIdentifier* EnableVBuffer( iPass, const Attributes& );

    void DisableVBuffer( iPass, ResourceIdentifier* );

    void EnableIBuffer();

    void DisableIBuffer();

 

    virtual void OnEnableVProgram( ResourceIdentifier* ) = 0;

    virtual void OnDisableVProgram( ResourceIdentifier* ) = 0;

    virtual void OnEnablePProgram( ResourceIdentifier* ) = 0;

    virtual void OnDisablePProgram( ResourceIdentifier* ) = 0;

    virtual void OnEnableTexture( ResourceIdentifier* ) = 0;

    virtual void OnDisableTexture( ResourceIdentifier* ) = 0;

    virtual void OnEnableVBuffer( ResourceIdentifier* ) = 0;

    virtual void OnDisableVBuffer( ResourceIdentifier* ) = 0;

    virtual void OnEnableIBuffer( ResourceIdentifier* ) = 0;

    virtual void OnDisableIBuffer( ResourceIdentifier* ) = 0;

 

    第一块函数由加载或释放资源的包装器组成。举例子,启用纹理的函数有个简单的格式

 

    void Renderer::EnableTexture( Texture* pkTexture )

    {

        LoadTexture( pkTexture );

        ResourceIdentifier* pkId = pkTexture->GetIdentifier( this );

        OnEnableTexture( pkId );

    }


    On*
虚函数由渲染器继承类实现。举例子,OnEnableTexture函数有责任将纹理状态通知给图形API,例如过滤方式,Mip映射方式,缠绕方式,边界颜色等等。OnDisableTexture函数在OpenGL和软件渲染器里面什么都没做,但Direct3D渲染器禁用相当的纹理单元。

    着色程序的启用涉及到一个初始代码块,正如展示出来的纹理部分。这个代码块使图形API知道代表程序的文本字符串。然而,必须执行额外的工作,使用实际的着色常量去设置寄存器。支持这个子系统的接口相当广泛。

 

    enum    // 常量类型

    {

        CT_RENDERER,

        CT_NUMERICAL,

        CT_USER

    };

 

    virtual void SetVProgramConstant( eCType, iBaseRegister, iRegisterQuantity, *afData ) = 0;

    virtual void SetPProgramConstant( eCType, iBaseRegister, iRegisterQuantity, *afData ) = 0;

 

    enum { SC_QUANTITY = 38 };

    typedef void (Renderer::*SetConstantFunction)(int,float*);

    static SetConstantFunction ms_aoSCFunction[SC_QUANTITY];

    void SetRendererConstant( RendererConstant::Type, *afData );

 

    // 操作有

    // 0 = 矩阵

    // 1 = 矩阵转置

    // 2 = 矩阵取逆

    // 3 = 矩阵转置取逆

    void GetTransform( Matrix4f&, iOperation, *afData );

    void SetConstantWMatrix( iOperation, *afData );

    void SetConstantVMatrix( iOperation, *afData );

    void SetConstantPMatrix( iOperation, *afData );

    void SetConstantWVMatrix( iOperation, *afData );
    void SetConstantVPMatrix( iOperation, *afData );

    void SetConstantWVPMatrix( iOperation, *afData );

 

    // 这些函数不使用可选参数,但参数包含进来,是

    // 为了允许类静态函数指针数组可以处理所有着色常量

    void SetConstantMaterialEmissive( int, *afData );

    void SetConstantMaterialAmbient( int, *afData );
    void SetConstantMaterialDiffuse( int, *afData );

    void SetConstantMaterialSpecular( int, *afData );
    void SetConstantFogColor( int, *afData );

    void SetConstantFogParameters( int, *afData ); 

    void SetConstantCameraModelPosition( int, *afData );

    void SetConstantCameraModelDirection( int, *afData ); 

    void SetConstantCameraModelUp( int, *afData ); 

    void SetConstantCameraModelRight ( int, *afData ); 

    void SetConstantCameraWorldPosition( int, *afData ); 

    void SetConstantCameraWorldDirection( int, *afData ); 

    void SetConstantCameraWorldUp( int, *afData ); 

    void SetConstantCameraWorldRight( int, *afData ); 

    void SetConstantProjectorModelPosition( int, *afData );

    void SetConstantProjectorModelDirection( int, *afData );

    void SetConstantProjectorModelUp( int, *afData );

    void SetConstantProjectorModelRight ( int, *afData );

    void SetConstantProjectorWorldPosition( int, *afData );

    void SetConstantProjectorWorldDirection( int, *afData );

    void SetConstantProjectorWorldUp( int, *afData );

    void SetConstantProjectorWorldRight( int, *afData );

    void SetConstantProjectorMatrix( int, *afData );

 

    // 这些函数设置光照状态。索引iLight07之间(当前支持8个光照)

    void SetConstantLightModelPosition( int, *afData );

    void SetConstantLightModelDirection( int, *afData );

    void SetConstantLightWorldPosition( int, *afData );

    void SetConstantLightWorldDirection( int, *afData );

    void SetConstantLightAmbient( int, *afData );

    void SetConstantLightDiffuse( int, *afData );

    void SetConstantLightSpecular( int, *afData );

    void SetConstantLightSpotCutoff( int, *afData );

    void SetConstantLightAttenuation( int, *afData );


    
着色常量有三种类型:

    1.渲染器常量。这些包括矩阵变换如世界,视和投影矩阵。任何组合是允许的,就像转置,取逆和转置取逆。这些也包括光照和材质参数,雾参数,以及摄像机和投影器框架信息。前面代码块里面提到的38个函数实现了为这些常量设置寄存器的功能。渲染器常量的当前值会被自动使用。

    2.数值常量。Direct3D要求你设置各种由着色程序使用的数值常量。OpenGL和软件渲染器不需要。

    3.用户自定义常量。当写着色程序时,你可以有你可能在你自己的应用程序代码里面修改的常量。这些常量倾向于与你正试图取绘制的物理模型有关。例如,一个折效果需要你指定一个折射度索引。这就是一个你用来产生正确视觉效果的着色常量。

 

    顶点程序启用器是

    void Renderer::EnableVProgram( VertexProgram* pkVProgram )

    {

        LoadProgram( pkVProgram );

        ResourceIdentifier* pkId = pkVProgram->GetIndetifier( this );

        OnEnableVProgram( pkId );

 

        // 处理渲染器常量

        int i;

        for( i=0; i<pkVProgram->GetRCQuantity(); i++ )

        {

            RendererConstant* pkRc = pkVProgram->GetRC(i);

            SetRendererConstant( pkRc->GetType(), pkRc->GetData() );

            SetVProgramConstant( CT_RENDERER, pkRc->GetBaseRegister(),

                                        pkRc->GetRegisterQuantity(), pkRc->GetData() );

        }

 

        // 处理数值常量

        for( i=0; i<pkVProgram->GetNCQuantity(); i++ )

        {

            NumericalConstant* pkNc = pkVProgram->GetNC(i);

            SetVProgramConstant( CT_NUMERICAL, pkNc->GetRegister(), 1, pkNc->GetData() );

        }

 

        // 处理用户自定义常量

        for( i=0; i<pkVProgram->GetUCQuantity(); i++ )

        {

            UserConstant* pkUc = pkVProgram->GetUC(i);

            SetVProgramConstant( CT_USER, pkUc->GetBaseRegister(),

                                        pkUc->GetRegisterQuantity(), pkUc->GetData() );

        }

 

    }

 

    程序加载到显存(如果还不在那里)以及启用来使用。剩下的代码循环三个常量数组,调用SetVProgramConstant。这个函数是纯虚函数,因此每个继承类实现它。每种图形API拥有真正设置寄存器关联到常量的函数。 
    
第一个循环设置渲染器常量,并且比其它循环有更多的工作去做。决定哪个渲染器常量需要设置是必需的。当顶点程序从磁盘加载之后,它被解析来获取渲染器常量的信息,包括它们是什么类型。被解析器用来保存常量信息的类是RendererConstant。接口很多,但主要枚举了各种类型(例如,矩阵,摄像机参数,光照参数)。这个类也保存了渲染器常量名字的字符串。这些字符串被解析器使用。最终证明是,在写CgHLSL着色程序时,渲染器常量的命名约定是有必要的。静态数组

 

    std::string RendererConstant::ms_kStringMap[];


    
保存字符串。如果你的着色程序需要输入从模型空间到裁剪空间的变换矩阵,相应的着色常量必需命名为WVPMatrix。当解析器遇到这个名字,它会找到ms_kStringMap里面相应的入口,并给RendererConstant对象赋相关的值。这些值通过pkRC->GetType()传递给Renderer函数SetRendererConstant,并且匹配的Renderer::SetConstant*函数会被找到和调用。

 

    目录

    当为了满足对象的需求,将资源从磁盘加载到系统内存时候,有可能其它对象共用这些资源。当遇到这些对象时,加载多一次资源在时间的使用方面是无效率的。在内存的使用方面也是无效率的,因为你会拥有两份资源的拷贝在内存里面,这意味着你不是真正的共用它。
    
为了支持共用,引擎提供了各种资源目录。受管资源有顶点程序,像素程序和图像。相关的类分别是VertexProgramCatalogPixelProgramCatalogImageCatalog
    
所有目录提供插入和移除条目功能。尽管你可以显式那样做,但引擎会自动实行目录管理。例如,如果你创建一个图像对象,你需要为它提供一个名字。图像对象会自动插入到图像目录。当图像对象被破坏,目录会相应更新。着色程序也是用相同的方法处理。
    
当一个资源必须从系统内存加载到显存(或AGP内存)时,目录首先会被查找。如果资源在目录里找到,它就存在系统内存里,并且已经被加载到显存。如果找不到,它会尝试从磁盘加载它到系统内存。如果成功了,资源然后会被从系统内存加载到显存,如果资源在磁盘里找不到,一个默认的资源会被使用。就图像来说,默认的是糟糕的紫色,这是为了在开发和测试的过程中吸引到你的注意。默认的顶点程序仅仅是变换模型空间位置到裁剪空间。默认的像素程序是创建紫色。
    
这个系统的思想是为了拥有一个内存层次。层次有磁盘,系统内存和显存。AGP内存是由图形驱动任凭使用的,因此你不要倾向于控制这层。以绘制为目的,如果一个资源已经在显存里,渲染器会良好地运行。如果不在显存而在系统内存,一个加载到显存的过程必须先发生。如果不在系统内存而在磁盘,首先从磁盘加载到系统内存,然后从系统内存加载到显存。最后,如果不在磁盘,默认的资源会被使用。希望这只会发生在开发期间——一个在最终产品运输之前必须要修复的应用程序的bug。前面讲述的加载和释放函数允许你保证,当需要的时候,资源在显存中,但你也可以依赖自动按需加载。
    
你可以每种资源拥有多个目录,但在某一时刻只能有一个起作用。每个目录类使用静态数据成员保存起作用的目录指针。这是一个只是为了使某些目录系统能工作的设计抉择。可以通过查找多个目录而不是仅仅当前起作用的目录来提高设计。

posted on 2009-04-16 03:14 saltyshrimp 阅读(1383) 评论(0)  编辑 收藏 引用 所属分类: 3D游戏引擎设计-实时计算机图形学的应用方法(第二版)


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理