粗略分析了CEGUI的渲染流程,总结一下供日后参考。CEGUI版本是0.7.5,OpenGL渲染器。
首先在CEGUI里面每张图片,每个字符都是一个quad,每个quad由2个三角面组成,包括6个顶点的坐标,颜色,纹理坐标,是发送给GPU的最基础的渲染单元。要注意的是,CEGUI并不局限于quad,它可以构造任意多的三角面以生成各种形状。要绘制一个窗口需要很多顶点数据,例如一个简单的button,背景图像需要1个quad(根据背景类型不同可能会更多),有4个文字的话又需要4个quad,总共就有5个quad,30个顶点。所以每个Window对象都有一个GeometryBuffer对象用来缓存自己的顶点数据。
窗口绘制输出的目的地称为RenderingSurface,所以每个窗口都要从属于某个RenderingSurface,否则无法显示。在渲染的时候,CEGUI会遍历所有的窗口,并将该窗口的GeometryBuffer依次提交到该窗口所属surface的渲染队列中去。对于CEGUI来说,这个过程就是“窗口绘制”,同时会触发绘制消息,而真正的绘制操作其实是在这之后。当所有相关的GeometryBuffer都被push进队列,RenderingSurface::draw就会被调用,此时顶点数据才真正提交到Renderer进行渲染输出。Renderer输出的目的地称为RenderTarget,由具体实现而定,可能是Frame buffer,Off-screen buffer,或者Texture object。在当前OpenGLRenderer中,是用纹理对象来实现的。之所以要引入RenderTarget,是为了能缓存surface的输出。
RenderingSurface有两个派生类,一个是RenderingRoot,它是Renderer默认的surface类型,在当前CEGUI的实现中只是对RenderingSurface的简单封装,没有任何额外的功能;第二个是RenderingWindow,它的作用是将渲染队列中的内容绘制到一张纹理图像上面,然后再用该纹理来绘制自己的GeometryBuffer到其他surface上面去。CEGUI 0.7中新增的窗口旋转和各种窗口特效就是通过这种方式来实现的。
在CEGUI中,每个Window对象都有自己的GeometryBuffer,但并不是每个窗口都有自己的surface。对于普通的四平八稳的窗口,它们共享由Renderer创建的RenderingRoot对象;而只有使用了旋转或特效的窗口才会创建属于自己的RenderingWindow(不要忘记,这是一个surface派生类哦)。成员函数Window::getTargetRenderingSurface用于获取窗口对象的surface,从中可以看出surface的从属关系:
RenderingSurface& Window::getTargetRenderingSurface() const
{
if (d_surface)
// 优先使用自己的surface
return *d_surface;
else if (d_parent)
// 如果自己没有surface,则使用父窗口的
return d_parent->getTargetRenderingSurface();
else
// 最后如果自己已经是顶层窗口
// 则使用Renderer默认的surface
return System::getSingleton().getRenderer()->\
getDefaultRenderingRoot();
}
实际的渲染调用流程从System::RenderGUI开始,它首先清空顶层窗口的surface渲染队列,然后调用顶层窗口的Window::render函数:
void Window::render()
{
// 是否可见
if (!isVisible())
return;
// 获取render context,其中包含了当前窗口所从属的surface
RenderingContext ctx;
getRenderingContext(ctx);
// 如果是自己的surface则清空geometry buffer
if (ctx.owner == this)
ctx.surface->clearGeometry();
// 如果没有surface或者surface被标记为无效
if (!d_surface || d_surface->isInvalidated())
{
// 绘制自己(生成顶点数据并提交给surface)
drawSelf(ctx);
// 递归调用所有子窗口的render
const size_t child_count = getChildCount();
for (size_t i = 0; i < child_count; ++i)
d_drawList[i]->render();
}
// 如果是自己的surface则提交GeometryBuffer到GPU进行渲染输出
if (ctx.owner == this)
ctx.surface->draw();
}
在Window::drawSelf中:
void Window::drawSelf(const RenderingContext& ctx)
{
// 构造GeometryBuffer
bufferGeometry(ctx);
// 提交GeometryBuffer到surface
queueGeometry(ctx);
}
void Window::bufferGeometry(const RenderingContext&)
{
// 仅当需要的时候才重新构造顶点数据
if (d_needsRedraw)
{
// 清空GeometryBuffer
d_geometry->reset();
// 触发相关的CEGUI渲染消息
WindowEventArgs args(this);
onRenderingStarted(args);
// HACK: ensure our rendered string
// content is up to date
getRenderedString();
// 这里才是真正产生顶点数据的地方
if (d_windowRenderer)
// 如果有指定的WindowRenderer
// 则交给它来处理
d_windowRenderer->render();
else
// 否则调用虚函数让用户自己负责生成
populateGeometryBuffer();
// 触发渲染结束消息
args.handled = 0;
onRenderingEnded(args);
// mark ourselves as no longer needed a redraw.
d_needsRedraw = false;
}
}
值得注意的是,CEGUI会记录各种窗口状态,只有在需要的时候才会重新构建窗口对象的GeometryBuffer,同样只有在需要的时候,才会重新绘制RenderingSurface。
上面代码中还提到了WindowRenderer(注意区分RenderingWindow)。这个class的作用是根据looknfeel的描述来生成生成顶点数据。在CEGUIFalagardWRBase工程下面有一大堆Fal开头的class,例如FalButton,FalEditbox等等,就是专门干这些工作的。经过WindowRenderer的层层调用,最终会落到以下三个component上面:
- FrameComponent:生成边框和背景
- ImageryComponent:生成静态图像
- TextComponent:生成文字
这几个component都是由FalagardComponentBase继承而来,作用是根据各种不同的配置,例如背景的样式,是否是边框,文字的排版方式等等,生成顶点数据。
综上,CEGUI的控件逻辑,控件样式,渲染数据是完全分离的。渲染部分采用两级缓存。第一级缓存用于记录顶点数据(GeometryBuffer);第二级缓存将渲染结果保存在纹理上面(RenderingSurface)。