您的位置:首页 > 运维架构

openSceneGraph源码分析——图形绘制过程

2017-06-13 09:44 821 查看
最长的一帧23:下面我们将开始场景绘制源代码的阅读,相关的函数是SceneView::draw。请记住我们现在还假设处于单线程(SingleThreaded)的运行模式下,并着力于解释渲染后台的筛选和绘制流程。后面我们将重点介绍OSG的多线程运行机制,并将眼光放在多个渲染线程的同步性实现上,不过现在还需要忍耐一段时间。

当前位置:osgUtil/SceneView.cpp第983行,osgUtil:: SceneView::draw ()
场景的绘制工作依然由OSG内部的场景视图类(SceneView)负责引领,而真正的绘制工作则通过渲染树的遍历,分散到各个Drawable类当中执行。

SceneView::draw函数的第一个工作是初始化osg::State类的GL库函数。State类在之前已经提到过,它保存了所有的OpenGL状态和属性参数;除此之外,State类还负责从当前系统平台的OpenGL链接库中获取函数的地址,这也是我们第一次执行场景绘制之前的必备工作,所用函数为State::initializeExtensionProcs。

之后,如果用户设置了场景视图初始化访问器(SceneView::setInitVisitor),那么draw函数第一次执行时将使用这个访问器遍历场景树。相关代码位于SceneView::init函数中。

然后将所有已经标记为要删除的节点或者Drawable对象统一从场景和内存中删除,执行flushDeletedGLObjects函数(参见第十八日)。每一帧绘制之前都会执行这一删除操作;而多线程渲染的过程中,每个图形线程也都可能执行这一删除操作。这样可能会出现多个线程同时申请使用flushDeletedGLObjects删除对象的情况,为此,SceneView类提供了一个成员变量_requiresFlush,用以避免多个图形线程同时执行对象的清理工作。

下一步就是场景绘制的核心工作了,OSG为立体显示提供的支持也在这里体现出来(1010-1475行)。针对不同的立体显示设置(DisplaySettings::getStereoMode),此处均提供了详尽的处理流程(譬如ANAGLYPHIC互补色显示,OSG将负责使用红色掩码渲染左眼视图,使用补色青色掩码渲染右眼视图),感兴趣的朋友不妨在这里细细品味,甚至把您所设计的立体显示方案于其中实现。

现在我们仅针对非立体显示的情形进行介绍(1477-1510行):

1、首先是设置渲染台(RenderStage)的读/写缓存(通常包括GL_NONE,GL_FRONT_LEFT,GL_FRONT_RIGHT,GL_BACK_LEFT,GL_BACK_RIGHT,GL_FRONT,GL_BACK,GL_LEFT,GL_RIGHT,GL_FRONT_AND_BACK以及GL_AUX辅助缓存),其中的值是根据摄像机的setDrawBuffer和setReadBuffer函数来设定的。

2、确保颜色掩码的每个颜色通道都是被激活的(使用osg::ColorMask)。

3、执行“前序渲染”渲染台的绘制(RenderStage::drawPreRenderStages)。

4、执行当前渲染台(即渲染树的根节点)的绘制(RenderStage::draw),无疑这是场景绘制的核心部分。

在结束了渲染树的绘制之后,SceneView::draw函数还负责恢复所有的OpenGL状态(使用State::popAllStateSets函数),判断是否在绘制过程中出现了OpenGL绘图命令错误,并将错误信息打印出来。

下面我们转入RenderStage::draw的战场。

当前位置:osgUtil/RenderStage.cpp第993行,osgUtil::RenderStage::draw ()
首先,简要地分析一下RenderStage::draw函数的执行流程:

1、执行摄像机的初始化回调(Camera::setInitialDrawCallback)。

2、运行摄像机设置(RenderStage::runCameraSetUp),详细的步骤我们将在下一日中讲解。

3、为了保证各个图形处理线程之间不会产生冲突,这里对当前调用的图形设备指针(GraphicsContext)做了一个检查。如果发现正在运行的图形设备与渲染台所记录的当前设备(RenderStage::_graphicsContext)不同的话,则转换到当前设备,避免指定渲染上下文时(GraphicsContext::makeCurrent)出错。

4、执行摄像机的绘制前回调(Camera::setPreDrawCallback)。

5、下一步就是实际的场景绘制工作了。对于多线程模型来说,这里将向图形设备线程(GraphicsContext::getGraphicsThread)添加一个新的Operation对象DrawInnerOperation,专用于绘制工作;以及一个阻塞器BlockAndFlushOperation(同为Operation对象),它强制在绘制结束之后方能继续执行线程的其它Operation对象。有关图形线程与Operation对象的关系,虽然已经日渐明朗,不过目前它还是“悬疑列表”的一部分。

6、对于单线程模型,这里将直接执行RenderStage::drawInner函数。后面会着重对这个函数进行介绍。

7、如果设定了摄像机的RTT(纹理烘焙)方式,则执行RenderStage::copyTexture函数,将场景拷贝到用户指定的纹理对象中。注意这与摄像机的“渲染目标实现方式”有关。还记得吗?第八日中我们曾经提及过Camera::getRenderTargetImplementation并将其遗忘在“悬疑列表”中很久了,那么,也许很快就可以让它重现天日。

8、执行摄像机的绘制后回调(Camera::setPostDrawCallback)。

9、对于单线程模型来说,这个时候应当使用glFlush刷新所有OpenGL管道中的命令,并释放当前渲染上下文(GraphicsContext::releaseContext)。

10、执行“后序渲染”渲染台的绘制(RenderStage::drawPostRenderStages)。

11、执行摄像机的绘制结束回调(Camera::setFinalDrawCallback)。可见场景绘制时总共会执行五种不同时机下调用的摄像机回调(尤其注意回调时机与渲染上下文的关系),根据我们的实际需要,可以选择在某个回调中执行OpenGL函数(初始化与结束回调时不能执行)或者自定义代码,完成所需的操作。

最长的一帧24
当前位置:osgUtil/RenderStage.cpp第1014行,osgUtil::RenderStage::draw
()
首先我们要解决的问题是RenderStage::runCameraSetUp和RenderStage::copyTexture这两个函数的工作内容。事实上也就是对“悬疑列表”中“什么是渲染目标实现方式”这个搁置了十数日的话题作一番讨论。

我们第一次接触到Camera::getRenderTargetImplementation函数是在“第八日”中,讲解Viewer::eventTraversal函数时(Viewer.cpp,623行),不过事件遍历的代码中并没有体现出“渲染目标”或者“Render Target”的任何作用来。事实上,这个函数帮助我们实现了一个场景渲染过程中可能非常重要的功能,即纹理烘焙(Render
To Texture,RTT),或者称之为“渲染到纹理”。RTT技术意味着我们可以将后台实时绘制得到的场景图像直接作为另一个场景中对象的纹理,从而实现更加丰富的场景表达效果。

在OpenGL的较早版本中,RTT技术的实现主要是通过从帧缓存(Frame Buffer)中取得数据并传递给纹理对象来实现的;而随着硬件水平的发展,现在我们有了帧缓存对象(Frame Buffer Object),像素缓存(Pixel Buffer)等多种绘制平台的选择。

RTT实现的基本步骤为:(1)首先创建一个“渲染纹理”(Render Texture),例如FBO对象,像素缓存对象等;(2)设置它为图形设备的渲染目标(Render Target);(3)将“渲染纹理”绑定到一个纹理或图片对象上;(4)此时图形设备的渲染将在后台进行,其结果将直接体现在所绑定的纹理对象上。

“渲染目标”的设定即通过Camera::getRenderTargetImplementation函数实现。其可用的传入参数中,FRAME_BUFFER表示帧缓存,可以适用于较广泛的硬件平台上;而FRAME_BUFFER_OBJECT表示FBO对象,它可以用来实现离屏渲染(Offscreen
Rendering)的工作,其渲染结果不会体现在图形窗口中。

设置渲染目标和绑定纹理的方法十分简单,例如:

osg::Texture2D* texture = new osg::Texture2D;

camera->setRenderTargetImplementation( osg::Camera::FRAME_BUFFER );

camera->attach( osg::Camera::COLOR_BUFFER, texture );

复制代码
即可将纹理对象texture与场景绘制的帧缓存绑定在一起。我们既可以将texture的内容保存成图片,作为场景的截图;也可以将纹理绑定到某个物体上,实现纹理烘焙的效果。这里osgprerender是一个很好的例子,使用附加参数--fb,--fbo,--pbuffer,--window等可以充分了解不同渲染目标实现的过程及其差异。

绑定到摄像机的实际纹理或者图片,在Camera类中均使用Camera::Attachment结构体来保存。而RenderStage::runCameraSetUp则反复遍历名为Camera::BufferAttachmentMap的映射表,检索并设置那些与颜色缓存(COLOR_BUFFER),深度缓存(DEPTH_BUFFER)等相对应的Attachment对象;RenderStage::copyTexture则负责针对FRAME_BUFFER渲染目标,拷贝场景图像到Attachment对象中。

下面我们就深入RenderStage::drawInner函数,它正是整个场景绘制的重点部分。

当前位置:osgUtil/RenderStage.cpp第780行,osgUtil::RenderStage::drawInner ()
此函数的工作首先是FBO对象的初始化,这里将使用FBOExtensions::isSupported和FrameBufferObject::hasMultipleRenderingTargets函数来判断显示卡是否支持FBO以及MRT(多重渲染目标)扩展,并使用FrameBufferObject::apply来调用实际的FBO执行函数。OpenGL为多重渲染目标的支持提供了多达十六个颜色缓存,在OSG中它们均表示为Camera::COLOR_BUFFERi,最后一个i取值为0到15。

如果没有启用FBO支持或者没有使用MRT的话,此时作为渲染树根节点的渲染台(RenderStage)还将负责使用glDrawBuffer和glReadBuffer分别设置场景绘制缓存和读取缓存的值(用户层次上则使用Camera类的成员函数setDrawBuffer 和setReadBuffer来实现)。当两个缓存的值均设置为GL_BACK时,场景的绘制将在后台缓存完成,并可以使用SwapBuffer动作交换前后双缓存的数据,避免场景绘制是产生闪烁。这也是OSG为场景缺省摄像机自动设置的特性(通过指定GraphicsContext::Traits::doubleBuffer的值,我们也可以在创建新的图形窗口时设置是否使用双缓存的特性)。

第二步是我们马上要重点研究的,RenderBin::draw函数。它负责从根节点开始遍历渲染树,并执行各个渲染叶(RenderLeaf)以及上层状态节点(StateGraph)所包含的内容。
完成场景的实际绘制工作之后,OSG将检测并显示出场景绘制当中遇到的错误。有的时候我们会在控制台看到“Warning: detected OpenGL error … after RenderBin::draw()”的字样,这事实上就是在RenderBin::draw函数绘制时产生的错误。通常是因为显示卡对OpenGL高版本的某些函数或枚举量不支持而造成的。

后面依然是有关FBO的操作,包括使用glBlitFramebufferEXT进行解算,将结果复制到关联的纹理以及图片对象中,并结束FBO的调用。

需要特别注意的是:如果希望使用FBO来实现纹理烘焙或者场景截图的话,不可以将场景主摄像机的setRenderTargetImplementation直接设置为相应的枚举量,那样将无法正常地看到场景(因为主摄像机对应的渲染台已经将场景绘制的结果绑定到FBO上了)。正确的作法是在场景树中增加一个Camera节点,设置“渲染目标实现方式”为FBO方式;并通过Camera::setRenderOrder设定它的渲染顺序,设置为PRE_RENDER可以保证这个摄像机在主场景之前执行绘制(它创建了一个“前序渲染台”,存入RenderStage::_preRenderList列表),从而实现“渲染到纹理”的效果。参见osgprerender例子以及第二十二日所述CullVisitor::apply(Camera&)函数的内容。

当前位置:osgUtil/RenderBin.cpp第387行,osgUtil::RenderBin::drawImplementation ()
RenderBin::draw函数的工作就是调用RenderBin::drawImplementation函数,当然用户也可用自定义的绘制回调(RenderBin::setDrawCallback)代替drawImplementation来完成这一绘制工作,当然前提是我们知道在场景绘制的过程中都需要完成什么。

我们曾经在第十七日的内容中列出了osg::State类的几点重要功能:(1)保存OpenGL的所有状态、模式、属性参数、顶点和索引数据;(2)提供了对OpenGL状态堆栈的处理机制,对即将进入渲染管线的数据进行优化;(3)允许用户直接查询各种OpenGL状态的当前值。

这里所述的第二点,对于OpenGL渲染状态堆栈的处理,实际上就是对于OSG状态树(StateGraph)的遍历处理。而各种OpenGL模式的开关设定(也就是我们熟悉的glEnable和glDisable)实际上是通过State::applyMode函数完成;顶点坐标,法线坐标以及各种顶点和索引数组的设置(即glVertexPointer,glNormalPointer等)也是由State类的相关函数,如setVertexPointer等实现的;各种渲染属性的OpenGL处理函数繁多而复杂,此时State类将使用applyAttribute函数,进而调用不同渲染属性对象的StateAttribute::apply(State&)函数,实现多种多样的渲染特性。

由此可见,osg::State类是OSG与OpenGL的主要接口,场景状态树的遍历者和整合者,也是各种渲染状态,以及顶点值的处理途径。但是我们早已知道,OSG的顶点坐标和索引信息是由osg::Geometry类负责保存的,那么负责将Geometry对象的数据传递给State对象的,就是渲染树的叶节点RenderLeaf了。它通过执行自己所包含的Drawable几何体对象的Drawable::draw函数,实现几何体的实际绘制;而在Geometry类的绘制过程中,则将自己记录的数据信息传递给State对象,由它负责完成顶点的载入和处理工作。

而渲染树在其中的作用,就是抽取每个渲染树节点(RenderBin)中的渲染叶(RenderLeaf)对象,交由osg::State整合它在状态树中继承的全部渲染状态,并将几何体数据传递给OpenGL管线,完成绘制的工作。

也许您对于这堆突如其来的结论颇为糊涂,没有关系,具体的代码我们将在下一日加以解析。

当前位置:osgUtil/RenderBin.cpp第387行,osgUtil::RenderBin::drawImplementation
()
根据上一日所述,我们首先得到OSG渲染后台中渲染树(RenderStage/RenderBin),场景树(StateGraph/RenderLeaf),状态机(State),渲染属性(StateAttribute的诸多派生类)和几何体(Drawable)之间的关系图,如下所示:
附图1

图中浅蓝色的箭头表示状态机对象中保存的各种OpenGL状态,即渲染属性的数据(例如Alpha检测,纹理,雾效等),模式数据(种种使用glEnable/glDisable开启或关闭的模式),以及顶点坐标、法线坐标、颜色坐标、纹理坐标,以及数据索引的数据。这些OpenGL编程中经常用到的概念在OSG中被良好地封装起来,而osg::State类就是它们的具体实现者。

如果我们需要自己创建新的派生自Drawable的对象(就像osgText中所实现的),或者自己创建一种新的渲染属性(派生自StateAttribute),那么图中同样介绍了一些值得注意和借鉴的地方:Drawable几何体对象的具体实现在于drawImplementation函数(事实上是通过draw函数间接调用的);而渲染属性的具现函数为StateAttribute::apply(State&),所有的渲染属性都重写了这一函数,以实现自己的功能。

了解了这些概念之后,事实上我们已经对OSG的渲染流程有了大体的认识,即:

1、渲染树的作用是遍历各个渲染元(RenderBin),并按照指定的顺序执行其中各个渲染叶的渲染函数(RenderLeaf::render)。

2、状态树保存了从根节点到当前渲染叶的路径,遍历这条路径并收集所有的渲染属性数据(StateGraph/moveStateGraph),即可获得当前渲染叶渲染所需的所有OpenGL状态数据。

3、渲染叶的渲染函数负责向状态机(osg::State)传递渲染状态数据,进而由渲染属性类本身完成参数在OpenGL中的注册和加载工作;渲染叶还负责调用几何体(Drawable)的绘制函数,传递顶点和索引数据并完成场景的绘制工作。
下面我们很快地浏览一遍代码,并即将骄傲地宣布对于OSG场景绘制模块的解读告一段落。

最长的一帧25
当前位置:osgUtil/RenderBin.cpp第387行,osgUtil::RenderBin::drawImplementation
()
执行流程如下:
1、首先判断当前RenderBin在渲染树中的位置,并在此位置临时插入一个新的渲染状态RenderBin::_stateset。对于透明渲染元(TRANSPARENT_BIN),此渲染状态会自动设置一个Alpha检测属性(osg::AlphaFunc),以便自动剔除绘制结果中颜色Alpha分量为0的像素。因此,我们可以直接指定某个几何体的StateSet为TRANSPARENT_BIN,从而自动实现背景透明的效果(如果纹理或者颜色的Alpha值设置正确的话)。

2、遍历所有的子渲染元(RenderBin::_bins),其中渲染顺序号小于0的渲染元将在这里执行它们的RenderBin::draw函数,由于draw函数内部调用了drawImplementation,因此这构成了一个递归调用,直至渲染树遍历至末端节点。在用户程序中,渲染顺序号的设置使用StateSet::setRenderBinDetails函数。

3、遍历当前RenderBin所保存的所有渲染叶(RenderBin::_renderLeafList),执行RenderLeaf::render函数,实现场景的绘制。通常只有被设置为“DepthSortedBin”的渲染元会选择保存渲染叶而非状态节点(StateGraph),因为这样便于按照深度值排序对象。

4、遍历当前RenderBin所保存的所有状态节点(RenderBin::_stateGraphList),获取其中保存的RenderLeaf对象(保存为StateGraph::_leaves),并执行其render函数。

5、遍历所有的子渲染元(RenderBin::_bins),其中渲染顺序号大于0的渲染元此时才执行它们的RenderBin::draw函数。

由此可知,渲染树中最先被绘制的将是那些顺序号小于0的末端RenderBin节点,其次则依次是顺序号等于0的末端节点,大于0的末端节点,小于0的倒数第二级节点……而作为渲染树根节点的RenderStage中保存的数据将最后被渲染。

如果在渲染树的同一层中顺序号小于0(或大于0)的渲染元不止一个,那么它们会按照顺序号从小到大的顺序依次被渲染,这是由于RenderBin::_bins变量是std::map的类型,在顺序遍历时会自动进行数据的排列。

渲染树同一层中不可能存在渲染顺序号相同的渲染元,因为使用setRenderBinDetails设置了相同数字参量的StateSet对象被构建成状态节点(StateGraph)之后,将插入到同一个RenderBin中。

当前位置:osgUtil/RenderLeaf.cpp第20行,osgUtil::RenderLeaf::render ()
前文中已经反复提到,渲染叶RenderLeaf是OSG渲染后台中几何体(Drawable)对象的唯一管理者;而节点树的构建,开关、变换和LOD等节点类型的应用,渲染状态的设置等工作,最终都要归结到几何体的渲染上来。而这里的render函数主要负责获取之前保存的Drawable指针,投影矩阵,模型视点矩阵,深度值等信息(传递这些信息的是第二十二日中提到的CullVisitor::addDrawableAndDepth函数),并将它们传递给负责渲染状态处理的State类,以及执行Drawable::draw函数。

它的工作流程概括如下:

1、使用State::applyProjectionMatrix传递投影矩阵。

2、使用State::applyModelViewMatrix传递模型视点矩阵。

3、如果当前渲染叶与上一次处理的渲染叶父节点不同,则需要遍历状态树中相应的路径,并更新State状态机中保存的渲染状态数据(采用std::map类型,分别名为_modeMap和_attributeMap)。用于更新的函数为StateGraph::moveStateGraph,它负责清除上一次使用的各种渲染状态,再沿着状态树中的路径,依次添加当前渲染叶所需的数据。最后执行函数State::apply(const StateSet*),由OSG状态机处理并执行相应的OpenGL指令。

4、如果当前渲染叶与上一次处理的渲染叶有相同的父节点(StateGraph对象),则不改变传入State状态机的状态数据,直接执行State::apply函数。

5、执行此渲染叶所保存的Drawable对象的draw函数,完成几何体的绘制。Geometry对象将在这一函数中(实际上是Drawable::drawImplementation)向状态机传递顶点和索引数据,并交由状态机对象来完成几何数据的绘制。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: