对于这种情况,什么是好的设计

What is a good design for this situation?

本文关键字:什么 于这种 情况      更新时间:2023-10-16

我正在制作一个基本的渲染引擎。

为了让渲染引擎在所有类型的几何体上操作,我上了这个课:

class Geometry
{
protected:
ID3D10Buffer* m_pVertexBuffer;
ID3D10Buffer* m_pIndexBuffer;
public:
[...]
};

现在,我希望用户能够通过继承这个类来创建自己的几何体。因此,假设用户制作了一个class Cube : public Geometry用户必须在初始化时创建vertexbuffer和indexbuffer。

这是一个问题,因为每次生成新的Cube对象时,它都会重新创建vertexbuffer和indexbuffer。每个派生类应该只有一个vertexbuffer和indexbuffer实例。要么是这样,要么是完全不同的设计。

解决方案可能是为继承类制作单独的static ID3D10Buffer*,并将继承类的指针设置为与构造函数中的指针相等。

但这需要一个像static void CreateBuffers()这样的静态方法,用户必须在应用程序中为他决定继承自Geometry的每种类型显式调用一次。这似乎不是一个好的设计。

什么是解决这个问题的好办法?

您应该将实例的概念与网格的概念分开。这意味着您可以为多维数据集创建一个几何体版本,该版本表示多维数据集的顶点和索引缓冲区。

然后引入一个名为GeometryInstance的新类,该类包含一个变换矩阵。这个类还应该有一个指向Geometry的指针/引用。现在,您可以通过创建所有引用同一几何体对象的几何体实例来创建几何体的新实例,而不会在创建新长方体时复制内存或工作。

编辑:假设你有问题中的Geometry类和评论中的Mesh类,你的Mesh类应该看起来像这样:

class Mesh {
  private:
  Matrix4x4 transformation;
  Geometry* geometry;
  public:
  Mesh(const Matrix4x4 _t, Geometry* _g) : transformation(_t), geometry(_g) {}
}

现在,当创建你的场景时,你想做一些事情,比如这个

...
std::vector<Mesh> myMeshes;
// OrdinaryGeometry is a class inheriting Geometry 
OrdinaryGeometry* geom = new OrdinaryGeometry(...);
for(int i = 0; i < ordinaryGeomCount; ++i) {
  // generateTransform is a function that generates some 
  // transformation Matrix given an index, just as an example
  myMeshes.push_back(Mesh(generateTransform(i), geom);
}
// SpecialGeometry is a class inheriting Geometry with a different
// set of vertices and indices
SuperSpecialGeometry* specialGeom = new SuperSpecialGeometry(...);
for(int i = 0; i < specialGeomCount; ++i) {
  myMeshes.push_back(Mesh(generateTransform(i), specialGeom);
}
// Now render all instances
for(int i = 0; i < myMeshes.size(); ++i) {
  render(myMeshes[i]);
}

请注意,我们只有两个在多个网格之间共享的几何体对象。理想情况下,应该使用std::shared_ptr或类似的方法来重新计数,但这超出了问题的范围。

在立方体示例中对几何体进行子分类的意义是什么?立方体只是几何体的一个实例,它有一组特定的三角形和索引。Cube类和Sphere类之间没有区别,只是它们用不同的数据填充三角形/索引缓冲区。因此,数据本身才是最重要的。您需要一种方法,允许用户向您的引擎提供各种形状数据,然后在数据生成后以某种方式引用这些数据。

对于提供形状数据,有两个选项。您可以决定将Geometry的详细信息保留为私有,并提供一些接口来获取原始数据,如文件中的字符串,或填充某个用户创建的函数的浮点数组,为该数据创建一个Geometry实例,然后为用户提供该实例的一些句柄(或允许用户指定句柄)。或者,你可以创建一些类,比如GeometryInfo,它有用户自己填充的addTriangle、addVertex等方法,然后有一些函数接受GeometryInfo、为该数据创建一个Geometry实例,然后再次给用户一些句柄。

在这两种情况下,你都需要提供一些界面,让用户说"这是一些数据,用它做一些东西,并给它一些处理。至少它会有我描述的功能。你需要在引擎中的某个地方维护一个创建的几何体实例的映射。这样你就可以强制执行每个形状一个实例的规则,这样你就能够关联用户想要的东西("Ball","Cube")与您的引擎所需的(带填充缓冲区的几何图形)。

现在谈谈把手。我会让用户将数据与一个名称相关联,比如"Ball",或者返回一些整数,然后用户将其与某个"Ball"实例相关联。这样,当你创建Rocket类时,用户可以从你的引擎请求"球"实例,其他各种对象可以使用"球",一切都很好,因为它们只是存储手柄,而不是球本身。我不建议存储指向实际几何体实例的指针。网格不拥有几何体,因为它可以与其他网格共享几何体。它不需要访问几何体的成员,因为渲染器处理繁重的工作。因此,这是一种不必要的依赖。唯一的原因是速度,但对句柄使用哈希也同样有效。

现在举几个例子:

提供形状数据:

//option one
engine->CreateGeometryFromFile("ball.txt", "Ball");
//option two
GeometryInfo ball;
ball.addTriangle(0, 1, 0, 1);
ball.addTriangle(...);
...
engine->CreateGeometryFromInfo(ball, "Ball");

使用句柄引用该数据:

class Drawable
{
    std::string shape;
    Matrix transform;
};
class Rocket : public Drawable
{
    Rocket() { shape = "Ball";}
    //other stuff here for physics maybe
};
class BallShapedEnemy : public Drawable
{
    BallShapedEnemy() { shape = "Ball";}
    ...
}
...
...in user's render loop...
for each (drawable in myDrawables)
{
    engine->Render(drawable.GetShape(), drawable.GetTransform());
}

现在,为每个不同的游戏对象(如火箭)设置一个单独的类是有争议的,这完全是另一个问题的主题,我只是在评论中让它看起来像你的例子。

这可能是一种草率的方法,但你能不能不做一个单例?

#pragma once
#include <iostream>
#define GEOM Geometry::getInstance()
class Geometry
{
protected:
    static Geometry* ptrInstance;
    static Geometry* getInstance();
    float* m_pVertexBuffer;
    float* m_pIndexBuffer;
public:
    Geometry(void);
    ~Geometry(void);
    void callGeom();
};
#include "Geometry.h"
Geometry* Geometry::ptrInstance = 0;
Geometry::Geometry(void)
{
}

Geometry::~Geometry(void)
{
}
Geometry* Geometry::getInstance()
{
    if(ptrInstance == 0)
    {
        ptrInstance = new Geometry();
    }
    return ptrInstance;
}
void Geometry::callGeom()
{
    std::cout << "Call successful!" << std::endl;
}

这个方法唯一的问题是,你只会有一个几何体对象,我想你可能想要不止一个?如果不是这样的话,它可能会很有用,但我认为Lasseralan的方法可能是一个更好的实现,适合您的需求。