Бурное развитие компьютерных технологий зачастую приводит к революционным изменениям в тех областях, где мы уже не ожидаем чего-нибудь принципиально нового. К таким случаям можно причислить появление шейдеров (shaders), которые свалились как снег на голову не только разработчикам ПО, но и создателям графических процессоров. Как это графических процессоров? — удивится читатель. — Ведь всегда именно разработчики графического оборудования были пионерами инноваций. Но только не в этот раз. Давно назревшая потребность в новых инструментах разработки для интерактивной виртуальной реальности наконец-то удовлетворена недавно вышедшей библиотекой DirectX 9. Полугодом ранее компания nVidia создала интереснейший инструмент CG — специальный графический компилятор для шейдеров, поддерживающих DirectX 9, но при этом до сих пор так и не выпустила в массовое производство графические процессоры для этого API. И только фирма ATI вполне успешно и массово продает графические процессоры с поддержкой DirectX 9 по достаточно приемлемым ценам.
Так что же шейдеры из себя представляют? Если кратко, то это довольно простые небольшие программки, выполняющиеся на графическом процессоре (ГП), а не на центральном (ЦП). На самом деле ГП уже давно взял на себя значительную долю обработки графики, освобождая тем самым ресурсы ЦП для других задач. Но принципиальная разница состоит в том, что ранее разработчики применяли специальные API для написания программ, которые взаимодействовали с ГП путем подготовки параметров и передачи их для выполнения отдельных функций. То есть ГП имел фиксированный набор функций, вызываемых по мере необходимости. Теперь же, благодаря шейдерам, взаимодействие с ГП сводится не к простому вызову его специфических функций, а к загрузке в него целых программ. Это дает как минимум два серьезных преимущества. Во-первых, значительно сокращается расходование ресурсов ЦП на подготовку данных для загрузки в ГП. Во-вторых, набор функций ГП стал на порядок разнообразнее благодаря тому, что разработчик фактически способен им полностью управлять. Это значит, что вместо фиксированной функциональности мы получаем платформу для разработки функций ГП.
Приведу пример. Допустим, ГП прошлого поколения предусматривает аппаратную реализацию какого-либо эффекта, а ГП нового поколения имеет шейдер для достижения того же эффекта. Но через год кто-нибудь придумает новую реализацию этого эффекта, делающую его более интересным. Владельцу старого ГП понадобится новая видеоплата, обладателю же ГП следующего поколения будет достаточно загрузить свежий шейдер. В еще большей степени это касается новых эффектов, которыми теперь можно совершенствовать графическую систему своего ПК. Пожалуй, единственным «но» останется производительность ГП — из-за нее все же придется периодически обновлять видеоплату, однако теперь уже гораздо реже.
Еще одно следствие появления шейдеров — принципиальное изменение методики программирования. Постепенно разработчики будут вынуждены полностью перейти от программирования фиксированной функциональности ГП к полностью программной, базирующейся на шейдерах. Фактически это означает, что роль таких API, как DirectX и OpenGL постепенно сойдет на нет, а главным станет универсальный и межплатформный язык шейдеров. Он уже сейчас позволяет не только реализовывать различные эффекты и преобразования, но и осуществлять полноценную анимацию, в том числе наиболее сложную анимацию персонажей. Для пользователей и разработчиков это дополнительно упростит перенос ПО с одной аппаратной платформы на другую.
Но для достижения последней цели необходимо, чтобы язык шейдеров стал не только высокоуровневым, но и универсальным.
Собственно язык
Шейдер — это текст программы, аналогичный ассемблерному коду, который разработчик должен загрузить в ГП. Исходный текст хранится либо в отдельном файле, либо как массив символов в коде программы. При загрузке «ассемблероподобной» программы в ГП она транслируется в машинный код для последующего выполнения в ГП.
Из вышесказанного следует вывод, что теперь программировать высококачественные приложения можно не только, к примеру, на Си++ или Паскале, но и на таких относительно «медленных» языках, как С#, Бейсик и Java. Ведь разработчику необходимо всего лишь создать подходящий программный контекст для ГП и загрузить в него строку символов! Роль языка программирования уменьшается, поскольку основная нагрузка ложится на ГП. Пожалуй, единственной проблемой останется недостаточная скорость обработки событий, связанных с функционированием клавиатуры, мыши или джойстика. Только она может помешать реализации быстрого интерактивного 3D-приложения для той или иной платформы.
Писать программы на ассемблере большинство из нас уже разучилось, а новое прагматичное поколение не так просто заставить неделями копаться в «примитивных» кодах ради достижения мизерных результатов. Именно поэтому разработчики программных интерфейсов создали специальный Си-подобный язык с развитыми операторами управления ходом программы. Благодаря этому с написанием собственных шейдеров справится даже непрофессиональный программист.
Представьте такую ситуацию: вы покупаете программный пакет (3D-игру, редактор и т. п.) в комплекте с шейдерами, полностью определяющими внешний вид программы. Я уверен, что любой продвинутый пользователь попробует отредактировать исходный текст шейдеров, дабы изменить до неузнаваемости всем привычный интерфейс или «поведение» стандартных компонентов. Конечно, такие эксперименты не всегда удачны, но тяга к творчеству, убежден, преодолеет все препятствия. Еще один аргумент в пользу того, что шейдерная технология коснется пользователей даже в большей степени, чем разработчиков, — уже реализованные системы с поддержкой шейдеров в таких программах, как Softimage, Maya, 3dStudioMax и др.
Но шейдеры — это все же дело будущего, скажете вы, ведь поддерживающая их библиотека DirectX 9 представлена совсем недавно, а OpenGL 2 существует пока только на бумаге. И будете не правы. На самом деле для работы с шейдерами не нужны ни DirectX 9 с Open GL 2, ни новейшие ГП компании ATI. Можно вполне успешно использовать ГП, совместимые с DirectX 8. Это обширнейший парк GeForce3 и 4 (но не MX), а также ATI Radeon (начиная с модели 8500 и выше). Правда, при этом не будет возможности реализовывать сложнейшие программные конструкции, поддерживаемые только в новейших ГП. Но язык шейдеров вполне доступен.
Между тем для современного языка шейдеров требуется еще компилятор, преобразующий его высокоуровневые конструкции в ассемблерные. Самый большой ажиотаж наблюдался вокруг выпуска компанией nVidia собственного инструмента разработки шейдеров — CG. С одной стороны, это радовало, так как теперь можно было разрабатывать интересные программы и для Windows, и для Linux. С другой стороны, — к сожалению, только для ГП nVidia. Но, как оказалось, и компания 3DLabs, флагман разработки проекта OpenGL 2, создала и сделала доступным для желающих компилятор шейдеров вместе с исходными текстами. Это можно считать большим шагом вперед, поскольку любая фирма с лета 2002 г. могла приступать к разработке шейдеров для своих программных или аппаратных решений. Сейчас на сайте www.opengl.org находится ссылка на компилятор шейдеров, так что теперь уж точно нет причин утверждать, что данная технология вам недоступна.
Нюансы
Теперь вкратце рассмотрим технологические аспекты: что именно представляют собой шейдеры. Как я уже упоминал, шейдер — это программа, но не обычная. Важнейшая нетрадиционная черта — конвейерный принцип обработки данных. То есть шейдеры обрабатывают объекты не целиком, а по мельчайшим частицам — вершинам или пикселам. Таким образом, вы не сможете предварительно анализировать и «осмыслять» объект. Также не будет возможности применять свои сложные алгоритмы для отдельных частей объекта: алгоритм поэлементной обработки вынужден быть не только простым, но и универсальным, поскольку каждый элемент обрабатывается одним и тем же алгоритмом.
Для решения этой проблемы разработчик должен учитывать еще две особенности шейдеров. Во-первых, это возможность взаимодействия шейдера с программой для ЦП путем набора переменных — в них допустима загрузка данных перед выполнением шейдера, но не во время него. Эту процедуру также можно отнести к процессу подготовки контекста выполнения шейдера. Вторая особенность, более важная, — это наличие множества специальных аппаратно реализованных функций ГП, которые способны перенести нас от обычных вычислений в векторные и аффинные. Именно благодаря этим функциям с помощью несложных шейдеров можно добиваться потрясающих эффектов, создавая реалистичные и сюрреалистические поверхности и даже имитируя шерсть. Однако используемая в шейдерах аффинная арифметика наряду с конвейерной архитектурой поначалу сбивают с толку некоторых разработчиков своим особым подходом к программированию. Поэтому потребуется определенное время для переучивания и осмысления новых концепций. Очень полезным событием в связи с этим можно назвать объявленный российским Интернет-ресурсом www.ixbt.com конкурс, участники которого должны создать свои шейдеры. Их исходные тексты останутся на сайте.
Теоретические основы 3D-программирования
Какой бы инструмент вы ни выбрали для программирования 3D-графики, всегда придется иметь дело с определенным базисом, на котором основана практически вся 3D-отрасль, будь то ГП, OpenGL, DirectX, Java3D или Shockwave. К такому базису можно отнести ключевые операции и алгоритмы 3D-графики. Безусловно, сами алгоритмы основаны на операциях, но зачастую каждый из них хотя и встречается во всех разновидностях 3D-продуктов (редакторы, игры, движки, системы рендеринга и проч.), тем не менее может иметь различные модификации. Из этого следует, что пусть алгоритмы и являются базисом, они все же могут иметь разные представления, а поэтому в большинстве случаев программируются каждый раз заново. Возьмем, к примеру, алгоритм сглаживания каркасных сеток. Имея множество вариантов, он обладает своими особенностями в Lightwave3D и 3dStudioMax. Более того, в каждой из этих программ есть как минимум по два алгоритма сглаживания сеток и ни один из них не идентичен другому. Аналогичная ситуация и с ГП. Так, у компании nVidia совершенно иное представление о сглаживании каркасных сеток, чем у ATI, что повлекло за собой создание двух несовместимых стандартов сглаживания.
К таким же алгоритмам можно отнести расчет освещения в сцене, вычисление инверсной кинематики, идентификацию столкновений и проч. Существуют и более простые алгоритмы: определение пересечения прямой с плоскостью, отсечение, свертка и др. Они обладают меньшим количеством вариаций, достаточно хорошо отработаны и в некоторых случаях реализованы в виде базовых операций (условно, как правило, такие алгоритмы реализованы в объектно-ориентированных системах и упакованы в классы, к примеру в Java3D API).
Но вернемся к ключевым операциям, на которых основаны не только базовые алгоритмы, но и вся компьютерная графика. Сами они основываются на специальных данных и командах (функциях). К данным относятся числа с плавающей точкой (float point), векторы и матрицы с размерностью от двух до четырех.
Числа с плавающей точкой предназначены для числовых данных. Обязательность наличия «плавающей точки» объясняется тем, что во многих ситуациях используются нормализованные данные со значениями от 0 до 1, или от -1 до 1. Поэтому требуется достаточное разрешение в промежутке между 1 и 0. Следовательно, необходимо представлять числа как минимум 64-битовыми записями. Но это касается аппаратной реализации, так что в принципе разработчику никто не мешает делать нужные преобразования во время выполнения программы и некоторые библиотеки позволяют обойти это ограничение.
Вектором является совокупность числовых значений (их может быть от двух до четырех). Основная потребность в них возникла из-за необходимости «упаковать» координаты (x, y, z) 3D-точки и составляющие цвета (red, green, blue для RGB) в одно значение. Поэтому в сущности векторы представляют собой массивы, но функциями обрабатываются как одиночные значения.
Матрицей является двумерный «квадратный» массив чисел с равным количеством столбцов и строк. Их широкому применению в 3D-графике способствовала аффинная арифметика; некоторые ее аспекты мы обсудим ниже.
А теперь рассмотрим, какие команды используются в 3D-графике, и в частности в шейдерах. Как я уже упоминал, существует набор специальных аппаратных функций, позволяющих достаточно легко выполнять различные преобразования и анализ в 3D-пространстве буквально за один шаг, поскольку одна и та же операция, без изменения параметров, способна перемещать объект, вращать его или трансформировать.
Представьте себе огромную модель с десятками тысяч вершин. Когда возникает потребность повернуть этот массив на какой-либо угол, переместить или масштабировать, то нет необходимости вычислять новое положение всех вершин при помощи хитроумных методов. Достаточно координаты каждой из них умножить на специальную матрицу трансформации, в которой определенные поля отвечают за некоторый параметр. Только в этом случае для всякого вида трансформации потребуется своя матрица. А если изменение координат объекта имеет нелинейный характер, то можно использовать серию умножений как частных случаев всей трансформации объекта. Это и есть конвейерный принцип обработки данных. Для его лучшего понимания полезно почитать литературу по аффинной арифметике. Возникшая задолго до появления компьютеров, она тем не менее как нельзя лучше подходит для конвейерной обработки в 3D-пространстве. Но для нас, простых «писателей» шейдеров, важны не принципы арифметики, а ее частные случаи. Это значит, что для выполнения преобразования нужно знать только шаблоны матриц для сдвига, скоса, масштабирования, вращения и т. д.
Матрицы преобразования |
Во врезке приведены примеры матриц преобразования.
Интересно и весьма полезно также использование в 3D-графике векторного и скалярного произведения векторов. Как вы наверняка помните, векторное произведение позволяет получить перпендикуляр к плоскости — нормаль. В нашем случае плоскость представляется тремя вершинами (vertex) или двумя отрезками, созданными из этих вершин. Теперь о нормали к полигону. На самом деле она является аккумулированной характеристикой полигона. Дело в том, что большинство алгоритмов 3D-графики основано на векторах, а треугольный полигон — это как минимум сразу два вектора. Поэтому для дальнейших вычислений довольно удобна эта объединенная характеристика полигона. Получив нормали двух полигонов, легко вычислить углы между ними, а также между полигонами и лучами. Последние широко используются для определения пересечений полигонов с указателем устройства ввода (к примеру, мыши), источниками освещения и проч. И как раз в этом случае на помощь приходит скалярное произведение. Ведь его результатом является не что иное, как косинус угла между векторами, а значит, можно определить и сам угол.
Рассмотренные выше и многие другие геометрические функции являются вычислительным базисом в области программирования 3D-графики. С их помощью можно решать любые задачи по обработке и созданию объектов виртуальных миров. Поэтому они реализованы в графических процессорах аппаратно, дабы упростить и ускорить процесс обработки графики. В шейдерах они представлены в виде функций, которые разработчик может вызывать, предварительно указав нужные параметры. В спецификации OpenGL 2 их несколько десятков. Если рассматривать по группам, то это тригонометрические, арифметические и геометрические функции, функции обработки экспоненты, умножения матриц, а также специализированные для обработки вершин и текстур.
* * *
В этой статье я попытался объяснить, что такое шейдеры, и дать минимальные теоретические основы для начала программирования 3D-графики. В дальнейшем мы обратимся к данному вопросу с сугубо практической стороны. А именно рассмотрим, как внедрять шейдеры в код на Си/Cи++, С#, как использовать графический компилятор и инструменты компании nVidia, а также познакомимся с примерами шейдеров.
С автором можно связаться по e-mail: Vit3D@mail.ru.