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.
Checking the stable version of the documentation...
Ваш перший 3D-шейдер
Ви вирішили почати писати свій власний просторовий шейдер. Можливо, ви побачили в Інтернеті класний трюк із шейдерами, або виявили, що StandardMaterial3D не зовсім відповідає вашим потребам. У будь-якому випадку, ви вирішили написати власний і тепер вам потрібно зрозуміти, з чого почати.
Цей підручник пояснює, як написати просторовий шейдер, і охоплює більше тем, ніж підручник CanvasItem.
Просторові шейдери мають більше вбудованих функцій, ніж шейдери CanvasItem. Очікування просторових шейдерів полягає в тому, що Godot вже надав функціональні можливості для звичайних випадків використання, і все, що користувачеві потрібно зробити в шейдері, це встановити правильні параметри. Це особливо актуально для робочого процесу PBR (фізично орієнтоване відтворення).
Це підручник із двох частин. У цій першій частині ми створимо рельєф, використовуючи зміщення вершин із карти висот у функції вершин. У second part ми візьмемо концепції з цього підручника та налаштуємо спеціальні матеріали у фрагментному шейдері, написавши шейдер океанської води.
Примітка
Цей підручник передбачає деякі базові знання шейдерів, наприклад типи (vec2, float, sampler2D) і функції. Якщо ви відчуваєте дискомфорт із цими концепціями, найкраще ознайомитися з Книгою шейдерів перед тим, як завершити цей посібник.
Куди призначити мій матеріал
У 3D об’єкти малюються за допомогою Meshes. Сітки — це тип ресурсу, який зберігає геометрію (форму вашого об’єкта) і матеріали (колір і те, як об’єкт реагує на світло) в одиницях, які називаються «поверхнями». Сітка може мати кілька поверхонь або лише одну. Як правило, ви імпортуєте меш з іншої програми (наприклад, Blender). Але Godot також має кілька PrimitiveMeshes, які дозволяють вам додавати базову геометрію до сцени без імпорту Meshes.
Існує кілька типів вузлів, які можна використовувати для малювання сітки. Основний з них MeshInstance3D, але ви також можете використовувати GPUParticles3D, MultiMeshes (з MultiMeshInstance3D), або інші.
Як правило, матеріал пов’язується з певною поверхнею в сітці, але деякі вузли, як-от MeshInstance3D, дозволяють замінити матеріал для певної поверхні або для всіх поверхонь.
Якщо ви встановлюєте матеріал на саму поверхню або сітку, то всі MeshInstance3D, які спільно використовують цю сітку, матимуть спільний доступ до цього матеріалу. Однак, якщо ви хочете повторно використовувати ту саму сітку в кількох екземплярах сітки, але мати різні матеріали для кожного екземпляра, вам слід встановити матеріал у MeshInstance3D.
У цьому підручнику ми встановимо наш матеріал на самій сітці, а не скористаємося можливістю MeshInstance3D перевизначати матеріали.
Налаштування
Додайте новий вузол MeshInstance3D до вашої сцени.
На вкладці інспектора встановіть властивість Mesh MeshInstance3D на новий ресурс PlaneMesh, натиснувши <empty> і вибравши New PlaneMesh. Потім розгорніть ресурс, натиснувши на зображення літака, що з’явиться.
Це додає літак до нашої сцени.
Потім у вікні перегляду натисніть у верхньому лівому куті кнопку Перспектива. У меню, що з’явиться, виберіть Відобразити каркас.
Це дозволить вам побачити трикутники, що утворюють площину.
Тепер встановіть Subdivide Width і Subdivide Depth для PlaneMesh на 32.
Ви бачите, що тепер у MeshInstance3D набагато більше трикутників. Це дасть нам більше вершин для роботи і, таким чином, дозволить додати більше деталей.
PrimitiveMeshes, як і PlaneMesh, мають лише одну поверхню, тому замість масиву матеріалів є лише одна. Встановіть для Material новий ShaderMaterial, а потім розгорніть матеріал, натиснувши на сферу, що з’явиться.
Примітка
Матеріали, успадковані від ресурсу Material, такі як class_StandardMaterial3D і class_ParticleProcessMaterial, можна перетворити на class_ShaderMaterial, а їхні існуючі властивості буде перетворено на супровідний текстовий шейдер. Для цього клацніть правою кнопкою миші на матеріалі в док-станції FileSystem і виберіть Перетворити на ShaderMaterial. Ви також можете зробити це, клацнувши правою кнопкою миші будь-яку властивість, що містить посилання на матеріал в інспекторі.
Тепер установіть для Shader матеріалу новий шейдер, натиснувши <empty> і виберіть New Shader.... Залиште параметри за замовчуванням, дайте назву своєму шейдеру та натисніть Створити.
Клацніть шейдер в інспекторі, і редактор шейдерів має вискочити. Ви готові почати писати свій перший просторовий шейдер!
Магія шейдерів
Новий шейдер уже створено за допомогою змінної shader_type, функції vertex() і fragment(). Перше, що потрібно шейдерам Godot, це декларація типу шейдера. У цьому випадку для shader_type встановлено spatial, тому що це просторовий шейдер.
shader_type spatial;
Функція vertex() визначає, де з’являться вершини вашого MeshInstance3D у фінальній сцені. Ми будемо використовувати його, щоб змістити висоту кожної вершини, щоб наша плоска площина виглядала як маленька місцевість.
Не маючи нічого у функції vertex(), Godot використовуватиме свій типовий вершинний шейдер. Ми можемо почати вносити зміни, додавши один рядок:
void vertex() {
VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}
Додавши цей рядок, ви повинні отримати зображення, схоже на наведене нижче.
Гаразд, давайте розпакуємо це. Значення y VERTEX збільшується. І ми передаємо компоненти x і z VERTEX як аргументи до cos() і sin(); що дає нам хвилеподібний вигляд по осях x і z.
Ми хочемо отримати вигляд невеликих пагорбів; адже. cos() і sin() вже виглядають як пагорби. Ми робимо це, масштабуючи вхідні дані для функцій cos() і sin().
void vertex() {
VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
Це виглядає краще, але воно все ще занадто гостре та повторюване, давайте зробимо це трохи цікавішим.
Карта висоти шуму
Шум є дуже популярним інструментом для підробки вигляду місцевості. Подумайте про це як про функцію косинуса, де у вас є пагорби, що повторюються, за винятком того, що з шумом кожен пагорб має різну висоту.
Godot надає ресурс NoiseTexture2D для створення шумової текстури, до якої можна отримати доступ із шейдера.
Щоб отримати доступ до текстури в шейдері, додайте наступний код у верхній частині шейдера поза функцією vertex().
uniform sampler2D noise;
Це дозволить вам надіслати текстуру шуму в шейдер. Тепер подивіться в інспекторі під ваш матеріал. Ви повинні побачити розділ під назвою Параметри шейдера. Якщо ви відкриєте його, ви побачите параметр під назвою «Шум».
Встановіть цей параметр Noise на новий NoiseTexture2D. Потім у вашому NoiseTexture2D встановіть його властивість Noise на новий FastNoiseLite. Клас FastNoiseLite використовується NoiseTexture2D для створення карти висот.
Після того, як ви налаштуєте його, він повинен виглядати так.
Тепер перейдіть до текстури шуму за допомогою функції texture():
void vertex() {
float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
VERTEX.y += height;
}
texture() приймає текстуру як перший аргумент і vec2 для позиції на текстурі як другий аргумент. Ми використовуємо канали x і z VERTEX, щоб визначити, де на текстурі шукати.
Оскільки координати PlaneMesh знаходяться в діапазоні [-1.0, 1.0] (для розміру 2.0), а координати текстури знаходяться в межах [0.0, 1.0], щоб переналаштувати координати, ми ділимо розмір PlaneMesh на 2.0 і додаємо 0.5.
texture() повертає vec4 каналів r, g, b, a у позиції. Оскільки текстура шуму є відтінками сірого, усі значення однакові, тому ми можемо використовувати будь-який із каналів як висоту. У цьому випадку ми будемо використовувати канал r або x.
Примітка
xyzw те саме, що rgba в GLSL, тому замість texture().x вище, ми можемо використовувати texture().r. Додаткову інформацію див. у документації OpenGL.
Використовуючи цей код, ви можете побачити, як текстура створює випадкові пагорби.
Зараз він занадто гострий, ми хочемо трохи пом’якшити пагорби. Для цього ми будемо використовувати форму. Ви вже використовували уніформу вище, щоб передати текстуру шуму, тепер давайте дізнаємося, як вони працюють.
Уніформа
Uniform variables дозволяють передавати дані з гри в шейдер. Вони дуже корисні для керування ефектами шейдерів. Уніформи можуть мати майже будь-який тип даних, який можна використовувати в шейдері. Щоб використовувати уніформу, ви оголосите її у своєму Shader за допомогою ключового слова uniform.
Зробимо форму, яка змінює висоту місцевості.
uniform float height_scale = 0.5;
Godot дозволяє ініціалізувати уніформу значенням; тут height_scale встановлено на 0,5. Ви можете встановити уніформи з GDScript, викликавши функцію set_shader_parameter() для матеріалу, що відповідає шейдеру. Значення, передане з GDScript, має пріоритет над значенням, яке використовується для його ініціалізації в шейдері.
# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)
Примітка
Зміна уніформ у просторових вузлах відрізняється від вузлів на основі CanvasItem. Тут ми встановлюємо матеріал всередині ресурсу PlaneMesh. В інших мережевих ресурсах вам може знадобитися спочатку отримати доступ до матеріалу, викликавши surface_get_material(). У MeshInstance3D ви отримаєте доступ до матеріалу за допомогою get_surface_material() або material_override.
Пам’ятайте, що рядок, переданий у set_shader_parameter(), має відповідати назві уніфікованої змінної в шейдері. Ви можете використовувати змінну uniform будь-де всередині вашого шейдера. Тут ми будемо використовувати його для встановлення значення висоти замість довільного множення на 0,5.
VERTEX.y += height * height_scale;
Тепер це виглядає набагато краще.
Використовуючи форму, ми навіть можемо змінювати значення кожного кадру, щоб анімувати висоту місцевості. У поєднанні з Tweens це може бути особливо корисним для анімації.
Взаємодія зі світлом
Спочатку вимкніть каркас. Для цього знову відкрийте меню Перспектива у верхньому лівому куті вікна перегляду та виберіть Звичайне відображення. Крім того, на панелі інструментів 3D-сцени вимкніть попередній перегляд сонячного світла.
Зверніть увагу, як колір сітки стає рівним. Це пояснюється тим, що освітлення на ньому рівне. Додамо світла!
Спочатку ми додамо OmniLight3D до сцени та перетягнемо його вгору над рельєфом.
Ви можете побачити, як світло впливає на місцевість, але це виглядає дивно. Проблема в тому, що світло впливає на рельєф так, ніби це плоска площина. Це тому, що світлотіньовий шейдер використовує нормалі з Mesh для обчислення світла.
Нормалі зберігаються в Mesh, але ми змінюємо форму Mesh у шейдері, тому нормалі більше не правильні. Щоб виправити це, ми можемо перерахувати нормалі в шейдері або використати нормальну текстуру, яка відповідає нашому шуму. Ґодо полегшує нам і те, і інше.
Ви можете обчислити нову нормаль вручну у функції вершини, а потім просто встановити NORMAL. З установкою NORMAL Godot виконає всі складні розрахунки освітлення за нас. Ми розглянемо цей метод у наступній частині цього уроку, а поки ми будемо читати нормалі з текстури.
Замість цього ми знову покладатимемося на NoiseTexture для розрахунку нормалей. Ми робимо це, передаючи другу текстуру шуму.
uniform sampler2D normalmap;
Встановіть цю другу однорідну текстуру на іншу NoiseTexture2D з іншою FastNoiseLite. Але цього разу позначте Як звичайна карта.
Якщо у нас є нормалі, які відповідають певній вершині, ми встановлюємо NORMAL, але якщо у вас є нормаль, яка походить від текстури, установіть нормаль за допомогою NORMAL_MAP у функції fragment(). Таким чином Godot автоматично оберне текстуру навколо сітки.
Нарешті, щоб переконатися, що ми читаємо з тих самих місць текстури шуму та текстури карти нормалей, ми передамо позицію VERTEX.xz від функції vertex() до функції fragment(). Ми робимо це за допомогою varying.
Над vertex() визначте variating vec2 під назвою tex_position. І всередині функції vertex() призначте VERTEX.xz до tex_position.
varying vec2 tex_position;
void vertex() {
tex_position = VERTEX.xz / 2.0 + 0.5;
float height = texture(noise, tex_position).x;
VERTEX.y += height * height_scale;
}
І тепер ми можемо отримати доступ до tex_position з функції fragment().
void fragment() {
NORMAL_MAP = texture(normalmap, tex_position).xyz;
}
З установленими нормалями світло тепер динамічно реагує на висоту сітки.
Ми навіть можемо перетягувати світло, і освітлення оновлюватиметься автоматично.
Повний код
Ось повний код для цього підручника. Ви бачите, що це не дуже довго, оскільки Godot впорається з більшістю складних речей за вас.
shader_type spatial;
uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;
varying vec2 tex_position;
void vertex() {
tex_position = VERTEX.xz / 2.0 + 0.5;
float height = texture(noise, tex_position).x;
VERTEX.y += height * height_scale;
}
void fragment() {
NORMAL_MAP = texture(normalmap, tex_position).xyz;
}
Це все для цієї частини. Сподіваємось, тепер ви розумієте основи вершинних шейдерів у Godot. У наступній частині цього посібника ми напишемо функцію фрагмента, яка супроводжуватиме цю вершинну функцію, і розглянемо більш просунуту техніку, щоб перетворити цю місцевість на океан рухомих хвиль.