Симуляция жидкости в WebGL: Этап адвекции

Содержание
- Адвекция
- Текстура скорости
- Полулагранжев метод
- Шейдер адвекции
- Цикл отрисовки на JavaScript
- Заметки к демо реализации
- Итоги и что дальше
В предыдущей статье была представлена техника пинг-понг — ключевой паттерн для создания итеративных симуляций с сохранением состояния на GPU. Было показано, как передавать результат одного кадра на вход следующему.
Теперь пришло время создать один из основных компонентов симуляции жидкости: адвекцию. Адвекция — это процесс, при котором что-то переносится или перемещается в пространстве. Шейдер, созданный в этой статье, будет основной рабочей частью симуляции.
Основы адвекции
Адвекция — это процесс переноса вещества потоком.
Ключевым компонентом для понимания работы этого процесса является поле скоростей — математическая функция, которая присваивает 2D-вектор каждой точке области симуляции, описывая локальное направление и скорость потока.
В симуляции жидкости адвекция описывает, как поле скоростей перемещает различные величины, такие как плотность, температура или краситель. Представьте, что вы капнули чернилами в реку; течение реки (поле скоростей) несёт чернила (величину) вниз по течению. Процесс движения чернил вместе с водой — это адвекция.
Процесс адвекции зависит от двух подаваемых текстур:
- Текстура величины: Текстура, где каждый пиксель хранит значение, которое мы хотим перемещать, например, концентрацию красителя, температуру или даже саму скорость.
- Текстура скоростей: Текстура, где каждый пиксель хранит 2D-вектор скорости, представляющий направление и скорость потока в данной точке. Это GPU-представление поля скоростей.
Текстура величины (рассмотренная в предыдущей статье о пинг-понге) обновляется во время этапа адвекции на основе данных, хранящихся в текстуре скоростей.
Текстура скорости
Текстура скорости — это представление поля скорости для графического процессора. Поле скорости — это математическое понятие, которое присваивает 2D-вектор каждой точке в области симуляции, описывая направление и скорость локального потока. Оно необходимо для управления движением при адвекции.
Текстура скорости используется в качестве входных данных для шейдера адвекции, который считывает эти векторы скорости, чтобы определить, как должны перемещаться величины. Каждый пиксель текстуры хранит 2D-вектор скорости в каналах R и G.
Шейдер ниже генерирует текстуру скорости, вычисляя процедурный паттерн вихря.
glsl1// GLSL - Шейдер для процедурной генерации поля скоростей2#version 300 es3precision highp float;4out vec4 outColor;5in vec2 vUV; // Нормализованные координаты от 0.0 до 1.06uniform float uAspectRatio;78void main() {9 // Центрируем координаты в диапазоне (-0.5, 0.5)10 vec2 centeredUV = vUV - 0.5;1112 // Учитываем соотношение сторон, чтобы сделать вихрь круглым13 centeredUV.x *= uAspectRatio;1415 // Вычисляем скорость для вихря16 float vx = -centeredUV.y;17 float vy = centeredUV.x;1819 // Сохраняем 2D-вектор в R и G каналах20 outColor = vec4(vx, vy, 0.0, 1.0);21}
Этот шейдер создаёт простой вихрь, вращающийся против часовой стрелки с центром в середине экрана. Шейдер достаточно запустить один раз для генерации поля скоростей и сохранения его в текстуру. Значения скоростей не нормализованы и варьируются в зависимости от расстояния до центра. На практике эти значения могут требовать масштабирования или ограничения максимальной скорости для контроля скорости симуляции.
Полулагранжев метод
Сложность реализации адвекции на GPU заключается в том, что каждый пиксель в выходной текстуре должен определить, какое значение он должен иметь после того, как поле скоростей переместило величины. Наивный подход прямого отображения, при котором значения перемещаются вперед с их текущих позиций, создает проблемы — несколько исходных пикселей могут записывать в одно и то же место назначения, в то время как другие яйчейки могут вообще не получить данных, создавая пропуски или наложения.
Чтобы избежать этих проблем, мы используем подход обратного отслеживания — распространенную технику, называемую полулагранжевым методом. Вместо перемещения каждого пикселя вперед алгоритм отслеживает назад от позиции каждого пикселя, чтобы найти исходное местоположение, затем интерполирует значение в той точке отправления и присваивает его текущему пикселю. Интерполяция необходима, поскольку источник обычно попадает между местоположениями пикселей. Она используется для плавного смешивания значений четырёх окружающих пикселей.
Его реализация на GPU требует тщательного управления памятью. Поскольку фрагментные шейдеры выполняются одновременно для всех пикселей, они все одновременно читали бы из одной и той же текстуры и записывали в неё, создавая непредсказуемые результаты. Паттерн пинг-понг, который был описан в предыдущей статье, решает эту проблему, читая из одной текстуры и записывая интерполированные значения в другую.
p_source = p_current - (v × Δt)
Где:
p_source
— это исходная позиция, полученная при поиске.p_current
— это координата текущего вычисляемого пикселя в выходной текстуре (пункте назначения).v
— это вектор скорости в точкеp_current
, полученный из текстуры скоростей.Δt
— это временной шаг, небольшая константа, которая контролирует, насколько далеко мы "шагаем" назад во времени.
Выбор правильного временного шага: Хорошее начальное значение для Δt
— 0.016 (для 60fps). Временной шаг не строго привязан к частоте кадров, а представляет, насколько симуляция продвигается за одну итерацию. Меньшие значения (0.008) создают более медленное и стабильное движение, тогда как большие значения (0.032) приводят к более быстрому, но потенциально менее стабильному результату.
После вычисления p_source
, по этой координате выполняется выборка текстуры с величинами из предыдущего кадра. Выбранное значение становится новым значением для текущего пикселя. Этот подход — «обратная выборка» — стабилен, эффективен и отлично подходит для фрагментного шейдера.
Шейдер адвекции
Следующий шаг — создание шейдера адвекции для анимации текстур или частиц через это поле. Адвекция моделирует то, как вещества (такие как краситель, дым или частицы) движутся вместе с потоком, создавая визуальное движение.
Этот шейдер выполняет полулагранжевый шаг адвекции. Он принимает на вход поле скоростей и текстуру величин из предыдущего кадра и записывает результат в выходную текстуру.
glsl1// GLSL - Шейдер адвекции2#version 300 es3precision highp float;45in vec2 vUV;6out vec4 outColor;78// Переносимая величина (например, краситель) из предыдущего кадра9uniform sampler2D uQuantity;10// Поле скоростей, которое управляет движением11uniform sampler2D uVelocity;12// Временной шаг13uniform float uDt;1415void main() {16 // 1. Считываем вектор скорости в позиции текущего пикселя17 vec2 velocity = texture(uVelocity, vUV).rg;1819 // 2. Вычисляем исходную позицию (смотрим назад во времени)20 vec2 sourceUV = vUV - velocity * uDt;2122 // Ограничиваем координаты границами текстуры, чтобы избежать артефактов "заворачивания"23 sourceUV = clamp(sourceUV, 0.0, 1.0);2425 // 3. Считываем величину из предыдущего кадра в исходной позиции26 vec4 advectedQuantity = texture(uQuantity, sourceUV);2728 // 4. Записываем результат29 outColor = advectedQuantity * 0.9993;30}
Этот шейдер — ядро симуляции. Повторный его запуск в рамках пинг-понг схемы создает непрерывный поток.
Ограничение clamp
создает границы, к которым "прилипает" жидкость. В зависимости от желаемого поведения можно выбрать альтернативные подходы.
Для текстур следует использовать линейную фильтрацию (gl.LINEAR
) для более гладкой интерполяции, что критически важно для плавного движения жидкости. Если в вашей реализации необходимо сохранять резкие границы, используйте фильтрацию по ближайшему соседу.
Этот подход к адвекции чрезвычайно эффективен на GPU, поскольку каждый пиксель может быть вычислен независимо, что делает его идеальным для параллельной обработки.
Цикл отрисовки на JavaScript
Адвекция в реальном времени требует координации множественных проходов рендеринга каждый кадр. Цикл рендеринга управляет операциями с фреймбуферами, обрабатывает переключение пинг-понг буферов и упорядочивает операции GPU, которые обеспечивают работу симуляции.
Код на JavaScript организует весь процесс. Нам нужны две текстуры величины для красителя, которые обновляются каждый кадр (техника пинг-понга), и текстура скорости. Распространённый подход требует создания отдельного фреймбуфера для каждой текстуры.
После первоначальной настройки (создания шейдерных программ, фреймбуферов и отрисовки начального красителя), цикл анимации выполняет эти шаги каждый кадр:
-
Проход адвекции: Привязать фреймбуфер записи как цель рендеринга, использовать программу шейдера адвекции, установить uniform-переменные для текстуры количества (из читающего FBO) и поля скорости, затем отрисовать полноэкранный квад для выполнения вычисления адвекции.
-
Проход отображения: Отрендерить результат на экран, привязав фреймбуфер канваса и отрисовав другой квад, используя недавно адвектированную текстуру количества.
-
Обмен: Поменять местами фреймбуферы чтения и записи для подготовки к следующей итерации кадра.
javascript1// --- Цикл отрисовки ---2function render() {3 // --- Проход адвекции ---4 gl.bindFramebuffer(gl.FRAMEBUFFER, quantityFboPair.write.framebuffer);5 gl.useProgram(advectionProgram);67 // --- Устанавливаем uniform-переменные ---8 // Uniform 0: Величина из предыдущего кадра9 gl.activeTexture(gl.TEXTURE0);10 gl.bindTexture(gl.TEXTURE_2D, quantityFboPair.read.texture);11 gl.uniform1i(advectionUniforms.quantity, 0);1213 // Uniform 1: Статичное поле скоростей14 gl.activeTexture(gl.TEXTURE1);15 gl.bindTexture(gl.TEXTURE_2D, velocityFbo.texture);16 gl.uniform1i(advectionUniforms.velocity, 1);1718 gl.uniform1f(advectionUniforms.dt, dt);1920 // Запускаем шейдер адвекции на полноэкранном кваде21 gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);2223 // --- Проход отображения ---24 // Рендерим результат на холст25 gl.bindFramebuffer(gl.FRAMEBUFFER, null);26 gl.useProgram(displayProgram);27 gl.activeTexture(gl.TEXTURE0);28 gl.bindTexture(gl.TEXTURE_2D, quantityFboPair.write.texture);29 gl.uniform1i(displayUniforms.displayTexture, 0);3031 gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);3233 // --- Обмен буферами для следующего кадра ---34 quantityFboPair.swap();35 requestAnimationFrame(render);36}
Этот фрагмент кода показывает основную структуру цикла рендеринга.
Полная реализация с настройкой шейдеров и инициализацией доступна на гитхабе. Посмотреть Демо.
Заметки к демо реализации
Обратите внимание, что шейдер начальной сцены обновлен для создания градиента, который переходит от оранжевого в центре к зеленому на внешнем крае. Он применяет переход цвета, основанный на расстоянии от центра. Это помогает визуализировать процесс адвекции.
Переход к текстурам с плавающей точкой
Поскольку для реализации адвекции требуются данные скорости, это создаёт новую проблему в настройке текстур. В отличие от простых цветовых данных из статьи о пинг-понге, векторы скорости должны представлять как положительные, так и отрицательные значения — поток может двигаться влево (-x) или вправо (+x), вверх (+y) или вниз (-y).
javascript1// Предыдущий подход (статья о пинг-понге)2gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);34// Лучший подход для данных скорости5gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
Стандартные 8-битные текстуры могут хранить только значения от 0 до 1, что означает, что отрицательные значения скорости обрезаются до нуля, повреждая данные о направлении потока. Чтобы предотвратить это повреждение, используется подход с ремаппингом для сдвига диапазонов скорости типа [-0.5, 0.5] в [0, 1] для хранения, а затем обратного преобразования в шейдерах.
Текстуры с плавающей точкой устраняют эту проблему. Они могут хранить отрицательные значения напрямую, обеспечивая более чистый код шейдеров и лучшую точность. Шейдер скорости может выводить vec4(-0.3, 0.2, 0.0, 1.0), а шейдер адвекции может считывать именно это — никаких математических преобразований не требуется.
Чтобы использовать текстуры с плавающей точкой, нужно запросить доступ к необходимому расширению:
javascript1const ext = gl.getExtension('EXT_color_buffer_float');
Компромисс заключается в использовании памяти — 16-битные числа с плавающей точкой используют в два раза больше памяти GPU, чем 8-битные целые числа. Для симуляции жидкости эта дополнительная точность того стоит. Результирующий код легче понимать, отлаживать и расширять.
Большинство современных браузеров поддерживают расширение EXT_color_buffer_float, необходимое для этого подхода, что делает его практичным выбором для приложений.
Итоги и что дальше
В этой статье рассмотрены основы моделирования жидкости через реализацию адвекции - процесса перемещения данных по сетке с использованием поля скорости. Это ключевая концепция в компьютерной графике и научных вычислениях.
Ключевые концепции:
- Полулагранжев метод: Эффективный и стабильный алгоритм "обратного прослеживания", идеально подходящий для реализации во фрагментном шейдере.
- Пинг-понг буферы: Техника управления эволюционирующим состоянием симуляции, передающая данные от кадра к кадру.
- Статические поля скорости: Создание заранее определенных картин течения, таких как вихри, для контролируемых эффектов адвекции.
Однако моделирование жидкости пока неполное. Поле скорости статично и не реагирует на саму жидкость. Далее будут рассмотрены этапы Дивергенции и Давления, которые вычисляют поведение жидкости, делая поле скорости динамичным и интерактивным.
Это часть моей серии статей о реализации интерактивных 3D-визуализаций с помощью WebGL 2.