Вершинный шейдер

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

Вершинный шейдер запускается ровно один раз на каждую вершину, преобразует её позицию в clip space и передаёт атрибуты дальше по конвейеру.

Зачем это знать

Именно здесь объект попадает в нужное место, под нужным углом и в перспективе — через умножение на MVP. Здесь же делают анимацию вершин: волны на воде, развевающийся флаг, «скининг» персонажей. Без вершинного шейдера геометрия не сдвинется с места.

Вход и выход

ВходВыход
атрибуты вершины: позиция, нормаль, UV, цветобязательно: gl_Position (clip space)
uniform: матрицы MVP, время, параметрыvarying: данные для фрагментного шейдера

Главная обязанность: gl_Position

Каждый вершинный шейдер обязан записать gl_Position — позицию вершины в clip space. Это умножение на MVP. Всё прочее (передача UV, нормалей дальше) — по необходимости.

// Вершинный шейдер с передачей данных дальше
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
uniform mat4 MVP;
varying vec3 vNormal;   // полетит во фрагментный шейдер
varying vec2 vUV;
void main() {
    vNormal = normal;
    vUV = uv;
    gl_Position = MVP * vec4(position, 1.0);
}

Анимация вершин: волна

Вершинный шейдер может смещать позиции — так делают воду. Идея: к высоте вершины прибавить синус от её координаты и времени. Смоделируем на Python для одной линии вершин.

import math

def wave_height(x, t, amp=0.5, freq=1.0, speed=2.0):
    return amp * math.sin(freq * x + speed * t)

t = 1.0  # момент времени
for x in range(0, 7):
    h = wave_height(x, t)
    bar = "#" * int((h + 0.5) * 10)
    print(f"x={x}  height={h:+.2f}  {bar}")

Вывод:

x=0  height=+0.45  #########
x=1  height=+0.07  #####
x=2  height=-0.38  #
x=3  height=-0.48
x=4  height=-0.14  ###
x=5  height=+0.33  ########
x=6  height=+0.49  #########

Высота каждой вершины зависит от её x и времени t — на GPU это даст бегущую волну. Меняя t каждый кадр, получаем анимацию.

Что вершинный шейдер не может

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

Как работает под капотом

Выходные varying-переменные не попадают во фрагментный шейдер как есть — они интерполируются по поверхности треугольника во время растеризации. Если в трёх вершинах UV равны (0,0), (1,0), (0,1), то в середине треугольника фрагмент получит промежуточное UV. Об этой интерполяции — следующий урок.

Частые ошибки

  • Не записать gl_Position — вершина не появится, рендер сломается.
  • Пытаться в вершинном шейдере обратиться к текстуре цвета пикселя — это работа фрагментного.
  • Ожидать, что varying придёт во фрагмент неизменным — он интерполируется между вершинами.

Итоги

  • Вершинный шейдер запускается на каждую вершину и обязан задать gl_Position.
  • Вход — атрибуты вершины и uniform; выход — clip-позиция и varying.
  • Он умеет анимировать геометрию (волны, флаги), но не создаёт вершины.
  • Varying-переменные интерполируются по треугольнику перед фрагментным шейдером.
Проверьте себя
1. Что обязан записать любой вершинный шейдер?
Agl_FragColor
Bgl_Position — позицию вершины в clip space
Cтекстуру
Dномер кадра
2. Может ли вершинный шейдер анимировать геометрию (например, воду)?
AНет, никогда
BДа, смещая позиции вершин по формуле от времени
CТолько через CPU
DТолько в 2D
3. Что происходит с varying-переменными между вершинным и фрагментным шейдером?
AОни удаляются
BОни интерполируются по поверхности треугольника
CОни умножаются на MVP
DОни становятся uniform