История
На платформе PC точкой отсчета для шейдеров является видеокарта NVidia GeForce 2 GTS. Именно в этой карте впервые появились шейдеры, но они не произвели прорыва. В связи с тем, что в то время шейдеры не были специфицированы и не поддерживались производителями игр, эта видеокарта так и осталась неким пробным камнем.

Официальная история шейдеров начинается с серии GeForce 3 — в то время появились шейдеры версии 1.Х и спецификации DirectX 8.1. Для того чтобы не отягощать тебя лишними индексами и моделями карт, далее я буду рассказывать только о поколениях шейдеров (с точки зрения программистов). Итак, сейчас мы имеем три поколения: 1.Х, 2.Х и 3.Х. На сегодняшний день (до релиза Windows Longhorn) последняя программная спецификация от Microsoft называется DirectX 9.0c, она включает в себя поддержку всех существующих версий шейдеров.

терминология
С точки зрения разработчика графической программы, шейдер — это набор состояний графического конвейера плюс программный код, который выполняется графическим процессором (GPU). Например, в первом Quake тоже были шейдеры. Удивлен? Все очень просто, таким термином обзывали программную абстракцию, которая управляла различными состояниями видеокарты. Бессмысленно рассматривать код, исполняемый GPU, отдельно от состояний графического конвейера — это единое и неделимое целое.

графический конвейер
Прежде чем начинать изучение шейдеров, необходимо представлять себе, как работает современный графический конвейер, какие данные приходят ему на вход и как он обрабатывает их. Представь себе, что ты хочешь нарисовать на экране трехмерный куб. В памяти твой куб будет представляться в виде набора вершин, которые описывают составляющие его треугольники (примитивы). Каждая вершина характеризуется своими координатами в 3D-пространстве: x, y, z — минимум, без которого не обойтись. Обычно вершина содержит дополнительные параметры: нормаль, диффузный и спекулярный цвета, несколько наборов текстурных координат и т.д. Для оптимизации и чтобы задействовать вершинный кеш более эффективно, примитивы используют общие вершины.

Формирование изображения на экране происходит следующим образом. В каждый момент времени главный поток программы генерирует кадры изображения. Например, для комфортной игры в шутер необходимо генерировать минимум 25 кадров в секунду. Создание кадра начинается с того, что карте сообщается, какую именно геометрию ей необходимо нарисовать для этого кадра (шаг «буфер вершин»). Каждая вершина обрабатывается вершинным процессором, в который заранее загружена вершинная программа (вершинный шейдер). Обычно эта программа трансформирует вершины исходя из заданных мировой и видовой матриц и для каждой вершины вычисляет специфические данные (нормаль, цвет). Как ты помнишь, вся геометрия разбивается на треугольники. Видимые на экране фрагменты треугольников (после отброса невидимых фрагментов) попадают в пиксельный процессор, где они обрабатываются фрагментным процессором (пиксельный шейдер), который рассчитывает цвет каждого фрагмента. Откуда берутся компоненты цвета фрагмента? Из текстур, покрывающих треугольник, из освещенности и затененности фрагмента и прочих эффектов. Параметры для каждого фрагмента передаются в пиксельный шейдер интерполированными по всему обрабатываемому треугольнику, то есть они находятся в прямой зависимости от параметров вершин треугольника. После расчета цвета фрагмента он смешивается с текущим кадром в зависимости от настроек конвейера. Буферы между различными частями конвейера позволяют задействовать конвейер наиболее эффективно — с целью минимизации его простоев.

поколения процессоров

Сравним возможности вершинных и фрагментных процессоров трех поколений. Поскольку первое поколение шейдеров было самым многочисленным (в плане версий), я возьму для сравнения только версию 1.4, остальные практически не отличаются от нее. Для версии шейдеров 2.0 буду рассматривать спецификацию расширенных шейдеров 2.Х.

Как видно по сравнительной таблице, с каждым новым поколением возрастает длина шейдеров и количество регистров, что позволяет усложнять и усложнять используемые эффекты. Введение инструкций для управления кодом (циклы, условия) позволяет проверять передаваемые из базового кода, выполняемого обычным CPU, условия и параметры (например позиции источников света). Благодаря этому становится возможным создавать реалистичное попиксельное освещение, зависящее от нескольких различных лампочек, создавать реальные (мягкие) тени, эффекты глубины поверхностей и т.д.

вершинный процессор
Теперь покопаемся в кишочках каждого процессора, первый в очереди — вершинный. Как видно по диаграмме «Графический конвейер», на его вход попадают вершины треугольников, образующих сцену. Для каждой вершины выполняется вершинный шейдер, заранее загруженный в видеокарту. Посмотри на рисунок «Пример шейдера»: в XML-узле VertexShader содержится код вершинной программы (функция main). Эта программа довольно простая, к каждой вершине она добавляет диффузный цвет и вычисляет значение текстурных координат для специальной текстуры, которые изменяются в зависимости от того, под каким углом мы смотрим на сцену. Однако для того чтобы начать расчет нужных данных, мы должны перевести координаты вершины из базового пространства (единичной системы координат) в мировое пространство — эти вычисления обязательны, если мы хотим, чтобы сцена отображалась на экране с учетом камеры и координат в мировом (сценическом) пространстве. Это преобразование выполняется с помощью строчки: o.pos = mul( i.pos, WorldViewProjection ). Данные о вершине попадают в функцию main() из потока входных данных — это так называемый однородный (uniform) ввод данных, которые попадают в процессор из локальной памяти видеокарты в регистры входных данных (Input registers). Данные, которыми ты влияешь на шейдер из своей управляющей программы, работающей на CPU, попадают в программу через константные регистры (варьируемый вход). Эти данные не могут быть изменены в коде шейдера, поэтому и называются константными. В нашем примере константные данные — различные матрицы (видовая и мировая), а также диффузный цвет вершины. Временные регистры используются для сохранения промежуточных результатов вычислений (в нашем случае — переменные nrm и e2v). Регистр предикатов содержит в себе флаг — некоторое условие, которое может быть задано заранее (извне шейдера) или рассчитано в программе. С помощью этого условия можно повлиять на ход выполнения программы (ветвления). В шейдерах второго поколения (2.0 и 2.Х) возможны ветвления кода, причем в шейдерах версии 2.0 возможны только статические ветвления (циклы и подпрограммы), то есть проверка условий, заданных извне шейдера. В расширенных шейдерах 2.Х возможно и динамическое управление ходом выполнения шейдера. Например, если рассчитанный диффузный цвет вершины красный, то следует делать одно, если красной компоненты не содержится — делать другое. После того как ты выполнишь все необходимые вычисления с входными данными, настанет время заполнить структуру выходных данных. Она ничем не отличается от структуры входных данных, за исключением текстурных координат. Выходные данные передаются через выходные регистры. Подведем итоги: вершинный процессор получает вершину, переводит ее из базового пространства в пространство мира и камеры, подготавливает данные вершины (текстурные координаты) для последующей обработки фрагментным процессором.

на пути к фрагментному процессору
После обработки вершинным процессором данные геометрии сцены складируются в промежуточном буфере готовых вершин. Из вершин формируются треугольники, а из треугольников — их фрагменты. С точки зрения программ, выполняемых GPU, этот процесс незаметен, поскольку на вход пиксельному шейдеру, завершающему обработку, попадают видимые фрагменты треугольников, но ты должен понимать все происходящее внутри видеокарты.

Три готовые вершины образуют треугольник. Треугольник разбивается на фрагменты. Эти фрагменты проверяются на видимость на экране с помощью Z-теста (проверка на видимость), и уже видимые фрагменты попадают на вход фрагментной программе. Почему я заостряю твое внимание на этом процессе? Здесь есть одна тонкость. Ранее ты рассчитывал текстурные координаты для вершин, а фрагментному процессору достаются текстурные координаты для фрагментов. Откуда они берутся? Интерполированием по всему треугольнику, между текстурными координатами трех его вершин.

фрагментный процессор
Посмотри еще раз на код примера (XML узел PixelShader). Входные данные для фрагментного процессора аналогичны выходным данным вершинного процессора с учетом того, что они интерполированы для конкретного фрагмента внутри треугольника. На выходе из фрагментной программы мы должны получить результирующий цвет фрагмента с учетом текстур, освещения, затенения и т.д. (в зависимости от решаемой задачи). В константных регистрах процессора задаются константы из главной программы. Временные регистры служат для сохранения промежуточных результатов вычислений. Самплеры — это то же самое, что и текстуры. Регистры текстурных координат содержат интерполированные текстурные координаты для фрагмента. Регистр предикатов и, соответственно, статические и динамические ветвления появляются только в 2.Х-версии пиксельных шейдеров. В последнем (третьем) поколении шейдеров появляется регистр стороны треугольника, который содержит положительное значение для передней стороны и отрицательное — для задней. Эта возможность используется для выбора различных схем расчета результирующего цвета (выбор освещения) для разных сторон треугольников.

недалекое будущее

Гадание на кофейной гуще — чрезвычайно увлекательный процесс, именно им и занимаются многие аналитики IT-рынка. Их фантазии нельзя назвать никак по-другому. В пришествии технологии шейдеров на платформу PC нет ничего удивительного. Если в прошлом видеокарты выполняли фиксированный набор действий, то теперь этих действий стало на порядок больше за счет того, что появилась возможность писать управляющие программы (шейдеры) для графических процессоров. После резкого перехода от технологий фиксированного к технологиям программируемого графического конвейера индустрия продолжила свою эволюцию. Шейдеры обрастали мясом функциональности, новыми возможностями и регистрами, высокой частотой и возможностями распараллеливания вычислений. На сегодня мегамонстр PC-графики держит на своем борту 512 Мб памяти, работает на частоте 600 МГц, данные обрабатываются 24-мя пиксельными конвейерами. Также имеются решения на базе технологии SLI из двух видеокарт. Налицо тенденции к распараллеливанию вычислений. Почему существуют такие тенденции? Все очень просто, инженеры подходят к границам миниатюризации технологического процесса, преодолеть которые невозможно физически. Так что остается двигаться в сторону параллельных вычислений: многоядерные процессоры, многоканальные контроллеры памяти, мультиконвейерные графические решения. В ближайшем будущем нас ожидает новая операционная система от Microsoft, вместе с ней к нам придет новая архитектура графических драйверов и новый DirectX. Прыгнут ли графические технологии еще раз вперед? Вряд ли. Получим ли мы ощутимую прибавку к производительности и качеству? Наверняка. Нужно ли нам это как пользователям? Вот здесь я сильно сомневаюсь, мне, к примеру, надоело менять видеокарту каждые два года, отваливая за нее от $100 до 200. А тебе?