Написание собственного GLSL-шейдера

A-Frame предоставляет возможность создавать кастомные шейдеры с использованием GLSL (OpenGL Shading Language), что позволяет управлять визуальными эффектами на уровне вершин и фрагментов. Шейдеры в A-Frame реализуются через компонент shader и систему registerShader, которая связывает GLSL-код с сущностью.

Каждый шейдер состоит из двух частей: вершинного шейдера (vertex shader) и фрагментного шейдера (fragment shader). Вершинный шейдер отвечает за позиционирование вершин объекта, а фрагментный — за цвет и текстуру пикселей. В A-Frame доступны стандартные атрибуты, такие как position, normal, uv, а также uniform-переменные для передачи данных из JavaScript.

AFRAME.registerShader('custom-shader', {
  schema: {
    color: {type: 'color', default: '#FFF'},
    time: {type: 'time', is: 'uniform'}
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 color;
    uniform float time;
    varying vec2 vUv;
    void main() {
      gl_FragColor = vec4(abs(sin(time + vUv.x)), 0.0, 0.0, 1.0);
    }
  `
});

Компоненты schema и uniform-переменные

Свойство schema описывает все данные, которые можно передавать шейдеру извне. Каждый параметр автоматически становится uniform-переменной в GLSL. Типы данных могут быть простыми (color, number, vec3) или специализированными (map для текстур, texture). Uniform-переменные обновляются каждый кадр, что позволяет создавать анимации и интерактивные эффекты.

schema: {
  color: {type: 'color', default: '#FF0000'},
  intensity: {type: 'number', default: 1.0}
}

В GLSL они доступны как обычные переменные:

uniform vec3 color;
uniform float intensity;

Использование атрибутов вершин

Вершинные атрибуты — это данные, связанные с геометрией модели. Наиболее часто используются:

  • position — координаты вершины
  • normal — нормаль вершины
  • uv — координаты текстуры

Эти атрибуты передаются в vertex shader автоматически. Для создания анимаций можно модифицировать позицию вершин с помощью тригонометрических функций или шума:

void main() {
  vec3 newPosition = position + normal * sin(time + position.x);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}

Взаимодействие с текстурами

Для работы с изображениями используется тип map. Текстура передаётся в шейдер как uniform-переменная типа sampler2D. Координаты uv позволяют правильно наложить текстуру на геометрию. В фрагментном шейдере текстуру можно смешивать с цветом:

uniform sampler2D map;
varying vec2 vUv;
uniform vec3 color;

void main() {
  vec4 texColor = texture2D(map, vUv);
  gl_FragColor = vec4(texColor.rgb * color, texColor.a);
}

Анимация и взаимодействие с временем

A-Frame автоматически передает uniform-переменную time в миллисекундах. Она удобна для создания динамических эффектов, таких как пульсация, волны или вращение:

float wave = sin(vUv.x * 10.0 + time * 0.001);
gl_FragColor = vec4(vec3(wave), 1.0);

Для интерактивности можно передавать значения через компоненты A-Frame, например, позицию контроллера, расстояние до камеры или события клика.

Разделение логики vertex и fragment

Vertex Shader: отвечает за преобразование вершин и геометрию. Здесь можно добавлять эффекты деформации, анимации поверхности, сдвиги текстуры и вращение.

Fragment Shader: работает на уровне пикселя. Здесь создаются эффекты освещения, смешивания цветов, наложения текстур, прозрачность и другие визуальные детали.

Пример комбинированного эффекта:

// Vertex Shader
varying vec2 vUv;
varying vec3 vNormal;
void main() {
  vUv = uv;
  vNormal = normal;
  vec3 pos = position + normal * sin(time + position.x);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

// Fragment Shader
uniform vec3 color;
varying vec2 vUv;
varying vec3 vNormal;
void main() {
  float light = dot(normalize(vNormal), vec3(0.0, 1.0, 0.0));
  gl_FragColor = vec4(color * light * abs(sin(time)), 1.0);
}

Оптимизация шейдеров в A-Frame

  1. Минимизация циклов и условий: GLSL плохо оптимизирует ветвления, особенно в fragment shader.
  2. Использование varying вместо повторного вычисления: передача данных из vertex shader в fragment shader экономит ресурсы.
  3. Повторное использование uniform-переменных: одно значение можно использовать в нескольких вычислениях, не создавая лишних переменных.
  4. Текстуры в правильном формате: использовать сжатые форматы (например, .jpg или .png с power-of-two размером) для уменьшения памяти GPU.

Примеры сложных эффектов

  • Градиентные поверхности: комбинируются координаты uv с математическими функциями.
  • Динамические шумовые текстуры: с использованием процедурного шума (Perlin, Simplex).
  • Имитация воды и огня: деформация вершин + цветовые градиенты + альфа-канал.
  • Glow и bloom эффекты: можно создать с помощью смешивания нескольких слоев и прозрачности.

Подключение к сущностям A-Frame

После регистрации шейдера его можно применять к любой сущности с компонентом material:

<a-box shader="custom-shader" color="#00FF00" position="0 1 -3"></a-box>
<a-sphere shader="custom-shader" color="#FF0000" position="2 1 -3"></a-sphere>

В этом случае все uniform-переменные шейдера будут доступны для динамического изменения через Jav * aScript:

document.querySelector('a-box').setAttribute('material', 'color', '#0000FF');

Использование кастомных GLSL-шейдеров в A-Frame открывает полное управление визуальной частью сцены, позволяет создавать уникальные эффекты и расширяет стандартные возможности движка WebVR.