基于OpenGL编写一个简易的2D渲染框架-11 重构渲染器-Renderer

摘要:
但是,VertexIndexData也被定义为链表的一个结点,VertexIndexData之间可以通过链表链接起来,用链表链接起来的VertexIndexData间不会进行排序,这是为粒子系统的渲染做的特别优化。VertexIndexData是一个存储顶点数据的结构:structVertexIndexData{char*vertexData;/*顶点数据指针*/unsignedint*indexData;/*顶点索引数据指针*/intvertexCount;/*顶点数量*/intindexCount;/*顶点索引数量*/intindex;/*渲染排序索引*/VertexIndexData*next;};这里只有一个指向顶点数据内存的指针,渲染器并不知道顶点数据的格式是怎样的,只需知道每一个顶点数据的跨度即可。首先设置渲染器当前的Pass:voidRenderer::setPass{if(pCurrentPass!

假如要渲染一个纯色矩形在窗口上,应该怎么做?

先确定顶点的格式,一个顶点应该包含位置信息 vec3 以及颜色信息 vec4,所以顶点的结构体定义可以这样:

structVertex
{
     Vec3 position;
     Vec4 color;
};

然后填充矩形四个顶点是数据信息:

        Vertex* data = ( Vertex* ) malloc(sizeof( Vertex ) * 4);
        data[0].position.set(0, 0, 0);
        data[1].position.set(0, 0, 0);
        data[2].position.set(0, 0, 0);
        data[3].position.set(0, 0, 0);

        data[0].color.set(1, 1, 1, 1);
        data[1].color.set(1, 1, 1, 1);
        data[2].color.set(1, 1, 1, 1);
        data[3].color.set(1, 1, 1, 1);

分配一块内存,将内存类型转换为 Vertex,最后设置数据。上面只是用了4个顶点,显然还要设置索引数据:

        int* indices = ( int* ) malloc(sizeof( int ) * 6);
        indices[0] = 0;
        indices[1] = 2;
        indices[2] = 1;
        indices[3] = 0;
        indices[4] = 3;
        indices[5] = 2;

有了数据之后,需要设置顶点属性指针(这里没有使用 VBO 和 VAO):

        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), ( char* ) data + sizeof( float ) * 0);
        glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), ( char* ) data + sizeof( float ) * 3);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

最后,使用着色程序,调用 glDrawElements 函数进行绘制:

glUseProgram(shaderProgram);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, indices);

就这样,一次绘制的流程就结束了,这次渲染器就是按照上面的步骤展开。上面只是绘制了一个矩形,如果要绘制上百个矩形和上千张图片,并且它们的渲染管线的状态各不相同,还要保持一定的效率,管理这些顶点可不是一个简单的问题。

其中的一个难点就是如何分配顶点的内存,如果每渲染一个图元就要调用一次 malloc 和 free 函数,这样会带来很大的开销。或许我们可以在开始的时候就分配一大块内存,绘制时统统把顶点数据复制到这块内存,然后通过起始索引来绘制指定范围内的顶点,前面的第一种渲染器的设计方式用的就是这种。

基于OpenGL编写一个简易的2D渲染框架-11 重构渲染器-Renderer第1张

这样做就会带来大量的 drawcall(对 OpenGL 来说绘制参数(状态值)的变更要比绘制大量的顶点更耗费 CPU),绘制效率有所下降。想要更高的性能,通过批次合并(即将合理的方式将渲染状态相同多个可渲染物的 draw 绘制数据合并到一批绘制)可以降低开销,但这样批次合并的顶点数据必须连续储存在一起。那么这样就不能简单的分配一大块内存来储存这些顶点数据了。

那么有没有好的方法合理管理顶点内存?我给出的一个答案是 BlockAllocator(来自于 Box2D 库的 SOA 内存管理方案),它的原理是分配一大块内存 chunk,然后将 chunk 切割成许多的小块内存 block(block 的大小根据需要合理设置),block 通过链表连接起来。但申请一块内存的时候不再使用 malloc 函数,而是 BlockAllocator 直接返回一块合适大小的 block,用完后返还给BlockAllocator 即可,这个过程只有非常少量的 malloc,可以说效率很高。关于BlockAllocator 的详细解析,将在下一篇文章中给出。

Renderer 的实现

基于OpenGL编写一个简易的2D渲染框架-11 重构渲染器-Renderer第2张

Renderer 有两个 Pass 列表,分别是不透明 Pass 列表和半透明 Pass 列表:

typedef std::map<Pass*, RenderOperation, PassSort> PassVertexIndexMap;
/*不透明列表 */PassVertexIndexMap solidVertexIndexMap;
/*透明列表 */PassVertexIndexMap transparentVertexIndexMap;

在渲染器中,在插入 Pass 时可以对 Pass 进行排序,定义 Map 容器时设置了一个规定排序规则的仿函数 PassSort:

        structPassSort
        {
            bool operator()(Pass* a, Pass* b) const{
                if ( (*a) < (*b) ) return true;
                return false;
            }
        };

每个 Pass 对象都有一个对应的渲染操作:

    structRenderOperation
    {
        intstride;

        intvertexCount;
        intindexCount;
        std::vector<VertexIndexData*>*vidVector;
    };

Pass 所对应的绘制数据都储存在 vidVector 数组中,对于半透明图元的渲染,会经过这个数组的排序,就能确保安顺序渲染这些半透明的图元了。实际上,使用 VertexIndexData 数组储存顶点数据就可以了。但是,VertexIndexData 也被定义为链表的一个结点,VertexIndexData 之间可以通过链表链接起来,用链表链接起来的VertexIndexData 间不会进行排序,这是为粒子系统的渲染做的特别优化。

VertexIndexData 是一个存储顶点数据的结构:

    structVertexIndexData
    {
        char* vertexData;            /*顶点数据指针 */unsigned int*  indexData;    /*顶点索引数据指针 */
        int vertexCount;             /*顶点数量 */
        int indexCount;              /*顶点索引数量 */    

        int index;                   /*渲染排序索引 */VertexIndexData*next;
    };

这里只有一个指向顶点数据内存的指针,渲染器并不知道顶点数据的格式是怎样的,只需知道每一个顶点数据的跨度(RenderOperation 的 stride)即可。

如何通过渲染器渲染渲染顶点数据呢?首先设置渲染器当前的 Pass:

    void Renderer::setPass(Pass* pass, booltransparent)
    {
        if ( pCurrentPass != nullptr && pCurrentPass->equal(pass) ) return;
        pCurrentPass =pass;

        PassVertexIndexMap& map = transparent ?transparentVertexIndexMap : solidVertexIndexMap;
        auto it =map.find(pCurrentPass);
        if ( it ==map.end() ) {
            it =map.insert(PassVertexIndexMap::value_type(pCurrentPass, RenderOperation())).first;
            it->second.vidVector = new std::vector<VertexIndexData*>();
            it->second.vertexCount = 0;
            it->second.indexCount = 0;
        }
        pCurrentRenderOperation = &it->second;
    }

渲染器会根据 transparent 这个变量确定 Pass 是被添加到不透明表 solidVertexIndexMap还是半透明表transparentVertexIndexMap,然后查找 Pass 表,存在 Pass 的话就记录下 Pass 对应的 RenderOperation。渲染器是通过 Pass 对象合并顶点数据的,所以在开发游戏的时候可以把许多小图合并成一张大图,一张纹理对着一个 Pass,也就意味着一个 drawcall,这样会减低开销。

设置好渲染器的 Pass 后,就可以设置顶点数据到渲染器,渲染器会管理这些数据:

    void Renderer::addVertexIndexData(VertexIndexData* viData, int stride, intdepth)
    {
        VertexIndexData* sp =viData;
        while( sp ) {

            /*顶点变换 */
            for ( int i = 0; i < sp->vertexCount; i++) {
                Vec3* pos = ( Vec3* ) (( char* ) sp->vertexData + i *stride);
                *pos = mTransformMatrix * (*pos);
            }
            pCurrentRenderOperation->vertexCount += sp->vertexCount;
            pCurrentRenderOperation->indexCount += sp->indexCount;

            sp = sp->next;
        }

        /*设置排序索引 */
        int size = pCurrentRenderOperation->vidVector->size();
        viData->index = (depth == -1) ? -size : depth;

        /*添加顶点数据 */pCurrentRenderOperation->vidVector->push_back(viData);
        pCurrentRenderOperation->stride =stride;
    }

这里,我把顶点变换的操作放在了这里,而不是放到顶点着色器中。传进来的顶点数据会添加到 RenderOperation 的 VertexIndexData 数组中,并记录当前 RenderOperation 的顶点数量的索引数量。

渲染器的渲染

调用 render 函数即可完成渲染操作:

    voidRenderer::render()
    {
        if ( solidVertexIndexMap.empty() == false) {
            this->renderPassVertexIndexMap(solidVertexIndexMap);
        }
        if ( transparentVertexIndexMap.empty() == false) {
            this->renderPassVertexIndexMap(transparentVertexIndexMap);
        }
    }

先渲染不透明的 Pass 列表,再渲染半透明的 Pass 列表。

    void Renderer::renderPassVertexIndexMap(PassVertexIndexMap&map)
    {
        for ( PassVertexIndexMap::iterator pass_it = map.begin(); pass_it != map.end(); ++pass_it ) {
            Pass* pass = pass_it->first;
            pass->setOpenGLState();

            RenderOperation* ro = &pass_it->second;

            std::sort(ro->vidVector->begin(), ro->vidVector->end(), compare);
            this->doRenderOperation(&pass_it->second, pass);

            delete ro->vidVector;
        }
        map.clear();
    }

函数中,迭代 Pass 列表,先调用 Pass 的setOpenGLState 函数实现 OpenGL 状态的设置。然后会顶点数据进行排序(这里不透明列表本不需要排序的,这里设计成了不透明列表也进行排序),这里使用 std::sort 函数对 std::vector 的数据排序,最后一个参数是自定义的排序函数:

        static bool compare(VertexIndexData* a, VertexIndexData*b)
        {
            return a->index < b->index;
        }

最后在函数 doRenderOperation中完成顶点数据的绘制。

    void Renderer::doRenderOperation(RenderOperation* ro, Pass*pass)
    {
        int vertexBytes = ro->vertexCount * ro->stride;
        int indexBytes = ro->indexCount * sizeof(int);

        assert(vertexBytes <= 512000 && indexBytes <= 102400);

        int vertexIndex = 0, indexIndex = 0;
        for ( auto& ele : *(ro->vidVector) ) {
            VertexIndexData* sp =ele;

            while( sp ) {
                /*拷贝顶点索引数据 */
                for ( int i = 0; i < sp->indexCount; i++) {
                    indexBuffer[indexIndex++] = vertexIndex + sp->indexData[i];
                }
                /*拷贝顶点数据 */memcpy(vertexBuffer + vertexIndex * ro->stride, sp->vertexData, sp->vertexCount * ro->stride);
                vertexIndex += sp->vertexCount;

                /*释放资源 */pBlockAllocator->free(sp->vertexData);
                pBlockAllocator->free(sp->indexData);
                pBlockAllocator->free(sp);
                sp = sp->next;
            }
        }

        Shader* shader = pass->getShader();
        shader->bindProgram();
        shader->bindVertexDataToGPU(vertexBuffer);

        /*绘制 */glDrawElements(pass->getPrimType(), ro->indexCount, GL_UNSIGNED_INT, indexBuffer);
        nDrawcall++;
    }

函数中对 VertexIndexData 数组的数据进行一次合并,储存在一块内存中:

        char vertexBuffer[1024 * 500];      /*用于合并顶点的缓冲区 */unsigned indexBuffer[25600];        /*用于合并索引的缓冲区 */

在绑定好着色程序(同时绑定 Uniform 数据)和设置顶点属性指针后,调用 glDrawElements 函数进行绘制,完成一次渲染通路的渲染。整个渲染器的设计就结束了,和前一个渲染器相比,代码结构清晰了许多。后续会使用 Simple2D 开发一个游戏,并逐步完善渲染器。

源码下载:http://pan.baidu.com/s/1skOmP21

免责声明:文章转载自《基于OpenGL编写一个简易的2D渲染框架-11 重构渲染器-Renderer》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇如何修改.net framework(转载)sql Server插不进数据,以及Id自增的教程及注意事项下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

在Android应用中使用OpenGL

Android为OpenGL  ES支持提供了GLSurfaceView组件,这个组件用于显示3D图形。GLSurfaceView本身并不提供绘制3D图形的功能,而是由GLSurfaceView.Renderer来完成了SurfaceView中3D图形的绘制。 归纳起来,在Android中使用OpenGL  ES需要三个步骤: 1、创建GLSurfaceV...

OpenGL的GLUT事件处理(Event Processing)窗口管理(Window Management)函数[转]

GLUT事件处理(Event Processing)窗口管理(Window Management)函数 void glutMainLoop(void)      让glut程序进入事件循环。在一个glut程序中最多只能调用一次。一旦调用,会直到程序结束才返回。 int glutCreateWindow(char* name);     产生一个顶层的窗口。...

KD-tree学习笔记(超全!)

目录 K-D树 更新信息 建树插入 查询k远/近询问 重构 完整模板 K远点对 MOKIA(三维数点) K-D 树优化建边 后记 因为之前找不到全的博客,唯一的一篇码风比较毒瘤。。。 所以我就来写了 K-D树 大概是高维二叉树吧 每次按一个维度对超空间内的点进行二分划分 树上存左右节点和这个节点所代表的的点 更新信息 我们保存几个信...

简单基于OPENGL的三维CAD框架(1)工具类

在vc++中有CDC类,同样也可以开发基于OPENGL的OPenGLDC类,这样可以像调用CDC类一样调用OPenGLDC类 首先给出两个工具类,点类和向量类 typedef struct tagVector3D {double dx;double dy;double dz;} VECTOR3D; class CVector3D : public VECT...

OpenGL_棋盘

#include "stdafx.h" #include <gl/glut.h> void myInit(void) { glClearColor(0.2, 0.2, 0.2, 0.0);//设置背景颜色为白; glColor3f(0.0f, 0.0f, 0.0f);//设置绘图颜色为黑; glPointSize(1.0);//设置点大小...

OpenGL的glViewport视口变换函数详解[转]

调用glViewPort函数来决定视见区域,告诉OpenGL应把渲染之后的图形绘制在窗体的哪个部位。当视见区域是整个窗体时,OpenGL将把渲染结果绘制到整个窗口。 voidglViewPort(GLInt x; GLInty; GLSizeiWidth; GLSizeiHeight);     其中,参数X,Y指定了视见区域的左下角在窗口中的位置,一般情...