logo资料库

《最长的一帧》王锐(array) 著 OSG.pdf

第1页 / 共73页
第2页 / 共73页
第3页 / 共73页
第4页 / 共73页
第5页 / 共73页
第6页 / 共73页
第7页 / 共73页
第8页 / 共73页
资料共73页,剩余部分请下载后查看
最长的一帧 王锐(array) 这是一篇有关 OpenSceneGraph 源代码的拙劣教程,没有任何能赏心悦目的小例子,也 不会贡献出什么企业级的绝密的商业代码,标题也只是个噱头(坏了,没人看了^_^)。 本文写作的目的说来很简单,无非就是想要深入地了解一下,OSG 在一帧时间,也就 是仿真循环的一个画面当中都做了什么。 对 OSG 有所了解之后,我们也许可以很快地回答这个问题,正如下面的代码所示: while (!viewer.done()) viewer.frame(); 就这样,用一个循环结构来反复地执行 frame()函数,直到 done()函数的返回值为 true 为止。每一次执行 frame()函数就相当于完成了 OSG 场景渲染的一帧,配置较好的计算机可 以达到每秒钟一二百帧的速率,而通常仿真程序顺利运行的最低帧速在 15~25 帧/秒即可。 很好,看来笔者的机器运行 frame()函数通常只需要 8~10ms 左右,比一眨眼的工夫都要 短。那么本文就到此结束吗? 答案当然是否定的,恰恰相反,这篇繁琐且可能错误百出的文字,其目的正是要深入 frame()函数,再深入函数中调用的函数……一直挖掘下去,直到我们期待的瑰宝出现;当然 也可能是一无所获,只是乐在其中。 这样的探索要到什么时候结束呢?从这短短的 10 毫秒中引申出来的,无比冗长的一帧, 又是多么丰富抑或无聊的内容呢?现在笔者也不知道,也许直到最后也不会明了,不过相信 深入源代码的过程就是一种享受,希望读者您也可以同我一起享受这份辛苦与快乐。 源代码版本:OpenSceneGraph 2.6.0;操作系统环境假设为 Win32 平台。为了保证教程 的篇幅不致被过多程序代码所占据,文中会适当地改写和缩编所列出的代码,仅保证其执行 效果不变,因此可能与实际源文件的内容有所区别。 由于作者水平和精力所限,本文暂时仅对单视景器(即使用 osgViewer::Viewer 类)的 情形作出介绍。 转载请注明作者和 www.osgchina.org 本文在写作过程中将会用到一些专有名词,它们可能与读者阅读的其它文章中所述有所 差异,现列举如下: 场景图形-SceneGraph;场景子树-Subgraph;节点-Node;摄像机-Camera;渲染器-Renderer; 窗口-Window;视口-Viewport;场景-Scene;视图-View;视景器-Viewer;漫游器-Manipulator; 访问器-Visitor;回调-Callback;事件-Event;更新-Update;筛选-Cull;绘制-Draw。 第一日 好了,在开始第一天的行程之前,请先打开您最惯用的编程工具吧:VisualStudio? CodeBlocks?UltraEdit?SourceInsight?Emacs?Vim?或者只是附件里那个制作低劣的记事 本 … … 总 之 请 打 开 它 们 , 打 开 OpenSceneGraph-2.6.0 的 源 代 码 文 件 夹 , 打 开
osgViewer/ViewerBase.cpp 这个文件……同样的话就不再重复了。但是如果您没有这样做, 而仅仅是一边聊着 QQ,一边在电话里和女朋友发着牢骚,一边还要应付突然冲进来的老板, 一边打开这篇教程的话——对不起,我想您会在 1 分钟以内就对其感到厌烦,因为它就像天 书一样,或者就像一坨乱七八糟的毛线团,只有家里那只打着哈欠的小猫可能会过来捅上一 下。 不过不用担心,如果您已经耐着性子读到了这里,并且已经看到了 ViewerBase.cpp 中总 共 752 行规规矩矩的代码,那么我会尽全力协助您继续走完漫长的一帧的旅程,并在其中把 自己所知所得(不仅仅局限于那个该死的 frame 函数,而是遍及 OSG 的方方面面)全数灌 输给您。当然也希望您尽全力给我以打压、批评和指正,我只是一个普普通通的梦想着先找 个女朋友成家立业的毛头小子而已,如果我的话全都是对的,那么那些真正的图形学权威们 一定都住在寒风刺骨的珠穆朗玛峰顶上。 好了,废话不再多说。我们这就开始。 当前位置:osgViewer/ViewerBase.cpp 第 571 行,osgViewer::ViewerBase::frame() frame 函数的内容本身几乎一眼就可以看完。不过要注意的是,这个函数是 ViewerBase 类的成员函数,而非 Viewer 类。因此,无论对于单视景器的 Viewer 类,还是多视景器的 CompositeViewer 类,frame 函数的内容都是相同的(因为它们都没有再重写这个函数的内容)。 该函数所执行的主要工作如下: 1、如果这是仿真系统启动后的第一帧,则执行 viewerInit();此时如果还没有执行 realize() 函数,则执行它。 2、执行 advance 函数。 3、执行 eventTraversal 函数,顾名思义,这个函数将负责处理系统产生的各种事件,诸 如鼠标的移动,点击,键盘的响应,窗口的关闭等等,以及摄像机与场景图形的事件回调 (EventCallback)。 4、执行 updateTraversal 函数,这个函数负责遍历所有的更新回调(UpdateCallback); 除此之外,它的另一个重要任务就是负责更新 DatabasePager 与 ImagePager 这两个重要的分 页数据处理组件。 5、执行 renderingTraversals 函数,这里将使用较为复杂的线程处理方法,完成场景的筛 选(cull)和绘制(draw)工作。 下面我们就按照 1~5 的顺序,开始我们的源代码解读之旅。 当前位置:osgViewer/View.cpp 第 227 行,osgViewer::View::init() Viewer::viewerInit 函数只做了一件事,就是调用 View::init()函数,而这个 init 函数的工 作似乎也是一目了然的:无非就是完成视景器的初始化工作而已。 不过在我们离开这个函数,继续我们的旅程之前,还是仔细探究一下,这个初始化工作 到底包含了什么? 阅读某个函数的源代码过程中,如果能够大致知道这个函数的主要工作,并了解其中用 到的变量的功能,那么即使只有很少的注释内容,应该也可以顺利地读完所有代码。如果对 一些命名晦涩的变量不甚理解,或者根本不知道这个函数于运行流程中有何用途,那么理解 源代码的过程就会麻烦很多。 View::init 函数中出现了两个重要的类成员变量:_eventQueue 和_cameraManipulator, 并且还将一个 osgGA::GUIEventAdapter 的实例传入后者的初始化函数。 代码如下: osg::ref_ptr initEvent = _eventQueue->createEvent();
initEvent->setEventType(osgGA::GUIEventAdapter::FRAME); if (_cameraManipulator.valid()) _cameraManipulator->init(*initEvent, *this); 从变量的名称可以猜测出_eventQueue 的功能,它用于储存该视景器的事件队列。OSG 中代表事件的类是 osgGA::GUIEventAdapter,它可以用于表达各种类型的鼠标、键盘、触压 笔和窗口事件。在用户程序中,我们往往通过继承 osgGA::GUIEventHandler 类,并重写 handle 函 数 的 方 法 , 获 取 实 时 的 鼠 标 / 键 盘 输 入 , 并 进 而 实 现 相 应 的 用 户 代 码 ( 参 见 osgkeyboardmouse)。 _eventQueue 除了保存一个 GUIEventAdapter 的链表之外,还提供了一系列对链表及其 元素的操作函数,这其中,createEvent 函数的作用是分配和返回一个新的 GUIEventAdapter 事件的指针。 随后,这个新事件的类型被指定为 FRAME 事件,即每帧都会触发的一个事件。 那么,_cameraManipulator 呢?没错,它就是视景器中所用的场景漫游器的实例。通常 我 们 都 会 使 用 setCameraManipulator 来 设 置 这 个 变 量 的 内 容 , 例 如 轨 迹 球 漫 游 器 (TrackballManipulator)可以使用鼠标拖动来观察场景,而驾驶漫游器(DriveManipulator) 则使用类似于汽车驾驶的效果来实现场景的漫游。 上面的代码将新创建的 FRAME 事件和 Viewer 对象本身传递给_cameraManipulator 的 init 函数,不同的漫游器(如 TrackballManipulator、DriveManipulator)会重写各自的 init 函 数,实现自己所需的初始化工作。如果读者希望自己编写一个场景的漫游器,那么覆写并使 用 osgGA::MatrixManipulator::init 就可以灵活地初始化自定义漫游器的功能了,它的调用时 机就在这里。 那么,回到 viewerInit 函数……很好,这次似乎没有更多的内容了。没想到一个短短的 函数竟然包含了那么多的信息,看来草率地阅读还真是使不得。 解读成果: osgGA::EventQueue::createEvent,osgGA::MatrixManipulator::init,osgViewer::View::init, osgViewer::Viewer::viewerInit。 悬疑列表: 无。 第二日 当前位置:osgViewer/Viewer.cpp 第 385 行,osgViewer::Viewer::realize() Viewer::realize 函数是我们十分熟悉的另一个函数,从 OSG 问世以来,我们就习惯于在 进入仿真循环之前调用它(现在的 OSG 会自动调用这个函数,如果我们忘记的话),以完成 窗口和场景的“设置”工作。那么,什么叫做“设置”,这句简单的场景设置又包含了多少 内容呢?艰辛的旅程就此开始吧。 首先是一行:setCameraWithFocus(0),其内容无非是设置类变量_cameraWithFocus 指向 的内容为 NULL。至于这个“带有焦点的摄像机”是什么意思,我们似乎明白,似乎又不明 白,就先放入一个“悬疑列表”(Todo List)中好了。 下面遇到的函数就比较重要了,因为我们将会在很多地方遇到它: Contexts contexts; getContexts(contexts); 变量 contexts 是一个保存了 osg::GraphicsContext 指针的向量组,而 Viewer::getContexts
函数的作用是获取所有的图形上下文,并保存到这个向量组中来。 对于需要将 OSG 嵌合到各式各样的 GUI 系统(如 MFC,Qt,wxWidgets 等)的朋友来 说,osg::GraphicsContext 类是经常要打交道的对象之一。一种常用的嵌入方式也许是这样实 现的: osg::ref_ptr traits = new osg::GraphicsContext::Traits; osg::ref_ptr windata = new osgViewer::GraphicsWindowWin32::WindowData(hWnd); traits->x = 0; traits->y = 0; …… traits->inheritedWindowData = windata; osg::GraphicsContext* gc = osg::GraphicsContext::createGraphicsContext(traits.get()); Camera* camera = viewer.getCamera(); camera->setGraphicsContext(gc); …… viewer.setCamera(camera); 这个过程虽然比较繁杂,但是顺序还是十分清楚的:首先设置嵌入窗口的特性(Traits), 例如 X、Y 位置,宽度和高度,以及父窗口的句柄(inheritedWindowData);然后根据特性 的设置创建一个新的图形设备上下文(GraphicsContext),将其赋予场景所用的摄像机。而 我们在 getContexts 函数中所要获取的,也许就包括这样一个用户建立的 GraphicsContext 设 备。 当前位置:osgViewer/Viewer.cpp 第 1061 行,osgViewer::Viewer::getContexts() 在 这 个 函 数 之 中 , 首 先 判 断 场 景 的 主 摄 像 机 _camera 是 否 包 含 了 一 个 有 效 的 GraphicsContext 设备,然后再遍历所有的从摄像机_slaves(一个视景器可以包含一个主摄像 级和多个从摄像机),将所有找到的 GraphicsContext 图形上下文设备记录下来。 随后,将这些 GraphicsContext 的指针追加到传入参数(contexts 向量组)中,并使用 std::sort 执行了一步排序的工作,所谓的排序是按照这样的原则来进行的: 1、屏幕数量较少的 GraphicsContext 设备排列靠前; 2、窗口 X 坐标较小的设备排列靠前; 3、窗口 Y 坐标较小的设备排列靠前。 如果希望观察自己的程序中所有的图形设备上下文,不妨使用这个函数来收集一下。简 单的情形下,我们的程序中只有一个主摄像机,也就只有一个 GraphicsContext 设备,它表 达了一个全屏幕的图形窗口;而 osgcamera 这个例子程序可以创建六个从摄像机,因此可以 得到六个图形上下文设备,且各个图形窗口的 X 坐标各不相同,这也正是这个例子想要表 达的。 可是,主摄像机的 GraphicsContext 呢?为什么 osgcamera 中不是七个 GraphicsContext 设备呢?答案很简单,主摄像机没有创建图形上下文,因此也就得不到设备的指针。为了理 解这个现象的原因,我们不妨先回到 realize 函数中。 当前位置:osgViewer/Viewer.cpp 第 394 行,osgViewer::Viewer::realize() 有一个显而易见的事实是:当程序还没有进入仿真循环,且对于 osgViewer::Viewer 还 没有任何的操作之时,系统是不会存在任何图形上下文的;创建一个新的 osg::Camera 对象 也不会为其自动分配图形上下文。但是,图形上下文 GraphicsContext 却是场景显示的唯一
平台,系统有必要在开始渲染之前完成其创建工作。 假设用户已经在进入仿真循环之前,自行创建了新的 Camera 摄像机对象,为其分配了 自定义的 GraphicsContext 设备,并将 Camera 对象传递给视景器,就像 osgviewerMFC 和 osgcamera 例子,以及我们在编写与 GUI 系统嵌合的仿真程序时常做的那样。此时,系统已 经不必为图形上下文的创建作任何多余的工作,因为用户不需要更多的窗口来显示自己的场 景了。所以就算主摄像机_camera 还没有分配 GraphicsContext,只要系统中已经存在图形上 下文,即可以开始执行仿真程序了。 但是,如果 getContexts 没有得到任何图形上下文的话,就说明仿真系统还没有合适的 显示平台,此时就需要尝试创建一个缺省的 GraphicsContext 设备,并再次执行 getContexts, 如果还是没能得到任何图形上下文的话,那么就不得不退出程序了。 创建缺省 GraphicsContext 设备的方法有以下几种: 1、读取 OSG_CONFIG_FILE 环境变量的内容:如果用户在这个环境变量中定义了一个 文件路径的话,那么系统会尝试用 osgDB::readObjectFile 函数读入这个文件,使用 cfg 插件 进行解析;如果成功的话,则调用 osgViewer::Viewer::take 函数,使用配置信息设置当前的 视景器。这些工作在 osgViewer::Viewer::readConfiguration 函数中实现。 2、读取 OSG_WINDOW 环境变量的内容:如果用户以“x y w h”的格式在其中定义了 窗口的左上角坐标(x,y)和尺寸(w,h)的话(注意要以空格为分隔符),系统会尝试使 用 osgViewer::View::setUpViewInWindow 函数来创建设备。 3、读取 OSG_SCREEN 环境变量的内容:如果用户在其中定义了所用屏幕的数量的话, 系统会尝试用 osgViewer::View::setUpViewOnSingleScreen 函数,为每一个显示屏创建一个全 屏幕的图形窗口;如果同时还设置了 OSG_WINDOW,那么这两个环境变量都可以起到作 用,此时将调用 setUpViewInWindow 函数。 4、如果上述环境变量都没有设置的话(事实上这也是最常见的情况),那么系统将调用 osgViewer::View::setUpViewAcrossAllScreens 函数,尝试创建一个全屏显示的图形设备。 那么,下文就从这几种图形设备建立的方法开始。至于后面的路,果然遥遥无期呢。 解读成果: osgViewer::Viewer::getContexts,osgViewer::Viewer::readConfiguration。 悬疑列表: 类变量_cameraWithFocus 的意义是什么? 第三日 当前位置:osgViewer/View.cpp 第 575 行,osgViewer::View::setUpViewInWindow() 这个函数有五个传入参数:窗口左上角坐标 x,y,宽度 width,高度 height,以及屏幕 数 screenNum。它的作用顾名思义是根据给定的窗口参数来创建一个图形设备。 首先函数将尝试获取 osg::DisplaySettings 的指针,这个类在 OSG 的窗口显示中扮演了 重要的地位:它保存了 OSG 目前用到的,与图形显示,尤其是立体显示有关的所有信息, 主要包括: _displayType:显示器类型,默认为 MONITOR(监视器),此外还支持 POWERWALL (威力墙),REALITY_CENTER(虚拟实境中心)和 HEAD_MOUNTED_DISPLAY(头盔 显示器)。 _stereoMode : 立 体 显 示 模 式 , 默 认 为 ANAGLYPHIC ( 互 补 色 ), 此 外 还 支 持
QUAD_BUFFER(四方体缓冲),HORIZONTAL_SPLIT(水平分割),VERTICAL_SPLIT(垂 直分割),LEFT_EYE(左眼用),RIGHT_EYE(右眼用),HORIZONTAL_INTERLACE(水 平交错),VERTICAL_INTERLACE(垂直交错),CHECKERBOARD(棋盘式交错,用于 DLP 显示器)。 _eyeSeparation:双眼的物理距离,默认为 0.05。 _screenWidth,_screenHeight:屏幕的实际宽度和高度,分别默认设置为 0.325 和 0.26, 目前它们影响的仅仅是视图采用透视投影时的宽高比。 _screenDistance:人眼到屏幕的距离,默认为 0.5。 _splitStereoHorizontalEyeMapping:默认为 LEFT_EYE_LEFT_VIEWPORT(左眼渲染左 视口),也可设为 LEFT_EYE_RIGHT_VIEWPORT(左眼渲染右视口)。 _splitStereoHorizontalSeparation:左视口和右视口之间的距离(像素数),默认为 0。 _splitStereoVerticalEyeMapping:默认为 LEFT_EYE_TOP_VIEWPORT(左眼渲染顶视 口),也可设为 LEFT_EYE_BOTTOM_VIEWPORT(左眼渲染底视口)。 _splitStereoVerticalSeparation:顶视口和底视口之间的距离(像素数),默认为 0。 _splitStereoAutoAdjustAspectRatio:默认为 true,用于屏幕分割之后对其宽高比进行补 偿。 _maxNumOfGraphicsContexts:用户程序中最多可用的 GraphicsContext(图形设备上下 文)数目,默认为 32 个。 _numMultiSamples:多重采样的子像素样本数,默认为 0。如果显示卡支持的话,打开 多重采样可以大幅改善反走样(anti-aliasing)的效果。 此外还有很多可以设置的类变量,如_minimumNumberStencilBits(模板缓存的最小位 数)等,其默认设置均在 osg::DisplaySettings::setDefaults 函数中完成,其中有些变量可能还 没有作用。要注意的是,DisplaySettings 的作用仅仅是保存所有可能在系统显示中用到的数 据,这个类本身并不会据此改变任何系统设置和渲染方式。 值得称道的是,DisplaySettings 可以很方便地从系统环境变量或者命令行参数中获取用 户对显示设备的设置,详细的调用方法可以参阅 DisplaySettings::readEnvironmentalVariables 和 DisplaySettings::readCommandLine 两个函数的内容,十分通俗易懂。 如果希望在用户程序中更改 DisplaySettings 中的显示设置,请务必在执行视景器的 realize 函数之前,当然也就是仿真循环开始之前。这一点也是要切记的。 不知不觉中,似乎完全跑题了,那么我们还是先设法回到主题上来…… 当前位置:osgViewer/View.cpp 第 579 行,osgViewer::View::setUpViewInWindow() 代码解读的工作完全没有进展,看来需要加快进度了。获取系统显示设备的设置参数之 后,下面我们要开始创建新的 GraphicsContext 设备了,回忆“第二日”的内容中所介绍的 OSG 与 GUI 窗口嵌合的流程,第一步是新建一个显示设备特性实例: osg::ref_ptr traits = new osg::GraphicsContext::Traits; 设置图形窗口的特性值。注意这里用到了一个函数 ScreenIdentifier::readDISPLAY,它 的工作仅仅是尝试检查系统环境变量 DISPLAY,并调用 ScreenIdentifier::setScreenIdentifier 函数,将其中的内容按照“hostName:0.0”的格式解析为系统的主机名称(hostName),显 示数(在 Win32 下必须为 0)和屏幕数(0 或者其它数字)。 根据立体显示模式的不同,窗口特性中的模板位数等参量也会有所区分。 下一步,创建新的 GraphicsContext:并将其设置给视景器的主摄像机: osg::ref_ptr gc = osg::GraphicsContext::createGraphicsContext(traits.get());
_camera->setGraphicsContext(gc.get()); 千 万 不 要 简 单 地 使 用 new 来 创 建 新 的 GraphicsContext 指 针 , 因 为 相 比 起 来 , createGraphicsContext 还完成了这样一些工作: 1、获取窗口系统 API 接口,即 GraphicsContext::WindowingSystemInterface 的实例; 2、执行 setUndefinedScreenDetailsToDefaultScreen 函数,如果用户没有设置屏幕数,则 自动设置为缺省值 0; 3、返回 WindowingSystemInterface::createGraphicsContext 的值。 看似一切顺利,但是稍一深究就会发现,这里面存在了一个重要但是不好理解的问题: WindowingSystemInterface::createGraphicsContext 可是一个纯虚函数,它怎么可能返回新建 立的图形设备上下文呢?事实上,这个看似简单的 WindowingSystemInterface 结构体也是另 有玄机的,注意这个函数: void GraphicsContext::setWindowingSystemInterface (WindowingSystemInterface* callback); 它的作用是指定操作平台所使用的视窗 API 接口,也就是在特定的系统平台上创建图 形窗口的时候,将会使用到哪些本地 API 函数。当然,Windows 系统要使用 Win32 API,而 Linux 系统要使用 X11 API,Apple 系统则使用 Carbon。 一切有关视窗 API 接口的工作都是由 GraphicsWindowWin32,GraphicsWindowX11 和 GraphicsWindowCarbon 这三个类及其协作类来完成;而指定使用哪一个窗口系统 API 接口 的 关 键 , 就 在 于 源 文 件 osgViewer/GraphicsWindowWin32.cpp 中 定 义 的 结 构 体 RegisterWindowingSystemInterfaceProxy 了 , 仔 细 研 读 一 下 这 个 结 构 体 和 刚 才 所 述 的 setWindowingSystemInterface 函 数 的 关 系 , 还 有 注 意 那 个 紧 跟 着 结 构 体 的 全 局 变 量 (GraphicsWindowWin32.cpp,2367 行),相信您一定会大呼巧妙的。 什么,GraphicsWindowX11.cpp 中也有这个结构体?那么请仔细检查一下 CMake 自动 生成的 osgViewer.vcproj 工程,看看有没有包含这个多余的文件(对于 Windows 系统来说) ——这也许就是使用 CMake 来实现跨平台编译的好处之一了。 至于 WindowingSystemInterface::createGraphicsContext 函数是如何使用 Win32 API 来实 现图形设备的创建的,鉴于本文并不想追赶《资本论》的宏伟规模,就不再深究了,读者不 妨自行刨根问底。 回来吧,回来吧。还是让我们回到 setUpViewInWindow 函数中来。 这个函数剩下的内容并不是很多,也不难理解,主要的工作有: 1、调用 osgGA::GUIEventAdapter::setWindowRectangle 记录新建立的窗口设备的大小, 因而这个设备上产生的键盘和鼠标事件可以以此为依据。 2、设置主摄像机_camera 的透视投影参数,并设置新的 Viewport 视口。 3、执行 osg::Camera::setDrawBuffer 和执行 osg::Camera:: setReadBuffer 函数,这实质上 相当于在渲染的过程中执行 glDrawBuffer 和 glReadBuffer,从而自动设置此摄像机想要绘制 和读取的缓存。 就这样。不过这回真是一次又一次地离题万里……希望我们还是从中得到了一些收获和 启迪的,对吗? 解读成果: osg::DisplaySettings::setDefaults,osg::GraphicsContext::createGraphicsContext, osgViewer::View:: setUpViewInWindow。 悬疑列表: 类变量_cameraWithFocus 的意义是什么?
第四日 当前位置:osgViewer/Viewer.cpp 第 426 行,osgViewer::Viewer::realize() setUpViewOnSingleScreen 和 setUpViewAcrossAllScreens 函数的实现流程与上一日介绍 的 setUpViewInWindow 区别不是很大。值得注意的是,setUpViewAcrossAllScreens 函数中调 用 GraphicsContext::getWindowingSystemInterface 函数取得了与平台相关的视窗 API 接口类 (其中的原理请参看上一日的文字),并进而使用 WindowingSystemInterface::getNumScreens 函数取得了当前系统的显示屏幕数。 事实上,如果我们需要在自己的程序中获取屏幕分辨率,或者设置屏幕刷新率的话,也 可以使用同样的方法,调用 getScreenResolution,setScreenResolution 和 setScreenRefreshRate 等相关函数即可。具体的实现方法可以参见 GraphicsWindowWin32.cpp 的源代码。 setUpViewAcrossAllScreens 函数可以自行判断屏幕的数量,并且使用多个从摄像机来对 应多个屏幕的显示(或者使用主摄像机_camera 来对应单一屏幕)。此外它还针对水平分割 显示(HORIZONTAL_SPLIT)的情况,对摄像机的左/右眼设置自动做了处理,有兴趣的读 者不妨仔细研究一下。 最后,本函数还执行了一个重要的工作,即 View::assignSceneDataToCameras,这其中 包括以下几项工作: 1、对于场景漫游器_cameraManipulator,执行其 setNode 函数和 home 函数,也就是设 置漫游器对应于场景图形根节点,并回到其原点位置。不过在我们使用 setCameraManipulator 函数时也会自动执行同样的操作。 2、将场景图形赋予主摄像机_camera,同时设置它对应的渲染器(Renderer)的相关函 数。这里的渲染器起到了什么作用?还是先放到悬疑列表中吧,不过依照我们的解读速度, 这个问题可能会悬疑很久。 3、同样将场景图形赋予所有的从摄像机_slaves,并设置每个从摄像机的渲染器。 终于可以回到 realize 函数的正轨了,还记得下一步要做什么吧?对,在尝试设置了缺 省的 GraphicsContext 设备之后,我们需要再次使用 getContexts 来获取设备,如果还是不成 功的话,则 OSG 不得不退出运行了(连图形窗口都建立不起来,还玩什么)。 当前位置:osgViewer/Viewer.cpp 第 446 行,osgViewer::Viewer::realize() 现在我们遍历所得的所有 GraphicsContext 设备(通常情况下,其实只有一个而已)。对 于每个 GraphicsContext 指针 gc,依次执行: gc->realize(); if (_realizeOperation.valid() && gc->valid()) { gc->makeCurrent(); (*_realizeOperation)(gc); gc->releaseContext(); } 一头雾水,但是决不能轻言放弃。仔细研究一下吧,首先是 GraphicsContext::realize 函 数,实际上也就是 GraphicsContext::realizeImplementation 函数。 realizeImplementation 是纯虚函数吗?没错,回想一下第三日的内容,当我们尝试使用 createGraphicsContext 来创建一个图形设备上下文时,系统返回的实际上是这个函数的值:
分享到:
收藏