Hardware Volume Rendering on NVidia Graphics cards

Theia is a hardware volume renderer that takes advantage of NVidias CUDA language to peform ray casting with GPUs instead of the CPU.

Only unigrid rendering is supported, but yt provides a grid mapping function to get unigrid data from amr or sph formats, see Extracting Fixed Resolution Data.

System Requirements

Nvidia graphics card - The memory limit of the graphics card sets the limit
on the size of the data source.

CUDA 5 or later and

The environment variable CUDA_SAMPLES must be set pointing to the common/inc samples shipped with CUDA. The following shows an example in bash with CUDA 5.5 installed in /usr/local :

export CUDA_SAMPLES=/usr/local/cuda-5.5/samples/common/inc

PyCUDA must also be installed to use Theia.

PyCUDA can be installed following these instructions :

git clone –recursive http://git.tiker.net/trees/pycuda.git

python configure.py python setup.py install

Tutorial

Currently rendering only works on uniform grids. Here is an example on a 1024 cube of float32 scalars.

from yt.visualization.volume_rendering.theia.scene import TheiaScene
from yt.visualization.volume_rendering.algorithms.front_to_back import FrontToBackRaycaster
import numpy as np

#load 3D numpy array of float32
volume = np.load("/home/bogert/log_densities_1024.npy")

scene = TheiaScene( volume = volume, raycaster = FrontToBackRaycaster() )

scene.camera.rotateX(1.0)
scene.update()

surface = scene.get_results()
#surface now contains an image array 2x2 int32 rbga values

The TheiaScene Interface

A TheiaScene object has been created to provide a high level entry point for controlling the raycaster’s view onto the data. The class TheiaScene encapsulates a Camera object and a TheiaSource that intern encapsulates a volume. The Camera provides controls for rotating, translating, and zooming into the volume. Using the TheiaSource automatically transfers the volume to the graphic’s card texture memory.

Example Cookbooks

OpenGL Example for interactive volume rendering:

from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
from OpenGL.GL.ARB.vertex_buffer_object import *

import sys, time
import numpy as np
import pycuda.driver as cuda_driver
import pycuda.gl as cuda_gl

from yt.visualization.volume_rendering.theia.scene import TheiaScene
from yt.visualization.volume_rendering.theia.algorithms.front_to_back import FrontToBackRaycaster
from yt.visualization.volume_rendering.transfer_functions import ColorTransferFunction
from yt.visualization.color_maps import *

import numexpr as ne

window = None     # Number of the glut window.
rot_enabled = True

#Theia Scene
ts = None

#RAY CASTING values
c_tbrightness = 1.0
c_tdensity = 0.05

output_texture = None # pointer to offscreen render target

leftButton = False
middleButton = False
rightButton = False

#Screen width and height
width = 1024
height = 1024

eyesep = 0.1

(pbo, pycuda_pbo) = [None]*2

def create_PBO(w, h):
    global pbo, pycuda_pbo
    num_texels = w*h
    array = np.zeros((w,h,3),np.uint32)

    pbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, pbo)
    glBufferData(GL_ARRAY_BUFFER, array, GL_DYNAMIC_DRAW)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    pycuda_pbo = cuda_gl.RegisteredBuffer(long(pbo))

def destroy_PBO(self):
    global pbo, pycuda_pbo
    glBindBuffer(GL_ARRAY_BUFFER, long(pbo))
    glDeleteBuffers(1, long(pbo));
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    pbo,pycuda_pbo = [None]*2

#consistent with C initPixelBuffer()
def create_texture(w,h):
    global output_texture
    output_texture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, output_texture)
    # set basic parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    # buffer data
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
                 w, h, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, None)

#consistent with C initPixelBuffer()
def destroy_texture():
    global output_texture
    glDeleteTextures(output_texture);
    output_texture = None

def init_gl(w = 512 , h = 512):
    Width, Height = (w, h)

    glClearColor(0.1, 0.1, 0.5, 1.0)
    glDisable(GL_DEPTH_TEST)

    #matrix functions
    glViewport(0, 0, Width, Height)
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    #matrix functions
    gluPerspective(60.0, Width/float(Height), 0.1, 10.0)
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

def resize(Width, Height):
    global width, height
    (width, height) = Width, Height
    glViewport(0, 0, Width, Height)        # Reset The Current Viewport And Perspective Transformation
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(60.0, Width/float(Height), 0.1, 10.0)


def do_tick():
    global time_of_last_titleupdate, frame_counter, frames_per_second
    if ((time.clock () * 1000.0) - time_of_last_titleupdate >= 1000.):
        frames_per_second = frame_counter                   # Save The FPS
        frame_counter = 0  # Reset The FPS Counter
        szTitle = "%d FPS" % (frames_per_second )
        glutSetWindowTitle ( szTitle )
        time_of_last_titleupdate = time.clock () * 1000.0
    frame_counter += 1

oldMousePos = [ 0, 0 ]
def mouseButton( button, mode, x, y ):
	"""Callback function (mouse button pressed or released).

	The current and old mouse positions are stored in
	a	global renderParam and a global list respectively"""

	global leftButton, middleButton, rightButton, oldMousePos

        if button == GLUT_LEFT_BUTTON:
	    if mode == GLUT_DOWN:
	        leftButton = True
            else:
		leftButton = False

        if button == GLUT_MIDDLE_BUTTON:
	    if mode == GLUT_DOWN:
	        middleButton = True
            else:
		middleButton = False

        if button == GLUT_RIGHT_BUTTON:
	    if mode == GLUT_DOWN:
	        rightButton = True
            else:
		rightButton = False

	oldMousePos[0], oldMousePos[1] = x, y
	glutPostRedisplay( )

def mouseMotion( x, y ):
	"""Callback function (mouse moved while button is pressed).

	The current and old mouse positions are stored in
	a	global renderParam and a global list respectively.
	The global translation vector is updated according to
	the movement of the mouse pointer."""

	global ts, leftButton, middleButton, rightButton, oldMousePos
	deltaX = x - oldMousePos[ 0 ]
	deltaY = y - oldMousePos[ 1 ]

	factor = 0.001

	if leftButton == True:
             ts.camera.rotateX( - deltaY * factor)
             ts.camera.rotateY( - deltaX * factor)
	if middleButton == True:
	     ts.camera.translateX( deltaX* 2.0 * factor)
	     ts.camera.translateY( - deltaY* 2.0 * factor)
	if rightButton == True:
	     ts.camera.scale += deltaY * factor

	oldMousePos[0], oldMousePos[1] = x, y
	glutPostRedisplay( )

def keyPressed(*args):
    global c_tbrightness, c_tdensity
    # If escape is pressed, kill everything.
    if args[0] == '\033':
        print('Closing..')
        destroy_PBOs()
        destroy_texture()
        exit()

    #change the brightness of the scene
    elif args[0] == ']':
        c_tbrightness += 0.025
    elif args[0] == '[':
        c_tbrightness -= 0.025

    #change the density scale
    elif args[0] == ';':
        c_tdensity -= 0.001
    elif args[0] == '\'':
        c_tdensity += 0.001 

def idle():
    glutPostRedisplay()

def display():
    try:
        #process left eye
        process_image()
        display_image()

        glutSwapBuffers()

    except:
        from traceback import print_exc
        print_exc()
        from os import _exit
        _exit(0)

def process(eye = True):
    global ts, pycuda_pbo, eyesep, c_tbrightness, c_tdensity

    ts.get_raycaster().set_opacity(c_tdensity)
    ts.get_raycaster().set_brightness(c_tbrightness)

    dest_mapping = pycuda_pbo.map()
    (dev_ptr, size) = dest_mapping.device_ptr_and_size()
    ts.get_raycaster().surface.device_ptr = dev_ptr
    ts.update()
   # ts.get_raycaster().cast()
    dest_mapping.unmap()


def process_image():
    global output_texture, pbo, width, height
    """ copy image and process using CUDA """
    # run the Cuda kernel
    process()
    # download texture from PBO
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, np.uint64(pbo))
    glBindTexture(GL_TEXTURE_2D, output_texture)

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
                 width, height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, None)

def display_image(eye = True):
    global width, height
    """ render a screen sized quad """
    glDisable(GL_DEPTH_TEST)
    glDisable(GL_LIGHTING)
    glEnable(GL_TEXTURE_2D)
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)

    #matix functions should be moved
    glMatrixMode(GL_PROJECTION)
    glPushMatrix()
    glLoadIdentity()
    glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)
    glMatrixMode( GL_MODELVIEW)
    glLoadIdentity()
    glViewport(0, 0, width, height)

    glBegin(GL_QUADS)
    glTexCoord2f(0.0, 0.0)
    glVertex3f(-1.0, -1.0, 0.5)
    glTexCoord2f(1.0, 0.0)
    glVertex3f(1.0, -1.0, 0.5)
    glTexCoord2f(1.0, 1.0)
    glVertex3f(1.0, 1.0, 0.5)
    glTexCoord2f(0.0, 1.0)
    glVertex3f(-1.0, 1.0, 0.5)
    glEnd()

    glMatrixMode(GL_PROJECTION)
    glPopMatrix()

    glDisable(GL_TEXTURE_2D)
    glBindTexture(GL_TEXTURE_2D, 0)
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0)
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0)


#note we may need to init cuda_gl here and pass it to camera
def main():
    global window, ts, width, height
    (width, height) = (1024, 1024)

    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH )
    glutInitWindowSize(width, height)
    glutInitWindowPosition(0, 0)
    window = glutCreateWindow("Stereo Volume Rendering")


    glutDisplayFunc(display)
    glutIdleFunc(idle)
    glutReshapeFunc(resize)
    glutMouseFunc( mouseButton )
    glutMotionFunc( mouseMotion )
    glutKeyboardFunc(keyPressed)
    init_gl(width, height)

    # create texture for blitting to screen
    create_texture(width, height)

    import pycuda.gl.autoinit
    import pycuda.gl
    cuda_gl = pycuda.gl

    create_PBO(width, height)
    # ----- Load and Set Volume Data -----

    density_grid = np.load("/home/bogert/dd150_log_densities.npy")

    mi, ma= 21.5, 24.5
    bins = 5000
    tf = ColorTransferFunction( (mi, ma), bins)
    tf.map_to_colormap(mi, ma, colormap="algae", scale_func = scale_func)

    ts = TheiaScene(volume = density_grid, raycaster = FrontToBackRaycaster(size = (width, height), tf = tf))

    ts.get_raycaster().set_sample_size(0.01)
    ts.get_raycaster().set_max_samples(5000)
    ts.update()

    glutMainLoop()

def scale_func(v, mi, ma):
    return  np.minimum(1.0, np.abs((v)-ma)/np.abs(mi-ma) + 0.0)

# Print message to console, and kick off the main to get it rolling.
if __name__ == "__main__":
    print("Hit ESC key to quit, 'a' to toggle animation, and 'e' to toggle cuda")
    main()

Warning

Frame rate will suffer significantly from stereoscopic rendering. ~2x slower since the volume must be rendered twice.

OpenGL Stereoscopic Example:

from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
from OpenGL.GL.ARB.vertex_buffer_object import *

import sys, time
import numpy as np
import pycuda.driver as cuda_driver
import pycuda.gl as cuda_gl

from yt.visualization.volume_rendering.theia.scene import TheiaScene
from yt.visualization.volume_rendering.theia.algorithms.front_to_back import FrontToBackRaycaster
from yt.visualization.volume_rendering.transfer_functions import ColorTransferFunction
from yt.visualization.color_maps import *

import numexpr as ne

window = None     # Number of the glut window.
rot_enabled = True

#Theia Scene
ts = None

#RAY CASTING values
c_tbrightness = 1.0
c_tdensity = 0.05

output_texture = None # pointer to offscreen render target

leftButton = False
middleButton = False
rightButton = False

#Screen width and height
width = 1920
height = 1080

eyesep = 0.1

(pbo, pycuda_pbo) = [None]*2
(rpbo, rpycuda_pbo) = [None]*2

#create 2 PBO for stereo scopic rendering
def create_PBO(w, h):
    global pbo, pycuda_pbo, rpbo, rpycuda_pbo
    num_texels = w*h
    array = np.zeros((num_texels, 3),np.float32)

    pbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, pbo)
    glBufferData(GL_ARRAY_BUFFER, array, GL_DYNAMIC_DRAW)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    pycuda_pbo = cuda_gl.RegisteredBuffer(long(pbo))

    rpbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, rpbo)
    glBufferData(GL_ARRAY_BUFFER, array, GL_DYNAMIC_DRAW)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    rpycuda_pbo = cuda_gl.RegisteredBuffer(long(rpbo))

def destroy_PBO(self):
    global pbo, pycuda_pbo, rpbo, rpycuda_pbo
    glBindBuffer(GL_ARRAY_BUFFER, long(pbo))
    glDeleteBuffers(1, long(pbo));
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    pbo,pycuda_pbo = [None]*2

    glBindBuffer(GL_ARRAY_BUFFER, long(rpbo))
    glDeleteBuffers(1, long(rpbo));
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    rpbo,rpycuda_pbo = [None]*2

#consistent with C initPixelBuffer()
def create_texture(w,h):
    global output_texture
    output_texture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, output_texture)
    # set basic parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    # buffer data
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
                 w, h, 0, GL_RGB, GL_FLOAT, None)

#consistent with C initPixelBuffer()
def destroy_texture():
    global output_texture
    glDeleteTextures(output_texture);
    output_texture = None

def init_gl(w = 512 , h = 512):
    Width, Height = (w, h)

    glClearColor(0.1, 0.1, 0.5, 1.0)
    glDisable(GL_DEPTH_TEST)

    #matrix functions
    glViewport(0, 0, Width, Height)
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    #matrix functions
    gluPerspective(60.0, Width/float(Height), 0.1, 10.0)
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

def resize(Width, Height):
    global width, height
    (width, height) = Width, Height
    glViewport(0, 0, Width, Height)        # Reset The Current Viewport And Perspective Transformation
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(60.0, Width/float(Height), 0.1, 10.0)


def do_tick():
    global time_of_last_titleupdate, frame_counter, frames_per_second
    if ((time.clock () * 1000.0) - time_of_last_titleupdate >= 1000.):
        frames_per_second = frame_counter                   # Save The FPS
        frame_counter = 0  # Reset The FPS Counter
        szTitle = "%d FPS" % (frames_per_second )
        glutSetWindowTitle ( szTitle )
        time_of_last_titleupdate = time.clock () * 1000.0
    frame_counter += 1

oldMousePos = [ 0, 0 ]
def mouseButton( button, mode, x, y ):
	"""Callback function (mouse button pressed or released).

	The current and old mouse positions are stored in
	a	global renderParam and a global list respectively"""

	global leftButton, middleButton, rightButton, oldMousePos

        if button == GLUT_LEFT_BUTTON:
	    if mode == GLUT_DOWN:
	        leftButton = True
            else:
		leftButton = False

        if button == GLUT_MIDDLE_BUTTON:
	    if mode == GLUT_DOWN:
	        middleButton = True
            else:
		middleButton = False

        if button == GLUT_RIGHT_BUTTON:
	    if mode == GLUT_DOWN:
	        rightButton = True
            else:
		rightButton = False

	oldMousePos[0], oldMousePos[1] = x, y
	glutPostRedisplay( )

def mouseMotion( x, y ):
	"""Callback function (mouse moved while button is pressed).

	The current and old mouse positions are stored in
	a	global renderParam and a global list respectively.
	The global translation vector is updated according to
	the movement of the mouse pointer."""

	global ts, leftButton, middleButton, rightButton, oldMousePos
	deltaX = x - oldMousePos[ 0 ]
	deltaY = y - oldMousePos[ 1 ]

	factor = 0.001

	if leftButton == True:
            ts.camera.rotateX( - deltaY * factor)
            ts.camera.rotateY( - deltaX * factor)
	if middleButton == True:
	    ts.camera.translateX( deltaX* 2.0 * factor)
	    ts.camera.translateY( - deltaY* 2.0 * factor)
	if rightButton == True:
	    ts.camera.scale += deltaY * factor

	oldMousePos[0], oldMousePos[1] = x, y
	glutPostRedisplay( )

def keyPressed(*args):
    global c_tbrightness, c_tdensity, eyesep
    # If escape is pressed, kill everything.
    if args[0] == '\033':
        print('Closing..')
        destroy_PBOs()
        destroy_texture()
        exit()

    #change the brightness of the scene
    elif args[0] == ']':
        c_tbrightness += 0.025
    elif args[0] == '[':
        c_tbrightness -= 0.025

    #change the density scale
    elif args[0] == ';':
        c_tdensity -= 0.001
    elif args[0] == '\'':
        c_tdensity += 0.001 

    #change the transfer scale
    elif args[0] == '-':
        eyesep -= 0.01
    elif args[0] == '=':
        eyesep += 0.01 

def idle():
    glutPostRedisplay()

def display():
    try:
        #process left eye
        process_image()
        display_image()

        #process right eye
        process_image(eye = False)
        display_image(eye = False)


        glutSwapBuffers()

    except:
        from traceback import print_exc
        print_exc()
        from os import _exit
        _exit(0)

def process(eye = True):
    global ts, pycuda_pbo, rpycuda_pbo, eyesep, c_tbrightness, c_tdensity
    """ Use PyCuda """

    ts.get_raycaster().set_opacity(c_tdensity)
    ts.get_raycaster().set_brightness(c_tbrightness)

    if (eye) :
        ts.camera.translateX(-eyesep)
        dest_mapping = pycuda_pbo.map()
        (dev_ptr, size) = dest_mapping.device_ptr_and_size()
        ts.get_raycaster().surface.device_ptr = dev_ptr
        ts.update()
        dest_mapping.unmap()
        ts.camera.translateX(eyesep)
    else :
        ts.camera.translateX(eyesep)
        dest_mapping = rpycuda_pbo.map()
        (dev_ptr, size) = dest_mapping.device_ptr_and_size()
        ts.get_raycaster().surface.device_ptr = dev_ptr
        ts.update()
        dest_mapping.unmap()
        ts.camera.translateX(-eyesep)


def process_image(eye =  True):
    global output_texture, pbo, rpbo, width, height
    """ copy image and process using CUDA """
    # run the Cuda kernel
    process(eye)
    # download texture from PBO
    if (eye) : 
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, np.uint64(pbo))
        glBindTexture(GL_TEXTURE_2D, output_texture)

        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
                 width, height, 0,
                 GL_RGB, GL_FLOAT, None)
    else :
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, np.uint64(rpbo))
        glBindTexture(GL_TEXTURE_2D, output_texture)

        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
                 width, height, 0,
                 GL_RGB, GL_FLOAT, None)

def display_image(eye = True):
    global width, height
    """ render a screen sized quad """
    glDisable(GL_DEPTH_TEST)
    glDisable(GL_LIGHTING)
    glEnable(GL_TEXTURE_2D)
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)

    #matix functions should be moved
    glMatrixMode(GL_PROJECTION)
    glPushMatrix()
    glLoadIdentity()
    glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)
    glMatrixMode( GL_MODELVIEW)
    glLoadIdentity()
    glViewport(0, 0, width, height)

    if (eye) :
        glDrawBuffer(GL_BACK_LEFT)
    else :
        glDrawBuffer(GL_BACK_RIGHT)

    glBegin(GL_QUADS)
    glTexCoord2f(0.0, 0.0)
    glVertex3f(-1.0, -1.0, 0.5)
    glTexCoord2f(1.0, 0.0)
    glVertex3f(1.0, -1.0, 0.5)
    glTexCoord2f(1.0, 1.0)
    glVertex3f(1.0, 1.0, 0.5)
    glTexCoord2f(0.0, 1.0)
    glVertex3f(-1.0, 1.0, 0.5)
    glEnd()

    glMatrixMode(GL_PROJECTION)
    glPopMatrix()

    glDisable(GL_TEXTURE_2D)
    glBindTexture(GL_TEXTURE_2D, 0)
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0)
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0)


#note we may need to init cuda_gl here and pass it to camera
def main():
    global window, ts, width, height
    (width, height) = (1920, 1080)

    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH | GLUT_STEREO)
    glutInitWindowSize(*initial_size)
    glutInitWindowPosition(0, 0)
    window = glutCreateWindow("Stereo Volume Rendering")


    glutDisplayFunc(display)
    glutIdleFunc(idle)
    glutReshapeFunc(resize)
    glutMouseFunc( mouseButton )
    glutMotionFunc( mouseMotion )
    glutKeyboardFunc(keyPressed)
    init_gl(width, height)

    # create texture for blitting to screen
    create_texture(width, height)

    import pycuda.gl.autoinit
    import pycuda.gl
    cuda_gl = pycuda.gl

    create_PBO(width, height)
    # ----- Load and Set Volume Data -----

    density_grid = np.load("/home/bogert/dd150_log_densities.npy")

    mi, ma= 21.5, 24.5
    bins = 5000
    tf = ColorTransferFunction( (mi, ma), bins)
    tf.map_to_colormap(mi, ma, colormap="algae", scale_func = scale_func)

    ts = TheiaScene(volume = density_grid, raycaster = FrontToBackRaycaster(size = (width, height), tf = tf))

    ts.get_raycaster().set_sample_size(0.01)
    ts.get_raycaster().set_max_samples(5000)

    glutMainLoop()

def scale_func(v, mi, ma):
    return  np.minimum(1.0, np.abs((v)-ma)/np.abs(mi-ma) + 0.0)

# Print message to console, and kick off the main to get it rolling.
if __name__ == "__main__":
    print("Hit ESC key to quit, 'a' to toggle animation, and 'e' to toggle cuda")
    main()

Pseudo-Realtime video rendering with ffmpeg:

#This is an example of how to make videos of 
#uniform grid data using Theia and ffmpeg

#The Scene object to hold the ray caster and view camera
from yt.visualization.volume_rendering.theia.scene import TheiaScene

#GPU based raycasting algorithm to use 
from yt.visualization.volume_rendering.theia.algorithms.front_to_back import FrontToBackRaycaster

#These will be used to define how to color the data
from yt.visualization.volume_rendering.transfer_functions import ColorTransferFunction
from yt.visualization.color_maps import *

#This will be used to launch ffmpeg
import subprocess as sp

#Of course we need numpy for math magic
import numpy as np

#Opacity scaling function
def scale_func(v, mi, ma):
      return  np.minimum(1.0, (v-mi)/(ma-mi) + 0.0)

#load the uniform grid from a numpy array file
bolshoi = "/home/bogert/log_densities_1024.npy"
density_grid = np.load(bolshoi)

#Set the TheiaScene to use the density_grid and 
#setup the raycaster for a resulting 1080p image
ts = TheiaScene(volume = density_grid, raycaster = FrontToBackRaycaster(size = (1920,1080) ))

#the min and max values in the data to color
mi, ma = 0.0, 3.6

#setup colortransferfunction
bins = 5000
tf = ColorTransferFunction( (mi, ma), bins)
tf.map_to_colormap(0.5, ma, colormap="spring", scale_func = scale_func)

#pass the transfer function to the ray caster
ts.source.raycaster.set_transfer(tf)

#Initial configuration for start of video
#set initial opacity and brightness values
#then zoom into the center of the data 30%
ts.source.raycaster.set_opacity(0.03)
ts.source.raycaster.set_brightness(2.3)
ts.camera.zoom(30.0)

#path to ffmpeg executable
FFMPEG_BIN = "/usr/local/bin/ffmpeg"

pipe = sp.Popen([ FFMPEG_BIN,
        '-y', # (optional) overwrite the output file if it already exists
	#This must be set to rawvideo because the image is an array
        '-f', 'rawvideo', 
	#This must be set to rawvideo because the image is an array
        '-vcodec','rawvideo',
	#The size of the image array and resulting video
        '-s', '1920x1080', 
	#This must be rgba to match array format (uint32)
        '-pix_fmt', 'rgba',
	#frame rate of video
        '-r', '29.97', 
        #Indicate that the input to ffmpeg comes from a pipe
        '-i', '-', 
        # Tells FFMPEG not to expect any audio
        '-an', 
        #Setup video encoder
	#Use any encoder you life available from ffmpeg
        '-vcodec', 'libx264', '-preset', 'ultrafast', '-qp', '0',
        '-pix_fmt', 'yuv420p',
        #Name of the output
        'bolshoiplanck2.mkv' ],
        stdin=sp.PIPE,stdout=sp.PIPE)
		
		
#Now we loop and produce 500 frames
for k in range (0,500) :
    #update the scene resulting in a new image
    ts.update()

    #get the image array from the ray caster
    array = ts.source.get_results()

    #send the image array to ffmpeg
    array.tofile(pipe.stdin)

    #rotate the scene by 0.01 rads in x,y & z
    ts.camera.rotateX(0.01)
    ts.camera.rotateZ(0.01)
    ts.camera.rotateY(0.01)

    #zoom in 0.01% for a total of a 5% zoom
    ts.camera.zoom(0.01)


#Close the pipe to ffmpeg
pipe.terminate()