TOPZNANIE

Цифровая практика без сложностей

Выберите город
MET Norway · CC BY 4.0
← На главную

· Общий обзор

Пишешь JSX, рисует Rust: от куба до мультика на raea

Шутка «всё перепишут на Rust» добралась до мультиков. 9 ноября вышел raea, Render Anything Engine: сцены описываются в JSX, а рисует их игровой движок.

Сама идея делать анимации кодом давно работает в Remotion: React-компоненты задают содержимое кадров, браузер их отрисовывает. raea связывает тот же React с Bevy, игровым движком на Rust. У него уже есть инструменты для 3D: геометрия, материалы, освещение, камеры и анимация.

Я увидел проект через два дня после релиза и сразу полез тыкать. Привычными компонентами управлять игровым движком? Такое я пропустить не мог. Чтобы разобраться, как это устроено, проследил путь одного куба от JSX до готового изображения.

Пишем куб на 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 ( <Mesh3d name=»cube» transform={Transform.fromXyz(0, 0.5 + y, 0).withRotation( Quat.fromEuler(«XYZ», 0, angle, 0), )} > <Cuboid half_size={[0.5, 0.5, 0.5]} /> <StandardMaterial base_color=»#c4f82e» /> </Mesh3d> );

Формула для y задаёт прыжок, angle отвечает за вращение. Mesh3d объединяет форму куба (Cuboid), его материал (StandardMaterial) и положение в пространстве (Transform). Камера и свет тоже задаются компонентами.

На кадре 15 центр куба должен оказаться на высоте 1,5 метра, а угол поворота составить 45°. Эти числа пригодятся, чтобы проверить, что добралось до движка.

Как React добирается до изображения

При стандартном рендере Remotion открывает композицию в Chrome без видимого окна. Браузер рисует страницу, Remotion забирает каждый кадр, а FFmpeg, программа для обработки видео, собирает ролик.

raea обходится без браузера. Встроенная среда выполняет JavaScript, React вычисляет параметры объектов, а Rust-часть программы по ним строит сцену. Сам React-код в Rust не переводится: между ними передаются данные.

Пишешь JSX, рисует Rust: от куба до мультика на raea

1. JSX превращается в JavaScript

Наш файл имеет расширение TSX: это TypeScript с JSX-разметкой. Сначала raea обрабатывает его сборщиком Rolldown. Тот объединяет код с зависимостями и преобразует его в JavaScript, готовый к выполнению. Запись <Mesh3d …> становится вызовом функции с параметрами. Результат можно посмотреть командой 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 ( <AbsoluteFill style={{ background: «#f1f1ef» }}> <Camera3d transform={Transform.fromXyz(0, 1.5, 5).lookingAt([0, 0.6, 0])} /> <DirectionalLight transform={Transform.fromXyz(2, 4, 3).lookingAt([0, 0, 0])} illuminance={10000} /> <Mesh3d name=»cube» transform={Transform.fromXyz(0, 0.5 + y, 0).withRotation( Quat.fromEuler(«XYZ», 0, angle, 0), )} > <Cuboid half_size={[0.5, 0.5, 0.5]} /> <StandardMaterial base_color=»#c4f82e» /> </Mesh3d> </AbsoluteFill> ); } registerRoot(() => ( <Composition id=»cube» component={Cube} width={1080} height={1080} fps={30} durationInFrames={60} /> ));

Теперь вместо куба человек и кофе

На кубе удобно разбирать устройство движка. Сложные сцены уже хочется поручать AI-агенту: он пишет React, запускает рендер и правит результат по замечаниям. Вручную расписывать комнату и движения персонажа я бы не стал, такого запаса терпения у меня нет.

Поэтому дал Claude Code задачу: человек заходит в комнату, садится за стол, тянется к мышке и опрокидывает кофе на ноутбук. После нескольких правок получилось так:

Пишешь JSX, рисует Rust: от куба до мультика на raea

Сценой управляет один TSX-файл, собранный агентом. Blender я не открывал, Rust не писал. Для человека и жидкости в raea есть дополнительные инструменты.

Модель человека называется Anny. Движения взяты из базы CMU: это мокап, записи движений людей. В коде задаются маршрут до стула и посадка, а motion matching подбирает подходящие кадры из библиотеки. Для руки можно отдельно указать цель. Обратная кинематика, IK, подбирает положение суставов, чтобы ладонь до неё дотянулась.

Кофе задан компонентом <Goo solver=»sph»>. Он подключает симуляцию жидкости SPlisHSPlasH: raea заранее рассчитывает её и воспроизводит по кадрам. Стол и ноутбук участвуют в столкновениях с жидкостью, а траектория кружки берётся из кода сцены.

Внешний вид тоже переключается компонентом: <Look mode=»cartoon» /> добавляет мультяшный контур, paint даёт плоскую иллюстрацию. Один кадр с разными настройками:

Пишешь JSX, рисует Rust: от куба до мультика на raea

Человек ехал, а чашка падала сама

В первой версии персонаж добирался до стола, почти не переставляя ноги. Выяснилось, что perform, функция подбора и соединения движений, выбирала кадры стойки. Подтяжка к маршруту всё равно тащила тело вперёд. Получилась ходьба на конвейере.

Разделили клипы ходьбы и стойки, добавили поворот и остановку из CMU, настроили подбор и уменьшили подтяжку. Вот первые три секунды до и после:

Пишешь JSX, рисует Rust: от куба до мультика на raea

Следом обнаружилась чашка, которая падала раньше, чем до неё дотягивалась рука. Пришлось добавить наклон корпуса и подвести кисть к ручке. Затем поправить момент движения: в промежуточной версии человек выглядел так, будто решил утопить ноутбук намеренно.

Удар рукой здесь не рассчитывается физикой. Опрокидывание кружки задано ключевыми кадрами и согласовано с движением кисти. Жидкость уже следует за кружкой по результатам симуляции.

На неподвижных кадрах такие ошибки легко пропустить. Агенту пришлось возвращаться к коду после просмотра ролика: успешный рендер ещё не означает, что человек нормально дошёл до стула.

На чём это запускать

Сцену с кофе рендерил на 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 ещё нет.

Если её доведут, сцену с кофе можно будет превратить в маленькую игру: дать зрителю пройти к столу, потянуться к мышке и попробовать не залить ноутбук. Заодно проверим, можно ли вообще сесть за этот стол без происшествий.

Теги:

  • raea
  • React
  • Bevy
  • Rust
  • Remotion
  • Three.js
  • Claude Code
  • 3D-анимация
  • typescript

Хабы:

  • ReactJS
  • Rust
  • 3D-графика
  • Программирование
  • Искусственный интеллект