Skip to main content
Version: develop

A New UI system: GGUI

CategoryPrerequisites
OSWindows / Linux / Mac OS X
Backendx64 / CUDA / Vulkan

Starting from v0.8.0, Taichi adds a new UI system GGUI. The new system uses GPU for rendering, making it much faster to render 3D scenes. That is why this new system gets its name as GGUI. This document describes the APIs that it provides.

IMPORTANT

If you choose Vulkan as backend, ensure that you install the Vulkan environment.

note

It is recommended that you familiarize yourself with GGUI through the examples in examples/ggui_examples.

note

The variables referenced in code snippets below are define like this:

vertices         = ti.Vector.field(2, ti.f32, shape=200)
vertices_3d = ti.Vector.field(3, ti.f32, shape=200)
indices = ti.field(ti.i32, shape=200 * 3)
normals = ti.Vector.field(3, ti.f32, shape=200)
per_vertex_color = ti.Vector.field(3, ti.f32, shape=200)

color = (0.5, 0.5, 0.5)

Create a window

ti.ui.Window(name, res) creates a window.

window = ti.ui.Window(name='Window Title', res = (640, 360), fps_limit=200, pos = (150, 150))
  • The name parameter sets the title of the window.
  • The res parameter specifies the resolution (width and height) of the window.
  • The fps_limit parameter sets the maximum frames per second (FPS) for the window.
  • The pos parameter specifies the position of the window with respect to the top-left corner of the main screen.

A ti.ui.Window can display three types of objects:

  • 2D Canvas, which is used to draw simple 2D geometries like circles and triangles.+ 3D Scene, which is used to render 3D meshes and particles, and provides configurable camera and light sources.
  • Immediate mode GUI components, such as buttons and textboxes.

2D Canvas

Create a canvas

The following code retrieves a Canvas object that covers the entire window.

canvas = window.get_canvas()

Draw on the canvas

canvas.set_background_color(color)
canvas.triangles(vertices, color, indices, per_vertex_color)

radius = 5
canvas.circles(vertices, radius, color, per_vertex_color)

width = 2
canvas.lines(vertices, width, indices, color, per_vertex_color)
canvas.set_image(window.get_image_buffer_as_numpy())

The arguments vertices, indices, per_vertex_color, and image must be Taichi fields. If per_vertex_color is provided, color is ignored.

The positions/centers of geometries are represented as floats between 0.0 and 1.0, which indicate the relative positions of the geometries on the canvas. For circles() and lines(), the radius and width arguments are relative to the height of the window.

The canvas is cleared after every frame. Always call these methods within the render loop.

3D Scene

Create a scene

scene = window.get_scene()

Configure camera

camera = ti.ui.Camera()
camera.position(1, 2, 3) # x, y, z
camera.lookat(4, 5, 6)
camera.up(0, 1, 0)
camera.projection_mode(ti.ui.ProjectionMode.Perspective)
scene.set_camera(camera)

Configuring light sources

Add a point light

Call point_light() to add a point light to the scene.

scene.point_light(pos=(1, 2, 3), color=(0.5, 0.5, 0.5))

Note that you need to call point_light() for every frame. Similar to the canvas() methods, call this method within your render loop.

3D Geometries

scene.lines(vertices, width, indices, color, per_vertex_color)
scene.mesh(vertices_3d, indices, normals, color, per_vertex_color)
scene.particles(vertices_3d, radius, color, per_vertex_color)

The arguments vertices, indices, per_vertex_color, and image are all expected to be Taichi fields. If per_vertex_color is provided, color is ignored.

The positions/centers of geometries should be in the world-space coordinates.

note

If a mesh has num triangles, the indices should be a 1D scalar field with a shape (num * 3), not a vector field.

normals is an optional parameter for scene.mesh().

  1. An example of drawing 3d-lines
import taichi as ti

ti.init(arch=ti.cuda)

N = 10

particles_pos = ti.Vector.field(3, dtype=ti.f32, shape = N)
points_pos = ti.Vector.field(3, dtype=ti.f32, shape = N)

@ti.kernel
def init_points_pos(points : ti.template()):
for i in range(points.shape[0]):
points[i] = [i for j in ti.static(range(3))]

init_points_pos(particles_pos)
init_points_pos(points_pos)

window = ti.ui.Window("Test for Drawing 3d-lines", (768, 768))
canvas = window.get_canvas()
scene = window.get_scene()
camera = ti.ui.Camera()
camera.position(5, 2, 2)

while window.running:
camera.track_user_inputs(window, movement_speed=0.03, hold_key=ti.ui.RMB)
scene.set_camera(camera)
scene.ambient_light((0.8, 0.8, 0.8))
scene.point_light(pos=(0.5, 1.5, 1.5), color=(1, 1, 1))

scene.particles(particles_pos, color = (0.68, 0.26, 0.19), radius = 0.1)
# Draw 3d-lines in the scene
scene.lines(points_pos, color = (0.28, 0.68, 0.99), width = 5.0)
canvas.scene(scene)
window.show()

Advanced 3d Geometries

window = ti.ui.Window(name='Advanced 3d Geometries', res = (720, 720))
scene = window.get_scene()
width = 2
radius = 5

scene.lines(vertices, width, indices, color, per_vertex_color, vertex_offset=0, vertex_count=10, index_offset=0, index_count=10)

scene.mesh(vertices_3d, indices, normals, color, per_vertex_color, vertex_offset=0, vertex_count=10, index_offset=0, index_count=10, show_wireframe=True)

scene.particles(vertices_3d, radius, color, per_vertex_color, index_offset=0, index_count=10)

scene.mesh_instance(vertices_3d, indices, normals, color, per_vertex_color, vertex_offset=0, vertex_count=10, index_offset=0, index_count=10, show_wireframe=True)

The additional arguments vertex_offset, vertex_count, index_offset and index_count control the visible part of the particles and mesh. For the mesh() and mesh_instance() methods, set whether to show wireframe mode through setting show_wireframe.

:::example

  1. Example of drawing a part of the mesh/particles
scene = window.get_scene()

center = ti.Vector.field(3, ti.f32, shape=10)

# For particles
# draw the 2-th to 7-th particles
scene.particles(center, radius=1, index_offset = 1, index_count = 6)

# For mesh
# 1. with indices
scene.mesh(
vertices_3d, indices, index_offset=1, index_count=3,
# vertex_offset is set to 0 by default, and it is not necessary
# to assign vertex_offset a value that otherwise you must.
vertex_offset = 1
)

# usually used as below:
# draw the 11-th to 111-th mesh vertexes
scene.mesh(vertices_3d, indices, index_offset=10, index_count=100)

# 2. without indices (similar to the particles' example above)
scene.mesh(
vertices_3d,
vertex_offset=2, # user defined first vertex index
vertex_count=3, # user defined vertex count
)
  1. An example of drawing part of lines
import taichi as ti

ti.init(arch=ti.cuda)

N = 10

particles_pos = ti.Vector.field(3, dtype=ti.f32, shape = N)
points_pos = ti.Vector.field(3, dtype=ti.f32, shape = N)
points_indices = ti.Vector.field(1, dtype=ti.i32, shape = N)

@ti.kernel
def init_points_pos(points : ti.template()):
for i in range(points.shape[0]):
points[i] = [i for j in range(3)]
# points[i] = [ti.sin(i * 1.0), i * 0.2, ti.cos(i * 1.0)]

@ti.kernel
def init_points_indices(points_indices : ti.template()):
for i in range(N):
points_indices[i][0] = i // 2 + i % 2

init_points_pos(particles_pos)
init_points_pos(points_pos)
init_points_indices(points_indices)

window = ti.ui.Window("Test for Drawing 3d-lines", (768, 768))
canvas = window.get_canvas()
scene = window.get_scene()
camera = ti.ui.Camera()
camera.position(5, 2, 2)

while window.running:
camera.track_user_inputs(window, movement_speed=0.03, hold_key=ti.ui.RMB)
scene.set_camera(camera)
scene.ambient_light((0.8, 0.8, 0.8))
scene.point_light(pos=(0.5, 1.5, 1.5), color=(1, 1, 1))

scene.particles(particles_pos, color = (0.68, 0.26, 0.19), radius = 0.1)
# Here you will get visible part from the 3rd point with (N - 4) points.
scene.lines(points_pos, color = (0.28, 0.68, 0.99), width = 5.0, vertex_count = N - 4, vertex_offset = 2)
# Using indices to indicate which vertex to use
# scene.lines(points_pos, color = (0.28, 0.68, 0.99), width = 5.0, indices = points_indices)
# Case 1, vertex_count will be changed to N - 2 when drawing.
# scene.lines(points_pos, color = (0.28, 0.68, 0.99), width = 5.0, vertex_count = N - 1, vertex_offset = 0)
# Case 2, vertex_count will be changed to N - 2 when drawing.
# scene.lines(points_pos, color = (0.28, 0.68, 0.99), width = 5.0, vertex_count = N, vertex_offset = 2)
canvas.scene(scene)
window.show()
  1. Details of mesh instancing
window = ti.ui.Window("Display Instanced Mesh", (1024, 1024))
scene = window.get_scene()

num_instance = 100
m_transforms = ti.Matrix.field(4, 4, dtype = ti.f32, shape = num_instance)


# For example: An object is scaled by 2, rotated by rotMat, and translated by t = [1, 2, 3], then
#
# The ScaleMatrix is:
# 2, 0, 0, 0
# 0, 2, 0, 0
# 0, 0, 2, 0
# 0, 0, 0, 1
#
# The RotationMatrix is:
# https://en.wikipedia.org/wiki/Rotation_matrix#General_rotations
#
# The TranslationMatrix is:
# 1, 0, 0, 1
# 0, 1, 0, 2
# 0, 0, 1, 3
# 0, 0, 0, 1
#
# Let TransformMatrix = TranslationMatrix @ RotationMatrix @ ScaleMatrix, then the final TransformMatrix is:
# 2 * rotMat00, rotMat01, rotMat02, 1
# rotMat10, 2 * rotMat11, rotMat12, 2
# rotMat20, rotMat21, 2 * rotMat22, 3
# 0, 0, 0, 1
...

# Draw mesh instances (from the 1st instance)
scene.mesh_instance(vertices_3d, indices, transforms = m_transforms, instance_offset = 1)
  1. Example of setting wireframe mode
window = ti.ui.Window("Display Mesh", (1024, 1024), vsync=True)
canvas = window.get_canvas()
scene = window.get_scene()
camera = ti.ui.Camera()

# slider_int usage
some_int_type_value = 0
def show_options():
global some_int_type_value

window.GUI.begin("Display Panel", 0.05, 0.1, 0.2, 0.15)
display_mode = window.GUI.slider_int("Value Range", some_int_type_value, 0, 5)
window.GUI.end()

while window.running:

...
# if to show wireframe
scene.mesh_instance(vertices_3d, indices, instance_count = 100 , show_wireframe = True)

canvas.scene(scene)
show_options()
window.show()
note

If indices is not provided, consider using like this:

scene = window.get_scene()
scene.mesh(vertices_3d, normals, color, per_vertex_color, vertex_offset=0, vertex_count=50, show_wireframe=True)

If indices is provided, consider using like this:

scene.mesh(vertices_3d, indices, normals, color, per_vertex_color, vertex_offset=0, index_offset=0, index_count=50, show_wireframe=True)

Rendering the scene

You can render a scene on a canvas.

window = ti.ui.Window(name='Title', res=(640, 360))
canvas = window.get_canvas()
canvas.scene(scene)

Fetching Color/Depth information

img = window.get_image_buffer_as_numpy()
window.get_depth_buffer(scene_depth)
depth = window.get_depth_buffer_as_numpy()

After rendering the current scene, you can fetch the color and depth information of the current scene using get_image_buffer_as_numpy() and get_depth_buffer_as_numpy(), which copy the gpu data to a NumPy array(cpu). get_depth_buffer() copies the GPU data to a Taichi field (depend on the arch you choose) or copies data from GPU to GPU.

:::example

  1. Example of fetching color information
window = ti.ui.Window("Test for getting image buffer from ggui", (768, 768), vsync=True)
video_manager = ti.tools.VideoManager("OutputDir")

while window.running:
# render_scene()
img = window.get_image_buffer_as_numpy()
video_manager.write_frame(img)
window.show()

video_manager.make_video(gif=True, mp4=True)
  1. An example of fetching the depth data
window_shape = (720, 1080)
window = ti.ui.Window("Test for copy depth data", window_shape)
canvas = window.get_canvas()
scene = window.get_scene()
camera = ti.ui.Camera()

# Get the shape of the window
w, h = window.get_window_shape()
# The field/ndarray stores the depth information, and must be of the ti.f32 data type and have a 2d shape.
# or, in other words, the shape must equal the window's shape
scene_depth = ti.ndarray(ti.f32, shape = (w, h))
# scene_depth = ti.field(ti.f32, shape = (w, h))

while window.running:
# render()
canvas.scene(scene)
window.get_depth_buffer(scene_depth)
window.show()

GUI components

The design of GGUI's GUI components follows the Dear ImGui APIs.

window = ti.ui.Window("Test for GUI", res=(512, 512))
gui = window.get_gui()
value = 0
color = (1.0, 1.0, 1.0)
with gui.sub_window("Sub Window", x=10, y=10, width=300, height=100):
gui.text("text")
is_clicked = gui.button("name")
value = gui.slider_float("name1", value, minimum=0, maximum=100)
color = gui.color_edit_3("name2", color)

Show a window

Call show() to show a window.

window.show()

Call this method only at the end of the render loop for each frame.

User input processing

To retrieve the events that have occurred since the last method call:

events = window.get_events()

Each event in events is an instance of ti.ui.Event. It has the following properties:

  • event.action, which can be ti.ui.PRESS, ti.ui.RELEASE, or ti.ui.MOTION.
  • event.key: the key related to this event.

To retrieve the mouse position:

  • window.get_cursor_pos()

To check if a key is pressed:

  • window.is_pressed(key)

The following is a user input processing example from mpm128:

gravity = ti.Vector.field(2, ti.f32, shape=())
attractor_strength = ti.field(ti.f32, shape=())

while window.running:
# keyboard event processing
if window.get_event(ti.ui.PRESS):
if window.event.key == 'r': reset()
elif window.event.key in [ti.ui.ESCAPE]: break
if window.event is not None: gravity[None] = [0, 0] # if had any event
if window.is_pressed(ti.ui.LEFT, 'a'): gravity[None][0] = -1
if window.is_pressed(ti.ui.RIGHT, 'd'): gravity[None][0] = 1
if window.is_pressed(ti.ui.UP, 'w'): gravity[None][1] = 1
if window.is_pressed(ti.ui.DOWN, 's'): gravity[None][1] = -1

# mouse event processing
mouse = window.get_cursor_pos()
# ...
if window.is_pressed(ti.ui.LMB):
attractor_strength[None] = 1
if window.is_pressed(ti.ui.RMB):
attractor_strength[None] = -1

window.show()

Image I/O

To write the current frame in the window to an image file:

window.save_image('frame.png')

Note that you must call window.save_image() before calling window.show().

Off-screen rendering

GGUI supports saving frames to images without showing the window. This is also known as "headless" rendering. To enable this mode, set the argument show_window to False when initializing a window.

window = ti.ui.Window('Window Title', (640, 360), show_window = False)

Then you can call window.save_image() as normal and remove the window.show() call at the end.