Python в TouchDesigner
Python 3.11 и модуль td: где писать код, как достать узел, параметр и канал, как создать узел, отложить действие и сохранить состояние. И почему Python на каждом кадре роняет кадры.
Внутри TouchDesigner свой Python — в ветке 2025.30000 это 3.11, в сборке
2025.33230 точнее 3.11.15 — и модуль td со всей сетью. Импортировать
его не нужно: op, me, parent, absTime, project доступны в любом
скрипте и выражении.
| Место | Для чего |
|---|---|
Textport (Alt+T) |
Проверить строчку, посмотреть ошибку |
| Text DAT | Модуль с функциями, вызываемыми откуда угодно — mod.имя_dat.функция() |
| Script DAT / CHOP / SOP / TOP / POP | Оператор, содержимое которого считает Python |
| Колбэки (Execute DAT и другие) | Реакция на события — Скрипты и колбэки |
| Параметр в режиме Expression | Однострочные выражения — Ссылки |
| Extensions | Классы, привязанные к компоненту — Extensions |
Текст из Text DAT целиком запускают через RMB → Run Script.
op('noise1') # оператор или None, если его нет
opex('noise1') # то же, но с ошибкой, если его нет
op('noise1').par.period = 4 # параметр
op('noise1').par.period.expr = "absTime.seconds" # выражение
op('constant1')['chan1'] # канал CHOP
op('table1')[0, 'name'] # ячейка DAT
op('geo1').render = False # флаг
ops('render*') # список по шаблону
parent() # родительский компонент
me # сам оператор
project.folder # папка проекта
absTime.seconds # время с запуска
opex() из ветки 2023.10000 сразу сообщает, что узла нет, вместо того
чтобы вернуть None и уронить код строчкой ниже.
n = parent().create(noiseTOP, 'noise_new')
n.nodeX, n.nodeY = 200, 0
n.par.period = 3
op('noise_old').destroy()
Позже и надолго
run("op('a').par.Reset.pulse()", delayFrames=1)
run("op('player').par.play = 1", delayMilliSeconds=500)
run с задержкой выполняет код после текущего кука, через нужное число
кадров или миллисекунд, и это исправляет много странных ошибок вроде правки
сети посреди кука — Отложенный запуск и потоки.
Состояние держат не в глобальных переменных модуля, а в хранилище оператора:
me.store('counter', 0) # записать
me.fetch('counter', 0) # прочитать, с значением по умолчанию
Хранилище сохраняется в .toe и .tox и возвращается при открытии; если
на старте нужно начальное значение, его заводят через storeStartupValue().
Подробно — Состояние и хранилище.
onFrameStart в Execute DAT, где перебирается сеть, — прямой путь к падению
кадров. Python работает в главном потоке, и пока он считает, не считается
ничего.
Значения передают CHOP и экспортом, на изменения отвечают колбэки, а не опрос, тяжёлое уносят в отдельный поток через Thread Manager или в процесс (Engine COMP), а сеть обходят один раз при старте и кладут результат в таблицу. Derivative советует то же: многие куски Python заменяет маленькая сеть CHOP или DAT, которая на порядок быстрее.
Отладка
print() пишет в Textport, debug() — туда же, но с именем оператора
и номером строки. Ошибки видны в Textport и в Error DAT, который удобно
вывести на служебную панель площадки, а op('...').errors()
и .warnings() дают их программно — на этом держится
мониторинг. Время кусков кода
меряют time.perf_counter().
Нашли в статье ошибку, устаревшее или непонятное место?