Taichi GUI 系统完全指南:窗口创建、图像显示、几何绘制与交互事件
【免费下载链接】taichiProductive, portable, and performant GPU programming in Python.项目地址: https://gitcode.com/GitHub_Trending/ta/taichi
Taichi 内置了一套轻量级的 GUI(图形用户界面)系统,用于可视化 Taichi field、NumPy ndarray 等数据容器中的仿真数据,并支持线、圆、三角形、矩形、箭头与文本等基础图元的绘制。本文以docs/lang/articles/visualization/gui_system.md为主线,结合 Python 侧实现python/taichi/ui/gui.py、C++ 侧软件光栅化画布taichi/ui/gui/gui.h与图像转换内核python/taichi/_kernels.py,系统讲解窗口生命周期、坐标系统、图像渲染(含fast_gui零拷贝模式)、批量几何绘制、鼠标键盘事件处理与交互控件,帮助你把仿真结果快速变成可交互的实时可视化程序。
创建并显示一个窗口
Taichi GUI 的核心类是GUI,位于 python/taichi/ui/gui.py,一行代码即可创建一个窗口:
gui = ti.GUI('Hello World!', (640, 360))第一个参数是窗口标题,第二个参数(640, 360)是窗口分辨率(宽 × 高)。从源码构造函数的签名可以看到res参数的实际语义与更多可选参数:
def __init__( self, name="Taichi", res=512, background_color=0x0, show_gui=True, fullscreen=False, fast_gui=False, ):参数说明(来源于 python/taichi/ui/gui.py 的 docstring 与实现):
| 参数 | 默认值 | 说明 |
|---|---|---|
name | 'Taichi' | 窗口标题 |
res | 512 | 窗口分辨率。若传入标量(如512),宽高相等;也可传(w, h)二元组 |
background_color | 0x000000 | 背景色,十六进制整型 |
show_gui | True | 是否真正渲染窗口,False时可离线渲染并保存截图 |
fullscreen | False | 是否全屏显示 |
fast_gui | False | 是否启用零拷贝快速模式,后文详述 |
窗口创建后需要调用gui.show()显示画面,且必须在while循环中调用,否则窗口会一闪而过立即消失:
while gui.running: gui.show()gui.running是一个只读属性(见 python/taichi/ui/gui.py),其值由 C++ 层should_close标志决定:只要用户没有关闭窗口,循环就持续刷新画面。在 C++ 侧,每一帧update()还会把帧间隔限制在frame_delta_limit = 1.0 / 60(即默认上限 60 FPS),并每 10 帧把实时 FPS 写进窗口标题(见 taichi/ui/gui/gui.h)。
Python 侧还提供了fps_limit属性(见 python/taichi/ui/gui.py),你可以自由调整帧率上限:
gui.fps_limit = 30 # 限制为 30 FPS gui.fps_limit = None # 取消限制另外,GUI.__init__会读取三个环境变量来覆盖默认行为(见 python/taichi/ui/gui.py):
TI_GUI_SHOW:覆盖show_gui;TI_GUI_FULLSCREEN:覆盖fullscreen;TI_GUI_FAST:覆盖fast_gui。
这在无显示器的服务器上做离线渲染、批量出图时非常实用。
关闭窗口
在while循环内将gui.running置为False即可优雅退出循环:
import random gui = ti.GUI('Window Title', (640, 360)) some_events_happend = lambda: random.random() < 0.8 while gui.running: if some_events_happend(): gui.running = False gui.show()此外还可以直接调用gui.close()(见 python/taichi/ui/gui.py)释放底层资源。一个值得注意的行为是:C++ 侧在用户点击窗口关闭按钮后,如果连续 5 帧该关闭事件都未被处理,会抛出RuntimeError强制退出,注释里明确写道这是为了兼容"忘记写break语句"的历史示例(见 taichi/ui/gui/gui.h)。因此标准写法始终是while gui.running: ... gui.show()。
坐标系
Taichi GUI 采用归一化坐标系:坐标原点位于窗口左下角,+x方向向右,+y方向向上,坐标值范围均为[0.0, 1.0]。其中:
(0.0, 0.0)对应窗口左下角;(1.0, 1.0)对应窗口右上角。
该归一化坐标由 C++ 画布层的transform_matrix映射到实际像素空间:Canvas构造时用Matrix3(Vector3(img.get_res(), 1.0))建立变换矩阵,transform()负责把[0,1]归一化坐标换算为像素坐标(见 taichi/ui/gui/gui.h)。这也解释了为什么单个图元的坐标参数用0.x这类小数——它们与窗口绝对分辨率无关。
显示 field 或 ndarray
gui.set_image()是显示数据的核心方法,同时接受 Taichi field 与 NumPy ndarray:
gui = ti.GUI('Set Image', (640, 480)) image = ti.Vector.field(3, ti.f32, shape=(640, 480)) while gui.running: gui.set_image(image) gui.show()由于 Taichi field 是全局数据容器,如果在while循环之间更新了 field 的内容,GUI 窗口就会自动刷新为最新画面。
从 python/taichi/ui/gui.py 的实现看,set_image()支持以下输入形态(宽高必须与窗口分辨率一致):
- 灰度:
ti.field(shape=(x, y))、np.ndarray(shape=(x, y)); - RG:
shape=(x, y, 2)或ti.Vector.field(2, ...); - RGB:
shape=(x, y, 3)或ti.Vector.field(3, ...); - RGBA:
ti.Vector.field(4, ...)(文档示例即以ti.Vector.field(3, ti.f32, shape=(640, 480))展示 RGB 图像)。
dtype 支持范围同样受约束(见cook_image(),python/taichi/ui/gui.py):
- 无符号整型:
uint8([0, 255])、uint16、uint32,会被等比例缩放到[0, 1]; - 浮点型:
float16、float32、float64([0, 1]),直接转为float32; - 其余 dtype 会抛出
ValueError。
重要:
set_image()会断言输入图像的 shape 与 GUI 窗口分辨率完全一致(assert res == self.res),不一致会直接报错,请务必保证二者匹配。
对于浮点型 ScalarField 与 Vector field,Taichi 会走优化路径——直接调用tensor_to_image/vector_to_image这两个 Taichi 内核(见 python/taichi/_kernels.py)把 field 内容复制到内部 RGBA 缓冲,再交给 C++ 层core.set_img;而整型 field 则先to_numpy()再走cook_image()的通用路径。
零拷贝帧缓冲:fast_gui 模式
普通模式下,gui.set_image()每帧都要把图像数据转换成可显示格式并拷贝到窗口缓冲,窗口分辨率越大开销越明显,难以维持高 FPS。如果只需要调用set_image()而不使用任何绘制命令,可以启用fast_gui模式:
gui = ti.GUI('Fast GUI', res=(400, 400), fast_gui=True)该模式允许 Taichi GUI 直接把图像数据写入帧缓冲,省去额外拷贝,显著提升 FPS。其实现位于内核vector_to_fast_image(见 python/taichi/_kernels.py):它按行把 RGB 打包成单个 32 位像素((r << 16) + (g << 8) + b),直接写入预分配的uint32帧缓冲(Python 侧在 python/taichi/ui/gui.py 创建np.zeros(res[0]*res[1], dtype=np.uint32)),并将缓冲指针传给 C++ 窗口后端,从而实现真正的零拷贝直写。
fast_gui模式对输入有严格限制(见 python/taichi/ui/gui.py 的断言):
- 传入
set_image()的必须是Taichi 向量场(ti.Vector.field),不支持 NumPy 数组与标量场; - 通道数必须为 3(RGB)或 4(RGBA);
- dtype 必须是
ti.f32、ti.f64或ti.u8之一。
在仓库测试 tests/python/test_gui.py 中,test_set_image_fast_gui_with_offset对 3/4 通道、三种 dtype 与多种 offset 组合进行了回归验证,并校验了打包后的像素字节序。
在窗口上绘制几何图形
Taichi GUI 支持绘制线、圆、三角形、矩形、箭头与文本等简单几何图形,并同时提供"绘制单个图元"与"批量绘制"两套 API。
单个几何图形
绘制单个图元非常直观:指定位置、大小等信息后调用对应 API 即可。坐标参数(如begin、end、center、pos)可以是 Python list、NumPy 数组或ti.Vector,只要可下标访问且维度为(2, )。
Line(线段),指定起点与终点:
import numpy as np gui = ti.GUI('Single Line', res=(400, 400)) begin = [0.1, 0.1] end = [0.9, 0.9] while gui.running: gui.line(begin, end, radius=1, color=0x068587) gui.show()Circle(圆),指定圆心与半径(像素单位):
import numpy as np gui = ti.GUI('Single Circle', res=(400, 400)) center = [0.5, 0.5] while gui.running: gui.circle(pos=center, radius=30, color=0xED553B) gui.show()Triangle(三角形),指定三个顶点:
import numpy as np gui = ti.GUI('Single Triangle', res=(400, 400)) p1 = [0.5, 0.5] p2 = [0.6, 0.5] p3 = [0.5, 0.6] while gui.running: gui.triangle(a=p1, b=p2, c=p3, color=0xEEEEF0) gui.show()Rectangle(矩形),指定左上角与右下角两点:
import numpy as np gui = ti.GUI('Single Rectangle', res=(400, 400)) p1 = [0.3, 0.4] p2 = [0.7, 0.6] while gui.running: gui.rect(topleft=p1, bottomright=p2, color=0xFFFFFF) gui.show()Arrow(箭头),指定起点与方向向量:
import numpy as np gui = ti.GUI('Single Arrow', res=(400, 400)) begin = [0.3, 0.3] increment = [0.5, 0.5] while gui.running: gui.arrow(orig=begin, direction=increment, color=0xFFFFFF) gui.show()Text(文本),指定位置与内容:
gui = ti.GUI('Text', res=(400, 400)) position = [0.3, 0.5] while gui.running: gui.text(content='Hello Taichi', pos=position, font_size=34, color=0xFFFFFF) gui.show()这些 API 在 python/taichi/ui/gui.py 中均有定义,默认值可归纳为:color默认0xFFFFFF(白色),radius默认1(像素),text()的font_size默认15。颜色统一使用 24 位十六进制整型(0xRRGGBB),Python 侧的hex_to_rgb会把高位 R、中位 G、低位 B 拆出并归一化到[0,1](见 python/taichi/ui/gui.py)。
从 C++ 实现看,这些图元最终走的是软件光栅化:Canvas::Line::stroke()逐像素计算到线段的有向距离并按距离混合颜色(抗锯齿),Canvas::Circle::finish()对圆心包围盒内像素做clamp(radius - dist)半透明混合,三角形则用叉积判断像素是否落在三边同侧(见 taichi/ui/gui/gui.cpp)。矩形rect实际由四条线段拼成(见 python/taichi/ui/gui.py),而箭头由一条主线段加两条按tip_scale=0.2、angle=45°旋转生成的箭头翼线段组成(见_arrow_to_lines,python/taichi/ui/gui.py)。
批量绘制多个几何图形
批量绘制时,各 API 的pos(或begin/end/orig/direction)参数接受Taichi field 或 NumPy 数组,而非 Python 原始列表。数组每个元素是一对[0.0, 1.0]范围内的浮点数,表示图元的相对位置。
Lines(批量线段),绘制 5 条宽度为 2 的蓝色线段,X、Y分别存放 5 个起点与 5 个终点:
import numpy as np X = np.random.random((5, 2)) Y = np.random.random((5, 2)) gui = ti.GUI("lines", res=(400, 400)) while gui.running: gui.lines(begin=X, end=Y, radius=2, color=0x068587) gui.show()Circles(批量圆),绘制 50 个半径为 5 的圆,并用与pos等长的整型数组indices为每个圆从调色板中随机选色:
import numpy as np pos = np.random.random((50, 2)) # 生成 50 个随机整型元素(值为 0 或 1),分别对应: # 0 -> 0x068587 # 1 -> 0xED553B indices = np.random.randint(0, 2, size=(50,)) gui = ti.GUI("circles", res=(400, 400)) while gui.running: gui.circles(pos, radius=5, palette=[0x068587, 0xED553B, 0xEEEEF0], palette_indices=indices) gui.show()palette与palette_indices需成对使用,其校验逻辑见 python/taichi/ui/gui.py:palette_indices必须是整型数组,shape 与pos一致,取值满足0 <= indices < len(palette),最终按索引查表生成逐圆颜色数组。
Triangles(批量三角形),X、Y、Z分别存放多个三角形的三个顶点:
import numpy as np X = np.random.random((2, 2)) Y = np.random.random((2, 2)) Z = np.random.random((2, 2)) gui = ti.GUI("triangles", res=(400, 400)) while gui.running: gui.triangles(a=X, b=Y, c=Z, color=0xED553B) gui.show()Arrows(批量箭头),生成 100 个随机方向的箭头,begins为起点、direction为增量方向:
import numpy as np begins = np.random.random((100, 2)) directions = np.random.uniform(low=-0.05, high=0.05, size=(100, 2)) gui = ti.GUI('arrows', res=(400, 400)) while gui.running: gui.arrows(orig=begins, direction=directions, radius=1) gui.show()注意这里用np.random.uniform(low=..., high=...)限制了随机数的取值范围,避免箭头方向过于分散。
批量 API 在 Python 层会把 NumPy 数组转为float32连续数组后,将数据指针传给 C++ 的circles_batched/paths_batched/triangles_batched(见 taichi/ui/gui/gui.cpp)。源码中有一条重要提示:不要写成pos = int(pos.ctypes.data)直接保存指针,否则原数组会被 Python 垃圾回收导致指针失效(见 python/taichi/ui/gui.py 的注释)。
批量 API 还支持更丰富的参数形式:radius与color既可以传标量作用于全部图元,也可以传与图元数量等长的数组实现逐个定制(见circles、lines的实现)。另外point_field()与arrow_field()两个辅助方法可以按二维网格批量生成点阵与箭头场(python/taichi/ui/gui.py),适合绘制粒子系统与矢量场可视化。
事件处理
Taichi GUI 提供了一组鼠标与键盘控制方法。输入事件分为三类:
ti.GUI.RELEASE # 按键抬起或鼠标按钮抬起 ti.GUI.PRESS # 按键按下或鼠标按钮按下 ti.GUI.MOTION # 鼠标移动或滚轮滚动事件键(Event key)是你按下的键盘或鼠标按键,取值如下:
# 用于 ti.GUI.PRESS 与 ti.GUI.RELEASE 事件: ti.GUI.ESCAPE # Esc ti.GUI.SHIFT # Shift ti.GUI.LEFT # 左方向键 'a' # 字母键使用小写 'b' ... ti.GUI.LMB # 鼠标左键 ti.GUI.RMB # 鼠标右键 # 用于 ti.GUI.MOTION 事件: ti.GUI.MOVE # 鼠标移动 ti.GUI.WHEEL # 鼠标滚轮在 Python 侧常量定义见 python/taichi/ui/gui.py,还包括ALT、CTRL、RETURN、TAB、BACKSPACE、SPACE、UP、DOWN、RIGHT、CAPSLOCK、MMB、EXIT(窗口关闭)等。
事件过滤器(Event filter)是key、type与(type, key)元组的组合。get_event()从事件队列中弹出一个事件,并用过滤器进行匹配:
# ESC 被按下或抬起: gui.get_event(ti.GUI.ESCAPE) # 任意键被按下: gui.get_event(ti.GUI.PRESS) # ESC 被按下或 SPACE 被抬起: gui.get_event((ti.GUI.PRESS, ti.GUI.ESCAPE), (ti.GUI.RELEASE, ti.GUI.SPACE))gui.get_event()弹出事件后会将其保存到gui.event中,事件的各字段在GUI.Event类中定义(type、modifier、pos、key、delta,见 python/taichi/ui/gui.py)。例如:
if gui.get_event(): print('Got event, key =', gui.event.key)下面这段代码定义了一个直到按下ESC才结束的循环:
while gui.running: if gui.get_event(ti.GUI.ESCAPE): break gui.show()gui.is_pressed()用于检测当前处于按下状态的键。必须与gui.get_event()配合使用——get_event()内部会同步刷新key_pressed状态集合(见 python/taichi/ui/gui.py),不调用它is_pressed()不会更新:
while gui.running: gui.get_event() # 必须在 is_pressed 之前调用 if gui.is_pressed('a', ti.GUI.LEFT): print('Go left!') elif gui.is_pressed('d', ti.GUI.RIGHT): print('Go right!') gui.show()注意:请始终在调用
gui.is_pressed()之前调用gui.get_event(),否则is_pressed()不生效。
如果需要一次性取出多个事件,可以用生成器形式的gui.get_events(*e_filter)(见 python/taichi/ui/gui.py),widgets 示例中就用它遍历每帧所有待处理事件。
获取光标位置
gui.get_cursor_pos()返回光标在窗口内的当前位置,返回值是一对[0.0, 1.0]范围内的浮点数:
mouse_x, mouse_y = gui.get_cursor_pos()C++ 侧实现会先取原始像素坐标,再通过canvas->untransform()反变换回归一化坐标(见 taichi/ui/gui/gui.h),与绘图坐标系保持一致(原点在左下角)。
GUI Widgets:交互控件
除事件系统外,Taichi GUI 还提供了slider()、label()、button()三种控件,用于定制交互控制界面。下面这段示例创建了一个带半径滑块、X 坐标标签和 OK 按钮的窗口:
import taichi as ti gui = ti.GUI('GUI widgets') radius = gui.slider('Radius', 1, 50, step=1) xcoor = gui.label('X-coordinate') okay = gui.button('OK') xcoor.value = 0.5 radius.value = 10 while gui.running: for e in gui.get_events(gui.PRESS): if e.key == gui.ESCAPE: gui.running = False elif e.key == 'a': xcoor.value -= 0.05 elif e.key == 'd': xcoor.value += 0.05 elif e.key == 's': radius.value -= 1 elif e.key == 'w': radius.value += 1 elif e.key == okay: print('OK clicked') gui.circle((xcoor.value, 0.5), radius=radius.value) gui.show()用法要点:
gui.slider(text, minimum, maximum, step=1):创建滑块,返回WidgetValue对象,通过.value读写当前值(默认step=1,见 python/taichi/ui/gui.py);gui.label(text):创建只读文本标签,同样通过.value显示/修改数值;gui.button(text, event_name=None):创建按钮,返回关联事件名(默认WidgetButton_{text})。在事件循环中把e.key与按钮事件名比较即可捕获点击。
.value的读写最终落在 C++ 层的get_widget_value/set_widget_value(见 python/taichi/ui/gui.py)。C++ 侧对应Widget/Slider/Label/Button四个类(taichi/ui/gui/gui.h):Slider::mouse_event根据鼠标在滑轨上的水平位置换算数值,整型滑块还会加0.5取整修正;Button::mouse_event在鼠标释放(release)时触发回调;每帧redraw_widgets()会先重置为像素坐标系再重绘所有控件(taichi/ui/gui/gui.h)。控件默认尺寸为200x40,从窗口右上角向下依次排列。
补充:无窗口离线渲染
在服务器或无显示器环境下,show_gui=False配合gui.show(file)即可离线渲染并保存帧图像。show()的实现(见 python/taichi/ui/gui.py)先调用core.update()完成绘制,再通过core.screenshot(file)把画布内容写入文件;file省略时 C++ 侧会用当前时间戳自动命名 PNG(见 taichi/ui/gui/gui.h)。仓库测试test_save_image_without_window(tests/python/test_gui.py)正是用这种方式逐像素校验了u8/f32两种 dtype 的输出图像。
小结
Taichi GUI 系统以极低的入门成本提供了完整的实时可视化链路:ti.GUI()创建窗口 →set_image()渲染 field/ndarray → 单图元或批量 API 叠加几何图形 →get_event()/is_pressed()/get_cursor_pos()处理交互 → 控件 widget 构建控制面板,其中fast_gui零拷贝模式为高分辨率场景提供了 FPS 优化手段。配合 python/taichi/ui/gui.py 与 taichi/ui/gui/gui.h 的源码阅读,你可以进一步理解其软件光栅化与事件队列的底层实现。若需要更现代的 3D 场景渲染与材质能力,可继续参考仓库中docs/lang/articles/visualization/目录下的 GGUI 相关文档。
【免费下载链接】taichiProductive, portable, and performant GPU programming in Python.项目地址: https://gitcode.com/GitHub_Trending/ta/taichi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考