Сайт: legendacr.github.io/size‑of‑space
Код
: github.com/LegendaCR/size‑of‑space (MIT)

Помните Size of Life от Нила Агарвала? Листаешь вправо, и рядом с каждым существом появляется следующее, побольше. Мне захотелось такого же, но про космос: начать с крошечного астероида и пролистать всё до самой границы того, что мы вообще можем увидеть.

Получилось 79 объектов и 24 порядка величины, от астероида Бенну диаметром 490 метров до наблюдаемой Вселенной в 93 миллиарда световых лет. Всё рисуется в реальном времени прямо в браузере, а под капотом есть трассировка лучей, ray marching, реальные карты поверхностей от NASA и генеративная музыка. Ниже расскажу, как это устроено и на какие грабли я наступил.

Полёт по Солнечной системе и сравнения
Неспешный тур по Солнечной системе и три сравнения

Что умеет сайт

  • Путешествие по масштабу. Листаешь колесом, свайпом или стрелками, и камера плавно отъезжает: астероиды → спутники → планеты → звёзды → чёрные дыры → туманности → галактики → сверхскопления → вся наблюдаемая Вселенная.

  • Карточка у каждого объекта: диаметр, во сколько раз он больше Земли и предыдущего объекта, расстояние до нас и интересный факт.

  • Режим сравнения. Выбираешь любые два объекта, и они встают рядом в реальном масштабе. Если меньший получается мельче пикселя, его место отмечается точкой, а рядом появляется лупа с увеличенной копией.

  • Музыка, которая растёт вместе с масштабом: чем дальше улетаешь, тем больше инструментов вступает.

  • Ссылка на любой объект и любое сравнение, например Солнце против Стивенсона 2–18.

Солнце рядом с Стивенсоном 2-18
Солнце рядом со звездой Стивенсон 2–18: разница в 2 150 раз по диаметру, Солнце превращается в точку

Проблема № 1: 24 порядка величины и точность float

Первое, во что упираешься: GPU считает во float32, а это около 7 значащих цифр. Положить в одну сцену астероид в 490 метров и Вселенную в 8,8·10²⁶ метров нельзя: астероид просто исчезнет в погрешности.

Поэтому реальные координаты объектов хранятся в JavaScript (там 64-битные числа), а в шейдеры уходят уже готовые экранные координаты в пикселях относительно камеры. Объекты стоят на одной линии в настоящих метрах, а камера в каждый момент знает два параметра: где она стоит и сколько пикселей приходится на метр.

Вторая тонкость — как интерполировать масштаб между соседями. Если между астероидом в 490 м и астероидом в 900 м зум менять линейно, а между звездой и галактикой тоже линейно, ощущения от прокрутки будут совершенно разные. Поэтому радиус в фокусе интерполируется в логарифмическом пространстве, и каждый шаг ощущается одинаково:

const R = Math.exp(Math.log(r0) + (Math.log(r1) - Math.log(r0)) * f);
// сдвиг камеры пропорционален текущему масштабу, а не прогрессу
const g = (R - r0) / (r1 - r0);
const camX = xs[i] + (xs[i + 1] - xs[i]) * g;
const k = focusRadius / R; // пикселей на метр

Если сдвигать камеру не пропорционально прогрессу, а пропорционально текущему масштабу, текущий объект дольше остаётся в кадре и не «уезжает» раньше, чем успеваешь его рассмотреть.

Один объект — один квад и один шейдер

Сцена построена на three.js, но почти без его возможностей: каждый объект — это прямоугольник из двух треугольников, а всё остальное рисует фрагментный шейдер. Шейдер получает координату пикселя в единицах радиуса объекта (край тела — r = 1) и радиус объекта в пикселях экрана. По последнему удобно делать сглаживание краёв и решать, сколько деталей вообще рисовать.

Типов шейдеров 16: каменные тела, планеты с текстурами, газовые гиганты, звёзды, нейтронная звезда, чёрные дыры, орбиты, гелиосфера, облако Оорта, туманности, скопления, галактики, группы галактик, космическая паутина и реликтовое излучение.

Настоящие поверхности планет

Первая версия рисовала всё процедурно, и Земля с выдуманными материками выглядела мультяшно. Поэтому для планет и спутников я взял настоящие карты: текстуры Solar System Scope (CC BY 4.0) и мозаики миссий New Horizons, Galileo, Cassini и Dawn из открытых архивов NASA/USGS. Это карты в равнопромежуточной проекции, и шейдер переводит точку сферы в широту и долготу.

Тут есть классические грабли — шов на линии смены дат. В месте, где долгота переходит через 180°, координата текстуры прыгает с 1 на 0, производные резко растут, GPU выбирает самый мелкий мип‑уровень, и по планете проходит тонкая уродливая линия. Лечится тем, что считаются производные двух вариантов координаты, сдвинутых на полоборота, и берутся меньшие:

vec4 texSph(sampler2D t, vec3 q) {  vec2 uv  = vec2(atan(q.x, q.z) / 6.2831853 + 0.5,                  asin(clamp(q.y, -1.0, 1.0)) / 3.14159265 + 0.5);  vec2 uv2 = vec2(fract(uv.x + 0.5), uv.y);  vec2 dx = dFdx(uv), dy = dFdy(uv);  vec2 dx2 = dFdx(uv2), dy2 = dFdy(uv2);  if (abs(dx2.x) < abs(dx.x)) dx.x = dx2.x;  if (abs(dy2.x) < abs(dy.x)) dy.x = dy2.x;  return textureGrad(t, uv, dx, dy);
}

У Земли четыре текстуры: день, ночные огни городов, облака и маска океана. Облака вращаются чуть быстрее поверхности и отбрасывают тени: для этого облака дополнительно сэмплируются в точке, сдвинутой к Солнцу. На океане есть блик Солнца, на ночной стороне горят города, по краю светится атмосфера с оранжевым оттенком у терминатора.

Земля
Земля

Почему Луна не похожа на шар

Если осветить Луну обычной моделью Ламберта, она выглядит как гладкий матовый мяч: яркая в центре и тёмная к краям. А настоящая полная Луна почти одинаково яркая по всему диску. Дело в реголите: пористая пыль рассеивает свет назад, к источнику. Для таких поверхностей есть закон Ломмеля — Зеелигера, который учитывает не только угол падения света, но и угол взгляда:

float lam = max(dot(n, LIGHT), 0.0); // Ламберт
float mu  = max(n.z, 0.02);           // косинус угла к наблюдателю
float ls  = 2.0 * lam / (lam + mu);   // Ломмель — Зеелигер
float diff = mix(lam, ls * 0.55, regolith);

Для Луны и Меркурия коэффициент regolith равен единице, для планет с атмосферой нулю. После этого Луна наконец стала похожа на фотографию.

Луна
Луна

Кольца Сатурна трассировкой лучей

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

А раз есть точка на кольце, бесплатно получаются тени. Из этой точки пускается луч к Солнцу: если он задевает сферу, кольцо в тени планеты. И наоборот, луч из точки на планете проверяется на пересечение с кольцами, и на диске появляется полосатая тень колец.

float zr = -(p.x * RN.x + p.y * RN.y) / RN.z; // пересечение с плоскостью колец
vec3 P = vec3(p, zr);
vec4 ring = ringAt(length(P));                 // текстура колец по радиусу
float b = dot(P, LIGHT);                       // луч P + t*LIGHT против единичной сферы
float disc = b * b - (dot(P, P) - 1.0);
float shadow = (disc > 0.0 && -b - sqrt(disc) > 0.0) ? 0.06 : 1.0;
Сатурн
Сатурн

Астероиды: ray marching по полю расстояний

Мелкие тела неправильной формы — астероиды, кометы, Фобос и Деймос. Для них текстура на сфере не годится: нужна объёмная форма. Поэтому они рисуются ray marching'ом по полю расстояний (SDF): сфера, деформированная шумом, а для кометы Чурюмова — Герасименко гладкое объединение двух сфер, та самая «резиновая уточка».

Дальше всё по классике: нормаль через центральные разности, кратеры через ячеистый шум Вороного, мягкие тени отдельным лучом к Солнцу и немного ambient occlusion. Самозатенение даёт ощущение настоящего камня, которого не было в плоской версии.

Дальний космос процедурно

Для звёзд, туманностей и галактик реальные фотографии не подходят: они разного масштаба, разной обработки и под разными лицензиями. Поэтому всё, что дальше Солнечной системы, генерируется:

  • Звёзды: потемнение к краю диска, грануляция (для Солнца из реальной карты фотосферы, для красных сверхгигантов огромные конвективные ячейки) и корона с протуберанцами из шума.

  • Чёрные дыры: тень, фотонное кольцо, линзированное изображение дальней стороны диска и аккреционный диск с доплеровским усилением. Сторона, которая летит к нам, ярче. У TON 618 добавлены джеты квазара.

  • Галактики: логарифмические спирали, пылевые прожилки на внутренней стороне рукавов, розовые области звездообразования, перемычка у Млечного Пути.

  • Крупномасштабная структура: космическая паутина из рёбер диаграммы Вороного (расстояние F2 − F1), сгустки галактик в узлах. У Ланиакеи нарисована её знаменитая граница, у войда Волопаса — пустота в центре.

Стрелец A*
Стрелец A*: аккреционный диск с доплеровским усилением

Режим сравнения и лупа

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

Проблема в том, что при большом отношении меньший объект становится меньше пикселя: Земля рядом с наблюдаемой Вселенной в 6,9·10¹⁹ раз меньше. Поэтому на настоящем месте объекта остаётся точка или кольцо по его реальному размеру, а над ним появляется лупа. Это второй проход рендера той же сцены, ограниченный scissor‑прямоугольником, только с увеличенным объектом.

Солнце рядом с Млечным Путём
Солнце рядом с Млечным Путём: в 680 миллиардов раз меньше по диаметру

Музыка, которая растёт вместе с масштабом

В Size of Life мне очень нравилось, что музыка нагнетается по мере прокрутки. Здесь саундтрек полностью генеративный, на Web Audio API, без единого аудиофайла:

  • в начале играет только тихий пэд из расстроенных пил через фильтр низких частот на аккордах ре минора;

  • дальше по очереди вступают саб‑бас, арпеджио, «хор» (пила через два полосовых фильтра на частотах формант гласной «а»), струнные с крещендо внутри каждого аккорда, литавры и колокольчики;

  • темп, громкость и яркость фильтра растут вместе с масштабом;

  • реверберация — свёртка с синтезированным импульсным откликом длиной 6 секунд, то есть просто экспоненциально затухающий шум.

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

Данные и точность

Размеры взяты из открытых источников, в основном из Википедии и NASA. Для некоторых объектов оценки сильно расходятся: крупнейшие звёзды, галактика IC 1101, Великая стена Геркулес — Северная Корона. В карточках это отмечено. Чёрные дыры измерены по горизонту событий. Поверхности планет и спутников — настоящие карты, а объекты дальнего космоса — художественная, но научно мотивированная интерпретация.

Технологии

  • TypeScript + Vite, единственная зависимость во время выполнения — three.js;

  • около 900 строк GLSL;

  • статический сайт на GitHub Pages, который после клонирования работает без интернета;

  • русский и английский интерфейс.

Проект открытый, под лицензией MIT. Добавить новый объект — это одна запись в файле с данными. Буду рад пулл‑реквестам с новыми объектами, улучшенными шейдерами и переводами.

Попробовать: legendacr.github.io/size‑of‑space
Исходники: github.com/LegendaCR/size‑of‑space

Если понравилось, поставьте звезду на GitHub: так проект увидит больше людей. А в комментариях расскажите, какого объекта вам не хватило.

Комментарии (2)


  1. InsiderCrush
    28.10.2026 12:13

    Относительные размеры давно уже мало кого впечатляют. Весь научпоп пронизан этими сравнениями, метафорами и наглядностями. Вы лучше сделайте приложение которое будет показывать реальные размеры пустоты между объектами во Вселенной. Что бы дикая грусть и тоска брала человека, от осознания на сколько космос реально пуст фактически.


  1. woodiron
    28.10.2026 12:13

    Интересно. А я вот раньше не видал Size of Life , так что для меня новинка.