Рисование светом: исследование освещенных ламп графического процессора с помощью TSL и WebGPU | Кодропс

Рисование светом: исследование освещенных ламп графического процессора с помощью TSL и WebGPU | Кодропс



Примечание редактора: Наше празднование самого первого Конференция Three.js это все о людях, идеях и экспериментах, которые делают это сообщество таким вдохновляющим. Сегодня мы невероятно рады пролить свет на Матис Биабьяничей талант и любопытство привели к этому прекрасному исследованию ламповых графических процессоров, TSL и WebGPU. Мы так благодарны, что Матис решил поделиться с нами этой фантастической работой в рамках празднования. Мы надеемся, что вам понравится погружаться в детали так же, как и нам.

🇫🇷 В Париже праздник продолжается! Самая первая конференция Three.js объединяет сообщество на два дня для переговоров, идей и связей. Использовать код CODROPS для скидка 15% и получить билет →

Откуда это взялось

Я рисую много линий. В последнее время моим любимым занятием было Макио-сеткабиблиотека сеток на базе TSL от Дэвид Ронай для рендерера WebGPU, который, честно говоря, делает почти все, что вам нужно: градиенты, штрихи, текстуры, смещения штрихов, создание экземпляров, позиции, управляемые графическим процессором, даже отбрасывание теней. Если вы работаете со строками в Three.js на WebGPU, это прекрасно.

Есть одна вещь, которую она не может сделать, и это не вина библиотеки. Его материал простирается MeshBasicNodeMaterialпоэтому он не освещен по конструкции. И как только вы это заметите, вы заметите это повсюду. Ваши линии никогда не отражают свет по краям, никогда не затеняются по ширине, никогда не ощущаются так, как будто они в сцену, а не нарисованную поверх нее.

Это не та ошибка, которую можно исправить. Сетка размещает каждую вершину дважды и раздвигает пару. в пространстве клипаперпендикулярно направлению линии на экране. К тому времени, когда геометрия существует, вы уже покинули мир. Там нет поверхности, поэтому нет ничего нормального, и свету не на что падать. Лента всегда обращена к вам. Вот и вся хитрость.

И вот этот образ застрял у меня в голове: две открытые руки, полностью вытянутые из ниток, рассыпающиеся на свободные пряди ниже запястий. Сначала я построил его с помощью линий сетки, и он выглядел хорошо, но плоско. Я хотел, чтобы нити были вещи. Я хотел, чтобы они отражали свет, правильно закрывали друг друга и имели объем.

Это означает настоящую геометрию. Имеется в виду трубы. Эта статья о трубчатой ​​системе, которая возникла в результате этого, о руках, построенных на ее основе, и о действительно унизительном экскурсе в то, почему одна линия векторной математики отказалась работать.

Я думаю, что стоит украсть три бита:

  1. Трубка, геометрия которой никогда не перестраивался. Позиции и нормали вычисляются в TSL поверх обычного материала PBR.
  2. Проблема поперечного сечения рамы и почему ее математически невозможно полностью решить.
  3. Авторские кривые ходить по сеткепоэтому пряди описывают форму, а не украшают ее.

Это все TSL в рендерере WebGPU, но идеи прекрасно переносятся на простой GLSL.

Очевидный подход и почему он не подходит

Стандартный ответ на вопрос «линия с объемом»: THREE.TubeGeometry: сэмплируйте кривую на ЦП, постройте кольцо вершин вокруг каждого сэмпла, а затем загрузите все это целиком.

Когда-то это здорово. Мои потоки перемещаются каждый кадр. Перестроение и повторная загрузка нескольких сотен тысяч вершин на кадр в ЦП — это именно та работа, ради которой вы купили графический процессор.

Итак, цель стала простой: треугольники никогда не меняются. Только там, где они есть.

Трубка, которая никогда не восстанавливается

Вот сдвиг, из-за которого все остальное выпадает. То, что я загружаю, не является трубкой. Это сетка, которая понятия не имеет, какой она формы:

  • progress — как далеко по кривой находится это кольцо, 0 → 1
  • angle — где в сечении находится эта вершина, 0 → 2π
  • индексы, сшивающие четверки между соседними кольцами

Вот и все. Никаких значимых позиций (обнуленный атрибут обеспечивает работоспособность конвейера) и никаких значимых нормалей. Для 90 сегментов × 3 радиальных стороны это несколько сотен вершин чистого пространство параметров.

for (let i = 0; i 

Фактическая форма отображается во время рендеринга. Материал занимает пробоотборник кривойфункция TSL, которая отображает progress в позицию и обрабатывает все остальное для каждой вершины:

// The whole contract: t in [0,1] → vec3. Everything else is derived.
const sampleCurve = 

this.positionNode = Fn(() => {
  const P     = sampleCurve(aProgress)
  const Pnext = sampleCurve(aProgress.add(EPS))   // EPS = half a segment
  const Pprev = sampleCurve(aProgress.sub(EPS))

  // Central difference gives us the tangent, without ever storing one.
  const delta   = Pnext.sub(Pprev)
  const tangent = delta.div(max(delta.length(), float(1e-6)))

  // Build a frame around that tangent (this is the fun part, see below)
  const N = normalize(cross(upAxis, tangent))
  const B = cross(tangent, N)

  // Sweep the ring. The radial direction IS the surface normal.
  const radial = N.mul(cos(aAngle)).add(B.mul(sin(aAngle)))

  vTubeNormal.assign(radial)
  return P.add(radial.mul(radius))
})()

this.normalNode = Fn(() => transformNormalToView(normalize(vTubeNormal)))()

Две вещи здесь делают тяжелую работу.

Обычный бесплатный. Направление, в котором вы отталкиваете вершину от позвоночника. является поверхность там нормальная. Запишите его в переменную и передайте на этап фрагмента через normalNodeи все готово. Вот что позволяет этому продолжаться. MeshStandardNodeMaterial вместо специального шейдера. Мы отменяем positionNode и normalNode и ничего больше, поэтому освещение, карты окружения, шероховатость, металличность и проходы теней — все продолжает работать. В отличие от меша, мы никогда не покидали мировое пространство, поэтому ничего подделывать не пришлось.

Кривая — это функция, а не данные. gpuPositionNode

Одна ошибка: поскольку позиции находятся в шейдере, ограничивающая рамка на стороне процессора — ложь. Отправьте его с frustumCulled = falseиначе сетка исчезнет при первом вращении по орбите.

Та часть, где я ошибся три раза

Все вышеперечисленное сработало с первого раза, кроме одной невинной строки: cross(upAxis, tangent). Я хочу пройти через это как следует, потому что я трижды ошибся, и каждая неудача научила меня чему-то, чего я не видел в письменном виде.

Проблема: чтобы провести кольцо по кривой, вам нужны два перпендикулярных направления в каждой точке. рамка. TubeGeometry делает это при параллельном транспорте: проходит кривую, переносит предыдущий кадр вперед и поворачивает его как можно меньше на каждом шаге. Это последовательный. Рамка н нужна рамка n−1а вершинный шейдер не имеет «предыдущей вершины». Каждая вершина существует сама по себе, и работать можно только с касательной.

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

Попытка 1: безветвевой ортонормированный базис (Дафф и др., Pixar, которым все пользуются). Быстрый, элегантный и надежный в отдельности. Он также содержит sign(tangent.z)что означает рамку переворачивает всякий раз, когда кривая пересекает tangent.z = 0 самолет. Два соседних кольца, расположенных по обе стороны этой плоскости, поворачиваются между собой на 180°, а соединяющий их четырехугольник сжимается в галстук-бабочку.

Среднее изображение вот такое, и это маленькое изящное доказательство: касательная спирали имеет z ∝ cos(a)который пересекает ноль ровно четыре раза за два оборота. Их ровно четыре, и все они расположены на центральной линии, потому что именно здесь точки пересечения приземляются в пространстве.

Попытка 2: совместить две опорные оси. Использовать (0,1,0)подойди к (0,0,1) поскольку касательная идет вертикально. Звучит разумно! Я даже уговорил себя поверить, что смесь безопасна.

Это не. Где-то в середине смеси, сама смешанная ось проходит прямо через касательную в точке |tangent.y| ≈ 0,93 с моим особым смягчением, и cross(up, tangent) схлопывается до нуля. Угадайте, в какой полосе направлений проводят все время почти вертикальные пряди рук.

Это единственная ошибка, которой нет на рисунке выше, и это самое интересное: она не создает чистый артефакт. Дафф дает вам неправильную картину хорошего поведения. А рухнул Frame дает вам нормализованный, почти нулевой вектор, то есть произвольное направление, то есть яркий мусор. На руках это выглядело как блестки, сотни крошечных выдутых кусочков, разбросанных по плетению. Я потратил удручающее количество времени, выискивая его при освещении и цветении, прежде чем мне пришло в голову, что геометрия лжет. Я нашел точную точку коллапса, только написав числовую развертку из десяти строк, после того как моя геометрическая интуиция уверенно убедила меня, что смесь не может пересечь касательную.

Попытка 3: выберите мировую ось, наименее совмещенную с касательной. Этот действительно не может деградировать. Наименьшая компонента единичного вектора не превышает 1/√3, поэтому векторному произведению всегда есть с чем работать. Я был доволен этим. Проверил на руках, где он выглядит безупречно, и пошел дальше.

Затем я надел его на спираль (правое изображение). Везде, где соединяются два касательных компонента, выбранная ось переключается, рама ломается, а трубка разбивается на сегменты. Вы можете видеть это на всем протяжении, с худшими артефактами на поворотах, где вы можете наблюдать, как поперечное сечение отходит в сторону. Выглядит как кусок бамбука.

На руках оно перешло как раз из-за того, что пряди там тонкие, как волос. На пряди недостаточно пикселей для отображения повернутого поперечного сечения. Это отдельный небольшой урок о что вы тестируете. Артефакт всегда был там; мой испытуемый просто не мог этого выразить. И даже там, где он скрывается, это все равно разрыв в параметризации поверхности, ожидающий возможности разорвать первую текстуру, нанесенную на нее.

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

Это переосмысливает все: от «найти правильную формулу» до «поместить неудачу туда, куда мой контент никогда не попадет». И ответ оказывается самым простым из четырех:

// One fixed reference axis. Singular ONLY where the curve runs exactly
// parallel to it — two points on the sphere. Not a plane. Not a set of seams.
const N = normalize(cross(upAxis, tangent))
const B = cross(tangent, N)

upAxis параметр, по умолчанию равный потому что содержание прядей (волосы, трава, водоросли, эти руки) в основном выполняется вертикально. Ссылка +Y разместит каждую кривую прямо над сингулярностью. Руки никогда не создают идеально выровненную по Z касательную, поэтому плохой случай просто никогда не визуализируется. Хеликс: чистый. Руки: чистые. И поворот, который бы Появление на Z-образной кривой в любом случае невидимо на круглой нетекстурированной трубке, потому что повернутый круг все равно остается кругом.

Если вам когда-нибудь понадобятся кадры, достаточно стабильные для текстур на произвольных кривых, настоящим улучшением будет вычисление кадров параллельной транспортировки за один проход вычислений. Пройти ~100 сэмплов последовательно — это пустяки для одной рабочей группы, и вы можете прочитать их в вершинном шейдере. Для работы с прядями достаточно одного перекрестного произведения.

Все три режима все еще находятся в сущности за frameMode переключаются, потому что их неудачи визуальныйи сравнение их рядом — единственный честный способ увидеть это. Именно так было сделано изображение выше.

Кормление настоящими кривыми

Процедурные кривые — это весело, но руки нужны авторские. Каждая нить — это 49–129 контрольных точек в буфере хранения, и сэмплер считывает их с помощью сплайна Catmull-Rom:

const positionNode = Fn(([progress]) => {
  const f  = progress.mul(float(SEGMENTS))
  const i0 = int(floor(f))
  const u  = f.sub(floor(f))

  const base = instanceIndex.mul(int(SEGMENTS + 1))   // this strand's slice
  const at = (k) => points.element(base.add(clampIndex(i0.add(int(k))))).xyz

  const p0 = at(-1), p1 = at(0), p2 = at(1), p3 = at(2)
  // ...standard Catmull-Rom, then wind / interaction displacement on top
})

Стоит отметить, почему это сплайн, а не mix(): с помощью линии сетки вы можете полностью обойтись без линейной интерполяции между контрольными точками, поскольку обводка не имеет поперечного сечения, которое можно было бы деформировать. Трубка наказывает его немедленно. Каждая контрольная точка становится видимой гранью, поскольку кадр строится из производнаяа производная полилинии скачет на каждом стыке. Гладких позиций недостаточно. Вам нужна гладкая производная.

С instanceIndex выбирает, какой фрагмент буфера читать, один экземпляр отрисовки визуализирует все более 500 цепочек. Та же сетка, один буфер,...



Источник

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх