《最长的一帧》理解01_场景渲染

osgViewer:: ViewerBase::renderingTraversals()

OSG 的场景渲染过程可以简单地分为三个阶段:用户(APP)阶段,更新用户数据,负责场景对象的运动和管理等等;筛选(CULL)阶段,负责对场景中的对象进行筛选裁减,略过那些不会被用户所见(因而不必渲染)的物体,并根据渲染状态的相似性对即将进入渲染管线的对象排序(从而避免OpenGL 状态量的频繁切换);绘制(DRAW)阶段,执行各种OpenGL 操作,将数据送入OpenGL 渲染管线及显示系统处理。
如果有多个图形设备(渲染窗口)时,需要分别为每个窗口的每个摄像机执行相应的筛选和绘制工作,因为各个摄像机的投影矩阵和观察矩阵均可能不同;不过用户(APP)阶段不需要被执行多次,因为用户数据应当是被各个图形设备所共享的。
对于单线程运行的系统来说,用户/筛选/绘制这三个阶段在每一帧当中都应当是顺序执行的;而对于多线程运行,以至多CPU 的系统来说,则可以将前后两帧的工作稍微有所交叠。用户更新(APP)和场景筛选(CULL),以及场景筛选和绘制(DRAW)的工作互相不能重叠;但是我们可以允许在上一帧的绘制没有结束之前,就开始下一帧的用户数据更新工作;我们还可以允许由不同的CPU 来执行不同图形设备的筛选和绘制工作,从而提高整体渲染的效率,实现实时渲染的目的。

单线程模式下,renderingTraversals 函数的基本执行步骤如下:
1、首先使用ViewerBase::checkWindowStatus 检查是否存在有效的图形设备,不存在的话,需要使用ViewerBase::stopThreading 停止线程运行。
2、记录渲染遍历开始的时间。
3、遍历视景器对应的所有Scene 场景,记录分页数据库的更新启动帧,(使用DatabasePager::signalBeginFrame,这将决定DatabasePager 中的数据请求是否过期),并计算场景节点的边界球。
4、获取当前所有的图形设备(GraphicsContext)和摄像机。
5、遍历所有摄像机的渲染器(Renderer),执行Renderer::cull 场景筛选的操作!
6、遍历所有的图形设备,设置渲染上下文(使用ViewerBase::makeCurrent)并执行GraphicsContext::runOperations,实现场景绘制的操作!
7、再次遍历所有的图形设备,执行双缓存交换操作(GraphicsContext::swapBuffers),这是避免动态绘图时产生闪烁的重要步骤。
8、遍历视景器中的场景,告知分页数据库更新已经结束。
9、释放当前的渲染上下文(ViewerBase::releaseContext)。
10、记录渲染遍历结束的时间,并保存到记录器当中。

当我们向视景器(Viewer)添加一个新的摄像机(Camera)时,一个与摄像机相关联的渲染器(Renderer)也会被自动创建。而当我们准备渲染场景时,与特定图形设备(GraphicsContext)相关联的摄像机也会自动调用其渲染器的相应函数,执行场景筛选与绘制等工作。

OSG 内部经常使用的类osg::State。简单来说,这个类是OpenGL状态机在OSG 中的具体实现。它封装了几乎所有的OpenGL 状态量,属性参数,以及顶点数组的设置值。我们编程时常见的对StateSet,Geometry 等类的操作,实质上最终都交由State 类来保存和执行。它提供了对OpenGL 状态堆栈的处理机制(因此我们不必像OpenGL开发者那样反复考虑堆栈处理的问题),对即将进入渲染管线的数据进行优化(执行渲染状数据的排序,减少OpenGL 状态的变化频率),同时还允许用户直接查询各种OpenGL 状态的当前值(直接执行State::captureCurrentState,而不必再使用glGet*系列函数)。

明确了单线程模型(SingleThreaded)下OSG 渲染遍历的工作流程。事实上无论是场景的筛选还是绘制工作,最后都要归结到场景视图(SceneView)的相应实现函数中去完成,渲染器类Renderer 只是一个更为方便和直观的公用接口而已。下图中演示了单线程运行时,OSG 系统的场景图形,摄像机,图形设备,渲染器和场景视图的关系:
单线程运行时OSG系统的场景图形、摄像机图形设备渲染器和场景图关系
OSG 视景器的摄像机(包括主摄像机_camera 和从摄像机组 _slaves)均包括了与其对应的渲染器(Renderer)和图形设备(GraphicsContext);同时,当我们使用setSceneData 将场景图形的根节点关联到视景器时,这个根节点实质上被添加为此Viewer 对象中每个主/从摄像机的子节点(使用View::assignSceneDataToCameras 函数),因而我们可以通过改变摄像机的观察矩阵来改变我们观察整个场景的视角。场景的筛选(CULL)和绘制(DRAW)工作实质上都是由内部类osgUtil::SceneView来完成的,但是OSG 也为场景渲染的工作提供了良好的公用接口,就是“渲染器”。渲染器Renderer 负责将场景绘制所需的各种数据(OpenGL 状态值,显示设置,筛选设置等)传递给SceneView 对象,并调用SceneView::cull 和SceneView::draw 函数,以完成场景的筛选/绘制工作。摄像机所对应的图形设备(GraphicsContext)同样也可能负责调用SceneView::draw 函数,这与我们选择的线程模型有关。

我们在进行用户程序的开发时,最常用到的场景管理方式是“场景节点树”的结构,场景树顶端的叶节点(osg::Geode)包含了各种需要渲染的几何体的顶点和渲染状态信息;而组节点(osg::Group)及其派生出的各种特殊功能节点则作为场景树的各个枝节节点,它们也可以拥有不同的渲染状态;有且只有一个节点可以直接作为整个场景的根节点,使用setSceneData 将其设置给场景的视景器系统,即等同于将整个场景树传递给OSG 的渲染和显示系统。而保存节点和几何体的各种渲染属性(osg::StateAttribute,例如纹理,雾效,材质,Alpha校验等)和模式开关,则使用节点所附带的渲染状态集(osg::StateSet)。一个状态集中可以包含多种不同的渲染属性和开关,处于场景树顶端的节点将继承并综合各级父节点的渲染状态,实现几何形状的正确渲染。

OSG 渲染后台的主体是场景视图(SceneView),它同样实现了“树状结构”的管理方式,并据此实现了多个专用于渲染工作的内部类。那么在深入介绍场景视图之前,我们先来认识一下OSG 渲染后台的几个“幕后英雄”:
osgUtil::CullVisitor:“筛选访问器”。虽然同样是继承自osg::NodeVisitor,不过这个访问器在整个OSG 系统中可是起了举足轻重的作用。当我们使用它遍历场景图形的各个节点时,CullVisitor 将会对每一个遇到的节点执行场景筛选的工作,判断它是否会超出视截锥体范围,过于渺小,或者被遮挡节点(OccluderNode)挡住,从而将无助益于场景浏览的物体筛选并剔除,降低场景绘制的资源消耗。我们甚至可以使用SceneView::setCullVisitor 来构建和指定使用自己设计的筛选访问器,不过在系统渲染后台之外的环境使用CullVisitor 通常并无用处。
osg::RenderInfo:“渲染信息”管理器。这个类负责保存和管理与场景绘制息息相关的几个重要数据:当前场景的视景器,当前场景对应的所有摄像机,以及当前所有OpenGL 渲染状态和顶点数据。这些数据将在场景筛选和渲染时为OSG 系统后台的工作提供重要依据。
osgUtil:: StateGraph:“状态节点”。我们可以对比场景树的组节点(Group),将StateGraph理解为OSG 渲染后台的组节点。它的组织结构与场景图形的节点结构类似,但是状态树的构建主要以节点的渲染状态集(StateSet)为依据:设置了StateSet 的场景节点,其渲染状态会被记录到“状态节点”中,并保持它在原场景树中的相对位置;状态节点采用映射表std::map 来组织它的子节点,同一层次的子节点如果渲染状态相同,则合并到同一个“状态节点”中。
osgUtil::RenderLeaf:“渲染叶”。我们可以把RenderLeaf 理解为OSG 渲染后台状态树的叶节点。但是,状态树的叶节点绝非等同于场景树的Geode 节点;事实上,“渲染叶”的工作主要是记录场景树中存在的各种Drawable 对象(以及与之相关的投影矩阵,模型视点矩阵等信息)。每个“状态节点”中都包含了一个渲染叶的列表(StateGraph::_leaves),不过只有最末端的“状态节点”会负责记录场景中的“渲染叶”。
osgUtil::RenderStage:“渲染台”。OSG 的渲染后台除了使用“状态树”来组织和优化节点的渲染状态之外,还有另外一种用于场景实际渲染的组织结构,我们称之为“渲染树”,“渲染树”的根节点就是“渲染台”
osgUtil::RenderBin:“渲染元”。它是OSG 渲染树的分支节点,不过对于没有特殊要求的场景渲染来说,更多的渲染树分支也许并不需要:场景中需要渲染的元素及其渲染属性被保存到各个“状态节点”和“渲染叶”当中;渲染树只要按照遍历的顺序,把这些数据记录到作为根节点的“渲染台”当中(即分别保存到std::vector 成员量RenderBin::_ stateGraphList和RenderBin::_renderLeafList 当中,注意RenderStage 派生自RenderBin),就可以执行场景的绘制工作了。

但是,很多时候我们需要某些几何体在其它对象之前被绘制,比如天空总是要被任何飞过的物体所遮挡;很多时候我们也需要在大部分对象绘制完成之后才绘制某个几何体的数据(例如HUD 文字总是显示在所有对象之上)。这种情况下,就有必要对“渲染台”中的数据进行排序,甚至为其创建新的分支“渲染元”,以实现这种复杂的渲染顺序处理。

首先我们来看一个场景构建的实例,并希望借此机会了解一下“状态节点”StateGraph和“渲染叶”RenderLeaf 所构成的状态树,“渲染台”RenderStage 及“渲染元”RenderBin所构成的渲染树,这两棵树之间错综复杂的关系,以及它们与场景节点树之间更为错综复杂的关系。
这里写图片描述
上面的场景结构图中,叶节点_geode3,以及所有六个几何对象均设置了关联的渲染状态集(StateSet),且几何体1 和几何体2 共享了同一个StateSet。图中用“ss”加上数字代号来标识这些StateSet 对象,后面括号中的两个参数分别表示setRenderBinDetails 的两个设置项(“-”表示空字串,“R”表示“RenderBin”,“D”表示“DepthSortedBin”)。

进入渲染后台之后,OSG 将为这个场景生成“状态树”,它是由“状态节点”StateGraph和“渲染叶”RenderLeaf 所组成的:
这里写图片描述
图中的“状态根节点”和“局部状态节点”都是由状态树自动生成的,其中后者的主要工作是保存和维护一些渲染后台自动创建的渲染属性;而“全局状态节点”则保存了一个名为_ globalStateSet 的渲染状态集对象。这就是“全局渲染状态”,它的取值是场景主摄像机的StateSet,换句话说,任何对状态树的遍历都将首先及至场景主摄像机的渲染状态,然后才是各个节点的渲染状态,这就是_globalStateSet 的功用所在了。

而整个状态树的构建过程则可以参考上面的场景树结构图,其规则为:
1、状态树是根据渲染状态(StateSet)来生成的,那些没有设置StateSet 的场景节点将不会影响状态树的构架;
2、场景中的Drawable 对象在状态树中被置入分别的渲染叶(RenderLeaf)中,而一个或多个渲染叶必然被一个状态树末端的节点(StateGraph)所拥有;
3、共享同一个渲染状态的Drawable 对象(图中的_drawable1 和_drawable2)在状态树中将置入同一个末端节点。

生成状态树的同时,OSG 渲染后台还将生成对应的“渲染树”,其组成为一个RenderStage对象和多个RenderBind 对象。如果我们不使用setRenderBinDetails 设置StateSet 的渲染细节的话,那么所有状态树中的末端节点(其中必然包含了一个或多个“渲染叶”)都会按遍历顺序保存到渲染树根节点(渲染台)中,渲染树的构建也就到此结束。

但是,如果我们对于场景中部件的渲染顺序有特殊要求的话,那么渲染树也会因而变得复杂,上面的场景示例最后可能得到如下的一株“渲染树”:

这里写图片描述
根据渲染顺序的不同,渲染树生出了三个分支。相应的状态节点置入各个渲染元(RenderBin)分枝中,其中渲染细节设置为“RenderBin”的状态节点(StateGraph)所处的渲染元也可称为“不透明体渲染元”;而设置为“DepthSortedBin”的状态节点则将其附带的渲染叶(RenderLeaf)送入“透明体渲染元”,于其中采用按深度值降序的方式排序绘制,以获得正确的透明体渲染结果;未设置渲染细节的状态节点则直接由根节点(渲染台,RenderStage)负责维护。

一个渲染元中可以保存一个或多个状态节点(或渲染叶);一个状态节点(或渲染叶)只能置入一个渲染元中。

最后,我们分别用一句话来总结“状态树”与“渲染树”的这几个组成类。之所以选择在经历了如此冗长的篇幅之后再作定义,也是为了便于读者进行归纳和总结,或者在阅读和实践的过程中提出自己的见解。

osgUtil::StateGraph:状态树的分枝节点(状态节点),负责管理场景树中的一个渲染状态(StateSet)对象,末端的StateGraph 节点还负责维护一个“渲染叶”(RenderLeaf)的列表。
osgUtil::RenderLeaf:状态树的叶节点(渲染叶),负责管理和绘制场景树末端的一个几何体(Drawable)对象。
osgUtil::RenderStage:渲染树的根节点(渲染台),负责管理默认渲染顺序的所有末端StateGraph 节点(附带“渲染叶”),并保存了“前序渲染”(pre-render)和“后序渲染”(post-render)的渲染台指针的列表。
osgUtil::RenderBin:渲染树的分枝节点(渲染元),负责管理自定义渲染顺序的末端StateGraph 节点(附带“渲染叶”);渲染树的根节点和分枝节点最多只能有“RenderBin”和“DepthSortedBin”两类子节点,但可以根据不同的渲染顺序号衍生出多个子节点,它们在渲染时将按照顺序号升序的次序执行绘制。

这里写图片描述
OSG 渲染后台与用户层的接口是摄像机类(Camera)。场景中至少有一个主摄像机,它关联了一个图形设备(GraphicsContext,通常是窗口),以及一个渲染器(Renderer);我们可以在场景树中(或者别的视图View 中,对于复合视景器而言)添加更多的摄像机,它们可以关联相同的或者其它的图形设备,但都会配有单独的渲染器,用以保存该摄像机的筛选设置、显示器设置等信息。

场景筛选和绘制的工作由渲染器来完成,而图形设备 GraphicsContext 则负责根据不同时机的选择,调用渲染器的相关函数。例如在单线程模式中,ViewerBase::renderingTraversals函数依次执行Renderer::cull 和Renderer::draw 函数(后者通过GraphicsContext::runOperations调用),而在多线程模型中调用者的关系将更加错综复杂。

OSG 渲染后台的调度中心是场景视图(SceneView),它负责保存和执行筛选访问器(CullVisitor)。CullVisitor 负责遍历并裁减场景,同时在遍历过程中构建对于场景绘制至关重要的渲染树和状态树;生成的状态树以StateGraph 为根节点和各级子节点(其中保存场景树的渲染状态StateSet 数据),以RenderLeaf 为末端叶节点的内容(其中保存场景树中的几何体Drawable 对象);渲染树则以RenderStage 为根节点,RenderBin 为各级子节点,根据渲染顺序和方法的设定,状态树中的节点和渲染叶(RenderLeaf)被记录到RenderStage 和各级RenderBin 中;SceneView 负责保存和维护状态树和渲染树。

绘制场景时,渲染树中的各级节点将取出保存的渲染叶数据,传递给OSG 状态机(State)。后者是OpenGL 状态机制的封装和实现,也是场景绘制的核心元件。状态机取得渲染叶中的几何数据之后,再向根部遍历状态树,取得该几何体绘制相关的所有渲染状态设置,并亲自或者交由StateAttribute 派生类完成渲染状态的实际设定,以及场景元素的实际绘制工作。

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

OSG 的渲染流程大体的认识,即:
1、渲染树的作用是遍历各个渲染元(RenderBin),并按照指定的顺序执行其中各个渲染叶的渲染函数(RenderLeaf::render)。
2、状态树保存了从根节点到当前渲染叶的路径,遍历这条路径并收集所有的渲染属性数据(StateGraph/moveStateGraph),即可获得当前渲染叶渲染所需的所有OpenGL 状态数据。
3、渲染叶的渲染函数负责向状态机(osg::State)传递渲染状态数据,进而由渲染属性类本身完成参数在OpenGL 中的注册和加载工作;渲染叶还负责调用几何体(Drawable)的绘制函数,传递顶点和索引数据并完成场景的绘制工作。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tingzhushaohua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值