【问题标题】:Engine to render different types of graphic objects渲染不同类型图形对象的引擎
【发布时间】:2014-12-30 02:11:08
【问题描述】:

我正在尝试编写一个类(某种图形引擎),基本上它的目的是渲染我传递给它的任何东西。在我见过的大多数教程中,对象都是自己绘制的。我不确定事情是否应该这样工作。我一直在互联网上搜索试图想出不同的方法来处理这个问题,我一直在一遍又一遍地审查函数模板和类模板(这听起来像是我可能正在寻找的解决方案)但是当我尝试使用模板,对我来说似乎很乱(可能是因为我不完全了解如何使用它们)然后我会想把模板类放下,然后我会再试一次,但后来我就接受了再次下降,我不确定这是否是要走的路,但可能是。最初它只是基于平铺的(包括屏幕上的可移动播放器和相机系统),但现在我正在尝试编写一个平铺地图编辑器,其中包含工具栏、列表、文本,甚至可能是图元未来的屏幕等等,我想知道如何使用特定的程序将所有这些元素绘制到屏幕上(该程序现在并不重要,我稍后会发现)。如果你们中的任何一个人要编写一个图形引擎类,你将如何让它区分不同类型的图形对象,例如一个不被绘制为精灵的图元或一个不被绘制为三角形图元的球体图元, ETC。?任何帮助,将不胜感激。 :)

这是它的标题,它现在不起作用,因为我一直在对其进行一些编辑,请忽略我使用“new”关键字的部分,我仍在学习,但我希望这可以让我了解我想要完成的工作:

//graphicsEngine.h

#pragma once
#include<allegro5\allegro.h>
#include<allegro5\allegro_image.h>
#include<allegro5\allegro_primitives.h>

template <class graphicObjectData>
class graphicsEngine
{
public:

    static graphicObjectData graphicObject[];
    static int numObjects;

    static void setup()
    {
        al_init_image_addon();
        al_init_primitives_addon();
        graphicObject = new graphicObjectData [1]; //ignore this line
    }

    template <class graphicObjectData> static void registerObject(graphicObjectData &newGraphicObject) //I'm trying to use a template function to take any type of graphic object
    {
        graphicObject[numObjects] = &newObject; 
        numObjects++;
    }

    static void process() //This is the main process where EVERYTHING is supposed be drawn
    {
        int i;
        al_clear_to_color(al_map_rgb(0,0,0));
        for (i=0;i<numObjects;i++) drawObject(graphicObject[i]);
        al_flip_display();
    }

};

【问题讨论】:

    标签: c++ graphics rendering


    【解决方案1】:

    我是模板的忠实粉丝,但您可能会发现在这种情况下它们很麻烦(尽管不一定是错误的答案)。由于看起来您可能需要绘图容器中的多种对象类型,因此继承实际上可能是一个更强大的解决方案。

    您将需要一个为绘图提供抽象接口的基本类型。这个类所需要的只是一些为实际绘制过程提供机制的函数。它实际上并不关心绘图是如何发生的,重要的是派生类知道如何绘制自己(如果您想将绘图和对象分开,请继续阅读,我将尝试解释一种方法来实现这一点):

    class Drawable {
    public: 
        // This is our interface for drawing.  Simply, we just need 
        // something to instruct our base class to draw something.
        // Note: this method is pure virtual so that is must be 
        // overriden by a deriving class.
        virtual void draw()  = 0;
    
        // In addition, we need to also give this class a default virtual 
        // destructor in case the deriving class needs to clean itself up.
        virtual ~Drawable() { /* The deriving class might want to fill this in */ }
    };
    

    从这里开始,您只需编写从 Drawable 类继承的新类并提供必要的 draw() 覆盖。

    class Circle : public Drawable {
    public:
    
        void draw() { 
            // Do whatever you need to make this render a circle.
        }
    
        ~Circle() { /* Do cleanup code */ }
    };
    
    class Tetrahedron : public Drawable {
    public:
        void draw() { 
            // Do whatever you need to make this render a tetrahedron.
        }
    
        ~Tetrahedron() { /* Do cleanup code */ }
    
    };
    
    class DrawableText : public Drawable {
    public:
        std::string _text;
    
        // Just to illustrate that the state of the deriving class 
        // could be variable and even dependent on other classes:
        DrawableText(std::string text) : _text(text) {}
    
        void draw() { 
            // Yet another override of the Drawable::draw function. 
        }
    
        ~DrawableText() { 
            // Cleanup here again - in this case, _text will clean itself 
            // up so nothing to do here.  You could even omit this since
            // Drawable provides a default destructor.
        }
    
    };
    

    现在,要将所有这些对象链接在一起,您可以简单地将它们放在您选择的容器中,该容器接受引用或指针(或在 C++11 及更高版本中,unique_ptr、shared_ptr 和朋友)。设置您需要的任何绘制上下文并循环调用 draw() 的容器的所有内容。

    void do_drawing() {
    
        // This works, but consider checking out unique_ptr and shared_ptr for safer 
        // memory management
        std::vector<Drawable*> drawable_objects;
        drawable_objects.push_back(new Circle);
        drawable_objects.push_back(new Tetrahedron);
        drawable_objects.push_back(new DrawableText("Hello, Drawing Program!"));
    
        // Loop through and draw our circle, tetrahedron and text.
        for (auto drawable_object : drawable_objects) {
            drawable_object->draw();
        }
    
        // Remember to clean up the allocations in drawable_objects!
    }
    

    如果你想为你的绘图机制提供状态信息,你可以要求它作为 Drawable 基类的 draw() 例程中的参数:

    class Drawable {
    public: 
        // Now takes parameters which hold program state
        virtual void draw(DrawContext& draw_context, WorldData& world_data)  = 0;
    
        virtual ~Drawable() { /* The deriving class might want to fill this in */ }
    };
    

    当然,派生类 Circle、Tetrahedron 和 DrawableText 需要更新它们的 draw() 签名以采用新的程序状态,但这将允许您通过设计的对象进行所有低级绘图用于图形绘制,而不是用这个功能给主类增加负担。您提供的状态完全取决于您和您的设计。它非常灵活。


    重大更新 - 使用组合的另一种方法

    我一直在仔细考虑,并决定分享我一直在做的事情。我上面写的过去对我有用,但是这一次我决定用我的引擎走一条不同的路线,完全放弃一个场景图。我不确定我是否可以推荐这种做事方式,因为它会使事情变得复杂,但它也为极大的灵活性打开了大门。实际上,我已经编写了较低级别的对象,例如 VertexBuffer、Effect、Texture 等,它们允许我以任何我想要的方式组合对象。这次我使用的模板不仅仅是继承(尽管对于为 VertexBuffers、Texture 等提供实现仍然需要继承)。

    我提出这个的原因是因为你在谈论获得更大程度的分离。使用我描述的系统,我可以像这样构建一个世界对象:

    class World {
    
    public:
    
        WorldGeometry geometry; // Would hold triangle data.
        WorldOccluder occluder; // Runs occlusion tests against
                                // the geometry and flags what's visible and 
                                // what is not.
        WorldCollider collider; // Handles all routines for collision detections.
        WorldDrawer drawer;     // Draws the world geometry.
    
        void process_and_draw();// Optionally calls everything in necessary
                                // order.
    };
    

    在这里,我将有多个对象,它们专注于我的引擎处理的一个方面。 WorldGeometry 将存储有关此特定世界对象的所有多边形详细信息。 WorldOccluder 将对相机和几何体进行检查,以查看实际可见的世界斑块。 WorldCollider 将处理针对任何世界对象的碰撞检测(为简洁起见,省略)。最后,WorldDrawer 实际上将负责世界的绘制,并根据需要维护 VertexBuffer 和其他较低级别的绘图对象。

    如您所见,这与您最初询问的内容更接近,因为几何实际上不仅仅用于渲染。它是关于世界多边形的更多数据,但可以提供给不做任何绘图的 WorldGeometry 和 WorldOccluder。事实上,World 类的存在只是为了将这些相似的类组合在一起,但 WorldDrawer 可能不依赖于 World 对象。相反,它可能需要一个 WorldGeometry 对象,甚至是一个三角形列表。基本上,您的程序结构变得高度灵活,并且依赖关系开始消失,因为对象不经常或根本不继承,并且只请求它们绝对需要的功能。举个例子:

    class WorldOccluder {
    public:
    
        // I do not need anything more than a WorldGeometry reference here //
        WorldOccluder(WorldGeometry& geometry) : _geometry(geometry)
    
        // At this point, all I need to function is the position of the camera //
        WorldOccluderResult check_occlusion(const Float3& camera) {
            // Do all of the world occlusion checks based on the passed 
            // geometry and then return a WorldOccluderResult
            // Which hypothetically could contain lists for visible and occluded
            // geometry
        }
    
    private:
        WorldGeometry& _geometry;
    };
    

    我选择 WorldOccluder 作为示例,因为我花了一天的大部分时间为我的引擎处理类似的东西,并且使用了与上面类似的类层次结构。我有 3D 空间中的盒子根据是否应该看到它们来改变颜色。我的课程非常简洁且易于理解,我的整个项目层次结构也很容易理解(我认为无论如何)。所以这似乎工作得很好!我喜欢度假!

    最后说明:我提到了模板,但没有解释它们。如果我有一个围绕绘图进行处理的对象,那么模板对此非常有效。它避免了依赖关系(例如通过继承),同时仍然提供了很大程度的灵活性。此外,编译器可以通过内联代码和避免虚拟样式调用来优化模板(如果编译器可以推断出这样的优化):

    template <typename TEffect, TDrawable>
    void draw(TEffect& effect, TDrawable& drawable, const Matrix& world, const Matrix& view, const Matrix& projection) {
    
        // Setup effect matrices - our effect template 
        // must provide these function signatures
        effect.world(world);
        effect.view(view);
        effect.projection(projection);
    
        // Do some drawing! 
        // (NOTE: could use some RAII stuff here in case drawable throws).
        effect.begin();
        for (int pass = 0; pass < effect.pass_count(); pass++) {
            effect.begin_pass(pass);
            drawable.draw();    // Once again, TDrawable objects must provide this signature
            effect.end_pass(pass);
        }
        effect.end();
    }
    

    【讨论】:

    • 感谢您的回复,这完全符合我通常渲染图形的方式,也是我看到其他人这样做的方式,但在某些情况下,我听说对象不应该担心关于与绘图有关的任何事情,这是图形引擎的全部责任,根据给定的数据(如矢量、大小和颜色)来确定如何绘制对象。这是专业程序员的做法吗?
    • 我实际上不确定专业人士会如何做到这一点,但如果它是相似的,我不会感到惊讶。实际上,我一直在研究自己的引擎,过去,这就是我处理它的方式。然而,这一次我一直在做不同的事情,甚至没有包括场景图。这听起来可能很奇怪,但我这样做是为了保持灵活性。但是,它确实使编写程序变得更加困难,因为没有通用的绘图系统。对象被抽象为诸如 VertexBuffer、Effect、Texture 之类的东西,我从中组合出其他类型。我会用信息更新我的答案。
    • 享受假期!我很惊讶你真的在做 3D!我主要还没有开始 3D,除了尝试像自上而下的 rpgs 那样实现 sprite z-order 之外,有一次我绘制了一个由线条和顶点组成的立方体,但我不知道我会如何拥有所有到处都是3D。不过现在,我有一些想法可以回顾。
    • 虽然您提供了很好的建议并且我会使用其中的大部分,但我相信我最好将我的图形引擎分成两个或更多类;一个为我的平铺编辑器界面图形(即:editorInterface 类)构建的,并且可能是您建议的另一个世界级的 worldDrawer。我认为拥有单独的图形引擎会更现实,因此当我调用它们时,它们将能够直接绘制它们所构建的内容,而不会混淆不同的数据类型。
    • 你比我更了解你的引擎,所以做最好的。我很高兴我能帮助激发一些想法! :)
    【解决方案2】:

    我的技术可能真的很烂,但我是这样做的。

    class entity {
    public:
        virtual void render() {}
    };
    
    vector<entity> entities;
    
    void render() {
        for(auto c : entities) {
            c->render();
        }
    }
    

    然后我可以做这样的事情:

    class cubeEntity : public entity {
    public:
        virtual void render() override {
            drawCube();
        }
    };
    
    class triangleEntity : public entity {
    public:
        virtual void render() override {
            drawTriangle();
        }
    };
    

    并使用它:

    entities.push_back(new cubeEntity());
    entities.push_back(new triangleEntity());
    

    人们说使用动态继承不好。他们比我聪明得多,但这种方法已经有一段时间了。确保将所有析构函数设为虚拟!

    【讨论】:

    • 感谢您的回复,您的方式很好理解,但我不确定是否应该这样做,或者我应该将“实体”的所有数据传递给一个类(即图形引擎类),它可以使用所有数据来进行绘图。这是大多数程序员的做法吗?
    • @Blahblahblah 我不知道。
    【解决方案3】:

    SFML 图形库绘制对象的方式(以及我认为最易于管理的方式)是让所有可绘制对象都继承自“可绘制”类(如 David Peterson 的回答中的那个),然后可以将其传递给图形引擎以便被绘制。

    要绘制对象,我需要:

    基类:

    class Drawable 
    {
        int XPosition;
        int YPosition;
        int PixelData[100][100]; //Or whatever storage system you're using
    }
    

    这可用于包含所有可绘制类共有的信息(如位置和某种形式的数据存储)。

    派生子类:

    class Triangle : public Drawable
    {
        Triangle() {} //overloaded constructors, additional variables etc
        int indigenous_to_triangle;
    }
    

    因为每个子类在很大程度上都是独一无二的,所以您可以使用此方法创建从精灵到图形基元的任何东西。

    每个派生类都可以通过引用传递给引擎

    引用基类的“绘制”函数:

    void GraphicsEngine::draw(const Drawable& _object);
    

    使用此方法,不再需要模板。不幸的是,您当前的 graphicsObjectData 数组不起作用,因为派生类将被“切片”以适应它。然而,创建一个 'const Drawable*' 指针(或者最好是智能指针)的列表或向量同样适用于密切关注所有对象,尽管实际对象必须存储在其他地方。

    您可以使用这样的东西来使用指针向量绘制所有内容(我试图保留您的函数和变量名称):

    std::vector<const Drawable*> graphicObject; //Smart pointers would be better here
    
    static void process()
    {
        for (int i = 0; i < graphicObject.size(); ++i) 
            draw(graphicObject[i]);
    }
    

    您只需确保在创建每个对象时将其添加到列表中。 如果你很聪明,你甚至可以在构造和破坏中做到这一点:

    class Drawable; //So the compiler doesn't throw an error
    
    std::vector<const Drawable*> graphicObject;
    
    class Drawable
    {
        Triangle() {} //overloaded constructors, additional variables etc
        int indigenous_to_triangle;
    
        std::vector<const Drawable*>::iterator itPos; 
    
        Drawable() {
            graphicObject.push_back(this);
            itPos = graphicObject.end() - 1;
        }
    
        ~Drawable() {
            graphicObject.erase(itPos);
        }
    }
    

    现在您可以创建对象,并且在调用 process() 时会自动绘制它们!一旦它们被销毁,它们甚至会从列表中删除!

    上述所有想法过去都对我很有帮助,所以我希望我能帮助你,或者至少给你一些思考。

    【讨论】:

    • 我很欣赏这里的所有答案,他们非常彻底。我会记住他们,看看我能做些什么。 :)
    猜你喜欢
    • 2021-06-19
    • 1970-01-01
    • 2011-05-05
    • 2013-06-08
    • 2011-04-18
    • 2013-01-10
    • 1970-01-01
    • 1970-01-01
    • 2018-08-10
    相关资源
    最近更新 更多