Создание бесконечной сетки из жидкого стекла с помощью Three.js, WebGPU и TSL | Кодропс

Создание бесконечной сетки из жидкого стекла с помощью Three.js, WebGPU и TSL | Кодропс



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

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

Привет! Я Филип, соучредитель Шейдерстудия творческого развития в Швеции. Большую часть года мы поставляем клиентам интерактивные 3D-приложения. Несколько недель мы тратим на странные эксперименты без каких-либо задач и сроков. Последняя из них — бесконечная карусель из жидкого стекла: сетка стеклянных видеокарт, работающая в браузере на WebGPU. Видео прогибается сквозь стекло, сетка не имеет краев, в сцене нет ни единого источника света.

Стек

Next.js с React Three Fiber v10 на рендеринге WebGPU. Материалы написаны на TSL (Three.js Shading Language), Motion обрабатывает пружины перетаскивания, а в качестве текстур используются видеопотоки HLS. Никакой постобработки и целей рендеринга. Все происходит за один проход.

Жидкое стекло, геометрия не требуется

Хитрость здесь в том, что каждая карта — это просто плоская, разделенная на части плоскость. Закругленные углы, скошенные края и преломление — все это имитация материала. Мы никогда не моделируем стекло.

Если вы использовали Three.js, вы можете подумать, что это именно то, что нужно. MeshPhysicalMaterial это для. И это: набор transmission, ior, thicknessи dispersion на закругленной коробке, и из коробки получается очень убедительное стекло. Но это стекло — настоящая геометрия, ему нужна освещенная сцена, и передача сначала рендерит все, что находится за объектом, в отдельный буфер. Это очень много машин для пары сотен карт, и это преломляет сцену, стоящую за картой, тогда как на самом деле мы хотим изменить собственное видео карты.

Итак, мы создаем свое: плоскую плоскость, небольшой собственный шейдер и один проход рендеринга.

Все начинается с двумерной функции расстояния со знаком. Для каждого пикселя на карте это дает нам расстояние до края скругленного прямоугольника. Негативное означает внутри, позитивное – снаружи. Это классический SDF с закругленными углами, написанный на TSL:

const roundedBoxDistance = Fn(([point]) => {
  const radius = min(u.cornerRadius, min(halfSize.x, halfSize.y));
  const q = abs(point).sub(halfSize).add(radius);
  return length(max(q, vec2(0)))
    .add(min(max(q.x, q.y), float(0)))
    .sub(radius);
});

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

const bevelHeight = Fn(([distance]) => {
  const edge = clamp(
    float(1).add(distance.div(max(uniforms.bevelWidth, float(0.001)))),
    0,
    1,
  );
  const power = max(uniforms.bevelPower, float(1));
  const profile = pow(
    max(float(1).sub(pow(edge, power)), float(0)),
    float(1).div(power),
  );
  return profile.mul(uniforms.thickness);
});

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

Теперь о хорошем. Когда у нас есть нормаль и направление взгляда, мы можем преломить луч. Если мы преломим его несколько раз с немного разным показателем преломления для каждого цветового канала, мы получим хроматическую дисперсию — радужную бахрому, которую вы видите по краям настоящего стекла. Каждое «нажатие» немного по-разному изгибает луч и в основном подает его в один цветовой канал:

let refracted = vec3(0);
for (const tap of taps) {
  const eta = float(1).div(
    max(uniforms.ior.add(uniforms.dispersion.mul(tap.offset)), float(1.0001)),
  );
  const ray = refract(viewDir.negate(), normal, eta);
  const travel = uniforms.thickness.div(max(abs(ray.z), float(0.05)));
  const displaced = baseUv.add(
    ray.xy.mul(travel).mul(uniforms.refractStrength).div(uniforms.planeSize),
  );
  refracted = refracted.add(
    mapNode.sample(displaced).rgb.mul(vec3(...tap.weight)),
  );
}

Обратите внимание, что это простой JavaScript. for петля. TSL строит шейдер как граф узлов, поэтому цикл разворачивается при компиляции шейдера. Таким образом, количество нажатий определяется простым регулятором качества: больше нажатий на мощных графических процессорах, меньше на мобильных устройствах, а код шейдера остается точно таким же.

Преломляемая текстура — это собственное видео карты, поэтому видео буквально прогибается сквозь стекло. Это базовый слой. Чтобы оно воспринималось как стекло, а не как шаткое видео, мы добавляем сверху еще два ингредиента, и ни для одного из них в сцене нет ни единого источника света.

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

Во-вторых, боковой свет. А smoothstep на расстоянии SDF дает нам тонкую полосу, охватывающую край карты, и мы тонируем ее градиентом сверху вниз между двумя цветами, чтобы блики выглядели так, как будто они имеют направление.

Соберите вместе:

const reflection = reflect(viewWorld.negate(), normalWorld);
const environment = texture(envMap, equirectUV(reflection)).rgb;

const fresnel = u.fresnelF0.add(
  float(1)
    .sub(u.fresnelF0)
    .mul(pow(saturate(float(1).sub(dot(normal, viewDir))), 5)),
);

const rim = smoothstep(u.rimWidth.negate(), float(0), distance).mul(u.rimIntensity);
const rimColor = mix(u.rimColor, u.rimColorTop, rimGradient);

const finished = mix(
  refracted.mul(u.tint),
  environment,
  saturate(fresnel.mul(u.envIntensity)),
).add(rimColor.mul(rim));

Вот и весь материал. Это MeshBasicNodeMaterial с обычаем colorNodeи каждый кусочек «освещения», который вы видите, имитируется из нормального, которое мы вычислили сами.

Бесконечная сетка, тайно живущая на сфере

Хитрость здесь: сетка вовсе не бесконечна. Перетащите достаточно далеко, и карта, которая выходит из одного края, незаметно телепортируется к другому.

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

Само перетаскивание обрабатывается жестом панорамирования Motion на полноэкранном элементе, при этом положение и скорость сохраняются как motionValueвне React. Цикл кадра R3F считывает их напрямую, поэтому при перетаскивании не требуется повторной визуализации.

Настоящий текст на поддельном стекле

Еще одна вещь, которую вы можете сначала не заметить: заголовок, категория и описание на каждой карточке — это настоящий HTML. Не текстура, не текст SDF, просто divс. Это означает четкий текст при любом масштабе, обычный CSS, выбираемый и доступный контент. Загвоздка в том, что каждый div должен располагаться точно над картой, которая находится на сфере внутри WebGPU, и идеально следовать за ней во время перетаскивания.

Хитрость та же, что и у Three.js. CSS3DRenderer использования, сделано вручную. Фиксированный полноэкранный слой получает CSS perspective получено из поля зрения камеры. Внутри него «камера» div несет обратную матрицу камеры как matrix3d. Внутри него один абсолютно позиционированный div каждая объединенная карта содержит матрицу мира сетки как свою собственную matrix3d. Перспективная математика браузера аналогична математике, которую использует графический процессор, поэтому div идеально приземляется на сетку до пикселя и наклоняется вместе с ней вдоль сферы:

rootLayer.style.perspective = `${fov}px`;
cameraLayer.style.transform = `translateZ(${fov}px) ${cssMatrix(camera.matrixWorldInverse)} translate(50vw, 50vh)`;
cardEl.style.transform = `translate(-50%,-50%) ${cssMatrix(mesh.matrixWorld)}`;

В каждом кадре цикл R3F обновляет div камеры один раз, затем проходит по пулу и записывает преобразование в div каждой видимой карты (и скрывает те, которые находятся за кадром). Мы убираем масштаб из матрицы и вместо этого применяем его как ширину и высоту в пикселях, что позволяет самому размеру текста с контейнерными единицами оставаться четким. То же правило, что и при перетаскивании: элементы div объединяются в пул в соответствии с количеством ячеек и обновляются с помощью прямой записи style.transform, а не посредством повторного рендеринга React.

Обновление: сделать его действительно жидким

После того, как мы опубликовали демо-версию, кто-то заметил, что не все стекло является «жидким стеклом» и что мы воспользовались ажиотажем Apple. Это был один человек, но они были правы. Итак, мы заставили сетку двигаться.

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

Текстовый слой DOM из предыдущего раздела не пережил перемещение. Текст, приклеенный к жесткой сетке, — это одно, а текст, следующий за колеблющейся тканью, — это другое. Метки теперь отображаются внутри сцены с помощью pmndrs/глифиспользуя шрифты MSDF, запеченные во время сборки, поэтому они деформируются и преломляются вместе со стеклом, на котором находятся.

Вот и все: SDF, притворяющийся стеклом, и сфера, притворяющаяся бесконечной плоскостью.

Приезжай поздороваться в Париже

Мы с Джейкобом будем в Конференция Three.js в Париже. Если собираешься, найди нас и поздоровайся! Мы всегда готовы поговорить о WebGPU, TSL или о какой-нибудь странной демонстрации, которую вы создаете. Чтобы нас было легче заметить, вот наша очень профессиональная фотография: Саймон справа. Он останется в Швеции, чтобы играть на скрипке.

А если вы хотите узнать больше о том, что мы делаем, посетите нашу целевую страницу по адресу шейдер.se это целый эксперимент.

Увидимся там!



Источник

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

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

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