Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Ваш другий 3D-шейдер

На високому рівні Godot надає користувачеві низку параметрів, які можна за бажанням встановити (AO, SSS_Strength, RIM тощо). Ці параметри відповідають різним комплексним ефектам (Ambient Occlusion, SubSurface Scattering, Rim Lighting тощо). Якщо код не записується, він викидається до компіляції, тому шейдер не бере на себе витрати на додаткові функції. Це спрощує для користувачів складне PBR-коректне затінення без написання складних шейдерів. Звичайно, Godot також дозволяє ігнорувати всі ці параметри та писати повністю налаштований шейдер.

Щоб отримати повний список цих параметрів, перегляньте довідковий документ spatial shader.

Різниця між вершинною функцією та фрагментною функцією полягає в тому, що вершинна функція виконується для кожної вершини та встановлює такі властивості, як VERTEX (позиція) і NORMAL, тоді як фрагментний шейдер працює для кожного пікселя і, що найважливіше, встановлює колір ALBEDO для MeshInstance3D.

Ваша перша функція просторового фрагмента

Як згадувалося в попередній частині цього посібника. Стандартне використання функції фрагмента в Godot полягає в тому, щоб налаштувати різні властивості матеріалу та дозволити Godot впоратися з рештою. Щоб забезпечити ще більшу гнучкість, Godot також надає речі, які називаються режимами візуалізації. Режими візуалізації встановлюються у верхній частині шейдера, безпосередньо під shader_type, і вони визначають, яку функціональність ви хочете мати у вбудованих аспектах шейдера.

Наприклад, якщо ви не хочете, щоб світло впливало на об’єкт, встановіть режим візуалізації на unshaded:

render_mode unshaded;

Ви також можете поєднати кілька режимів візуалізації разом. Наприклад, якщо ви хочете використовувати мультяшне затінення замість більш реалістичного затінення PBR, встановіть дифузний режим і дзеркальний режим на мультяшне:

render_mode diffuse_toon, specular_toon;

Ця модель вбудованої функціональності дозволяє писати складні власні шейдери, змінюючи лише кілька параметрів.

Щоб отримати повний список режимів візуалізації, перегляньте Spatial shader reference.

У цій частині підручника ми розповімо, як взяти вибоїсту місцевість із попередньої частини та перетворити її на океан.

Спочатку встановимо колір води. Ми робимо це, встановлюючи ALBEDO.

ALBEDO - це vec3, який містить колір об'єкта.

Давайте встановимо гарний відтінок синього.

void fragment() {
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/albedo.png

Ми встановлюємо дуже темний відтінок синього, тому що більша частина блакитного відтінку води походить від відблисків неба.

Модель PBR, яку використовує Godot, спирається на два основні параметри: METALLIC і ROUGHNESS.

ROUGHNESS визначає, наскільки гладкою/шорсткою є поверхня матеріалу. Низька ROUGHNESS надасть матеріалу вигляду блискучого пластику, тоді як висока шорсткість робить матеріал більш однотонним за кольором.

METALLIC визначає, наскільки об'єкт схожий на метал. Краще встановити близько до 0 або 1. Подумайте про METALLIC як про зміну балансу між відображенням і кольором ALBEDO. Високий METALLIC майже ігнорує ALBEDO взагалі, і виглядає як дзеркало неба. Тоді як низький METALLIC має більш рівне представлення кольору неба та кольору ALBEDO.

ROUGHNESS збільшується від 0 до 1 зліва направо, а METALLIC збільшується від 0 до 1 зверху вниз.

../../../_images/PBR.png

Примітка

METALLIC має бути близьким до 0 або 1 для правильного затінення PBR. Встановлюйте його лише між ними для змішування між матеріалами.

Вода не є металом, тому ми встановимо для її властивості METALLIC значення 0.0. Вода також має високу відбивну здатність, тому ми також встановимо її властивість ROUGHNESS досить низькою.

void fragment() {
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/plastic.png

Тепер ми маємо гладку пластикову поверхню. Настав час подумати про деякі особливі властивості води, які ми хочемо наслідувати. Є два основних, які перенесуть це від дивної пластикової поверхні до красивої стилізованої води. По-перше, це дзеркальні відбиття. Дзеркальне відображення – це ті яскраві плями, які ви бачите звідти, де сонце відбивається прямо у ваше око. По-друге, це коефіцієнт відбиття Френеля. Коефіцієнт відбиття Френеля — це властивість об’єктів ставати більш відбиваючими під невеликими кутами. Це причина, чому ви можете бачити воду під собою, але далі вона відображає небо.

Щоб збільшити дзеркальні відбиття, ми зробимо дві речі. По-перше, ми змінимо режим візуалізації для specular на toon, оскільки режим рендерингу toon має більші відблиски.

render_mode specular_toon;
../../../_images/specular-toon.png

По-друге, ми додамо освітлення обода. Підсвічування обода посилює ефект світла під кутами огляду. Зазвичай він використовується для імітації того, як світло проходить через тканину на краях об’єкта, але ми використаємо його тут, щоб допомогти досягти приємного водянистого ефекту.

void fragment() {
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/rim.png

Щоб додати коефіцієнт відбиття френеля, ми обчислимо член френеля в нашому фрагментному шейдері. Тут ми не будемо використовувати справжній термін Френеля з міркувань продуктивності. Натомість ми апроксимуємо його за допомогою скалярного добутку векторів NORMAL і VIEW. Вектор NORMAL вказує від поверхні сітки, тоді як вектор VIEW є напрямком між вашим оком і цією точкою на поверхні. Скалярний добуток між ними — це зручний спосіб визначити, що ви дивитеся на поверхню прямо чи під кутом.

float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));

І змішайте це в ROUGHNESS і ALBEDO. Це перевага ShaderMaterials над StandardMaterial3Ds. За допомогою StandardMaterial3D ми могли встановити ці властивості за допомогою текстури або рівного числа. Але за допомогою шейдерів ми можемо встановити їх на основі будь-якої математичної функції, про яку тільки можемо мріяти.

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);
}
../../../_images/fresnel.png

І тепер, маючи лише 5 рядків коду, ви можете мати складну воду. Тепер, коли у нас є освітлення, ця вода виглядає занадто яскравою. Давайте затемнити його. Це легко зробити, зменшивши значення vec3, які ми передаємо в ALBEDO. Давайте встановимо для них значення vec3(0.01, 0.03, 0.05).

../../../_images/dark-water.png

Анімація за допомогою TIME

Повертаючись до вершинної функції, ми можемо анімувати хвилі за допомогою вбудованої змінної TIME.

TIME — це вбудована змінна, яка доступна з функцій вершин і фрагментів.

У минулому посібнику ми обчислювали висоту, читаючи з карти висот. Для цього підручника ми зробимо те саме. Помістіть код карти висот у функцію під назвою height().

float height(vec2 position) {
  return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
}

Щоб використовувати TIME у функції height(), нам потрібно передати його.

float height(vec2 position, float time) {
}

І переконайтеся, що ви правильно передали його всередину вершинної функції.

void vertex() {
  vec2 pos = VERTEX.xz;
  float k = height(pos, TIME);
  VERTEX.y = k;
}

Замість використання карти нормалей для обчислення нормалей. Ми збираємося обчислити їх вручну у функції vertex(). Для цього використовуйте наступний рядок коду.

NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));

Нам потрібно обчислити NORMAL вручну, оскільки в наступному розділі ми будемо використовувати математику для створення складних на вигляд хвиль.

Тепер ми збираємося зробити функцію height() трохи складнішою, зміщуючи position на косинус TIME.

float height(vec2 position, float time) {
  vec2 offset = 0.01 * cos(position + time);
  return texture(noise, (position / 10.0) - offset).x;
}

Це призводить до хвиль, які рухаються повільно, але не дуже природним чином. У наступному розділі глибше буде розглянуто використання шейдерів для створення складніших ефектів, у цьому випадку реалістичних хвиль, додавши ще кілька математичних функцій.

Розширені ефекти: хвилі

Що робить шейдери такими потужними, так це те, що ви можете досягти складних ефектів за допомогою математики. Щоб проілюструвати це, ми збираємося підняти наші хвилі на наступний рівень, змінивши функцію height() і ввівши нову функцію під назвою wave().

wave() має один параметр, position, який є таким самим, як і у height().

Ми будемо викликати wave() кілька разів у height(), щоб підробити вигляд хвиль.

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);
}

Спочатку це виглядає складно. Тож давайте розглянемо це рядок за рядком.

position += texture(noise, position / 10.0).x * 2.0 - 1.0;

Зміщення позиції текстурою шум. Це зробить хвилі кривими, тому вони не будуть прямими лініями, повністю вирівняними з сіткою.

vec2 wv = 1.0 - abs(sin(position));

Визначте хвилеподібну функцію за допомогою sin() і position. Зазвичай хвилі sin() дуже круглі. Ми використовуємо abs() для абсолютизації, щоб надати їм різкий хребет і обмежити їх діапазоном 0-1. Потім ми віднімаємо його від 1.0, щоб поставити пік зверху.

return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);

Помножте хвилю, спрямовану по x, на хвилю, спрямовану по осі y, і зведіть її до степеня, щоб піки були різкішими. Потім відніміть це значення від 1.0 так, щоб хребти стали піками, і зведіть це до степеня, щоб загострити хребти.

Тепер ми можемо замінити вміст нашої функції height() на wave().

float height(vec2 position, float time) {
  float h = wave(position);
  return h;
}

Використовуючи це, ви отримуєте:

../../../_images/wave1.png

Форма хвилі гріха надто очевидна. Тож давайте трохи розвіємо хвилі. Ми робимо це шляхом масштабування position.

float height(vec2 position, float time) {
  float h = wave(position * 0.4);
  return h;
}

Тепер це виглядає набагато краще.

../../../_images/wave2.png

Ми можемо зробити ще краще, якщо накладемо кілька хвиль одна на одну з різними частотами та амплітудами. Це означає, що ми будемо масштабувати положення для кожного з них, щоб зробити хвилі тоншими або ширшими (частота). І ми збираємося помножити вихід хвилі, щоб зробити їх коротшими або вищими (амплітуда).

Ось приклад того, як ви можете нашаровувати чотири хвилі, щоб отримати красивіші хвилі.

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;
}

Зверніть увагу, що ми додаємо час до двох і віднімаємо його від двох інших. Це змушує хвилі рухатися в різних напрямках, створюючи складний ефект. Також зауважте, що всі амплітуди (число, на яке множиться результат) у сумі становлять 1,0. Це тримає хвилю в діапазоні 0-1.

З цим кодом у вас повинні вийти складніші хвилі, і все, що вам потрібно зробити, це додати трохи математики!

../../../_images/wave3.png

Щоб дізнатися більше про просторові шейдери, ознайомтеся з документами Shading Language і Spatial Shaders. Також подивіться на більш просунуті підручники в розділі Shading і 3D розділах.