Вершинный и фрагментный шейдеры: роли и взаимодействие

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

Основные задачи вершинного шейдера

  1. Преобразование координат Для каждой вершины используется матрица модели, вида и проекции:

    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);

    Здесь position — локальные координаты вершины, а итоговая gl_Position определяет её положение на экране.

  2. Передача атрибутов Вершинный шейдер может передавать фрагментному шейдеру любые данные через varying (GLSL 1.0) или out (GLSL 3.0), например цвета, нормали или UV-координаты:

    varying vec3 vNormal;
    void main() {
        vNormal = normalMatrix * normal;
        gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    }

    Здесь vNormal используется для расчёта освещения в фрагментном шейдере.

  3. Динамические эффекты Вершинный шейдер позволяет реализовывать эффекты деформации: колебания вершин, волны, анимацию скелета, морфинг. Например, простая волна по оси Y:

    vec3 pos = position;
    pos.y += sin(time + position.x) * 0.5;
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0);

Фрагментные шейдеры: формирование пикселя

Фрагментный шейдер (fragment shader) отвечает за определение окрашивания каждого пикселя, который попадает в область, образованную треугольниками. Здесь вычисляется цвет, прозрачность, освещённость и любые визуальные эффекты поверхности.

Основные задачи фрагментного шейдера

  1. Расчёт цвета Цвет пикселя может определяться как текстурой, так и результатом освещения:

    varying vec3 vNormal;
    uniform vec3 lightDir;
    
    void main() {
        float intensity = max(dot(normalize(vNormal), normalize(lightDir)), 0.0);
        vec3 color = vec3(1.0, 0.5, 0.2) * intensity;
        gl_FragColor = vec4(color, 1.0);
    }

    Здесь vNormal — нормаль, полученная из вершинного шейдера, а lightDir — направление источника света.

  2. Работа с текстурами Использование sampler2D позволяет накладывать текстуры:

    uniform sampler2D map;
    varying vec2 vUv;
    
    void main() {
        vec4 texColor = texture2D(map, vUv);
        gl_FragColor = texColor;
    }
  3. Постобработка и эффекты Фрагментный шейдер используется для эффектов, таких как размытие, свечения, fog, ambient occlusion. Например, простое затемнение по глубине:

    varying float vDepth;
    
    void main() {
        float fog = smoothstep(5.0, 50.0, vDepth);
        vec3 baseColor = vec3(0.8, 0.3, 0.1);
        gl_FragColor = vec4(baseColor * (1.0 - fog), 1.0);
    }

Взаимодействие вершинного и фрагментного шейдеров

Вершинный и фрагментный шейдеры тесно связаны через передаваемые данные. Основные принципы:

  1. Varying-переменные Данные, рассчитанные в вершинном шейдере, интерполируются по поверхности треугольника и передаются во фрагментный шейдер:

    varying vec3 vNormal;
    void main() {
        vNormal = normalMatrix * normal;
        gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    }

    Фрагментный шейдер получает интерполированные значения для каждого пикселя.

  2. Единая логика освещения Вершинный шейдер отвечает за нормали и позиции вершин, фрагментный — за финальный цвет с учётом этих данных. Это позволяет реализовать Phong, Blinn-Phong или PBR освещение.

  3. Оптимизация Часть вычислений можно делать в вершинном шейдере для уменьшения нагрузки на фрагментный, но слишком грубые приближения могут привести к заметной дискретизации на поверхности.


Практическая интеграция в Three.js

В Three.js вершинный и фрагментный шейдеры задаются через ShaderMaterial:

const material = new THREE.ShaderMaterial({
    vertexShader: vertexShaderSource,
    fragmentShader: fragmentShaderSource,
    uniforms: {
        time: { value: 0.0 },
        lightDir: { value: new THREE.Vector3(1, 1, 1) }
    }
});
  • vertexShaderSource — строка с GLSL кодом вершинного шейдера.
  • fragmentShaderSource — строка с GLSL кодом фрагментного шейдера.
  • uniforms — глобальные переменные, доступные в обоих шейдерах.

Использование ShaderMaterial позволяет полностью контролировать рендеринг, создавая уникальные визуальные эффекты и динамику геометрии.


Особенности работы с нормалями и координатами

  • normalMatrix — матрица для корректного преобразования нормалей при трансформации модели.
  • modelMatrix, viewMatrix, projectionMatrix — ключевые матрицы для перевода вершин из локальной системы координат в экранные координаты.
  • gl_Position — обязательная переменная вершинного шейдера, определяющая итоговое положение вершины на экране.
  • Интерполяция varyings — GLSL автоматически интерполирует значения между вершинами для каждого фрагмента.

Типичные ошибки

  1. Несоответствие типов данных между varying в вершинном и фрагментном шейдере.
  2. Игнорирование нормалей при расчёте освещения, что делает объект плоским и лишённым глубины.
  3. Перегрузка фрагментного шейдера сложными вычислениями на каждый пиксель, что сильно снижает производительность.
  4. Неправильная работа с матрицами, особенно при комбинации model, view и projection.

Расширенные техники

  • Morph Targets — изменение позиции вершин в вершинном шейдере для анимации.
  • Tessellation и displacement — увеличение детализации поверхностей через вершины.
  • Deferred shading — разделение геометрической обработки и освещения с использованием нескольких проходов.
  • Post-processing — использование фрагментных шейдеров для эффектов экрана (glow, blur, bloom).

В совокупности, вершинные и фрагментные шейдеры дают полный контроль над визуальной составляющей 3D-сцены, позволяя создавать как реалистичные, так и стилизованные эффекты.