CEGUI in Panda3d (WIP)

Hi all,

I’ve been working on integrating CEGUI (http://www.cegui.org.uk) into Panda3d.

Here is a what I have right now:

Full version
http://imgur.com/GIE7.png

How is this better than DirectGUI?

  • It knows about pixels AND does relative element positioning;
  • It uses XML for layouts;
  • It doesn’t use 3D coordinates to position 2D GUI elements!
  • It uses the coordinate system where (0, 0) is the top left corner of the screen, just like the rest of the world does :slight_smile:.
  • It is skinnable, check out http://www.cegui.org.uk/skins/
  • It is not visually painful :slight_smile:

There is a Python integration module called CEGUIPython, but I didn’t get to that yet.

Limitations: nothing major so far. Currently, OpenGL only, but why would you use anything else :wink:?

Here are some implementation details, on which I’d like to have some feedback and suggestions.

To render Cegui in OpenGL, we need to call CEGUI::System::renderGUI() every frame. The trick here is that this must be called in the render thread between begin_frame and end_frame (that’s what my experience shows). The only way I could find to do that is to create a new cull bin, CullBinCegui, which has this code:

void CullBinCegui::
draw(bool force, Thread *current_thread) {
  CEGUI::System::getSingleton().renderGUI();
}

So, to use Cegui, we must put some visible geometry into this new bin. This geometry must be visible, so that it would not be culled away, and draw would eventually be called. I used CardMaker to generate a white card, parented that under render2d, and set the bin to my new bin, “cegui”.

The question is, is there an easier way to do this? I tried to subclass a Geom, so that when it’s being drawn, it would render Cegui, but Geom.draw is not virtual (for performance reasons, I guess). The only virtual method I could find in the render code is CullBin.draw.

If this is the way to do it, then the quesiton is: how to integrate this gracefully into the Panda3d system? Right now, I have to modify these files:

panda3d_src/panda/src/cull/config_cull.cxx         |    6 +
panda3d_src/panda/src/cull/cullBinCegui.cxx        |   72 +
panda3d_src/panda/src/cull/cullBinCegui.h          |   72 +
panda3d_src/panda/src/cull/cull_composite1.cxx     |    1 +
panda3d_src/panda/src/pgraph/cullBinEnums.h        |    1 +
panda3d_src/panda/src/pgraph/cullBinManager.cxx    |   12 +

This seems excessive to me.

One way to make it easier would be to eliminate the CullBinEnums. Essentially, there is a mapping [CullBin name (string)] --> CullBin, and there is no need for these enums. Then, if I want to add a CullBin, I just say CullBinManager.add_bin(string name, CullBin bin) - no need for the enums.

Another issue is passing input events to Cegui (mouse and keyboard). Cegui requires to inject input events into it. For that, I’m using CeguiInputHandler, a subclass of DataNode, to listen to input data from MouseAndKeyboard, convert it and pass it to Cegui. The plan is to insert CeguiInputHandler below MouseAndKeyboard, and reparent all the children of MouseAndKeyboard to CeguiInputHandler, so they would only get those events that did not get consumed by Cegui. However, this may cause some problems.

Right now, I’m writing this in C++, so I’m testing it with PandaFramework, but I’m looking for a way to make it work the same way with ShowBase. Both ShowBase and PandaFramework define a MouseAndKeyboard node, so it should work the same way.

So this is the story so far. Any help, feedback and comments appreciated.
Thanks,

Nik

definetly a nice thing especially since cegeui seems to be quite popular in some areas.
i sorta have to disagree with your “why its better” in 2 points^
-directGUI is as skinable and customizable as you want,too.
directgui (in fact does look not too nice by default) can be made really good looking,too^

anyway. great work. if you can fix it up (especially the python and gl-only part) it’ll be quite intresting for some users

This is fantastic as I am now faced with the terror of developing a UI for my project. I look forward to a release! :smiley:

Oh, I have to share this: http://imgur.com/GUYZ.png
This basically shows off CEGUI’s text editing features, which work just as they should.

Great work! I think if the devs can see value in this they would gladly make patches to the codebase to make this work with panda. The ability to separate GUI layout from gamecode is an important one I think.

I applaud this effort! Thanks for sharing it! It’s great to see Panda integrated with an existing, robust GUI system. I’m a little sad that the GUI system in question is implemented via direct OpenGL calls, though, because it means (a) using this will always be constrained to OpenGL only, and (b) it won’t work with Panda’s upcoming pipelined asynchronous renderer when it is available. But within those limitations, I can certainly see lots of value in providing it.

It seems like a fine way to integrate it. It’s a little hacky, granted, but short of explicit callbacks in the code (which also aren’t out of the question), it’s the cleanest way to integrate. As far as the amount of code you had to fiddle with, it hardly seems excessive; that seems about the right amount of code for a C++ change of this nature. (If Panda had explicit support for callbacks, it would still require about the same amount of code to use it.)

The CullBinEnums serve an important purpose, which is to allow you to define bins in the Config.prc file, via the cull-bin variable. But there’s no requirement that you add your own enumerated type to the CullBinEnums list, if you’d rather not deal with it (and if you don’t want to allow people to define the cegui bin in the Config.prc file).

David

Excellent job, nik!

Actually, it also supports DirectX. According to the CEGUI manual:

(and then requires the renderGUI call just before the call to EndScene)

But yeah, I don’t know about TinyGL.

Sorry, I meant that I’ve only integrated it with the OpenGL version of CEGUI.

CEGUI does support DirectX, and to make the rendering call cleaner, it makes more sense to use the callback approach than to add a CullBin.

drwr, what do you mean by callbacks? Adding a mechanism to add functions to call during the certain stages of the render process? GraphicsEngine looks like a good place to do that, to call the callbacks just before every call to gsg->end_scene(). Then OpenGL and DirectX users would add different specialized callbacks to draw CEGUI using this mechanism.

Or do you mean something more generic?

Also, what about the async renderer you mentioned? Why wouldn’t it work?

DirectX support is not my priority at the moment. If I use callbacks in GraphicsEngine, it should be easy to adapt it later.

As for TinyGL, which is a subset of OpenGL, I guess it is a matter of whether CEGUI uses anything outside this subset. I’m not familiar with either of these.

Nik

Yes, that’s what I mean, though I’d rather design something so that the programmer just needs to enable “CeGUI”, not futz with the appropriate callback to his particular graphics API. Panda’s design is intended to make the graphics API completely transparent to the programmer; in fact, in production, it may select one API or another according to what the user’s drivers provide most readily, without respect to the programmer’s original wishes.

Because the rendering process would be in a completely different memory space from the application process. Since CeGUI is making direct OpenGL calls, it would have to be in the same memory process with the renderer. But then the application wouldn’t be able to control it! In order to make this work, we would have to build a RPC system to relay messages from the application to and from the CeGUI interface.

It isn’t really a subset of OpenGL. It presents an API that resembles a subset of OpenGL, which is different. It doesn’t actually create an OpenGL context or do anything at all with the graphics hardware; but this means that CeGUI will do nothing with TinyGL. A relatively minor detail, though.

David

When I test this with some geometry (should have done this earlier), I get segfaults when drawing CEGUI using its OpenGLRenderer:

1   GLEngine                       	0x1d39f199 gleDrawArraysOrElements_Core + 157
2   GLEngine                       	0x1d423eef gleDrawArraysOrElements_VBO_Exec + 853
3   libGL.dylib                    	0x92b37575 glDrawArrays + 97
4   libCEGUIOpenGLRenderer.0.dylib 	0x1cb81852 CEGUI::OpenGLRenderer::renderVBuffer() + 44 (openglrenderer.cpp:475)
5   libCEGUIOpenGLRenderer.0.dylib 	0x1cb81c74 CEGUI::OpenGLRenderer::doRender() + 114 (openglrenderer.cpp:217)
6   libCEGUIBase.1.dylib           	0x19c3c2c9 CEGUI::System::renderGUI() + 33 (CEGUISystem.cpp:439)
7   libpgraph.dylib                	0x03a47108 CullResult::draw(Thread*) + 250 (cullResult.cxx:272)

And if I move the rendering code outside of the draw_bins function, and therefore, outside the gsg->begin_scene/end_scene, I only see the CEGUI window, but not the model.

I have also found out that CEGUI expects its users to define their own renderers. The OpenGl renderer is the most popular one, but there are renderers for Irrlicht and Ogre engines.

In view of what David had said, it sounds like it would make sense to write a CEGUI to Panda3d renderer.

A CEGUI renderer takes textured quads and renders them as it sees fit. The API requred by CEGUI is here: http://www.cegui.org.uk/api_reference/classCEGUI_1_1Renderer.html. Panda3d would then take care of using the applicable renderer to render the quads.

I’ll try to write this interface, but will probably come back with more questions.

Thanks,

Nik

Hey, that sounds like the perfect solution! I didn’t realize CEGUI was so full-featured. You should have all the information you need about constructing geometry et al in the Panda3D manual, but of course don’t hesitate to ask if anything is confusing or unclear. Having CEGUI directly generate Panda3D vertex tables would certainly solve all of my objections, though.

Ironically, I’ve been spending some time today robustifying the Panda3D callback system. People do occasionally want to make direct OpenGL (or DirectX) calls into Panda’s graphics context, and a robust callback system will make this possible. Of course, as you demonstrate above, there are still lots of pitfalls when you have two separate applications mucking about together in the same graphics state. It sounds like CEGUI and Panda aren’t quite on the same page about saving and restoring state properly for each other.

David

Ok, I’m stuck. I need some pointers about implementing the CEGUI renderer.

It comes down to two things.

First, I need a way to load a texture from an array of bytes. The texture is in either RGB or RGBA mode, one byte per channel, so 3 or 4 bytes per pixel. Texture dimensions are also given. (This is CEGUITexture.loadFromMemory method).

Second, I need a way to ‘cut out’ rectangles from this texture, store them in a list, and render them together at a later point. (This is CEGUIRenderer.addQuad and doRender).

What classes/methods should I use to do this? I’m guessing Texture, as well as a bunch of Geoms to store the rectanlgles. But how do I load bytes into a Texture? and how do I link a Texture to geometry?

In the meantime, I’ll be trying out the new callbacks functionality, which sounds great. Compiling now!

Thanks,

Nik

You can load textures from raw memory using an interface like this:

a = PTAUChar()
a.setData(myDataString)
tex = Texture('tex')
tex.setup2dTexture(xsize, ysize, Texture.TUnsignedByte, Texture.FRgb)
tex.setRamImage(a)

Substitute FRrgba for 4-component texture data, of course. The data arrangement should actually be BGR or BGRA, which is what some people actually mean when they say RGB and RGBA; but if the arrangement is not right, your red and green colors will be reversed. In this case you will have to reverse the data source, which is a nuisance. Pro-rsoft, what do you think about providing a set_ram_image_as() method to balance the new get_ram_image_as() method?

To “cut out” a rectangle, just create a quad (i.e. two triangles) and assign UV coordinates to the vertices that correspond to the corners of the rectangle you require. The UV coordinates range from (0, 0) in the lower left to (1, 1) in the upper right of the texture image. To create a bunch of these rectangles, you will create a GeomVertexData that has all of the vertices for each rectangle, a GeomTriangles that references those vertices, and a Geom that contains them together. See the reference manual for more details on this process.

To apply the texture to your newly-created geometry, simply call np.setTexture(tex) once you have the new geometry all wrapped up in a NodePath.

David

That sounds fair. I’ll put it on my todo-list, I’ll probably have time for that after 1.6 is out the door.
In the meantime, you can also use PNMImage.read and specify a StringStream where you load in the data. I’m not sure, but I believe this does read RGBA correctly.

Though, it would surprise me if CEGUI didn’t provide a way to get BGRA images instead of RGBA, since it’s OpenGLRenderer also has to do that internally, right?

Actually, no, since both OpenGL and DirectX provide interfaces to do this transparently. (Panda doesn’t hook into those interfaces, though, because it needs to be self-consistent with its own image storage format.)

David

ok, I have a working renderer, but there is one problem I can’t solve cleanly.

The problem is to avoid Z-fighting.
Cegui passes in quads’ Z coordinates, and sometimes two quads with the same Z value are on top of each other. In this case, the quad that came later should be in the front.

I use the Z value as the Y coordinate of the node, place the node under render2d and set the cull bin to (‘transparent’, 50).

This produces Z-fighting when the depth value is the same.

Solutions that work, but that I don’t like:

  1. Fiddle with the depth value by incrementing it a little bit after each quad.
    This is not clean, because even if I make the increment very small, having too many quads might break this setup, as the small increments will overflow into the next layer. This Z fighting only happens in few rare situations, so this should not happen. Also also relies on the knowledge of rangge of the Z values from Cegui. Still, this is the best solution so far.

  2. Use the fact that quads are passed in back-to-front order (Z value non-increasing), set the nodes to the ‘unsorted’ bin, and attach them while incrementing sort order.
    This is bad because it ignores the Z coordinate passed in, and it relies on Cegui passing quads in back-to-front order, which may be accidental (this is not in the docs).

Solution that I like that doesn’t work:
use sort order when attaching the GeomNodes to the common parent.
This has no effect at all. Is sort order used in the back-to-front cullbins?

Right now I’m inclined to go with the first approach and hope that nobody will have a million intersecting buttons on one GUI sheet expecting them to render properly.

Any other suggestions?

Won’t setting the Y coordinate work? Giving one a higher Y coordinate than the other… no-one will notice the difference between two different Y’s anyways.
Otherwise, take a look at DecalEffect:
panda3d.org/apiref.php?page=DecalEffect

(I might be totally wrong though)

Yes, this is option 1 I described.
I’ve also tried the Decal effect, and it didn’t work.

I would simply disable the depth test and probably depth write on your GUI node; it’s traditionally disabled for GUI systems anyway, since the depth buffer is only really useful for 3-D objects. No doubt CeGUI is expecting this to be implicitly disabled, so it is presumably passing your objects in back-to-front order anyway. As you say, “the quad that came later should be in the front.”

This is approximately your (2) solution, with the addendum of node.set_depth_test(false) and node.set_depth_write(false).

David

Yes, tried with and without depth test/write - same result. It’s under render2d, which has it disabled by default.

Is the sort order respected in back-to-front bins? Looks like it’s not in my case.
Does any other bin respect both the Y coordinate and the sort order?