Skip to content


CEGUI渲染流程简析

粗略分析了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上面:

  1. FrameComponent:生成边框和背景
  2. ImageryComponent:生成静态图像
  3. TextComponent:生成文字

这几个component都是由FalagardComponentBase继承而来,作用是根据各种不同的配置,例如背景的样式,是否是边框,文字的排版方式等等,生成顶点数据。

综上,CEGUI的控件逻辑,控件样式,渲染数据是完全分离的。渲染部分采用两级缓存。第一级缓存用于记录顶点数据(GeometryBuffer);第二级缓存将渲染结果保存在纹理上面(RenderingSurface)。

(完)

Posted in 游戏开发, 编程札记.

Tagged with .


0 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.



Some HTML is OK

or, reply to this post via trackback.