Inquirer EntertainmentKris Aquino puts trust in doctors amid ‘5-year prolonged war’: ‘Lumalaban ako’Punch‘No refund’ policies can’t override consumer rights, Lagos warns businessesThe Jerusalem PostIran can still produce 50 ballistic missiles a month, despite US-Israeli strikes - reportDaily MaverickWHAT’S COOKING: Chicken coconut curry with ground almonds and curry leavesInquirerHow social aid could fund green jobs and reforestationZDF heuteEntdecken Sie das ZDF-NachrichtenstudioObservador Desporto“Cavalo Selvagem Nove”: Malkovich paga o bilheteKhaosod English17-MILLION-BAHT WASTE CENTRE LEFT UNUSABLE IN PHICHITBollywood HungamaImtiaz Ali's O Saathi Re starring Aditi Rao Hydari, Avinash Tiwary and Arjun Rampal, to Premiere on Netflix on December 4Wirtualna PolskaMateriały wybuchowe na torach. Śledczy ujawnili rolę dywersantaSportstarIND vs AFG, U19 Asia Cup Live Score: Raichandani, Sehwag score fifties; IND 109/0 (15)Il Fatto QuotidianoArisa: “Ho aggiustato il ginocchio della Barbie che avevo rotto e per la psicomagia io non cado più. Io Sanremo? Stefano De Martino, mi ha già detto che non è cosa”
The Daily Newsstand · Free, Always
Thursday, November 12, 2026

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

Translate

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

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

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

Куб, описанный на React и отрендеренный через Bevy

Куб, описанный на 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 (
  <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 не переводится: между ними передаются данные.

Путь кадра от React до видео в Remotion и raea

Путь кадра от React до видео в Remotion и 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 задачу: человек заходит в комнату, садится за стол, тянется к мышке и опрокидывает кофе на ноутбук. После нескольких правок получилось так:

Сцена после исправления проходки и касания чашки

Сцена после исправления проходки и касания чашки

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

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

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

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

Один кадр в режимах Cartoon, Paint и без Look

Один кадр в режимах 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 ещё нет.

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.