array 发表于 2008-9-25 13:35:24

OSG原创教程:最长的一帧(30)最终章

当前位置:osgViewer/ViewerBase.cpp第361行,osgViewer::ViewerBase::startThreading()
DrawThreadPerContext模式事实上是默认的一种线程模式,如果ViewerBase类的成员函数suggestBestThreadingModel没有找到适合当前计算机系统的线程模式的话,将自动采用这一模式来完成渲染的工作。不过值得注意的是,DrawThreadPerContext模式没有设置渲染启动栅栏_startRenderingBarrier和结束栅栏_endRenderingDispatchBarrier。因此这一渲染模式下将不会在每一帧对场景的筛选和绘制工作进行同步,且用户的更新动作将有可能在某些线程的渲染动作还未结束时即开始运行。

DrawThreadPerContext模式下会根据场景中摄相机的数量设置_endDynamicDrawBlock变量的值,这个阻塞器用于在每个渲染器都渲染完毕之前阻塞主进程的运行,以免用户对数据的更新动作与动态对象的渲染动作产生冲突。

DrawThreadPerContext需要执行的任务列表如下:

[*]1、osg::RunOperations任务(393行),它将负责场景的绘制(即最终执行Renderer::draw函数)。该任务不会从列表中删除。
[*]2、swapReadyBarrier任务(401行),它实质上是一个BarrierOperation栅栏对象,强度等于GC线程的数目。它的作用是在交换双缓存之前对所有GC线程执行一次同步。该任务不会从列表中删除。
[*]3、swapOp任务(404行),它是一个SwapBuffersOperation对象,用于执行绘制后的双缓存交换操作。该任务不会从列表中删除。
[*]这三个任务将在线程运行的过程中反复被执行。


那么,场景的筛选(CULL)工作呢?线程中不负责场景元素的裁剪的话,整个场景渲染的过程中不就缺少了重要的环节吗?不过不必心急,就像单线程模式中所实现的那样,场景筛选的工作是由ViewerBase::renderingTraversals函数完成的,正如下面的代码所示(ViewerBase.cpp,680行):if (!renderer->getGraphicsThreadDoesCull() && !(camera->getCameraThread()))
    renderer->cull();Renderer::getGraphicsThreadDoesCull函数只有在CullDrawThreadPerContext或者单线程模式下为true,而摄像机线程getCameraThread只有最后一种多线程渲染模式中才会被创建(CullThreadPerCameraDrawThreadPerContext)。因此此处的Renderer::cull函数将被执行。

这样一来,场景的筛选工作将在每一帧当中仅执行一次,而绘制工作则交给GC线程来完成。是否会因此造成筛选和绘制数据之间的冲突呢?答案当然是否定的,还记得我们在第十七日和十八日中所介绍的吗:Renderer::cull最后将向绘制队列_drawQueue中添加一个已完成筛选的场景视图对象(SceneView),而Renderer::draw函数一开始就会尝试从这个队列中取出一个数据(takeFront函数),并清空它在队列中的位置。因此,如果新的线程绘制工作提前到来的话,由于场景筛选的函数还没有把SceneView传入到_drawQueue队列中,因此这次多余的绘制动作将自动宣告结束(事实上也会暂时阻塞这个“多事”的线程)。

那么,我们依然假设有三个条件不一的GC渲染线程,并得到相应的流程示意图如下。注意主进程中三个设备的筛选工作与各个设备线程中的绘制工作是首尾相连的,即,主进程中的筛选一旦结束,线程所控制的绘制就会马上开始;而由于有swapReadyBarrier交换缓存栅栏的存在,各个图形设备的交换缓存工作依然是统一执行的。
附图1

图中还应注意的是,由于DrawThreadPerContext模式不存在渲染启动和结束栅栏,因此主进程执行完场景的筛选之后就可以继续执行,进而开始新一帧的用户数据更新工作。而此时GC线程的绘制工作很有可能还没有完成,如果此时场景中某些需要绘制的数据有了改变,将会造成无法预料的事情发生,最严重的当然就是系统崩溃了。

幸好我们还有动态对象阻塞器_endDynamicDrawBlock这个保护伞。在使用这一线程模式以及后面马上要介绍的CullThreadPerCameraDrawThreadPerContext模式时,不要忘记为场景中的变度对象设置setDataVariance(Object:: DYNAMIC)。

当前位置:osgViewer/ViewerBase.cpp第414行,osgViewer::ViewerBase::startThreading()
下面我们即将介绍的CullThreadPerCameraDrawThreadPerContext模式是OSG目前所采用的四种线程模型的最后一种,目前来看它也是实现效率最高的。

CullThreadPerCameraDrawThreadPerContext模式正如它的名字所说的那样:建立了多个摄像机线程,用于场景的筛选;同时建立了多个GC线程,用于场景的绘制。这种模式也提供了动态对象阻塞器_endDynamicDrawBlock,以免用户更新动作影响到场景的渲染工作。其中,各个GC线程的任务列表的建立,与DrawThreadPerContext模式并没有差异:

[*]1、osg::RunOperations任务负责场景的绘制。
[*]2、swapReadyBarrier任务的作用是在交换双缓存之前对所有GC线程执行一次同步。
[*]3、swapOp任务(404行)用于执行绘制后的双缓存交换操作。


而建立摄像机线程的数目与场景中摄像机的数目相同。其任务列表如下:

[*]1、_startRenderingBarrier,渲染启动栅栏在这里又起到了应有的作用,不过注意这种模式下它应当改个名字,叫做“筛选启动栅栏”。其强度是场景摄像机的数目加一,因此在renderingTraversals函数中(ViewerBase.cpp,662行)将会通过阻塞主线程,对场景的筛选线程(即各个摄像机)执行一次同步。
[*]2、osgViewer::Renderer,它也可以作为一个操作任务存在的,当线程中执行到这个任务时,会转而执行Renderer:: operator()(osg::Object* object)函数,并在其中执行Renderer::cull函数,完成场景的筛选。


以上所有的任务都会在线程的运行过程中循环执行。那么我们假设有三个条件不一的GC绘制线程和三个摄像机筛选线程,并得到流程示意图如下。注意这里摄像机线程的筛选工作与GC设备线程中的绘制工作同样是首尾相连的,即,摄像机线程的筛选一旦结束,GC线程的绘制就会马上开始。
附图2

这里我们还要简单地介绍一下OSG分配CPU的策略。对于各个GC线程(除了单线程之外的三种模式),将使用OpenThreads::Thread::setProcessorAffinity函数平均地安排到每个CPU上;如果还有摄像机线程的话,则按照与GC线程相同的做法,安置到最后一个GC线程之后的CPU上。

下图演示了一个四核CPU环境中,将三个GC线程与三个摄像机线程分配到各个CPU的分配方案:
附图3

当前位置:osgViewer/ViewerBase.cpp第593行,osgViewer::ViewerBase::frame ()
本节内容事实上也是这篇教程的最后一个部分,完成了对于startThreading和renderingTraversals函数的解析之后,frame函数也揭开了它的最后一缕面纱。不过随着仿真循环的执行,它依然会周而复始地循环下去……OSG的强大,也许就是如此简单,但我们却不得不佩服其创造者丰富的知识水平和良好的系统架构,惊叹于其中种种的优化策略和线程操作技巧,并庆幸自己能够在这个日渐庞大的软件系统仍在飞速发展的时期加入到它的开发者的行列。

那么,幸运的我们又该做些什么呢?是日复一日地翘首盼望新的功能?是抱怨OSG又出现了这样以及那样的毛病?是“跪求”他人贡献出各式各样的中英文的教程和代码?我想答案应当都是否定的。我们理应致力于OSG在国内的发展与成熟,致力于它的传播,致力于扩展它的功能和文档,以及做出各种各样自己力所能及的贡献。

国内的虚拟仿真行业依然处于创立和发展的阶段,由于它的市场和利润丰厚,技术含量较高,希望投身其中的企业和个人自然不在少数,有志于成就事业,甚至梦想着成为VR行业的比尔•盖茨的朋友也是大有人在。但是,坦白来说,国内的技术水平及相关产品目前还缺少足够的说服力;学校等相关研究机构似乎也鲜有高水平的研究成果问世(至少我在清华大学所知所见的寥寥);暂时更不存在如OpenSceneGraph,Orge,Vega,Virtools等广泛应用于实践,功能和稳定性都能够让人信服的中间件产品;各种高水平的中间件在求学者中间的普及程度恐怕也十分有限(OpenGL类的读物倒是海量,可惜大多内容重复,粗制滥造者比比皆是)。这也许可以说是巨大的机遇;但如果十年之后,我们所见所处的依然是这种环境,依然眼巴巴地望着国际级游戏大作的精妙效果,用着好不容易盗版得来的“洋软件”的话,就不得不说是一种永远难以逾越的差距,是一种悲哀。

不要以为我是在做一番“愤世青年”的感慨。作为一名曾经的机械行业的学生,我曾亲耳聆听一位名声赫赫的老教师的感慨:中国的液压技术永远达不到国际水平,从业者已经彻底放弃对它的研究,而是专心地为了购买欧美的过时产品而费尽口舌,忍受着高昂价格和肆意的限制。大飞机的研究也濒临这种命运,还有数控机床,还有伺服电动机……是否不知几时,还会多了虚拟仿真业呢?

就像我在这篇《最长的一帧》的序言中所说的那样:我们的任务是一直挖掘下去,找出期待的抑或不曾期待的瑰宝。很高兴我们能一直走到这一步,也很高兴OSG的方方面面还未有个尽头。笔者在艰难地写出这篇晦涩文章的同时,一直为自己的大大小小的发现而惊喜着,甚至有了以后开发自己的渲染引擎的念头和信心,相信读者您也如此(或者已经走在我的前面了 ^_^)。是的,我们的所得也许并非多么渺小,恰恰相反,它可能是一个新的成功作品的开端,而它的作者,也许就是您。

MiniGUI的成功曾经让我们中国的开源软件名扬世界(虽然如今的发展策略让它日渐闭塞起来)。那么,什么时候我们能拥有名扬世界的国产3D渲染引擎呢?最好就在明日,但愿就在不远的将来。

(全文完)

解读成果:
ViewerBase::frame。
悬疑列表:
无。

shengrendan2 发表于 2008-9-28 14:06:49

:lol

likai11 发表于 2008-10-8 22:24:49

写的非常的好 ,很实用 到此我全部看了一遍,大家都向楼主学习啊,这种研究精神很好:victory: :victory: :victory: :victory: :victory:

forest37 发表于 2008-10-10 00:03:25

非常感谢Array的奉献

请问Array,漫游器与RenderLeaf中的ViewMatrix是如何联系起来的啊

array 发表于 2008-10-10 08:40:21

原帖由 forest37 于 2008-10-10 00:03 发表 http://bbs.osgchina.org/images/common/back.gif
非常感谢Array的奉献

请问Array,漫游器与RenderLeaf中的ViewMatrix是如何联系起来的啊

这个分析起来来是比较复杂的。

漫游器设置的相机观察矩阵getInverseMatrix将赋给场景的主摄像机getCamera
摄像机会把ViewMatrix传递给场景的CullVisitor,然后在节点遍历的过程中,计算出渲染叶对应的正确的投影矩阵和模型视点矩阵,再传递给RenderLeaf

几个比较重要的函数:
CullVisitor::apply(osg::Camera&),CullVisitor::apply(Transform&),CullVisitor::apply(Geode&),StateGraph::addLeafAndDepth(RenderLeaf*)

forest37 发表于 2008-10-10 14:12:25

Array真是太敬业了,呵呵。
其实发帖之后我终于找到了他们的关联,只是打不开网页了,怀疑这个论坛的服务器是不是12点以后关了啊。

还是非常感谢Array

forest37 发表于 2008-11-28 13:44:36

读书笔记:
单线程模式下Renderer::getGraphicsThreadDoesCull虽然为true,但GraphicsThread并没有被创建
页: [1]
查看完整版本: OSG原创教程:最长的一帧(30)最终章