Old Byte Tavern | Indie Games by gmaker

Old Byte Tavern | Indie Games by gmaker 

Indie game dev making pixel worlds and stories

14subscribers

13posts

Water Shader Graph Development - Шейдер Граф для воды (devlog Tiny Fishing 3D #0)

Water.shadergraph100.63 Kb
mat
WaterMaterial.mat1.77 Kb

🔥 Введение | Introduction

Помните игру про рыбалку, о которой я писал. Решил я её всё же написать, но в 3D и сделать упор на нарратив. Чтоб с ходу раскрывалась история. Я себе это вижу так:

Remember the fishing game I wrote about? I decided to finally make it, but in 3D, and with a strong focus on narrative. I want the story to unfold right away. Here’s how I see it:
Наше поселение будет уже находиться у водоёма, и сидеть будут все на каком-нибудь лобном месте у костра, рядом будут хижины. И вот какой-то гонец приносит известия о том, что идёт стихийное бедствие, пожар или активный вулкан, либо же мы замечаем в первый день в дали горящие леса. И вот мы решаемся запастись в последний день всем необходимым и рвануть через леса, добывая себе еду. Но животные, скорее всего, все уже убежали и нам остаётся бродить по водоёмам в поисках рыбы. Так на глобальной карте мы будем выбирать, куда идём на следующий день. Всё как я описывал в 2D прототипе.

Our settlement will already be located by the water, people sitting by the fire with huts nearby. A messenger arrives with news of a natural disaster - a fire, an active volcano, or we notice forests burning in the distance on the first day. So we decide to stock up on supplies and head into the forests, finding food along the way. But most animals will likely already be gone, so we’ll wander along rivers and lakes looking for fish. On the global map, we’ll choose where to go the next day - just like I described in the 2D prototype.
Я уже написал генерацию ландшафта с водоёмом, где в дальнейшем будет находиться наш переносной лагерь.

I’ve already written the terrain generation with a body of water where our portable camp will later be placed.

🎣 Вершинный шейдер - волны на геометрии | Vertex Shader - Waves on Geometry

Потом добавятся уже детали в виде леса, места для рыбалки и т.д.
Later, details will be added like forests, fishing spots, etc.
И сегодня (по просьбе одного из подписчиков) я отступлю немного от нарратива и займусь написанием шейдера для воды. Точнее шейдер графа, я попробую объяснить нюансы, чтобы вы могли на его основе или даже с нуля создать свой шейдер. Итак, всё что нам надо - это создать пустой Shader Graph.

And today (by request from one of my subscribers) I’ll step away from the narrative a bit and work on a water shader. Specifically, a Shader Graph. I’ll try to explain the nuances so you can build your own shader from it, or even from scratch. All we need is to create an empty Shader Graph.
Выглядит он так

It looks like this
Итак, перед нами по сути есть два шейдера - вертексный и фрагментный.

So basically, we have two shaders here - Vertex and Fragment.
Поговорим сперва про первый из них. Он исполняется для каждой вершины в геометрическом объекте - в нашем случае это просто Plane меш, т.е. плоский объект с большим количеством треугольников. В дальнейшем, чем больше треугольников, тем выше разрешение, но мы делаем low-poly игру. Поэтому они у нас достаточно большие.

Let’s talk about the first one. It runs for every vertex in a geometry object - in our case, a simple Plane mesh, a flat object with lots of triangles. The more triangles, the higher the resolution. But since we’re making a low-poly game, they are pretty large.
Итак, это наша вода. Когда мы кидаем эту геометрию в видеокарту, вертексный шейдер проходит по каждой вершине, чтобы отрисовать её в нужном месте. Для этого ему передаётся несколько матриц типа ViewMatrix, ModelMatrix, ProjectionMatrix, и просто локальные координаты вершины в aPos. Вот реальный код, как это происходит:

So, this is our water. When we send this geometry to the GPU, the Vertex Shader processes every vertex to render it in the right place. For that, it’s given matrices like ViewMatrix, ModelMatrix, ProjectionMatrix, and the local vertex coordinates in aPos. Here’s real code showing how this happens:
// WebGL 1.0 / GLES2
attribute vec3 aPos;
uniform mat4 uModel; // ModelMatrix
uniform mat4 uView; // ViewMatrix
uniform mat4 uProj; // ProjectionMatrix
void main() {
    gl_Position = uProj * uView * uModel * vec4(aPos, 1.0);
}
И получается, мы должны вернуть в gl_Position уже преобразованную к мировым координатам локальную точку. Но мы не будем писать код - это просто для понимания. Итак, верхний кусок Shader Graph Vertex на картинке выше хочет, чтобы мы передали ему Position (Normal, Tangent мы не используем). Если оставить дефолт, то точка будет рисоваться там, где должна. Но мы хотим волны, ведь так?

So basically, we must return a world-space point into gl_Position. But we won’t be coding directly - this is just for understanding. So, the upper part of the Shader Graph Vertex node (see picture) wants us to feed Position (Normal, Tangent we won’t use). If we leave defaults, the point draws correctly. But we want waves, right?
Поэтому нам надо в Position передать изменённую координату. Как её изменять? Например, используя Perlin Noise или Gradient Noise, смещая его по времени и умножая на нормаль вверх. Дальше просто складываем с реальной позицией. И получится, что точка будет плавно смещаться вверх-вниз на основе шума, который мы можем настроить (высота волны и скорость).

So we need to pass a modified coordinate into Position. How do we modify it? For example, by using Perlin Noise or Gradient Noise, offset by time, and multiplied by the upward normal. Then add it to the actual position. The result is that the vertex will smoothly move up and down based on noise, adjustable with parameters (wave height and speed).
Заводим две переменные (в реале это Uniform для шейдера, если бы мы писали код, т.е. значения, которые мы передаем из-вне).
We define two variables (in actual shader code these would be uniforms, i.e. values passed into the shader from outside).
Они нам помогут в реальности играться с параметрами. Далее создаем пару блоков, это Time, просто время которое будет тикать и нашу переменную WaveSpeed перемножаем на время.
They will help us tweak the parameters in real time. Next, we create a couple of nodes: Time (a ticking time value) and multiply our WaveSpeed parameter by Time.
Далее для Gradient Noise нам потребуется 2D координата и параметр scale, которым мы будем контролировать высоту волн через WaveHeight. На выходе получится "карта высот", изменяющаяся во времени от 0 до 1. Для этого используем Tiling And Offset модуль, он позволяет влиять на UV-координаты. Дальше мы умножаем результат GradientNoise на нормаль, как я говорил, и прибавляем к реальной позиции.
Next, Gradient Noise needs a 2D coordinate and a scale parameter, which we’ll control with WaveHeight. The output is essentially a "height map" changing over time from 0 to 1. For this, we use the Tiling And Offset node, which lets us affect UV coordinates. Then multiply the Gradient Noise output by the normal, and add it to the actual position.
Тут видно, как меняется зерно шума при изменении WaveHeight и скорость смещения при изменении WaveSpeed.

Here we can see how the noise grain changes with WaveHeight and how speed shifts with WaveSpeed.
Теперь подключаем это всё к Vertex → Position:

Now connect this all to Vertex → Position:
И смотрим, что же происходит с нашими вершинами:
And let’s see what happens with our vertices:
Тадам! Теперь мы видим колебания воды. Это можно сделать по-разному, я показал один из самых простых способов.
Ta-dah! Now we see water oscillation. There are many ways to do this, but I’ve shown one of the simplest.

🌊 Фрагментный шейдер - цвет и рябь | Fragment Shader - Color and Ripples

Ну и давайте разберёмся с фрагментным шейдером. Он, в отличие от вершинного, работает не по вершинам, а по каждому пикселю треугольника и решает, каким цветом его рисовать. Обычно цвет интерполируется между вершинами, но мы можем вмешаться и менять цвет как захотим.

Now let’s dive into the Fragment Shader. Unlike the vertex one, it works per pixel of each triangle and decides what color to paint it. Normally color is interpolated between vertices, but we can intervene and change it however we like.
Нам понадобятся переменные:

We’ll need some variables:
Color BaseColor - основной цвет воды | base water color
Float RippleSpeed - скорость ряби | ripple speed
Float RippleDensity - плотность ряби | ripple density
Float RippleSlimness - тонкость (толщина) ряби | ripple slimness
Color RippleColor - цвет ряби | ripple color
Vec2 Resolution - разрешение эффекта | effect resolution
Для волн можно использовать разные техники (например, текстуры), но мы будем генерировать их на лету через Диаграмму Вороного (Voronoi Diagram). Она встроена в Shader Graph и создаёт переливы. Мы поиграемся с частотой, цветом, смещением по времени и разрешением.

There are many ways to create ripples (like textures), but we’ll generate them on the fly using the Voronoi Diagram. It’s built into Shader Graph and creates patterns similar to shimmering ripples. We’ll tweak frequency, color, time offset, and resolution.
Чтобы получить квантование (дискретизацию), берём UV координаты, умножаем на Resolution, округляем до целых и умножаем обратно через Reciprocal (1/X). Получаем дискретные UV-координаты, которыми можно управлять.

To get quantization (discretization), we take UV coordinates, multiply by Resolution, floor them, then scale back using Reciprocal (1/X). That gives us quantized UVs that we can control.
Затем добавляем Radial Shear (радиальное искажение UV), плюс время * RippleSpeed.

Then we add Radial Shear (distorting UVs radially), plus time * RippleSpeed.
Дальше подаём это в Voronoi, задаём Density, угол и скорость. Возводим в степень, чтобы усилить контраст: оставляем белое. Потом добавляем RippleColor, смешиваем с BaseColor воды.
Next, we feed this into Voronoi, setting Density, angle, and speed. Raise it to a power to enhance contrast, keeping the white areas. Then multiply by RippleColor, and finally add BaseColor of the water.
Подключаем это всё во Fragment → Base Color.
Connect it all to Fragment → Base Color.
Смотрим результат:
See the result:

✨ Итог

Теперь мы можем видеть подобие колебания воды. Его можно сделать разными способами, я показал один из самых простых. Для low-poly игры этого достаточно, но в будущем можно добавить отражения, прозрачность и глубину.

Now we can see water-like ripples. This can be done in many ways, but I showed one of the simplest. For a low-poly game, it’s enough, but later we can add reflections, transparency, and depth.

🚀 Поддержка проекта | Support the Project

Эта статья открыта для всех - мне важно, чтобы как можно больше людей могли вдохновиться и попробовать делать свои шейдеры. Но если вы хотите поддержать разработку Tiny Fishing 3D, то подписка на Boosty даёт несколько бонусов:

This article is open for everyone - I want as many people as possible to get inspired and try making their own shaders. But if you’d like to support the development of Tiny Fishing 3D, subscribing on Boosty gives you some bonuses:
  • 🎁 доступ к файлам проекта (Shader Graph, материалы, тестовые сцены)
  • 🎁 access to project files (Shader Graph, materials, test scenes)
  • ⏱️ ранний доступ к девлогам и видео до их публичного выхода
  • ⏱️ early access to devlogs and videos before public release
  • 🐟 закрытые посты с дополнительными разборами фич Tiny Fishing 3D
  • 🐟 exclusive posts with extra breakdowns of Tiny Fishing 3D features
  • 💬 возможность предлагать темы для следующих постов
  • 💬 ability to suggest topics for upcoming posts
Каждая подписка помогает мне тратить больше времени на разработку игры 🙌
Every subscription helps me spend more time developing the game 🙌
👉 Присоединяйтесь, чтобы вместе развивать Tiny Fishing 3D:
👉 Join and help grow Tiny Fishing 3D together:
Стать подписчиком / Become a supporter
Subscription levels4

Tiny Pixel

$0.7 per month
Just a small thank-you! Your name will be added to the credits of my games.

Beta Tester

$2.09 per month
 Get early access to builds, test new features, and help shape the development.

Tavern Patron

$4.2 per month
Everything from previous tier + exclusive devlogs, private polls, and name shout-out.

Game Spirit

$7 per month
All above + access to concept art, internal docs, and a Discord “Supporter” role.
Go up