Мы уже делали симуляцию движения кораблика по волнам (вот тут), но это была, так сказать, математическая подготовка к более сложной версии решения. Теперь, давайте сделаем симулятор волн, но в 3D.
Начало
За основу я взял три технологии: JS, WASM и WebGL. Задумка в том, чтобы генерировать точки внутри wasm-пакета (я выбираю rust), передавать их в js приложение, а уже оттуда передаваться в webgl. Поскольку рисунок морской качки будет повторяться - разобьём пространство на плитки, скажем, MxN и каждую плитку будем отрисовывать единым шейдером, но по разным параметрам.
О технологиях
Я уже рассказывал, как подключать wasm-пакеты тут и как использовать webgl в своих веб приложениях здесь. Так что повторяться не стану.
Математика вычислений
Красивые волны сделать непросто, особенно для тех, кто, как и я, не особо силён в вопросах её математического аппарата, поэтому я пойду по пути наименьшего сопротивления:
- Волны - будут вычисляться по формуле y=sin(x);
- Каждая плитка имеет свой набор точек, но он согласован с набором соседней плитки (то есть связь будет бесшовной);
- Существуют два тип волн - продольные и поперечные, они имеют разную амплитуду и, как следствие, это даёт псевдо-уникальное значение в каждой точке;
- Точки на плитке рассчитываются относительно её смещения от центра (некой условной точки).
Поехали
Начал я с того, что нарисовал квадрат на экране. Для этого написал небольшой класс на js, создающий программу отрисовки для webgl. Больше про шейдеры можете посмотреть тут: много, но не сложно.
Отличительная особенность программ отрисовки, что их может быть много: первая рисует фон, вторая - водоросли, третья - воду, четвёртая - корабли поверх всего этого. Каждая программа будет обслуживаться самостоятельно и самостоятельно отрисовываться.
Да, так много текста и только для того, чтобы подготовиться к рисованию.
Коротко по коду (функция render): используем созданную программу, выбираем все точки, которые надо отобразить, отрисовываем их командой drawArrays. В конечном итоге, получился такой вот рисунок.
Хоть планировался синий квадрат, получился синий прямоугольник - издержки использования webgl. Но я это поправлю позже.
Время перспективы
Если коротко, то взгляд сверху/снизу/со стороны - это математическое вычисление. Перемножаешь координаты точек на подготовленную матрицу (подробности тут) - получаешь искажение изображения, которое выглядит как трёхмерная графика. И никакого волшебства.
Обзор с помощью мыши
Самовращающаяся плитка это хорошо, но управляемая мышкой - ещё лучше.
Данный код отвечает за "взгляд" камеры. Есть точнее, то положения мышки отслеживается и, на его основании, вычисляются углы поворота по осям X и Z, а также смещение.
Вся композиция сцены строится относительно центра; плитки, из которых формируется морская гладь, также, будут выстраиваться хоть и по матрице, но относительно центра. Смещение каждой плитки - это отдельное вычисление смещения (translate), отдельно центра. "Камера" же - это отдельные вычисления, которые будут "наложены" поверх вычислений положений плиток - это даст не только правильное размещение на экране, но и нужный угол обзора.
Угловатая плитка есть - сделаем её волнистой
Внешний источник волн
Сделаем так: модуль написанный на rust будет генерировать точки "волнующейся" плитки. Для всех плиток, что будут на экране.
Проблемы для одной плитки нет, а вот когда их будет, скажем, с десяток - они начнутся: всё ведь считается на процессоре. Получается примерно 10 кадров в секунду для 25 плиток. Но можно оптимизировать: перенести вычисления внутрь шейдерной программы. Это позволит производить все вычисления внутри GPU. Да, есть свои издержки такого подхода, но выйдет быстрее.
В оптимизации мы перестали передавать все точки плитки, а просто передаём временное смещение, в зависимости от которого считается амплитуда волны. Надо сказать, кода получилось примерно в 5 раз меньше, чем при использовании wasm-модуля. Поэтому я от него откажусь.
Много-много плиток
А теперь давайте разместим много плиток. Среднее время рассчёта схемы для первого варианта - примерно 170 милисекунд; второго варианта, в среднем, 0.2 милисекунды.
Ну вот и готова водная гладь. Хотел код приложить, но там прямо очень много получилось.
Заключение
Простые выводы, которые можно сделать из всего этого безобразия:
- Даже такую простую вещь, как колышущееся море можно делать долго и трудно, потому что математики там много, а для изменения поведения даже в пустяках требует, порой, правку и программу-исполнение, и шейдеры.
- Все основные вычисления лучше перекладывать на GPU и не испытывать надежды, мол, посчитаю на процессоре и всё будет хорошо. Не будет. Даже вычисление изменения поведения 25 точек для каждой из 25 плиток на экране может просадить производительность отрисовки до 10 кадров в секунду, зато на видеокарте всё считается просто и быстро. Да и код проще.
- Надежды на то, что wasm будет работать быстрее - необоснованны. Наверное есть какие-то задачи, где прироста производительности добиться удастся, но для большинства задач - это пустая трата времени.
- Трёхмерная графика - это не про милых анимированных котиков, это про математику.
P.S. посмотреть, как это выглядит можно тут, исходники здесь.