Во время своего доклада на второй день конференции 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 and JavaScript Jump Start (используйте уже написанный JS-код, чтобы создать приложение).
И, конечно же, вы всегда можете воспользоваться некоторыми нашими бесплатными инструментами для оптимизации работы в вебе: Visual Studio Community, пробную версию Azure и кроссбраузерные инструменты для тестирования на Mac, Linux или Windows.
Автор: Plarium