IxEngine开发笔记

My Links

Blog Stats

News

2008年9月2日 #

第二回 关于vector和deque


     vector是我最早用的stl容器,用得也最顺手,它的结构也是清晰易懂.deque就比较神秘一些,帮助上提到它的一个最显著的特点就是可以从容器的 前端插入成员,而且效率很高.当时觉得很神奇,不知道是怎么做的.stl的源代码看起来太痛苦了,所以到今天也没准确的了解它的结构.不过后来用的多了, 对它的习性也有了一定的了解,有些地方相对于vector来说其实是很有优势的.下面就把两者对比着说说:
 
     *.先说内部结构.vector就是一块连续的内存,这块连续的内存会随着成员的添加而不断的re-alloc,而且在重分配的时候,分配的内存的大小会 比实际需要的多一些,下次再添加成员时,就可以添加在这些多余的空间里,而不会导致每添加一个成员就需要重分配内存.vector封装的是一块连续的内 存,这是我最喜欢它的地方,因为可以把它的成员直接转换成指针来进行访问,很灵活.
 
     *.deque可以根据一个索引进行随机访问,所以我一度也以为它内部有一块连续的内存,直到有一次我真这么干的时候把程序搞当了.才意识到这个错误.deque内部应该是由很多定长的内存块组成的链表,这是我猜的,因为似乎只有这种结构才能和它的表现相符.
 
     *.往vector,deque里添加数据应该都是很快的吧,毕竟这是这两个容器的卖点.这个我没有具体测过.
 
     *.vector的遍历速度是很快的,应该是到极限了,不管你用iterator来遍历还是用一个递增的下标进行访问,经过编译器的优化都可以有最高的效率.
 
     *.deque的遍历速度也不慢,如果使用iterator来遍历,可以有接近于vector的效率,但如果直接用递增的下标进行遍历,好像编译器无法优化至最高效率,好像慢一倍左右:
      std::deque<int> buf;
      std::deque<int>::iterator it;
      int sum=0;
      for (it=buf.begin();it!=buf.end();it++)  //这样遍历比较快
            sum+=*it;
      for (int i=0;i<buf.size();i++)  //这样遍历比较慢
            sum+=buf[i];
 
     *.vector内部分配的内存是永不释放的,即使你调用clear()也不会,这一点很不好,有误导性.有可能一个vector只在瞬间需要很大的容 量,但大多数时间只需要很小的容量,结果却是长时间的占用了很大的,没有被使用到的内存.vector也没有提供函数来释放它内部的内存,不过有一个简单 的办法,前几天在网上找到的:
     i_math::vector<BYTE>buf;
     buf.resize(100000);//分配了一块至少100000 bytes的内存
     if (TRUE)//清空buf的内存
     {
           i_math::vector<BYTE> t;
           buf.swap(t);//把这块内存交换到一个临时的vector里去
     }
     assert(buf.capacity()==0);//内存被清空了
 
     *.deque就不一样了,deque永远不会占用太多冗余的内存,你只需要把它resize()到一个你希望的大小,它会自动释放掉那些被多余占用的内存
 
     *.vector还有一个不好的地方,当你往一个vector里添加一个成员的时候,所有指向这个vector的原来成员的指针就不能保证有效了,因为 vector会re-alloc内存.而deque不会,无论从前面还是后面添加新成员,旧的成员都不会移动位置,这一点有时候很有用.
 
     所以我觉得deque其实在很多地方都有优势,比起vector,它欠缺的就是内存不连续,使用起来不够灵活,但它对内存使用的更经济,而访问的效率也比 vector慢不了太多,当然,它还能从前面快速插入删除,这是压倒性的优势.总之,deque是在关键时候能帮上忙的那种,平时可能还是vector更 好用一些.
 
     最后顺便说说list,我一点也不喜欢list,几乎没怎么用过,往list里添加成员是很慢的(相对与vector,deque),好像每添加一个成员 都要分配一次内存,它的遍历也很慢,好像就比map的遍历快一点,不能随机访问.唯一的优势就是可以在容器中间插入删除,不过我觉得都是可以用 vector/deque加上一些技巧解决的。反正我在程序里很少碰到过非用list不可的情况,也许是我写的程序类型还不够多吧呵呵.
 
     ps.我用的是stlport,vc的实现应该也差不多吧.

posted @ 2008-09-02 21:03 ixnehc 阅读(2286) 评论(8) 编辑

第一回 开篇 D3D渲染流程简介


     开发这个3D engine已经两年半了,从06年8月刚开始统计的4万多行,到今年7月份的21万多行,有一些感慨,感觉有那么点成就感,不过更多的是惴惴之 心:这些代码可以很好的在一起工作吗,足够快吗?bug肯定不少,因为测试的强度毕竟不高,这还不是最重要,架构上会不会有致命的疏忽的地方?会不会在未 知的需求面前不堪一击?前些日子看了看两年前写的代码,觉得不满意的地方很多,代码写出来的一瞬间就开始贬值,两年多时间,也的确该贬得差不多了,有心要 重写,似乎又没多少时间,接下来还有很多事情要做,而自由宽松的开发环境不是能永远维持下去的.还有一些担心:我是否真的尽力了呢?完成这些功能,是否真 的需要这么长时间呢?它们是否真的有价值呢?不仔细回忆,都有点想不起来一年前都干了些什么.虚度光阴?有这个可能--往消极的一面想,还真能想出不少东 西.不过要积极点呢,也有不少值得欣慰的地方,两年来至少学到了不少东西,对3D图形方面的经验有了些积累,不像两年前那么生疏了,脚本也接触了一些,编 辑器写得更熟练了,外观也更漂亮了(这个很关键,写程序不就是为了写点好看的东西吗?),模板也用得比以前熟练多了,有些问题的解决方法自己也挺满 意...二十多万行垃圾代码里,居然也有那么些闪光点,若隐若现,叫人安慰.
     所以我打算从这个炎热的夏天开始,关于这个engine写点什么,一来可以留作纪念,二来可以整理下思路,并做将来的备忘,三来可以把自己的一些想法公布 出来,用以交流,不敢说高台教化,起码是劝人向善,教人学好(呵呵,怎么和郭德纲一个口风),如果能有人看到这些东西,受到些启发,也是很好的事.
 
 先从最基础的写起吧,关于Device的渲染流程.
 D3D9的Device就是D3D给我们提供的一个绘制3D图形的工具,它的绘制流程大致是这样的:
 
*.首先Device的使用者要准备好顶点数据,也就是一个顶点的数组,称为A
 
*.然后这个数组A被传入device的渲染管线
 
*.device内部依次对每个顶点进行处理,有两种模式,固定管线和shader模式,所谓固定管线就是device内部实现的一个固定的程 序,用户只能通过设定各种参数(一些RenderState)来控制它,当然这不够灵活,所以有了shader模式,也就是说,用户需要写一个程序片段 (所谓vertex shader),传给device,然后device使用这个片段对每个顶点进行处理.这个程序片段是在显卡上执行的.
 
*.传入的顶点数组A的每一个元素被转换后,存储到另一个数组B中.数组B中的每个元素必须至少包含一个透视空间的位置,用来做裁剪.
 
*.数组B被传入到device的下一个计算阶段,在这个阶段里,数组B中的(被转换过的)顶点被组织成一个个三角形,然后对这些三角形进行裁 剪(利用顶点数据里包含的那一个透视空间的位置),背面剔除(注意背面剔除和顶点的法线是没关系的),最后剩下的三角形被保存到一个数组C中.(注意在这 个阶段里顶点数组变成了三角形数组)
 
*.数组C被传入到下一个计算阶段,光栅化,对于数组C中每一个三角形,首先把它们从透视空间映射到屏幕空间,然后找出它们在屏幕上覆盖的像素 (一个三角形覆盖的像素的数量有可能是很多的),对于每一个像素,根据它在三角形中的位置,通过三角形的顶点进行线性插值,计算出一个像素数据(注意像素 数据是通过三角形的顶点数据插值而来,所以它们的数据类型是一致的),所有三角形算出来的像素数据最后被存储到一个数组D中.(在这个阶段里,三角形的数 组变成了像素数据数组)
 
*.数组D被传入到下一个计算阶段,在这个阶段里,device会对这些像素做一些初步的过滤,主要是进行stencil test(根据stencil
buffer上的值)和z-test(根据这个像素的Z值和z-buffer上的值进行比较),根据测试结果会对stencil buffer进行一些修改(使用一组render state来控制这个过程),通过这些test的像素被存储到数组E.
 
*.数组E被传入到下一个计算阶段,在这个阶段里,device对每个像素数据进行处理,这个阶段也有两种模式,固定管线和shader模式, 与顶点处理阶段类似,用户也可以写一个程序片段,来对每一个像素数据进行处理,称为pixel shader.像素数据可能包含各种类型的数据,但经过这一阶段的处理后,输出是很简单的,一般就是一个颜色值和一个alpha值(透明度),也可以输出 一个Z值,不过好像不常用.在pixel shader里还可以使用专门的指令来放弃某一个像素的后续处理.所有的像素数据被处理后,结果存在一个数组F中.
 
*.数组F进入下一个阶段,在这一个阶段里,进行alpha test(根据像素的alpha 值),alpha test是最后一个test了,通过了alpha test的像素可以保证绘制到屏幕上去,通过test的像素会把它们的z值更新到z-buffer中去(具体由一组render state控制),通过test的像素被存入数组G
 
*.数组G进入下一个阶段,在这个阶段里,主要是把数组G里的像素和屏幕上已有的像素进行混合,具体混合的方式有多种多样,由一系列render state进行控制.混合以后的像素就被"画"到屏幕上了
 
基本上的流程就是这样了,环节很多,每个环节也有很多技巧,很多需要注意的地方,太多了,就不铺开来写了,我觉得这个流程很重要,虽然理解起来 并不困难,但真的能够非常熟练的记住它,并且运用其中的各个环节来解决实际的问题就不是那么容易的事了.所以不厌其烦的又把它粗粗的写了一遍,希望自己也 能进一步加深印象.
 

最近正在写关于多线程渲染的代码,下一回可能写写这方面的东西.

 

 

谢谢楼下的评论纠正了我的一个错误,stencil test和depth test的确是在pixel shader后才进行的,虽然这看上去没什么道理, 不过标准的渲染应该是这样的流程,但好像某些显卡会在pixelshader前做一些早期的test(early-z和early-stencil)来提高性能,我希望这样的显卡越多越好.看来我要去重新check一下引擎中使用stencil buffer提高性能的地方了,不过好像当时的测试的确是提高了一些性能的.

posted @ 2008-09-02 20:53 ixnehc 阅读(3136) 评论(14) 编辑