《最长的一帧》理解

概况

宗旨:了解OSG在一帧时间,也就是仿真循环的一个画面当中做了什么。

while(!viewer.done())
        viewer.frame();

osgViewer::ViewerBase::frame()函数:
viewerInit():完成视景器的初始化工作;
realize():完成窗口和场景的设置工作;

advance():一帧经历的时间、帧数以及弃用对象的删除;
eventTraversal():执行用户设置的EventCallback,为所有的用户交互和系统事件提供一个响应的机制;
它必须在每一帧的仿真过程中,取出已经发生的所有事件,摒弃那些对场景不会有助益的(例如,在视口以外发生的鼠标移动事件和胡乱点击),依次交付给各个事件处理器,最后清空现有的事件队列,等待下一帧的到来。
updateTraversal():处理用户的更新回调对象之外,还要负责更新摄像机的位置,并且更新分页数据库DatabasePager 和图像库ImagePager 的内容。
更新回调与事件回调最大的不同在于:每当一个用户交互或系统事件产生时,每一个节点(以及Drawable 对象)的事件回调都会被调用一次;而节点(以及Drawable 对象)的更新回调只会在每帧中被调用一次。这一区别决定了我们应当在什么时候使用事件回调,以及在什么时候使用更新回调。
renderingTraversals():场景的渲染遍历工作。

osgViewer::View::init()

两个重要的类成员变量:_eventQueue和_cameraManipulator;

_eventQueue 存储视景器的事件队列。OSG中代表事件的类是osgGA::GUIEventAdapter,它可以用于表达各种类型的鼠标、键盘、触压笔和窗口事件。在用户程序中,我们往往通过继承osgGA::GUIEventHandler 类,并重写handle函数的方法, 获取实时的鼠标/ 键盘输入, 并进而实现相应的用户代码。

_eventQueue 除了保存一个GUIEventAdapter 的链表之外,还提供了一系列对链表及其元素的操作函数,这其中,createEvent 函数的作用是分配和返回一个新的GUIEventAdapter事件的指针。随后,这个新事件的类型被指定为 FRAME 事件,即每帧都会触发的一个事件。

_cameraManipulator是视景器中所用的场景漫游器的实例。通常我们都会使用setCameraManipulator 来设置这个变量的内容, 例如轨迹球漫游器(TrackballManipulator)可以使用鼠标拖动来观察场景,而驾驶漫游器(DriveManipulator)则使用类似于汽车驾驶的效果来实现场景的漫游。

osgViewer::Viewer::realize()

这里写图片描述
1、视景器Viewer 的主/从摄像机均需要使用setGraphicsContext 设置对应的图形设备上下文,实际上也就是对应的显示窗口;
2、GraphicsContext 的创建由平台相关的抽象接口类WindowingSystemInterface 负责,对于Win32 平台而言,这个类是由GraphicsWindowWin32.cpp 的Win32WindowingSystem 类具体实现的,它创建的显示窗口设备即osgViewer::GraphicsWindowWin32 的实例。

OSG 的视景器包括四种线程模型,可以使用setThreadingModel 进行设置,不同的线程模型在仿真循环运行时将表现出不同的渲染效率和线程控制特性。

osgViewer:: Viewer::advance()

至此正式进入仿真循环。

1、获取上一次记录的参考时间(Reference Time);
2、根据当前时刻,重新记录参考时间,并因此得到两次记录之间的差值,即一帧经历的时间;
3、记录已经经过的帧数;
4、有的时候我们需要将帧速率,参考时间等内容予以记录并显示给用户,此时需要通过ViewerBase::getStats 函数获得osg::Stats 对象,用以进行帧状态的保存和显示;
5、如果需要的话,使用Referenced::getDeleteHandler()来处理osg::Referenced 对象被弃用之后的删除工作。

osgViewer::Viewer::eventTraversal()

总结一下OSG 视景器、摄像机与场景的关系:
视景器
视景器包括几个最主要的组件:
漫游器_cameraManipulator,用于实现交互式的场景漫游;
事件处理器组_eventHandlers,负责处理视景器的事件队列_eventQueue,主要是键盘/鼠标等事件的处理;
场景_scene,它包括视景器所对应的场景图形根节点,以及用于提高节点和图像数据处理速度的两个分页数据库;
摄像机_camera_slaves,前者为场景的主摄像机,后者为从摄像机组,不过OSG 并没有规定一定要使用主摄像机来显示场景,它的更重要的作用是为OSG 世界矩阵的计算提供依据。

摄像机是 OSG 视图显示的核心器件,没有摄像机就没有办法将场景图形的实景展现给用户。它包括:
1、视口(Viewport),它指示了摄像机显示窗口的位置和尺寸。
2、图形上下文(GraphicsContext),通常这也就是平台相关的图形显示窗口(即GraphicsWindow,对于Win32 系统而言,它实际上是通过CreateWindowEx 这个熟悉的API来创建的),不过也可能是离屏渲染的设备(例如PixelBufferWin32)。

osg仿真环境与WindowsAPI
首先使用 Viewer::getContexts 函数找到视景器中所有已有的GraphicsWindow 图形窗口,然后执行GraphicsWindowWin32::checkEvents 函数。请求分发消息直接指向了GraphicsWindowWin32 的实例。TranslateMessage和DisoatchMessage的工作当然也很明确:通知Windows 执行窗口的消息回调函数,进而执行用户交互和系统消息的检查函数GraphicsWindowWin32::handleNativeWindowingEvent。而这个函数负责把WM_ 消息转化并传递给osgGA::EventQueue 消息队列。之后,使用EventQueue::takeEvents 函数,把当前GraphicsWindow 图形窗口对象gw 的事件队列保存到指定的变量gw_events 中。

下一步,遍历刚刚所得的所有事件,对于每一个GUIEventAdapter事件对象event:
1、首先处理Y 轴的方向问题,通常的GUI 窗口系统都会将屏幕左上角定义为(0, 0),右下角定义为(Xmax, Ymax),但是OSG 的视口座标系定义为左下角(0, 0),右上角(Xmax, Ymax),此时,有必要对每个event 对象的鼠标座标值做一步转换。
2、对于符合条件的摄像机,设置为_cameraWithFocus。

注意: 场景节点的回调对象必须继承自 osg::NodeCallback,并重写NodeCallback::operator()函数以实现回调的具体内容。由此有所不同的是,Drawable 对象的事件回调必须继承自Drawable::EventCallback,并具现EventCallback::event 函数的内容;其更新回调则必须继承Drawable::UpdateCallback 并具现UpdateCallback::update 函数。

osgViewer::Viewer::updateTraversal()

OSG 更新回调的作用与事件回调有类似之处:由专门的访问器对象_ updateVisitor 的负责场景图形更新遍历;所有的节点和Drawable 几何体对象都可以使用setUpdateCallback 设置更新回调;通过具体实现NodeCallback::operator()或者Drawable::UpdateCallback::update 函数,可以在回调对象中添加自定义的工作。
OSG更新遍历流程:
1、获取函数的起始时刻。
2、使用预设的更新访问器_updateVisitor,访问场景图形的根节点并遍历其子节点,实现各个节点和Drawable 对象的更新回调。
3、使用DatabasePager::updateSceneGraph 函数以及ImagePager::updateSceneGraph 函数,分别更新场景的分页数据库和分页图像库
4、处理用户定义的更新工作队列_updateOperations。
5、执行主摄像机_camera 以及从摄像机组_slaves 的更新回调(但是不会遍历到它们的子节点),注意摄像机回调的执行时机与场景节点还是有所区别的。
6、根据漫游器_cameraManipulator 的位置姿态矩阵,更新主摄像机_camera 的观察矩阵。
7、使用View::updateSlaves 函数更新从摄像机组_slaves 中所有摄像机的投影矩阵,观察矩阵和场景筛选设置(CullSettings)。
8、获取函数的结束时刻,将相关的时刻信息保存到记录器中。

从摄像机组_slaves 的更新

osgViewer::View::updateSlave():从摄像机组_slaves 的更新。从摄像机组与主摄像机的关系:从摄像机组从本质上继承了主摄像机的投影矩阵,观察矩阵和场景筛选设置,但是可以在使用View::addSlave 添加从摄像机时,设置投影矩阵与观察矩阵的偏置值,还可以使用CullSettings::setInheritanceMask 设置CullSettings(场景筛选) 的继承掩码。OSG目前支持多种场景筛选方式。

更新场景的分页数据库和分页图像库

更新场景的分页数据库和分页图像库:在解读DatabasePager之前,先了解OpenThreads 库,其包含了以下几个最主要的线程处理类:
Thread 类:线程实现类。每定义一个Thread 类,就相当于定义了一个共享进程资源、但是可以独立调度的线程。通过重写run()和cancel()这两个成员函数,即可实现线程运行时和取消时的操作;通过调用start()和cancel(),可以启动或中止已经定义的进程对象。
Mutex 类:互斥体接口类。OpenThreads 提供了互斥体操作的机制,它有效地避免了各个线程对同一资源的相互竞争,由lock()函数和unlock()函数实现共享资源加解锁。一个线程类中可以存在多个Mutex 成员,用于在不同的地点或情形下为共享区域加锁;但是一定要在适当的时候解锁,以免造成线程的共享数据无法再访问。
Condition 类:条件量接口类。它依赖于某个Mutex 互斥体,互斥体加锁时阻塞所在的线程,解锁或者超过时限则释放此线程,允许其继续运行
Block 类:阻塞器类。顾名思义,这个类的作用就是阻塞线程的执行,使用block()阻塞执行它的线程(注意,不一定是定义它的Thread 线程,而是当前执行了block 函数的线程,包括系统主进程),并使用release()释放之前被阻塞的线程。
BlockCount 类:计数阻塞器类。它与阻塞器类的使用方法基本相同:block()阻塞线程,release()释放线程;不过除此之外,BlockCount 的构造函数还可以设置一个阻塞计数值。计数的作用是:每当阻塞器对象的completed()函数被执行一次,计数器就减一,直至减到零就释放被阻塞的线程。
Barrier 类:线程栅栏类。这是一个对于线程同步颇为重要的阻塞器接口。每个执行了Barrier::block()函数的线程都将被阻塞;当被阻塞在栅栏处的线程达到指定的数目时,就好比栅栏无法支撑那么大的强度一样,栅栏将被冲开,所有的线程将被释放。重要的是,这些线程是几乎同时释放的,也就保证了线程执行的同步性。
注意 BlockCount 与Barrier 的区别,前者是由其它任意线程执行指定次数的completed()函数,即可释放被阻塞的线程;而后者则是必须阻塞指定个数的线程之后,所有的线程才会同时被释放。

updateSceneGraph 函数的工作是更新分页数据库的内容,它的内容简单到只包含了两个执行函数的内容:
1、DatabasePager::removeExpiredSubgraphs:用于去除已经过期的场景子树;
我们首先遍历 DatabasePager::_pagedLODList 这个成员变量,并执行其中每个PagedLOD对象的removeExpiredChildren 函数,取得其中已经过期的子节点并记录到一个列表里。将这些过期节点标记为“可删除”,并传递给_fileRequestQueue->_childrenToDeleteList成员,也就是的“待删除列表”,同时唤醒DatabaseThread 线程。

下一步,将过期节点从_pagedLODList 中删除,由于它们已经被传递到“待删除列表”当中,因此ref_ptr 引用计数不会减到零,也就不会在主仿真循环中触发内存释放(delete)动作。

最后还要执行 SharedStateManager::prune 函数。这里的osgDB::SharedStateManager 指的是一个渲染状态共享管理器,它负责记录分页数据库中各个节点的渲染属性(StateAttribute),并判断节点之间是否共享了同一个渲染属性,从而节省加载和预编译的时间。prune 函数的工作是从SharedStateManager 中剔除那些没有被共享的渲染属性
如果希望启用 SharedStateManager (默认是关闭的,其性能目前可能没有想象的那么好),需要在进入仿真循环之前执行:
osgDB::Registry::instance()->getOrCreateSharedStateManager();

2、DatabasePager::addLoadedDataToSceneGraph:用于向场景图形中添加新载入的数据。
这里首先取得“待合并列表_dataToMergeList,并遍历其中每一个DatabaseRequest 对象。遍历过程中,首先执行 SharedStateManager::share 函数,将新加载节点_loadedModel 的渲染属性保存到SharedStateManager 管理器中。随后执行 DatabasePager::registerPagedLODs,在加载的节点及其子树中搜索PagedLOD节点,并添加到刚刚提到的_pagedLODList 列表中。最后,判断DatabaseRequest::_groupForAddingLoadedSubgraph 对象(也就是新加载节点在场景中的父节点)是否合法,并将DatabaseRequest::_loadedModel 添加为它的子节点。

以下是分析:
osgDB::DatabasePager 类执行的是这一工作:每一帧的更新遍历执行到updateSceneGraph 函数时,都会自动将“一段时间之内始终不在当前页面上”的场景子树去除,并将“新载入到当前页面”的场景子树加入渲染,这里所说的“页面”往往指的就是用户的视野范围。这些分页和节点管理的工作如果由渲染循环来完成的话,恐怕是费时又费力的,对于场景的显示速度有较大的影响,因此,DatabasePager 中内置了专用于相关工作处理的DatabaseThread 线程。

在讲解 DatabaseThread 线程之前,我们理应先仔细考虑一下,OSG 的分页数据库应该使用单独的线程来处理什么:
1、删除过期的场景数据:这一步工作当然也可以在仿真循环中进行,但是这样做很可能会造成场景渲染的延迟,我们采用线程来处理场景数据的理由也正是因为如此。过期对象的统一删除工作在这里完成,而更新遍历则负责将检索到的对象送入相应的过期对象列表。
2、获取新的数据加载请求:请求加载的可能是新的数据信息,也可能是已有的场景数据(曾经从“当前页面”中去除,更新又回到“当前页面”中);可能是本地的数据文件,也可能来自网络,并需要把下载的数据缓存在本地磁盘上。这些都需要在线程中一一加以判断。
3、编译加载的数据:有些数据如果提前进行编译可以有效地提升效率,例如为几何体数据创建显示列表(Display List),以及将纹理对象提前加载到纹理内存;虽然OSG 同样可以在渲染时根据用户需要执行这些工作,但是那样势必会造成帧的延迟,对于大型场景的加载来说这种延迟将更为严重。因此预编译加载的数据是很有必要的。在数据处理线程执行预编译工作当然不为过,但是如果系统配置足够高级的话,也可以选择由图形设备线程(GraphicsContext::getGraphicsThread)来完成这些原属于它们的工作。
4、将加载的数据合并至场景图形:直接由线程来完成这一工作显然是不合适的,因为我们不知道当DatabaseThread 线程试图操作场景中的节点时,OSG 的渲染器在做些什么。最好的方法是将读入的数据先保存在一个列表中,并且由仿真循环负责获取和执行合并新节点的操作。
osg的分页数据库使用单独的线程处理什么
左侧的图框表示数据的检索和输入,中间的白色图框表示用于数据存储的内存空间,而右边的图框表示存储数据的输出。此外,蓝绿色图框表示可以在DatabaseThread 线程中完成的工作,而橙色图框表示由线程之外的函数完成的工作。

这幅图中事实上已经标示出了 DatabasePager 中的几个重要成员变量。不过在认识它们之前,我们还需要了解一下DatabasePager 类所定义的各种数据结构:
1、DatabasePager::DatabaseThread 类:这是分页数据库的核心处理线程,它负责实现场景元素的定期清理,加载以及合并工作;但是让它一直处于检查各个数据列表的循环状态,这未免太过耗费系统资源。因此,这个线程在平常状态下应当被阻塞,需要时再予以唤醒。
2、DatabasePager::DatabaseRequest 结构体:这个结构体保存了用户的单个数据请求,包括数据文件名,请求时间,数据加载后存入的节点,以及要进行合并的父节点等;除此之外还有一个重要的编译映射表_dataToCompileMap,这个映射表负责保存图形设备ID 与编译对象(几何体显示列表,纹理等)的映射关系。
3、DatabasePager::RequestQueue 结构体:它负责保存和管理一个“数据请求列表”_ requestList,也就是由DatabaseRequest 对象组成的向量组,除此之外还负责对列表中的数据按请求时间排序。上图中所示的_ dataToCompileList_dataToMergeList 实际上都是RequestQueue 类型的对象,不过它们所保存的“请求列表”事实上是已经完成加载的“待编译/待合并列表”了。
4、DatabasePager::ReadQueue 结构体:这个结构体继承自RequestQueue,不过还增加了一个“弃用对象列表”_childrenToDeleteList,也就是osg::Object 对象组成的向量组。它是数据处理线程中最重要的对象之一,除了可以随时向两个列表里追加数据请求和弃用对象之外,这个结构体还包括了一个updateBlock 函数,负责阻塞或者放行DatabaseThread 线程,其根据是:列表中是否存在新的数据请求或弃用对象需要处理,以及用户是否通过函数设置暂时不要启用线程(DatabasePager ::setDatabasePagerThreadPause)。

osgDB:: DatabasePager::DatabaseThread::run ():现在我们可以进入线程循环体内部浏览了。每次循环开始时,数据处理线程都被自动阻塞,避免无谓的系统消耗;直到updateBlock 函数在外部被执行才会放行,继续下面的代码。
updateBlock 函数可能在以下几种情形下被执行:
1、ReadQueue 对象中的“数据请求列表”被修改,例如新的数据加载请求被传入,请求被取出,列表被重置。
2、ReadQueue 对象中的“弃用对象列表” 被修改,例如有新的过期对象被送入,对象被删除,列表被重置。
3、执行了DatabasePager ::setDatabasePagerThreadPause 函数,当线程被重新启动时,会自动检查线程是否应当被唤醒。
这之后是过期数据的删除工作,即取出 _ childrenToDeleteList 中的所有对象,并安全地将它们析构。随后,使用DatabasePager::ReadQueue::takeFirst 函数,从当前线程对应的ReadQueue 对象(_fileRequestQueue 或_httpRequestQueue)的队列中取出并清除第一个数据加载请求(DatabaseRequest)。

什么情形下我们才会用到DatabasePager?使用 osg::PagedLOD 和osg::ProxyNode 节点的时候。
总结DatabasePager的流程:
这里写图片描述
注意:ProxyNode 和PagedLOD 的区别:ProxyNode 的功能主要是在运行时加载一个或多个模型文件作为子节点;而PagedLOD 虽然可以实现相同的功能,但它还有另外一项重要的工作,那就是根据用户的视点范围来实现场景树的“修剪”——剔除对场景长期没有助益的节点,加载用户可见的节点。这也是这几日以来我们一直强调的“分页”的精髓所在了吧。

1、首先,osg::PagedLOD 节点或者osg::ProxyNode 节点使用setFileName 函数,请求运行时加载模型文件为子节点。
2、在场景的筛选(Cull)过程中,OSG 将自动取出PagedLODProxyNode 中保存的文件名数据,并使用DatabasePager::requestNodeFile 函数将其保存到“数据请求列表”中(RequestQueue::_requestList)。
3、DatabasePager 内置了两个数据处理线程(DatabaseThread),分别用于处理本地文件和HTTP 数据,线程的主要工作是删除“已弃用队列”(RequestQueue::_childrenToDeleteList)中的对象,并从“数据请求列表”中获取新的请求。
4、线程中如果取得新的数据请求,则尝试加载新的模型,判断是否需要预编译模型,并送入“等待编译列表”(_dataToCompileList)。预编译的含义是执行显示列表的创建,纹理绑定,GLSL 数据绑定等OpenGL 动作,通常情况下预编译模型可以避免它在显示时出现帧延迟。
5、对于编译完成或者无需编译的数据请求,首先创建其KDTree 包围体(用于K-Dop Tree碰撞检测计算),然后送入“等待合并列表”(_dataToMergeList),线程让出控制权。
6、场景的每次更新遍历均会执行updateSceneGraph 函数,于其中将一段时间内没有进入用户视野的节点送入“已弃用队列”(注意这一工作只限于PagedLOD 节点的子节点),并将“等待合并列表”中的新数据使用addChild 送入当前的场景图形。

ImagePager 与DatabasePager 没什么大的区别,它主要负责的是纹理图片文件的运行时加载工作。 ImageSequence 类的主要功能是使用自身包含的Image图片对象序列,实现一种动画纹理的效果。与 DatabasePager 相同,ImagePager 也内置了一个处理线程,其中随时读取“图片加载请求”的内容,并根据其中的文件名使用osgDB::readImageFile 加载数据(osg::Image 对象)。加载之后的图片数据将被加入到申请它的ImageSequence 对象中。ImagePager 本身暂时不具备“分页”的功能,换句话说,在目前的版本中,它不会负责将长时间不用的图片删除。

发布了82 篇原创文章 · 获赞 58 · 访问量 18万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章