Построение интерактивной сетки куба распространения волн с помощью Three.js | Кодропс

Построение интерактивной сетки куба распространения волн с помощью Three.js | Кодропс



Привет, это Фрэнки Хунг. Я креативный фронтенд-разработчик из Гонконга. В этом уроке мы собираемся создать минималистичный, но завораживающий фон из 3D-волновой сетки!

Однажды я просматривал Pinterest в поисках вдохновения и наткнулся на очень крутую вещь. анимация демонстрация волн, распространяющихся по сетке кубов, с реалистичными тенями. Я подумал про себя: «Кажется, это не так уж и сложно воссоздать в Three.js, почему бы не считать это небольшой проблемой?»

Прежде чем углубиться в детали, вот краткий обзор этого урока:

  1. Структура проекта
  2. Настройка сцены
  3. Реализация следа мыши
  4. Распространение волн по следу мыши
  5. Магия постобработки
  6. Готовые к производству оптимизации
  7. Настройте свою волновую сетку!

Давайте начнем!

Структура проекта

root
├── src
|  ├── ThreeJS
|  |  ├── Camera.js
|  |  ├── Orchestrator.js
|  |  ├── Renderer.js
|  |  ├── Stage.js
|  |  ├── Effects
|  |  |  ├── MouseTrail.js
|  |  |  └── VignetteRGBShiftShader.js
|  |  └── Utils
|  |     ├── Debug.js
|  |     └── Sizes.js
|  ├── index.html
|  ├── script.js
|  └── style.css
├── public
|  └── arrow.svg
├── package.json
└── vite.config.js

Для комплектации выбран Vite, поскольку это широко используемый и очень эффективный современный инструмент сборки. script.js — это наша запись, которая инициализирует сцену Three.js, привязывает ее к элементу холста в index.html и запускает другой интерактивный код. Все, что связано с Three.js, сгруппировано в разделе ThreeJS папка.

То, как я структурирую свой код Three.js, во многом основано на шаблонах Бруно Саймона в его курсе. Позвольте мне кратко объяснить роль каждого файла:

  • Orchestrator.js : Центральный синглтон, который инициализирует и координирует сцену, камеру, рендерер, основные утилиты и цикл обновления. Это основа нашей сцены Three.js.
  • Camera.js : Управляет PerspectiveCamera и поведение наклона камеры при помощи мыши.
  • Renderer.js : Занимается WebGLRenderer настройка и конвейер постобработки с использованием EffectComposer.
  • Stage.js : Управляет всем содержимым, отображаемым на сцене, включая освещение, сетку интерактивного куба и графические элементы управления для всех настраиваемых параметров.
  • Utils/Debug.js : Содержит одноэлементный экземпляр графического интерфейса и переключает меню элементов управления в зависимости от того, #debug присутствует в URL.
  • Utils/Sizes.js : Отслеживает размеры окна и соотношение пикселей, генерируя события изменения размера для камеры и средства рендеринга для соответствующего обновления.
  • Effects/MouseTrail.js : Отслеживает историю мыши для создания динамической текстуры следа, используемой для смещения волн в реальном времени.
  • Effects/VignetteRGBShiftShader.js : Пользовательский шейдер, обеспечивающий тонкое периферийное затемнение и постобработку хроматических аберраций.

Настройка сцены

Время строить! Я не буду вставлять сюда каждую строку кода, поэтому при необходимости обращайтесь к репозиторию Github.

Хотя тысяч кубов не будет, но сокращение 400 вызовов отрисовки в один определенно является серьезной причиной для использования создания экземпляров. Однако в этом уроке мы стремимся к эстетике относительно больших кубов, поэтому будет отображена только часть из 400 кубов. Поскольку кубики нам нужны для отбрасывания теней, MeshPhongMaterial было бы достаточно; нам не нужно MeshStandardMaterial здесь, поскольку нам не нужно физически точное освещение для простых поверхностей куба.

Давайте определим setGrid, updateGrid функции и setLighting функции для своих соответствующих ролей.

setGrid() {
    const count = this.gridSize * this.gridSize; // gridSize: 20
    const geometry = new THREE.BoxGeometry(
        this.cubeWidth, // width: 0.8
        this.cubeHeight, // height: 3
        this.cubeWidth,
    );
    const material = new THREE.MeshPhongMaterial({ color: 0xffffff });

    // Per-instance XZ world position passed to the vertex shader
    this.offsetAttribute = new THREE.InstancedBufferAttribute(
        new Float32Array(count * 2),
        2,
    );
    geometry.setAttribute("aOffset", this.offsetAttribute);

    this.instancedMesh = new THREE.InstancedMesh(geometry, material, count);
    this.instancedMesh.castShadow = true;
    this.instancedMesh.receiveShadow = true;
    this.scene.add(this.instancedMesh);

    // Arrange cube instances into a grid
    this.updateGrid();
}

Обратите внимание, что мы определяем дополнительный атрибут буфера, он действует как идентификатор и 2D-позиция каждого экземпляра, что пригодится позже. Важно установить castShadow и receiveShadow значение true, чтобы позволить кубам отбрасывать на себя тени.

В setLighting()эти строки особенно важны:

this.directionalLight.castShadow = true; // don't forget this else there'll be no shadows!
this.directionalLight.shadow.mapSize.set(1024, 1024);
this.directionalLight.shadow.radius = 6;

shadow.mapSize определяет разрешение карты теней, отображаемой этим источником света. Чтобы максимально оптимизировать ваше приложение, уменьшайте разрешение до минимума, пока качество изображения не начнет падать ниже приемлемого. Увеличьте радиус тени, чтобы получить более мягкие тени. Наконец, не забудьте включить главный переключатель теней для рендерера!

this.instance.shadowMap.enabled = true;
this.instance.shadowMap.type = THREE.PCFShadowMap; // PCFSoftShadowMap was deprecated in Three.js version r182

И вуаля! Теперь у вас есть сетка из белых кубиков, в которой особо нечего увидеть!

Реализация следа мыши

Теперь, когда наша статическая сетка готова, пришло время воплотить ее в жизнь! Основная идея состоит в том, чтобы создать волны, которые расходятся по пути курсора. Для этого нам нужно знать, где находится курсор не на 2D-экране, а в нашем 3D-мире. Вся эта логика обрабатывается внутри нашего MouseTrail.js сорт. Давайте погрузимся.

Классический метод Three.js заключается в использовании Raycaster для перевода положения курсора в трехмерное мировое положение с активным прослушивателем событий pointermove, привязанным к холсту. Мы «стреляем» лучом из камеры через нормализованные координаты указателя и видим, где он пересекается с большой невидимой горизонтальной плоскостью, находящейся в точке y = 0то же, что и сетка.

this.raycaster = new THREE.Raycaster();

// Invisible horizontal plane for pointer → world-space raycasting
this.rayPlane = new THREE.Mesh(
    new THREE.PlaneGeometry(bounds, bounds),
    new THREE.MeshBasicMaterial({
        side: THREE.DoubleSide,
        visible: false,
    }),
);
this.rayPlane.rotation.x = -Math.PI / 2;
this.rayPlane.updateMatrixWorld(true); // important!

Обратите внимание, что нам нужно позвонить rayPlane.updateMatrixWorld(true) после поворота из-за его отсутствия в реальной сцене для отображения; Three.js не обновляет свою мировую матрицу автоматически, поэтому нам нужно делать это вручную. Это дает нам плоскую поверхность, с которой можно последовательно пересекаться, что позволяет нам получить (x, z) координаты мыши на плоскости сетки.

Далее нам нужно инициализировать данные и текстуру следа для дальнейшего использования:

// DataTexture (MAX_TRAIL × 1, RGBA float): trail data for the shader
this.trailData = new Float32Array(MAX_TRAIL * 4);
this.trailTexture = new THREE.DataTexture(
    this.trailData,
    MAX_TRAIL, // 128
    1,
    THREE.RGBAFormat,
    THREE.FloatType,
);
this.trailTexture.needsUpdate = true;

После того, как rayPlane готов, вот что мы делаем всякий раз, когда pointermove пожары событий:

  1. Использование raycaster для расчета координат (x, z) мыши, проецируемых на трехмерную сетку.
  2. Рассчитать distDelta который хранит расстояние между текущей позицией курсора и последней. Это будет полезно при корректировке высоты волн.
  3. Исключить самую старую точку в массиве данных следа, если мы храним больше, чем MAX_TRAIL определяет.
  4. Вставьте новую точку в массив данных следа, также назначив координаты новой точки в качестве последней точки для следующего цикла.

Для массива данных следа для каждой точки мы храним: { x, z, age, distDelta }. Мы добавляем новую точку только тогда, когда курсор переместился на минимальное расстояние (trailSpacing), что предотвращает слишком плотный след, когда мышь почти неподвижна.

Чтобы эффективно передать эти данные в графический процессор, мы сериализуем их в DataTexture. Это специальная текстура, с помощью которой мы можем напрямую манипулировать необработанными пиксельными данными. Мы создадим MAX_TRAIL текстура x 1, где каждый «пиксель» представляет собой 4-компонентное число с плавающей запятой (RGBA), в котором хранятся данные для одной точки следа.

  • Р: x позиция
  • Г: z позиция
  • Б: age точки в секундах, 0 для новой точки
  • А: distDelta (расстояние от предыдущей точки), 0 для первой точки

Обновление следа в каждом кадре

В нашем основном цикле анимации мы вызываем update() метод MouseTrail. Этот метод имеет две задачи:

  1. Возраст и чернослив: перебирает trail массив, увеличивает age каждой точки и удаляет все точки, срок действия которых превысил срок их службы.
  2. Загрузка в графический процессор: копирует данные из trail массив в наш trailData Float32Arrayа затем помечает текстуру для обновления, установив this.trailTexture.needsUpdate = true. Это говорит Three.js загрузить наши последние данные следа на видеокарту.
// Points survive for fadeTime * 4 seconds; at that age the shader
// fade factor exp(-4) ≈ 0.018 makes them visually negligible.
const expiry = this.params.fadeTime * 4;

for (let i = this.trail.length - 1; i >= 0; i--) {
    this.trail[i].age += delta;
    if (this.trail[i].age > expiry) {
        this.trail.splice(i, 1);
    }
}
// Upload the latest MAX_TRAIL live points to the texture
const count = Math.min(this.trail.length, MAX_TRAIL);

if (count > 0 || this._uniforms.uTrailCount.value > 0) {
    for (let i = 0; i 

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

Распространение волн по следу мыши

Это самое интересное! Наконец-то мы можем использовать наши trailTexture поднимать волны! Мы будем изменять значение по умолчанию MeshPhongMaterial вершинный шейдер для смещения вершин наших кубов в зависимости от их близости к следу мыши. Давайте окунемся обратно в Stage.js.

Расширение вершинного шейдера

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

Внутри Stage.jsмы определим метод overrideVertexShader который берет вершинный шейдер по умолчанию и внедряет нашу волновую логику. Этот метод также будет повторно использован в пользовательском материале глубины позже. Ядро этой логики происходит внутри цикла, который проходит через каждую точку нашего uTrailTexture.

Давайте разберем здесь ключевые понятия:

Текстурный цикл

// inside overrideVertexShader(vertexShader)
...inserted after #include 
for ( int i = 0; i 

Поскольку наш DataTexture размеры 128×1, нам нужно только повернуть координату x uv-индекса. Мы также добавляем 0,5 к обеим осям, чтобы точно определить центральное положение каждого текселя.

Гауссов конверт

float dist      = length( worldXZ - td.rg );
float wavefront = uWaveSpeed * td.b;
float relDist   = dist - wavefront;

// Gaussian envelope centred on the expanding wavefront
float window = exp( -( relDist * relDist ) / ( uWaveWidth * uWaveWidth ) );

Для каждого куба и каждой точки следа мыши мы вычисляем расстояние куба от расширяющегося кругового волнового фронта. relDist Ключевая переменная: это расстояние куба от точки следа минус расстояние, которое волна уже прошла (wavefront = uWaveSpeed * age), что дает нам расстояние между волновым фронтом и кубом.

Затем мы используем это relDist в функции Гаусса. Это создает гладкий колоколообразный «выпуклость» с центром на фронте волны. uWaveWidth униформа контролирует ширину этого выступа. В результате получается мягкая, естественная рябь.

Ослабление волн

Чтобы эффект казался более физическим и менее хаотичным, мы применяем несколько форм ослабления, чтобы контролировать силу волны (weight):

// Exponential time-fade + distance attenuation
float fade   = exp( -td.b / uFadeTime );
float atten  = 1.0 / ( 1.0 + dist * 0.1 );
float weight = fade * window * atten * td.a; // td.a is distDelta, used to weaken waves from closely spaced trail points

Время исчезновения: По мере того, как точка следа становится старше (ее ageхранится в td.bувеличивается), его влияние экспоненциально затухает. Увеличьте uFadeTime равномерное, чтобы замедлить затухание, зависящее от времени.

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

Затухание скорости: Помнить distDelta от MouseTrail.js? Мы сохранили его на канале «А»…



Источник

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

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

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