Cables.gl: нодовий граф від нуля до експорту
Браузерний редактор нодового графа, що генерує WebGL-сцену в реальному часі. Урок для того, хто вже бачив ноди (TouchDesigner, shader-графи, Blender geometry nodes) і вміє читати WebGL/three.js-логіку, але в cables руками ще не збирав патч і не доводив його до експортованої сторінки.
Передумови: уявлення про canvas/WebGL і про те, що таке нода з входами-виходами; решта пояснюється на місці.
§Термінологія наперед
Нижче — рівно ті слова, що зустрічаються далі в тексті. Кожне: одне речення суті плюс англійський оригінал, як воно підписане в UI.
Ops.Graphics.Meshes.Circle_v4; у пошуку набирається коротка частина. namespaceEsc; головний спосіб знайти потрібний вузол. op search1Що це і коли брати
cables — це редактор, де замість коду пишеться граф: коробочки-ops із портами, з'єднані кабелями, а на виході — звичайна веб-сторінка з canvas. Розробляє undev (Берлін); рушій і бібліотека ops відкриті під MIT, реєстрація і патчинг безкоштовні, ліцензію купувати не треба.
Чому це окремий інструмент, а не просто «three.js мишкою»: граф дає миттєвий зворотний зв'язок і прибирає цілий клас рутини (компіляція, boilerplate сцени, ручний рендер-цикл), тому чорновий візуал збирається за хвилини. Ціна — менше контролю над архітектурою, ніж у коді.
Як це працює на рівні продукту
Патч живе в акаунті за URL, рендериться в реальному часі просто в редакторі, і будь-якої миті експортується в статичну теку. Усе, що вміє браузер, доступне з коробки: 3D-меші, пост-обробка, відео- й вебкам-текстури, WebAudio-аналіз, MIDI, DOM поверх сцени.
Приклад доречності: аудіо-реактивний фон для лендінга чи кліпу збирається графом швидше, ніж пишеться руками. Типова помилка вибору: тягнути в cables повноцінний застосунок зі станом, роутингом і тестами — це візуальний рушій сцени, а не фреймворк для аплікух; логіку застосунку туди пхати не варто.
MainLoop, а не тягне дані з кінця ланцюга. Саме через цю різницю в перший день «нічого не малюється».
2Модель: patch → op → port → cable
Уся робота в cables — чотири сутності, вкладені одна в одну. Зрозуміти їхній стосунок важливіше, ніж вивчити будь-який конкретний op: далі все зводиться до цих чотирьох.
Чотири рівні
- Patch — документ. Автозберігається, має URL, буває публічним чи приватним, ділиться в команду.
- Op — вузол. Кожен має ім'я в namespace-стилі (
Ops.Graphics.Meshes.Circle_v4); у пошуку набирається коротка назва. - Port — вхід або вихід op-а. Типізований: number, string, boolean, array, object, texture і окремо trigger.
- Cable — з'єднання порт→порт, забарвлене за типом. Тому «кабель одного типу в порт іншого» просто не приєднається.
Ключова теза, яку варто прийняти одразу: trigger-порт — це не дані, це виклик. MainLoop раз на кадр смикає свій trigger, той смикає наступний op, той — наступний. Усе, що не підвішене до цього ланцюга, не виконується взагалі.
Інтерактив: збери trigger-ланцюг
Перемикай з'єднання і дивись прев'ю праворуч. Обірваний жовтий ланцюг = порожній екран — та сама логіка, через яку в реальному патчі «нічого не малюється».
Типова помилка: новачок під'єднує data-кабель (наприклад, Time у rotation) і чекає картинку, забувши, що сам меш не підвішений до trigger-ланцюга. Дані течуть, виклику нема — екран порожній.
3Порти й типи: що куди підключається
Колір кабеля кодує тип, і це не косметика — з'єднати різнотипні порти система не дасть. Знання, який порт що несе, економить більшість часу дебагу: половина «не працює» — це кабель не того типу або не в той порт.
Таблиця типів портів
| Тип порту | Що несе | Куди типово йде |
|---|---|---|
| Trigger | виклик виконання (не значення) | trigger-вхід наступного op; корінь — MainLoop |
| Number / Float | одне число | rotation, scale, колірний канал, індекс, uniform |
| String | текст | шлях до файлу, назва, ім'я uniform-а в шейдері |
| Boolean | true/false | вмикач гілки, прапорець матеріалу |
| Array | масив чисел або об'єктів | позиції інстансів, спектр з аудіо-аналізу |
| Object | складений об'єкт | geometry, material, shader |
| Texture | об'єктний порт із текстурою | вхід матеріалу, пост-ефекту, наступного рендера (≈ TOP) |
Приклад і типова помилка
Реактив будується так: number-вихід аудіо-аналізу заводиться в number-порт scale або rotation трансформа. Типова помилка — тягнути object/texture-кабель у number-порт (не приєднається) або чекати, що boolean «сам» стане числом. Різнотипні порти конвертуються тільки через явний op-конвертер.
4Перший патч за 5 хвилин
Мінімальний робочий патч — MainLoop, меш і трансформ на одному trigger-ланцюзі. Але ланцюг збирається не додаванням ops поруч, а перетягуванням кабеля з жовтого output-порту. Це та деталь, на якій старий варіант уроку давав порожній екран.
Крок за кроком, як воно є в живому редакторі:
- Логін на cables.gl → New Patch. Канвас порожній, по центру напис «This patch is empty. press escape to add operators»; preview — прозорий шаховий фон,
?? FPS. Esc→ op-search → набратиMain→ MainLoop (Ops.Gl.MainLoop_v2, «Trigger other ops once every frame») → Add. Корінь рендер-дерева стоїть сам і ще нічого не малює.- Ключовий крок. Навести на жовтий trigger-output MainLoop — окремий жовтий квадратик у нижньому-лівому куті коробки — і потягнути з нього кабель у порожнє місце. З точки, де кабель відпущено, одразу відкривається op-search. Кабель не з'являється сам: його тягне рука.
- У цьому op-search набрати
circle→ Circle (Ops.Graphics.Meshes.Circle_v4, «Draws a circle») → Add. Op лягає вже під'єднаним: жовтий trigger MainLoop заходить у вхід Circle, підписанийRender. У preview — сірий диск на чорному. - Вставити
Transformу розрив: клікнути по кабелю MainLoop→Circle і в op-search додати Transform — він сідає між ними, кабель стає MainLoop→Transform→Circle. Esc→Time(абоSin) → завести number-вихід у number-портrotationтрансформа — коло обертається.mod + s— зберегти. Статус-тост знизу справа: «Patch saved (N ops / …kb)».
Типова помилка — саме та, що дає порожній екран: додати Circle через [esc] і чекати картинку. Op лягає на канвас, але без вхідного trigger-кабеля він не під деревом виконання MainLoop — preview лишається порожнім, навіть коли обидва op стоять поруч. З'єднання MainLoop→меш ставиться виключно перетягуванням з жовтого output-порту (крок 3), а не автоматично в момент додавання.
Чому важливо: це весь принцип у мініатюрі. Далі той самий ланцюг, тільки замість Circle — меш із геометрії й матеріалу, а замість Time — аудіо, миша чи змінна. Але кожна нова ланка чіпляється до trigger-дерева тим самим рухом — drag з жовтого порту.
5UI, op-search і хоткеї
Швидкість у cables — це хоткеї плюс op-search; миша по меню програє на порядок. [mod] тут — Cmd на макі, Ctrl на вінді/лінуксі.
Найкорисніші хоткеї
| Клавіша | Дія |
|---|---|
Esc | додати op / закрити діалог op-search |
mod + s | зберегти патч (або файл у редакторі коду) |
mod + shift + s | зберегти як новий патч |
mod + n | новий патч |
space | play/pause таймлайну |
space + drag | панорамування полотна |
mod + c / mod + v / del | копі / паст / видалити виділені ops |
d | вимкнути op, не видаляючи його |
a | Align selected ops — вирівняти виділені ops |
shift + a | Compress selected ops vertically — стиснути виділені ops вертикально (не «вирівняти по горизонталі») |
f | Toggle flow visualization — не пошук: пошуку ops за тегом unconnected хоткеєм mod+f у вкладці Keyboard Shortcuts нема, ⚠ПЕРЕВІР реальний виклик у редакторі |
e / t / x / y / c / u | Edit op code / Set Title / Unlink selected ops / Cut Cables / Center Selected Ops / Goto parent subpatch |
Дрібниці UI, що економлять час
Клік по op-у відкриває панель параметрів; лівий клік по output-полю копіює значення в буфер (зручно вигрібати згенеровані масиви). Виділені ops → Create Area робить кольорову групу; Alt-drag тягне area без вмісту. Правий клік по кінцю кабеля з Shift+Alt перекидає з'єднання на інший порт.
mod+s / mod+shift+s / mod+n у видимій частині вкладки (секція Global) цим кадром не підтверджені; звірятися треба саме там, а не гадати.
d глушить гілку, не видаляючи її.6Порядок виконання: trigger vs data
Дев'ять із десяти «нічого не рендериться» — це порядок виконання, а не битий op. Модель проста, але контрінтуїтивна для тих, хто звик до pull-графів (TouchDesigner, шейдер-ноди), де дані тягнуться з кінця. Тут — push згори вниз.
Trigger проти data — у чому різниця
| Аспект | Trigger-порт | Data-порт |
|---|---|---|
| Несе | виклик: «виконайся зараз» | значення: «ось число/об'єкт» |
| Колір кабеля | окремий (жовтий) | за типом даних |
| Задає послідовність | так — визначає, що коли виконується | ні — просто читається в момент виклику |
| Без з'єднання | op взагалі не виконується | порт бере дефолтне значення |
| Коли читається | щоразу, як прийшов виклик | у момент виклику власника порту |
Правила, що з цього випливають
- Малює лише те, що висить під
MainLoopчерез trigger-ланцюг. - Порядок гілок — зверху вниз по позиції на полотні. Так робиться порядок відмальовки: фон → об'єкти → пост.
- Матеріал/шейдер вмикається до меша: ланцюг material ▸ mesh, а не навпаки.
- Data-порт читається в момент виклику. Якщо джерело не оновлюється щокадру — значення застигне.
Debug-прийом і типова помилка: тимчасово d (disable) на половині гілок і видно, що зникло. Логіка «знайти висячі ops без trigger-кабеля» — найчастіша причина порожнього екрану, але хоткей mod+F → тег unconnected зі старого тексту не підтверджений: ⚠ПЕРЕВІР реальний спосіб виклику цього пошуку в UI.
Тренажер: порти, ops і порядок виконання
Сім питань по моделі виконання й портах. Помилка — правильна відповідь підсвітиться зеленим.
7Файли, CORS і аудіо-реактив
Ассети й зовнішній звук — місце, де патч найчастіше «мовчки ламається» без помилки на екрані. Причина майже завжди одна з двох: CORS або відсутність кліку користувача перед стартом звуку.
Files і зовнішні URL
- Локальні ассети (картинки, звук, 3D) — вкладка Files у патчі. Заливаються туди й вибираються у порті op-а.
- Зовнішній файл по URL працює тільки, якщо сервер віддає заголовок
Access-Control-Allow-Origin. Без нього текстура буде чорна, а в консолі — типова WebGL-помилка. Для власної статики це один рядок у nginx.
Аудіо-реактив
WebAudio-ops дають спектр і гучність як звичайні number/array-порти. Реактив = завести цей number у масштаб, колір чи зсув. Типова помилка: браузер не стартує звук без жесту користувача, тому в експорті завжди потрібна кнопка старту — інакше на чужій сторінці буде тиша й порожній аналіз, і це виглядає як «зламаний патч».
8Свої ops, subpatch і змінні
Коли граф починає розповзатися, рятують три речі: власний op, subpatch і змінні. Усі троє про одне — прибрати дублювання й довгі кабелі через пів-полотна.
Власний op
Op — це JS-файл із оголошеними портами:
const inVal = op.inFloat("value", 0);
const outVal = op.outNumber("result");
inVal.onChange = () => { outVal.set(inVal.get() * 2); };
- Входи:
op.inFloat,op.inString,op.inBool,op.inTrigger,op.inObject,op.inArray; виходи симетрично —op.outNumber,op.outTrigger,op.outObject. - GLSL живе в attachments op-а. Для дебагу шейдера є ShaderInfo — показує, що реально скомпілювалось.
Subpatch і змінні
SubPatchOp згортає шматок графа в один вузол — доречно, коли той самий ланцюг копіпаститься втретє. Variables дають іменоване значення, доступне з будь-якої точки без кабеля: зручно для глобальних параметрів (гучність, тема, час), які інакше тяглися б кабелем через увесь патч.
op.inFloat/op.outNumber та назви (наприклад, inValueFloat у частині версій) варто звірити з чинним API на docs.cables.gl — назви методів портів між великими версіями рушія мінялися.9Перформанс і дебаг
Просідання fps у cables майже завжди діагностується двома вбудованими ops — тому здогадуватися не треба, треба виміряти. Спершу цифри, потім оптимізація.
Два вимірювачі
- Performance op — таймінги по гілках прямо в патчі: видно, який вузол їсть кадр.
- Profiler — скільки разів який op викликано за кадр. Число в тисячах = цикл там, де мав бути один виклик.
Типові помилки → симптом → лікування
| Помилка | Симптом | Лікування |
|---|---|---|
| Op не під trigger-ланцюгом | нічого не малюється | під'єднати trigger; mod+F → unconnected |
| Матеріал після меша | меш білий / без текстури | порядок material ▸ mesh, матеріал вище по гілці |
| Зовнішній URL без CORS | чорна текстура, помилка в консолі | заголовок Access-Control-Allow-Origin або залити у Files |
| Звук без кнопки старту | тиша, аналіз = 0 | кнопка старту (жест користувача) в експорті |
| Масив перебудовується щокадру | fps просідає, Profiler у тисячах | будувати масив раз, кешувати; інстансинг замість десятків мешів |
Експорт відкрито по file:// | порожня сторінка | підняти HTTP-сервер, відкрити через localhost |
Мобілка окремо: WebGL1-фолбек, обмежена пам'ять текстур і жодного автоплею звуку. Перевіряти на телефоні треба до експорту, а не після.
10Експорт і вбудовування
Patch → Export дає zip: JS-рушій, JSON патча і ассети — тобто готову статичну сторінку. Але «експорт» — це чотири різні шляхи з різною ціною, і вибір між ними визначає, як патч потім оновлювати.
Матриця експорту
| Спосіб | Що дає | Чого коштує |
|---|---|---|
| Patch Export (zip) | повна самодостатня сторінка: рушій + JSON + ассети | важить більше; оновлення = переекспорт і перезаливка |
| Export without files | той самий zip без ассетів — вони роздаються з власного CDN | треба самому стежити за шляхами й CORS ассетів |
| iframe embed | найшвидше в чужу CMS / лендінг | окремий контекст; керувати сценою ззовні складніше |
| cables-cli | експорт зі скрипта в CI / деплой без кліків | потрібен standalone-білд і токен; GUI-майстра нема |
Як віддати сторінку
- Розпакувати zip у теку на своєму хостингу.
- Відкрити
index.html— це вже готова сторінка з canvas. - Далі або віддати як є, або смикати патч зі своєї сторінки: cables API дає доступ до змінних і зовнішніх тригерів, тобто власний JS міняє параметри сцени на льоту.
Патч-експорт (JSON графа) можна імпортнути назад через ZIP-Upload у «My Data» — це резервна копія самого графа, не сторінки.
file:// експорт не запуститься — рушій вантажить ассети по HTTP. Треба підняти python3 -m http.server у теці й дивитися на localhost. Наявність саме готового index.html у корені zip варто підтвердити на чинній версії експортера — структуру архіву cables оновлював.11Standalone і CLI
Для роботи без хмари й для автоматизації є два окремі інструменти поза браузерним редактором. Обидва беруть той самий патч, але знімають різні обмеження.
- cables standalone (Electron, mac/win/linux) — той самий редактор офлайн, файли з диска без завантаження в хмару. Пісочниці браузера тут нема: більше можливостей і більше відповідальності за те, що робить патч.
- cables-cli — експорт патча з командного рядка. Це те, що вставляється в деплой-скрипт: експорт → rsync у веб-корінь → готово, без ручного клацання zip.
Приклад: нічний білд візуалу для сайту — cables-cli експортує патч, скрипт кладе результат у веб-корінь, сторінка оновилась без відкриття редактора. Типова помилка: чекати від standalone тих самих шляхів до ассетів, що й у хмарі — офлайн вони резолвляться від диска, і забуті відносні шляхи ламають експорт.
12Приватність і шеринг патча
Патч за замовчуванням прив'язаний до акаунта, але видимість і право редагування налаштовуються окремо — це варто зрозуміти до того, як ділитися посиланням. Публічний патч видно й копіювати іншим, приватний — ні.
Ділитися можна лінком (перегляд/копія) або запросити в команду на редагування. Типова помилка: дати публічний лінк на патч із приватними ассетами чи ключами в тексті op-а — усе, що в патчі, їде разом із ним.
13Частинки на GPU: emit → update → render
Частинки в cables — це не ще один op у trigger-ланцюзі, який «віддає число далі». Це окрема модель, де між кадрами живе буфер стану тисяч частинок, і кожен кадр цей буфер спершу поповнюється, потім просувається, потім малюється одним проходом. Три фази:
- emit — спавнить нові частинки: задає стартові
position,velocity, скидаєlifetime. Джерело емісії (точка, форма, темп) — параметр цієї фази. - update — крок симуляції для всього буфера одразу:
position += velocity * dt,lifetime -= dt, за потреби гравітація/шум наvelocity, а вичерпані частинки гасяться або respawn-яться. Мутація йде на місці, в тому ж буфері. - render — намалювати весь буфер за один draw call (points або instanced-спрайти), читаючи
position/size/colorз буфера, а не з окремих портів.
Чому це не звичайний op-ланцюг. У звичайному ланцюгу порт несе одне миттєве значення, яке читається в момент виклику trigger-а і забувається. Тут стан не миттєвий: буфер лежить у GPU-пам'яті (текстура або VBO) і накопичується між кадрами — update бачить те, що emit/update записали раніше. Op не має «одного числа на виході»; він тримає масив на частинку, який еволюціонує в часі.
Атрибути частинки
| Атрибут | Тип | Сенс |
|---|---|---|
position | vec2 / vec3 | де частинка зараз |
velocity | vec2 / vec3 | напрям і швидкість руху за крок симуляції |
lifetime / age | float | скільки лишилось жити; на нулі — respawn або зникнення |
size | float | розмір точки / спрайта при render |
color / seed | vec4 / float | колір або зерно рандому на конкретну частинку |
Приклад використання: іскри/дим/поле точок під музику — emit сипле частинки з темпом, прив'язаним до number-виходу аудіо-аналізу; update штовхає їх velocity + шум і гасить по lifetime; render малює як спрайти, а size/color беруться з буфера. Реактив тут заходить у параметр фази (темп емісії, сила поля), а не в атрибут окремої частинки.
Типова помилка. Трактувати атрибут як миттєве число — завести один Number у «velocity» і чекати, що симуляція оживе. Атрибут — це буфер на частинку; одне значення в порт або перезадає весь буфер константою (усі частинки однакові, рух вироджується в один вектор), або взагалі не туди. Вплинути на стан треба через фазу update (сила, шум, dt), а не підстановкою скаляра.
Ops.Extension.GlParticles — один op чи набір. Живого кадру частинок серед скріншотів нема, конкретику UI треба вивірити прямо в редакторі.§Use cases
Аудіо-реактивний візуал під рейв / DJ-сет
Вбудований патч як фон сторінки / лендінга
index.html віддається як фон під контент. Через cables API власний JS сторінки міняє параметри сцени (тема, швидкість, реакція на скрол) — сцена перестає бути статичною картинкою і реагує на поведінку відвідувача.Живий VJ-сет із патча в редакторі
space) і Variables дозволяють тримати кілька станів сцени й перемикатися між ними наживо.Обмін патчем і рев'ю в команді
Прототип шейдера / пост-ефекту без boilerplate
Нічний білд візуалу через CLI
§Практика й інструменти
Офіційна документація — найточніше джерело по ops, портах, експорту й API. Перше, куди йти, коли UI виглядає інакше, ніж в уроці (cables оновлюється часто).
Галерея публічних патчів. Будь-який відкривається в редакторі й копіюється собі — найшвидший спосіб розібрати, як зроблений чужий ефект.
Сторінка завантаження офлайн-редактора (Electron, mac/win/linux) — коли треба працювати з файлами з диска без хмари.
Живі відповіді від undev і користувачів — сюди з edge-кейсами по портах, шейдерах і експорту, які не покриті доками.
Репозиторій рушія й бібліотеки ops під MIT. Корисно, щоб глянути реальну реалізацію op-а, коли поведінка неочевидна.
Дев-збірка для standalone і cables-cli — звідси піднімається локальна інсталяція й автоматизований експорт.
§Що далі
- Написати перший власний op — JS-файл із
op.inFloat/op.outNumber, який робить те, чого нема в стандартній бібліотеці; - GLSL у attachments + ShaderInfo — власний фрагментний шейдер як пост-ефект на повний екран;
- cables API зі своєї сторінки — керувати змінними патча й слати зовнішні тригери з JS хост-сторінки;
- cables-cli у деплой-скрипт — автоматизований експорт замість ручного zip;
- Instancing і Profiler — переписати десятки мешів на один інстансований і звірити виграш по кадру.