Your second 3D shader¶
На высоком уровне, то, что делает Godot, дает пользователю кучу параметров, которые можно установить по желанию (AO
, SSS_Strength
, RIM
и т.д.). Эти параметры соответствуют различным сложным эффектам (Ambient Occlusion, SubSurface Scattering, Rim Lighting и т.д.). Если эти параметры не записаны, код выбрасывается до компиляции, и таким образом шейдер не несет затрат на дополнительную функцию. Это позволяет пользователям легко получить сложное PBR-корректное затенение без написания сложных шейдеров. Конечно, Godot также позволяет игнорировать все эти параметры и написать полностью индивидуальный шейдер.
For a full list of these parameters see the spatial shader reference doc.
A difference between the vertex function and a fragment function is that the
vertex function runs per vertex and sets properties such as VERTEX
(position) and NORMAL
, while the fragment shader runs per pixel and, most
importantly, sets the ALBEDO
color of the Mesh.
Your first spatial fragment function¶
As mentioned in the previous part of this tutorial. The standard use of the
fragment function in Godot is to set up different material properties and let
Godot handle the rest. In order to provide even more flexibility, Godot also
provides things called render modes. Render modes are set at the top of the
shader, directly below shader_type
, and they specify what sort of
functionality you want the built-in aspects of the shader to have.
For example, if you do not want to have lights affect an object, set the render
mode to unshaded
:
render_mode unshaded;
You can also stack multiple render modes together. For example, if you want to use toon shading instead of more-realistic PBR shading, set the diffuse mode and specular mode to toon:
render_mode diffuse_toon, specular_toon;
This model of built-in functionality allows you to write complex custom shaders by changing only a few parameters.
Полный список режимов рендера смотрите в Spatial shader reference.
In this part of the tutorial, we will walk through how to take the bumpy terrain from the previous part and turn it into an ocean.
First let's set the color of the water. We do that by setting ALBEDO
.
ALBEDO
is a vec3
that contains the color of the object.
Давайте установим красивый оттенок синего.
void fragment() {
ALBEDO = vec3(0.1, 0.3, 0.5);
}

Мы установили очень темный оттенок синего, потому что большая часть синевы воды будет обусловлена отражением от неба.
Модель PBR, которую использует Godot, опирается на два основных параметра: МЕТАЛЛИЧЕСКИЙ
и РОГАННОСТЬ
.
ROUGHNESS
определяет, насколько гладкой/шероховатой является поверхность материала. Низкая ROUGHNESS
сделает материал похожим на блестящий пластик, в то время как высокая шероховатость заставит материал казаться более твердым по цвету.
METALLIC
определяет, насколько объект похож на металл. Лучше установить значение, близкое к 0
или 1
. Думайте о METALLIC
как об изменении баланса между отражением и цветом ALBEDO
. При высоком METALLIC
почти полностью игнорируется ALBEDO
и выглядит как зеркало неба. В то время как низкий METALLIC
имеет более равное представление цвета неба и ALBEDO
.
ROUGHNESS
увеличивается от 0
к 1
слева направо, а METALLIC
увеличивается от 0
к 1
сверху вниз.

Примечание
METALLIC
должен быть близок к 0
или 1
для правильного затенения PBR. Для смешивания материалов устанавливайте только между ними.
Вода не является металлом, поэтому мы установим ее свойство METALLIC
на 0.0
. Вода также обладает высокой отражательной способностью, поэтому мы установим ее свойство ROUGHNESS
на довольно низком уровне.
void fragment() {
METALLIC = 0.0;
ROUGHNESS = 0.01;
ALBEDO = vec3(0.1, 0.3, 0.5);
}

Теперь у нас есть гладкая поверхность, похожая на пластик. Пришло время подумать о некоторых свойствах воды, которые мы хотим имитировать. Есть два основных свойства, которые позволят нам превратить странную пластиковую поверхность в красивую стилизованную воду. Первое - это зеркальные отражения. Спекулярные отражения - это яркие пятна, которые вы видите, когда солнце отражается прямо в ваш глаз. Второе - отражение Френеля. Отражение Френеля - это свойство объектов становиться более отражающими под небольшим углом. Именно по этой причине вы можете видеть воду под собой, а вдали от вас отражается небо.
Для того чтобы увеличить зеркальные отражения, мы сделаем две вещи. Во-первых, мы изменим режим рендеринга для specular на toon, потому что режим рендеринга toon имеет больше спекулярных бликов.
render_mode specular_toon;

Во-вторых, мы добавим ободковое освещение. Окантовочное освещение усиливает эффект света под скользящими углами. Обычно оно используется для имитации прохождения света через ткань на краях объекта, но мы будем использовать его здесь, чтобы добиться красивого эффекта воды.
void fragment() {
RIM = 0.2;
METALLIC = 0.0;
ROUGHNESS = 0.01;
ALBEDO = vec3(0.1, 0.3, 0.5);
}

Чтобы добавить френелевое отражение, мы вычислим член Френеля в нашем фрагментном шейдере. Здесь мы не будем использовать реальный терм Френеля по соображениям производительности. Вместо этого мы аппроксимируем его с помощью точечного произведения векторов NORMAL
и VIEW
. Вектор NORMAL
направлен в сторону от поверхности сетки, а вектор VIEW
- это направление между вашим глазом и этой точкой на поверхности. Точечное произведение между ними - удобный способ определить, когда вы смотрите на поверхность в упор или под углом.
float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
And mix it into both ROUGHNESS
and ALBEDO
. This is the benefit of
ShaderMaterials over SpatialMaterials. With SpatialMaterial, we could set
these properties with a texture, or to a flat number. But with shaders we can
set them based on any mathematical function that we can dream up.
void fragment() {
float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
RIM = 0.2;
METALLIC = 0.0;
ROUGHNESS = 0.01 * (1.0 - fresnel);
ALBEDO = vec3(0.1, 0.3, 0.5) + (0.1 * fresnel);
}

And now, with only 5 lines of code, you can have complex looking water. Now that
we have lighting, this water is looking too bright. Let's darken it. This is
done easily by decreasing the values of the vec3
we pass into ALBEDO
.
Let's set them to vec3(0.01, 0.03, 0.05)
.

Animating with TIME
¶
Going back to the vertex function, we can animate the waves using the built-in
variable TIME
.
TIME
is a built-in variable that is accessible from the vertex and fragment
functions.
In the last tutorial we calculated height by reading from a heightmap. For this
tutorial, we will do the same. Put the heightmap code in a function called
height()
.
float height(vec2 position) {
return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
}
In order to use TIME
in the height()
function, we need to pass it in.
float height(vec2 position, float time) {
}
And make sure to correctly pass it in inside the vertex function.
void vertex() {
vec2 pos = VERTEX.xz;
float k = height(pos, TIME);
VERTEX.y = k;
}
Instead of using a normalmap to calculate normals. We are going to compute them
manually in the vertex()
function. To do so use the following line of code.
NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));
We need to compute NORMAL
manually because in the next section we will be
using math to create complex-looking waves.
Now, we are going to make the height()
function a little more complicated by
offsetting position
by the cosine of TIME
.
float height(vec2 position, float time) {
vec2 offset = 0.01 * cos(position + time);
return texture(noise, (position / 10.0) - offset).x;
}
This results in waves that move slowly, but not in a very natural way. The next section will dig deeper into using shaders to create more complex effects, in this case realistic waves, by adding a few more mathematical functions.
Advanced effects: waves¶
What makes shaders so powerful is that you can achieve complex effects by using
math. To illustrate this, we are going to take our waves to the next level by
modifying the height()
function and by introducing a new function called
wave()
.
wave()
has one parameter, position
, which is the same as it is in
height()
.
We are going to call wave()
multiple times in height()
in order to fake
the way waves look.
float wave(vec2 position){
position += texture(noise, position / 10.0).x * 2.0 - 1.0;
vec2 wv = 1.0 - abs(sin(position));
return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
}
At first this looks complicated. So let's go through it line-by-line.
position += texture(noise, position / 10.0).x * 2.0 - 1.0;
Offset the position by the noise
texture. This will make the waves curve, so
they won't be straight lines completely aligned with the grid.
vec2 wv = 1.0 - abs(sin(position));
Define a wave-like function using sin()
and position
. Normally sin()
waves are very round. We use abs()
to absolute to give them a sharp ridge
and constrain them to the 0-1 range. And then we subtract it from 1.0
to put
the peak on top.
return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
Multiply the x-directional wave by the y-directional wave and raise it to a
power to sharpen the peaks. Then subtract that from 1.0
so that the ridges
become peaks and raise that to a power to sharpen the ridges.
We can now replace the contents of our height()
function with wave()
.
float height(vec2 position, float time) {
float h = wave(position);
return h;
}
Using this, you get:

The shape of the sin wave is too obvious. So let's spread the waves out a bit.
We do this by scaling position
.
float height(vec2 position, float time) {
float h = wave(position * 0.4);
return h;
}
Now it looks much better.

We can do even better if we layer multiple waves on top of each other at varying frequencies and amplitudes. What this means is that we are going to scale position for each one to make the waves thinner or wider (frequency). And we are going to multiply the output of the wave to make them shorter or taller (amplitude).
Here is an example for how you could layer the four waves to achieve nicer looking waves.
float height(vec2 position, float time) {
float d = wave((position + time) * 0.4) * 0.3;
d += wave((position - time) * 0.3) * 0.3;
d += wave((position + time) * 0.5) * 0.2;
d += wave((position - time) * 0.6) * 0.2;
return d;
}
Note that we add time to two and subtract it from the other two. This makes the
waves move in different directions creating a complex effect. Also note that the
amplitudes (the number the result is multiplied by) all add up to 1.0
. This
keeps the wave in the 0-1 range.
With this code you should end up with more complex looking waves and all you had to do was add a bit of math!

For more information about Spatial shaders read the Shading Language doc and the Spatial Shaders doc. Also look at more advanced tutorials in the Shading section and the 3D sections.