Створення шейдерів на основі Babylon.js і WebGL: теорія та приклади

Під час свого доповіді на другий день конференції Build 2014 євангелісти Microsoft Стівен Гуггенхаймер і Джон Шевчук розповіли про реалізацію підтримки Babylon.js для Oculus Rift. Одним з ключових пунктів їх демонстрації була згадка розробленої нами технології імітації лінз:



Я також був присутній на доповіді Френка Олів'є і Бена Констебля на тему використання графіки в IE із застосуванням Babylon.js.

Ці доповіді нагадали мені про одне питання, яке мені часто задають щодо Babylon.js: «Що ви розумієте під шейдерами?» Я вирішив присвятити цьому питанню цілу статтю з метою пояснити принцип роботи шейдерів і привести декілька прикладів їх основних типів.
Цей переклад є частиною серії статей для розробників від компанії Microsoft.

Теорія
Перш ніж розпочинати наші досліди, потрібно зрозуміти, як все функціонує.

Працюючи з апаратно прискореної 3D графікою, ми маємо справу з двома різними процесорами: центральним (CPU) і графічним (GPU). Графічний процесор – це всього лише різновид вкрай спеціалізованого центрального процесора.

GPU – це кінцевий автомат, що настроюється за допомогою CPU. Приміром, саме CPU дає GPU команду відображати лінії замість трикутників, включити прозорість і т. д.

Як тільки всі стани будуть налаштовані, CPU визначить, що потрібно рендери, виходячи з двох основних складових: геометрії, яка розраховується на основі списку точок або вершин (зберігаються в масиві під назвою буфер вершин), та списку індексів – граней або трикутників, які зберігаються в буфері індексів.

Нарешті, CPU визначить, як рендери геометрію, і спеціально для цього задасть шейдери для GPU. Шейдери являють собою фрагмент коду, що виконується GPU для всіх вершин і пікселей, які потрібно отрендерить.

Вершина – це свого роду точка в 3D просторі (на відміну від точки в 2D просторі).

Існує 2 виду шейдерів: вершинні і піксельні (фрагментные) шейдери.

Графічний пайплайн

Перш ніж перейти безпосередньо до шейдерам, зробимо ще одне невелике відступ. Для відображення пікселів GPU отримує з CPU дані геометрії.

За допомогою буфера індексів, що містить список індексів вершин, 3 вершини об'єднуються в трикутник. Кожна запис в буфері індексів відповідає номеру вершини в буфері вершин (це дозволяє уникнути дублювання вершин).

Приміром, буфер індексів на прикладі нижче – це список з двох граней: [1 2 3 1 3 4]. Перша грань містить вершини 1, 2 і 3. Друга грань містить вершини 1, 3 і 4. Таким чином, в даному випадку геометрія складається з чотирьох вершин:



Vertex — Вершина
Vertex Buffer — Буфер вершин
Index Bufer — Буфер індексів
Face — Грань

Вершинний шейдер виконується на кожній вершині трикутника. Основне призначення вершинного шейдерів – відобразити піксель для кожної вершини (тобто виконати проекцію 3D вершини на 2D екран).



Використовуючи ці 3 пікселя (задають параметри 2D трикутника на екрані), GPU проаналізує всі відносяться до пікселя (принаймні, до його положенню) значення і застосує піксельний шейдер, щоб згенерувати колір кожного пікселя даного трикутника.



Те ж саме виконується для всіх граней в буфері індексів.

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

GLSL

Як було сказано раніше, для візуалізації трикутників графічному процесору потрібні 2 шейдера: верховий і піксельний. Обидва пишуться на спеціальному мовою під назвою GLSL (Graphics Library Shader Language), який трохи схожий на C.

Спеціально для Internet Explorer 11 ми розробили компілятор, що перетворював GLSL в HLSL (High Level Shader Language) – шейдерний мова DirectX 11. Це дозволило нам підвищити безпеку коду шейдера:



Ось приклад простого вершинного шейдера:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec2 vUV;

void main(void) {
gl_Position = worldViewProjection * vec4(position, 1.0);

vUV = uv;
}


Структура вершинного шейдера

Вершинний шейдер містить наступні елементи:

  • Атрибути: Атрибут визначає частину вершини. За замовчуванням вершина повинна мати, принаймні, дані про становище (vector3:x, y, z). Але ви, як розробник, можете надати більше даних. Наприклад, у коді вище є vector2 під назвою uv (координати текстури, що дозволяють застосовувати 2D текстуру на 3D об'єкт).
  • Uniform-змінні: Визначаються центральним процесором і використовуються шейдером. Єдина uniform-змінна, яка є у нас в даному випадку, – це матриця, яка використовується для проекції положення вершини (x, y, z) на екран (x, y).
  • Varying-змінні: Являють собою значення, які створюються вершинним шейдером і передаються в піксельний. У нашому випадку вершинний шейдер передасть у піксельний шейдер значення vUV (проста копія uv). Отже, тут визначаються координати текстури і положення пікселя. GPU додасть ці значення, а використовувати їх безпосередньо піксельний шейдер.
  • main: Функція main() – це код, який виконується в GPU для кожної вершини. Він повинен як мінімум давати значення для gl_position (положення поточної вершини на екрані).


Як видно з прикладу вище, немає нічого складного в вершинном шейдере. Він генерує системну змінну (починається на gl_) під назвою gl_position, щоб визначити стан конкретного пікселя, а також задає varying-змінну під назвою vUV.

Чари в основі матриць

Матриця в нашому шейдере називається worldViewProjection. Вона проектує положення вершини в змінну gl_position. Але як же нам отримати значення цієї матриці? Оскільки це uniform-змінна, нам потрібно визначити її на стороні CPU (за допомогою JavaScript.

Це важкий для розуміння аспект роботи з 3D графікою. Потрібно непогано розбиратися у складних математичних обчисленнях (або користуватися 3D движком зразок Babylon.js про що ми поговоримо пізніше).

Матриця worldViewProjection складається з трьох окремих матриць:



У результаті виходить матриця, що дозволяє перетворювати 3D вершини в 2D пікселі, враховуючи при цьому позицію точки огляду і все, що відноситься до положення, масштабу та повороту поточного об'єкта.

Завдання 3D дизайнера – створити цю матрицю і підтримувати її актуальність даних.

І знову шейдери

Після того як вершинний шейдер виконається на кожній вершині (тобто 3 рази), ми отримаємо 3 пікселя з правильним значенням vUV і gl_position. Далі GPU перенесе ці значення на кожен піксель всередині трикутника, утвореного трьома основними пікселями.

Потім до кожного пікселя буде застосований піксельний шейдер:

precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;

void main(void) {
gl_FragColor = texture2D(textureSampler, vUV);
}


Структура піксельного (або фрагментного) шейдера

По своїй структурі піксельний шейдер схожий на верховий:

  • Varying-змінні: Являють собою значення, які створюються вершинним шейдером і передаються в піксельний шейдер. У нашому випадку піксельний шейдер отримає з вершинного шейдера значення vUV.
  • Uniform-змінні: Визначаються центральним процесором і використовуються шейдером. Єдина uniform-змінна, яка є у нас в даному випадку – це семплер, який потрібен для зчитування квітів текстури.
  • main: Функція main – це код, який виконується в GPU для кожного пікселя. Він повинен як мінімум давати значення для gl_FragColor (колір поточного пікселя).


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

Ось що вийшло в результаті.
Рендеринг виконується в реальному часі; ви можете рухати сферу мишкою.

Щоб отримати такий результат, треба добряче попрацювати з кодом WebGL. Звичайно, WebGL – це дуже потужний API. Але він низькорівневий, тому доведеться все робити самостійно: від створення буфера до визначення структури вершин. Вам також потрібно буде виконувати багато математичних обчислень, налаштовувати стану, управляти завантаженням текстури і так далі.

Занадто складно? BABYLON.ShaderMaterial поспішає на допомогу

Я знаю, про що ви подумали: «Шейдери – це, звичайно, круто, але я не хочу розбиратися у всіх тонкощах WebGL і самостійно виробляти всі обчислення».

Не проблема! Саме тому ми і створили Babylon.js.

Ось як виглядає код для тієї ж сфери в Babylon.js. Для початку вам знадобиться проста веб-сторінка:

<!DOCTYPE html>
<html>
<head>
<title>Babylon.js</title>
<script src="Babylon.js"></script>

<script type="application/vertexShader" id="vertexShaderCode">
precision highp float;

// Attributes
attribute vec3 position;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Normal
varying vec2 vUV;

void main(void) {
gl_Position = worldViewProjection * vec4(position, 1.0);

vUV = uv;
}
</script>

<script type="application/fragmentShader" id="fragmentShaderCode">
precision highp float;
varying vec2 vUV;

uniform sampler2D textureSampler;

void main(void) {
gl_FragColor = texture2D(textureSampler, vUV);
}
</script>

<script src="index.js"></script>
<style>
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
margin: 0px;
overflow: hidden;
}

#renderCanvas {
width: 100%;
height: 100%;
touch-action: none;
-ms-touch-action: none;
}
</style>
</head>
<body>
<canvas id="renderCanvas"></canvas>
</body>
</html>


Шейдери тут задаються тегами script. В Babylon.js їх також можна задавати в окремих файлах формату .fx.

Babylon.js доступний для скачування за посиланням тут або в нашому репозиторії на GitHub. Для отримання доступу до об'єкта BABYLON.StandardMaterial потрібна версія 1.11 і вище.

Нарешті, основний JavaScript-код виглядає наступним чином:
«use strict»;

document.addEventListener. ("DOMContentLoaded", startGame, false);

function startGame() {
if (BABYLON.Engine.isSupported()) {
var canvas = document.getElementById("renderCanvas");
var engine = new BABYLON.Engine(canvas, false);
var scene = new BABYLON.Scene(engine);
var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene);

camera.attachControl(canvas);

// Creating sphere
var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene);

var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, {
vertexElement: "vertexShaderCode",
fragmentElement: "fragmentShaderCode",
},
{
attributes: ["position", "uv"],
uniforms: ["worldViewProjection"]
});
amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene));

sphere.material = amigaMaterial;

engine.runRenderLoop(function () {
sphere.rotation.y += 0.05;
scene.render();
});
}
};


Як видно, я використовую BABYLON.ShaderMaterial, щоб позбутися від необхідності компілювати шейдери, линковать їх або керувати ними.

При створенні об'єкта BABYLON.ShaderMaterial потрібно вказати елемент DOM, використовуваний для зберігання шейдерів або базова ім'я файлів, в яких знаходяться шейдери. Для другого варіанту потрібно також створити файл для кожного шейдера, використовуючи наступний принцип іменування: basename.vertex.fx і basename.fragment.fx. Потім потрібно буде створити матеріал на зразок цього:

var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader",
{
attributes: ["position", "uv"],
uniforms: ["worldViewProjection"]
});


Потрібно також вказати імена будь-яких використовуваних атрибутів і uniform-змінних. Потім можна безпосередньо задати значення uniform-змінних і семплери з допомогою функцій setTexture, setFloat, setFloats, setColor3, setColor4, setVector2, setVector3, setVector4 і setMatrix.

Досить просто, правда?

Пам'ятайте матрицю worldViewProjection? З Babylon.js і BABYLON.ShaderMaterial вам не доведеться про неї хвилюватися. Об'єкт BABYLON.ShaderMaterial обчислить все автоматично, так як ми оголошуємо матрицю в списку uniform-змінних.

Об'єкт BABYLON.ShaderMaterial може самостійно керувати такими матрицями:
  • world;
  • view;
  • projection;
  • worldView;
  • worldViewProjection.


Ніяких складних розрахунків. Наприклад, при кожному виконанні sphere.rotation.y += 0.05 матриця world даної сфери генерується та передається в GPU.

CYOS: Створіть шейдер своїми руками

Ми підемо ще далі і створимо сторінку, на якій можна буде динамічно редагувати шейдери і спостерігати за результатом в реальному часі. Для цього нам знадобиться попередній код, а також об'єкт BABYLON.ShaderMaterial для компіляції та виконання створених шейдерів.

Я використовував CYOS редактор коду під назвою ACE. Він неймовірно зручний і оснащений функцією підсвічування синтаксису.

У полі Templates можна вибирати передвстановлені шейдери, ми про них поговоримо трохи пізніше.

Ви також можете змінити 3D-об'єкт, який використовується для попереднього перегляду шейдерів в поле Meshes.

Кнопка Compile використовується для створення нового об'єкта BABYLON.ShaderMaterial з шейдерів. Ось її код:

// Compile
shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, {
vertexElement: "vertexShaderCode",
fragmentElement: "fragmentShaderCode",
},
{
attributes: ["position", "normal", "uv"],
uniforms: ["world", "worldView", "worldViewProjection"]
});

var refTexture = new BABYLON.Texture("ref.jpg", scene);
refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE;
refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE;

var amigaTexture = new BABYLON.Texture("amiga.jpg", scene);

shaderMaterial.setTexture("textureSampler", amigaTexture);
shaderMaterial.setTexture("refSampler", refTexture);
shaderMaterial.setFloat("time", 0);
shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero());
shaderMaterial.backFaceCulling = false;

mesh.material = shaderMaterial;


Підозріло просто, правда? Отже, залишилося тільки отримати 3 попередньо обчислених матриці: world, worldView і worldViewProjection. Дані про вершинах будуть містити значення положення, нормалі та координат текстур. Також завантажаться 2 наступні текстури:


amiga.jpg


ref.jpg

А це renderLoop, де я оновлюю 2 uniform-змінні:

  • змінну time – щоб отримувати забавні анімації;
  • змінну cameraPosition – щоб отримувати дані про стан камери в шейдери (що дуже стане в нагоді при розрахунку освітлення);


engine.runRenderLoop(function () {
mesh.rotation.y += 0.001;

if (shaderMaterial) {
shaderMaterial.setFloat("time", time);
time += 0.02;

shaderMaterial.setVector3("cameraPosition", camera.position);
}

scene.render();
});


До того ж, CYOS тепер доступний і для Windows Phone завдяки виконаній нами роботі для Windows Phone 8.1:



Basic

Почнемо з базового шейдера в CYOS.

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

Щоб вирахувати положення пікселя, потрібна матриця worldViewProjection і положення вершини:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec2 vUV;

void main(void) {
gl_Position = worldViewProjection * vec4(position, 1.0);

vUV = uv;
}


Координати текстури (uv) передаються у піксельний шейдер незміненими.

Зверніть увагу на перший рядок: precision mediump float; – її обов'язково потрібно додати в верховий і піксельний шейдер для правильної роботи в Chrome. Вона відповідає за те, щоб для поліпшення продуктивності не використовувалися числа високої точності.

З піксельною шейдером все йде ще простіше: потрібно всього лише використовувати координати текстури і отримати колір текстури:

precision highp float;

varying vec2 vUV;

uniform sampler2D textureSampler;

void main(void) {
gl_FragColor = texture2D(textureSampler, vUV);
}


Як було видно раніше, uniform-мінлива textureSampler заповнена текстурою amiga, тому результат виглядає так:



Black and white

Перейдемо до другого шейдеру, чорно-білому.

Він буде використовувати параметри попереднього, але в чорно-білому режимі візуалізації. Залишимо колишні налаштування вершинного шейдерів і внесемо невеликі зміни в код піксельного.

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

precision highp float;

varying vec2 vUV;

uniform sampler2D textureSampler;

void main(void) {
gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0);
}



Ми використовували .ggg замість .rgb (в комп'ютерній графіці ця операція називається swizzle).

Але якщо потрібно отримати цей чорно-білий ефект, краще всього обчислити відносну яскравість, яка враховує всі компоненти кольору:

precision highp float;

varying vec2 vUV;

uniform sampler2D textureSampler;

void main(void) {
float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11));
gl_FragColor = vec4(luminance, luminance, luminance, 1.0);
}


Скалярний добуток обчислюється наступним чином:

result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z


У нашому випадку:

luminance = r * 0.3 + g * 0.59 + b * 0.11 
(ці значення розраховуються з урахуванням того, що людське око більш чутливий до зеленого кольору)



Cell-shading

Наступний за списком – шейдер заповнених клітинок, він трохи складніше.

В даному випадку нам потрібно додати в піксельний шейдер положення вершини і нормаль до вершини. Вершинний шейдер буде виглядати так:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 world;
uniform mat4 worldViewProjection;

// Varying
varying vec3 vPositionW;
varying vec3 vNormalW;
varying vec2 vUV;

void main(void) {
vec4 outPosition = worldViewProjection * vec4(position, 1.0);
gl_Position = outPosition;

vPositionW = vec3(world * vec4(position, 1.0));
vNormalW = normalize(vec3(world * vec4(normal, 0.0)));

vUV = uv;
}


Вчіть, що дані положення вершини і нормалі до вершини передаються незміненими, тому нам також потрібно застосувати матрицю world, щоб враховувати обертання об'єкта.

Ось як буде виглядати піксельний шейдер:

precision highp float;

// Lights
varying vec3 vPositionW;
varying vec3 vNormalW;
varying vec2 vUV;

// Refs
uniform sampler2D textureSampler;

void main(void) {
float ToonThresholds[4];
ToonThresholds[0] = 0.95;
ToonThresholds[1] = 0.5;
ToonThresholds[2] = 0.2;
ToonThresholds[3] = 0.03;

float ToonBrightnessLevels[5];
ToonBrightnessLevels[0] = 1.0;
ToonBrightnessLevels[1] = 0.8;
ToonBrightnessLevels[2] = 0.6;
ToonBrightnessLevels[3] = 0.35;
ToonBrightnessLevels[4] = 0.2;

vec3 vLightPosition = vec3(0, 20, 10);

// Light
vec3 lightVectorW = normalize(vLightPosition - vPositionW);

// diffuse
float ndl = max(0., dot(vNormalW, lightVectorW));

vec3 color = texture2D(textureSampler, vUV).rgb;

if (ndl > ToonThresholds[0])
{
color *= ToonBrightnessLevels[0];
}
else if (ndl > ToonThresholds[1])
{
color *= ToonBrightnessLevels[1];
}
else if (ndl > ToonThresholds[2])
{
color *= ToonBrightnessLevels[2];
}
else if (ndl > ToonThresholds[3])
{
color *= ToonBrightnessLevels[3];
}
else
{
color *= ToonBrightnessLevels[4];
}

gl_FragColor = vec4(color, 1.);
}


Цей шейдер призначений для симуляції світла, тому, щоб не обчислювати плавне затінення по всій поверхні об'єкта, ми будемо вираховувати інтенсивність світла на основі декількох порогів яскравості. Наприклад, якщо інтенсивність дорівнює від 1 (максимум) до 0.95, колір об'єкта, взятий з текстури, буде накладатися безпосередньо, без змін. Якщо ж інтенсивність буде від 0.95 до 0.5, до значення кольору буде застосований множник 0.8 і так далі.

У підсумку процес створення такого шейдера можна розбити на 4 етапи:

  • Спочатку оголошуємо пороги яскравості і константи для кожного ступеня інтенсивності.
  • Розраховуємо освітлення на основі алгоритму Фонга (виходячи з міркування, що джерело світла не рухається).


vec3 vLightPosition = vec3(0, 20, 10);

// Light
vec3 lightVectorW = normalize(vLightPosition - vPositionW);

// diffuse
float ndl = max(0., dot(vNormalW, lightVectorW));


Інтенсивність світла, що падає на піксель, що залежить від кута між нормаллю до вершини і напрямком світла.

  • Отримуємо колір текстури для пікселя.
  • Перевіряємо поріг яскравості і застосовуємо константу відповідної ступеня інтенсивності.


В результаті ми отримаємо щось схоже на мультиплікаційний ефект:



Phong

Ми вже використали алгоритм Фонга в попередньому прикладі. Тепер розглянемо його детальніше.

З вершинним шейдером все буде досить просто, так як основна частина роботи доведеться на піксельний:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUV;

void main(void) {
vec4 outPosition = worldViewProjection * vec4(position, 1.0);
gl_Position = outPosition;

vUV = uv;
vPosition = position;
vNormal = normal;
}


Згідно алгоритму, потрібно обчислити дифузну і дзеркальну складові, використовуючи напрямку світла і нормаль до вершини:

precision highp float;

// Varying
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUV;

// Uniforms
uniform mat4 world;

// Refs
uniform vec3 cameraPosition;
uniform sampler2D textureSampler;

void main(void){
vec3 vLightPosition = vec3(0, 20, 10);

// World values
vec3 vPositionW = vec3(world * vec4(vPosition, 1.0));
vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0)));
vec3 viewDirectionW = normalize(cameraPosition - vPositionW);

// Light
vec3 lightVectorW = normalize(vLightPosition - vPositionW);
vec3 color = texture2D(textureSampler, vUV).rgb;

// diffuse
float ndl = max(0., dot(vNormalW, lightVectorW));

// Specular
vec3 angleW = normalize(viewDirectionW + lightVectorW);
float specComp = max(0., dot(vNormalW, angleW));
specComp = pow(specComp, max(1., 64.)) * 2.;

gl_FragColor = vec4(color * ndl + vec3(specComp), 1.);
}


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


Автор: Brad Smith aka Rainwarrior

Результат:



Discard

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

Вершинний шейдер буде в даному випадку таким же, як і для базового шейдера:

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec2 vUV;

void main(void) {
gl_Position = worldViewProjection * vec4(position, 1.0);

vUV = uv;
}


Піксельний шейдер буде перевіряти колір кожного пікселя і використовувати ключове слово discard, коли, наприклад, значення зеленого компонента буде занадто великим:

precision highp float;

varying vec2 vUV;

// Refs
uniform sampler2D textureSampler;

void main(void) {
vec3 color = texture2D(textureSampler, vUV).rgb;

if (color.g > 0.5) {
discard;
}

gl_FragColor = vec4(color, 1.);
}


Результат виглядає досить забавно:



Wave

Мабуть, ми вже награлися з піксельними шейдерами. Тепер мені хотілося б приділити більше уваги вершинним шейдерам.

Для даного прикладу нам знадобиться піксельний шейдер з затінення по Фонгу.

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

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;
uniform float time;

// Varying
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUV;

void main(void) {
vec3 v = position;
v.x += sin(2.0 * position.y + (time)) * 0.5;

gl_Position = worldViewProjection * vec4(v, 1.0);

vPosition = position;
vNormal = normal;
vUV = uv;
}


Синус множиться на position.y, і це дає наступний результат:



Spherical Environment Mapping

На створення даного шейдера нас надихнув цей прекрасний тутора. Рекомендую ознайомитися з ним самостійно, а потім подивитися шейдер Wave в CYOS.



Fresnel

І наостанок мій улюблений шейдер, Fresnel.

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

Вершинний шейдер тут точно такий же, як і для шейдера заповнених клітинок, і ми легко можемо обчислити необхідне для піксельного шейдера значення френелевского відображення (для визначення напрямку перегляду дані нормалі і положення камери можна використовувати):

precision highp float;

// Lights
varying vec3 vPositionW;
varying vec3 vNormalW;

// Refs
uniform vec3 cameraPosition;
uniform sampler2D textureSampler;

void main(void) {
vec3 color = vec3(1., 1., 1.);
vec3 viewDirectionW = normalize(cameraPosition - vPositionW);

// Fresnel
float fresnelTerm = dot(viewDirectionW, vNormalW);
fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.);

gl_FragColor = vec4(color * fresnelTerm, 1.);
}




Ваш шейдер

Думаю, тепер ви готові створити власний шейдер. Можете сміливо ділитися результатами експериментів в коментарях нижче.

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

Репозиторій Babylon.js;
Форум Babylon.js;
CYOS;
Стаття про GLSL на Вікіпедії;
Документація по GLSL.

І ще кілька моїх статей на ту ж тему:

Introduction to WebGL 3D with HTML5 and Babylon.JS;
Cutting Edge Graphics in HTML.

А також уроки по JavaScript від нашої команди:

Practical Performance Tips to Make your HTML/JavaScript Faster (серія уроків з семи частин, зачіпає безліч тем: від адаптивного дизайну до оптимізації продуктивності і казуальних ігор);
The Modern Web Platform Jump Start (основи HTML, CSS і JS);
Developing Universal Windows App with HTML, JavaScript and Jump Start (використовуйте вже написаний JS-код, щоб створити додаток).

І, звичайно ж, ви завжди можете скористатися деякими нашими безкоштовними інструментами для оптимізації роботи в інтернеті: Visual Studio Community, пробну версію Azure і кросбраузерні інструменти для тестування на Mac, Linux або Windows.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.