Tutorials
Tutorials

This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English

Урок із Shadertoy

Shadertoy.com — це сайт, на якому зібрано GL-шейдери, створені користувачами. Це чудове джерело коду шейдерів і натхнення. У цьому уроці ми візьмемо шейдер із Shadertoy і адаптуємо його для роботи в Defold. Передбачається, що ви маєте базове розуміння шейдерів. Якщо потрібно ознайомитися з темою, почніть із посібника з шейдерів.

Ми використаємо шейдер Star Nest, який створив Pablo Andrioli (користувач «Kali» на Shadertoy). Це суто процедурний фрагментний шейдер, що за допомогою математичної чорної магії створює надзвичайно ефектне зоряне поле.

Star Nest

Шейдер містить лише 65 рядків досить складного коду GLSL, але не хвилюйтеся. Ми розглядатимемо його як чорну скриньку, що виконує свою роботу на основі кількох простих вхідних даних. Наше завдання — змінити шейдер так, щоб він взаємодіяв із Defold замість Shadertoy.

Об’єкт для текстурування

Star Nest — це суто фрагментний шейдер, тож нам потрібен лише об’єкт, який він текстуруватиме. Є кілька варіантів: спрайт, карта плиток, GUI або модель. У цьому уроці ми використаємо просту 3D-модель. Причина в тому, що рендеринг моделі легко перетворити на повноекранний ефект — це потрібно, наприклад, для візуальної постобробки.

Можемо почати з порожнього проєкту.

  1. Відкрийте Defold і виберіть Create From Templates.
  2. Виберіть Empty Project.
  3. Укажіть Title і виберіть Location на диску.
  4. Натисніть Create New Project.

Початок роботи

Можна використати вбудовану сітку quad.gltf з builtins/assets/meshes.

За бажанням можна також створити сітку квадратної площини в Blender або будь-якій іншій програмі 3D-моделювання — для зручності координати її 4 вершин мають значення -1 і 1 на осі X та -1 і 1 на осі Y. У Blender вісь Z за замовчуванням спрямована вгору, тому потрібно повернути сітку на 90° навколо осі X. Також переконайтеся, що для сітки згенеровано правильні UV-координати. У Blender виділіть сітку, перейдіть у Edit Mode, а потім виберіть Mesh ▸ UV unwrap... ▸ Unwrap.

Blender — це безкоштовна програма для роботи з 3D з відкритим кодом, яку можна завантажити із сайту blender.org.

Чотирикутник у Blender

  1. Відкрийте файл “main.collection” у Defold і створіть новий ігровий об’єкт (game object) “star-nest”.
  2. Додайте компонент (component) Model до ігрового об’єкта “star-nest”.
  3. Укажіть нашу сітку quad.gltf у властивості Mesh.
  4. Потрібно задати матеріал моделі, тому поки що виберіть вбудований model.material.

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

Чотирикутник у Defold

Створення матеріалу

  1. Створіть новий файл матеріалу star-nest.material: натисніть Right Mouse Button на папці main у панелі Assets, виберіть New->Material і назвіть його star-nest.

Матеріал

  1. Так само створіть програму вершинного шейдера star-nest.vp і програму фрагментного шейдера star-nest.fp:
  2. Відкрийте star-nest.material.
  3. Укажіть star-nest.vp у Vertex Program.
  4. Укажіть star-nest.fp у Fragment Program.
  5. Додайте Vertex Constant, назвіть її “view_proj” і задайте тип Viewproj (від «view projection» — проєкція виду).
  6. Додайте тег “tile” до Tags. Завдяки цьому чотирикутник буде включено в прохід рендерингу, під час якого малюються спрайти й плитки.

Матеріал

Вершинна програма

  1. Відкрийте файл програми вершинного шейдера star-nest.vp. Він має містити такий код:

     #version 140
    
     // positions are in world space
     in vec4 position;
     in vec2 texcoord0;
    
     out vec2 var_texcoord0;
    
     uniform vertex_inputs
     {
         mat4 view_proj;
     };
    
     void main()
     {
         gl_Position = view_proj * vec4(position.xyz, 1.0);
         var_texcoord0 = texcoord0;
     }
    

Фрагментна програма

  1. Відкрийте файл програми фрагментного шейдера star-nest.fp і змініть код так, щоб колір фрагмента визначався на основі складових X і Y UV-координат (var_texcoord0). Так ми перевіримо, чи правильно налаштовано модель:

     #version 140
    
     in vec2 var_texcoord0;
    
     out vec4 out_fragColor;
    
     void main()
     {
         out_fragColor = vec4(var_texcoord0.xy, 0.0, 1.0);
     }
    
  2. У компоненті моделі ігрового об’єкта star-nest у main.collection задайте для властивості Material щойно створений матеріал star-nest.

Тепер редактор має відображати модель із новим шейдером, і ми зможемо чітко побачити, чи правильні UV-координати: нижній лівий кут має бути чорним (0, 0, 0), верхній лівий — зеленим (0, 1, 0), верхній правий — жовтим (1, 1, 0), а нижній правий — червоним (1, 0, 0):

Чотирикутник у Defold

Камера

Тепер можна запустити проєкт (Project->Build або сполучення клавіш Ctrl/Cmd + B), але ми побачимо чорний екран (ну, майже — можливо, за винятком одного крихітного пікселя в нижньому лівому куті). Це тому, що камери немає, а стандартний скрипт рендерингу використовує простий резервний варіант, який показує величезний 2D-простір, тоді як наша модель розташована в позиції (0,0,0) і має ширину лише 1.

Додаймо ігровий об’єкт із компонентом камери, щоб визначити, що ми бачитимемо в грі.

  1. Додайте ігровий об’єкт з іменем camera у позиції (0,0,1). (Важливо задати координату Z рівною 1, щоб цей ігровий об’єкт перебував перед нашою моделлю, оскільки зараз, у стандартному 2D-налаштуванні, вісь Z спрямована до нас).
  2. Додайте компонент Camera, і ви побачите попередній перегляд камери з нашим чотирикутником усередині. За такого розташування стандартні властивості вдало підходять: нічого змінювати не потрібно, і ми вже маємо бачити правильний результат. Є лише один виняток — нам не потрібна така велика піраміда видимості камери, тож можна зменшити Far Z до 2.

Камера

За бажанням можна змінити тип камери, установивши Orthographic Projection у true, а потім також задати для Orthographic Zoom значення приблизно 600. Але в цьому разі співвідношення сторін не налаштовуватиметься автоматично, тому модель не заповнюватиме екран.

Шейдер Star Nest

Тепер, коли все готово, почнімо працювати з кодом самого шейдера. Спочатку погляньмо на оригінальний код. Він складається з кількох частин:

Код шейдера Star Nest

Ми використаємо сучасний конвеєр із GLSL версії 140 — для цього оголосимо версію на початку файлу за допомогою #version 140.

  1. Рядки 5–18 визначають низку констант. Їх можна залишити без змін. Це звичайні константи GLSL, які не залежать від особливостей Shadertoy чи Defold.

  2. Рядки 21 і 63 містять вхідні текстурні координати X та Y фрагмента в екранному просторі (in vec2 fragCoord) і вихідний колір фрагмента (out vec4 fragColor).

    Defold передає текстурні координати з вершинного шейдера до фрагментного через інтерпольовану змінну у вигляді UV-координат (у діапазоні 0–1). У нашому вершинному шейдері її оголошено з кваліфікатором out:

     // in star-nest.vp
     out vec2 var_texcoord0;
    

    У фрагментному шейдері те саме значення отримується з кваліфікатором in:

     // in star-nest.fp
     in vec2 var_texcoord0;
    

    Далі в GLSL 140 явно оголошуємо вихідну змінну фрагмента з кваліфікатором out:

     // in star-nest.fp
     out vec4 out_fragColor;
    

    Отже, там, де оригінальний код Shadertoy записує значення у fragColor, наш шейдер Defold записує його в out_fragColor.

  3. Рядки 23–27 задають розміри текстури, напрямок руху й масштабований час. У Shadertoy шейдер отримує позицію пікселя через fragCoord, а роздільна здатність області перегляду/текстури передається в шейдер як uniform vec3 iResolution. Шейдер обчислює координати на зразок UV із правильним співвідношенням сторін на основі координат фрагмента й роздільної здатності. Також застосовується певне зміщення відносно роздільної здатності, щоб отримати кращу композицію кадру.

    У Defold ми починаємо не з координат пікселів. Натомість уже отримуємо нормалізовані UV-координати з вершинного шейдера через var_texcoord0. Ці координати охоплюють діапазон від 0.0 до 1.0 по всій поверхні чотирикутника, що рендериться.

    У версії для Defold потрібно змінити ці обчислення, щоб використовувати UV-координати з var_texcoord0. Типове перетворення має такий вигляд:

     vec2 uv = var_texcoord0.xy;
     uv = uv * 2.0 - 1.0;
     uv.x *= aspect;
    

    Точне значення aspect залежить від налаштування прикладу. Якщо ефект рендериться на повноекранному чотирикутнику з відомим розміром екрана, для уроку співвідношення сторін можна жорстко задати в коді. Якщо ефект має підтримувати довільні розміри вікна, передавайте роздільну здатність як фрагментну константу й розмістіть її всередині uniform-блока GLSL 140.

    Тут також задається час. Він передається в шейдер як uniform float iGlobalTime. Defold (починаючи з версії 1.12.3) надає шейдерам час через спеціальну константу Time, яку ми й використаємо.

    У сучасному Defold uniform-змінні, що не належать до непрозорих типів (non-opaque), оголошують усередині uniform-блоків. У фрагментному шейдері оголосимо це так:

     uniform fragment_inputs
     {
         vec4 time;
     };
    

    Потім у star-nest.material додамо Fragment Constant з іменем time і встановимо для неї тип Time.

    Далі це значення можна використовувати так:

     float iGlobalTime = time.x;
     float dt = time.y;
    

    де time.x — це час від запуску рушія, а time.y — час, що минув із попереднього кадру.

  4. Рядки 29–39 задають поворот об’ємного рендерингу, на який впливає позиція миші. Координати миші передаються в шейдер як uniform vec4 iMouse.

    У цьому уроці ми пропустимо введення з миші.

  5. Рядки 41–62 — це основна частина шейдера. Цей код можна залишити без змін.

Змінений шейдер Star Nest

Після опрацювання наведених вище частин і внесення потрібних змін отримуємо такий код шейдера. Його трохи впорядковано для кращої читабельності. Відмінності між версіями для Defold і Shadertoy позначено:

#version 140 // <1>

// Star Nest by Pablo Román Andrioli
// This content is under the MIT License.

#define iterations 17
#define formuparam 0.53

#define volsteps 20
#define stepsize 0.1

#define zoom   0.800
#define tile   0.850
#define speed  0.010

#define brightness 0.0015
#define darkmatter 0.300
#define distfading 0.730
#define saturation 0.850

in vec2 var_texcoord0; // <2>

out vec4 out_fragColor; // <3>

uniform fragment_inputs // <4>
{
	vec4 time;
};

void main() // <5>
{
	// get coords and direction
	vec2 res = vec2(1.0, 1.0); // <6>
	vec2 uv = var_texcoord0.xy * res.xy - 0.5;
	vec3 dir = vec3(uv * zoom, 1.0);

	float iGlobalTime = time.x; // <7>
	float shader_time = iGlobalTime * speed;

	float a1 = 0.5; // <8>
	float a2 = 0.8;
	mat2 rot1 = mat2(cos(a1), sin(a1), -sin(a1), cos(a1));
	mat2 rot2 = mat2(cos(a2), sin(a2), -sin(a2), cos(a2));

	dir.xz *= rot1;
	dir.xy *= rot2;

	vec3 from = vec3(1.0, 0.5, 0.5);
	from += vec3(shader_time * 2.0, shader_time, -2.0);
	from.xz *= rot1;
	from.xy *= rot2;

	// volumetric rendering
	float s = 0.1;
	float fade = 1.0;
	vec3 v = vec3(0.0);

	for (int r = 0; r < volsteps; r++) {
		vec3 p = from + s * dir * 0.5;

		// tiling fold
		p = abs(vec3(tile) - mod(p, vec3(tile * 2.0)));

		float pa = 0.0;
		float a = 0.0;

		for (int i = 0; i < iterations; i++) {
			// the magic formula
			p = abs(p) / dot(p, p) - formuparam;

			// absolute sum of average change
			a += abs(length(p) - pa);
			pa = length(p);
		}

		// dark matter
		float dm = max(0.0, darkmatter - a * a * 0.001);

		a *= a * a;

		// dark matter, don't render near
		if (r > 6) {
			fade *= 1.0 - dm;
		}

		v += fade;

		// coloring based on distance
		v += vec3(s, s * s, s * s * s * s) * a * brightness * fade;

		fade *= distfading;
		s += stepsize;
	}

	// color adjust
	v = mix(vec3(length(v)), v, saturation);

	out_fragColor = vec4(v * 0.01, 1.0); // <9>
}
  1. Ми оголошуємо #version 140 на початку файлу, щоб використовувати сучасний конвеєр GLSL у Defold. Далі залишаємо визначення без змін.
  2. Вершинний шейдер передає UV-координати до фрагментного через var_texcoord0. У GLSL 140 фрагментний шейдер отримує це інтерпольоване значення з кваліфікатором in.
  3. У GLSL 140 фрагментний шейдер має явно оголошувати вихідну змінну замість запису в gl_FragColor. Тут ми використовуємо out vec4 out_fragColor.
  4. Константа матеріалу Time у Defold доступна шейдеру через uniform-блок. У star-nest.material додайте Fragment Constant з іменем time і встановіть для неї тип Time.
  5. Shadertoy використовує mainImage(out vec4 fragColor, in vec2 fragCoord). У Defold ми використовуємо звичайну точку входу void main(), читаємо інтерпольовані UV-координати з var_texcoord0 та записуємо остаточний колір в out_fragColor.
  6. Для цього уроку задаємо статичне значення роздільної здатності/співвідношення сторін для рендерингу. Наразі модель квадратна, тож можна використати vec2 res = vec2(1.0, 1.0);. Для прямокутної моделі розміром 1280×720 натомість можна використати vec2 res = vec2(1.78, 1.0); і помножити UV-координати на це значення, щоб зберегти правильне співвідношення сторін.
  7. Оригінальний шейдер Shadertoy використовує iGlobalTime. У цій версії для Defold time.x містить час від запуску рушія, тому ми присвоюємо його локальній змінній iGlobalTime і використовуємо її, щоб анімувати рух камери крізь зоряне поле.
  8. Щоб не ускладнювати урок, повністю прибираємо значення iMouse. Сам поворот залишаємо, оскільки він зменшує візуальну симетрію об’ємного рендерингу.
  9. Зрештою шейдер записує отриманий колір фрагмента в out_fragColor.

Збережіть програму фрагментного шейдера. Тепер модель має бути гарно текстурована зоряним полем у редакторі сцени й під час виконання:

Чотирикутник зі Star Nest

Анімація

Остання частина завдання — додати час, щоб зорі почали рухатися. Defold (починаючи з версії 1.12.3) надає його автоматично через фрагментну константу типу Time.

  1. Відкрийте star-nest.material.
  2. Додайте Fragment Constant і назвіть її “time”.
  3. Установіть для її Type значення Time.

Константа часу

І це все! Ми вже обробляємо цей time у фрагментному шейдері. Готово!

Вправи

Цікавою вправою для продовження буде додати до шейдера оригінальне введення руху миші. Потрібно створити нову Fragment Constant, цього разу типу User, та оновлювати її в on_input у скрипті, що виявляє рух миші, за допомогою функції go.set(), задаючи новій константі координати введення.

Приємної роботи з Defold!