Сайт: 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.

Проблема № 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), сгустки галактик в узлах. У Ланиакеи нарисована её знаменитая граница, у войда Волопаса — пустота в центре.

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

Музыка, которая растёт вместе с масштабом
В 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: так проект увидит больше людей. А в комментариях расскажите, какого объекта вам не хватило.
InsiderCrush
Относительные размеры давно уже мало кого впечатляют. Весь научпоп пронизан этими сравнениями, метафорами и наглядностями. Вы лучше сделайте приложение которое будет показывать реальные размеры пустоты между объектами во Вселенной. Что бы дикая грусть и тоска брала человека, от осознания на сколько космос реально пуст фактически.