Создание шейдеров с помощью Babylon.js
Опубликовано: 2022-03-10Шейдеры являются ключевой концепцией, если вы хотите раскрыть грубую мощь вашего графического процессора. Благодаря Babylon.js я помогу вам понять, как они работают, и даже поэкспериментировать с их внутренней силой.
Как это работает?
Прежде чем экспериментировать, мы должны увидеть, как все работает внутри.
При работе с 3D с аппаратным ускорением вам придется иметь дело с двумя процессорами: основным процессором и графическим процессором. GPU — это своего рода чрезвычайно специализированный процессор.
Дальнейшее чтение на SmashingMag:
- Создание кроссплатформенной игры WebGL с помощью Babylon.js
- Использование Gamepad API в веб-играх
- Введение в полигональное моделирование и Three.js
- Как создать отзывчивую 8-битную драм-машину
GPU — это конечный автомат, который вы настраиваете с помощью CPU. Например, ЦП настроит ГП на отображение линий вместо треугольников; он определит, включена ли прозрачность; и так далее.
Как только все состояния установлены, ЦП может определить, что отображать: геометрию.
Геометрия состоит из:
- список точек, которые называются вершинами и хранятся в массиве, называемом буфером вершин,
- список индексов, которые определяют грани (или треугольники), хранящиеся в массиве с именем index buffer.
Последним шагом для ЦП является определение того, как визуализировать геометрию; для этой задачи ЦП будет определять шейдеры в графическом процессоре. Шейдеры — это фрагменты кода, которые графический процессор будет выполнять для каждой из вершин и пикселей, которые он должен отобразить. (Вершина — или вершины, когда их несколько — это «точка» в 3D).
Существует два типа шейдеров: вершинные шейдеры и пиксельные (или фрагментные) шейдеры.
Графический конвейер
Прежде чем углубляться в шейдеры, давайте сделаем шаг назад. Для рендеринга пикселей GPU возьмет геометрию, определенную CPU, и сделает следующее:
- Используя индексный буфер, три вершины собираются для определения треугольника.
- Буфер индексов содержит список индексов вершин. Это означает, что каждая запись в буфере индексов является номером вершины в буфере вершин.
- Это действительно полезно, чтобы избежать дублирования вершин.
Например, следующий индексный буфер представляет собой список из двух лиц: [1 2 3 1 3 4]. Первая грань содержит вершину 1, вершину 2 и вершину 3. Вторая грань содержит вершину 1, вершину 3 и вершину 4. Итак, в этой геометрии четыре вершины:

Вершинный шейдер применяется к каждой вершине треугольника. Основная цель вершинного шейдера — создать пиксель для каждой вершины (проекция на 2D-экране 3D-вершины):

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

Этот процесс выполняется для каждого лица, определенного буфером индексов.
Очевидно, что из-за своей параллельной природы GPU может обрабатывать этот шаг для большого количества лиц одновременно и достигать действительно хорошей производительности.
GLSL
Мы только что видели, что для рендеринга треугольников графическому процессору нужны два шейдера: вершинный шейдер и пиксельный шейдер. Эти шейдеры написаны на языке под названием Graphics Library Shader Language (GLSL). Похоже, Ц.
Вот пример распространенного вершинного шейдера:
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-объекту). - Униформа . Униформа — это переменная, используемая шейдером и определяемая процессором. Единственный юниформ, который у нас есть, — это матрица, используемая для проецирования положения вершины (x, y, z) на экран (x, y).
- Варьируется . Переменные — это значения, созданные вершинным шейдером и переданные пиксельному шейдеру. Здесь вершинный шейдер будет передавать значение
vUV
(простую копиюuv
) в пиксельный шейдер. Это означает, что пиксель здесь определяется положением и координатами текстуры. Эти значения будут интерполированы графическим процессором и использованы пиксельным шейдером. - Главная . Функция с именем
main
— это код, выполняемый GPU для каждой вершины, и она должна как минимум выдавать значениеgl_position
(положение текущей вершины на экране).
В нашем примере мы видим, что вершинный шейдер довольно прост. Он генерирует системную переменную (начиная с gl_
) с именем gl_position
для определения положения связанного пикселя и устанавливает изменяющуюся переменную с именем vUV
.
Вуду за матрицами
Суть нашего шейдера в том, что у нас есть матрица с именем worldViewProjection
, и мы используем эту матрицу для проецирования положения вершины в переменную gl_position
. Это круто, но как нам получить значение этой матрицы? Это униформа, поэтому мы должны определить ее на стороне процессора (используя JavaScript).
Это одна из сложных частей работы с 3D. Вы должны понимать сложную математику (или вам придется использовать 3D-движок, такой как Babylon.js, который мы увидим позже).
Матрица worldViewProjection
представляет собой комбинацию трех разных матриц:

Использование полученной матрицы позволяет нам преобразовывать 3D-вершины в 2D-пиксели, принимая во внимание точку зрения и все, что связано с положением, масштабом и вращением текущего объекта.
Это ваша обязанность как 3D-разработчика: создавать и поддерживать эту матрицу в актуальном состоянии.
Назад к шейдерам
Как только вершинный шейдер будет выполнен для каждой вершины (то есть три раза), у нас будет три пикселя с правильным gl_position
и значением vUV
. Графический процессор будет интерполировать эти значения для каждого пикселя, содержащегося в треугольнике, созданном с помощью этих пикселей.
Затем для каждого пикселя будет выполняться пиксельный шейдер:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Пиксельная (или фрагментарная) структура шейдера
Структура пиксельного шейдера аналогична структуре вершинного шейдера:
- Варьируется . Варьирующиеся переменные — это значения, созданные вершинным шейдером и переданные пиксельному шейдеру. Здесь пиксельный шейдер получит значение
vUV
от вершинного шейдера. - Униформа . Униформа — это переменная, используемая шейдером и определяемая процессором. Единственный юниформ, который у нас есть, — это сэмплер, инструмент, используемый для считывания цветов текстуры.
- Главная . Функция с именем
main
представляет собой код, выполняемый графическим процессором для каждого пикселя, который должен как минимум выдавать значение дляgl_FragColor
(т. е. цвет текущего пикселя).
Этот пиксельный шейдер довольно прост: он считывает цвет из текстуры, используя координаты текстуры из вершинного шейдера (который, в свою очередь, получает его из вершины).
Проблема в том, что при разработке шейдеров вы находитесь только на полпути, потому что вам приходится иметь дело с большим количеством кода WebGL. Действительно, WebGL действительно мощный, но при этом очень низкоуровневый, и вы должны делать все самостоятельно, от создания буферов до определения структур вершин. Вы также должны сделать всю математику, установить все состояния, обрабатывать загрузку текстур и так далее.
Слишком сложно? BABYLON.ShaderMaterial спешит на помощь
Я знаю, о чем вы думаете: «Шейдеры действительно крутые, но я не хочу заморачиваться с внутренней инфраструктурой WebGL или даже с математикой».
И вы правы! Это совершенно законный вопрос, и именно поэтому я создал Babylon.js!
Чтобы использовать Babylon.js, вам сначала понадобится простая веб-страница:
<!DOCTYPE html> <html> <head> <title>Babylon.js</title> <script src="Babylon.js"></script> <script type="application/vertexShader"> 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"> 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></canvas> </body> </html>
Вы заметите, что шейдеры определяются тегами <script>
. С помощью Babylon.js вы также можете определить их в отдельных файлах (файлы .fx
).
- Источник Babylon.js
- Репозиторий GitHub
Наконец, основной код 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"] });
Вы также должны указать имена атрибутов и форм, которые вы используете.
Затем вы можете напрямую установить значения ваших униформ и сэмплеров, используя setTexture
, setFloat
, setFloats
, setColor3
, setColor4
, setVector2
, setVector3
, setVector4
, setMatrix
.
Довольно просто, правда?
А вы помните предыдущую матрицу worldViewProjection
с использованием Babylon.js и BABYLON.ShaderMaterial
. Вы просто не должны беспокоиться об этом! BABYLON.ShaderMaterial
автоматически вычислит его для вас, потому что вы объявите его в списке униформ.
BABYLON.ShaderMaterial
также может обрабатывать следующие матрицы:
-
world
, -
view
, -
projection
, -
worldView
, -
worldViewProjection
.
Математика больше не нужна. Например, каждый раз, когда вы выполняетеsphere.rotation.y sphere.rotation.y += 0.05
, world
матрица сферы будет генерироваться для вас и передаваться на графический процессор.
Смотрите живые результаты сами.
Создайте свой собственный шейдер (CYOS)
Теперь давайте пойдем дальше и создадим страницу, на которой вы сможете динамически создавать свои собственные шейдеры и сразу же видеть результат. На этой странице будет использоваться тот же код, который мы обсуждали ранее, и будет использоваться объект BABYLON.ShaderMaterial
для компиляции и выполнения создаваемых вами шейдеров.

Я использовал редактор кода ACE для создания собственного шейдера (CYOS). Это невероятный редактор кода с подсветкой синтаксиса. Не стесняйтесь взглянуть на это.
Используя первое поле со списком, вы сможете выбрать предопределенные шейдеры. Мы увидим каждого из них сразу после.
Вы также можете изменить сетку (т.е. 3D-объект), используемую для предварительного просмотра ваших шейдеров, используя второе поле со списком.
Кнопка компиляции используется для создания нового 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;
Невероятно просто, правда? Материал готов отправить вам три предварительно вычисленные матрицы ( world
, worldView
и worldViewProjection
). Вершины будут иметь координаты позиции, нормали и текстуры. Также для вас уже загружены две текстуры:

amiga.jpg
(Посмотреть большую версию) 
ref.jpg
(Посмотреть большую версию) Наконец, в renderLoop
я обновляю два удобных юниформа:
- Один называется
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: базового шейдера.
Мы уже знаем этот шейдер. Он вычисляет 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); }
Ранее мы видели, что textureSampler
заполнена текстурой amiga
. Итак, результат следующий:

Черно-белый шейдер
Давайте продолжим с новым шейдером: черно-белым шейдером. Цель этого шейдера — использовать предыдущий, но с черно-белым режимом рендеринга.
Для этого мы можем оставить тот же вершинный шейдер. Пиксельный шейдер будет немного изменен.
Первый вариант, который у нас есть, — взять только один компонент, например, зеленый:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }
Как видите, вместо использования .rgb
(эта операция называется swizzle) мы использовали .ggg
.
Но если нам нужен действительно точный черно-белый эффект, то лучше вычислить яркость (которая учитывает все компоненты):
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); }
dot
операция (или dot
произведение) вычисляется следующим образом: 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; }
Обратите внимание, что мы также используем мировую матрицу, потому что положение и нормаль сохраняются без каких-либо преобразований, и мы должны применить мировую матрицу, чтобы учесть вращение объекта.
Пиксельный шейдер выглядит следующим образом:
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 раза. И так далее.
В этом шейдере в основном четыре шага.
Во-первых, мы объявляем пороги и уровни константами.
Затем мы вычисляем освещение, используя уравнение Фонга (будем считать, что свет не движется):
vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));
Интенсивность света на пиксель зависит от угла между нормалью и направлением света.
Затем мы получаем цвет текстуры для пикселя.
Наконец, мы проверяем порог и применяем уровень к цвету.
Результат выглядит как мультяшный объект:

Фонг Шейдер
Мы использовали часть уравнения Фонга в предыдущем шейдере. Давайте использовать его полностью сейчас.
Вершинный шейдер тут явно простой, потому что все будет делаться в пиксельном шейдере:
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.); }
Мы уже использовали диффузную часть в предыдущем шейдере, поэтому здесь нам просто нужно добавить зеркальную часть. Вы можете найти больше информации о затенении Фонга в Википедии.
Результат нашей сферы:

Отменить шейдер
Для шейдера сброса я хотел бы ввести новую концепцию: ключевое слово 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; }
Пиксельный шейдер на своей стороне должен будет протестировать цвет и использовать отбрасывание, когда, например, зеленый компонент слишком высок:
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.); }
Результат немного забавный:

Волновой шейдер
Мы много играли с пиксельным шейдером, но я также хочу сообщить вам, что мы можем многое сделать с помощью вершинных шейдеров.
Для волнового шейдера мы будем повторно использовать пиксельный шейдер Фонга.
Вершинный шейдер будет использовать униформу с именем 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; vx += sin(2.0 * position.y + (time)) * 0.5; gl_Position = worldViewProjection * vec4(v, 1.0); vPosition = position; vNormal = normal; vUV = uv; }
Синус применяется к position.y
, и результат выглядит следующим образом:

Сферическое картирование окружающей среды
Этот шейдер во многом был вдохновлен статьей «Создание шейдера сферического отражения/сопоставления среды». Я дам вам прочитать эту прекрасную статью и поиграть с соответствующим шейдером.

Шейдер Френеля
Я хотел бы завершить эту статью своим любимым: шейдером Френеля.
Этот шейдер используется для применения различной интенсивности в соответствии с углом между направлением взгляда и нормалью вершины.
Вершинный шейдер — это тот же самый, который используется в шейдере затенения ячеек, и мы можем легко вычислить член Френеля в нашем пиксельном шейдере (поскольку у нас есть нормаль и положение камеры, которые можно использовать для оценки направления взгляда):
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, официальный сайт
- Babylon.js, репозиторий GitHub
- Форум Babylon.js, разработчики игр HTML5
- Создайте свой собственный шейдер (CYOS), Babylon.js
- Язык шейдинга OpenGL», Википедия.
- Язык шейдеров OpenGL, документация