← Четвёртое измерение

Глава 11 · 20 мин

Как нарисована витрина

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

Эта глава — экскурсия в машинное отделение витрины. Весь код лежит в resources/js/hyper/: в core/ — чистая математика без графики (её же проверяют тесты), в render/ — сцена на three.js со своими шейдерами на GLSL 3. Ниже — путь от списка чисел до готового кадра, по одному приёму на раздел. Всё, что здесь утверждается, можно найти в коде, а цифры получены на нём же.

Виджет ниже собирает картинку витрины слой за слоем — это заодно и план главы. Он работает на том же движке, что и витрина, и меняет только его «ручки»: униформы шейдеров и видимость слоёв.

Попробуйте

Пройдите шаги по порядку кнопкой «Дальше». На третьем шаге подвигайте $k$ от 0 до 1: тессеракт перейдёт от почти ортогональной тени к стереографической проекции. На пятом сравните трубки до и после конформного множителя. Shift или два пальца толкают фигуру в четвёртое измерение.

Многоячейник из вершин и нормалей

У 120-ячейника 600 вершин, 1200 рёбер, 720 пятиугольных граней и 120 ячеек-додекаэдров. Вбивать это таблицами не нужно: все многоячейники витрины строятся одной функцией buildPolytope(V, N) из двух списков — вершин $V$ на единичной сфере $\S^3$ и внешних нормалей $N$ к ячейкам.

  • Ячейка с нормалью $n$ — это вершины, крайние в направлении $n$: те $v$, у которых $v\cdot n$ равно максимуму по всем вершинам. Для выпуклого тела это ровно те вершины, которых касается опорная гиперплоскость.
  • Грань — пересечение двух ячеек, если у них не меньше трёх общих вершин. Вершины грани упорядочиваются по углу в её плоскости.
  • Рёбра — соседние вершины в гранях.

Нормали берутся у двойственного многогранника: нормали к ячейкам тессеракта — это вершины 16-ячейника и наоборот, у 120-ячейника — вершины 600-ячейника и наоборот. 5-ячейник и 24-ячейник самодвойственны: у первого нормали — это вершины с обратным знаком, у второго — та же фигура, повёрнутая на 45°, то есть 24 кватерниона Гурвица.

Остаётся получить вершины, и самый интересный случай — 120-ячейник. Вершины 600-ячейника — 120 икосианов, единичных кватернионов, образующих группу по умножению: 8 точек вида $(\pm1, 0, 0, 0)$, 16 вида $(\pm\tfrac12, \pm\tfrac12, \pm\tfrac12, \pm\tfrac12)$ и 96 чётных перестановок $(\pm\tfrac{\varphi}{2}, \pm\tfrac12, \pm\tfrac{1}{2\varphi}, 0)$, где $\varphi$ — золотое сечение. Ближайшие пары — это 720 рёбер длины $1/\varphi$. Четвёрки попарно соседних вершин (4-клики графа рёбер) — 600 тетраэдров-ячеек. А центры этих тетраэдров, вытолкнутые на сферу, — 600 вершин 120-ячейника. В Node на Apple M4 Pro весь 120-ячейник собирается примерно за 7 мс.

многоячейникVEFCнормали к ячейкам
5-ячейник510105его же вершины со знаком минус
8-ячейник (тессеракт)1632248вершины 16-ячейника
16-ячейник8243216вершины тессеракта
24-ячейник2496962424 кватерниона Гурвица
600-ячейник1207201200600вершины 120-ячейника
120-ячейник6001200720120вершины 600-ячейника

Ни одно число в этой таблице не вписано руками: их считает конвейер, а тест сверяет с известными значениями и с эйлеровой характеристикой $V - E + F - C = 0$. Почему правильных многоячейников ровно шесть, рассказано в главе о них.

Одна формула на три линзы

Ориентация тела в 4D — матрица $4\times4$ в униформе uRot. Каждый кадр она домножается на маленькие повороты в плоскостях (см. главу о вращении), а раз в 120 кадров ортонормируется заново, чтобы ошибки округления не накапливались. Дальше повёрнутую точку $p = (x, y, z, w)$ надо отправить в наше пространство, и все три линзы витрины оказываются одной формулой: $$X = \frac{(x,\ y,\ z)}{1 - k\,w}.$$ При $k = 0$ это ортогональная тень: четвёртая координата просто отбрасывается. При $0 < k < 1$ — перспектива (витрина берёт $k = 1/2{,}4$). При $k = 1$ — стереографическая проекция.

Теорема

При $k > 0$ точка $X$ — это место, где прямая из «глаза» $E = (0, 0, 0, 1/k)$ через точку $p$ пересекает гиперплоскость $w = 0$. В частности, при $k = 1$ и $|p| = 1$ формула даёт стереографическую проекцию сферы $\S^3$ из полюса $(0, 0, 0, 1)$.

Точки прямой — $E + t\,(p - E)$. Их четвёртая координата равна $\tfrac1k + t\,(w - \tfrac1k)$ и обращается в ноль при $t = \dfrac{1/k}{1/k - w} = \dfrac{1}{1 - k w}$. Первые три координаты глаза нулевые, поэтому в этой точке $(x, y, z)$ умножаются на $t$ — это и есть $X$. При $k = 1$ глаз стоит в точке $(0,0,0,1)$ на самой единичной сфере, а проекция сферы из её точки на экваториальную гиперплоскость по определению стереографическая. ∎

Число $s = 1/(1 - k w)$ — локальный масштаб линзы. Для стереографической проекции он точный: она конформна, то есть в каждой точке растягивает все направления одинаково, в $1/(1 - w)$ раз. Это проверено тестом, и этим числом мы ещё воспользуемся. Переключение линз на витрине — просто анимация $k$ и доли «дуговости» за 1,4 секунды; подробно линзы разобраны в главе о проекциях.

Дуги без slerp

Вершины всех многоячейников лежат на сфере $\S^3$, и в стереографической линзе их рёбра лучше всего рисовать дугами больших окружностей: стереографическая проекция переводит окружности в окружности, и тело превращается в клубок круглых дуг. Классический способ взять точку на такой дуге — slerp с синусами и арккосинусом. В шейдере есть способ проще: взять точку на хорде и нормировать.

Теорема

Пусть $a$ и $b$ — единичные векторы, угол между которыми $\theta < \pi$. Когда $t$ идёт от 0 до 1, точка $\mathrm{normalize}\bigl((1-t)\,a + t\,b\bigr)$ монотонно проходит короткую дугу большой окружности от $a$ до $b$ — ту же, что и slerp, — причём угловая скорость в середине в $1/\cos^2(\theta/2)$ раз больше, чем у концов.

Точка хорды лежит в плоскости $\mathrm{span}(a, b)$, проходящей через центр; после нормировки она лежит ещё и на единичной сфере, то есть на большой окружности, по которой эта плоскость пересекает сферу. Возьмём в плоскости единичный вектор $m$ биссектрисы угла и перпендикулярный ему $m_\perp$. Тогда $a = \cos\frac\theta2\, m - \sin\frac\theta2\, m_\perp$, $b = \cos\frac\theta2\, m + \sin\frac\theta2\, m_\perp$ и $$(1-t)\,a + t\,b = \cos\frac\theta2\, m + (2t-1)\sin\frac\theta2\, m_\perp.$$ Угол этой точки от биссектрисы $\psi(t) = \arctan\bigl((2t-1)\tan\frac\theta2\bigr)$ монотонно растёт от $-\frac\theta2$ до $\frac\theta2$. Его производная $\psi'(t) = \dfrac{2\tan\frac\theta2}{1 + (2t-1)^2\tan^2\frac\theta2}$ равна $2\tan\frac\theta2$ при $t = \frac12$ и $\sin\theta$ при $t = 0$ и $t = 1$; их отношение $\dfrac{2\tan(\theta/2)}{\sin\theta} = \dfrac{1}{\cos^2(\theta/2)}$. ∎

Для 120-ячейника угол ребра — 15,5°, и неравномерность всего 1,9%: на глаз её нет. У 600-ячейника (36°) — 10,6%, у тессеракта и 24-ячейника (60°) — треть, а у 5-ячейника (104,5°) кольца трубки у концов ребра стоят в 2,7 раза гуще, чем в середине, — но кривая всё равно та же. В шейдере это одна строка: mix(p, p * (r / length(p)), uSph), где униформа uSph плавно превращает хорду в дугу, когда линза переключается на стереографию.

Трубки гнёт видеокарта

Процессор один раз загружает в видеопамять концы рёбер — по два vec4 и цвету на ребро. Всё остальное происходит на GPU для каждой вершины каждого кадра: поворот, проекция и построение трубки. Трубка — это один цилиндр-шаблон, размноженный инстансингом: у каждой его вершины есть атрибут aTube = (t, угол) — где она вдоль ребра и где по окружности. Шаблон — 14 отрезков вдоль и 9 вершин по кругу (135 вершин, 252 треугольника), на устройствах с сенсорным экраном — 10 × 7.

Вершинный шейдер вычисляет кривую ребра шесть раз: в своей точке $t$, в обоих концах, в середине и в точках $t \pm 0{,}02$. По двум последним получается касательная $T$. Нужна ещё рамка — два перпендикуляра, вдоль которых строится окружность сечения трубки. Если брать «какой-нибудь перпендикуляр», трубка перекручивается. Помогает геометрия.

Теорема

При любой линзе $k$, любой доле дуги $\mathrm{sph} \in [0, 1]$ и любом повороте проекция ребра лежит в одной плоскости, проходящей через начало координат нашего пространства.

Пусть $a$ и $b$ — концы ребра после поворота. Точка хорды $(1-t)\,a + t\,b$ лежит в двумерной плоскости $P = \mathrm{span}(a, b)$ пространства $\R^4$; «выталкивание» на сферу умножает её на число, так что и точка кривой остаётся в $P$. Линза отправляет точку $p$ в $X = (x, y, z)/(1 - k w)$ — тоже в число раз умноженную тройку $(x, y, z)$. Отображение $p \mapsto (x, y, z)$ линейно, поэтому образ плоскости $P$ — плоскость через начало координат в $\R^3$ (или прямая, если $P$ содержит ось $w$). Все точки $X$ — кратные векторов из этого образа и лежат в нём же. ∎

Нормаль $B$ этой плоскости одна на всё ребро, и рамка $(T,\ B \times T,\ B)$ не крутится вдоль трубки. В шейдере $B$ — нормированное векторное произведение хорды $c_1 - c_0$ на $c_{1/2} - c_0$; если кривая выродилась в отрезок на прямой через центр, годится любой перпендикуляр. В чистых линзах кривая — отрезок (перспектива переводит прямые в прямые) или дуга окружности (стереография переводит окружности в окружности), а во время перехода между ними — дуга эллипса, но всё равно плоская.

Второй приём — толщина. Радиус трубки в шейдере равен uRadius * min(s, uMaxScale) * uScale, где $s = 1/(1 - k w)$ — тот самый локальный масштаб. Трубка постоянной толщины в четырёхмерном пространстве после конформной проекции и должна выглядеть трубкой толщины, пропорциональной $s$: ячейки у полюса проекции становятся не только большими, но и толстыми, а у центра — тонкими. Картинка перестаёт быть «проволочной» и обретает глубину. Потолок uMaxScale = 3 не даёт трубкам раздуться безгранично.

Как погасить полюс

Когда вершина подходит к полюсу $(0, 0, 0, 1)$, масштаб $s = 1/(1 - w)$ уходит в бесконечность: ячейки раздуваются и закрывают весь экран. То, что слишком близко к полюсу, нужно плавно гасить: множитель затухания равен $1 - \mathrm{smoothstep}(1{,}9;\ 2{,}9;\ s)$. Вопрос в том, как именно гасить.

  • Уменьшать радиус до нуля. Трубки, сходящие на нет, превратились в шипы — тело стало похоже на морского ежа.
  • Alpha-to-coverage — прозрачность через маску сэмплов сглаживания, без сортировки. При четырёх сэмплах на пиксель у прозрачности всего пять градаций, и плавное затухание превратилось в растровую сетку.
  • Два прохода. Та же геометрия рисуется дважды. Непрозрачный проход оставляет только фрагменты с множителем больше 0,985 и пишет глубину. Прозрачный проход (renderOrder = 3, без записи глубины) рисует только гаснущие фрагменты с альфой, равной квадрату множителя.

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

Цвет, свет и плёнки

Проекция съедает одно измерение, и его приходится возвращать цветом. Все цвета строятся в пространстве OKLCH (светлота, насыщенность, тон) и переводятся в линейный sRGB прямо в шейдере; на процессоре для интерфейса — та же матрица. В OKLCH равные шаги по светлоте и тону выглядят равными, поэтому переходы не проваливаются в грязь. Палитр три, и шейдер смешивает их с весами из униформы uColorMode, так что смена палитры — плавное перетекание весов.

  • Глубина w. Параметр $t = (1 + w)/2$ ведёт тон от 4,35 до 7,0 радиана (холодный синий → фиолетовый → тёплый янтарный) и светлоту от 0,52 до 0,80: дальнее в четвёртом измерении — тёмное и холодное, ближнее — светлое и тёплое.
  • Хопф. Отображение Хопфа $H(q) = q\,u\,\bar q$ переводит точку $\S^3$ (кватернион) в точку обычной сферы $\S^2$. Тон — долгота этой точки, светлота растёт с широтой. Все точки одной большой окружности-слоя получают один цвет (подробно — в главе о гиперсфере).
  • Ячейки. Собственный цвет каждой грани — среднее цветов двух её ячеек.

Свет — два источника, привязанных к камере, блик и френелевская кайма. Грани — «мыльные плёнки»; в стиле «неон» они складываются аддитивно и сортировки не требуют. Кадр рисуется в HDR-буфер с половинной точностью и 4-кратным сглаживанием, затем идут свечение (UnrealBloom), виньетка, зерно, едва заметная хроматическая аберрация к углам и тональная кривая ACES Filmic.

Двенадцать колец 120-ячейника

На витрине 120-ячейник раскрашен по Хопфу и вращается так, что его ячейки текут по двенадцати кольцам. Это не подобрано на глаз, а следует из алгебры икосианов. Нормали 120 ячеек — это 120 икосианов, вершины 600-ячейника. Ось отображения Хопфа взята у икосиана $$g = \tfrac{\varphi}{2} + \tfrac12\,i + \tfrac{1}{2\varphi}\,j = \cos 36^\circ + u \sin 36^\circ,\qquad u = \frac{\tfrac12\, i + \tfrac{1}{2\varphi}\, j}{\sin 36^\circ}.$$

Теорема

Отображение Хопфа с осью $u$ разбивает 120 ячеек 120-ячейника на 12 колец по 10 ячеек: внутри кольца цвет один, у разных колец — разный.

Кватернион $e^{u\alpha} = \cos\alpha + u\sin\alpha$ коммутирует с $u$, поэтому $H(q\,e^{u\alpha}) = q\,e^{u\alpha}\,u\,e^{-u\alpha}\,\bar q = q\,u\,\bar q = H(q)$: правое умножение на $e^{u\alpha}$ движет точку вдоль её слоя. Икосиан $g = e^{u\pi/5}$ имеет порядок 10, и правое умножение на него переставляет икосианы, ведь они образуют группу. Значит, 120 нормалей разбиваются на смежные классы $q\,\langle g\rangle$ по 10 элементов — это 12 колец, и внутри каждого $H$ постоянно. Обратно, если $H(q) = H(q')$, то $r = q^{-1}q'$ коммутирует с $u$, то есть $r = e^{u\alpha}$ — икосиан на окружности $\{e^{u\alpha}\}$. Икосианы на ней образуют конечную, а значит циклическую, подгруппу, содержащую $g$; в группе икосианов нет элементов порядка больше 10, поэтому эта подгруппа — сама $\langle g\rangle$, и $q'$ лежит в том же кольце, что и $q$. ∎

Тест проверяет это численно и заодно то, что 12 точек-образов на $\S^2$ — вершины икосаэдра: модуль косинуса угла между любыми двумя, кроме противоположных, равен $1/\sqrt5$. Вращение «поток» на витрине — правое умножение всего тела на $e^{u\alpha}$ с растущим $\alpha$. Каждая ячейка переезжает туда, где только что была соседка по кольцу того же цвета, поэтому геометрия течёт, а раскраска пространства стоит на месте. На этом тесте, кстати, нашлась ошибка: строки -0.000000 и 0.000000 как ключи словаря разрезали кольца пополам, и получалось 24 кольца вместо 12.

Сечения: процессор, каждый кадр

Линза «Сечение» показывает, что увидел бы житель нашего пространства, пока тело проходит сквозь него (см. главу о сечениях). Для многоячейников сечение считает процессор, каждый кадр заново: функция slicePolytope поворачивает вершины и режет каждую ячейку гиперплоскостью $w = h$. Ячейка — трёхмерный выпуклый многогранник, поэтому её разрез — выпуклый многоугольник: на каждом ребре, концы которого по разные стороны от гиперплоскости, берётся точка $a + \frac{d_a}{d_a - d_b}(b - a)$, где $d = w - h$, а полученные точки упорядочиваются по углу вокруг центра. Все многоугольники вместе ограничивают многогранник-сечение. Ячейки раскрашены оттенками, отстоящими друг от друга на золотой угол (≈137,5°), чтобы соседи не сливались.

Это дёшево: в Node сечение 120-ячейника занимает в среднем 0,17 мс (около 54 многоугольников и 103 точек), 600-ячейника — 0,34 мс (около 217 многоугольников). Сзади светится аддитивный «призрак» всего тела, и рёбра вспыхивают там, где их пересекает гиперплоскость: яркость — $\exp\bigl(-((w-h)/0{,}035)^2\bigr)$.

Гладкие тела: рей-марчинг в срезе

Для гиперсферы, «тигра», дитора, дуоцилиндра, сфериндра и кубиндра треугольники не нужны: их сечения рисует фрагментный шейдер на полноэкранном прямоугольнике. У каждого тела есть четырёхмерная функция расстояния (SDF), обычно в одну строку: у гиперсферы $|p| - 1$, у «тигра» $\bigl|\,(|p_{xy}| - R,\ |p_{zw}| - R)\,\bigr| - r$. Луч из камеры идёт в нашем пространстве, его точка $X$ поднимается в 4D как $p = R^{\mathsf T}(X, h)$ — это точка разрезающей гиперплоскости в системе координат самого тела, — и по лучу шагают на расстояние $\mathrm{sdf}(p)$. Это sphere tracing, и почему так можно — отдельная теорема.

Теорема

Пусть $K \subset \R^4$ — тело, $K_h = \{\,Y \in \R^3 : (Y, h) \in K\,\}$ — его сечение гиперплоскостью $w = h$. Тогда для любой точки $X \in \R^3$ $$\mathrm{dist}_4\bigl((X, h),\ K\bigr) \;\le\; \mathrm{dist}_3\bigl(X,\ K_h\bigr).$$ Следовательно, шаг длины $\mathrm{dist}_4$ по лучу в $\R^3$ никогда не перескакивает поверхность сечения.

Для любой точки $Y \in K_h$ точка $(Y, h)$ лежит в $K$, и $|(X, h) - (Y, h)| = |X - Y|$. Значит, каждое расстояние, по которому берётся нижняя грань справа, встречается и среди расстояний слева, где нижняя грань берётся по большему множеству $K$; она может быть только меньше. Шар радиуса $\mathrm{dist}_4$ вокруг $X$ в $\R^3$ лежит внутри шара радиуса $\mathrm{dist}_3$, в котором точек $K_h$ нет, — перескочить поверхность нельзя. Поворот $R^{\mathsf T}$ сохраняет расстояния, а если вместо точной SDF используется оценка снизу, неравенство только усиливается. ∎

Цена безопасности — лишние шаги. Если тело близко к гиперплоскости «по четвёртой координате», но далеко внутри среза, 4D-расстояние мало, и луч ползёт мелкими шагами. Виджет ниже показывает то же самое на ступеньку ниже: трёхмерный тор, плоскость $z = h$ сквозь него и луч в этой плоскости, который шагает по 3D-расстоянию до тора.

Попробуйте

При $h = 0$ плоскость режет тор поперёк кольца, и оба расстояния почти совпадают. Поднимите срез к $h \approx 1$: тор нависает над плоскостью, голубые шаги становятся заметно меньше фиолетовых окружностей, и шагов нужно в два-три раза больше. Кнопка «Шагать по d₂» показывает, как шёл бы луч с точным расстоянием в срезе.

В шейдере на луч отведено до 160 шагов внутри ограничивающей сферы; луч останавливается, когда расстояние меньше $0{,}0006\,t$ (с дальностью растёт и размер пикселя), а шаг для надёжности умножается на 0,95. Нормаль — градиент SDF по четырём точкам тетраэдра, затенение в щелях — пять проб вдоль нормали.

Кватернионное множество Жюлиа

Самый неожиданный экспонат — фрактал $z \mapsto z^2 + c$, где $z$ и $c$ — кватернионы. Квадрат кватерниона $q = a + \vec v$ равен $a^2 - |\vec v|^2 + 2a\vec v$. Множество Жюлиа — граница множества начальных точек, орбиты которых остаются ограниченными; оно четырёхмерно, и мы видим его трёхмерные срезы. Точной функции расстояния у него нет, есть оценка расстояния: $$d \approx \frac{|z_n|\,\ln|z_n|}{2\,|z_n'|},\qquad |z_{n+1}'| = 2\,|z_n|\,|z_n'|,\quad |z_0'| = 1.$$ Скалярная производная работает потому, что норма кватернионов мультипликативна: $|pq| = |p|\,|q|$. Точнее, производная отображения $z \mapsto z^2$ — это $\delta \mapsto z\delta + \delta z$ (кватернионы не коммутируют), и $|z\delta + \delta z| \le 2\,|z|\,|\delta|$, так что рекуррентная формула даёт оценку производной сверху. Сама формула расстояния асимптотическая, а не строгая граница, поэтому для Жюлиа шаг уменьшен до 0,8 от оценки. В шейдере 11 итераций и выход при $|z|^2 > 16$, а параметр $c$ медленно дрейфует по замкнутой петле, и фрактал непрерывно перетекает.

Сколько это стоит

Самый тяжёлый экспонат — 120-ячейник со всеми слоями:

слойинстансоввершин на инстанстреугольников
трубки рёбер (14 × 9)1200135302 400
сферы вершин (икосфера)600540108 000
грани (720 пятиугольников × 5 × 9)——32 400
итого442 800

Трубки и сферы рисуются дважды (непрозрачный и гаснущий проходы), и каждая из 162 000 вершин трубок шесть раз поворачивает и проецирует точку кривой. Расслоение Хопфа — 70 окружностей по 56 дуг, то есть 3920 коротких трубок по 21 вершине. В безголовом Chrome при 1440 × 900 (ANGLE поверх Metal, Apple M4 Pro) витрина держит 60 кадров в секунду — предел вертикальной синхронизации — на 120-ячейнике в стереографии и в сечении, на расслоении Хопфа и на «тигре»; на кватернионном Жюлиа в разных замерах выходило от 52 до 60. При плотности пикселей 2 сцена рисуется в разрешении 1,5× и тоже держит 60.

Разрешение подстраивается само: раз в 40 кадров сцена смотрит на среднее время кадра и, если оно больше 24 мс, снижает плотность пикселей в 0,82 раза (не ниже 0,6), а если меньше 14 мс — повышает в 1,12 раза (не выше плотности экрана и не выше 2). Когда холст за экраном или вкладка скрыта, цикл отрисовки спит.

Тесты и грабли

Математическое ядро проверяется тестами на встроенном в Node node:test (node --test tests/Js/hyper-*.test.cjs): числа граней и эйлерова характеристика всех многоячейников, равенство рёбер и плоскость граней, ортогональность поворотов, конформность стереографии, сечения тессеракта, кольца Хопфа, объёмы $n$-шаров. Теоремы этой главы проверяет hyper-drawing.test.cjs: плоская проекция ребра при случайных $k$ и доле дуги, неравномерность $1/\cos^2(\theta/2)$, безопасный шаг на гиперсфере и на торе, трюк с постоянным радиусом из виджета в начале главы, мультипликативность нормы кватернионов.

Самые поучительные ошибки случились на стыке с three.js. В шейдерах GLSL 3 у ShaderMaterial нет gl_FragColor — нужен свой out vec4. InstancedBufferGeometry запоминает число инстансов при первой отрисовке, и расслоение Хопфа рисовало одну дугу из 3920, пока кэш не стали сбрасывать после увеличения буфера. А у гладких тел нет радиуса трубки: undefined, умноженное на число, дало NaN, и анимация из NaN навсегда ломала все следующие экспонаты.

Итоги

  • Все многоячейники строятся одним конвейером из вершин и нормалей к ячейкам; нормали — это вершины двойственного многогранника, а 120-ячейник выводится из тетраэдров 600-ячейника.
  • Ортогональная тень, перспектива и стереография — одна формула $X = (x, y, z)/(1 - k w)$ с разными $k$.
  • Точка хорды, нормированная на сферу, лежит на той же дуге, что и slerp; неравномерность хода — $1/\cos^2(\theta/2)$.
  • Проекция любого ребра — плоская кривая, поэтому рамка трубки не крутится; радиус умножается на конформный множитель, а то, что у полюса, гаснет во втором, прозрачном проходе.
  • Цвет возвращает потерянное измерение: глубина по $w$ или отображение Хопфа в OKLCH; у 120-ячейника это ровно 12 колец по 10 ячеек.
  • Гладкие тела рисуются рей-марчингом в срезе: 4D-расстояние не больше расстояния в срезе, поэтому шаг безопасен.

На этом справочник заканчивается. Вернитесь на витрину и посмотрите на неё глазами программиста: включите сечение 120-ячейника или стереографию с палитрой Хопфа. Математика, которая стоит за этими приёмами, разобрана в главах о проекциях, вращении, сечениях и гиперсфере.

Источники

  • Код витрины: resources/js/hyper/core/ (многоячейники, вращения, проекции, сечения, Хопф), resources/js/hyper/render/ (шейдеры, слои, SDF), тесты tests/Js/hyper-*.test.cjs.
  • H. S. M. Coxeter. Regular Polytopes. 3rd ed. Dover, 1973 — правильные многоячейники, двойственность, координаты.
  • J. H. Conway, D. A. Smith. On Quaternions and Octonions. A K Peters, 2003 — икосианы и бинарная группа икосаэдра.
  • J. C. Hart. Sphere tracing: a geometric method for the antialiased ray tracing of implicit surfaces. The Visual Computer 12 (1996), 527–545.
  • J. C. Hart, D. J. Sandin, L. H. Kauffman. Ray tracing deterministic 3-D fractals. Computer Graphics (SIGGRAPH ’89) 23:3, 289–296 — оценка расстояния для кватернионных множеств Жюлиа.
  • Björn Ottosson. A perceptual color space for image processing, 2020 — пространство OKLab/OKLCH и матрицы перевода.
  • Inigo Quilez. Distance functions — функции расстояния и приёмы рей-марчинга.
  • three.js documentation — InstancedBufferGeometry, ShaderMaterial, EffectComposer, UnrealBloomPass.