OGRE 14.3
Object-Oriented Graphics Rendering Engine
Loading...
Searching...
No Matches
Working with NumPy

Ogre includes a Python component which automatically generates Python bindings from the C++ headers. However, with Python, you most likely do not only want to just use Ogre, but connect it to other components. For this, the Component uses standard python protocols, that offer exposing the API in a pythonic way. In this tutorial, we will look how Ogre integrates with numpy.

Note
this tutorial can be run live in Google Colab here.

We start with a simple 3 channel python array representing a green gradient:

arr = np.zeros((256, 256, 3), dtype=np.uint8)
arr[:,:,1] = np.mgrid[0:256,0:256][1]

To be able to load it into Ogre we now have to convert it to Ogre.Image. The underlying C++ API takes a raw uchar*. However, the python bindings accept any object implementing the Buffer Protocol. This means we can pass the numpy array as is.

ogre_img = Ogre.Image()
ogre_img.loadDynamicImage(arr, 256, 256, Ogre.PF_BYTE_RGB)
Ogre.TextureManager.getSingleton().loadImage("gradient", "General", ogre_img)
Class representing an image file.
Definition OgreImage.h:61
static TextureManager & getSingleton(void)
Get the singleton instance.

Note that Ogre.Image is merely a view on the underlying array and no data is copied. While this is efficient, it also means that you have to ensure that the array does not get out of scope manually. Otherwise the application will crash due to accessing an invalid pointer.

For completeness we also create a small scene where we map the texture on a screen-centred rectangle.

mat = Ogre.MaterialManager.getSingleton().create("gradient_mat", "General")
rpass = mat.getTechniques()[0].getPasses()[0]
rpass.setLightingEnabled(False)
rpass.createTextureUnitState("gradient")
rect = scn_mgr.createScreenSpaceRect(True)
rect.setCorners(-0.5, 0.5, 0.5, -0.5) # in normalized screen space
rect.setMaterial(mat)
scn_mgr.getRootSceneNode().createChildSceneNode().attachObject(rect)
static MaterialManager & getSingleton(void)
Get the singleton instance.

As the rectangle does not cover the full scene, we also set a background colour

gray = np.array([0.3, 0.3, 0.3])
vp.setBackgroundColour(gray)

Here, the standard python sequence protocol is used. Therefore, the data is copied.

Finally, we want read-back rendered image into an array. To avoid superficial copies of the data, we again allocate the memory with numpy:

mem = np.empty((win.getHeight(), win.getWidth(), 3), dtype=np.uint8)
pb = Ogre.PixelBox(win.getWidth(), win.getHeight(), 1, Ogre.PF_BYTE_RGB, mem)
win.copyContentsToMemory(pb, pb)
A primitive describing a volume (3D), image (2D) or line (1D) of pixels in memory.
Definition OgrePixelFormat.h:349

Note, that the convention of specifying width and height is swapped between Ogre and numpy.

Now we can store the image to disk using pyplot.

pyplot.imsave("screenshot.png", mem)
Note
There is also Ogre::RenderTarget::writeContentsToFile if you do not need the pixel data in Python.

Background Threads

By default, the Python bindings do not release the GIL (Global Interpreter Lock) during rendering, which means that no other Python code can run in parallel. This is usually not a problem and constantly re-acquiring the GIL would reduce performance. However, this means that the following code will not work:

import threading
def printer():
for _ in range(15):
print("Thread")
threading.Thread(target=printer).start()
root.startRendering()

The "printer" Thread will be blocked until the rendering is finished. To allow background threads to run, you can use root.allowPyThread(), which will release the GIL during swapping, while rendering is waiting for vsync.