Шутка «всё перепишут на Rust» добралась до мультиков. 9 ноября вышел raea, Render Anything Engine: сцены описываются в JSX, а рисует их игровой движок.
Сама идея делать анимации кодом давно работает в Remotion: React-компоненты задают содержимое кадров, браузер их отрисовывает. raea связывает тот же React с Bevy, игровым движком на Rust. У него уже есть инструменты для 3D: геометрия, материалы, освещение, камеры и анимация.
Я увидел проект через два дня после релиза и сразу полез тыкать. Привычными компонентами управлять игровым движком? Такое я пропустить не мог. Чтобы разобраться, как это устроено, проследил путь одного куба от JSX до готового изображения.
Куб, описанный на React и отрендеренный через Bevy
Пишем куб на React
Работа с кадрами знакома по Remotion. Функция useCurrentFrame() возвращает номер кадра, а по нему вычисляются высота и угол поворота куба. JSX служит разметкой сцены: компонент описывает 3D-объект, как в интерфейсе описывал бы кнопку.
Вот основная часть нашего Cube:
const frame = useCurrentFrame();const y = Math.abs(Math.sin((frame / 30) * Math.PI));const angle = (frame / 60) * Math.PI;return ( );
Формула для y задаёт прыжок, angle отвечает за вращение. Mesh3d объединяет форму куба (Cuboid), его материал (StandardMaterial) и положение в пространстве (Transform). Камера и свет тоже задаются компонентами.
На кадре 15 центр куба должен оказаться на высоте 1,5 метра, а угол поворота составить 45°. Эти числа пригодятся, чтобы проверить, что добралось до движка.
Как React добирается до изображения
При стандартном рендере Remotion открывает композицию в Chrome без видимого окна. Браузер рисует страницу, Remotion забирает каждый кадр, а FFmpeg, программа для обработки видео, собирает ролик.
raea обходится без браузера. Встроенная среда выполняет JavaScript, React вычисляет параметры объектов, а Rust-часть программы по ним строит сцену. Сам React-код в Rust не переводится: между ними передаются данные.
Путь кадра от React до видео в Remotion и raea1. JSX превращается в JavaScript
Наш файл имеет расширение TSX: это TypeScript с JSX-разметкой. Сначала raea обрабатывает его сборщиком Rolldown. Тот объединяет код с зависимостями и преобразует его в JavaScript, готовый к выполнению. Запись становится вызовом функции с параметрами. Результат можно посмотреть командой raea bundle.
Внутри Mesh3d обнаруживается обычный div. Так raea записывает описание объекта в дерево React:
function node3d(kind, props, children, extra) { return jsx("div", { "data-3d": kind, "data-props": JSON.stringify(serialisable(props)), ...extra, children, });}
Этот div служит контейнером данных, а не элементом веб-страницы. В data-3d записан тип 3D-узла, в data-props его параметры, включая положение и поворот. JSON.stringify упаковывает их в строку для передачи в Rust.
2. Свой рендерер передаёт изменения в Rust
React описывает нужные элементы, а рендерер связывает их с конкретной средой. В браузерном приложении этим занимается react-dom: создаёт и обновляет элементы страницы.
У raea свой рендерер на библиотеке react-reconciler. Он получает изменения дерева React и превращает их в команды для Rust. Пиксели на этом шаге ещё никто не рисует.
При первом кадре нужно создать узлы и связать их между собой. Дальше достаточно обновлять изменившиеся свойства, или пропсы. В исходниках этот обмен называется wire, «проводом». По нему идут команды create (создать), append (добавить дочерний узел) и update (обновить свойства).
У нашего куба меняются высота и поворот. Именно эти изменения рендерер передаст дальше.
3. Rust собирает сцену
Bevy хранит объекты по модели ECS, entity-component-system. У каждой сущности есть идентификатор и набор компонентов с данными: например, положением, геометрией и материалом. «Компонент» здесь означает набор данных, не React-функцию.
Rust-часть raea принимает команды и создаёт в Bevy сущности для узлов React. Их свойства сохраняются в компоненте ReactProps. В обработчике create это выглядит так; фрагмент сокращён:
let e = commands .spawn(( ReactNode(id("id").unwrap_or(0)), ReactKind(op["kind"].as_str().unwrap_or("").into()), )) .id();fresh_props.insert(e, p);ents.by_id.insert(id("id").unwrap_or(0), e);
Вызов spawn создаёт сущность, id() возвращает её идентификатор. Следующие строки сохраняют пропсы и связь с исходным React-узлом.
Затем функция read3d достаёт из ReactProps знакомые атрибуты data-3d и data-props. Rust получает описание нашего куба: форму, положение и поворот.
4. Bevy рисует кадр
Чтобы из описания получилась картинка, нужна геометрия. Меш (mesh) задаёт поверхность объекта вершинами и треугольниками. Вызов Mesh::from(Cuboid::new(w, h, d)) создаёт меш коробки нужных размеров.
Положение и поворот записываются в Transform объекта Bevy. В ветке обновления есть такие строки:
let Some(nt) = place(e) else { continue };if *t != nt && !driven { *t = nt;}
t здесь текущий Transform, nt новый. Строка *t = nt применяет изменение. Так значения из нашего React-компонента доходят до объекта в игровом движке.
У Bevy теперь есть всё для отрисовки: геометрия, материал, положение объекта, камера и свет. Движок рассчитывает вид сцены с выбранного ракурса и рисует кадр на видеокарте. Из последовательности таких кадров raea кодирует видео.
Проверим куб на кадре 15:
raea scene explain cube.tsx --frame 15 --json
Команда вернула границы: по высоте от 1 до 2 метров, по X и Z от −0,7071 до +0,7071. Центр оказался на высоте 1,5 метра, а размеры по горизонтали соответствуют повороту на 45°. Всё сошлось с формулами из JSX.
Разбор сделан по исходникам raea 0.1.1-rc4. Полный пример с камерой и светом:
Полный TSX-файл с кубом
import { AbsoluteFill, Composition, registerRoot, useCurrentFrame } from "raea";import { Camera3d } from "raea/camera";import { DirectionalLight } from "raea/light";import { Cuboid, Mesh3d } from "raea/mesh";import { StandardMaterial } from "raea/pbr";import { Quat, Transform } from "raea/transform";function Cube() { const frame = useCurrentFrame(); const y = Math.abs(Math.sin((frame / 30) * Math.PI)); // прыжок раз в секунду const angle = (frame / 60) * Math.PI; // пол-оборота в секунду return ( );}registerRoot(() => ( ));
Теперь вместо куба человек и кофе
На кубе удобно разбирать устройство движка. Сложные сцены уже хочется поручать AI-агенту: он пишет React, запускает рендер и правит результат по замечаниям. Вручную расписывать комнату и движения персонажа я бы не стал, такого запаса терпения у меня нет.
Поэтому дал Claude Code задачу: человек заходит в комнату, садится за стол, тянется к мышке и опрокидывает кофе на ноутбук. После нескольких правок получилось так:
Сцена после исправления проходки и касания чашки
Сценой управляет один TSX-файл, собранный агентом. Blender я не открывал, Rust не писал. Для человека и жидкости в raea есть дополнительные инструменты.
Модель человека называется Anny. Движения взяты из базы CMU: это мокап, записи движений людей. В коде задаются маршрут до стула и посадка, а motion matching подбирает подходящие кадры из библиотеки. Для руки можно отдельно указать цель. Обратная кинематика, IK, подбирает положение суставов, чтобы ладонь до неё дотянулась.
Кофе задан компонентом . Он подключает симуляцию жидкости SPlisHSPlasH: raea заранее рассчитывает её и воспроизводит по кадрам. Стол и ноутбук участвуют в столкновениях с жидкостью, а траектория кружки берётся из кода сцены.
Внешний вид тоже переключается компонентом: добавляет мультяшный контур, paint даёт плоскую иллюстрацию. Один кадр с разными настройками:
Один кадр в режимах Cartoon, Paint и без Look
Человек ехал, а чашка падала сама
В первой версии персонаж добирался до стола, почти не переставляя ноги. Выяснилось, что perform, функция подбора и соединения движений, выбирала кадры стойки. Подтяжка к маршруту всё равно тащила тело вперёд. Получилась ходьба на конвейере.
Разделили клипы ходьбы и стойки, добавили поворот и остановку из CMU, настроили подбор и уменьшили подтяжку. Вот первые три секунды до и после:
Слева персонаж скользит к столу, справа делает шаги и останавливается перед посадкой
Следом обнаружилась чашка, которая падала раньше, чем до неё дотягивалась рука. Пришлось добавить наклон корпуса и подвести кисть к ручке. Затем поправить момент движения: в промежуточной версии человек выглядел так, будто решил утопить ноутбук намеренно.
Удар рукой здесь не рассчитывается физикой. Опрокидывание кружки задано ключевыми кадрами и согласовано с движением кисти. Жидкость уже следует за кружкой по результатам симуляции.
На неподвижных кадрах такие ошибки легко пропустить. Агенту пришлось возвращаться к коду после просмотра ролика: успешный рендер ещё не означает, что человек нормально дошёл до стула.
На чём это запускать
Сцену с кофе рендерил на MacBook Pro с M3 Pro. Получился ролик 1920×1080, 30 fps, чуть больше девяти секунд.
Пробовал и Linux без GPU, с программным Vulkan. Простой куб там считался примерно по 0,18 секунды на кадр. Один кадр студийной сцены с тенями вместе с запуском занял уже 28 секунд, поэтому сложный ролик делал на маке.
Для подготовки тела и переноса движений понадобились Python, bun и инструменты из исходников raea. Это опыт с версией 0.1.1-rc4: проект только вышел и быстро меняется. Для коммерческого использования стоит отдельно прочитать условия FSL-1.1.
А дальше можно дать зрителю управление
У авторов в плане экспорт в браузер. В исходниках уже есть работа над WebAssembly-сборкой, вариантом движка для запуска на веб-странице. Готовой команды экспорта в 0.1.1-rc4 ещё нет.
Если её доведут, сцену с кофе можно будет превратить в маленькую игру: дать зрителю пройти к столу, потянуться к мышке и попробовать не залить ноутбук. Заодно проверим, можно ли вообще сесть за этот стол без происшествий.
Источник: habr.com