这种情况下有什么合适的设计模式?

3

我正在尝试实现一个模型(gfx)类,但是我似乎找不到一个合适的设计。

我目前拥有的是这个:[伪代码]

class Material {
  public:
    virtual void SetPerFrameInfo(void* pData)=0;
    //[...]
}

class SpecificMaterial : public Material {
  public:
    void SetPerFrameInfo(void* pData);
    //[...]
}

void  SpecificMaterial::SetPerFrameInfo(void* pData) {
    ThisMaterialPerFrameInfoStruct* pInfo = (ThisMaterialPerFrameInfoStruct*)pData;
    //[...]
}

class Model {
  public:
    Model(Material* pMaterial){m_pMatirial = pMaterial;}
    void Draw();
  private:
    Material* m_pMaterial
    //[...]
}

void Model::Draw() {
    PerFrameInformation info;
    m_pMaterial->SetPerFrameInfo(&info);
}

如何使用它:

Material* pMaterial = new SpecificMaterial();
Model someModel(pMaterial);

你可能已经注意到,我有两个主要问题:

1)传递给SetPerFrameInfo()的结构体类型取决于实际的材质,那么我应该如何填写它呢?

2)根据实际的材质,Model类可能需要一些其他的数据成员,因为对于某些材料,我可能会决定实现一个计时器,而对于另一些材料,我可能不需要。在Model类中为所有材料都拥有数据成员将浪费内存。

请帮帮我,我似乎找不到任何合适的设计模式。


如果Model类不知道SetPerFrameInfo的参数类型,它如何传递正确的对象?另一方面,如果它确实知道,您可以将参数类型从void *更改为更具体的内容。 - Thomas
这正是我的问题 :) 我需要想办法找到另一种方式。 我只是这样实现来描述我想要做的事情,但当然在现实中这并不起作用。 - xcrypt
4个回答

1

根据你的说法,听起来你的材质类(Material class)抽象了你的着色器(shaders),而每个材质之间的PerFrameData是不同的,因为每个着色器可以接受一组不同的输入。问题在于你有一个单一的数据存储类Model,它存储了可能有各种输入排列的内容。也就是说,你的模型包含纹理、顶点颜色还是两者都有?使用什么样的顶点格式?三角形是按照条带还是列表索引?模型是否公开法线图?光照图?规范?等等。

你真的需要定义一组标准的组件,让你的渲染系统能够理解,并且设计一个接口,该接口了解所有这些组件,从而成为Model和Material之间的桥梁。Material需要通过这个接口查询Model的数据,并验证它是否公开了正确的内容。Model可以构造这个类并将其传递给Material以进行渲染。

另一个需要考虑的因素是性能。如果你的渲染器循环遍历所有的Model对象调用Render,那可能会非常低效,因为每个Model可能有多种材质,因此你可能会在每帧中多次切换着色器状态,导致巨大的性能损失。如果你改为按照材质分组进行渲染通路(render passes),你可以节省很多时间。


1

1.) 我认为你遇到的问题是由于试图将太多内容放入基类中。由于模型在调用SetPerFrameInfo函数时需要知道它正在使用的特定材料类型,为什么不给每个派生自材料的类一个自己的Set..()函数,以取代void*?因为材料基类没有办法与其每帧信息(它是void*)交互,那么为什么要将其泛化为基类呢?

2.) 只需将特定于材料的数据成员放入派生的材料类中即可。我假设每个材料实例对应于一个唯一的模型,因此模型的特定于材料的信息应该在其派生的材料类中。

针对您的澄清回答:

好的,我误解了两个类之间的关系。基本上,您想要在理想情况下拥有一组不知道模型细节的材料类,以及可以与任何材料一起工作而不知道材料细节的模型集合。因此,您可能希望将Rough材料实例应用于六个不同的模型。

我认为没有任何疑问,如果每个材料实例由几个模型共享,每个模型实例都不知道不同的材料,就需要第三个针对模型对象的对象来存储计时器或绘制所需的任何实例数据。 因此,每个模型都会接收到自己的AppliedMaterial对象,该对象指向一个共享的材料实例以及将该材料应用于该模型所需的数据成员。 对于每种材料类型都必须有不同的AppliedMaterial子类,存储与每种材料相关的数据值。 使用第三个上下文对象的示例:

struct GlossMaterialContext
{
    Timer t;
    int someContextValue;
};

class GlossMaterial : Material
{
    void * NewApplicator() { return new GlossMaterialContext; }
    void FreeApplicator(void *applicator) { delete (GlossMaterialContext*)applicator; }
    void ApplyMaterial(void *applicator)
    {
        // Set up shader...
        // Apply texture...
    }
};

class 3DModel
{
    Material *material;
    void *materialContext;

    void SetMaterial(Material *m)
    {
        material = m;
        materialContext = m->NewApplicator();
    }
    void Draw()
    {
        material->ApplyMaterial(materialContext);
    }
};

这不是一种非常干净的技术。我总觉得处理void*指针很繁琐。你也可以让第三个上下文对象基于一个类,比如:

class AppliedGlossMaterial : AppliedMaterial

并且:

class GlossMaterial : Material
{
    AppliedMaterial * NewApplicator() { return new AppliedGlossMaterial; }
    ...

如果数据成员同时依赖于模型类型和材料类型,那么您的模型和材料太过紧密耦合,无法将它们变成彼此不知道的独立类。您需要创建 Model 的子类:Glossy3DModel、Rough3DModel 等等。

因为在构造函数中应该向“Model”提供指向材料的指针,为每种材料创建不同的“Model”实现会很奇怪。我将在我的问题中更新这些信息。 - xcrypt
例如,我们可以有一个带有光泽材质的3D模型。但是我们也可以使用外观不同的材质来创建相同的3D模型。 - xcrypt
请问您能否澄清第五段内容?(关于每个模型对象的第三个)如果可以的话,能否提供一些伪代码,这样更容易理解。为了澄清您对渲染的困惑:渲染代码是在模型类中实现的,其中涉及设置材质参数(到着色器),然后使用该着色器绘制模型。 - xcrypt
在SetMaterial()方法中,如果不进行检查,你怎么知道如何填写新创建的结构体呢? - xcrypt
如果需要了解模型的详细信息,则必须通过AppliedMaterial基类传递这些信息。正如@brendanw所指出的那样,更有效(甚至可能更容易)的方法是将其分离并按材料分组。例如,列出所有使用光泽材料的模型,应用该材料,然后绘制列表中的所有模型。 - jowo
显示剩余2条评论

1

首先声明一下,我之前没有实现过这样的系统,但是我有一些想法。

问题出在 Model::Draw 方法自己改变了材质,而它并不知道具体的材质类。这就需要将这个“材质修改”操作放到外部去做。

void Model::Draw() {
    // do draw by using the current material
}

void Model::UpdateMaterial(Material* mat) {
    m_pMaterial = mat
}

由于您在每个帧的其他地方修改材料,因此您应该在那时知道其类型。这种更改的材料被排队等待特定对象和帧。

然后,在渲染帧之前,您可以进行准备阶段,在那里进行必要的簿记,并将材料替换为新材料。

我还建议使用类似 boost::shared_ptr 的东西来管理您的 Material 对象,因为跟踪哪些对象共享材料以及是否需要删除它非常麻烦。


这可能是一个有效的解决方案,但看起来很奇怪! - xcrypt
确实。要带着一点怀疑的态度看待这个想法,因为这只是我尝试的一个非常模糊的初步想法。关键是你应该在模型之外处理这些修改。如果你仔细想想,为什么模型要处理帧呢?我认为其他类应该处理所有的帧记录。这样你就可以把所有东西放在一个地方,而不是分散在许多类/方法中。 - Alexander Kondratskiy

0

我认为大多数渲染引擎都会将实际渲染收集到一个中央算法/算法族中,因此draw()将在一个中央渲染器类中实现,该类将持有所有要在即将到来的帧中呈现的对象的集合。然后,它将调用方法来获取适当的低级数据(顶点和颜色、着色器等),这些数据将通过模型和材质接口公开。这是因为材质可能对于成为材质知道很多,但它可能不应该知道任何关于DirectX或OpenGl的东西。

然而,根据您发布的内容,您可以实现类似以下的东西:

class PerFrameData
{
    //Methods to get data in a standard way
}

class Material
{
    setPerFrameData(PerFrameData data)
    {
        if (data.hasVertices())
        // or whatever
    }
}

class Model
{
    PerFrameData data;
    Material material;

    draw()
    {
        material->setPerFrameData(data);
    }
}

当然,那样做是可行的,我也考虑过,但这不是一个特别好的设计:它会涉及太多的检查。好的面向对象编程代码应该是“告诉,而不是询问”,对吧? - xcrypt
@xcrypt 是的,告诉而不是询问是面向对象编程的一个原则。但是,基于接口编程和抽象细节更为根本。面向对象系统可能有更多的代码行,但您将封装每帧数据的逻辑,使其与模型或材质的逻辑分离,而不是让模型和材质都了解您的 void* 的细节。 - Tim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接