Text
                    

Рэнди Дж. Рост OpenGL Яг Яг Яг Трехмерная графика и язык программирования шейдеров ^ППТЕР Москва • Санкт-Петербург - Нижний Новгород Воронеж Новосибирск - Ростов-на-Дону Екатеринбург • Самара Киев Харьков Минск 2005
Краткое содержание Отзывы о книге................................... 16 Об этой книге ...................................... 17 Предисловие.......................................... 19 Об авторе................................................24 Об авторах разделов, вошедших в книгу....................25 Благодарности.................................... 26 Глава 1. Обзор OpenGL ............ 28 Глава 2. Основы.....................55 Глава 3. Определение языка........ 79 Глава 4. Программируемая часть операций OpenGL................................102 Глава 5. Встроенные функции........121 Глава 6. Пример простого шейдера. 139 Глава 7. API языка шейдеров OpenGL.152 Глава 8. Разработка шейдера........182 Глава 9. Традиционные шейдеры....................... 193 Глава 10. Шейдеры с сохранением данных в текстурах..............................209 Глава 11. Процедурные текстурные шейдеры................233
6 Краткое содержание Глава 12. Шум......................................254 Глава 13. Анимированные шейдеры....................274 Глава 14. Сглаживание процедурных текстур..........290 Глава 15. Нефотореалистичные шейдеры...............305 Глава 16. Шейдеры для обработки изображения........324 Глава 17. Сравнение языков программирования .......343 Послесловие.................................. 354 Приложение А. Грамматика языка................... 356 Приложение Б. Справочник функций API.............363 Приложение В. Перевод подписей к цветным рисункам...............................407 Словарь терминов................................ .411 Алфавитный указатель............................ 423
Содержание Отзывы о книге................................................ 16 Об этой книге............................................. 17 Предисловие................................................... 19 Предполагаемая аудитория........................................20 Содержание книги................................................20 О примерах шейдеров............................................. 22 Опечатки........................................................23 От издателя перевода............................................ 23 Об авторе..........................................................24 Об авторах разделов, вошедших в книгу............................. 25 Благодарности.................................................. 26 Глава 1. Обзор OpenGL..............................................28 1.1. История OpenGL.............................................28 1.2. Развитие OpenGL.......................................... 30 1.3. Архитектура приложений OpenGL..............................31 1.4. Буфер кадров...............................................31 1.5. Состояние..................................................34 1.6. Конвейер операций..........................................34 1.7. Рисование геометрических фигур.............................35 1.7.1. Определение геометрических фигур......................35 1.7.2. Операции обработки вершин.............................38 1.7.3. Сборка примитивов.....................................39 1.7.4. Обработка примитивов..................................39 1.7.5. Растеризация..........................................40 1.7.6. Предварительная обработка фрагментов..................41 1.7.7. Операции над фрагментами..............................41 1.7.8. Операции с буфером кадров.............................41 1.8. Рисование изображений......................................42 1.8.1. Распаковка пикселов...................................42 1.8.2. Перемещение пикселов..................................43
8 Содержание 1.8.3. Растеризация и окончательная обработка...................43 1.8.4. Проверка чтения ..........................................44 1.9. Преобразования координат......................................44 1.10. Текстурирование..............................................48 1.11. Итоги........................................................ 53 1.12. Ссылки....................................................... 53 Глава 2. Основы....................................................... 55 2.1. Введение в язык шейдеров OpenGL...............................55 2.2. Для чего нужны шейдеры...................................... 56 2.3. Программируемые процессоры OpenGL.............................57 2.3.1. Вершинный процессор......................................58 2.3.2. Фрагментный процессор....................................62 2.4. Общее представление о языке...................................65 2.4.1. Анализ языка шейдеров....................................65 2.4.2. Основы С............................................... 67 2.4.3. Дополнения к языку С.....................................67 2.4.4. Дополнения из C++........................................69 2.4.5. Не поддерживаемые возможности С..........................69 2.4.6. Другие отличия ..........................................69 2.5. Обзор системы.................................................70 2.5.1. Модель драйвера..........................................70 2.5.2. Компилятор и компоновщик языка шейдеров OpenGL...........72 2.5.3. Расширения OpenGL API.................................. 73 2.6. Основные преимущества.........................................75 2.7. Итоги.........................................................77 2.8. Ссылки........................................................78 Глава 3. Определение языка............................................. 79 3.1. Примеры шейдеров..............................................79 3.2. Типы данных...................................................81 3.2.1. Скалярные типы...........................................81 3.2.2. Векторы..................................................82 3.2.3. Матрицы................................................ 83 3.2.4. Дискретизаторы...........................................84 3.2.5. Структуры................................................85 3.2.6. Массивы..................................................85 3.2.7. Void................................................... 86 3.2.8. Объявления и область видимости......................... 86 3.2.9. Согласование и преобразование типов................... 87 3.3. Инициализаторы и конструкторы.................................87 3.4. Преобразования типов..........................................89 3.5. Спецификаторы и интерфейс шейдера........................... 89 3.5.1. Спецификатор attribute...................................90 3.5.2. Спецификатор uniform.....................................90
Содержание 9 3.5.3. Спецификатор varying.................................... 91 3.5.4. Спецификатор const........................................91 3.5.5. Отсутствие спецификатора..................................92 3.6. Последовательность выполнения .................................92 3.6.1. Функции ..................................................93 3.6.2. Соглашение о вызовах......................................93 3.6.3. Встроенные функции........................................94 3.7. Операции ......................................................95 3.7.1. Обращение по индексу......................................95 3.7.2. Обращение к компонентам...................................96 3.7.3. Покомпонентные операции...................................97 3.8. Препроцессор...................................................98 3.9. Выражения препроцессора........................................99 3.10. Обработка ошибок............................................ 100 3.11. Итоги..................................................... 100 3.12. Ссылки...................................................... 100 Глава 4. Программируемая часть операций OpenGL.......................102 4.1. Вершинный процессор.......................................... 102 4.1.1, Атрибуты вершины........................................ 104 4.1.2. Uniform-переменные...................................... 105 4.1.3. Выходные переменные..................................... 105 4.1.4. Встроенные varying-переменные........................... 106 4.1.5. Определенные разработчиком varying-переменные.......... 107 4.2. Фрагментный процессор.................................... 107 4.2.1. Varying-переменные...................................... 108 4.2.2. Uniform-переменные...................................... 109 4.2.3. Переменные для входных данных........................... 109 4.2.4. Переменные для выходных данных.......................... 109 4.3. Встроенные uniform-переменные................................ 110 4.4. Встроенные константы......................................... 114 4.5. Взаимодействие со стандартными операциями OpenGL............. 115 4.5.1. Двухсторонний цветовой режим............................ 115 4.5.2. Режим размера точки..................................... 115 4.5.3. Отсечение............................................... 116 4.5.4. Координаты растра..................................... 117 4.5.5. Инвариантность координат................................ 117 4.5.6. Текстурирование......................................... 118 4.6. Итоги........................................................ 119 4.7. Ссылки....................................................... 119 Глава 5. Встроенные функции.......................................... 121 5.1. Угловые и тригонометрические функции......................... 122 5.2. Экспоненциальные функции..................................... 123 5.3. Общие функции................................................ 124
10 Содержание 5.4. Геометрические функции...................................... 130 5.5. Матричные функции......................................... 131 5.6. Функции отношения векторов.................................. 132 5.7. Функции доступа к текстуре.................................. 134 5.8. Функции обработки фрагмента..................................136 5.9. Функции шума.................................................136 5.10. Итоги...................................................... 137 5.11. Ссылки..................................................... 138 Глава 6. Пример простого шейдера......................................139 6.1. Обзор шейдера кирпичной стены............................... 139 6.2. Вершинный шейдер............................................ 140 6.3. Фрагментный шейдер.......................................... 145 6.4. Замечания ................................................ 149 6.5. Итоги....................................................... 150 6.6. Ссылки...................................................... 150 Глава 7. API языка шейдеров OpenGL....................................152 7.1. Создание шейдерных объектов................................. 153 7.2. Компиляция шейдерных объектов............................... 154 7.3. Компоновка и использование шейдеров......................... 155 7.4. Удаление данных............................................. 158 7.5. Функции запроса состояния....................................158 7.6. Установка атрибутов вершин.............................. 161 7.7. Установка uniform-переменных.............................. 169 7.8. Семплеры.................................................... 174 7.9. Средства диагностики........................................ 175 7.10. Значения, зависящие от реализации.......................... 176 7.11. Код приложения для шейдеров кирпичной стены ................176 7.12. Итоги...................................................... 180 7.13. Ссылки..................................................... 181 Глава 8. Разработка шейдера......................................... 182 8.1. Общие принципы.............................................. 182 8.1.1. Понимание проблемы..................................... 182 8.1.2. Постепенное усложнение................................. 183 8.1.3. Тестирование и повторение.............................. 183 8.1.4. Упрощение.............................................. 184 8.1.5. Модульность............................................ 184 8.2. Анализ производительности................................... 184 8.2.1. Частота вычислений..................................... 184 8.2.2. Анализ алгоритма....................................... 185 8.2.3. Использование встроенных функций....................... 185 8.2.4. Использование векторов................................. 185 8.2.5. Представление сложных функций в виде текстур........... 186 8.2.6. Анализ информационного журнала......................... 186
Содержание 11 8.3. Отладка шейдера............................................. 186 8.3.1. Анализ выходных данных вершинного шейдера.............. 186 8.3.2. Анализ выходных данных фрагментного шейдера............ 187 8.3.3. Простые геометрические фигуры ..........................187 8.4. Средства разработки шейдеров.................................187 8.4.1. RenderMonkey......................................... 187 8.4.2. Внешний интерфейс компилятора языка шейдеров OpenGL....... 190 8.5. Итоги....................................................... 191 8.6. Ссылки.................................................... 191 Глава 9. Традиционные шейдеры...................................... 193 9.1. Преобразования ..............................................193 9.2. Источники освещения..........................................195 9.2.1. Источники направленного света.......................... 195 9.2.2. Точечные источники..................................... 196 9.2.3. Прожекторы............................................. 197 9.3. Свойства материала и освещение.............................. 199 9.4. Двухстороннее освещение......................................201 9.5. Отсутствие освещения...................................... 201 9.6. Эффект дымки.................................................202 9.7. Формирование текстурных координат............................204 9.8. Произвольное отсечение...................................... 206 9.9. Наложение текстуры...........................................206 9.10. Итоги...................................................... 207 9.11. Ссылки.................................................... 208 Глава 10. Шейдеры с сохранением данных в текстурах........................................................ 209 10.1. Обращение к текстуре из шейдера.............................210 10.2. Простой пример текстурирования..............................211 10.2.1. Настройка приложения...................................212 10.2.2. Вершинный шейдер.......................................213 10.2.3. Фрагментный шейдер................................... 213 10.3. Пример множественной текстуры...............................214 10.3.1. Подготовка приложения................................ 216 10.3.2. Вершинный шейдер.......................................216 10.3.3. Фрагментный шейдер.....................................217 10.4. Пример наложения карты среды................................218 10.4.1. Подготовка приложения .................................219 10.4.2. Вершинный шейдер.......................................219 10.4,3. Фрагментный шейдер.....................................220 10.5. Полиномное отображение текстуры с BRDF-данными..............222 10.5.1. Подготовка приложения................................ 225 10.5.2. Вершинный шейдер.......................................226 10.5.3. Фрагментный шейдер.....................................229 10.6. Итоги.......................................................231 10.7. Ссылки..................................................... 231
12 Содержание Глава 11. Процедурные текстурные шейдеры..............................233 11.1. Повторяющиеся шаблоны...................................... 235 11.1.1. Вершинный шейдер полосок................................235 11.1.2. Фрагментный шейдер полосок..............................236 11.2. Игрушечный шар............................................. 238 11.2.1. Подготовка приложения...................................238 11.2.2. Вершинный шейдер....................................... 239 11.2.3. Фрагментный шейдер..................................... 240 11.3. Сетка ......................................................244 11.4. Бугристая поверхность.......................................245 11.4.1. Подготовка приложения ..................................247 11.4.2. Вершинный шейдер....................................... 249 11.4.3. Фрагментный шейдер .................................... 250 11.4.4. Карты нормали...........................................251 11.5. Итоги ..................................................... 252 11.6. Ссылки..................................................... 252 Глава 12. Шум..................................................... 254 12.1. Определение шума............................................255 12.1.1. 20-шум............................................... 259 12.1.2. Шум большей размерности.................................260 12.1.3. Шум в шейдерах OpenGL...................................260 12.2. Текстуры шума.............................................. 260 12.3. Выбор метода создания шума..................................263 12.4. Простой шейдер шума.........................................264 12.4.1. Настройка приложения................................... 264 12.4.2. Вершинный шейдер........................................265 12.4.3. Фрагментный шейдер..................................... 265 12.5. Завихрения................................................ 266 12.5.1. Шейдер поверхности солнца...............................267 12.5.2. Мрамор............................................... 268 12.6. Гранит......................................................268 12.7. Дерево..................................................... 269 12,7.1. Настройка приложения................................... 269 12.7.2. Фрагментный шейдер..................................... 270 12.8. Итоги ..................................................... 272 12.9. Ссылки.................................................... 272 Глава 13. Анимированные шейдеры ......................................274 13.1. Два режима рисования объекта................................275 13.2. Пределы.................................................... 275 13.3. Преобразования............................................. 275 13.4. Интерполяция ключевых кадров............................... 276 13.4.1. Вершинный шейдер с интерполяцией ключевых кадров....... 277
Содержание 13 13.5. Другие эффекты перехода....................................278 13,6. Системы частиц............................................ 278 13.6.1. Настройка приложения..................................279 13.6.2. Вершинный шейдер хлопушки с конфетти..................282 13.6.3. Фрагментный шейдер хлопушки с конфетти................283 13.6.4. Улучшение шейдеров....................................284 13.7. Колебания................................................. 284 13.8. Итоги .................................................... 288 13.9. Ссылки.....................................................288 Глава 14. Сглаживание процедурных текстур...........................290 14.1. Причины алиасинга..........................................290 14.2. Избежание алиасинга .......................................291 14.3. Увеличение разрешения......................................292 14.4. Пример сглаживания полосок.................................293 14.4.1. Рисование полос...................................... 293 14.4.2. Аналитический отбор.................................. 294 14.4.3. Адаптивный аналитический предварительный отбор........295 14.4.4. Аналитическое интегрирование......................... 298 14.4.5. Фрагментный шейдер кирпичной стенки со сглаживанием...300 14.5. Отбрасывание частот....................................... 301 14.5.1. Фрагментный шейдер шахматной доски со сглаживанием....301 14.6. Итоги ................................................ 302 14.7. Ссылки.................................................... 303 Глава 15. Нефотореалистичные шейдеры................................305 15.1. Штриховка..................................................305 15,1.1. Настройка приложения..................................306 15.1.2. Вершинный шейдер......................................306 15.1.3. Рисование штриховых полосок...........................307 15.1.4. Вычисление толщины штриха.............................308 15.1.5. Освещение........................................... 309 15.1.6. Имитация ручной работы.............................. 310 15.1.7. Фрагментный шейдер штриховки..........................311 15.2. Примеры технических иллюстраций............................312 15.2.1. Настройка приложения..................................314 15.2.2. Вершинный шейдер......................................315 15.2.3. Фрагментный шейдер................................... 315 15.3. Пример Мандельброта........................................316 15.3.1. О множестве Мандельброта..............................316 15.3.2. Вершинный шейдер......................................318 15.3.3. Фрагментный шейдер ...................................319 15.3.4. Множества Джулии......................................321 15.4. Итоги..................................................... 322 15.5. Ссылки.................................................... 322
14 Содержание Глава 16. Шейдеры для обработки изображения............................324 16.1. Геометрические преобразования изображения......................325 16.2. Математические отображения.....................................325 16.3. Операции поиска в таблице.................................... 326 16.4. Преобразования цвета...........................................326 16.5. Интерполяция и экстраполяция изображения.......................327 16.5.1. Яркость.................................................. 328 16.5.2. Контраст................................................. 328 16.5.3. Насыщенность цвета....................................... 329 16.5.4. Резкость................................................. 329 16.6. Режимы плавного перехода...................................... 330 16.6.1. Обычный.................................................. 330 16.6.2. Среднее значение......................................... 331 16.6.3. Растворение.............................................. 331 16.6.4. «Позади»................................................. 331 16.6.5. Очистка ................................................. 331 16.6.6. Затемнение............................................... 331 16.6.7. Осветление............................................... 332 16.6.8. Умножение................................................ 332 16.6.9. Экран.................................................... 332 16.6.10. Усиление цвета...........................................332 16.6.11. Ослабление цвета........................................ 332 16.6.12. Наложение................................................332 16.6.13. Мягкое освещение........................................ 333 16.6.14. Жесткое освещение....................................... 333 16.6.15. Сложение.................................................333 16.6.16. Вычитание............................................... 334 16.6.17. Разность.................................................334 16.6.18. Инверсная разность.......................................334 16.6.19. Исключение...............................................334 16.6.20. Прозрачность............................................ 334 16.7. Свертка........................................................334 16.7.1. Сглаживание...............................................336 16.7.2. Выделение края............................................339 16.7.3. Увеличение резкости.......................................339 16.8. Итоги..........................................................340 16.9. Ссылки........................................................ 341 Глава 17. Сравнение языков программирования.............................343 17.1. Хронология возникновения шейдерных языков..................... 343 17.2. Язык RenderMan................................................ 344 17.3. Язык OpenGL Shader (ISL) ..................................... 346 17.4. Язык HLSL................................................... 348 17.5. Язык Cg..................................................... 350 17.6. Итоги......................................................... 351 17.7. Ссылки........................................................ 352
Содержание 15 Послесловие...................................... ........................354 Приложение А. Грамматика языка............................................356 Приложение Б. Справочник функций API..................................... 363 glAttachObjectARB......................................................363 gIBindAttribLocationARB................................................364 glCompileShaderARB.....................................................366 gJCreateProgramObjectARB...............................................367 glCreateShaderObjectARB............................................... 368 gIDeleteObjectARB......................................................370 gIDetachObjectARB......................................................371 glEnableVertexAttribArrayARB...........................................371 gIGetActiveAttribARB...................................................372 glGetActiveUniformARB..................................................374 glGetAttachedObjectsARB............................................... 376 glGetAttribLocationARB.................................................378 gIGetHandleARB.........................................................379 glGetlnfoLogARB...................................................... 379 glGetObjectParameterARB.............................................. 381 gIGetShaderSourceARB...................................................384 gIGetUniformARB................................................... 385 gIGetUniformLocationARB............................................ 386 glGetVertexAttribARB...................................................387 gIGetVertexAttribPointervARB...........................................389 glUnkProgramARB........................................................390 gIShaderSourceARB......................................................392 glUniformARB.......................................................... 394 glUseProgramObjectARB..................................................398 gIValidateProgramARB...................................................400 glVertexAttribARB......................................................401 glVertexAttribPointerARB...............................................404 Приложение В. Перевод подписей к цветным рисункам.........................................................407 Словарь терминов......................................................... 411 Алфавитный указатель.......................................................423
Отзывы о книге Автор выполнил отличную работу: определил цель — подготовить читателя к раз- работке шейдеров, нашел средства, помогающие ее достичь, и совместил все это. Джеффери Галииовски, менеджер по стратегии программного обеспечения корпорации Intel Книга представляет собой своевременное, исчерпывающее и занимательное введение в язык шейдеров высокого уровня, утвержденный OpenGL Architecture Review Board (ARB). Является ли читатель опытным разработчиком или нович- ком, он почерпнет из книги жемчужины знаний, она станет справочником во вре- мя усердной работы с шейдерным API. Читатель пройдет с этой книгой путь от алгоритмов до API. Боб Куенн, генеральный директор компании Blue Newt Software Компьютерная графика и технологии рендеринга шагнули далеко вперед, и в это время производители графического аппаратного обеспечения принимают на вооружение новый язык — язык шейдеров OpenGL. В этой книге захватываю- щие технологии подробно разобраны таким образом, чтобы быть наиболее понят- ными и полезными разработчикам игр. Энди МакГоверн, основатель компании Virtual Geographies, Inc. Язык шейдеров OpenGL находится в эпицентре революции программируемой графики, а автор книги, Рэнди Рост, был в центре разработки этого нового про- мышленного стандарта. Если хотите разобраться с языком шейдеров OpenGL, чтобь! использовать новые графические эффекты и понять, как работает новое поколение графического аппаратного обеспечения, то эта книга для вас. Нейл Тривитт, старший вице-президент по освоению рынка компании 3Dlabs
Об этой книге Эта книга — поразительное свидетельство того, как эффективно и как быстро продвигается работа по интерактивной работе с изображением. Совсем недавно пользователь не мог создавать и закрашивать объекты, судить об успехах в работе с графикой можно было только по фильмам. Затем появилось несколько исследо- вательских проектов, в результате которых процесс рисования и закрашивания объектов был изменен так, чтобы его можно было выполнять в реальном масшта- бе времени. И наконец, широко известные коммерческие системы, соревнуясь друг с другом, начали поддерживать «шейдинг». В настоящее время есть язык шейде рое для рисования трехмерных объектов в реальном масштабе времени, разрабо- танный сообществом нескольких производителей, поддерживающих OpenGL, н утвержденный официально как расширение OpenGL Architecture Review Board. И эта книга, автор которой — один из лидеров разработки и утверждения языка шейдеров OpenGL, послужит читателю руководством по этому языку и расшире- ниям OpenGL и поможет начать использовать их. Я начал интересоваться процедурным рисованием трехмерных объектов до- вольно необычным образом. В 1990 г. я поступил в аспирантуру университета Северной Каролины, в Чапел-Хилл, так как мне показалось, что университет — хорошее место для людей, интересующихся интерактивной ЗО-графикой. Там я начал работать над проектом Pixel-Planes. Результатом этого проекта стала но- вая графическая машина с несколькими интересными возможностями, связан- ными с производительностью, — она могла выполнять рендеринг большого коли- чества многоугольников за секунду. В частности, одна из возможностей машины очень сильно повлияла па исследования, проводимые мной на протяжении сле- дующих 13 лет. В проекте Pixel-Planes 5 уже применялось много программируе- мых пиксельных процессоров. Программирование этих процессоров было во мно- гом похоже па создание программ для рисования фрагментов наязыке ассемблера, появившихся в мире компьютерной графики в последние несколько лет. Программировать их было интересно, но иногда это совсем изводило. Я был далеко не единственным, кто прилагал большие усилия для написания низкоуров- невого кода, который бы выполнялся над каждым пикселом. Другая группа раз- работчиков проекта Pixel-Planes разработала ассемблер для шейдерного кода, по- зволявший упростить его написание, хотя создать хороший шейдер все равно было трудно. Шейдеры знакомы каждому, гсто видел демонстрационные программы любого из последних графических программных продуктов, и оы не будет удив- лен. обнаружив варианты некоторых из них в этой книге: шейдеры для создания
18 Об этой книге дерева, облаков, кирпичной стены, каменной поверхности, воды с отражением и волнами и, конечно же, набор фракталов Мандельброта. Полученные результаты и трудности, проявившиеся при создании шейдеров Pixel-Planes 5, привели к тому, что некоторые модели и решения были заимство- ваны при.разработке следующей графической машины, PixelFlow. PixelFlow была спроектирована и реализована совместно университетом и компанией, сначала Division, потом Hewlett-Packard. В результате получилась первая интерактивная система, способная работать с процедурными шейдерами, скомпилированными из кода на высокоуровневом языке шейдеров. PixelFlow демонстрировалась на конференции SIGGRAPII в 1997 г. Несколько лет спустя, если бы вам повезло попасть в компанию UNC, вы могли бы писать процедурные шейдеры и запус- кать их в режиме реального времени, в то время как никто другой еще не мог этого делать. И, конечно же, это было единственное место, где их можно было увидеть в действии. Я покинул UNC, перейдя в шейдерный проект, начатый компанией SGI, в на- дежде участвовать в создании языка шейдеров, который поддерживался бы на ком- мерческом уровне и мог использоваться более чем на одном компьютере в одном вычислительном центре. Тем временем в Стэнфорде начались работы над еще од- ним исследовательским проектом языка шейдеров. Его целью было добиться того, чтобы язык шейдеров мог работать на графическом аппаратном обеспечении уров- ня персонального компьютера. Все производители аппаратного обеспечения для персональных компьютеров начали придавать своей продукции низкоуровневые возможности. Очень скоро любой мог написать какой-либо шейдерный код, очень похожий на тот, что вдохновил меня во время работы над Pixel-Planes 5. И неуди- вительно, что повсюду заговорили о необходимости разработки высокоуровнево- го языка для интерактивного рисования. Исследования в области возможностей применения и усовершенствования этих языков продолжились в моей лаборатории в университете Мэриленд, округ Бал- тимор, и во многих других местах. Однако простое существование высокоуровне- вых языков шейдеров реального времени больше не является предметом иссле- дований. Интерактивные шейдерные языки стали доступны всем. Существует множество вариантов для любого, кто захочет разрабатывать приложения с ис- пользованием возможностей современного графического аппаратного обеспече- ния. Основные варианты выбора — Cg, HLSL и язык шейдеров OpenGL. Отличи- тельная особенность последнего — в том, что это единственный язык шейдеров, который тщательно анализировался и оценивался производителями аппаратного обеспечения. Я участвовал в этом процессе вместе с парой дюжин представите- лей множества компаний и университетов. И возвращаясь к книге. Если вы держите ее в руках, скорее всего, вас заинтере- совали идеи, которые привели к созданию языка шейдеров OpenGL: стремление к кроссплатформенности, переносимости между операционными системами, на- дежности и стандартизации языка шейдеров. Хотите узнать, как всего этого до- биться? Начинайте читать. Затем приступайте к программированию! Марк Олат, университет Мэриленд, округ Балтимор, MD Сентябрь 2003 г.
Предисловие Программируемое графическое аппаратное обеспечение существует почти столько же времени, сколько и обычное. Через несколько лет после создания графических акселераторов гибкость стала жизненной необходимостью для их конструкторов. Графические API продолжают совершенствоваться, и из-за того, что разработка нового акселератора от начала до конца может занять несколько лет, единствен- ный способ гарантировать поддержку современного для времени выпуска нового акселератора API — внести некоторую программируемость с самого начала. До последнего времени в области программирования графических акселерато- ров работали немногие, в основном исследователи и разработчики драйверов для видеокарт. Исследования в направлении программируемости графического аппа- ратного обеспечения велись, но целью этих исследований не было создание жизне- способного аппаратного и программного обеспечения для разработчиков приложе- ний и конечных пользователей. Разработчики драйверов сосредоточились на более насущных задачах — поддержке основных графических API того времени: PHIGS, РЕХ, Iris GL, OpenGL, Direct3D и др. До последнего времени ни один из этих API не позволял разработчикам задействовать возможности программируемости ап- паратного обеспечения, так что разработчики приложений были вынуждены ис- пользовать только стандартные возможности традиционных графических API. Производители графического аппаратного обеспечения также не раскрывали возможностей программируемости своих продуктов, так как это требовало дорого- стоящего обучения и обязательной поддержки пользователей, поскольку интер- фейсы были низкоуровневыми и зависели от конкретного устройства, а иногда кардинально менялись с разработкой нового поколения графического аппарат- ного обеспечения. Разработчики приложений, использовавшие такой зависимый интерфейс, были вынуждены изменять свои приложения всякий раз, как только производители выпускали новую версию аппаратного обеспечения. О поддержке аппаратного обеспечения от разных производителей не было и речи. С наступлением XXI в. некоторые из фундаментальных принципов разработ- ки графического аппаратного обеспечения изменились. Разработчики требовали от производителей все новых и новых возможностей, чтобы создавать все более сложные эффекты. В результате современные графические акселераторы стали более программируемыми, чем когда-либо раньше. Стандартное графическое API меняется, чтобы соответствовать новинкам аппаратного обеспечения. Для OpenGL результатом этих процессов стало огромное количество расширений к базовому API, так как производители видеокарт добавляли все новые возможности, кото- рых требовали разработчики программ.
20 Предисловие Сейчас отрасль компьютерной графики на перепутье. Происходит изменение всей системы понятий, что отбрасывает старую фиксированную функциональ- ность и старые графические АРР. значение графического процессора, или VPU (visual processing unit), становится не меньшим, чем значение центрального про- цессора, или CPU (central processing unit). VPU будет оптимизирован для обра- ботки ЗО-графики и видео. Параллельная обработка данных в виде чисел с плава- ющей запятой — первостепенная задача для VPU, и гибкость VPU будет означать, что его можно использовать также для обработки данных, отличающихся от на- бора традиционных команд для работы с графикой. Приложения могут использо- вать преимущества и CPU, и VPU для более оптимального выполнения задачи. В этой книге описывается программируемость графического аппаратного обес- печения, представленная в виде высокоуровневого языка на базе лидирующего кроссплатформенного графического 3D API: OpenGL. Этот язык, называемый языком шейдеров OpenGL, позволяет приложениям полностью контролировать некоторые наиболее важные этапы обработки графики. Теперь нет ограничения па алгоритмы рендеринга и формулы, которые ранее выбирались конструктора- ми видеокарт и фиксировались в кремнии; сейчас разработчики программного обеспечения могут выбирать любые алгоритмы, использовать программируемость для создания уникальных эффектов в режиме реального времени. Предполагаемая аудитория Самая многочисленная категория читателей этой книги — разработчики прило- жений, которые собираются писать собственные шейдеры. Читатели, желающие научиться писать шейдеры на языке шейдеров OpenGL, найдут в этой книге и спра- вочник, и учебник. Некоторые из них будут использовать книгу только как спра- вочник, другие — только как учебник. Надеюсь, что структура этой книги подхо- дит всем, можно читать любые ее части, а не только с начала до конца. Читателям для усвоения материала книги желательно, ио не обязательно знать OpenGL. В книгу включен краткий обзор OpenGL, однако это не учебное пособие и не справочник по OpenGL. Разработчик, пробующий создавать шейдеры, вдоба- вок к этой книге должен иметь и документацию по программированию на OpenGL. Компьютерная графика основана на математике, и знания по алгебре и тригоно- метрии помогут читателям попять и оценить некоторые представленные подроб- ности. С появлением программируемого аппаратного обеспечения основные эта- пы графической обработки стали подконтрольны разработчикам программ. Для успешного создания шейдеров необходимо, чтобы разработчики понимали мате- матические основы компьютерной графики. Содержание книги Книгу можно условно разделить на три части. Главы 1-8 предназначены для оз- накомления читателя с языком шейдеров OpenGL и приемам его использования. В этой части приводятся описания команд OpenGL для создания и управления
Содержание книги 21 шейдерами и подробности языка. Главы 9-16 содержат примеры шейдеров и опи- сания применяемых алгоритмов, с помощью которых читатель ближе знакомится с процессом создания шейдеров. Эта часть книги может использоваться как осно- ва для написания собственных шейдеров и как источник новых идей. И наконец, в главе 17 язык шейдеров OpenGL сравнивается с другими коммерческими шей- дерными языками. Приложения А и Б содержат справочник по языку и список функций, поддерживающих язык шейдеров OpenGL. Главы расположены в порядке, соответствующем потребностям читателя, сла- бо знакомого с OpenGL и шейдерными языками. Некоторые главы опытные раз- работчики могут и пропустить. Зачастую специальные технические книги не дол- жны читаться «от корки до корки», и эта книга — не исключение. □ Глава ! содержит обзор основ OpenGL API. Знакомые с OpenGL читатели мо- гут сразу переходить к следующей главе. □ Глава 2 — введение в язык шейдеров OpenGL и описание функций, которые были добавлены в OpenGL для поддержки языка. Это глава для тех, кто хочет быстро понять, что такое язык шейдеров OpenGL и у кого есть время для про- чтения только одной главы. □ Глава 3 подробно описывает язык шейдеров OpenGL. Материал этой главы си- стематизирован таким образом, чтобы представить язык программирования в подробностях. Эта глава — справочник для тех, у кого уже сложилось пони- мание языка шейдеров OpenGL. □ В главе 4 обсуждается взаимодействие ос татков стандартной функционально- сти с программируемыми частями. Сюда включено описание встроенных пе- ременных языка шейдеров OpenGL. □ В главе 5 описываются встроенные функции, являющиеся частью языка шей- деров OpenGL. Эта глава — справочник для тех, кто уже хорошо понимает язык шейдеров OpenGL. □ Глава 6 представляет простой пример шейдера. Она будет особенно полезна тем, кто усваивает новые знания на примерах лучше, чем в теории. □ В главе 7 описываются функции, добавленные в OpenGL для поддержки со- здания шейдеров и управления ими. Этот материал понадобится разработчи- кам, которые собираются использовать шейдеры в своих приложениях. □ В главе 8 можно найти несколько общих советов по разработке шейдеров и опи- сание процесса разработки, рассматриваются также доступные инструменталь- ные средства для разработки шейдеров. □ Главой 9 начинается последовательность глав, в которых рассматриваются при- меры шейдеров. Здесь представлены шейдеры, повторяющие соответствующие части стандартной функциональности OpenGL. □ Глава 10 представляет шейдеры, которые работают с текстурными картами: со- храняют и считывают данные. □ Глава 11 посвящена шейдерам, построенным по процедурному принципу (эф- фекты вычисляются алгоритмически, а не формируются с помощью данных, прочитанных из текстур).
Т1 Предисловие □ Глава 12 описывает эффект шума и способы его правильного использования. □ Глава 13 содержит примеры шейдеров, которые создают изменяющиеся с те- чением времени эффекты. □ В главе 14 обсуждается проблема неровности края изображения (aliasing) и рас- сматриваются способы написания шейдеров, уменьшающих этот недостаток. □ В главе 15 приводятся примеры шейдеров, которые используются для созда- ния нефотореалистичных эффектов — например, технической иллюстрации, черчения или штриховки. □ Глава 16 представляет несколько шейдеров для изменения изображений, на- рисованных с помощью OpenGL. □ В главе 17 язык шейдеров OpenGL сравнивается с другими известными ком- мерческими шейдерными языками. □ Приложение А содержит четкое описание грамматических правил языка шей- деров OpenGL. □ Приложение Б содержит справочник по функциям OpenGL для работы с язы- ком шейдеров OpenGL. О примерах шейдеров Примеры шейдеров, приведенные в этой книге, в первую очередь являются ма- ленькими программами, демонстрирующими возможности языка OpenGL. Ни один из шейдеров не предполагает самого оптимального способа создания пред- ставленного эффекта. (На самом деле, оптимальные способы еще должны быть определены с использованием моши и гибкости программируемого графического аппаратного обеспечения.) Теоретически возможно улучшить производительность любого шейдера, работающего на любой аппаратной базе. Качество изображения, создаваемого большинством шейдеров, можно улучшить, позаботившись о сгла- живании неровностей изображения. Исходный код этих шейдеров написан таким образом, что (я надеюсь) пред- ставляет собой разумный компромисс между удобочитаемостью кода, переноси- мостью и скоростью выполнения. Читатель может учиться на этих шейдерах, а по- том изменять их нужным образом для использования в собственных проектах. За исключением изображений созданных шейдером игрушечных шариков (раз- работанного ATI Research, Inc. на аппаратном обеспечении от ATI), все иллюст- рации этой книги были сделаны на первом графическом акселераторе, поддер- живающем реализации языка шейдеров OpenGL от 3Dlabs Wildcat VP. В этой реализации — масса ограничений, так что в некоторых случаях шейдер хоть и за- пускается на конкретном оборудовании, но имеет некоторые отличительные осо- бенности, зависящие от аппаратного обеспечения или драйвера. Улучшенные вер- сии таких шейдеров рассматриваются и обсуждаются в этой книге. Я приложил немало усилий для того, чтобы представить «правильные» шейдеры вместо шей- деров с индивидуальными особенностями, возникающими во время работы на разной аппаратной базе, которые создавались для самой первой реализации. Са-
От издателя перевода 23 мне последние, лучшие версии шейдеров доступны на веб-сайте данной книги: http://3dshaders.com. Работа над этой книгой происходила в то время, когда реализации языка шей- деров OpenGL находились на начальном этапе развития. Целью, которую я пре- следовал, выбирая примеры шейдеров, было представить работающие шейдеры, а не шейдеры, которые должны будут работать после того, как реализации нач- нут полностью поддерживать спецификацию языка шейдеров OpenGL. Примеры более сложных шейдеров — первые кандидаты на включение во вторую редак- цию книги, так как они реализуют возможности, на данный момент не доступные ни в одной существующей реализации. Опечатки Я знаю, что эта книга может содержать ошибки, но я пытался свести их количе- ство к минимуму. Пожалуйста, присылайте сообщения об ошибках по адресу: randi@3dshaders.com; список уже обнаруженных ошибок находится на сайте http:/ /3dshaders.com. От издателя перевода Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Цветные рисунки к книге вы найдете по адресу http://www.piter.com/download, Подробную информацию о наших книгах вы найдете на веб-сайте издательства http://www.piter.com.
06 авторе Рэнди Рост является менеджером группы разработчиков графического программ- ного обеспечения в компании 3Dlabs (Форт-Коллинз, Колорадо). Эта группа со- здает реализацию стандарта OpenGL 2.0 и занимается разработкой драйверов OpenGL для графических продуктов компании 3Dlabs. До начала работы в 3Dlabs Рэнди был главой разработки (архитектором) графического программного обес- печения в компании Hewlett-Packard’s Graphic Software Lab и главным архитек- тором графического программного обеспечения в корпорации Kubota Graphics, Рэнди начал свой путь в отрасли компьютерной графики больше 25 лет тому назад и в течение 15 лет принимал участие в создании стандартов для машинной графики . Он занимался разработкой OpenGL с самой первой версии, которая вы- шла в 1992 г. Рэнди — один из нескольких разработчиков, принимавших участие в работе над каждой следующей основной версией OpenGL, включая Open GL 1.5. Он был одним из главных создателей спецификации РЕХ, а также сотрудником комитета по оценке характеристик средств машинной графики (GPC, Graphics Performance Characterization) во время разработки тестов для визуальной срав- нительной оценки таких средств (PLB, Picture-Level Benchmark). В качестве пред- ставителя от 3Dlabs он работал в группе Khronos со времени ее создания в 1999 г. идо выхода спецификации OpenGL 1.0 и в то время возглавлял графический под- комитет этой организации. В 1993 г. он получил премию Национальной ассо- циации по машинной графике (NCGA, National Computer Graphics Association) за продвижение стандартов в области машинной графики. Рэнди организовывал множество консультаций по графике в SIGGRAPH1 или принимал в них участие, а с 1990 г. участвовал в конференциях для разработчи- ков игр. Он давал консультации по языку шейдеров OpenGL на SIGGRAPH-2002 и SIGGRAPH-2003, а также представлял презентации на эту тему на конферен- циях для разработчиков игр в 2002 и 2003 гг. Рэнди получил степень бакалавра компьютерных наук и математики в уни- верситете штата Миннесота, Манкато, в 1981 г. и степень магистра вычислитель- ных наук в университете штата Калифорния, Дэйвис, в 1983 г. Special interest Group in Computer Graphics, специальная группа по компьютерной графике ассоциа- ции вычислительной техники США — Примвч. иерее.
Об авторах разделов, вошедших в книгу Бартольд Лихтенбельт получил степень .магистра электротехники в 1994 г. в уни- верситете Твенти, Голландия. С 1994 г. по 1998 г. он работал над технологиями объемной визуализации1 в компании Hewlett-Packard, сначала в лабораториях в Пало-Альто, Калифорния, а потом в Graphic Software Lab в Форт-Коллинз, Коло- радо. В течение всего этого времени он принимал участие в создании книги «Вве- дение в объемную визуализацию» и написал несколько статей по этой теме. Лих- тенбельт получил четыре патента в области объемной визуализации. В 1998 г. Бартольд присоединился к компании Dynamic Pictures, впоследствии приобре- тенной 3Dlabs. Там он работал с драйверами OpenGL и Direct3D для профессио- нальных графических акселераторов. В последние несколько лет он серьезно ра- ботал над расширением OpenGL API и является автором трех расширений ARB, поддерживающих язык шейдеров OpenGL. Бартольд также возглавлял разработ- ку первых драйверов 3Dlabs, использующих эти расширения. Джон Кэссенич работал в Форт-Коллинз как разработчик программного обес- печения для множества областей, включая приложения для автоматизированно- го проектирования (CAD), ядра операционных систем и трехмерную графику. Он имеет патенты на использование веб-браузера и на архитектуру процессора. Джон специализировался в области математики для компьютерной графики, языков программирования и компиляторов в университете штата Колорадо, где и получил степень бакалавра по прикладной математике в 1985 г. Позже, работая в Hewlett- Packard, он получил степень магистра по прикладной математике (в 1988 г.). Рабо- тая в 3Dlabs, на протяжении 4 лет Джон занимался созданием драйверов OpenGL и в конце концов стал играть одну из главных ролей в разработке языка шейдеров OpenGL. Джон является одним из авторов языка шейдеров OpenGL и в данный момент работает в группе разработчиков оптимизирующего компилятора. Метод формирования изображения, при котором представляется ы. то.чысо общий вид, но и внут- реннее строение трехмерного объекта. — Нрнмеч. перев.
Благодарности Джон Кэссенич из компании 3Dlabs, ведущий разработчик спецификации языка шейдеров OpenGL, является автором главы 3 этой книги. Часть немного обработан- ных материалов спецификации языка шейдеров OpenGL включена в главы 3 -5, а грамматика языка шейдеров OpenGL, написанная Джоном, включена полностью как приложение А. Джон неустанно трудился над спецификацией, обсуждая язык и API, документируя их и принимая решения; вносил обновления в специфика- цию; обучал других участников группы. Джон также разработал несколько шей- деров, включая ранние версии дерева, обсуждаемые в этой книге. Бартольд Лихтенбельт из компании SDlabs был ведущим разработчиком и ре- дактором документации по спецификациям расширений OpenGL, определенных в API языка шейдеров OpenGL. Некоторые материалы этих спецификаций с из- менениями были включены в главу 7. Бартольд неустанно работал над специфи- кациями; обсуждал и документировал вопросы и проблемы, принимал решения; помогал участникам рабочей группы ARB-GL2 прийти к соглашению. Бартольд является соавтором главы 4 этой книги. Инициатива определения высокоуровневого языка для Open GL возникла с по- явлением официального документа «Язык шейдеров OpenGL 2.0», написанного Дэйвом Болдуином (2002) из компании 3Dlabs. Его идеи послужили основой, из которой язык шейдеров OpenGL стал эволюционировать. Этот документ был опубликован почти на год раньше всей прочей информации о завершении и ком- мерческой жизнеспособности высокоуровневых шейдерных языков. Дэйв досто- ин похвалы как человек, который привлек внимание к высокоуровневому шей- дерному языку. Дэйв принимал участие в создании языка и API на протяжении всей работы над ними. Созданный им документ также содержал код разных шей- деров. Этот код послужил основой для нескольких шейдеров, рассматриваемых в этой книге: шейдера кирпичной стены (главы 6 и 14), традиционных шейдеров (глава 9), шейдера шахматной доски со сглаживанием (глава 14) и шейдера Ман- дельброта (глава 15). Стив Корен из компании 3Dlabs отвсчш! за работоспособ- ность шейдера кирпичной стены без сглаживания и шейдера Мандельброта на самом первом реальном аппаратном обеспечении. Берт Фрейденберг из университета Магдебурга разработал алгоритм шейдера штриховки, описанного в главе 15. Он исследовал также некоторые особенности, связанные с аналитическим сглаживанием на программируемом графическом ап- паратном обеспечении. Я включил в книгу несколько диаграмм, предоставлен- ных Бертом, и результаты его исследований (глава 14). Билл Лайси-Кейи из ATI Research разработал шейдер шариков, представленный в главе 11, и познакомил меня с «теорией операций». Шейдер полосок, представленный в главе 11, был со-
Благодарности 27 здан в компании LightWork Design, Ltd. Антонио Тийада из компании 3Dlabs при- думал и разработал шейдер колебаний и шума, представленный в главе 13. Я хотел бы поблагодарить моих коллег из 3Dlabs за помощь в работе над OpenGL 2.0, в частности за помощь в создании этой книги. Особенно много сделали Джон Кэссен ич, Бартольд Лихтенбельт и Стив Корен, разработавшие для языка шейдеров OpenGL- компилятор и компоновщик, а также поддерживавшие объекты в реализации OpenGL от 3Dlabs. Дэйв Хьюлтон и Майк Вайбельн работали над RenderMonkey и создавали шейдеры на этом языке. Тери Моррисон и На Ли разрабатывали и тестировали рас- ширения OpenGL. Филипп Райдаут, Этул, Гупта и еще несколько сотрудников 3Dlabs усердно работали над созданием полнофункционального оптимизирующего компи- лятора для языка шейдеров OpenGL. Их работа позволила создать код и изображе- ния, приведенные в этой книге в качестве примеров. Эта группа, которую я имею честь возглавлять вот уже несколько лет, сейчас занимается созданием доступной специ- фикации и исходного кода для языкашейдеров OpenGL и API языка шейдеров OpenGL. Дейл Киркленд, Джереми Моррис, Фил Хаксли и Антонио Теджада из 3Dlabs участвовали в обсуждении OpenGL 2.0 и очень помогли, подавая хорошие идеи. Антонио также сделал первый парсер для языка шейдеров OpenGL. Другие раз- работчики, входящие в группу разработки драйверов, тоже участвовали в работе над языком. Руководство 3Dlabs помогало идеями и обеспечивало ресурсами. Осо- бенно хочется поблагодарить Османа Кента, Пока Леова, Нейла Треветта, Джер- ри Петерсона и Джона Шимпфа. В обсуждении OpenGL 2.0 принимали участие множество людей. Я хочу поблаго- дарить своих коллеги друзей из ATI, SGI, NVIDIA, Intel, Microsoft, Evans & Suther- land, IBM, Sun Microsystems, Apple, Imagination Technologies, Dell, Compaq, HP, входящих в представительство ARB. В частности, Билл Лайси-Кейн из ATI воз- главил рабочую группу ARB-GL2 с момента ее создания и привел ее к успеху за довольно короткое время. Билл, Эван Харт, Джереми Сендмел, Бенджамин Лип- чак, Гленн Ортнер из ATI тщательно анализировали документы языка шейдеров OpenGL и его API и комментировали их. Стив Гленвилл и Касс Эверитт из NVIDIA помогали проектировать язык шейдеров OpenGL, а Пат Браун (тоже из NVIDIA) принимал участие в разработке API. Огромное спасибо всем разработчикам программ, которые переписывались по элек- тронной почте рели отвечали на анкету, размещенную на сайте http://opengl.org. Так как нашей целью было представить лучшее API для разработчиков графических про- грамм, время, потраченное на изучение этой корреспонденции, поистине бесценно. Особая благодарность создателям языков С, RendcrMan и OpenGL. Возможно, язык шейдеров OpenGL будет следовать традиции успеха и отличного качества. Мне очень помогли читатели черновиков этой книги. Спасибо Марку Олано, Бобу Куенну, Энди МакГоверну, Джеффри Галиновски, Славеку Килановски, Брэду Риттеру, Берту Фрейденбергу, Стиву Каннингему, Майку Вайблену, Бар- тольду Лихтенбельту, Джону Кэссеиичу, Терри Моррисону, Дэйву Хьюлтону, На Ли, Фолкеру Шемелю, Терезе Рости и двум неизвестным читателям. Спасибо моим маленьким дочуркам Рашель и Ханне за то, что они находили время для игр с папой, за улыбки, смех и поцелуи, которые очень помогли мне. Маленький Закари боролся на этапе редактирования книги за мое внимание — и побеждал. И наконец, спасибо Терезе за поддержку в написании этой книги. Это были трудные времена, но ее терпение и сила очень мне помогли. Спасибо за помощь в рождении этой книги.
Обзор OpenGL Эта глава содержит краткий обзор программного интерфейса OpenGL, который не претендует на полноту, а призван лишь познакомить читателя с фундамен- тальными понятиями, необходимыми для усвоения материала последующих глав. Читатель, хорошо знакомый с OpenGL, может сразу перейти к следующей главе. Тем же, кто знаком с другими графическими 3D APT, эта глава поможет освоить OpenGL до такого уровня, чтобы начать программировать на языке шейдеров1 OpenGL. Все описания функциональности OpenGL, если не указан другой источник, взяты из спецификации OpenGL 1.5. 1.1. История OpenGL OpenGL — это промышленный стандарт кроссплатформенного программного интерфейса (API). Его спецификация приняла законченный вид в 1992 г., а первые реализации появились в 2003 г. Интерфейс оказался почти полностью совмести- мым с уже запатентованным программным интерфейсом графической библиоте- ки от Silicon Graphics, Inc. — Iris GL API, который разрабатывался и поддержи- вался этой компанией. Ею же, совместно с другими производителями графического аппаратного обеспечения, был создан общедоступный стандарт, который назвали OpenGL. У OpenGL много общего с его «родственником», Iris GL. Он должен обеспечи- вать доступ к возможностям графического аппаратного обеспечения на самом низ- ком уровне, какого только можно достичь, не теряя независимости от платформы. Изначально OpenGL создавался как интерфейс самого низкого уровня доступа к графическому «железу». Реализация OpenGL доступна на многих платформах — Macintosh, PC, Unix-системах. Она включает поддержку графических аппарат- ных архитектур, начиная с тех, что предоставляют простой доступ к графическо- му буферу, и заканчивая такими, в которых все делается аппаратно. С момента выхода первой версии OpenGL (1.0) в июне 1992 г. стандарт перера- батывался и дополнялся еще пять раз. Каждая переработка сопровождалась внесе- нием новой функциональности в API. Текущая версия спецификации OpenGL — 1.5. Шейдер — это небольшая программа, состоящая из набора элементарных операций, часто применяю- щихся в ЗБ-графикс. Опа загружается в видеокарту и управляет работой самого графического про- цессора. — Ппн.< ч перев.
1.1. История OpenGL 29 Первые реализации, соответствующие стандарту OpenGL 1.0, появились в 1993 г. Подготовка версии 1.1 закончена в 1997 г., внее добавлены две важные функции — обработка массивов вершин (vertex arrays) и обработка текстурных объектов (texture objects). Спецификацию OpenGL 1.2 с поддержкой ЗЭ-текстур и дополнительными функциями обработки изображения опубликовали в 1998 г. Спецификацию OpenGL 1.3 завершили в 2001 г., добавив работу с кубическими текстурами (cube map textures), сжатыми текстурами, множественными текстурами и другие воз- можности. OpenGL 1.4 выпустили в 2002, добавив автоматическое создание тек- стур для объектов различной удаленности (mipmap generation), дополнительные функции сглаживания (Wending), внутренние форматы текстур для хранения глу- бины для использования в вычислении теней, рисование множественных масси- вов вершин одной командой, больший контроль растеризации, контроль сглажи- вания границ с помощью шаблонных текстур (stencil wrapping), а также различные дополнения к текстурированию. Стандарт OpenGL 1.5 был утвержден в июле и опубликован в октябре 2003 г. Он поддерживает буферизацию массивов вершин (vertex buffer objects) для вы- сокопроизводительного вывода геометрии, дополнительные функции сравнения для схем затенения (shadow comparison functions), асинхронную проверку ви- димости объектов (occlusion query), создание многоуровневых текстур с размера- ми, не кратными степени двойки (non-power of two textures) для более эффектив- ного использования текстурной памяти. Выход новой версии стандарта OpenGL (2.0) ожидается в начале 2004 г.1 Спецификация в основном будет описывать возможности программирования основных этапов обработки вершин и фрагмен- тов. С этой версией OpenGL приложения получат способность реализации соб- ственных алгоритмов рендеринга, используя высокоуровневый язык шейдеров2 (Shading Language). Основная часть новой спецификации позволит работать как с вершинными, так и с фрагментными шейдерами. Эта новая возможность программирования в OpenGL — революционное изменение стандарта, отсюда и изменение номера версии с 1.5 на 2.0. Тем не менее изменение «старшего» номера не вызовет потери совместимости с предыдущими версиями OpenGL. OpenGL 2.0 будет полностью совместим с OpenGL 1.5, Приложения, которые запускаются с OpenGL 1.5, смо- гут без изменений запускаться на OpenGL 2.0. Компания Silicon Graphics создала ассоциацию, контролирующую развитие OpenGL в соответствии с набором предписаний, и назвала ее Architecture Review Board (ARB). Главная задача группы - - направлять развитие OpenGL, контро- лируя спецификацию и подтверждающие тесты. Изначально в ARB были пред- ставлены SGI, Intel, Microsoft, Compaq, Digital Equipment Corporation, Evans & Suther- land, IBM. Сейчас в ее состав входят 3Dlabs, Apple, ATI, Dell, Evans & Sutherland, Hewlett-Packard, IBM, Intel, Matrox, NVIDIA, SGI, Sun Microsystems. ’ Версия спецификации OpenGL 2.0 была представлена 10 августа 2004 г. компанией Silicon Graphics и независимым советом OpenGL Architecture Review Board па выставке «SIGGRAPH (Специальная группа по компьютерной графике) 2004», проходившей в Лос-Анджелесе, 2 GLSL (OpenGL Shading Language), иначе называемый glSLang, — ато ионий язык высокого уровня для создания фрагмептпых и верш и и пых шейдеров. — Примеч. перев.
30 Глава 1. Обзор OpenGL 1.2. Развитие OpenGL В различных реализациях OpenGL множество функций доступно в виде рас- ширений (extensions). Механизм расширений хорошо продуман, и фирмы-по- ставщики аппаратного обеспечения могут определять и реализовывать свои рас- ширения, которые позволяют использовать новые аппаратные возможности. Из-за того что дизайн OpenGL фундаментален и фиксирован, единственный способ из- менить OpenGL — определить для него расширения; и это могут сделать только те, кто создает реализацию стандарта. Приложения, использующие реализацию OpenGL, не могут расширять функциональность, дополняя ту, что была предо- ставлена их поставщиком OpenGL. В данный момент существует около 300 раз- личных расширений. Расширения, поддерживаемые только одним поставщиком, можно распознать по короткому префиксу в их именах, уникальному для каж- дого поставщика (например, для расширений, разработанных фирмой Silicon Graphics, Inc., используется префикс SGI). Расширения, поддерживаемые многи- ми поставщиками, обозначены префиксом EXT. Расширения, тщательно проверен- ные комитетом ARB, обозначаются префиксом ARB. Именно им рекомендуется отдавать предпочтение при выборе способа расширения конкретной функциональ- ности. Следующая ступенька после расширения ARB — добавление функциональ- ности прямо в спецификацию OpenGL. Опубликованные спецификации для рас- ширений OpenGLMOJKHo получить по адресу http://oss.sgi.com/projects/ogl-sample/ registry. Чтобы узнать, какие расширения поддерживаются конкретной реализацией OpenGL, нужно передать константу GL_EXTENSIONS в функцию glGetString, которая вернет строку со списком расширений, поддерживаемых данной реализацией. Не- которые поставщики сейчас поддерживают до 100 расширений OpenGL. Быстрое увеличение количества расширений в целом является позитивным фактором для развития OpenGL, хотя и создает некоторые проблемы. Поставщикам аппаратно- го обеспечения легко добавить новое расширение, зато разработчикам приложе- ний приходится сталкиваться с. головокружительным количеством нестандарт- ных возможностей. Как и любой комитет по стандартам, ARB осмотрительно относится к включению расширения в состав стандарта OpenGL. За все время существования стандарта в нем не расширили ни одной основной возможности графического аппаратного обеспечения. Создатели OpenGL, Марк Сигал (Mark Segal) и Курт Экли (Kurt Akeley), так объясняют это: «Одна из при- чин такого решения — производительность. Графическое оборудование обычно спроектировано таким образом, что конкретные операции должны выполняться в определенном порядке; замена этих последовательностей произвольными алго- ритмами обычно недопустима»-. Это утверждение почти соответствовало состоя- нию стандарта на 1994 г. (хотя тогда уже использовались программируемые гра- фические архитектуры). Но сегодня практически все графическое аппаратное обеспечение является программируемым. Быстрое увеличение количества рас- ширений и необходимость поддерживать интерфейс DirectX от Microsoft просто не оставляет производителям другого выбора. (Как станет ясно из последующих глав, предоставление разработчикам доступа к этим возможностям и есть цель языка шейдеров OpenGL.)
1.4. Буфер кадров 31 1.3. Архитектура приложений OpenGL Основная часть функций OpenGL API предоставляет приложению возможности рисования в некоем графическом буфере кадров, но существуют и функции для получения данных из буфера. Интерфейс ориентирован на рисование как трех- мерных геометрических фигур (точек, линий, многоугольников, которые в целом называются примитивами), так и растровых изображений и рисунков. Архитектура приложений OpenGL представлена моделью клиент-сервер. При- ложение (клиент) посылает команды, которые интерпретируются и обрабатыва- ются реализацией OpenGL (сервером). Приложение и OpenGL-сервер могут вы- полняться как на одном компьютере, так и на двух разных. Архитектура OpenGL подразумевает необходимость сохранять некоторые параметры состояния во вре- мя работы приложения. Часть из них хранится в адресном пространстве приложе- ния (клиентские состояния), но большинство в адресном пространстве OpenGL- сервера (серверные состояния). Команды OpenGL всегда обрабатываются в том же порядке, в котором они по- лучены сервером. Иногда завершение их выполнения может задерживаться из-за промежуточных операций, тогда команды OpenGL буферизуются. Внеочередное выполнение команд OpenGL не разрешено. Это означает, например, что следую- щий примитив не будет нарисован до тех пор, пока не будет полностью закончено рисование предыдущего. Такой порядок выполнения команд справедлив также для операций чтения из буфера и запросов состояния. Эти команды возвращают результат, полностью согласованный с выполнением всех предыдущих команд. Связывание данных в OpenGL происходит не тогда, когда функции выполня- ются, а тогда, когда они вызываются. Именно тогда данные интерпретируются и, если это необходимо, копируются в память OpenGL. Дальнейшее изменение этих данных в приложении никак не влияет на данные, которые были сохранены внутри OpenGL. 1.4. Буфер кадров Так как OpenGL — это программный интерфейс для рисования графики, основ- ное его назначение - преобразование переданных приложением данных в нечто видимое на экране, то есть процесс визуализации, который в русскоязычной ли- тературе обычно называют рендерингом (от англ, rendering — визуализация). Как правило, такие преобразования ускоряются специально сконструированной ап- паратурой, но некоторые или даже все операции в OpenGL могут выполняться и программной реализацией, использующей обычный процессор. Для пользова- теля OpenGL не имеет значения, какая часть операций выполняется аппаратно, а какая — программно. Важно лишь, чтобы результаты рендеринга соответство- вали их определению в спецификации OpenGL. Аппаратное обеспечение, пред- назначенное для рисования и работы с содержимым экрана, обычно называется графическим ускорителем. Практически у всех графических ускорителей есть область памяти, выделен- ная для работы с содержимым экрана. Каждый видимый элемент изображения
32 Глава 1. Обзор OpenGL (пиксел) на экране представлен одним или более байтом памяти графического ускорителя. Монохромному дисплею обычно достаточно всего одного байта па- мяти на пиксел для храпения уровня яркости пиксела. Полноцветному же дисп- лею зачастую необходимо по одному байту для красного, зеленого и синего цве- тов, чтобы хранить цвет каждого пиксела. Эта так называемая видимая память считывается (обновляется) определенное количество раз в секунду, чтобы изоб- ражение не мигало. У большинства графических ускорителей ес ть и не отобража- емые на экран области памяти, называемые закадровой памятью. Они использу- ются для хранения данных, которые не должны быть видимы на экране. Доступ к видимой и закадровой памяти OpenGL получает через API оконной системы. Система решает, какие части памяти могут быть доступны OpenGL, п оп- ределяет ее структуру. В каждой среде, в которой поддерживается OpenGL, есть небольшой набор функций, который привязывает OpenGL к этому конкретному окружению. В среде Microsoft Windows такой набор функций называется WGL (произносится «вигл»1), в среде X Window System — GLX, па Macintosh — AGL. В каждом окружении этот набор функций обеспечивает такие возможности, как выделение и освобождение областей видеопамяти, выделение и освобождение па- мяти для графических контекстов, выбор текущего графического контекста, вы- бор области видеопамяти для рисования и синхронизация команд между OpenGL и оконной системой. Графический контекст — это структура данных, которая хра- нит состояние OpenGL. Область видеопамяти, которую OpenGL меняет в процессе рендеринга, назы- вается буфером кадров. В оконной системе термину «буфер кадров» соответству- ет термин «окно». В функциях OpenGL, которые зависят от оконной системы, есть специальные возможности выбора характеристик буфера кадров для окна. От характеристик оконной системы будет зависеть также реакция буфера кадров OpenGL на перекрытие окоп. В безоконной системе буфер кадров OpenGL соот- ветствует целому экрану. Окно, которое поддерживает OpenGL-рсидерииг (то есть буфер кадров), мо- жет состоять из комбинаций таких частей: □ нескольких (до четырех) цветных буферов; □ буфера глубины; □ буфера шаблона; □ накопительного буфера; □ буфера мультиссмплиига. В большинстве графических ускорителей есть и первичный, и вторичный бу- феры, что позволяет им поддерживать двойную буферизацию. Приложение обра- батывает данные во вторичном буфере (закадровом), в то время как первичный буфер отображается на экране. После завершения визуализации буферы меняют- ся местами, так что резул ьтат отображается на экране как первичный буфер, и рен- деринг можно начинать со вторичного буфера. При использовании двойной бу- феризации пользователь никогда не видит сам процесс рисования изображения, 1 Скорее всего, тут игра слов: английское слово wiggle имеет осмысленное значение. — Примеч. переа.
1.4. Буфер кадров 33 а всегда — только законченную картинку. Такая техника используется для того, чтобы анимация была более плавной в интерактивном просмотре’. Стереоизображение формируется с помощью двух цветных буферов: одного для правого глаза, другого — для левого. Двойная буферизация поддерживается с помощью первичного и вторичного буферов. Таким образом, стерсоокно двой- ной буферизации будет иметь четыре цветных буфера; передний левый, передний правый, задний левый, задний правый. Обычное (не стерео) окно с двойной бу- феризацией будет иметь и первичный, и вторичный буферы. Однобуферное окно ограничивается одним первичным буфером. Буфер глубины применяется для удаления невидимых областей трехмерных объектов. Этот буфер храпит глубину объекта для каждого его пиксела. При ри- совании нескольких объектов сравнивается глубина для каждого пиксела, чтобы определить, видим новый объект или скрыт. Буфер шаблона используется для выполнения сложных операций наложения масок. В буфере шаблона можно сохранить любую сложную фигуру, и все после- дующие операции рисования будут проверять его содержимое, чтобы определить, рисовать ли каждый пиксел. Накопительный буфер — это цветной буфер с компонентами более высокой, чем в обычных цветных буферах, точности. Несколько изображений могут накап- ливаться в нем, чтобы получилось сос тавное изображение. Один из вариантов при- менения накопительного буфера — рисование в него нескольких контуров объекта в движении. Каждый пиксел накопительного буфера делится на количество кон- туров, и в результате получается картинка, которая показывает неясные очерта- ния движущихся объектов. Можно использовать похожие техники для имитации эффектов глубины резкости и высококачественного полноэкранного устранения ступенчатости изображения, которое в дальнейшем станем называть сглажива- нием (full-screen antialiasing). В процессе рендеринга происходит дискретизация данных, при этом опре- деляется, какой именно графический примитив влияет на отдельно взятый пиксел. В дальнейшем параметры этого примитива используются для установки парамет- ров пиксела. А буфермультиселпыиига* 1 2 хранит в себе данные в виде нескольких семплов для каждого пиксела. Это позволяет выполнять высококачественное пол- ноэкранное сглаживание3 без перерисовывания сцены. Каждый семпл содержит информацию о цвете, глубине и шаблоне пиксела, а приложение может узнать количество семплов на пиксел. Когда окно имеет буфер мультисемплинга, у пего нет отдельных буферов глубины или шаблона. В процессе рендеринга объектов семплы одного пиксела сочетаются, учитывая некоторые семплы соседних пик- селов, для получения единственного значения цвета. Именно это значение потом Обычно процесс копирования содержимого вторичною буфера синхронизируется с обратным ходом луча ЭЛТ-монитора. Этим достигается плавная смена кадров. — Примеч. перев. 1 Мультисемил ИНГОМ называют одну из технологий устранения ступенчатости изображения, основан- ную на оценке параметров соседних участков изображения, которые в дальнейшем будем называть семплами (от англ, sample - образец). — Примем,чиуч.ред. 1 Здесь и в дальнейшем термином «сглаживание» будем называть устранение ступенчатости изобра- жения. Несмотря на то что нодсглаживаиисм в общем случае подразумевают другую операцию, в лите- ратуре по OpenGL этот термин довольно широко используется в смысле именно устранения сту- пенчатости изображения, вызванного, например, изменением масштаба растра. — Примеч. туч. ред. 2 Зак. 218
34 Глава 1. Обзор OpenGL используется для записи в буфер цвета. Из-за того что буферы мультисемплинга могут содержать много семплов (зачастую 4, 8 или 16) глубины, цвета и шаблона для каждого пиксела, они могут занимать большой объем закадровой видеопамяти. 1.5. Состояние Процесс обновления информации в буфере кадров в OpenGL построен по прин- ципу конечного автомата. Преобразование графических примитивов, растровых и других изображений в пикселы на экране контролируется довольно большим количеством параметров состояния. Изменение одного параметра никак не влияет на другие. В целом параметры состояния определяют, как именно будет прово- диться рендеринг и как примитивы преобразовываются в пикселы на экране. Состояние OpenGL хранится в структуре данных, которая называется графи- ческим контекстом. Часть функций для работы с графическими контекстами предоставляется ЛР1 оконной системы. В число таких функций входит создание и удаление графических контекстов, а также назначение текущего графического контекста и буфера кадров, применительно к которым будут выполняться после- дующие OpenGL-команды. Существует довольно много серверных параметров OpenGL, имеющих всего два состояния: включено или выключено. Для изменения состояния конкретного параметра нужно передать соответствующую константу либо в команду gl Ena bl е, которая установит этот параметр в состояние «включено», либо в команду gl Di sa Ы е, которая установит этот параметр в состояние «выключено». Клиентские парамет- ры (например, указатели для определения массивов вершин) могут быть включе- ны вызовом gl Ena bl eCi 1 entState и выключены вызовом gl Di s a bl eCi 1 entState. Любые параметры состояния OpenGL-сервера можно сохранять в стеке функци- ей gl PushAttrl b и извлекать функцией glPopAttrib. У OpenGL-клиента есть свой стек, с которым можно работать посредством функций gl PushCI 1 entAttrib и gl PopCl i entAttri b. Параметры графического контекста можно получить функцией gl Get. Для про- стых параметров определены символические константы (например, GL_CURRENT_COLOR, GI__L INFWIDTH и т. п.). Эти значения передаются какаргументы в glGet, а возвраща- ются текущие значения указанного параметра графического контекста. Есть не- сколько разных функций glGet, которые возвращают integer, float, double или boolean. Более сложные значения получают с помощью функций с приставкой get, каждая из которых возвращает определенный тип, например gl Get Cl 1 pPI ane, gl GetLi ght, glGetMaterial и т. п. Функция gl Get Error возвращает тип ошибки. 1.6. Конвейер операций В OpenGL определено много операций, которые должны применяться одна за другой, в определенном порядке. Поэтому в стандарте есть правила, которые оп- ределяют последовательность обработки графических данных в OpenGL. В OpenGL, до версии 1.5 включительно, определен набор операций, которые можно назвать базовой функциональностью (рис. 1.1). Так OpenGL работает с вы- хода первой его версии. Это упрощенное представление о том, как он работает
1.7. Рисование геометрических фигур 35 и сейчас. Правда, в новых версиях появились некоторые новые функции, но базовая архитектура сохранилась. Термин «базовая функциональность» здесь использу- ется потому, что любая реализация OpenGL, выполняя операции, должна получать результат, соответствующий стандарту. И набор операций, и последовательность их выполнения определены в стандарте. ------► Вершины ------Фрагменты -----► Текстуры — — ► Группы пикселов Рис. 1.1. Обзор операций в OpenGL От конкретных реализаций Open GL не требуется в точности следовать опреде- ленному стандартом порядку выполнения операций. Единственное, чего нужно добиваться, — соответствия результатов рендеринга в разных реализациях. Из мно- жества новаторских программных и аппаратных архитектур, которые реализуют OpenGL, ни одна не дает результаты, полностью не соответствующие тому, что изображено на рис. 1.1. Несмотря на это, диаграмма может помочь читателю по- нять процесс рендеринга в OpenGL в целом, даже если отдельные реализации ра- ботают немного иначе. 1.7. Рисование геометрических фигур Данные для рисования геометрических фигур — точек, линий, многоугольников (в специальной литературе обычно называются графическими примитивами) — изначально хранятся в памяти приложения (см. рис. 1.1, 1). Эта память может быть либо основной оперативной памятью компьютера, либо видеопамятью графичес- кого ускорителя — все зависит от последних изменений в OpenGL и кэширова- ния данных в его реализации. В любом случае, это память, содержащая данные о геометрических фигурах, которые приложение собирается рисовать. 1.7.1. Определение геометрических фигур OpenGL поддерживает следующие графические примитивы: точки, линии, лома- ные линии, замкнутые ломаные линии, выпуклые многоугольники, треугольники,
36 Глава 1. Обзор OpenGL множества связных треугольников, множества связанных наподобие веера тре- угольников, четырехугольники и множества связных четырехугольников. Существуют три основных способа передачи данных о геометрических фигу- рах в OpenGL. Метод «вершина за раз» использует gl Begin для начала рисования примитива и gl End — для завершения. Между этими вызовами обычно выполня- ются команды, которые устанавливают атрибуты вершин — положение вершин, цвет, нормаль, координаты текстуры, вторичный цвет, видимость граней, коорди- наты дымки, используя функции gl Vertex, gl Col or, gl Normal и glTexCoord. (Суще- ствует много разновидностей этих функций, которые позволяют передавать раз- ные тины данных как по значению, так и по ссылке.) Стандарт OpenGL, включая версию OpenGL 1.5, не предусматривает установку пользовательских данных для вершин. Для каждой вершины можно установить только те параметры, которые определены стандартом. При использовании метода «вершина за раз» вызов gl Vertex означает конец передачи данных для отдельной вершины, он также может являться маркером окончания описания примитива. После вызова gl Begin и указания типа примити- ва OpenGL будет считать графический примитив полностью определенным, если команда gl Vertex была вызвана необходимое количество раз. Например, для оп- ределения треугольника достаточно задать три вершины посредством трех после- довательных вызовов gl Vertex. При определении множества связанных треуголь- ников (triangle strip) первый треугольник задается тремя последовательными вызовами gl Vertex, а каждый последующий вызов gl Vertex определяет дополни- тельный треугольник1. Второй метод рисования примитивов основан на использовании массивов вер- шин. Приложения помещают атрибуты вершин в массивы, запоминают указате- ли на эти массивы и используют gl DrawArrays, glMuitiDrawArrays, glDrawElevents, glMul ti DrawEI ements, gl DrawRangeEl ements или gl Inter! eavedArrays для рисования боль- шого количества примитивов за один раз. Этот метод предпочтительнее, если для приложения критична производительность, — упомянутые функции эффектив- но передают большое количество данных в OpenGL. glBegi n/gl End-методы требу- ют вызова функции для каждого атрибута каждой вершины, так что из-за множе- ственных вызовов функций при рисовании объектов с тысячами вершин время выполнения существенно увеличивается, в то время как использование массивов вершин позволяет обойтись всего одним вызовом. Ко всему прочему предвари- тельная организация данных в массивы позволяет реализациям OpenGL исполь- зовать более эффективные алгоритмы для их обработки. Текущий массив значений цвета можно указать функцией glColorPointer, те- кущий массив координат вершин — функцией gl VertexPoi nter, текущий массив векторов нормали — функцией gl Normal Pointer и т. д. Функция gl Inter! eavedArrays может работать одновременно с несколькими каким-то образом соотносящимися друг с другом массивами (например, каждая вершина должна быть определена тремя числами, представляющими нормаль, и тремя числами, представляющими координаты вершины). После N выловов gIVertcx количество треугольников, входящих н примитив, будет равно N — 2. При атом каждый N-ii вызов glVcitex определяет треугольник со следующими вершинами: для нечет- ных я — с номерами п, п + 1, п + 2; для четных п — с номерами п + 1, п, п + 2. — Примеч. персе.
1.7. Рисование геометрических фигур 37 Предыдущие два метода являются методами непосредственного режима, так как примитивы обрабатываются сразу же, как только приходит информация о них. Используя третий метод, можно сохранить последовательность вызовов функ- ций как для единичных вершин, так и для массивов в дисплейном списке (так на- зывается структура данных, управляемая OpenGL, в которой команды ожидают своего выполнения). Дисплейные списки могут включать в себя и команды для установки состояния, и команды рисования. Они хранятся на сервере и могут быть обработаны позже вызовами glCal'IList или glCallLists. Этот альтернативный способ передачи данных не показан па рис. 1.1, но тоже является частью последо- вательности обработки данных в OpenGL. Определение дисплейного списка на- чинается вызовом glNewList и завершается вызовом gl EndLl st. Все команды, нахо- дящиеся между этими двумя вызовами, сохраняются в дисплейном списке (хотя некоторые из команд OpenGL вообще не могут быть сохранены там). В некото- рых реализациях режим дисплейного списка может дать преимущество в скорос- ти выполнения по сравнению с непосредственным режимом, так как последова- тельность команд в списке может быть оптимизирована под конкретное аппаратное обеспечение, на котором она будет выполнена. При этом команды могут сохра- няться там, где обеспечивается лучшая производительность, иногда даже в памя- ти графического ускорителя. Для того чтобы, применяя этот метод, действитель- но получить преимущество в производительности, нужно использовать созданный дисплейный список более чем один раз, так как при формировании списка и оп- тимизации требуются дополнительные вычисления и перемещения данных. Массив вершин можно сохранять на сервере с помощью новых функций API, по- явившихся в версии OpenGL 1.5. При этом рендеринг будет выполняться быстрее, так как данные могут быть сохранены в память графического ускорителя, в резуль- тате чего не будут каждый раз передаваться через шипу ввода-вывода для рендерин- га. OpenGL API позволяет также эффективно передавать данные с клиента на сер- вер посредством специальных буферных объектов. Буферный объект создается командой glBindBuffer и заполняется командами gl BufferData и gl BufferSubData. Для заполне- ния буфера также можно использовать команду gl MapBuffer, которая отображает буфер в адресное пространство клиента и возвращает указатель па клиентскую область памяти. (Этот указатель можно использовать для непосредственного досту- пах содержимому буфера.) Перед рендерингом обязательно нужно вызвать gl Unmap - Buffer. Команда glBindBuffer используется также для привязки состояния к теку- щему буферу. Например, если текущим буфером является «нулевой» буфер1, то команды glCol orPointer, gl Normal Pointer, gl VertexPoi nter и др. интерпретируют пе- реданный им указатель как указатель на память на стороне клиента. Если был связан какой-либо буфер (ненулевое значение), переданный указатель интерпре- тируется как смещение внутри текущего буфера. Поэтому последовательные вы- зовы одной из команд рисования из массивов вершин (например, gl Mui tiDrawArrays) могут получать данные либо с клиентской, либо с серверной памяти, л ибо из обеих. В OpenGL можно обрабатывать кривые и поверхности с помощью вычислите- лей. Вычислители используют полиномиальное отображение для определения «Нулевой» буфер является активным по умолчанию и. фактически, предоставляет доступ ко всей клиентской памяти, в то время как остальные буферные объекты размещаются в серверной памя- ти. — Примеч. парен.
38 Глава 1. Обзор OpenGL параметров вершины: цвета, нормали и координат, — которые точно таким же об- разом интерпретируются в дальнейшей обработке, как будто они были заданы обычным способом. За подробностями об этой функциональности следует обра- титься к спецификации OpenGL. 1.7.2. Операции обработки вершин Конечный результат использования любого из рассмотренных методов рисова- ния примитивов — передача данных о примитивах на первый этап их обработки в OpenGL, обработку вершин (см. рис. 1.1, 2). Здесь координаты вершин преобра- зуются по видовым матрицам и матрицам проекции, нормали преобразуются об- ратным транспонированием левой верхней части матрицы модели-вида разме- ром 3x3, текстурные координаты преобразуются с помощью текстурных матриц, накладываются вычисления освещения, из-за чего меняется основной цвет; тек- стурные координаты могут быть вычислены автоматически, наложены свойства материала и вычислены размеры точек. Все это строго определено в стандарте. Операции выполняются в определенном порядке по определенным формулам, контролируемым определенными параметрами состояния OpenGL. Иногда этап обработки вершин называют преобразование и освещение, или T&L (Transformation and Lighting), так как в данный момент названные операции са- мые значимые. Приложение может контролировать этот этап только через изме- нение параметров состояния OpenGL (напргтмер, включить или выключить осве- щение функциями gl Enable и gl Di sable, изменить атрибуты функциями gl Light и gl LightModel, изменить свойства материала функцией glMaterial, изменить ви- довую матрицу, вызывая функции glMatrixMode, gl LoadMatrlx, glMijltMatrix, gl Rotate, gl Seal e, gl Trans 1 ate и др.). На этом этапе обработки каждая вершина рассматрива- ется отдельно. Над вычисленными на этапе преобразования координатами вершин затем вы- полняется отсечение (см. раздел 1.9). Эффекты освещения в OpenGL можно контролировать, манипулируя парамет- рами одного или нескольких источников освещения, определенными в OpenGL. Для конкретной реализации OpenGL есть ограничение на количество источни- ков света (GL_MAX_LIGHTS). Минимальное значение, заданное стандартом, — 8. По- лучить поддерживаемое количество источников света можно функцией glGet. Каждый источник освещения в OpenGL можно настроить либо как бесконечно удаленный источник направленного света, либо световую точку, либо прожектор1. Здесь можно настраивать цвет излучаемого света (рассеянный свет, матовое отра- жение, зеркальное отражение, выраженные в значениях интенсивности RGBA2); положение источника освещения; факторы затухания, определяющие, как интен- сивность освещения зависит от расстояния до источника; направление, порядок и коэффициент затухания для точечных источников. 1 Световая точка направляет свет одинаковой интенсивности во все стороны. Источник направленно- го света излучает свет только в одну определенную сторону. Прожектор отличается от источника на- правленного света тем, что излучает сиет в виде конуса. — Приме». перев. - RGBA (Red, Green, Blue, Alpha) — четырехкомпонентпая цветовая модель, в которой определение цвета состоит из интенсивностей красного, зеленого, синего цветов и степени прозрачности. — При- меч. перев.
1.7. Рисование геометрических фигур 39 Все эти параметры для каждого источника света можно изменить функцией glLight. Отдельные источники можно включить или выключить функциями gl ЕпаЫ е и gl Di sаЫ е, задавая символическую константу, указывающую источник света. Ос- вещение дает первичный и вторичный цвета для каждой вершины. Его можно в целом включить или выключить, вызвав соответственно gl Enable или gl Disabl е с параметром GL_LIGHTING. Если освещение выключено, значения первичного и вто- ричного цветов определяются по последним значениям, установленным коман- дами gl Col or и gl Secondarycolor. В дальнейшем, чтобы определить цвет конкретной освещенной вершины, эф- фект от включенных источников освещения накладывается на свойства материа- ла поверхности. У материала есть следующие характеристики: излучаемый свет; уровень рассеянного, матового и зеркального отражений; яркость. Свойства ма- териала можно определять отдельно для передней (front-facing) и для задней (back- facing) поверхностей вызовами функции glMaterial. Общие параметры освещения контролируются функцией glLightModel. Мож- но устанавливать значение общего рассеянного освещения для всей сцены, распо- ложение зрителя (либо локально, либо в бесконечности — это влияет на вычисле- ние углов зеркального отражения), задавать вычисление одно- или двухстороннего освещения для многоугольников (при вычислениях одностороннего освещения используются свойства материала только передней грани, двухстороннего — свой- ства обеих граней, нормали при этом инвертируются) и вычисление отражения отдельно каждого цвета. Значения отражений цветов учитываются в дальнейшем на этапе текстурирования. 1.7.3. Сборка примитивов После того как определены все параметры каждой конкретной вершины, эти дан- ные используются на этапе, называемом сборкой примитивов (см. рис. 1.1,3). Здесь происходит формирование примитивов: точки из одной вершины, линии из двух вершин, треугольника из трех вершин, четырехугольника из четырех вершин и многоугольника из произвольного количества вершин. Для API-метода «вер- шина за раз» тип примитива передается в функцию gl Begin, а для метода массивов вершин — в функцию, принимающую массив вершин. Уже на следующем этапе операции выполняются над наборами точек и зависят от типа примитива — например, отсечение работает по-разному для точек, линий и мно- гоугольников. Поэтому сборка примитивов — необходимый предварительный этап. 1.7.4. Обработка примитивов Следующий этап (см. рис. 1.1,4) па самом деле состоит из нескольких шагов, для простоты объединенных на диаграмме в один блок. Первый шаг — отсечение, опе- рация, которая сравнивает каждый примитив с плоскостями отсечения, заданны- ми вызовами функции gl Cl i рР1 апе, и с отображаемым объемам из видовой матрицы и матрицы проекции. Если примитив полностью находится внутри отображаемо- го объема и плоскостей отсечения, он передается для последующей обработки. Если примитив полностью снаружи, он отбрасывается. Но если примитив час- тично внутри, а частично — снаружи, его разделяют (отсекают) таким образом, что для последущей обработки передается только внутренняя часть.
40 Глава 1. Обзор OpenGL Следующим шагом этого этапа является расчет перспективы. Если данный вид примитива — с перспективой, то координаты х, укг каждой вершины делятся на однородную координату да. В соответствии с этим каждая вершина будет транс- формирована для области отображения на экране, определяемой функциями gl DepthRange и gl Vi ewport., путем генерирования координат внутри окна (области отображения). На шаге отбраковки проверяется, является ли каждый многоугольный прими- тив лицевым (расположен на переднем плане) для текущей точки обзора. Отбра- ковка включается функцией gl Enable, a gl Cull Face определяет, какие примитивы переднего и заднего плана можно игнорировать (отбраковывать). 1.7.5. Растеризация Графические примитивы, обрабатываемые OpenGL, определяются набором дан- ных для каждой вершины примитива. На следующем этапе (см. рис. 1.1, 5) при- митивы (точки, линии, многоугольники) разбиваются на меньшие части в соот- ветствии с расположением пикселов в буфере кадров. Этот процесс называется растеризацией. Каждый из элементов примитива, полученный после растериза- ции, называется фрагментом. Например, если нарисованная на экране линия в ко- нечном итоге займет 5 пикселов, при растеризации она, изначально заданная дву- мя вершинами, будет разделена на пять фрагментов. Фрагмент определяется координатами окна, глубиной, цветом, координатами текстуры и другими пара- метрами. Значения каждого из атрибутов вычисляются с помощью интерполя- ции значений из вершин примитива. В процессе растеризации у каждой вершины определяют первичный и вторичный цвета. Функцией glShadeModel можно уста- новить, будут ли значения цвета для пиксела интерполироваться из значений в вер- шинах (плавное затенение) или для целого примитива будет использоваться зна- чение цвета последней вершины (постоянное затенение)'. Для каждого типа примитивов есть свои правила растеризации и разные со- стояния OpenGL. У точек есть ширина, контролируемая glPointSize и другими параметрами, определяемыми вызовом функции gl PointParemeter. У линий также есть ширина, устанавливаемая gl LineWidth, и шаблон пунктира, устанавливае- мый gl Li neSt 1 ppi е. У многоугольников тоже есть шаблон пунктира, но он уста- навливается другой функцией - gl PolygonStipple. Многоугольники могут быть нарисованы по-разному: заполненные, только контуры, только точки вершин. Это контролируется функцией gl PolygonMode. Также есть возможность функцией gl PolygonOffset установить для каждого фрагмента многоугольника глубину, от- личающуюся от вычисленной ранее. Размещение многоугольников на переднем плане принудительно устанавливается функцией glFrontFace. Антиалиасинг — это сглаживание острых углов примитива. Он включается вызовом функции gl Enable с соответствующей константой (GL_POINT_SMOOTH, GL_LINE_SMOOTH или GL_POLYGON_SMOOTH). Плавное затенение, или затенение методом Гуро, — один из самых популярных алгоритмов затене- ния, обеспечивающий прорисовку главных теней вокруг изображаемого объекта, При использовании постоянного затенения поверхность получается более низкого качества и выглядит разделенной на блоки, но зато вычисления выполняются намного быстрее. — Примеч. перев.
1.7. Рисование геометрических фигур 41 1.7.6. Предварительная обработка фрагментов После растеризации над фрагментами выполняется еще ряд операций, в целом они называются обработка фрагментов (см. рис. 1.1, 6). Самая важная из них — это текстурирование. По координатам текстуры, вычисленным для данного фрагмента на предыдущем этапе, OpenGL обращается к текстурной памяти (см. рис. 1.1, 7). В OpenGL определено много состояний, которые влияют па текстурирование и оп- ределяют способ доступа к текстурам и их наложение на текущий фрагмент. Для реализации этого этапа существует множество расширений, довольно сложных для начинающих. Читатель узнает больше о текстурировании в разделе 1.10. Паэтом этапе выполняются и другие операции над фрагментами. Дымка может изменять цвет фрагмента в зависимости от его расстояния от точки обзора. Сложе- ние цветов получается сочетанием первичного и вторичного цветов фрагмента. Параметры дымки устанавливаются функцией gl Fog, а вторичный цвет передается как атрибут вершины в функцию glSecondaryColor или вычисляется на этапе освещения. 1.7.7. Операции над фрагментами После предварительной обработки над фрагментами выполняется еще ряд простых операций, называемых фрагментными операциями. К ним относятся проверка на видимость, отсечение по прямоугольнику (прямоугольник устанавливается функ- цией gl Scissor), проверка прозрачности (используется значение прозрачности фрагмента и правило, установленное функцией gl Al phaFunc), отсечение по шабло- ну (для сравнения шаблона и фрагмента существуют функции glStencl 1 Func и glStencl 10р), проверка глубины (функция glDepthFunc сравнивает глубину обра- батываемого фрагмента с глубиной из кадрового буфера), смешение (окончатель- ный цвет для записи в кадровый буфер определяется как сочетание цвета фраг- мента, базового цвета из кадрового бугфера с использованием правил смешивания, устанавливаемых функциями gl Bl endFunc, gl Bl endCol or и gl Bl endEquati on), сглажи- вание и логические операции, (наложение окончательного значения фрагмента на значение из кадрового буфера с. использованием логической операции, установ- ленной функцией gl LoglcOp). Все эти операции не зависят от других и могут быть эффективно и дешево выполнены с использованием аппаратного обеспечения. Некоторые из них под- разумевают обращение к буферу кадров за значениями цвета, глубины или шаб- лона. Несмотря на вызванное этим замедление работы, на современных графичес- ких ускорителях все операции последнего этапа визуализации могут выполняться со скоростью миллион пикселов в секунду. 1.7.8. Операции с буфером кадров Операции с буфером кадров контролируют буфер в целом (см. рис. 1.1, 9). По- средством выбора состояний OpenGL можно влиять на ту часть буфера кадров, в которую будут нарисованы примитивы. Буфер назначения рендеринга обычно надо выбирать, так как OpenGL работает и со стереоизображениями, и с двойной буферизацией. Части буфера кадров также называются буферами: первичный, вто- ричный, левый, правый, первичный левый, первичный правый, вторичный левый,
42 Глава 1. Обзор OpenGL вторичный правый и первично-вторичный. Рендеринг можно перенаправить в лю- бой из этих буферов с помощью функции glDrawBuffer. Некоторые части внутри буфера или буферов можно защищать от записи. Режим записи или чтения различных компонентов буфера назначения — красно- го, зеленого, синего, уровня прозрачности — устанавливается функцией gl Col orMask. Разрешить или запретить запись глубины позволяет функция gl DepthMask. Изме- нять режим записи в буфер шаблона можно функцией glStencil Mask. Можно так- же очистить (инициализировать заново) все основные значения в буфере кадров, вызвав функцию gl Cl ear. Отдельные компоненты — цвет, глубина, шаблон, буфер накопления — инициализируются соответствующими функциями: glClearColor, glClearDepth, gl ClearStencl 1 и glClearAccum. Затем функцией glAccumустанавлива- ется операция накопительного буфера. Из соображений улучшения производительности реализации OpenGL прибега- ют к различным способам буферизации для того, чтобы отправлять на аппаратную обработку группы графических примитивов большего размера. Чтобы ускорить завершение рисования графических примитивов для конкретного контекста рен- деринга, приложение должно вызвать функцию glFlush. Чтобы удостовериться в том, что рендеринг завершен для всех примитивов контекста, приложение вы- зывает функцию gl Ft m st. Эта команда блокирует дальнейшую работу и ожидает окончания выполнения всех предыдущих команд. При этом страдает скорость работы в целом, так что использовать эти функции необходимо осторожно. Выполнение всех описанных этапов приводит к тому, что графические прими- тивы, определенные приложением, оказываются сконвертированными в пикселы в буфере кадров для последующего отображения на экране. Но мы обсудили только графические примитивы — точки, линии, многоугольники. В OpenGL есть воз- можность выполнять также рендеринг растровых и других изображений. 1.8. Рисование изображений В дополнение к рисованию объектов ЗО-геометрии OpenGL поддерживает рисо- вание изображений. В терминах OpenGL изображения называются пиксельными прямоугольниками. С самого начала определение пиксельного прямоугольника хранится в памяти приложения (см. рис. 1,1, ?/). Монохромные пиксельные пря- моугольники заносятся в буфер кадров функцией gl Drawpixel s, а растровые изоб- ражения — функцией gl Bitmap. Изображения для текстурной памяти задаются функциями glTeximage и glTexSublmage. Каждая из этих функций выполняет базо- вую обработку изображения вплоть до отдельных его точек. 1.8.1. Распаковка пикселов В OpenGL данные изображений можно передавать во множестве форматов. Па- раметры, определяющие способ хранения изображений в памяти, задаются функ- цией gl Pi xel Store (длина каждой строки пикселов, пропускаемое количество строк перед первой, пропускаемое количество пикселов в каждом ряду). Для большей точности выполняемых над пикселами изображения операций все пикселы пре- образуются в особую последовательность с помощью операции распаковка пиксе-
1.8. Рисование изображений 43 лов (см, рис. 1.1, 12). После того как пикселы переданы вызовом функции типа glDrawPixel s, выполняется распаковка пикселов, при этом применяются парамет- ры распаковки, обеспечивающие правильное прочтение и интерпретацию данных. Каждый полученный пиксел присоединяется к группе пикселов, которая содер- жит либо цвет, либо глубину, либо маску. Группа пикселов цвета направляется в буфер цвета. Группа пикселов глубины направляется в буфер глубины. Группа пикселов шаблона направляется в буфер шаблона. Значения цвета хранятся в схе- ме RGBA и определяются из исходного изображения, следуя набору определен- ных OpenGL правил. В результате получается поток значений RGBA, которые отправляются на дальнейшую обработку. 1.8.2. Перемещение пикселов После распаковки пикселов пиксельные прямоугольники подвергаются серии операций, называемых перемещением пикселов (см. рис. 1.1, 13). Эти операции применяются в разных направлениях: как при передаче данных из приложения в OpenGL (glDrawPixel s, glTeximage, glTexSublmage), так и при копировании данных внутри OpenGL (gl Copy Р1 хе 1 s, gl CopyTexImage, gl CopyTexSubImage) или передаче дан- ных обратно в приложение (gl ReadPI xel s). Перемещение пикселов можно настраивать функцией gl Pi xelTransfer. При этом устанавливаются параметры, определяющие, каким образом значения красного, зеленого, синего цветов, прозрачности и глубины будут масштабированы и сдви- нуты, Также применяется специальная таблица преобразования для отображе- ния одних параметров на другие (цвета или шаблона). Таблица преобразования задается командой gl Pixel Map. Некоторые дополнительные операции, проводимые на этом этапе, являются необязательной частью формирования изображения, Производители графических ускорителей, считающие эти операции важными, по желанию могут включать на- бор дополнительных операций над изображением (imaging subset) в собственные реализации OpenGL, но другие производители могут и ие поддерживать эти опера- ции. Приложение может определить доступность дополнительной операции с по- мощью функции gl Get St ri ng, передавая ей параметр GL_EXTENSIONS. Функция вернет список расширений, поддерживаемых данной реализацией. Приложение должно проверить вхождение подстроки «ARB_i magi ng» в возвращенную функцией строку. В набор дополнительных операций перемещения пикселов входят искривле- ние, матрица линейного преобразования, гистограмма, вычисление минимума и максимума, дополнительные таблицы преобразования цвета. Все это может быть использовано для более полной обработки изображений и операций корректиро- вания цвета. 1.8.3. Растеризация и окончательная обработка После перемещения пикселов фрагменты проходят растеризацию пиксельных прямоугольников примерно таким же способом, как они были сгенерированы из ЗП-примитивов (см. рис. 1.1, 14). На этом этапе определяется, как изображение будет размещено в буфере кадров. Растеризация учитывает текущее положе- ние растра (которое можно установить функциями glRasterPos или glW1 ndowPos)
44 Глава 1. Обзор OpenGL и текущий масштаб (устанавливаемый gl Pixel Zoom и влияющий на увеличение или уменьшение рисуемой картинки). После растеризации фрагменты из пиксельных прямоугольников подвергаются тем же самым операциям, что и графические примитивы (см. рис. 1.1, б), и потом проходят тот же самый путь, что и графические примитивы, пока пикселы не бу- дут окончательно размещены в буфере кадров (см. рис. 1.1,5-10). Если значения пикселов задаются функциями gl Teximage или gl TexSub Lmage, они не проходят растеризацию и дальнейшую обработку, а непосредственно записы- ваются в указанную часть текстурной памяти (см. рис. 1.1, 15). 1.8.4. Проверка чтения Функция gl ReadPixel s читает прямоугольные области из буфера кадров и возвраща- ет их приложению. Чтобы просто скопировать их внутри буфера кадров из одной области в другую, используется функция gl Copy Pixel s. Для копирования из буфера кадров в текстурную память применяются функции glCopyTexImage и glCopyTex- Sublmage. Во всех перечисленных случаях та часть кадрового буфера, что будет прочитана, контролируется этапом проверка чтения OpenGL и устанавливается командой gl ReadBuffer (см. рис. 1.1, 16). Прочитанные из буфера значения могут проходить этап перемещения пиксе- лов и подвергаться различным операциям обработки изображений. ] Полученные после обработки пикселы в процессе копирования записываются в текстурную память или обратно в буфер кадров в зависимости от использован- ной команды. Если же выполняется операция чтения, пиксел ы проходят еще и этап упаковки пикселов (см, рис. 1.1, 17). Это операция, противоположная распаковке пикселов (см. рис. 1.1, 12). Существует несколько параметров, указывающих, как данные изображения будут храниться в памяти (длина каждой строки пикселов, количество пропущенных строк сверху, количество пропущенных пикселов сле- ва). Эти параметры задаются функцией gl Pixel Store. Все это дает приложению возможность выбрать, в каком формате OpenGL вернет ему данные изображения, прочитанные из буфера кадров. 1.9. Преобразования координат Цель всего процесса обработки изображений в OpenGL — преобразовать трехмер- ные описания объектов в двухмерную картинку, которую можно показать па эк- ране. Во многом этот процесс похож па фотографирование, где для преобразова- ния образа реального трехмерного мира в двухмерный отпечаток используется фотоаппарат. Чтобы завершить преобразование из трех измерений в два, OpenGL определяет несколько пространств координат и правила преобразований между этими координатами. Каждое пространство координат имеет свойства, полезные для какой-либо части процесса визуализации. Преобразования, определенные в OpenGL, дают приложениям большую гибкость в отображении трехмерного снимка на двухмерную плоскость. Очень важно понимать, как все эти простран- ства координат и преобразования используются OpenGL, — это поможет впо- следствии успешно писать программы-шейдеры па языке шейдеров OpenGL,
1.9. Преобразования координат 45 Моделирование в компьютерной графике — это определение численного пред- ставления сложного объекта для рендеринга. В понятиях OpenGL это обычно оз- начает представление объекта большим количеством многоугольников — прими- тивов, встроенных в OpenGL. Это представление в минимальном виде включает в себя координаты каждой вершины каждого многоугольника и информацию о том, как соединить эти вершины. Дополнительными данными могут быть цвет каждой вершины, нормаль поверхности в каждой вершине, несколько текстур- ных координат в каждой вершине и т. п. В прошлом моделирование какого-либо сложного объекта требовало боль- ших усилий, точного измерения и ввода данных. (Вот почему чайник Юты, смо- делированный Мартином Ньюэллом (Martin Newell) в 1975 г., использовался в большом количестве графических изображений и получил широкую известность. Это интересный объект, численные данные для которого легко было получить. Несколько шейдеров, приведенных в книге в качестве примеров, иллюстриро- ваны этим объектом; см., например, цветной рис. 201.) В последнее время до- ступными стали новые инструменты для моделирования, как программные, так и аппаратные, что значительно облегчило создание трехмерных моделей для рен- деринга. Атрибуты трехмерных объектов, такие как координаты вершин и нормали поверхности, определяются в предметном пространстве. Это координатное про- странство удобно для описания моделируемого объекта. Координаты указывают- ся в тех единицах, которые удобно использовать для конкретного объекта. Мик- роскопические объекты могут быть смоделированы в ангстремах, повседневные объекты могут быть смоделированы в дюймах или сантиметрах, планеты — в ми- лях или километрах, галактики в световых годах или парсеках. Начало коорди- нат в этой координатной системе (то есть точка с координатами (0, 0, 0)) тоже удобна для моделирования. Для некоторых объектов начало координат удобно помещать в угол трехмерного ограничивающего параллелепипеда, для других — в центр тяжести объекта. Из-за тесной связи с задачей моделирования коорди- натное пространство иногда называют пространством моделей или модельной си- стемой координат. Координаты также могут называться как координатами объек- та, так и модельными координатами. Чтобы скомпоновать сцену из нескольких трехмерных объектов, каждый из которых может быть определен в собственном предметном пространстве, необхо- дима общая система координат. Такая система называется глобальным простран- ством или глобальной системой координат., и в ней определяется соотношение всех объектов сцены. В глобальном пространстве определяются пространствен- ные отношения между объектами, источники света, точка обзора. Единицы изме- рения в этом пространстве выбираются такие, чтобы были удобными для целой сцены. Если, к примеру, моделируется комната в доме, единицами будут футы или метры, но если конструируется силуэт города, в качестве единицы измерения пространства можно выбирать и городские кварталы. Начало координат также можно расположить в произвольном месте. Для сцены определяется трехмерный ограничивающий параллелепипед, начало координат устанавливается в углу, так 1 Цветные рисунки к книге: hLtp://www.pitcr.coin/downIoad,
46 Глава 1. Обзор OpenGL что все остальные его координаты положительны. Можно сделать и по-другому — например, установить начало координат в важную точку сцены (в угол строения, в место расположения ключевого персонажа и т. д.). После определения глобального пространства все объекты, участвующие в сце- не, должны быть трансформированы из их предметного пространства в глобаль- ную систему координат. Модельное преобразование Приводит координаты предметного пространства объекта к координатам глобального пространства. Если, допустим, модельные ко- ординаты определены в футах, а глобальное пространство в дюймах, для преоб- разования будет использован множитель 12. Если объект в сцене должен быть повернут другой стороной, к координатам объекта применяется вращение. Также к объектам обычно применяется смещение. Все эти индивидуальные преобразо- вания могут сочетаться в простой матрице, модельной матрице преобразования, которая полностью преобразует координаты объекта к глобальному пространству координат. После компоновки объектов в сцену необходимо указать параметры вида — точку обзора (положение камеры или глаза), точку фокуса (то есть направление взгля- да) и направление верха (направление, в котором камера смотрела бы снизу вверх). Параметры обзора, сочетаясь, определяют путь трансформации изображения при изменении точки обзора и могут быть объединены в матрицу визуализации. С помощью этой матрицы координаты могут преобразовываться из глобального пространства в пространство обзора. Начало координат в этой координатной си- стеме находится в точке обзора (положение камеры). Пространственные отноше- ния сцены не меняются, зато легко определять расстояние от точки обзора до раз- личных объектов сцены. Почти все графические 3D API позволяют приложениям определять отдельно матрицу преобразования и матрицу визуализации. В OpenGL они объединены в единую матрицу, называемую модели-вида, которая преобразует координаты из предметного пространства сразу в пространство об- зора (рис. 1.2). Существует множество матриц, которыми можно оперировать в OpenGL. Что- бы выбрать либо модельно-видовую, либо другую OpenGL-матрицу, можно вы- звать функцию glMatrixMode. Можно установить единичную матрицу в качестве текущей функцией gl LoadIdent 1 ty или заменить любой другой произвольной ма- трицей с помощью функции gl LoadMatriх (при этом стоит убедиться в необходи- мости этого, так как в результате преобразований есть вероятность получить ма- лопонятное изображение). Умножением можно совместить текущую матрицу с произвольной с помощью функции glMultMatrix. Обычно приложения сначала устанавливают матрицу вида в качестве текущей, а потом накладывают на нее прочие нужные матрицы моделирования. Матрица может быть установлена для разумных преобразований вида функцией gl uLookAt (она не является частью OpenGL, но входит во вспомогательную библиотечку, поставляемую со всеми реализациями OpenGL). Матрицы модели-вида в OpenGL обычно хранятся в стеке, и самую верхнюю матрицу можно скопировать на верх стека вызовом функции glPushMatnx. После этого все преобразования могут быть наложены на самую верхнюю матрицу функциями gl Scale, glTranslate и glRotate, чтобы определить преобразования для каждого трехмерного объекта в сцене. По- ложение источников освещения задается функцией glLight, трансформируются
1.9. Преобразования координат 47 они текущей матрицей модели-вида, так что координаты источников освещения также хранятся как координаты обзора. Матрица модели-вида должна быть пол- ностью сформирована перед преобразованиями координат источников освеще- ния, иначе эффекты освещения будут полностью нарушены. вершины Предметное пространство Матрица модели-вида Пространство обзора Матрица проекции Пространство отсечения Перспектива Нормализованное пространство приборных координат Диапазон масштаба \ и смещения отображения/глубины ) Оконное 1 г пространство Оконные координаты Рис. 1.2. Пространства координат и преобразования в OpenGL Вычисления освещения в OpenGL происходят над вершинами в системе коор- динат обзора. Чтобы правильно вычислить отражение, положения источников ос- вещения и нормали, поверхности нужно расположить в одной и той же системе координат. Часто в реализациях OpenGL это выполняется в системе координат обзора, поэтому заданные нормали поверхности также нужно преобразовать в про- странство обзора. Обычно такие преобразования делаются обратной транспози- цией матрицы модели-вида. На этом этапе формулы освещенности OpenGL можно применять для опре- деления освещенности каждой вершины. После преобразования координат в систе- му обзора обычно определяют видимый объем — часть трехмерной сцены, которая будет видима в окончательном изображении. Преобразование, которое помещает объекты видимого объема в пространство отсечения (система координат, прием- лемая для отсечения), называется проецированием. В OpenGL проецирование на- страивается функцией gl Mat ri xMode, при этом выбирается и правильно устанавли- вается матрица проекции. Параметры, которые могут передаваться, — это область обзора (размер видимой части сцены), соотношение сторон (вертикальный раз- мер поля обзора может не соответствовать горизонтальному), поверхности от- сечения для отбраковки объектов либо слишком близких, либо слишком дале- ких (при вычислении перспективы получаются причудливые результаты, если
48 Глава 1. Обзор OpenGL пытаться рисовать прямо в точке обзора или сразу перед пей). Три вспомогатель- ных функции, glOrtho, glFrustum и gluPerspective, могут устанавливать матрицу проекции, glOrtho определяет параллельную проекцию (параллельные линии в сцене проецируются па параллельные линии в окончательном двухмерном изоб- ражении), a gl Frustum и gl uPerspect 1 ve определяют проекцию в перспективе (па- раллельные линии в сцене спроецированы таким образом, чтобы смыкаться в точке схода, примером могут служить рельсы, сходящиеся на горизонте). Отсечение усеченным конусом исключает любые графические примитивы, на- ходящиеся вне прилегающего к оси пространства отсечения. Это пространство определено формулой, где координаты отсеченного пространствах, у и z меньше или равны координате w или больше или равны -w (то есть - w < х < w, -w < у < w, -w<z< w). Графические примитивы или их части, находящиеся снаружи, будут отброшены. Отсечение конусом в OpenGL всегда применяется для всех приходя- щих примитивов. Произвольное отсечение можно выключить пли включить. При- ложения могут вызывать функцию gl Cl 1 рР1 апе для задания одной или больше плос- костей отсечения, которые, в свою очередь, ограничивают область обзора еще больше. Каждая плоскость должна быть отдельно задана функцией gl Ena bl е. Если произвольные плоскости отсечения указаны, они преобразуются в пространство обзора с помощью инверсии текущей матрицы модели-вида. Каждая плоскость, таким образом, определяет отсеченную полуплоскость, в результате будут нари- сованы только те части примитивов, которые попадают в пересечение изначаль- ной области обзора и разрешенных полуплоскостей! Следующий шаг в преобразованиях координат вершин — расчет перспективы. Эта операция делит координаты каждого компонента отсеченного пространства на однородную координату w. Полученные координаты х, у и z будут находиться в диапазоне [-1,1], полученная же координата w всегда будет равна единице, так что ее можно отбросить. Иными словами, все видимые графические примитивы будут преобразованы так, чтобы помещаться в пространство между точками (-1, -1, -1) и (1, 1,1). Это нормализованное пространство приборных координат, которое является промежуточным пространством на пути к окну и применяется для лучшего отображения графики в окне произвольных размера и глубины. Пикселы в окне на экране, конечно же, не имеют дробных координат из проме- жутка (-1, 1). В окне обычно используются оконные координаты, где координата х меняется от 0 до значения ширины окна минус 1, а координата у — от 0 до значе- ния высоты окна минус 1. Поэтому нужно выполнить еще одно преобразование. Оконное преобразование определяет правила отображения из нормализованных приборных координат в оконные координаты, и эти правила можно назначить функцией gl Viewport (назначаются правила отображения для х- и у-координат). Растеризация графических примитивов происходит уже в оконных координатах. 1.10. Текстурирование Отображение текстуры - один из самых сложных видов операций в OpenGL API. Для него определено больше расширений, чем для других видов OpenGL-опера- ций, так как этот вид был чуть ли не единственным, для которого применяли гра- фические ускорители в то время, когда был определен OpenGL (в начале 1990 гг.).
1.10. Текстурирование 49 Программируемость, привнесенная языком шейдеров OpenGL, продвигает эти операции далеко вперед, по уже существующие в OpenGL-функции все еще ис- пользуются для создания текстур, их изменения и определения их поведения. Данный раздел описывает ту функциональность работы с текстурами, которая использовалась еще в OpenGL 1.5. В этой модели произошли некоторые значи- тельные изменения, в частности в принципах текстурирования, и появились рас- ширения OpenGL, которые поддерживают язык шейдеров OpenGL. В OpenGL существует четыре основных типа текстурных карт: одномерная, двухмерная, трехмерная и кубическая (OpenGL 1.0 поддерживал только одномер- ные и двухмерные текстуры). 1 D-текстура — это массив, в котором хранятся зна- чения ширины, 2В-текстура — массив, содержащий значения высоты и ширины, а BD-текстура содержит значения ширины, высоты и глубины. Кубическая тек- стура содержит шесть двухмерных текстур, для каждого главного направления оси (то есть ±х, ±у, ±z). В OpenGL используется понятие модуля текстур. Этот модуль соответствует части графического ускорителя, которая выполняет различные операции с тек- стурами. В OpenGL 1.3 появилась возможность работы с несколькими текстур- ными модулями. Каждый модуль хранит несколько состояний для текстурных операций: □ состояние модуля — включен или выключен; □ стек текстурной матрицы, используемый для преобразования текстурных ко- ординат; □ флаг, используемый для автоматического генерирования текстурных координат; □ состояние текстурного окружения; □ текущую одномерную текстуру; □ текущую двухмерную текстуру; □ текущую трехмерную текстуру; □ текущую кубическую текстуру. Все эти параметры можно установить для активного модуля текстур соответ- ствующими командами. Модули нумерованы от 0 до GL_MAX_TEXTURE_UNITS-1 (это значение можно получить вызовом функции gl Get), а активный модуль текстур выбирается (функцией glActiveTexture с параметром в виде соответствующей вы- бираемому модулю константы. Все команды установки параметров текстуры рабо- тают только с активным модулем текстуры. Модуль можно настроить на одно-, двух-, трехмерные или же кубические текстуры, передав в функцию gl Enable со- ответствующую константу. К активному модулю можно применять обработку текстурных координат. Су- ществуют команды доступа к текущему стеку матриц (в случае если GL_MATR1X_MODE установлен в GL_TEXTURE), glTexGen, gl Enabl e/gl DI sabl e (если включен какой-либо спо- соб генерирования текстурных координат), запросы текущих текстурных коорди- нат и текущих растровых текстурных координат. Есть и команды обработки текстур- ных изображений — все варианты gl TexEnv, gl TexPararneter и glTexImage; gl Bi ndTexture; gl Enabl e/gl DI sable для любого вида текстуры (например, GL_~FX“URE?j); все вари- анты запросов состояния.
50 Глава 1. Обзор OpenGL Объект текстуры может быть создан функцией gl BindTexture, если ей передать в качестве параметра символическую константу, определяющую вид текстуры, и имя текстуры (целое, отличное от нуля), которое впоследствии будет использо- ваться для ссылки на созданный текстурный объект. Этот объект становится актив- ным, и все последующие команды будут выполняться над ним. Если gl Bi ndTexture вызывается с именем уже созданного объекта, этот объект просто становится ак- тивным. Таким образом приложение может создавать любое количество текстур и легко переключаться между ними. После создания текстурного объекта необходимо задать для него пиксельные значения, определяющие текстуру. Значения для трехмерной текстуры задаются функцией glTexImage3D, для двухмерной или кубической — функцией gl TexIrnage2D, для одномерной — glTexImagelD, При использовании любой из этих трех команд каждое измерение текстурной карты наряду с шириной границы должно иметь размер, кратный степени двойки. Все эти команды работают почти так же, как и glDrawPixel 5, а отличаются тем, что пикселы, из которых состоит текстура, рас- полагаются в текстурной памяти перед растеризацией. Если нужно переопределить только часть текстуры, используются функции glTexSuMmagelD/2D/3D. При этом уже нет ограничений на степень двойки. Также можно создавать или менять тек- стуры из значений, взятых из буфера кадров, с помощью функций gl CopyTexImagelD/ 2D/3D или gl CopyTexSubImagelD/2D/30. Функции glCompressedTexImagelD/2D/3D ид! CompressedTexSubIniagelD/2D/3D исполь- зуются для работы с текстурами, представленными в сжатых форматах. Такие тек- стуры занимают гораздо меньше памяти графического ускорителя, за счет чего она освобождается для дополнительной функциональности или повышается ско- рость выполнения. Стандарт OpenGL не определяет никаких конкретных форма- тов, так что приложения должны вначале получить список расширений и опреде- лить, поддерживает ли данная реализация сжатые текстуры. Каждая из команд, предшествующих созданию текстуры, включает аргумент степени детализации, поддерживающий создание текстур для объектов различной удаленности (mipmap textures). Такая текстура — это набор массивов с одним и тем же изображением. Каждый массив в каждом своем измерении имеет разрешение, вполовину меньшее разрешения предыдущего массива. Смысл использования та- ких текстур в том, что поверхность объектов, созданная с их помощью, будет хо- рошо выглядеть при любом удалении. Если для объекта указана такая текстура, OpenGL будет автоматически выбирать нужный уровень уменыиенности тексту- ры при рисовании объекта. Пикселы, из которых состоит текстура, называются текселами. Возможна также интерполяция между текселами двух соседних уров- ней уменьшенности. Объекты, которым назначены текстуры с уровнями удален- ности, обрабатываются с высоким качеством вне зависимости от их реального раз- мера на экране. После определения текстурного объекта и присоединения его к текстурному модулю можно устанавливать для текстуры параметры, не имеющие отношения к пикселам, функцией glTexParameter. Этой командой устанавливаются парамет- ры, определяющие интерпретацию текстурного объекта при первичном введении, изменении объекта или доступе к объекту.
1,10, Текстурирование 51 Параметрами текстурного объекта являются; □ поведение объекта в направлении каждого измерения (будет ли текстура по- вторяться, просто смыкаться или зеркально отображаться при выходе за пре- делы диапазона [0, 1]); □ фильтр уменьшения, который определяет способ дискретизации текстуры, если она должна быть уменьшена при преобразовании из текстурного пространства в оконное пространство и наложении на поверхность; □ фильтр увеличения, который определяет способ дискретизации текстуры, если она должна быть увеличена при преобразовании из текстурного пространства в оконное пространство и наложении на поверхность; □ цвет рамки в случае, если поведение объекта в направлении каждого измере- ния учитывает смыкание с рамкой; □ приоритетность текстуры (значение от 0 до 1, которое может быть присвоено текстуре. Это значение определяет скорость работы OpenGL с данной текстурой); □ параметры замыкания и смешения для значения уровня детализации, вычис- ляемого OpenGL; □ уровень базового (наивысшего) разрешения для множественных текстур; □ уровень базового (самого низкого) разрешения для множественных текстур; □ параметры, определяющие, будут ли выполняться какие-либо операции срав- нения при доступе к текстуре, какие именно и как рассматривать результат сравнения (эта особенность используется обычно в процессе работы с тексту- рами глубины — для создания тени); □ параметр для множественных текстур, определяющий, будут ли перевычис- ляться автоматически все уровни текстуры в случае, когда текстура только что задана или изменена. Способ наложения текстуры на графический примитив определяется парамет- рами текстурной среды, которые устанавливаются функцией glTexEnv. Существу- ет обширный набор жестко определенных формул для замены цвета объекта тем, который был вычислен на основании параметров налаженной текстуры. Доста- точно сказать, что такие функции поддерживают замену, модуляцию, декориро- вание, сглаживание, сложение и сложные комбинации значений красного, зеле- ного, синего цветов и компонента прозрачности. Большого разнообразия эффектов можно достичь с помощью гибкости, обеспечиваемой функцией glTexEnv. Ее мож- но использовать, чтобы задать дополнительные параметры смешения для моду- лей, которые будут добавлены к параметрам смешения текстурного объекта, опи- санным в предыдущем абзаце, OpenGL поддерживает концепцию множественности текстур, заключающую- ся в том, что па объект может быть наложено несколько текстур и по ним совмест- но определяется значение отдельного фрагмента. У каждой текстурной единицы есть функция текстурной среды. Текстурные единицы соединяются последовательно. Значение фрагмента вычисляется из
52 Глава 1. Обзор OpenGL первой текстурной единицы, при этом используется значение, прочитанное из тек- стурной памяти, и ее функции текстурной среды, результат передается в качестве входного значения для следующей текстурной единицы. Это значение совме- щается с функцией текстурной среды для второго текстурного модуля и со зна- чением, прочитанным из текстурной памяти, и используется для вычисления третьего текстурного модуля. То же повторяется для всех включенных текстур- ных модулей. После того как текстурные объекты определены и один или больше текстур- ных модулей правильно установлены и включены, текстурирование применяется ко всем графическим примитивам последовательно. Текстурные координаты для каждой вершины задаются функциямиglTexCoord или glMultiTexCoord (при мето- де «вершина за раз») или как массив, переданный в функцию glTexCoordPointer (при методе массивов вершин). Функция gl Mui tlTexCoord определяет текстурные координаты, которые будут применяться для заданного текстурного модуля. Ко- манда glTexCoord почти аналогична glMultiTexCoord, с тем лишь отличием, что па- раметр текстуры всегда установлен в GL_TEXTUREO. Для массивов вершин необхо- димо вызывать командой gl Cl 1 entActi veTexture между вызовами gl TexCoordPoi nter. чтобы задавать различные массивы текстурных координат для разных текстур- ных модулей. Координаты текстур также могут быть автоматически сгенерированы OpenGL. Параметры настройки такой автоматической генерации устанавливаются для каждого текстурного модуля отдельно командой glTexGen. Эта функция дает при- ложению возможность выбрать функцию генерирования текстуры и указать коэффициенты для текущего текстурного модуля. Поддерживаемые функции ав- томатической генерации бывают линейные для объекта (они используются при создании моделей местности), линейные для взгляда (используются при создании динамических контурных линий движущегося объекта) и сферические (исполь- зуются только при наложении карты среды с одной текстурой). После того как текстурные координаты были заданы для OpenGL либо сге- нерированы функцией, они преобразуются с помощью текущей матрицы пре- образования. Команда glMatrixMode используется при выборе нужного стека матрицы для ее изменения, и несколько последующих команд, примененных к мат- рице, изменят ее для активного текстурного модуля. Это обеспечивает возмож- ность лучше использовать текстуру для объекта — создать поворот, растяжение, скос и т. д. И генерирование, и преобразование текстуры определены OpenGL как часть обработки вершин (то есть выполняются перед растеризацией для каждой вер- шины отдельно). После растеризации графического примитива (и интерполяции преобразован- ных текстурных координат) можно получить доступ к текстуре — текстурный мо- дуль использует координаты для доступа к текстуре. Доступ выполняется с уче- том связанных с текстурным объектом параметров для фильтрации, обертывания, вычисленного уровня детализации и т. д. Полученное значение текстуры сочетается со значением цвета в соответствии с текстурной функцией, заданной glTexEnv. Эта операция называется наложение текстур. В результате вычислений получается новое значение цвета фрагмента,
1,12. Ссылки 53 которое используется для всех последующих операций с этим фрагментом. И до- ступ к текстуре, и наложение текстур применяются к любому фрагменту, про- шедшему растеризацию. 1.11. Итоги В главе представлен небольшой обзор основ программного интерфейса OpenGL. Рассмотрено подавляющее большинство самых важных функций OpenGL. Этой информации должно быть достаточно, чтобы сориентироваться и подготовиться к написанию шейдерных программ на языке шейдеров OpenGL. Читатель, ранее использовавший другой программный SD-иптерфейс, должен получить представ- ление об OpenGL, достаточное для написания шейдеров. Если же нужна допол- нительная информация, в следующем разделе перечислены ссылки на другие ре- сурсы для изучения OpenGL. 1.12. Ссылки Самая свежая информация для сообщества OpenGL-разработчиков содержится на веб-сайтах http://opengl.org и http://opengl.org.ru, где можно найти и форумы для разработчиков, н ссылки на техническую информацию, и демонстрационные программы. Эти сайты присутствуют в списке избранных ссылок любого OpenGL- разработчика и довольно часто посещаются. Могут оказаться полезными и немного устаревшие справочники по OpenGL API [4 и 5], обе изданы OpenGL Architecture Review Board. Эти книги вышли в четвертой редакции в 2004 г. Еще одна полезная книга по OpenGL — [10]. Хороший обзор OpenGL можно найти в статье [8]. И конечно же, основным документом является сама спецификация по языку [7]. Веб-сайт http://opengl.org — источник хороших примеров исходного кода. Еще один полезный сайт — http://delphi3d.net. Производители графических ускори- телей, которые поддерживают OpenGL, обычно предоставляют много примеров, особенно для новых возможностей и расширений. Веб-сайты компаний SGI, NVIDIA и ATI могут пригодиться разработчикам. 1. ATI. Веб-сайт для разработчиков (http://www.ati.com/na/pages/resoLirce~centre/ dev_rel/devrel.html). 2. DelphiBD. Веб-сайт для разработчиков (http://delphi3d.net). 3. NVIDIA. Веб-сайт для разработчиков (http://developer.nvidia.com). 4. Neider J., Davis Т., Woo М. OpenGL Programming Guide. 3rd ed. Reading, MS: Add (son-Wesley, 1999. 5. OpenGL Reference Manual. 3rd ed. Reading, MS: Addison-Wesley, 1999. 6. OpenGL. Официальный веб-сайт (http://opengl.org).
54 Глава 1. Обзор OpenGL 7. Sega! М., Akeley К. The OpenGL Graphics System: A Specification (Version 1.5)/ Ed.: C. Frazier (v. 1.1), J. Leech (v. 1,2-1.5). 2003 (http://opengl.org). 8. Segal M., Akeley K. The Design of the OpenGL Graphics Interface/Silicon Gra- phics Inc. 1994 (http://opengL.org). 9. SGI OpenGL. Веб-сайт (http://www.sgi.com/software/opengl). 10. Wright R., Sweet M. OpenGL SuperBible. 2nd ed. Corte Madera, California: Waite Group Press, 1999 (http://www.starstonesoftware.com/OpenGL/opengl_super- bbile.htm).
Основы Данная глава представляет язык шейдеров OpenGL, она призвана помочь читате- лю как можно быстрее начать писать собственные шейдеры. В процессе чтения он должен усвоить, каким образом добавлена в OpenGL программируемость, и под- готовиться вникать в детали языка шейдеров, описываемых в следующих трех главах, и рассмотреть простой пример в главе 6. После прочтения данной главы читатель способен изучать в подробностях API, которое поддерживает язык шейдеров, и пробовать выполнять примеры. 2.1. Введение в язык шейдеров OpenGL Цель данной книги помочь читателю изучить и использовать высокоуровне- вый язык программирования, который формально называется языком шейдеров OpenGL. Этот язык и расширения OpenGL, которые его поддерживают, были ут- верждены как ARB-расширения еще в июне 2003 г. Ожидается, что они будут до- бавлены в базовый стандарт OpenGL в версии 2.0. Замена постоянной функциональности программируемостью на тех этапах обработки изображения, которые становятся все более и более сложными, — по- следняя тенденция для графических ускорителей. Такими этапами являются об- работка вершин и обработка фрагментов. Обработка вершин включает в себя опе- рации, выполняемые над каждой вершиной, — в основном это преобразование и настройка освещения. Фрагменты — это структуры данных для каждого пиксела, которые создаются в результате растеризации графических примитивов. У фраг- мента есть все данные, необходимые для обновления одного пиксела в буфере. Обработка фрагментов состоит из операций, выполняемых над одним фрагмен- том, — в основном это чтение из текстурной памяти и применение значений тек- стуры к каждому фрагменту. С языком шейдеров OpenGL постоянная функцио- нальность обработки вершин и фрагментов дополнительно получила возможности программируемой функциональности, которая умеет то же, что и постоянная, и мно- го больше. Язык шейдеров OpenGL создавался для того, чтобы программисты могли ускорять описанные этапы обработки графической информации в OpenGL. Целостный фрагмент кода на языке шейдеров, предназначенный для вы- полнения на одном из программируемых OpenGL-процессоров, называется шейдером. Иногда термин «OpenGL-шейдер» используется для того, чтобы отли- чить шейдеры, написанные на языке шейдеров OpenGL, от шейдеров, написан- ных на другом языке, например RenderMan. Так как в OpenGL определены два
56 Глава 2. Основы программируемых процессора, существует два типа шейдеров: вершинные шейде- ры и фрагментные шейдеры. В OpenGL есть механизм для компиляции шейдеров и линкования их в выполняемый код (программу). Программа содержит один или несколько выполняемых модулей, которые могут запускаться на программируе- мых модулях обработки графической информации. Язык шейдеров OpenGL корнями уходит в язык С и имеет практически такие же возможности, как RenderMan и другие шейдерные языки. В языке содержится обширный набор типов, включая вектор и матрицу, которые позволяют сделать код шейдерных программ для обычных ЗО-графических операций более лаконич- ным. Сущес твует в нем и специальный набор типов, каждый из которых опреде- ляет уникальную форму данных ввода или вывода для шейдеров. В языке есть заимствованные из C++ механизмы: перегрузка функций по типу аргумента и объ- явление переменных непосредственно перед использованием вместо начала бло- ка. Язык поддерживает циклы, подпрограммы и условные выражения. Богатый набор встроенных функций обеспечивает много возможностей, необходимых для создания шейдерных алгоритмов. Об этом языке можно сказать следующее: □ Язык шейдеров OpenGL — высокоуровневый процедурный язык. □ Такой же язык, с небольшими изменениями, используется для вершинных и фрагмептпых шейдеров. □ Он базируется на синтаксисе и управлении С и C++. □ Изначально в нем поддерживаются векторные и матричные операции, так как они являются неотъемлемой частью многих графических алгоритмов. □ Язык более жестко проверяет типы, чем С и C++, и функции могут вызывать- ся по возвращаемому значению. □ Он использует квалификаторы типов чаще, чем управление вводом-выводом. □ У него пет ни ограничений на длину шейдера, ни необходимости ее запраши- вать. В следующих разделах читатель найдет некоторые ключевые положения, ко- торые необходимо понимать для того, чтобы эффективно использовать язык шей- деров OpenGL. Детали будут описаны позже, в других главах, а эта глава поможет увидеть картину в целом. 2.2. Для чего нужны шейдеры До последнего времени OpenGL предоставлял программистам гибкий, но статичный интерфейс для рисования графики на дисплее. Как сказано в главе 1, OpenGL это последовательность операций, применяемых к геометрическим или растро- вым данным, отправленным через графический ускоритель для вывода на экран. На этих этапах можно менять различные параметры, чтобы настраивать обработ- ку графики нужным образом. Но только с OpenGL API невозможно изменить ни фундаментальную операцию, ни их порядок. Предоставляя поддержку для традиционных механизмов рендеринга, OpenGL эволюционировал. Он удовлетворял запросы большого количества приложений.
2.3. Программируемые процессоры OpenGL 57 Если какое-либо приложение устраивает традиционная модель рендеринга, ему никогда не понадобятся шейдеры. Цель языка шейдеров OpenGL и соответствующего OpenGL АР] — дать прило- жениям возможность определять, как будет происходить обработка графики иа некоторых этапах с помощью высокоуровневого языка программирования, сконст- руированного специально для этого. Этапы, о которых идет речь, сделаны програм- мируемыми, чтобы дать приложениям полную свободу в определении способа обработки изображения. Это позволяет приложениям использовать графический ускоритель для достижения большого количества эффектов визуализации. Чтобы увидеть, какие эффекты можно получить с помощью шейдеров OpenGL, посмотрите па цветные вклейки. Эта книга представляет большое разнообразие шейдеров, которые улучшают поверхность настолько, насколько это возможно, С каждым новым поколением графических ускорителей все более сложные тех- ники рендеринга могут быть выполнены как OpenGL-шейдеры и использованы в приложениях для рендеринга в реальном времени. Вот краткий синеок того, чего можно достичь с использованием шейдеров OpenGL: □ увеличенная реалистичность отображения материалов — металла, камня, де- рева, краски и т. д.; □ увеличенная реалистичность эффектов освещения — освещенных областей, мягких теней и т. д.; □ изображение природных явлений — огня, дыма, воды, облаков и т. д,; □ создание не фотографически точных изображений — имитация живописи, ри- сунка пером, воспроизведение техники иллюстрации и т. д.; □ использование текстур для хранения нормалей, блеска, полиномных коэффи- циентов и т. д. — новые возможности использования текстурной памяти; □ меньшее количество способов доступа к текстурам — текстуры могут быть со- зданы с помощью процедур, а не посредством доступа к текстурным картам, хранящимся в текстурной памяти; □ использование новых способов обработки изображений — искривления, нечет- кой обработки маски, сложного сглаживания и т. д.; □ создание эффектов анимации — интерполяции ключевого кадра, процедурно определенных движений; □ использование программируемых методов антиалиасинга. Многие из этих техник раньше были доступны только через программные реа- лизации, ограниченно — если вообще доступны. Сейчас же они могут быть реали- зованы с использованием аппаратного ускорения, обеспеченного сложными гра- фическими акселераторами. Это означает, что скорость визуализации может быть увеличена, а процессор разгружен для выполнения других задач. 2.3. Программируемые процессоры OpenGL Самое большое изменение в OpenGL со времени его создания, которое и послужи- ло причиной создания языка шейдеров, — внедрение программируемых вершинных
58 Глава 2. Основы и фрагментных процессоров. В главе 1 обсуждалась фиксированная функциональ- ность обработки вершин и фрагментов. С внедрением программируемости в слу- чае, если язык шейдеров используется приложением, фиксированная функцио- нальность выключается. Далее описывается процесс обработки графики в OpenGL при активных про- граммируемых процессорах (рис. 2.1). В этом случае фиксированная часть обра- ботки вершин и фрагментов (см. рис. 1.1) заменяется программируемой. Осталь- ные этапы обработки графики остаются неизменными. ----► Вершины ----► Фрагменты ----)► Текстуры — — )► Группы пикселов Программируемый процессор Рис. 2.1. Логическая диаграмма операций в OpenGL с программируемыми процессорами для вершинных и фрагментных шейдеров Эта диаграмма иллюстрирует, как данные проходят через программируемые процессоры, определенные как часть языка OpenGL. Потоки данных идут от при- ложения к вершинному процессору, потом к фрагмептному процессору и в ко- нечном счете попадают в буфер кадров. 2.3.1. Вершинный процессор Вершинный процессор — это программируемый модуль, который выполняет опе- рации над входными значениями вершин и другими связанными с ними данны- ми. Вершинный процессор предназначен для выполнения следующих традици- онных операций с графикой: □ преобразования вершин; □ преобразования нормали, нормализации; □ генерирования текстурных координат; □ преобразования текстурных координат;
2.3, Программируемые процессоры OpenGL 59 □ настройки освещения; □ наложения цвета материала. Так как этот процессор является программируемым, его можно использовать и для других вычислений. Шейдеры, которые предназначены для выполнения на этом процессоре, называются вершинными шейдерами. Вершинные шейдеры мож- но использовать для определения общей последовательности операций, которые будут выполняться над вершиной и другими связанными с ней данными. Вер- шинные шейдеры, выполняющие некоторые из вычислений, перечисленных в списке, отвечают за написание кода для всей функциональности из списка. На- пример, невозможно использовать одновременно и стандартные функции преоб- разования вершины и нормали, и вершинный шейдер для вычисления освеще- ния. Написанный вершинный шейдер должен выполнять все три операции. Вершинный процессор не может заменить графические операции, которым требуются данные о нескольких вершинах или о топологии объектов. Стандарт- ные операции, которые не могут выполняться вершинным и фрагментным про- цессорами, — это вычисление перспективы и создание графического окна, сборка примитивов, отсечение конусом и плоскостями, отбраковка задних поверхностей, выбор двухстороннего освещения, режим, многоугольников, смещение многоуголь- ников, плоское или мягкое затенение, диапазон глубины. Вершинные шейдеры можно использовать, чтобы улучшить алгоритм, выпол- няемый на вершинном процессоре. Они получают входные данные и производят выходные данные (рис. 2.2). Для управления входными и выходными данными вершинного процессора ис- пользуются квалификаторы типов, определенные как часть языка шейдеров OpenGL. Переменные, определенные в вершинном шейдере, рассматриваются как пе- ременные атрибутов. Эти переменные представляют собой данные, которые пере- даются вершинному процессору от приложения. Поскольку эти типы используют- ся только для передачи данных о вершинах из приложения, их можно использовать лишь как часть вершинного шейдера. Приложения могут задавать значения атри- бутов либо между вызовами gl Beglп и gl End, либо вызовом функций работы с вер- шинными массивами, так что значения могут меняться так же часто, как и теку- щая вершина, то есть задаваться для каждой вершины отдельно. Существует два типа переменных атрибутов: встроенные и определенные раз- работчиком. Стандартные переменные атрибутов в OpenGL включают значения цвета, нормали поверхности, координаты текстуры и координаты вершины. За- дать их можно OpenGL-функциями glColor, glNormal, glVertex и др. Команды OpenGL для обработки массивов вершин также можно использовать, чтобы за- дать стандартные OpenGL-атрибуты вершин вершинному процессору. При выполнении вершинный шейдер получает заданные значения через встро- енные переменные атрибутов с названиями gl_Color, gl_Norrnal, gl_Vertex и др. Описанный способ передачи данных ограничивает возможные данные уже опре- деленным в OpenGL набором. Чтобы обойти это ограничение, был добавлен новый интерфейс, позволяющий приложениям передавать произвольные данные для вер- шин. В OpenGL API определены общие вершинные атрибуты, на которые ссылаются по номерам от 0 до какого-то определенного конкретной реализацией максималь- ного значения. Команда gl VertexAttг 1 bARB отправляет общие вершинные атрибу- ты в OpenGL, и ей нужно задавать помер общего а трибута и значение для него.
60 Глава 2. Основы ' Встроенные ; переменные | атрибутов: । gl_Colorgl_Normal I gt_ Vertex J glJMultiTexCoordO и др. Определенные пользователем переменные атрибутов: StartColor Velocity Elevation Tangent к др. > Карты 1_ I текстур |' вершинный гбоцедедр i Определенные пользователем 1 J uniform-леременные: ; ModelScaleFactor, EyePos, Epsilon, I | LightPosition, WeightingFactorl и др. | [Встроенные uniform-переменные: i _jgl_ModelViewMatrix, gl_FrontMaterial,i । g(_LighfSourcep..n/, gi_Fog и др. । ______ fT Встроенные varytng-леременные. g/J=roniCofor gl^BackColor glFogFragCoortf и др. j Специальные переменные вывода;: gij>os№on д) PointSize gl^Clip Vertex ...______t____________ Определенные пользователем переменные вывода? Normal ModelCoord Refractionindex Density и др. : ?: ------Непосредственно от приложения ------Косвенно от приложения япнияи! От вершинного процессора Рис. 2.2. Входные и выходные данные вершинного процессора Вершинные шейдеры могут получать заданные значения общих атрибутов че- рез определенные разработчиком переменные атрибутов. Для этого в OpenGL была добавлена новая команда gl BindAttribLocat 1 onARB, которая ассоциирует номер об- щего вершинного атрибута с именем переменной в вершинном шейдере. Однообразные (uniform) переменные приложение может использовать для пере- дачи как вершинному, так и фрагмептному процессору. Такие переменные обычно используются для передачи относительно редко изменяемых данных. Шейдер может быть исполнен так, чтобы получать входные параметры через uniform-переменные. Приложение задает начальные значения uniform-переменным, а пользователь манипулирует ими через графический интерфейс, чтобы достичь разнообразия эффектов в одном шейдере. Но эти начальные значения не могут задаваться меж- ду вызовами glBegin и glEnd, так что менять их можно не чаще, чем один раз для каждого примитива. Язык шейдеров OpenGL поддерживает и встроенные, и определенные разра- ботчиком uniform-переменные. Вершинные и фрагментные шейдеры могут полу-
2.3. Программируемые процессоры OpenGL 61 чать текущее состояние через встроенные переменные, которые начинаются с пре- фикса с '_. Приложение может задавать определенные разработчиком переменные, чтобы передавать произвольные значения данных непосредственно тендеру. С помощью функции gl Get Uni formLocat 1 onARB можно определить расположение определенной разработчиком переменной, которая была объявлена как часть шей- дера. Загрузить данные в это место можно другой функцией, gl Uni formARB. Суще- ствуют разные варианты этой команды — для загрузки числа с плавающей запя- той, целого, булевой переменной, матриц и массивов. У вершинных процессоров есть еще одна особенность — возможность чтения из текстурной памяти. Это позволяет вершинным шейдерам реализовать, в част- ности, алгоритмы смещения и отображения. (Не во всех реализациях OpenGL эта возможность доступна, так как минимальное количество требуемых от реализа- ции вершинных текстурных модулей может равняться нулю.) Уровень детализа- ции для доступа к многоуровневым текстурам задается непосредственно в шей- дере. Существующие в OpenGL параметры для текстурных карт определяют его поведение при операциях фильтрации, на границах и при наложении текстур. По определению вершинный процессор работает только с одной вершиной одновременно, по реализация может иметь несколько вершинных процессоров, которые будут работать параллельно. Вершинный шейдер выполняется один раз для каждой заданной вершины. Схема вершинного процессора сосредоточена на функциональности для преобразования и освещения отдельной вершины. Вер- шинные шейдеры должны вычислить однородную координату в пространстве отсечения и сохранить результат в специальной выходной переменной gl_Position. Значения для пользовательского отсечения и точки растеризации будут хранить- ся в выходных переменных gl_ClipVertex и gl_PointS1 хе. Переменные, в которых данные передаются из вершинного процессора фрагмент- ному, называются разнообразными (varying) переменными. Они бывают и встроен- ные, и определенные разработчиком. Переменные называются разнообразными, потому что значения для каждой вершины будут потенциально разными, и для каждого фрагмента будет выполняться интерполяция для перспективы, с тем что- бы в дальнейшем эти значения использовались фрагментным шейдером. Суще- ствуют встроенные varying-переменные для значений цвета и координат текстуры. Вершинный шейдер может использовать определенные разработчиком varying- переменные, чтобы получить дополнительные входные данные для интерполя- ции цвета, нормалей (для вычисления освещения фрагментов), текстурных коор- динат, модельных координат и других произвольных значений. Иногда вершинный шейдер может выдавать больше данных, чем нужно фраг- ментному шейдеру, и в этом нет ничего страшного, кроме возможного уменьше- ния производительности. Если фрагментный шейдер требует меньше данных, чем приходит из вершинного шейдера, может генерироваться предупреждение. Но все же иногда требуется использовать какой-то общий вершинный шейдер со- вместно с разными фрагментными шейдерами, которые могут быть специально написаны так, чтобы работать с каким-либо подмножеством выходных данных вершинного шейдера. Таким образом, для приложений, работающих с большим количеством шейдеров, может оказаться более выгодным иметь больше универ- сальных шейдеров п таким образом снизить цепу разработки и поддержки, чем немного выиграть в производительности.
62 Глава 2. Основы Выходные данные вершинного процессора (специальные выходные перемен- ные, а также встроенные и определенные разработчиком varying-переменные) отправляются на следующие этапы обработки, точно такие же, как в OpenGL 1.5: сборка примитивов, отсечение усеченным конусом и пользовательское, режим многоугольников, режим затенения, отбраковка. 2.3.2. Фрагментный процессор Фрагментный процессор — это программируемый модуль, который выполняет операции над фрагментами и другими связанными с ними данными. Фрагмент- ный процессор может выполнять следующие стандартные графические операции: □ операции над интерполированными значениями; □ доступ к текстурам; □ наложение текстур; □ создание эффекта дымки; □ наложение цветов. Этот процессор способен выполнять и множество других вычислений. Шей- деры, предназначенные для запуска на этом процессоре, называются фрагмент- ными шейдерами. Фрагментные шейдеры представляют собой алгоритмы, кото- рые выполняются на фрагментном процессоре и производят выходные данные, основываясь на переданных им входных данных. Фрагментный шейдер не может изменить координаты х и у фрагмента. Фрагментные шейдеры, которым нужно выполнять некоторые операции из приведенного списка, должны производить все вычисления. Например, невозможно вычислять стандартным способом дымку и в то же время иметь фрагментный шейдер, который будет выполнять доступ к тексту- рам и наложение текстур. Фрагментный шейдер должен выполнять все три опе- рации. Фрагментный процессор не заменяет графические операции, требующие сведений о нескольких фрагментах сразу. Чтобы поддержать параллельность на этапе обработки фрагментов, фрагментные шейдеры должны работать только с од- ним фрагментом, в этот момент соседние фрагменты недоступны. Реализация может иметь несколько фрагментных процессоров, работающих одновременно. Фрагментный процессор может быть использован для. выполнения операций над каждым из фрагментов, полученных в результате рендеринга точек, линий, многоугольников, пиксельных прямоугольников, растровых изображений. Если изображения сначала загружены в текстурную память, фрагментный процессор можно использовать также для обработки пикселов, в процессе которой требует- ся доступ и к самому пикселу, и к соседним с ним пикселам. При рисовании пря- моугольника с включенным текстурированием фрагментный процессор может читать данные об изображении из текстурной памяти и накладывать их на прямо- угольник, выполняя при этом обычные операции: □ масштабирование пиксела; □ масштабирование и смещение; □ поиск в таблице цветов; □ искривление; □ обработку матрицы линейного преобразования.
2.3. Программируемые процессоры OpenGL 63 Фрагментный процессор не заменяет стандартные графические операции, выпол- няемые в конце обработки пикселов, — покрытие, проверка на видимость, отсече- ние по прямоугольнику, наложение шаблона, проверка прозрачности, проверка глубины, отсечение по шаблону, наложение смешиваемых цветов, логические операции, сглаживание, определение видимости плоскостей. На рис. 2.3 показаны входные и выходные значения фрагмеитного процессора. । glJ-'9htSource[0..n], gl_Fog и др. PJ Полученные в результате растеризации -----Непосредственно от приложения -----Косвенно от приложения И От фрагмеитного процессора Рис. 2.3. Входные и выходные данные фрагментного процессора Первичными входными данными для фрагментного процессора являются ин- терполированные в результате растеризации varying-переменные — как встроен- ные, так и определенные разработчиком. Определенные разработчиком varying- переменные должны быть определены внутри фрагментного шейдера, и их типы должны совпадать с типами, определенными в вершинном шейдере. Значения, вычисляемые стандартной функциональностьго между вершинным и фрагмент- ным процессорами, доступны посредством специальных входных переменных. Оконные координаты фрагмента и атрибут, обозначающий, был ли фрагмент сгенерирован для передней поверхности, передаются через входные переменные gl_FragCoord и gl_FrontFacing.
64 Глава 2. Основы Состояния OpenGL доступны фрагментному шейдеру через встроенные uni- form-переменные — так же, как и вершинному шейдеру. Фактически все состояния OpenGL, передающиеся через встроенные ипИопп-перемепные, доступны и вер- шинному, и фрагментному шейдерам. Именно поэтому возможно реализовать стандартные операции над вершинами, например выбор освещения, во фрагмент- ном шейдере, используя стандартные OpenGL-операции управления состоянием. Приложение может использовать определенные разработчиком uniform-пере- менные для передачи произвольных, нечасто меняющихся данных фрагмеитно- му шейдеру. Одна и та же uniform-леремепная может быть использована как вер- шинным, так и фрагментным шейдером, если она объявлена в обоих как один и тот же тип данных. Одно из наибольших преимуществ фрагментного процессора заключается в том, что он имеет доступ к текстурной память! в любое время и может сочетать значения, прочитанные различными способами. Фрагментный шейдер может чи- тать много значений из одной текстуры или много значений из нескольких тек- стур. Результат одного чтения текстуры может быть использован для другого чте- ния (это так называемое зависимое чтение текстуры). Ограничений на размер вложенности таких зависимых чтений не существует. Фрагментный шейдер мо- жет также реализовывать алгоритмы прохождения лучей. Параметры OpenGL для текстурных карт определяют поведение операций фильтрации, создания границ, сглаживания и режимов сравнения текстур. Эти операции выполняются при получении доступа к текстуре из шейдера. Шейдер может использовать окончательный результат, а также читать несколько значе- ний из текстуры и выполнять произвольные операции фильтрации. Текстуру мож- но использовать также для выполнения операций с таблицей преобразования. Фрагментный процессор может выполнять почти все операции преобразова- ния пикселов, которые определены в OpenGL 1.5, включая и дополнительные операции над изображениями. Это значит, что фрагментный процессор поддер- живает современные способы обработки пикселов. Операции с таблицей преоб- разования можно выполнять с ID-текстурным доступом, при этом приложения полностью контролируют размер и формат текстур. Операции масштабирования и смещения легко расширяются с помощью языка программирования. Доступ к матрицам цвета можно получить через встроенные uniform-переменные. Правиль- ные значения для искривления и масштабирования пиксела вычисляются посред- ством множественного доступа к текстуре. Гистограмма и операции вычисления минимума и максимум остаются расширениями, так как их довольно трудно рас- параллелить для отдельных пикселов. Рассматривая каждый фрагмент, фрагментный шейдер л ибо вычисляет его цвет и глубину (сохраняя результат в переменных gl FragColor п gl_FragDepth), либо полностью отбрасывает фрагмент. Результаты отправляются на дальнейшую об- работку, которая остается такой же, как и в OpenGL 1.5. Данные о фрагментах используются для расчета наложения границ, проверки пикселов на видимость, отсечения по прямоугольнику, проверки прозрачности, отсечения по шаблону, проверки глубины, смешивания, размывания, проведения логических операций, операций с маской — перед тем как полностью обновить буфер кадров. Эти завер- шающие операции остаются фиксированной функциональностью, так как их лег- че сделать непрограммируемыми частями графического ускорителя.
2.4. Общее представление о языке 65 Сделать эти функции программируемыми более сложно, так как операции чте- ния/изменения записи могут вызвать значительные проблемы с распределением команд и задержки во всей обработке графики. Если необходимо, большинство из фиксированных операций можно отключить и выполнить произвольные опе- рации, их заменяющие, во фрагментом шейдере, хотя при этом придется согла- ситься с потерей в скорости. 2.4. Общее представление о языке Спецификация OpenGL получила широкое признание. Именно поэтому она ста- ла объектом деятельности по созданию промышленного стандарта высокоуров- невого языка шейдеров. Язык шейдеров, который был создан общими усилиями членов OpenGL ARB, называется языком шейдеров OpenGL (OpenGL Shading Language (GLSL)). Язык был сконструирован таким образом, чтобы учитывать дальнейшее развитие и поддерживать программируемость в различных средах. В этом разделе читатель найдет краткий обзор языка шейдеров OpenGL. Пол- ное описание языка приводится в главах 3-5. 2.4.1. Анализ языка шейдеров Запоследние несколько лет полупроводниковые технологии продвинулись в своем развитии настолько, что вычисления для вершин и фрагментов выполняются на- много быстрее, чем требуется для традиционных механизмов OpenGL, в которых устанавливаются различные состояния, влияющие на операции, выполняемые над графикой. Один из приемлемых способов уменьшить эту сложность, а также бы- стро увеличить количество расширений состоит в том, чтобы позволить заменять стандартные операции программируемыми. В некоторых последних расширени- ях OpenGL это реализовано, по па языке ассемблера. Язык ассемблера является аппаратно-зависимым по определению, и это заставляет разработчиков создавать код, который будет зависеть от платформы или производителя аппаратного обес- печения и не будет запускаться на будущих поколениях графических ускорителей. Идеальное решение — создать аппаратно-независимый язык высокого уров- ня, легкий в использовании, достаточно мощный для того, чтобы просущество- вать длительное время, способный заменить большинство расширений. Эти идеи были совмещены с потребностями реализаций одного или двух поколений гра- фических ускорителей. Язык шейдеров OpenGL должен преследовать следующие цели. □ Обеспечение хорошей совместимости с OpenGL. Язык шейдеров OpenGL был создан специально для использования совмест- но с OpenGL. Он предоставляет программируемые альтернативы для некото- рых частей стандартной функциональности OpenGL. Поэтому и сам язык, и оп- ределяемые им программируемые процессоры должны иметь как минимум такую же функциональность, какую они заменят. Более того, из шейдера, по замыслу, легко получать доступ к текущим OpenGL-состояниям. Здесь легко также совмещать стандартную функциональность и программируемость. 3 Зак 218
66 Глава 2. Основы □ Использование гибкости графических акселераторов ближайшего будущего. Модели графических акселераторов, быстро меняются, и новые модели позно- ляют программировать обработку вершин и фрагментов. Чтобы использовать эту программируемость, был создан высокоуровневый язык шейдеров с необ- ходимым уровнем абстракции для данной предметной области. В языке есть богатый набор встроенных функций, которые позволяют выполнять операции как над скалярными величинами, так и над векторами. Выражение возможно- стей аппаратного обеспечения через высокоуровневый язык программирования также помогло уменьшить количество расширений OpenGL, которые просто вносили небольшие изменения в стандартную функциональность. С появле- нием языка шейдеров, предоставляющего необходимый уровень абстракции, не зависящий от графического ускорителя, исчезает необходимость развивать и поддерживать такие частичные расширения OpenGL. □ Предоставление независимости от графического ускорителя. Как уже говорилось ранее, первые попытки сделать графическую обработку программируемой закончились созданием интерфейсов на языке ассемблера. Разработчикам довольно сложно идти по этому пути, так как тогда программы будут непереносимыми. Цель высокоуровневого языка шейдеров — обеспечить уровень абстракции, достаточный для переносимости; и производители гра- фических ускорителей используют гибкость этого языка, чтобы внедрять но- вейшие архитектуры аппаратного обеспечения и технологии компиляции. □ Увеличение производительности. На сегодняшний день технология компиляции такова, что позволяет генери- ровать хорошо оптимизированный по скорости выполнения программный код. Процессоры стали настолько сложными, что трудно написать вручную хотя бы такой же быстрый код, как сгенерированный компилятором. Объектный код, сгенерированный для шейдера, должен быть независим от других состоя- ний OpenGL, поэтому перекомпиляция или управление несколькими копия- ми объектного кода не требуется. □ Обеспечение легкости использования. Написание шейдеров должно быть по возможности простым и легким. Так как большинство программистов графических приложений привыкли к С и C++, наиболее выразительные средства этих языков послужили базой для языка шейдеров OpenGL. При этом наиболее сложные фрагменты кода должен со- здавать компилятор. Поэтому язык для всех программируемых процессоров, в том числе и будущих, должен быть один (с очень небольшими различиями), и очень простой. Это позволит программистам быстрее обучаться и затем при- менять этот язык для всех задач, где необходимо программирование процессо- ров, определенных в OpenGL. □ Обеспечение актуальности языка в будущем. При создании языка шейдеров OpenGL были приняты во внимание ранее создан- ные языки, такие как С и RenderMan. Разработчики языка шейдеров OpenGL надеются, что первые программы, написанные на нем, будут актуальными и че- рез десять лет. Долговечность требует тщательной стандартизации, поэтому
2.4. Общее представление о языке 67 много усилий было потрачено на приведение стандарта к виду, удовлетворяю- щему как производителей аппаратного обеспечения, так и OpenGL ARB. □ Невмешательство в более высокие уровни параллельной обработки. Новые архитектуры графических ускорителей позволяют выполнять парал- лельную обработку как вершин, так и фрагментов, и чем новее модели, тем больше возможностей распараллеливания. Язык шейдеров OpenGL проекти- ровался тщательно, с учетом возможного распараллеливания обработки на более высоких уровнях. На определение языка это повлияло не сильно, но из- менения были важными. □ Обеспечение легкости разработки программ. Некоторые возможности языка С усложнили создание оптимизирующих ком- пиляторов для него. Язык шейдеров OpenGL позволяет разрешить эту про- блему. Например, С дает возможность использовать альтернативные имена для ссылки на одну и ту же область памяти с помощью указателей; указатели мо- гут передаваться как аргументы функций, и, таким образом, одна и та же об- ласть памяти может именоваться по-разному в пределах одной программы. Эти потенциальные альтернативные имена препятствуют оптимизатору, усложня- ют программы и делают код хуже оптимизируемым. Язык шейдеров OpenGL не поддерживает указатели и избегает альтернативных имен, используя пере- дачу параметров по значению. В целом отсутствие альтернативных имен об- легчает работу оптимизатора. 2.4.2. Основы С Как говорилось ранее, язык шейдеров OpenGL основан на синтаксисе языка про- граммирования ANSI С, и на первый взгляд его программы выглядят очень похо- жими на программы, написанные на языке С. Это было сделано умышленно, так как обычно разработчики графических программ используют С и/или C++, и им будет легче изучать и использовать язык шейдеров OpenGL. Основная структура программы на языке шейдеров OpenGL аналогична струк- туре С-программы. Точкой входа группы шейдеров является функция void main О, тело которой находится внутри фигурных скобок. Константы, идентификаторы, операторы, выражения и предложения - понятия, идентичные для языка шейде- ров и языка С. Организация потока в циклах, 1 f-then-el se и вызовы функций в языке шейдеров практически такие же, как в С. 2.4.3. Дополнения к языку С В языке шейдеров OpenGL есть множество возможностей, которые отражают его специализированность как языка для реализации графических алгоритмов. Да- лее перечислены возможности языка шейдеров OpenGL, которых нет в ANSI С. Векторные типы здесь поддерживаются для чисел с плавающей запятой, це- лых и булевых значений. Для чисел с плавающей запятой они называются vec2 (два числа), vec3 (три числа), vec4 (четыре числа). С этими типами так же легко вы- полнять арифметические операции, как и со скалярными величинами. Чтобы сло- жить векторы v и v , нужно просто написать: vl + v2. К отдельным элементам
68 Глава 2. Основы вектора можно получить доступ либо с помощью индексации (как в массиве), либо по именованным полям (как в структуре). Значения цвета можно получить так: первое значение — добавляя . г к имени вектора, второе — добавляя . д, третье — добавляя .Ь, четвертое — добавляя .а. Значения координат доступны по именам .х, .у, .Z, .w, а текстурные значения — по именам .5, -t, .р, . q. Можно получить сразу несколько компонентов, указав их имена подряд, например .ху. В число базовых типов входят также табличные (матричные) типы для чисел с плавающей запятой. Тип данных mat2 обозначает таблицу 2x2 из чисел с плава- ющей запятой, гпаРЗ — таблицу 3x3, mat4 — таблицу 4x4. Это удобный тип для вы- полнения линейных преобразовании, используемых в ЗЭ-графике. В матрице можно выбрать столбцы с помощью индексации, как для массива, и получить век- тор, с которым можно работать, как описано ранее. Для того чтобы шейдеры могли легко работать с текстурной памятью, был создан набор базовых типов — дискретизаторов. Дискретизатор — это специальный тип переменной, применяемый для доступа к конкретной текстурной карте. Перемен- ная типа samplerlD используется для доступа к одномерной текстурной карте, пе- ременная типа samp! er2D — для доступа к двухмерной текстурной карте и т. д. Тек- стуры глубины и кубические текстуры этим механизмом также поддерживаются. Для управления входными и выходными данными шейдеров существуют осо- бые спецификаторы. Спецификаторы attribute, uni form и varying определяют вид переменной. В attribute-переменных хранятся часто изменяющиеся значения. Они служат для передачи данных из приложения вершинному шейдеру. Uniform-пе- ременные хранят редко изменяющиеся значения и служат для передачи данных из приложения любому шейдеру. Varying-переменные передают интерполирован- ные значения от верщршного шейдера фрагментному. Шейдеры, написанные на языке шейдеров OpenGL, используют встроенные переменные, которые начинаются с зарезервированного префикса gl_, для доступа к состояниям OpenGL и постоянной функциональности OpenGL. Например, и вер- шинные, и фрагментные шейдеры могут использовать встроенные переменные, содержащие состояния текущего контекста рендеринга. Например, это переменная gl _Model Vi ewMatri x для получения текущей матрицы модели-вида, gl JJ ghtSourcef i ] — для получения текущих параметров 1 -го источника света, gl Fog. col or — для досту- па к текущему цвету дымки. Вершинный шейдер должен занести нужное значе- ние в переменную gl Position, чтобы передать данные для стандартной обработки между вершинным и фрагментным шейдерами, то есть для сборки примитивов, отсечения, отбраковки и растеризации. Фрагментный шейдер обычно устанавли- вает другие переменные, gl _FragCol or и g1_FragDepth, или одну из них. Они содер- жат значения вычисленных цвета и глубины фрагмента и используются в оконча- тельных операциях стандартной обработки — проверке прозрачности, отсечении по шаблону, проверке глубины, — перед тем как попасть в буфер кадров. Язык шейдеров OpenGL предоставляет также множество встроенных функ- ций, чтобы сделать кодирование легким и лучше использовать возможности гра- фических акселераторов для конкретных операций. Язык определяет встроенные функции для различных тригонометрических операций (синус, косинус, тангенс и Др.), операций степени (нахождение степени, экспоненты, логарифма, корня квадратного, обратного корня квадратного), общих математических операций (ок- ругление до меньшего, округление до большего, определения абсолютного зпаче-
2.4. Общее представление о языке 69 ния, дробной части, модуля числа и т. д.), геометрических операций (нахождение длины, расстояния, скалярного произведения, векторного произведения, норма- лизация пт.д.), операций отношений для векторов (покомпонентное «больше», «меньше», «равно» и т. д.), специализированные функции фраг.ментного шейде- ра для вычисления производных и оценки ширины фильтра для сглаживания, функции доступа ктекстурной памяти и функции, возвращающие значения шума для текстурных эффектов. 2.4.4. Дополнения из C+ + Язык шейдеров OpenGL заимствовал несколько необходимых возможностей из C++. Например, в нем поддерживается перегрузка функций, которая облегчает определение функций, различающихся только типом передаваемых аргументов. Многие встроенные функции языка шейдеров OpenGL перегружены. Например, функция для вычисления скалярного произведения перегружена для типов fl oat, vec2, vec3 и vec4. Идея конструкторов тоже пришла из C++. Инициализировать данные в языке шейдеров OpenGL одним или несколькими способами можно толь- ко с помощь to конструкторов. Другая возможность, позаимствованная из C++, — то, что переменные могут быть объявлены непосредственно там, где они будут использоваться, а не в нача- ле блока. Поддерживается тип bool. Функции должны быть объявлены перед ис- пользованием: либо определением (телом функции), либо прототипом. 2.4.5. Не поддерживаемые возможности С В отличие от ANSI С в языке шейдеров OpenGL нет неявного приведения типов. Если в выражении будут использованы переменные различных типов, компиля- тор воспримет это как ошибку. Например, выражение float f - 0; ошибочно, a float f = 0.0: — правильно. Это может показаться неудобным, зато упрощает язык, так как не нужно задавать и проверять правила приведения типов. Это также из- бавляет язык от неоднозначности при вызове функции, перегруженной по типу аргументов. Язык шейдеров OpenGL не поддерживает указатели, строки или символы и лю- бые операции с ними. Это язык для работы с числовыми, а пе строковыми данны- ми, и нет смысла усложнять его. Не поддерживаются здесь и числа с плавающей запятой двойной точности, а также короткие и длинные целые, беззнаковые — чтобы пе обременять компилятор и графический ускоритель. Для упрощения языка шейдеров OpenGL в нем отсутствуют также union, enum, битовые поля и побитовые операторы. Так как язык не файловый, не будет ника- ких директив #i nd ude или других ссылок на имена файлов. 2.4.6. Другие отличия В некоторых случаях язык шейдеров OpenGL предоставляет ту же функциональ- ность, что и язык С, по действия выполняются по-другому. Например, конструкто- ры, а не операторы приведения используются для приведения типов; конструкто- ры, а не инициализация в стиле С используются для инициализации переменных. В языке не существует неявного приведения типов, и конструкторы помогают
70 Глава 2. Основы сохранять типовую безопасность языка. В конструкторах сохраняется синтаксис вызова функции, где имя функции — это название нужного типа и аргументы — значения, используемые для инициализации. Конструкторы предоставляют гораздо более богатый выбор операций, чем про- стое приведение типов или инициализация в стиле С, и эта гибкость делает еще более удобной работу с векторными и матричными типами данных. Например, конструктор vec3(1.0. 2.0. 3.0) создает переменную типа vec3 из трех скалярных значений, а конструктор vec3(myVec4) отбрасывает четвертый компонент из myVec4, чтобы создать значение типа vec3. Еще одно существенное отличие состоит в том, что все входные и выходные па- раметры функций передаются по значению, а не по указателю, как это возможно в С. В языке шейдеров OpenGL входные параметры копируются в функцию сразу после вызова, а выходные параметры копируются обратно перед выходом из нее. Так как функция работает только с копиями параметров, она не имеет доступа к внешним данным, которые, в свою очередь, не получают дополнительных псев- донимов. Входные параметры функции обозначаются спецификатором 1 г, выход- ные — спецификатором out, а входные и выходные одновременно — специфика- тором inout (если не указано конкретно, параметры считаются входными). 2.5. Обзор системы В предыдущем разделе были описаны некоторые элементы OpenGL, которые по- зволяют приложениям программировать графические ускорители. В следующем разделе рассматривается их совместная работа. 2.5.1. Модель драйвера Часть программного обеспечения, управляющая какой-либо аппаратурой и пре- доставляющая доступ к ней, называется драйвером. OpenGL — тоже драйвер: он предоставляет доступ к графическому ускорителю. Некоторые задачи OpenGL должны контролироваться или управляться средствами операционной системы. OpenGL-шейдеры работают в среде выполнения программ OpenGL (рис. 2.4). Приложения передают команды OpenGL, вызывая функции, являющиеся частью OpenGL API. Новая функция OpenGL, gl CreateShaderObjectARB, позволяет прило- жениям выделить внутри драйвера OpenGL структуры данных, которые будут использованы для OpenGL-шейдера. Эти структуры данных называются шейдер- ные объекты. После создания шейдерного объекта приложение должно установить исходный код шейдера вызовом функции gl ShaderSourceARB, в которую передают- ся символьные строки с исходным кодом шейдера. Компилятор языка шейдеров на самом деле является частьго OpenGL-драйвера (см. рис. 2.4). Это одно из суще- ственных отличий язьгка шейдеров OpenGL от других шейдерных языков, напри- мер Стэнфордского языка шейдеров, HLSL' (от Microsoft) или Cg (от NVIDIA). В этих языках компилятор высокоуровневого языка шейдеров находится на уро- вень выше графического API и преобразует код в последовательность вызовов 1 High-Level Shader Language — высокоуровневый язык шейдеров. — Примем, персе.
2.5, Обзор системы 71 графического интерфейса, находящегося уровнем ниже. (Более подробное описа- ние читатель найдет в главе 17.) При использовании языка шейдеров OpenGL ис- ходный код шейдеров передается драйверу OpenGL, и для данного сочетания аппа- ратного и программного обеспечения шейдеры будут скомпилированы в присущий данной среде машинный код настолько эффективно, насколько это возможно. Графический акселератор — От разработчика приложения ... От производителя аппаратного обеспечения Рис. 2.4. Модель выполнения OpenGL-шейдеров После загрузки кода шейдера в шейдерный объект OpenGL-драйвера его необхо- димо скомпилировать функцией gl Compl 1 eShader ARB. Программный объект — это струк- тура данных, управляемая OpenGL. Она служит вместилищем шейдерных объектов. Приложения должны связывать шейдерные объекты с программным объектом ко- мандой gl AttachObjectARB. При таком связывании скомпилированные шейдерные объекты могут быть скомпонованы вместе функцией gl Li nkProgramARB. Это одно из основных различий между языком шейдеров OpenGL и API, использующими уро- вень ассемблера, например, таким расширениями OpenGL, как ARB_vertex_program и ARB_f ragment_program — несколько разных шейдеров можно скомпоновать в одну про- грамму. Для более сложных задач использование нескольких отдельно скомпилиро- ванных шейдерных объектов может оказаться предпочтительнее одного цельного блока уже скомпонованного кода.
72 Глава 2. Основы На этапе компоновки разрешены внешние ссылки между шейдерами, прове- ряется совместимость вершинного и фрагментного шейдеров, выделяется память для uniform-переменных и т. д. В результате получается одна или несколько вы- полняемых программ, которые могут быть инсталлированы как часть текущего состояния OpenGL функцией gl UsePragramObJectARB. Эта команда загружает ис- полняемые программы как в вершинный, так и во фрагментный процессор, где они используются для рендеринга всех последующих графических примитивов. 2.5.2. Компилятор и компоновщик языка шейдеров OpenGL Исходный код шейдера — это массив строк, состоящих из символов. Каждая сим- вольная строка может состоять из нескольких обычных строк, разделенных сим- волом-признаком новой строки. Впрочем, в символьной строке вполне может не быть ни одного такого признака новой строки, а одна обычная строка может быть сформирована из нескольких символьных — когда OpenGL соединяет символь- ные строки в шейдер, он не вставляет никаких дополнительных символов. Толь- ко от разработчика зависит, будет ли исходный код шейдера содержать символы перевода строки между предложениями. Сообщения об ошибках и предупреждения, которые появляются после компи- ляции шейдера, должны указывать и номер обычной строки в символьной строке, и символьную строку, в которой появились ошибка или предупреждение. Нуме- рация строк последовательная и начинается с 0. При грамматическом анализе кода номер текущей строки на единицу больше, чем количество уже обработанных строк. Препроцессор компилятора языка шейдеров OpenGL выпущен компанией 3Dlabs как программный продукт с открытым исходным текстом, и кто угодно может использовать его для написания собственного компилятора. Этот препро- цессор может выполнять лексический разбор исходного кода языка шейдеров OpenGL, выдавая последовательность лексем, а затем выполнять синтаксический и семантический анализ лексем для создания двоичного высокоуровневого пред- ставления языка. Этот препроцессор работает как «ссылка па реализацию» языка шейдеров OpenGL и, следовательно, тесно взаимодействует со спецификацией языка. Еще одно преимущество его открытости в том, что синтаксис и семантика шейдеров будут проверяться одинаково во всех реализациях, использующих этот препро- цессор. Большая согласованность разных реализаций компилятора облегчает на- писание шейдеров для разработчиков, которые могут работать с ними. Предполагается, что вторая часть компилятора языка шейдеров OpenGL будет реализована по-разному на разных платформах. Она будет получать высокоуров- невое представление от препроцессора и выдавать оптимизированный машинный код для конкретной аппаратной базы. Производители аппаратного обеспечения реализуют это таким образом, чтобы устанавливалось соответствие между высо- коуровневым представлением и соответствующими машинными командами их аппаратного обеспечения. Сборка модулей также зависима от аппаратной базы, так как включает в себя зависимые операции, например размещение переменных в физической памяти аппаратного обеспечения.
2.5. Обзор системы 73 В конечном итоге, производители графических ускорителей будут реализовы- вать большую часть компилятора и компоновщика языка шейдеров OpenGL. Это программное обеспечение будет включаться в стандартный пакет от производи- теля вместе с Open GL-драйвером. 2.5.3. Расширения OpenGL API До последней версии OpenGL функциональность языка OpenGL не включалась в стандарт, а была доступна scat; набор расширений, поддерживаемых различными производителями графических акселераторов. Ожидается, что эта функциональ- ность приживется в стандарте, и можно будет убрать суффикс ARB из имен функ- ций, констант и типов данных этих расширений. Расширение для шейдеров, программных объектов и функциональности, об- щей для всех программируемых процессоров, называется ARB_shader_objects. Это расширение включает в себя следующие компоненты: □ gl СгеаteShaderObjectARB — создание шейдерного объекта; □ glCreateProgramObjectARB - создание программного объекта; □ gl Del eteObjectARB — удаление шейдерного или программного объекта; □ gl ShaderSourceARB — загрузка строк исходного кода в шейдерный объект; □ gl Compi 1 eShaderARB — компиляция шейдера; □ gl AttachObjectARB — прикрепление шейдерного объекта к программному объекту; □ gl DetachObjectARB — отсоединение шейдерного объекта от программного объекта; □ gl Li nkProgramARB — компоновка программного объекта для создания выполня- емого кода; □ gl UseProgramObjectARB — установка выполняемого кода в качестве текущего со- стояния; □ gl Vai IdateProgramARB — проверка правильности программного объекта; □ glUnlformARB — установка значения uniform-переменной; □ gl GetActi veUn 1 formARB — получение имени, размера и типа активной uniform- переменной программного объекта; □ gl GetAttachedObjectsARB — получение списка присоединенных к программному объекту шейдерных объектов; □ gl GetHandl eARB — получение дескриптора используемого в данный момент про- граммного объекта; □ gl GetObjectРагаmeterARB — получение параметров объекта; □ gl GetShaderSourceARB — получение исходного кода заданного шейдерного объекта; □ gl Get Um formARB — получение текущего значения uniform-переменной; □ gl GetUm formLocation ARB — запрос назначенного компоновщиком расположения uniform-переменной; □ glGetIrfoLogARB — получение информационного журнала записей (лога) для шейдерного или программного объекта.
74 Глава 2. Основы Другое расширение, ARB_vertex_shader, позволяет использовать возможности но- вого, недавно определенного программируемого вершинного процессора. Это рас- ширение определяет место этого процессора в общей схеме обработки графики OpenGL и предоставляет API для уникальных возможностей вершинного про- цессора. Это расширение включает в себя: □ создание вершинных шейдеров; □ блокирование/разблокирование вершинных шейдеров; □ определение того, какая стандартная функциональность OpenGL недоступна при активном вершинном шейдере; □ передачу значений, предназначавшихся для стандартной обработки, в вершин- ный шейдер; □ обработку настраиваемых атрибутов вершин; □ взаимодействие вершинного шейдера со стандартной функциональностью — сборка примитивов, отсечение, растеризация. В ARB_vertex_shader определены следующие функции: □ gl VertexAttrl bARB — установка настраиваемых атрибутов вершин, вершина за раз; □ gl VertexAttrlbPointerARB — формирование вершинных массивов, установка рас- положения и структуры настраиваемых атрибутов вершин; □ gl Bi nd At t гд bLocati onARB — установка индекса настраиваемого атрибута вершины для определенной разработчиком переменной-атрибута в вершинном шейдере; □ gl ЕпаЫ eVertexAtt ri bArrayARB — включение режима вершинных массивов для пе- редачи настраиваемых атрибутов вершин OpenGL; □ gl Di sabl ©VertexAttrl bArrayARB — выключение режима вершинных массивов для передачи настраиваемых атрибутов вершин OpenGL; □ gl GetVertexAttri bARB — возврат текущего состояния заданного настраиваемого атрибута вершины; □ gl Get Vert exAtt гд bLocati onARB — возврат индекса настраиваемого атрибута вер- шины, связанного с определенной разработчиком переменной атрибута; □ gl GetVertexAttribPointerARB — возврат значения указателя вершинного масси- ва для определенного настраиваемого атрибута вершины; □ glGetActi veAttri bARB - получение имени, размера и типа активного атрибута программного объекта. Третье и последнее расширение, поддерживающее язык шейдеров OpenGL, — ARB_fragment_shdder. Оно похоже на ARB_vertex_shader, за исключением того, что определяет возможности нового программируемого фрагментного, а не вершин- ного процессора и его место в процессе обработки изображения в OpenGL. Это расширение не предоставляет дополнительного API, так как построено на настраи- ваемых возможностях расширения ARB_shader_objects. Тем не менее оно предо- ставляет следующую функциональность: □ создание фрагментных шейдеров; □ блокирование/разблокирование фрагментных шейдеров;
2.6. Основные преимущества 75 □ определение того, какая стандартная функциональность OpenGL недоступна при активном фрагментном шейдере; □ передача значений, полученных при растеризации, фрагмснтному шейдеру; □ взаимодействие фрагментного шейдера со стандартной функциональностью — окончательной обработкой изображения. Все новые функции более подробно описаны в главе 7. Полный список функ- ций этих расширений представлен в приложении Б в конце книги. 2.6. Основные преимущества При создании языка шейдеров OpenGL приходилось делать выбор за или против каких-либо факторов. В результате он приобрел следующие преимущества. □ Тесная интеграция с OpenGL. Язык шейдеров OpenGL был спроектирован для использования совместно с OpenGL. Специально предусмотрено, что практически любое OpenGL-прило- жение можно легко изменить, чтобы иметь возможность использовать новей- шие программируемые возможности графического ускорителя. Дополнитель- ным удобством языка является то, что он основан на существующей архитектуре OpenGL, использует хорошо знакомые разработчикам API-функции и имеет встроенные возможности доступа к внутреннему состоянию OpenGL. □ Компиляция во время выполнения. Исходный код шейдера хранится в первоначальном, легко поддерживаемом виде и компилируется только при необходимости. Приложение передает ис- ходный код в OpenGL-реализацию, и там код компилируется и выполняется. Нет необходимости поддерживать много двоичных выполняемых файлов для разных платформ1. □ Независимость от языка ассемблера разных производителей. И DirectX и OpenGL поддерживают интерфейсы языка ассемблера для програм- мируемости графики. Высокоуровневые языки шейдеров могут быть (и были) построены на базе этих интерфейсов языка ассемблера, и можно транслиро- вать программу на высокоуровневом языке в программу на языке ассемблера вообще без OpenGL или DirectX. Это дает некоторые преимущества, но, если полагаться на интерфейс языка ассемблера как основной, осложняет работу проектировщиков графических акселераторов и затрудняет нововведения. Проектировщики, использующие выразительный язык высокого уровня, а не ограничивающий язык ассемблера, имеют больше шансов получить выигрыш 1 В момент написания книги OpenGL ARB нес еще предполагает, что необходимо также альтернатив- ное представление шейдеров в другой, нежели исходный код, форме. Главными целями такого пред- ставления являются зашита интеллектуальной собственности и улучшение производительности за счет частичной предварительной компиляции. Когда будстопрсдслсно такое API. шейдеры могут стать непереносимыми между платформами, зато разработчики приложений будут иметь возможность за- щитить свой код и улучшить производительность.
76 Глава 2. Основы в производительности. Слишком рано думать о стандарте ассемблерного языка для программирования графики. С другой, стороны, язык С был придуман за- долго до современных ассемблерных языков для процессоров, и он все еще актуален для разработки программ. □ Неограниченные возможности для оптимизации компилятора, обеспечива- ющие оптимальную производительность на различных платформах. Компиляторы гораздо лучше и быстрее людей делают код эффективным. Если компилировать исходный код с помощью OpenGL, а не вне его, выполняет- ся оптимизация кода и увеличивается его производительность. Фактически, усовершенствовать компиляторы можно с каждой новой версией драйвера OpenGL, а приложения не должны будут ни менять код, ни перекомпилиро- вать или перекомпоновывать его. Более того, текущие интерфейсы языка ас- семблера основаны на строках, так что строковый высокоуровневый исходный код должен быть преобразован в строковый код языка ассемблера, и этот про- межуточный код должен передаваться в OpenGL и преобразовываться там в ма- шинный код. □ Открытый межплатформенный стандарт. За исключением языка шейдеров OpenGL, нет других шейдерных языков, явля- ющихся частью открытого межплатформенного стандарта. Как и сам OpenGL, язык шейдеров OpenGL может быть реализован разными производителями на различных платформах. □ Один высокоуровневый язык используется для всей программируемой це- почки обработки графики. Язык шейдеров OpenGL используется для написания как вершинных, так и фрагментныхшейдеров для OpenGL, эти процессы различаются незначитель- но. В будущем предполагается, что язык шейдеров будет использоваться для программирования и других графических операций в OpenGL. Например, уже обсуждалась программируемость сжатия и извлечения изображений произ- вольных форматов и мозаичного представления поверхностей. □ Модульное программирование. Разделение компиляции и компоновки на два отдельных шага - удачное реше- ние: при разработке программ для сложных алгоритмов проявляется большая гибкость. Вместо того чтобы создавать один большой шейдер, разработчики могут сделать целую коллекцию небольших шейдеров, их можно компилиро- вать независимо и затем прикреплять к программному объекту. Шейдеры будут создаваться на базе общих интерфейсов и, таким образом, станут взаимозаме- няемыми, а операция компоновки соединит их в программу. □ Отсутствие дополнительных библиотек или программ. Язык шейдеров OpenGL, компилятор и компоновщик определены как часть OpenGL. Приложение не заботится о загрузке дополнительных библиотек. А усовершенствование компилятора происходит с установкой обновлений драйвера OpenGL.
27. Итоги 77 2.7. Итоги Следующие выводы помогут понять, как все части взаимодействуют на этапе вы- полнения: □ Выполняемая программа для вершинного процессора, установленная как часть текущего состояния, будет выполняться один раз для каждой заданной вер- шины. □ Выполняемая программа для фрагментного процессора, установленная как часть текущего состояния, будет выполняться один раз для каждого фрагмен- та, который приходит с этапа растеризации. □ Есть два способа сообщения между приложением и вершинным шейдером: через attribute- и uniform-переменные. □ Attribute-переменные могут изменяться часто: приложение устанавливает но- вое значение для каждой новой вершины. □ Приложения передают произвольные данные вершин в вершинный шейдер че- рез определенные разработчиком attribute-переменные. □ Приложения передают стандартные данные вершин (цвет, нормаль, коорди- наты текстуры, расположение в пространстве и т. д.) в вершинный шейдер че- рез встроенные attribute-переменные. □ Приложения могут передавать данные во фрагментный шейдер через uniform- переменные. □ Uniform-переменные изменяются довольно редко — как минимум для целого графического примитива значение всегда будет оставаться неизменным. □ Компилятор и компоновщик встроены в OpenGL (но средства для компиля- ции, компоновки и отладки шейдеров могут существовать сами по себе). Итак, далее описаны самые важные особенности языка шейдеров OpenGL: Язык основан на синтаксисе языка С. Основные элементы и многие ключевые слова заимствованы из С. Векторы и матрицы добавлены как базовые типы. * Появились новые типы attribute, uniform, varying, которые описывают пе- ременные для управления вводом/выводом шейдера. Тип attribute используется для передачи часто изменяемых значений от приложения к вершинному шейдеру. Тип varying используется для передачи промежуточных данных от вершин- ного шейдера фрагментному шейдеру. Тип uni form используется для передачи нечасто изменяемых данных от при- ложения к вершинному и фрагментному шейдерам. • Добавлен дискретизатор для доступа к текстурам. Существует доступ к состоянию OpenGL и стандартной функциональнос- ти через встроенные переменные.
78 Глава 2. Основы Включено множество встроенных функций для выполнения графических операций, Из языка C++ заимствованы обязательность объявления функций и пере- грузка функций по количеству и типу аргументов. Переменные можно объявлять при необходимости, а не в начале блока. Чтобы установить и использовать шейдеры OpenGL, нужно проделать следу- ющее: 1, Создать один или несколько пустых шейдерных объектов функцией gl Create- ShaderObjectARB. 2. Задать исходный код шейдеров функцией gl ShaderSourceARB. 3. Скомпилировать каждый шейдер функцией gl Compi 1 eShaderARB. 4. Создать программный объект функцией gl CreateProgramObject ARB. 5. Присоединить все созданные шейдерные объекты к программному объекту функцией gl AttachOb jectARB. 6. Скомпоновать программный объект функцией gl Li nkProgramARB. 7. Установить выполняемую программу как часть текущего состояния OpenGL функцией gl UseProgramObjectARB. Все следующие графические примитивы будут нарисованы с помощью этих шейдеров, а не с использованием стандартных операций OpenGL. 2.8. Ссылки Чтобы лучше разобраться в предмете, просто продолжайте изучать книгу. А за техническими подробностями можно обратиться к спецификации. На веб-сайте компании 3 Diabs можно найти дополнительную информацию — презентации, де- монстрационные программы, примеры шейдеров и их исходный код, 1, 3Dlabs. Веб-сайт для разработчиков (http://www.3dlabs.com/support/developer). 2. Kessenich J„ Baldwin D., Rost R, The OpenGL Shading Language, Version 1.051/ 3Dlabs. 2003 (http,7/www.3dlabs,com/support/devetoper/ogl2). 3, OpenGL Architecture Review Board: спецификация расширения ARB_vertex_sha- der, реестр расширений OpenGL (http://oss.sgi.conn/projects/ogl-sample/registry). 4. OpenGL Architecture Review Board: спецификация расширения ARB_fragment_sha - der, реестр расширений OpenGL (http://oss,sgi.com/projects/ogl-sample/registry). 5. OpenGL Architecture Review Board: спецификация расширения ARB_shader_ob- jects, реестр расширений OpenGL (http://oss.sgi.com/projects/ogl-sampte/regis- try).
Определение языка1 В этой главе представлены основные возможности языка шейдеров OpenGL. Сна- чала будет приведен небольшой пример работы в ларе вершинного и фрагмент- ного шейдеров, демонстрирующий их структуру и интерфейсы. Каждая особен- ность языка будет обсуждаться отдельно. Синтаксис языка шейдеров OpenGL заимствован у семейства языков программи- рования С. Лексемы, идентификаторы, точки с запятыми, обозначение вложения блоков фигурными скобками, структурная схема и многие ключевые слова выгля- дят так же, как в языке С. Доступны оба вида комментариев,.// ... и /* ... */. Но многие особенности отличаются от используемых в С, и в этой главе все серьезные отличия будут описаны. Каждый пример шейдера представлен в виде, в котором он может появиться в файле или на экране. Но на самом деле шейдеры передаются в OpenGL в виде строк, а не файлов. 3.1. Примеры шейдеров Обычно программа содержит два шейдера: вершинный и фрагментный. На самом деле шейдеров каждого типа может быть больше, но для всех шейдеров каждого типа может использоваться только одна функция main, поэтому обычно все-таки ограничиваются одним шейдером каждого типа. Далее приведена простая пара вершинного и фрагментного шейдеров, которая может равномерно выразить температуру цветом. Амплитуда температурных зна- чений и их цветов параметризована. Первый фрагмент относится к вершинному шейдеру. Он выполняется один раз для каждой вершины: // uniform-переменные, изменяемые не чаще, чем при смене примитива uniform float CoolestTemp; uniform float TempRange; // attribute-переменные, обычно меняются для каждой вершины attribute float VertexTemp; // varying-переменные передают значения // из вершинного шейдера фрагментному varying float Temperature: void tnalnO 1 Автор этой главы — Джон Кэссенич.
80 Глава 3. Определение языка // вычисляется температуре для интерполирования во фрагментах // в диапазоне [0.0. 1.0] Temperature = (VertexTemp - CoolestTemp) / TempRange; /* Размещение вершины, установленное функцией glVertexO. можно прочесть из встроенной переменной glVertex. Растеризатору для определения положения вершины нужно знать и это значение, и текущую матрицу модели-вида. */ gl_Pos1tlon = glModelViewProjectionMatrlх * gl_Vertex; Это код вершинного шейдера. Затем выполняется сборка примитивов, после ко- торой растеризатор получает достаточно информации для создания фрагментов. Растеризатор интерполирует значения Temperature в вершинах и создает значения для фрагмента. После этого каждый фрагмент передается фрагментному шейдеру: // uni form-переменные меняются не чаще, чем для каждого примитива // vec3 объявляет вектор из трех чисел с плавающей запятой1 uniform vec3 CoolestColor; uniform vec3 HottestColor: // Temperature содержит интерполированное для фрагментов значение // температуры, установленное вершинным шейдером varying float Temperature; void main О { И получить цвет из промежутка между самым холодным и самым теплым // с помощью встроенной функции ттпх() vec3 color = mixfCoolestColor. HottestColor, Temperature): // Создать вектор из 4 чисел с плавающей запятой, добавив значение // прозрачности, равное 1.0, и установить фрагменту зтот цвет gl_FragColor = vec4(color. 1.0); } Оба шейдера получают данные от приложения через объявленные uniform- переменные. Вершинный шейдер получает информацию о каждой вершине через attribute-переменную. Из вершинного шейдера фрагментному шейдеру инфор- мация передается через varying-переменные, объявления которых должны совпа- дать в обоих шейдерах. Стандартная функциональность, расположенная между вершинным и фрагментным процессорами, интерполирует вершинные значения из varying-переменной. Фрагментный шейдер читает уже интерполированные значения из той же varying-переменной. Шейдеры взаимодействуют со стандартной функциональностью OpenGL через встроенные переменные, которые начинаются с префикса д!_. В предыдущих приме- рах запись в переменную gl_Posit1 on сообщает OpenGL, где размещены преобразо- ванные вершины, а запись в переменную gl_FragCol or обозначает цвет фрагмента. Эти шейдеры выполняются много раз при обработке одного примитива: вершин- ный шейдер по одному разу для каждой вершины, фрагментный шейдер — по одному разу для каждого фрагмента. Причем большую часть времени вершинный В тексте программ десятичным разделителем является точка, по у пас принято этот тип называть имен- но так. — При.иеч. науч. ред.
12. Типы данных 81 ифрагментный шейдеры могут работать параллельно. По большому счету, не оп- ределена ни последовательность, ни связь таких выполнений шейдеров. Инфор- мация не может передаваться от вершины к вершине и от фрагмента к фрагменту. 1.2. Типы данных Впримерах, приведенных в предыдущем разделе, использовались векторы из чи- сел с плавающей запятой. Упрощению графических алгоритмов в языке шейде- ров OpenGL способствует доступность многих других встроенных типов данных: булевых, целых, матриц, векторов других типов, структур и массивов. В этом раз- деле будет описан каждый из типов. Бросается в глаза отсутствие строковых и сим- вольных типов: они не используются при обработке вершин и фрагментов. 3.2.1. Скалярные типы В языке шейдеров OpenGL доступны следующие скалярные типы: □ float — объявляет число с плавающей запятой; □ int — объявляет целое число; □ bool — объявляет булеву переменную. Эти типы используются для объявления переменных таким же способом, как в С и C++: float f; float g. h = 2.4: int NumTextures - 4; bool skipProcessing; В отличие от языка С здесь нужно обязательно указывать имя типа — не суще- ствует типа по умолчанию. Как и в C++, объявлять переменные можно в любой части кода, а не только после открытой фигурной скобки {. Как и в языке С, числа с плавающей запятой здесь можно объявлять с помо- щью специальных символов (хотя суффиксы для обозначения точности отсут- ствуют, так как в языке шейдеров всего один числовой тип с плавающей запятой); 3.14159 3. 0.2 .609 1.5е10 0.4Е-4 etc. В целом операции над числами с плавающей запятой выполняются так же, как ив языке С. Не следует ожидать, что в языке шейдеров OpenGL и операции над целыми числами будут выполняться так же, как в С, так как здесь не предъявляется тре- бование, чтобы при вычислениях они помещались в соответствующий регистр ограниченной разрядности. Не определено, что случится с числом при перепол- нении или исчезновении значащих разрядов, так как этого не случается. Не под- держиваются и битовые операции - сдвиг влево («), битовое «И» (&), а также все подобные им операции.
82 Глава 3. Определение языка Гарантировано, что целое число может быть не меньшей, чем 16 бит точности; оно может быть положительным, отрицательным, нулем; арифметические опера- ции, аргументы и результат которых не превышают заданной точности, всегда дают ожидаемый результат. Точность здесь подразумевает использование 16 бит и знака числа, так что полный диапазон может быть [-65535, 65535] или даже больше. Целые числа в языке шейдеров OpenGL, как и в языке С, могут быть представ- лены в десятичном, восьмеричном или шестнадцатеричном виде. 42 И представление десятичного целого 052 // представление восьмеричного целого 0х2А // представление целого в шестнадцатеричном виде Для целых чисел тоже не используются суффиксы, так как целочисленный тип — единственный. Целые числа служат для обозначения размеров структур или массивов или используются в качестве счетчиков. А графические величины, например цвет или координаты, представлять в шейдере лучше всего числами с плавающей запятой. Здесь булевы переменные, как и в C++, называются bool. Они могут иметь одно из двух значений: «истина» или «ложь». Для их обозначения существует две кон- станты: true и false. Результатом операторов отношения, таких как «меньше» (<), и логических операторов, таких как «логическое И» (&&), всегда является логи- ческое значение. Конструкции языка типа; f—else принимают только выражения булева типа. В этом отношении язык шейдеров OpenGL ограничивает разработ- чиков больше, чем C++. 3.2.2. Векторы В языке шейдеров OpenGL есть встроенные типы векторов из чисел с плавающей запятой (float), целых (int) или булевых (bool). Существуют разные типы для двух-, трех- и четырехэлементных векторов: □ vec2 — вектор из двух чисел с плавающей запятой; □ v.ec3 — вектор из трех чисел с плавающей запятой; □ vec4 — вектор из четырех чисел с плавающей запятой; □ 1 vec2 — вектор из двух целых; □ 1 vec3 вектор из трех целых; □ i vec4 — вектор из четырех целых; □ bvec2 — вектор из двух булевых значений; □ bvec3 — вектор из трех булевых значений; □ bvec4 — вектор из четырех булевых значений. Встроенные векторы очень полезны — в них удобно хранить цвет, координаты вершин и текстур и т. п. Встроенные переменные и функции, использующие эти типы, делают их необходимыми. Для встроенных сложных типов поддерживают- ся операции над ними, и в графических ускорителях могут быть предусмотрены специальные возможности обработки векторов, например зеркальное отражение векторных выражений в шейдерах.
3.2. Типы данных 83 Компилятор не способен самостоятельно отличить вектор цвета или коорди- нат от вектора с другими значениями. С точки зрения языка все они — лишь век- торы чисел с плавающей запятой. Элементы вектора можно адресовать как по именам (как в структурах), так и по индексам (как в массивах). Например, пусть position — переменная типа vec3, она трактуется как вектор (г, у, а), и к первому элементу вектора можно обратить- ся position, х. Существует несколько видов адресации векторов по именам: □ х, у, z, w — вектор рассматривается как координаты или направление; □ г, д, Ь, а — вектор рассматривается как значение цвета; □ s, t, р, q — вектор рассматривается как координаты текстуры. При всем этом невозможно явно указать, что обозначает вектор — цвет, коор- динаты или что-нибудь еще. Скорее, эти имена придуманы для удобства и чита- бельности. При компиляции происходит единственная проверка — достаточно ли вектор велик для того, чтобы вмещать заданный компонент. Если выбрано не- сколько компонентов сразу (настройка по адресам, обсуждаемая в разделе 3.7.2), проверяется, что все указанные компоненты — из одной группы. К элементам вектора можно обращаться и по индексу. Например, posit1on[2] возвращает третий элемент вектора posi ti on. В качестве индекса можно подстав- лять значение переменной, что позволяет организовать цикл по компонентам век- тора. Операция умножения векторов существует и осуществляется так же, как линейное умножение обычных матриц. Настройка по адресам, индексирование и другие операции будут подробно рассматриваться в разделе 3.7. 3.2.3. Матрицы В языке существуют также встроенные типы матриц из чисел с плавающей запя- той — размером 2x2, 3x3 и 4x4: □ mat2 — матрица из чисел с плавающей запятой размером 2x2; □ mat3 — матрица из чисел с плавающей запятой размером 3x3; □ mat4 — матрица из чисел с плавающей запятой размером 4x4. В переменных этих типов можно хранить данные для линейных преобразова- ний или какие-либо другие. При выполнении операций над этими типами они всегда рассматриваются как математические матрицы, в частности, при перемно- жении вектора и матрицы выполняются правильные с математической точки зре- ния вычисления. Представлять матрицы в OpenGL принято по столбцам. Матрицу можно рассматривать также как массив столбцов-векторов: напри- мер, если переменная transform типа mat4, то transform[2] — третий столбец матрицы transform, и его тип будет vec4. Индекс первого столбца 0. Так как trans- form^] — вектор, а векторы являются и массивами, transform[3][l] — второй эле- мент вектора из четвертого столбца матрицы transform. Таким образом, мы можем рассматривать transform еще и как двухмерный массив. Следует лишь учитывать, что первый индекс выбирает столбец, а не строку, а уже второй индекс выбирает строку.
84 Глава 3. Определение языка 3.2.4. Дискретизаторы Чтобы получить нужную текстуру, нужно указать ее и/или ее текстурный мо- дуль. В стандарте OpenGL не определено, как и в каком виде будут реализова- ны текстурные модули или другие способы организации хранения текстур. Вме- сто этого OpenGL предоставляет некий абстрактный «черный ящик» для доступа к текстуре. Такие «черные ящики» называются дискретизаторами (семпле- рами). Типы доступных дискретизаторов: □ sampl erlD — предоставляет доступ к одномерной текстуре; □ sampl er2D — предоставляет доступ к двухмерной текстуре; □ sampl er3D — предоставляет доступ к трехмерной текстуре; □ sampl erCube предоставляет доступ к кубической текстуре; □ sampl erlDShadow — предоставляет доступ к одномерной текстуре глубины со срав- нением; □ sampl erZDShadow — предоставляет доступ к двухмерной текстуре глубины со срав- нением. При инициализации дискретизатора реализация OpenGL записывает в него все необходимое для поиска текстур. Сами шейдеры не могут ничего туда запи- сывать. Они могут только получить доступ к семплерам через соответствующую uniform-переменную или передать их пользователю или во встроенные функции. Семплер как входной параметр не может быть изменен, так что шейдеры не могут их редактировать. Объявить семплер можно, например, таким образом: uniform sampler2D Grass; (О модификаторе uniform подробно рассказывается в разделе 3.5.) Эту переменную затем можно передать в соответствующую функцию поиска текстур: vec4 color = texture2D(Grass, coord); Здесь coord — переменная типа vec2, в которой содержится двухмерная коор- дината текстуры Grass, и color — результат выполнения поиска по текстурам. И компилятор, и OpenGL API проверят, что Grass действительно двухмерная тек- стура и что она передается только двухмерным семплерам. Шейдеры не могут сами работать со значениями семплеров. Например, нельзя написать выражение Grass + 1. Если в шейдере нужно совместить несколько тек- стур по какому-то особому правилу, используется массив семплеров, как в при- мере: const Int NumTextures = 4; uniform sampler2D textures[NumTextures]; Потом их можно поместить в цикл: for (int 1 = 0; i < NumTextures; ++i) ... = texture2D(textures[1]. ...); После этого необходимое, но недоступное выражение Grass + 1 может выгля- деть как вполне доступное EGrassindex + 1].
3.2, Типы данных 85 3.2.5. Структуры Структуры в языке шейдеров OpenGL похожи на структуры языка С: "Struct light тесЗ position: vec3 color; }: Как и в C++, имя структуры является типом данных, и ключевое слово typedef в данном случае использовать не обязательно. Переменная типа light из этого примера может быть объявлена так: light ceilingLIght; Все прочие особенности работы со структурами такие же, как в С. Структуры можно делать Пленами других структур или объявлять внутри других структур, но Тогда они не могут быть безымянными. В состав структур могут входить мас- сивы. У каждого уровня вложенности есть собственное пространство имен для членов структуры. Битовые поля (объявление целых чисел как набора битов) не поддерживаются. Сейчас все структуры являются только типами, определенными пользовате- лем. Ключевые слова union, enum и class зарезервированы для возможного исполь- зования в будущем. 3.2.6. Массивы В языке шейдеров OpenGL можно создавать массивы любых типов. Определение vec4 pointsElOJ: создает массив из десяти элементов типа vec4 с номерами от 0 до 9. Массив можно объявить только с помощью квадратных скобок, так как в языке нет указателей. Объявляются массивы без указания размера. Например, объявление vec4 points[J: допускается, пока выполняется одно из следующих условий: 1. Перед ссылкой на массив в программе он объявлен еще раз с указанием разме- ра такого же типа, как в предыдущем объявлении, например: vec4 points[]: // points - массив неизвестного размера vec4 polnts[10]: // points - массив размером 10 элементов Но нельзя объявлять этот же массив еще раз: vec4 pointsEJ: И points - массив неизвестного размера vec4 pointsClO]; // points - массив размером 10 элементов vec4 points[20]; // это недопустимо тес4 pointsCl: И это тоже недопустимо 2. Все индексы, ссылающиеся на массив, — константы времени компиляции. При этом компилятор может увеличивать массив, чтобы он мот вместить все эле- менты с используемыми в программе индексами, например: vec4 pointstl: points[2] = vec4(1.0); points[7] - vec4(2.0); И points - массив неизвестного размера 11 points - массив размером 3 элемента // points стал массивом на 8 элементов
86 Глава 3. Определение языка В приведенном примере после компиляции в программе будет находиться век- тор размером 8 элементов. Окончательный размер массива определяется ука- занием максимального индекса. Эти возможности особенно полезны для работы со встроенными массивами текстурных координат. Внутри OpenGL массив объявлен без указания индекса: varying vec4 gl_TexCoord[]: Если программа указывает только константы 0 и 1, которые определяются во время компиляции, массиву будет неявно присвоен размер 2 элемента: gl _ТexCoord [2]. Если же шейдер для обращения к массиву использует неконстантные перемен- ные, он должен объявить массив явно и указать нужный размер. Желательно объявлять массив не больший, чем это необходимо, так как размер аппаратных ресурсов здесь ограничен. Если несколько шейдеров совместно используют один массив, он может ока- заться объявленным разного размера. В этом случае компоновщик сделает размер массива соответствующим максимальному объявлению. 3.2.7. Void Тип void традиционно используется для объявления того, что функция не возвра- щает никакого значения. В данном примере функция main ничего не возвращает и объявлена как тип void: void ma1n() { }’ Для других целей этот тип не используется. 3.2.8. Объявления и область видимости Объявления переменных в языке шейдеров OpenGL такие же, как в C++. Пере- менные могут быть объявлены по необходимости, а не в начале блока и имеют такую же область видимости, например: float f; f = 3.0: vec4 u, v; for (int 1 =0: 1 <10: ++1) v = f * и + v: Область видимости переменной, объявленной в операторе for, заканчивается в конце тела цикла. Переменные нельзя объявлять в операторе i f, что упрощает реализацию определения области действия в операторе el se и делает ее лишь чуть менее удобной. Как и в С, в именах переменных учитывается регистр, они должны начинаться с буквы или подчеркивания, а содержать могут только буквы, цифры и подчерки- вания. Определенные разработчиком переменные не могут начинаться с gl_, так как все эти имена являются зарезервированными, как и имена, в которых встре- чаются несколько подчеркиваний подряд (_).
3.3. Инициализаторы и конструкторы 87 12,9 . Согласование и преобразование типов Язык шейдеров OpenGL является строго типизированным. Связываемые типы должны соответствовать друг другу: аргумент, передаваемый в функцию, должен соответствовать формальному объявлению данного параметра функции, а типы, с которыми производятся операции, должны соответствовать требованиям конк- ретного оператора. Типы переменных, к которым применяется оператор, должны совпадать с тре- буемыми оператором типами. Здесь не существует неявного преобразования ти- пов. Иногда из-за этого приходится довольно часто делать явные преобразова- ния. Зато это упрощает язык, исключая возможность появления неочевидного кода и ошибок. Например, для языка OpenGL не существует неоднозначностей при использовании перегруженных функций. 3.3. Инициализаторы и конструкторы Переменные шейдера можно инициализировать вместе с объявлением. Следую- щий пример инициализирует Ь, оставляя а и с не определенными: float а. b = 3.0. с; Переменные, объявленные константными, должны быть инициализированы обязательно: const int Size = 4: // требуется значение для инициализации Attribute-, uniform- и varying-переменные нельзя инициализировать при объяв- лении: attribute float Temperature; // // инициализатора нет значение устанавливается вершинным API uniform int Size: // инициализатора нет. // значение устанавливается uniform-API varying float density: // инициализатора нет, // значение устанавливается вершинным шейдером Чтобы инициализировать составные типы либо на этапе объявления, либо поз- же, используются Конструкторы. Это единственный способ инициализации, так как язык шейдеров не заимствовал из С синтаксис с фигурными скобками ([...}). Конструкторы похожи на вызовы функций, где вместо имени функции подстав- ляется имя типа. Например, чтобы инициализировать вектор vec4 значениями (1,0, 2,0,3,0,4,0), нужно написать: vec4 v = vec4(1.0. 2.0. 3.0. 4.0); или, что то же самое: vec4 v; v = vec4(1.0. 2.0. 3.0. 4.0): Конструкторы существуют для всех встроенных типов, кроме семплеров, и для структур.
88 Глава 3. Определение языка Приведем несколько примеров: vec4 v - vec4(1.0. 2.0. 3.0, 4.0): 1vec2 с = 1vec2(3, 4); vec3 color = vec3(0.2. C.5. 0.8): mat2 m - mat2(1.0, 2.0. 3.0. 4.0): struct light { vec4 position; struct lightColor { vec3 color; float intensity: } ) lightl = lighttv. lightColorlcolor. 0.9)): Порядок заполнения компонентов матриц — по столбцам. Переменная m из предыдущего примера — это матрица '1,0 3,0' 2,0 4,0_ В приведенных примерах для каждого компонента инициализируемой пере- менной в конструктор передавалось значение. Во встроенные конструкторы для векторов можно передать всего одно значение, которое распространяется на все элементы вектора: vec3 v = vec3(0.6): Это выражение эквивалентно выражению vec3 v - vec3(0.6. 0.6, 0.6): Такое можно проделать только с векторами. Для структур необходимо переда- вать значение каждому компоненту. Конструкторы матриц также имеют возмож- ность получать только один аргумент, но при этом инициализируется только диа- гональ матрицы. Остальным элементам устанавливается значение 0,0: mat2 m = mat2(1.0): // создается единичная матрица 2Г2 Данное выражение эквивалентно выражению mat2 m - mat2(1.0. 0.0. 0.0. 1.0); П создается единичная матрица 2Г2 Матрицы и векторы также могут передаваться в конструкторы. При этом нужно соблюдать единственное правило — переданных компонентов должно быть достаточно для инициализации всех составляющих конструируемого объекта: vec4 v = vec4(1.0): vec2 и = vec2(v): // первые два компонента v инициализируют и mat2 m = mat2(v): vec2 t = vec2(1.0, 2.0. 3.0): // Можно. Значение 3,0 не используется Компоненты матриц и извлекаются, и заполняются по столбцам, а лишние иг- норируются без предупреждений. Это может использоваться для «сжатия» зна- чений (например, для отбрасывания значения прозрачности alpha из цвета или четвертой компоненты ® из координат).
3.5, Спецификаторы и интерфейс шейдера 89 3.4. Преобразования типов С помощью конструкторов можно выполнять явные преобразования типов дан- ных, например: float f = 2.3; bool b = bool(f); Этот код выставит значение true для b. Такие преобразования могут оказаться полезными в операторах управления типа if, которые получают логическое зна- чение. Булевы конструкторы преобразуют ненулевые числовые значения в true, а нулевые — в false. Язык шейдеров OpenGL не предоставляет возможностей синтаксиса язы- ка С для преобразований типов, которые могут оказаться неоднозначными. На самом деле в языке шейдеров нет способа интерпретировать какое-либо значе- ние по-разному, предполагая какой-либо другой тип. В языке нет указателей, объединений (union), неявных преобразований типов и явных преобразований в неподходящие типы (reinterpretes st). Вместо всего этого для преобразова- ний типов используются конструкторы. Аргументы конструктора преобразуются в тип, для которого вызывается конструктор. Например, следующий код явля- ется правильным: float f = float(3): // целое 3 становится числом с плавающей запятой 3.0 float g = float(b): // преобразование логического b в число с плавающей //запятой vec4 v = vec4(2): // все компоненты вектора v устанавливаются в 2.0 При преобразовании из логического типа true становится 1 или 1,0, a fа 1 se — нулем. 3.5. Спецификаторы и интерфейс шейдера Перед объявлением переменных или формальных параметров функции можно указывать спецификаторы. Те из них, что используются для указания вида входных параметров функции (const, in, out, incut), описаны в разделе 3.6.2. Здесь же рас- смотрим другие спецификаторы, большинство из которых формируют интерфейс шейдера. Далее приведен полный список спецификаторов, которые можно исполь- зовать вне формальных параметров функций: □ attribute для часто меняющейся информации, которая передается от при- ложения вершинному шейдеру; □ urn' form — для информации, которая меняется сравнительно редко и использу- ется как вершинным, так и фрагментный шейдером; □ varying — для интерполированной информации, передающейся от вершинно- го шейдера фрагментному; □ const — для объявления неизменяемых константных идентификаторов, значе- ния которых известны еще на этапе компиляции, как и в С.
90 Глава 3. Определение языка Передача информации в шейдер и из него немного отличается от обычной пе- редачи информации в функцию и из функции при обычном программировании. Информация передается использованием встроенных переменных и определен- ных разработчиком attribute-, uniform-, varying-переменных записью в них и чтением из них. Наиболее часто используемые встроенные переменные приве- дены в примере в начале этой главы. Это gl_Position для передачи однородных координат вершины и gl _FragCo’l or для передачи цвета фрагмента из фрагментно- го шейдера (полный список всех встроенных переменных см. в главе 4). Attribute-, uniform- и varying-переменные в примерах описаны коротко, они использованы для передачи информации в шейдеры и из них. Каждый вид переменных будет описан далее в этом разделе. Attribute-, uniform- или varying-переменные должны быть объявлены в гло- бальной области видимости, так как они должны быть видимы вне шейдеров, и в случае единственной программы все они находятся в одном пространстве имен. Спецификаторы, если они нужны, указывают перед типом переменной, и так как в языке шейдеров нет типов по умолчанию, объявление любой переменной с квалификатором всегда должно содержать тип; attribute float Temperature: const int NumLights = 3; uniform vec4 LightPositionfNumLights]; varying float Lightintensity: 3.5.1. Спецификатор attribute Приложение использует attribute-переменные (или атрибуты) для передачи дан- ных во фрагментный шейдер. Для каждой вершины обычно задаются новые зна- чения либо непосредственно приложением, либо иным элементом. Существуют встроенные attribute-переменные, такие как gl_Vertex и gl Jtormal, предназначен- ные для получения состояний OpenGL. Разработчик может объявлять и свои attribute-переменные. Возможность задавать типы для атрибутов ограничена: атрибутами могут быть только числа с плавающей запятой, векторы чисел с плавающей запятой и матри- цы. Целые числа, логические переменные, структуры или массивы не могут быть атрибутами. Это сделано для того, чтобы увеличить производительность, так как атрибуты меняются довольно часто. Шейдер не может изменить переменные, объявленные атрибутами. В вершинном шейдере нельзя объявлять атрибуты. 3.5.2. Спецификатор uniform Переменные, объявленные как uniform, устанавливаются только вне шейдера и за- даются отдельно для каждого примитива. С этим квалификатором можно объяв- лять переменные всех типов данных и массивы. Все вершинные и фрагментные шейдеры, собранные в одну программу, совместно используют глобальное про- странство имен для uniform-переменных. Так что если и в вершинном, и во фраг- ментом шейдере объявлена uniform-переменная с одним и тем же именем, это будет одна и та же переменная.
3.5. Спецификаторы и интерфейс шейдера 91 Шейдер не может изменять uniform-переменные. Это важно, так как несколь- ко обработчиков могут совместно использовать одни и те же ресурсы, и поэтому изменение переменных внутри шейдера может вызвать ошибки. Несмотря на то что семплер (например, sampler2D) может являться парамет- ром функции, для его объявления все равно нужно использовать спецификатор uniform, так как семплеры непрозрачны. Такое объявление поможет проверить, что семплер инициализирован текстурой и текстурный модуль правильно исполь- зуется внутри шейдера. 3.5.3. Спецификатор varying Через переменные, объявленные как varying, вершинный шейдер передает резуль- таты вычислений фрагментному шейдеру. Из таких переменных во время выпол- нения формируется интерфейс между вершинным и фрагментный шейдерами. Для каждого атрибута, принадлежащего примитиву, и каждой вершины должно быть задано свое значение, и эти значения интерполируются для фрагментов при- митива. Вершинный шейдер записывает значения каждой вершины в varying-пе- ременную, и когда фрагментный шейдер читает из нее, он получает интерполиро- ванное между вершинами значение. Если для какого-либо атрибута интерполяция не требуется, вершинный шейдер вообще не должен передавать никаких значе- ний фрагментному. Вместо этого приложение само передаст нужное значение фрагментному шейдеру через uniform-переменную. Наряду с использованием этих переменных для интерполированных значений, их можно использовать также для любого значения, которое будет часто изме- няться — с каждым новым треугольником, или группой треугольников, или вер- шиной. Быстрее было бы передать эти значения в attribute-переменные и потом переадресовать как varying-переменные, так как частое использование uniform- переменных может плохо повлиять на производительность. Автоматическая интерполяция переменных, определенных как varying, выпол- няется с учетом перспективы, и не имеет значения, какой тип данных интерполи- руется. В противном случае такие значения не сглаживались бы правильно на гра- нях при разделении поверхностей. Результат интерполяции был бы непрерывным, но его производная не была бы определена. Обычно вершинный шейдер устанавливает varying-переменную, а фрагмент- ный шейдер ее использует, не имея возможность изменить значение. Вершинный шейдер может читать из переменной записанное перед этим значение. Попытка получить значение переменной до записи в нее возвращает непредсказуемое зна- чение. 3.5.4. Спецификатор const Переменные, объявленные как const, обычно являются константами времени ком- пиляции (кроме объявленных const формальных параметров функций), они не видимы вне шейдера, внутри которого объявлены. Поддерживаются также кон- станты составных типов: const int numiterations = 10: const float pi = 3.14159; const vec2 v = vec2(1.0. 2.0k
92 Глава 3. Определение языка const vec3 u “ vec3(v. pi); const struct light { vec3 position: vec3 color: } fixedLight - 1 i ght (vec3( 1.0. 0.5, 0.5). vec3(0.8. 0.8. 0.5).): Все эти значения — константы времени компиляции. Компилятор может ко- пировать и сжимать константы во время компиляции, используя точность про- цессора, на котором выполняется компилятор, а во время выполнения такие кон- станты не требуют ресурсов. 3.5.5. Отсутствие спецификатора Если спецификатор для переменной вообще не указан и она не является описани- ем параметра функции, то шейдер может и читать из нее значения, и записывать их в нее. Такие переменные, объявленные в глобальной области видимости, мо- гут использоваться совместно шейдерами одного и того же типа, скомпонованны- ми в одну программу. И вершинные, и фрагментные шейдеры могут иметь соб- ственные пространства имен для переменных без квалификаторов. Вне программы такие переменные не видимы. Такие права доступа зарезервированы для пере- менных, определенных как attribute или uniform, а также для встроенных пере- менных состояния OpenGL. Время существования переменных без спецификатора ограничено одним запус- ком шейдера. В языке шейдеров OpenGL не существует понятия «статическая переменная», как в функциях языка С, что позволило бы сохранить значение пере- менной от одного запуска шейдера до другого, так как это противоречит концеп- ции параллельной обработки (возможность одновременно обрабатывать разные данные, используя одну и ту же память). Обычные переменные, используемые шейдером, не могут быть использованы совместно в параллельной обработке. Из-за того, что такие глобальные переменные находятся в разных пространствах имен для разных шейдеров, невозможно через них передать информацию между вершинным и фрагментным шейдерами. Переменные, предназначенные только для чтения и используемые совместно, должны быть объявлены как uniform, а пе- ременные, в которые записывает значения вершинный шейдер, а считывает их — фрагментный, должны быть объявлены как varying. 3.6. Последовательность выполнения Последовательность выполнения программы на языке шейдеров OpenGL похожа на выполнение С++-программы. Точка входа в шейдер — функция main. В про- грамме с разными шейдерами существует две точки входа ma 1 п: одна для вершин- ного шейдера, для обработки каждой вершины, а другая — для фрагментного, для обработки каждого фрагмента. Перед входом в функцию main выполняется ини- циализация глобальных переменных. Организация циклов выполняется с помощью операторов for, while, do-while так же, как и в C++. Переменные можно объявлять в for и while, и область их
3.6. Последовательность выполнения 93 действия распространяется только на тело цикла. Ключевые слова break и continue имеют такое же назначение, как и в С. Условия можно задавать операторами Ifni f-el se, как и в C++, но здесь нельзя объявлять переменные в операторе i f. Есть также оператор выбора (?:) со следу- ющим ограничением: тип второго операнда должен совпадать с типом третьего. Тип выражения, переданный в операторы 1 f и whi 1 е, а также второй операнд for должны иметь логический тип. Как и в С, правый операнд логического «И» (&&) не вычисляется, если левый является false, а правый операнд логического «ИЛИ» ((|) не вычисляется, если левый является true. Точно так же не будет вычисляться один из операндов оператора выбора (:?), Логическое «исключаю- щее ИЛИ» здесь существует, и для него всегда вычисляются оба операнда. Специальный оператор discard может запретить записывать фрагмент в кад- ровый буфер. Если выполнение доходит до этого оператора, текущий фрагмент считается отброшенным. Реализация может продолжать или завершать выполне- ние шейдера, а данный оператор всего лишь гарантирует, что кадровый буфер не изменится. В языке шейдеров нет эквивалента ключевому слову goto и нет аналогов ме- ток. Выбор оператором swi tch одного из нескольких вариантов также отсутствует. 3.6.1. Функции Работа с функциями в языке шейдеров OpenGL построена почти так же, как в C++. Функции можно перегружать по количеству и типу входных параметров, но не ис- ключительно по возвращаемому типу. Либо тело функции, либо ее объявление долж- ны находиться в области видимости перед вызовом функции. Типы параметров всегда проверяются. Это значит, что пустой список параметров () в объявлении функции однозначный — отсутствие аргументов. Типы передаваемых значений должны совпадать с объявленными параметрами, так как не происходит неявного преобразования типов. При перегрузке выбор нужной функции очень прост. Выход из функции с помощью оператора return происходит так же, как и в C++. Функции, которые что-то возвращают, должны возвращать значение, совпадаю- щее по типу с объявленным. Нельзя вызвать функцию рекурсивно ни явно, ни косвенно. 3.6.2. Соглашение о вызовах В языке шейдеров OpenGL параметры передаются в функцию по значению. Та- кие вызовы знакомы по языку С: входные параметры при вызове функции будут копироваться, а не передаваться по ссылке. Так как в языке нет указателей, не следует беспокоиться о том, что функция нечаянно изменит какие-либо параметры, ие предназначенные для этого, которые являются ссылками на внешние области данных. Если какие-либо параметры изменяются и их нужно передать обратно, по выходе из функции они также будут скопированы. Чтобы определить, когда какие параметры будут копироваться, нужно указать для них соответствующие спецификаторы: in, out или inout. Если нужно, чтобы параметры копировались в функцию только перед ее выполнением, использует- ся in. Это спецификатор по умолчанию, и он будет выставлен автоматически, если
94 Глава 3. Определение языка для параметра спецификатор не указан вообще. Если нужно, чтобы параметры копировались только при выходе из функции, используется спецификатор out. Чтобы копирование происходило и перед выполнением, и перед возвратом, ука- зывается спецификатор inout: □ in — копирует при входе, но не копирует при выходе; внутри функции можно делать запись; □ out — копирует только при выходе; функция может читать из него, но при вхо- де в функцию значение параметра не определено; □ 1 nout — копирует и при входе, и при выходе. К параметрам функции можно применять и спецификатор const. Это значит, что функция не может изменять передаваемый параметр. Кстати, обычный in- параметр функция изменять может, он просто не копируется при возврате обрат- но. Так что есть разница между параметром, объявленным как const in, и па- раметром, объявленным просто in или вовсе без спецификатора. Конечно, out- и lnout-параметры не могут быть объявлены как const. Приведем несколько примеров: void ComputeCoordf in vec3 normal. // Параметр 'normal' копируется вначале. Н в него можно записывать, но он // не копируется при выходе. vec3 tangent. It Точно так же. как 'normal’ inout vec3 coord) , // Копируется при входе и при выходе. Или vec3 ComputeCoord( const vec3 normal. // нельзя записывать в normal vec3 tangent. in vec3 coord) // функция вернет результат Следующие объявления некорректны: void ComputeCoorcKconst out vec3 normal. // нельзя записывать в normal const inout vec3 tang. // нельзя записывать в tang in out yec3 coord) // неверно: следует использовать //i nout Функции возвращают либо какое-то значение, либо ничего. Если функция ничего не возвращает, ее надо объявлять как vol d. Если функция возвращает зна- чение, оно может быть любого типа, кроме массива, хотя и массивы могут возвра- щаться в составе структур. 3.6.3. Встроенные функции В языке шейдеров OpenGL доступен обширный набор встроенных функций. Они подробно описаны в главе 5. Шейдер может переопределить любую из этих функций. Чтобы переопреде- лить функцию, нужно объявить ее прототип или определение в области видимос- ти шейдера. При этом компилятор или компоновщик, встретив вызов этой функ- ции, будет искать определенную шейдером реализацию, чтобы переадресовать вызов туда. Например, одна из встроенных тригонометрических функций объяв- лена как float sinCfloat x);
3.7. Операции 95 Если шейдер хочет переопределить эту функцию или специализировать ее для конкретного типа параметров, это можно сделать следующим образом: float sin(float х) return <.. какая-то функция от х..> ) void mainC) { // вызывается функция, определенная ранее, а не встроенная float s = siп(х): ) Это напоминает стандартные приемы компоновки разных библиотек и функ- ций, где преимуществом обладает локальная функция — если функция определе- на, то вызывается именно она, а не функция из внешней библиотеки. Если опре- деление такой функции находится в другом шейдере, нужно просто перед вызовом этой функции указать в текущем шейдере ее прототип. В противном случае будет использоваться встроенная функция. 3,7. Операции В табл. 3.1 в порядке приоритетности приведены операторы, доступные в языке шейдеров OpenGL. Приоритетность и ассоциативность совместимы с С. Таблица 3.1. Операторы в порядке приоритетности Оператор Описание [] Индекс Выбор компонента ++ -- Постфиксный инкремент/декремент ++ -- Префиксный инкремент/декремент -1 Унарное отрицание и логическое «НЕ» */ Умножение и деление + - Сложение и вычитание <><=>= Относительные X - | = Равенства && Логическое «И» АЛ Логическое «исключающее ИЛИ» 1! Логическое «ИЛИ» Выбор = += -= *= /= Присваивание t Последовательность 3.7.1. Обращение по индексу К векторам, матрицам и массивам можно обращаться с помощью оператора ин- декса ([ ]). Все индексы начинаются с нуля; первый элемент всегда имеет ин- декс 0. Индексирование массива такое же, как в С.
96 Глава 3. Определение языка Оператор индекса для вектора возвращает скалярный компонент. Это позво- ляет компонентам вектора иметь номера 0, 1... и обеспечивает произвольный дос- туп к ним, например: vec4 v = vec4(1.0. 2.0, 3.0. 4.0): float f = v[2]: /7 f принимает значение 3.0 В данном случае v[2] — скалярное значение с плавающей запятой 3,0, которое присваивается f. Применение оператора индекса к матрице возвращает соответствующий стол- бец матрицы в виде вектора, например: mat4 m = mat4(3.0): // инициализирует диагональные элементы значением 3.0 vec4 v: v - mCl]; 7/ Помещает вектор (0.0. 3.0. 0.0, 0.0) в v Здесь второй столбец рассматривается как ш[1 ] и записывается в V. Если применяется отрицательный индекс или больший размера объекта, его поведение становится непредсказуемым. 3.7.2. Обращение к компонентам Оператор выбора компонентов (.), обычно использующийся в структурах, здесь применяется также для обращения к компонентам вектора по именам после опе- ратора (swizzling), например; vec4 v4; v4.rgba: v4.rgb: V4. b; v4.xy: v4.xgba: // то же самое, что vec4: просто синоним v4 // то же самое, что vec3 /7 число с плавающей запятой // то же самое, что vec2 // некорректно - имена компонентов из разных наборов Имена компонентов можно указывать в другом порядке, чтобы переупорядо- чить или скопировать компоненты: vec4 pos = vec4(1.0. 2.0. 3.0, 4.0); vec4 sw1z = pos.wzyx: // swiz - (4.0. 3.0. 2.0. 1.0) vec4 dup = pos.xxyy; // dup = (1.0. 1.0. 2.0. 2.0) Можно указывать до четырех имен компонентов за одно обращение; иначе невозможно будет определить тип, получаемый в результате. Правила указания имен различаются для r-значений (выражения, используемые только для чтения) и /-значений (выражения, указывающие, куда записывать результат). В г-значе- ниях допустимы любые комбинации и повторения компонентов, но в /-значениях компоненты повторяться не могут, например: vec4 pos = vec4(1.0. 2.0. 3.0, 4.0): pos.xw “ vec2(5.0, 5.0): 7/ pos = (5.0. 2.0, 3.0. 6.0) pos.wx = vec2(7,0. 8.0): // pos = (8.0, 2.0. 3.0. 7.0) pos.xx = vec2(3.0. 4.0): // неправильно - 'x' указан дважды В r-значениях этот синтаксис можно использовать для любого выражения, ре- зультат которого является вектором. Например, получить вектор из двух компо- нентов функции поиска по текстуре можно таким образом: vec2 v = texturelDfsampler. coord).ху: Здесь встроенная функция texturelD возвращает значение типа vec4.
3.7. Операции 97 3.7.3. Покомпонентные операции Если к вектору применяется какой-либо оператор, операция выполняется так же, как если бы она выполнялась над каждым компонентом вектора в отдельности (есть нескольких важных исключений). Например,операция vec3 V. и; float f; v = U + f: будет эквивалентна операции у. х = и. х + f: V.y - u.y + f; v.z - u.z + f: А операция vec3 v, u, w; w = v + u: будет эквивалентна операции w.x = v.x + u.x; w.y = v.y + u.y; W.Z = V.Z + U.Z; Если над вектором и скалярным числом производится бинарная операция, число используется для каждого компонента вектора. Если операция выполняет- ся, над двумя векторами, их размеры должны совпадать. Исключение — умноже- ние вектора на матрицу или матрицы на вектор, где выполняется математичес- кое, а не покомпонентное умножение. Операторы приращения, уменьшения и унарного отрицания в языке шейде- ров OpenGL такие же, как в С. Примененные к вектору или матрице, они выпол- няются над каждым компонентом по отдельности. Эти операторы выполняются над целыми или числами с плавающей запятой. Арифметические операторы сложения (+), вычитания (-), умножения (*) и де- ления (/) здесь такие же, как в С, или покомпонентные, за исключением случаев математического умножения векторов и матриц: vec4 V. и; mat4 m: v * и; // Покомпонентное умножение v * ш; И Математическое умножение строки-вектора на матрицу m * v; // Математическое умножение матрицы на столбец-вектор m * т; // Математическое умножение матрицы на матрицу Все остальные операции выполняются покомпонентно. Логическое «НЕТ» (!), логическое «И» (&&), логическое «ИЛИ» (| |) и логи- ческое «исключающее ИЛИ» (лл) применяются только для операндов логическо- го типа, результат их выполнения — тоже логического типа. Эти операции не могут выполняться над векторами. Существует встроенная функция not для выполне- ния операции покомпонентного логического «НЕТ» над векторами из булевых значений. Операции отношений (<, >, <- и >=) выполняются только над целыми числами и числами с плавающей запятой, результатом их выполнения являются значения 4 Зак. 218
98 Глава 3. Определение языка логического типа. Существуют встроенные функции, например lessThartEqual, ко- торые возвращают вектор логических значений как результат покомпонентного сравнения двух векторов. Операторы сравнения (== и !=) выполняются над всеми типами, кроме масси- вов. Они сравнивают каждый компонент или член структуры операндов. Резуль- тат операции сравнения — логическое значение. Для того чтобы операнды оказа- лись равны, должны совпадать типы операндов, а также значения каждого из компонентов или членов структуры с обеих сторон. Чтобы сравнить покомпонент- но векторы, используются функции equal и notEqual. Скалярные логические значения являются результатом выполнения операто- ров -«равно» (=), «неравно» (!=), относительных (<,>, <= и >=) и логического «НЕТ» (!) в основном потому, что операторы 1 f, for и другие ожидают значения такого типа. Результатом выполнения функций типа equal являются векторы из логи- ческих значений, которые можно преобразовать в скалярные логические значе- ния с помощью встроенных функций any и all. Например, чтобы выполнить ка- кие-нибудь действия только в случае, если хотя бы один компонент вектора меньше соответствующего компонента другого вектора, нужно написать: vec4 и. v: if (any(1essThan(u. v))J Присваивание (=) требует полного совпадения типов операндов. Присваивать можно любые типы, кроме массивов. Другие операторы присваивания (+=, -=, *= и /=) работают почти так же, как в С, — за одним исключением: а *= b . а = а * b Выражение а * b должно быть семантически правильным, и тип выражения а * b должен быть таким же, как и тип а. Другие операторы присвоения работают так же. Тернарный оператор выбора (?:) работает с тремя операндами-выражениями (выражение! ? выражение? : выражениеЗ). Оператор вычисляет первое выражение, результат которого должен быть логическим значением. Если результат true, вы- числяется второе выражение, в противном случае вычисляется третье выраже- ние. Всегда вычисляется только один операнд из двух последних, либо второй либо третий, и результаты вычислений должны быть одного и того же типа любого, кроме массива. Результат всего оператора — это результат вычисленного второго либо третьего выражения. Оператор последовательности (.) применяется для разделения выражений и возвращает тип и значение самого правого выражения из списка выражений, разделенных этим оператором. Все выражения вычисляются слева направо. 3.8. Препроцессор Препроцессор языка шейдеров OpenGL похож на препроцессор языка С. Он под- держивает директивы #def 1 пе, #undef, #1 f, #1 fdef, #i fndef, #el se, #el 1 f, #endi f и #defi ned. Эти директивы имеют такое же значение, как и в языке С. Также поддержива- ются макросы с аргументами и макрорасширения. Встроенными макросами яв-
3.9. Выражения препроцессора 99 ЛЯЮТСЯ__LINE_,__FILE_,__VERSION_. Вместо_LINE__подставляется десятичная целая константа, на единицу большая количества символов перевода строки, пред- шествующих текущей строке исходного текста программы. Вместо__FILE__подставляется десятичная целая константа, обозначающая номер строки исходного кода. Вместо__VERSION_подставляется десятичное число, обозначающее номер вер- сии языка шейдеров OpenGL. Версия языка шейдеров, описанная в этом доку- менте, будет иметь значение_VERSION_100. Поддерживаются также директивы #error message, #11 пе и #pragma. Директива #еггог помещает message в информационный лог шейдера. Компи- лятор обрабатывает это сообщение как найденную ошибку. Действие директивы #pragma зависит от реализации. Если реализация не рас- познает идущие подряд лексемы, эта директива игнорируется. Однако эти дирек- тивы являются переносимыми. Чтобы включить или выключить оптимизацию, можно использовать следую- щие директивы: #pragma optimlze(on), #pragma optimize (off). Оптимизация обычно выключается для отладки шейдеров. Эту директиву мож- но указывать только вне определения функций. По умолчанию для всех шейде- ров оптимизация включена. Опция отладки этой директивы при компиляции добавляет в шейдер отладоч- ную информацию, которую можно использовать в отладчике: #pragma debug! on), fpragina debug! off). Эту директиву можно указывать только вне определения функ- ций. По умолчанию отладочная информация не добавляется. Директива #1 i пе после макроподстановки выглядит примерно так: #11 пе стро- ка, #11 пе строка номер_строки_исходного_кода, где «строка» и «номер_стро- ки_исход|1ого ,кода» — константные целые выражения. После обработки этой директивы реализация будет считать, что компиляция происходит на строке «стро- ка + 1», а номер строки исходного кода — это «номер_строки_ исходного кода», и такая нумерация сохранится до следующей директивы #1 i пе. 3.9. Выражения препроцессора Выражения препроцессора могут содержать операторы (табл. 3.2). Таблица 3.2. Операторы препроцессора Оператор + -~ ! */% + - == != &А | && || Описание Унарные Мультипликативные Аддитивные Побитовый сдвиг Отношения Равенства Побитовые Логические
100 Глава 3. Определение языка Приоритет и значения операторов такие же, как в обычном препроцессоре языка С. Важно понимать, что выражения препроцессора будут вычисляться не на гра- фическом процессоре, который выполняет шейдер, а на процессоре, на котором выполняется компилятор. Поэтому точность вычислений будет соответствовать точности главного процессора и отличаться от точности, обеспечиваемой языком шейдеров OpenGL, Строковые типы и операции не поддерживаются, как и операторы #, ##, опера- тор sizeof и т. д. 3.10. Обработка ошибок Иногда компиляторы не замечают некоторых ошибок, так как невозможно их все отследить. Например, невозможен полный контроль над попытками использова- ния неинициализированной переменной. Шейдеры, которые содержат такие ошиб- ки, могут по-разному выполняться на разных платформах. Поэтому спецификация языка шейдеров OpenGL гарантирует переносимость только для правильных про- грамм. Компиляторы языка шейдеров OpenGL должны выявлять неправильные про- граммы и выдавать диагностические сообщения, но спецификация не требует делать это во всех случаях. Компиляторы должны выдавать сообщения, если шей- деры содержат лексические, грамматические или семантические ошибки. Такие шейдеры вообще не могут выполняться. Способы получения диагностических сообщений обсуждаются в разделе 7.5. 3.11. Итоги Язык шейдеров OpenGL — процедурный язык высокого уровня, созданный спе- циально для работы в среде OpenGL. Этот язык позволяет приложениям програм- мировать обработку некоторых операций, выполняющихся параллельно на гра- фическом аппаратном обеспечении. Язык содержит конструкции, позволяющие писать лаконичные выражения для графических алгоритмов, удобные для про- граммистов, привыкших работать на языках С и C++. Язык шейдеров OpenGL поддерживает скалярные, векторные и матричные типы, структуры и массивы; семплеры для текстур; спецификаторы для различ- ных типов данных; конструкторы для инициализации и преобразования типов; операторы управления потоком данных и операторы для выполнения операций над данными, как и языки С и C++. 3.12. Ссылки Определение языка шейдеров OpenGL дается в документе [3]. Грамматика языка шейдеров OpenGL описана в приложении А данной книги. Эти два документа можно использовать для уточнения деталей языка. Дополни-
3.12. Ссылки 101 тельные учебники, презентации, официальные документы можно найти на веб- сайте 3Dlabs. Функциональность языка шейдеров OpenGL дополнена расширениями. Что- бы получить полное представление о системе поддержки языка шейдеров OpenGL, желательно прочесть спецификации для расширений и спецификацию библиоте- ки OpenGL. Список книг по OpenGL приведен в конце первой главы. Язык С подробно рассматривается в распространенном справочнике [2], язык C++ — в справочнике [8]. Доступно и множество других книг по этим двум языкам. 1, 3Dlabs. Веб-сайт компании (http://www.3dLabs.com/support/developer). 2. Kernighan В., Ritchie D. The С Programming Language. 2nd ed. Englewood Cliffs, NJ: Prentice Hall, 1988. 3, Kessenich J., Baldwin D., Rost R. The OpenGL Shading Language, Version 1.051 / 3Dlabs. 2003 (http://www.3dlabs.com/support/deveLoper/ogL2). 4. OpenGL Architecture Review Board: спецификация расширения ARB_vertex_sha- der, реестр расширений OpenGL (http://oss.sgi.com/projects/ogL-sampLe/registry). 5. OpenGL Architecture Review Board: спецификация расширения ARB_fragment_sha- der, реестр расширений OpenGL (http://oss.sgi.com/projects/ogl-sampLe/registry). 6. OpenGL Architecture Review Board: спецификация расширения ARB_shader_ob- jects, реестр расширений OpenGL (http://oss.sgi.com/projects/ogL-sampLe/regis- try). 7. Segal M„ Akeley K. The OpenGL Graphics System: A Specification (Version 1.5) / Ed.: Ch. Frazier (v. LI), J. Leech (v. 1.2-1.5). 2003 (http://opengl.org). 8. Stroustrup B. The C++ Programming Language. Special 3rd ed. Reading, MS: Addison-Wesley, 2000.
Программируемая часть операций OpenGL1 Язык шейдеров OpenGL был создан специально для работы с OpenGL. Входные и выходные данные для вершинного и фрагментного шейдеров были встроены в стандартную последовательность операций OpenGL в строго определенной фор- ме. Место программируемых процессоров в последовательности операций OpenGL описано в разделе 2.3. В данной главе обсуждаются подробности интегрирования шейдеров и механизмы языка, позволяющие это сделать. Приложения передают данные шейдеру посредством определенных разработ- чиком attribute- и uniform-переменных. Язык предоставляет также встроенные переменные для передачи данных между программируемыми процессорам и и стан- дартной функциональностью следующим образом. □ Стандартные атрибуты OpenGL можно получить из вершинного шейдера с по- мощью встроенных attribute-переменных. □ Состояния OpenGL доступны как из вершинного, так и из фрагментного шей- деров через встроенные uniform-переменные. □ Вершинные шейдеры передают данные для последующей обработки через спе- циальные встроенные выходные переменные вершинного шейдера и встроен- ные varying-переменные. □ Фрагментные шейдеры получают результаты предварительной обработки че- рез встроенные входные переменные фрагментного шейдера и встроенные varying-переменные. □ Фрагментные шейдеры возвращают результаты вычислений через специаль- ные выходные переменные фрагментного шейдера. Встроенные константы доступны из обоих видов шейдеров, они определяют некоторые платформозависимые значения, доступные посредством функции с ' Get. Все встроенные идентификаторы начинаются с префикса д!_. 4.1. Вершинный процессор На вершинном процессоре и выполняется вершинный шейдер, который замеща- ет стандартную обработку вершин в OpenGL, отличительные особенности которой 1 Информационная поддержка Бартольда Лихтепбельта (Barthold Lichtenbelt).
4.1. Вершинный процессор 103 перечислены далее (вершинный шейдер не может определить, примитиву какого типа принадлежит вершина): □ матрица модели --вида не накладывается на координаты вершины; □ матрица проекции не накладывается на координаты вершины; □ матрица текстуры не накладывается на координаты текстуры; □ нормали не преобразуются в координаты пространства обзора; □ нормали не масштабируются и не нормализуются; □ не выполняется нормализация по GL_ALITO_NORMAL; :Q текстурные координаты не генерируются автоматически; □ не вычисляется освещение; □ не вычисляется цвет материала; □ не вычисляется цвет освещения; □ не вычисляется поглощение света в зависимости от расстояния. Все эти особенности учитываются при установке координат текущего растра. Перечисленные далее стандартные операции выполняются над значениями вер- шин, полученными в результате обработки вершинным шейдером: □ замыкание цвета на крае или наложение маски (для встроенных varying-пере- менных для хранения цвета, но не для определенных разработчиком varying- переменных); □ вычисление перспективы в области отсечения; □ переход в оконную систему координат; □ определение диапазона глубины; □ отсечение, включая определенное пользователем отсечение; □ определение передней поверхности; □ плоскостная затушевка; □ вычисление цвета, координат текстуры, дымки, размера точки и определенно- го пользователем отсечения; □ окончательная обработка цвета. Основные операции, выполняемые вершинным процессором, обсуждались в разделе 2.3.1. Вершинный шейдер получает данные через attribute- и uniform-переменные, встроенные или определенные разработчиком, или текстурные карты — новую функциональность, которая появилась с языком шейдеров OpenGL (см. рис. 2.2). Результат можно получить из вершинного процессора через встроенные или оп- ределенные разработчиком varying-переменные и специальные выходные пере- менные вершинного шейдера. Встроенные константы (см. раздел 4.4) тоже дос- тупны из вершинного шейдера. В OpenGL можно установить режим индексированного цвета вместо RGBA- режима. Но этот режим не поддерживается вершинными шейдерами. Это означа- ет, что, если буфер кадров использует индексирование цвета, поведение вершин- ного шейдера при попытке использования не определено.
104 Глава 4. Программируемая часть операций OpenGL 4.1.1. Атрибуты вершины Приложения, рисующие изображения с помощью OpenGL, должны задавать ат- рибуты для каждой вершины примитива — нормаль, цвет, координаты текстуры и др. Атрибуты задают для каждой вершины отдельно с помощью функций gl Normal, gl Col or и gl TexCoord. Если использовать эти функции, задаваемые атрибуты на вре- мя станут частью состояния OpenGL. Изображения можно рисовать также с помощью массивов вершин. Использую- щее этот метод приложение должно поместить атрибуты вершин в отдельные мас- сивы координат, нормалей, цвета, текстурных координат и т. п. Вызвав функцию gl DrawArrays, можно затем установить эти атрибуты вершин для OpenGL за один вызов функции. Буферные объекты вершин (вместилище атрибутов вершин в OpenGL) появились в OpenGL версии 1.5 для повышения производительности. Атрибуты вершин бывают стандартные и произвольные. Стандартные атри- буты — те, что предусмотрены OpenGL: первичный и вторичный цвет, нормаль, координаты вершины, координаты текстуры, координаты дымки. Некоторые атрибуты вершинному шейдеру недоступны, например атрибут индекса, который используется для установки индекса цвета, и флаг видимости грани. Тем не менее приложение может устанавливать флаг видимости грани в OpenGL, используя вершинный шейдер. Вершинный шейдер получает данные из стандартных атрибутов, обращаясь к ним по специальным встроенным в язык именам. Из фрагментного шейдера эти данные недоступны, поэтому при попытке использовать их имена во фрагмент- ном шейдере компилятор будет порождать ошибку: И И Атрибуты вершин // attribute vec4 gl_Color; attribute vec4 gl_SecondaryColor; attribute vec3 gl Normal: attribute vec4 gl_Vertex; attribute vec4 g1_MultiTexCoordO: attribute vec4 g1_MultiTexCoordl; attribute vec4 gl_Mu1tiTexCobrd2: // ... до gl_MultiTexCoordN-l. где N = gl_MaxTextureCoords attribute float gl_FogCoord: Подробнее о передаче атрибутов вершинному шейдеру с помощью API-языка шейдеров OpenGL говорится в разделе 7.6. И стандартные, и произвольные атрибуты — часть состояния OpenGL. Это оз- начает, что однажды установленные значения сохраняются в дальнейшем. При- ложение может установить любой атрибут и рассчитывать, что OpenGL сохранит его (за исключением координат вершины, см. раздел 7.6). Существует, однако, ог- раничение на количество атрибутов вершины, которые может использовать вер- шинный шейдер. Обычно разрешенное количество атрибутов меньше, чем общее количество стандартных и произвольных атрибутов. Получить это максимальное количество атрибутов, обычно зависящее от конкретной реализации OpenGL, мож- но с помощью функции gl Get, задав константу GL_MAX_VERTEX_ATTR1BS_ARB. Тем не менее каждая реализация OpenGL обязана поддерживать не меньше 16 вершин- ных атрибутов.
4.1. Вершинный процессор 105 Чтобы при установке атрибутов можно было отделить атрибуты одной вершины от атрибутов другой, используют специальный атрибут gl _Vertex. Если приложение устанавливает этот атрибут командой gl Vertex или одной из команд для массива вершин, значит, определение вершины закончилось и все последующие атрибуты будут устанавливаться для следующей. Вызовом функции gl VertexAttrl bARB с ин- дексом можно установить произвольный атрибут, который находится по этому индексу. Эти две команды выполняют одни и те же действия, и каждая обозначает окон- чание определения вершины. 4.1.2. Uniform-переменные Шейдер может получить состояние OpenGL через встроенные uniform-перемен- ные, начинающиеся с префикса gl_. Например, к текущей матрице модели-вида можно обратиться по имени g1_Model VI ewMa.tr! х. Свойства источника света можно получить из массива, в котором содержатся данные обо всех источниках света, нацример gl_L1 ghtSource[2]. spotDi recti or. Изменения любого состояния OpenGL, однажды полученного шейдером, в дальнейшем отслеживаются и сразу становятся доступными шейдеру. Это позволяет приложению использовать команды OpenGL для косвенного управления шейдером. И вершинный, и фрагментный шейдеры могут получать состояние OpenGL через встроенные uniform-переменные. Эти переменные приведены в разделе 4.3. Приложения могут определять свои uniform-переменные для вершинного шей- дера и использовать функции OpenGL API для установки их значений (см. раз- дел 7.7). Существует ограничение на максимальное количество uniform-перемен- ных, зависящее от реализации. Общее количество и встроенных, и определенных разработчиком uniform-переменных не может быть больше установленного макси- мума, подсчитывается оно в компонентах размера float: например, vec2 состоит из двух компонентов, vec3 — из трех и т. д. Это максимальное значение можно получить из результата выполнения функции gl Get, передав константу fiL__MAX_VER- T.EX_UNIFORM_COMPONENTS-ARS. 4.1.3. Выходные переменные Из материала предыдущих глав становится понятно, как результаты вычислений из вершинного шейдера попадают в OpenGL для дальнейшей обработки, в том числе для сборки примитивов и растеризации. В языке шейдеров OpenGL опре- делено несколько встроенных переменных, в которые вершинный шейдер может записывать вычисленные значения. Встроенные переменные, обсуждаемые в этом разделе, доступны для записи только вершинному шейдеру. Переменная gl_Positi on хранит координаты вершины после их обработки шей- дером. Хорошо сформированный шейдер обновляет значение этой переменной при каждом новом выполнении. Компилятор в некоторых случаях может порож- дать ошибку, предупреждая разработчика о том, что шейдер не обновляет пере- менную gl-Position или читает из нее значение до первого обновления, но не все такие ситуации компилятор может выявить. Если шейдер не устанавливает зна- чение переменной gl-Position, результат его выполнения получается неопреде- ленным.
106 Глава 4. Программируемая часть операций OpenGL Встроенная переменная gl_Poi ntSI ze хранит размер (диаметр) точки примити- ва, измеряемый в пикселах. С помощью этого значения вершинный шейдер мо- жет, к примеру, вычислить размер экрана относительно расстояния до точки. Бо- лее подробное описание gl_Pon ntSize приведено в разделе 4.5.2, Если включено произвольное отсечение, оно выполняется внутри OpenGL после выполнения вершинного шейдера. Чтобы отсечение правильно работало совместно с вершинным шейдером, шейдер должен вычислить еще и координаты вершины относительно определенных произвольных плоскостей отсечения, а затем записать результат вычисления в переменную gl _С11 pVertex. Приложение должно самосто- ятельно контролировать, записываются ли эти значения шейдером и определены ли все плоскости отсечения в одной и той же системе координат. Произвольное отсечение такими определенными плоскостями работает только при линейном преобразовании. Более подробное описание gl_С11 pVertex приведено в разделе4.5.3. Каждая из этих переменных находится в глобальной области видимости, она доступна для записи в любой момент выполнения вершинного шейдера. Из них можно читать значения, которые были ранее в них записаны, но нельзя читать значение из неинициализированной переменной, так как это вызовет неопреде- ленное поведение. Только последнее записанное значение используется для по- следующих операций. Обращаться к этим переменным можно только из вершинного шейдера, опре- делены они таким образом: vec4 gl-Position; И сюда float gl_PointSize; // сюда vec4 gl_ClipVertex: // сюда должно быть записано значение может быть записано значение может быть записано значение 4.1.4. Встроенные varying-переменные Как говорилось ранее, varying-переменные используются для атрибутов прими- тива. Вершинный шейдер должен записывать в них значения для последующей интерполяции. В дальнейшем фрагментный шейдер считывает интерполирован- ные результаты и работает с ними. Если шейдеры используют какие-либо опреде- ленные разработчиком varying-переменные, последние должны быть объявлены в обоих шейдерах, иначе компоновщик будет порождать ошибку. Язык шейдеров OpenGL определяет несколько встроенных varying-перемен- ных. Среди них есть такие, что доступны для записи вершинному шейдеру, но не видимы фрагментному шейдеру, а также такие, что доступны для чтения фраг- ментному шейдеру, но не известны вершинному шейдеру. Вершинный шейдер может использовать следующие встроенные varying-пе- ременные, обязательные для заполнения, для последующей обработки фрагмен- тов либо стандартными операциями OpenGL, либо фрагментным шейдером: varying vec4 gl_FrontColor; varying vec4 gl_BackColor; varying vec4 gl_FrontSecondaryColor; varying vec4 gl_BackSecondaryColor; varying vec4 gl_TexCoord[gl_MaxTextureCoordsl; varying float gl_FogFragCoord: Значения, записанные в переменные gl_FrontColor, gl_BackCol or, gl_FrontSecon- daryColor Hgl_BackSecondaryColor, после выхода из вершинного шейдера будут при-
4.2. Фрагментный процессор 107 введены к диапазону [0, 1]. По этим четырем значениям можно определить, какой грани принадлежит примитив - передней или задней. Потом вычисляются gl_Col от и gl_Secondar,yCol or для фрагментного шейдера. В массив gl-TexCoord из вершинного шейдера передается несколько наборов координат текстуры. Так текстуры становятся доступными для OpenGL, если нет фрагментного шейдера, но если он есть, тоже можно использовать gl_TexCoord. В переменную gl J^ogFragCoord записывается информация о координатах для дымки. Записываемое значение зависит от того, какой режим дымки ранее уста- новлен функцией gl Fog. Если в нее передавалась константа GL_FRAGMENT_DEPTH, то Bgl_FogFragCoord нужно записывать расстояние от точки обзора до вершины в пространстве координат обзора, где координаты точки обзора (0, 0, 0, 1). Если же установлен режим дымки GL_FOG_COORDINATE, в gl_FogFragCoord нужно запи- сывать значение координаты дымки, которое затем будет интерполироваться .в целом по примитиву (то есть используется встроенная attribute-переменная g,1_FogCoord). 4.1.5. Определенные разработчиком varying-переменные В вершинных шейдерах можно определять varying-переменные для передачи любых данных фрагментному шейдеру. Эти значения не модифицируются, но используются при отсечении и интерполяции. Существует ограничение на коли- чество значений с плавающей запятой, которые могут быть интерполированы, и это ограничение определяется реализацией. Узнать максимальное количе- ство таких значений можно с помощью функции glGet, передав ей константу ,GL_MAX_VARYING_FLOATS_ARB. 4.2. Фрагментный процессор Фрагментный процессор выполняет фрагментные шейдеры и замещает стандарт- ные операции текстурирования, наложения цветов и создания дымки. В частно- сти, следующие операции над фрагментами замешаются: □ не применяются текстурные функции; □ не накладываются текстуры; □ не накладываются цвета; □ не накладывается дымка. Как обычно выполняются прочие операции: □ детализация текстур; □ альтернативная детализация текстур; □ сжатая детализация текстур; □ расчет параметров текстуры (они не изменяются, даже если доступ к текстуре происходит из фрагментного шейдера); □ определение состояния текстуры;
108 Глава 4. Программируемая часть операций OpenGL □ детализация текстурного объекта; □ применение режимов сравнения текстур. Основные функции, выполняемые фрагментным процессором, описаны в раз- деле 2.3.2. Данные поступают во фрагментный шейдер через встроенные или оп- ределенные разработчиком varying-переменные, uniform-переменные, специальные входные переменные или текстурные карты (см. рис. 2.3), Результаты вычисле- ний извлекаются из фрагментного процессора с помощью специальных перемен- ных выхода фрагментного шейдера. Встроенные константы, описанные в разде- ле 4.4, тоже доступны из фрагментного шейдера. Поведение фрагментного шейдера, как и вершинного, не определено, если буфер кадров работает не в режиме RGBA, а в режиме индексированного цвета. 4 .2.1. Varying-переменные К некоторым встроенным varying-переменным можно обратиться из фрагмент- ного шейдера. Переменные gl_Col or и gl ^Seconds ryCol or здесь те же, что и встроен- ные attribute-переменные в вершинном шейдере. Никакой неоднозначности здесь нет, так как те переменные доступны только из вершинного шейдера, а эти — только из фрагментного: varying vec4 gl_Color; varying vec4 gl^SecondaryColor; varying vec4 gl_TexCoord[g1_MaxTextureCoords]: varying float gl_FogFragCoord: Значения gl Color и gl_SecondaryColor будут вычислены автоматически из значений gl_FrontCo1or, gl_BackColor, gl_FrontSecondaryColor и gl_BackSecondary Color при определении принадлежности примитива передней или задней грани (см. раздел 4.5.1). Если вершинный шейдер не задействован и выполняются стан- дартные операции, в gl_FogFragCoord будет содержаться либо z-координата фраг- мента в пространстве координат обзора, либо интерполированное значение ко- ординаты дымки, в зависимости от того, установлен ли GL_FRAGMENT_DEPTH или GL_FOG_COORDINATE. Массив gl_TexCoordL] будет содержать значения, записанные вершинным шейдером, а если вершинный шейдер отсутствует, то массив будет содержать результаты стандартных операций над вершинами — текстурные ко- ординаты. Автоматически деление текстурных координат по «у-компоненте не производится. Если вершинный шейдер отсутствует, а фрагментный шейдер есть, нужно учи- тывать некоторые тонкости; к примеру, если обработка фрагментов происходит после обычной растеризации пиксельного прямоугольника или растрового изоб- ражения, а фрагментный шейдер использует не встроенные varying-переменные, результаты обращения к таким переменным не определены. В данном случае за- полненными оказываются только встроенные varying-переменные, в которых со- держатся данные о положении используемого растра. Вообще фрагментные шейдеры могут получать данные из определенных разработчиком varying-переменных. И встроенные, и определенные в шейдере varying-переменные содержат результат интерполяции с поправкой на перспек- тиву значений для каждой вершины.
4.2, Фрагментный процессор 109 4 .2.2. Uniform-переменные Состояние OpenGL доступно и для вершинных, и для фрагментных шейдеров через встроенные uniform-переменные, имена которых начинаются с зарезерви- рованного префикса gl_ (см. раздел 4.1.2). Список uniform-переменных, из кото- рых можно получать параметры состояния OpenGL, приведен в разделе 4.3. Определенные разработчиком uniform-переменные можно определять и ис- пользовать во фрагментных шейдерах точно так же, как и в вершинных. Суще- ствуют OpenGL API-функции для доступа к таким переменным (см. раздел 7.7). Максимальное количество uniform-переменных можно получить функцией glGet, передав в нее константу GL_MAX_FRAGMENT_UHIFORM_COMPONENTS_ARB. Суммарное количество всех uniform-переменных, и встроенных, и определенных в шейдерах, должно быть не больше этого зависящего от реализации количества. 4 .2.3. Переменные для входных данных Фрагментному шейдеру переменная gl _Frag.Coord доступна только для чтения, она содержит относительные оконные координаты фрагментах, у, z и 1/ж. Эти значе- ния — результат интерполяции примитивов после обработки вершин, использу- ются они для формирования фрагментов. Z-компонент после вычисления смеще- ний многоугольников содержит значение глубины. Эта встроенная переменная нужна для реализации операций вычисления оконных координат, например про- зрачности экранного окна (применение di scard к любому фрагменту с нечетными gl_FragCoord .х или gl_FragCoord.у, но не с обеими сразу). Фрагментный шейдер может работать также с переменнойgl JronLFaci ng, кото- рая обозначает принадлежность фрагмента к передней (true) или задней (false) грани. Если имитируется двухстороннее освещение, вершинный шейдер вычисля- ет два значения цветами в зависимости от gl_FrontFacing будет выбран один из этих цветов. Эти данные могут быть полезны также при задании разных способов затенения для передней или задней грани. Принадлежность фрагмента какому-ли- бо виду грани зависит от примитива, из которого формируется фрагмент. Все фраг- менты, созданные из примитивов, не являющихся многоугольниками, треугольни- ками или четырехугольниками, считаются расположенными на передней грани. Для всех других фрагментов (в том числе и созданных из точечных изображений или просто из отрезков) сначала проверяется знак координат примитива в оконном пространстве координат. В дальнейшем знак может быть изменен на противополож- ный в зависимости от того, каким был последний вызов gl FrontFace. Если знак по- ложительный, фрагменты принадлежат передней грани; отрицательный — задней. Переменные для входных данных находятся в глобальной области видимости, но могут быть доступны только из фрагментного шейдера. Объявлены они таким образом: vec4 gl_FragCoorci: bool gl_FrontFacing: 4 .2.4. Переменные для выходных данных Фрагментный шейдер должен вычислять значения для буфера кадров. Над выход- ными данными из фрагментного шейдера выполняется несколько окончательных
110 Глава 4. Программируемая часть операций OpenGL операций, данные для которых берутся из встроенных переменных g I FragCol or и gl_FragDepth. Эти встроенные переменные для фрагментного шейдера находят- ся в глобальной области видимости, и шейдер может записывать в них значения сколько угодно раз — все равно используется только последнее из них. Значение цвета, которое должно быть записано в буфер кадров (предполагает- ся, что окончательные операции не изменят его), вычисляется фрагментным шей- дером и сохраняется во встроенной переменной gl_FragCol or. Большинство шей- деров все-таки будут вычислять значение для gl J^ragCol or, но это не обязательно. Фрагментный шейдер должен записать лишь gl_FragDepth. Но если и последую- щие операции не вычислят gl_FragColor, результат окажется непредсказуемым. К любому фрагменту шейдер может применить ключевое слово di scard, чтобы сразу отбросить его. Если при рисовании применяется буфер глубины, а шейдер не вычисляет gl_FragDepth, глубина вычисляется с помощью стандартных операций. При обра- ботке фрагмента шейдер записывает значение глубины для текущего фрагмента в gl_FragDepth. После того как фрагментный шейдер завершится, это значение бу- дет приведено к значению из диапазона [0,1 ] и к форме представления числа с фик- сированной запятой с таким количеством бит, чтобы соответствовать размеру бу- фера кадров. Разработчик шейдера должен позаботиться о том, чтобы переменная gl^Frag- Depth заполнялась при любом варианте прохождения выполняемых ветвей кода, иначе при некоторых условиях значение gl_FragDepth останется неопределенным. Нет гарантии, что шейдер вычислит значение глубины точно так же, как это делают обычные OpenGL-операции, которые он замещает. Значение может отли- чаться даже в том случае, если оно просто копируется из gl_FragCoord . z в gl_Frag- Depth. Только инвариантность глубины гарантирует, что фрагментные шейдеры будут вычислять глубину так же, как это делается в стандартных операциях. Значения, записываемые в gl_FragCol or и gl_FragDepth, не усекаются шейдером, но могут быть приведены позже к значению из диапазона, приемлемого для буфе- ра, в который записывается фрагмент. Если шейдер выполняет для фрагмента discs rd, фрагмент отбрасывается, a gl _F rag - Depth и g1_FragCplor становятся не нужны. Переменные для выходных данных фрагментного шейдера находятся в гло- бальной области видимости, к ним можно обращаться только из фрагментного шейдера и объявлены они таким образом: vec4 gl_FragColor; float gl_FragDepth: 4.3. Встроенные uniform-переменные OpenGL спроектирован как конечный автомат. Существует много состояний, в которые его можно устанавливать. Когда графические примитивы передаются в OpenGL для рендеринга, их обработка зависит от текущих установок OpenGL. Некоторые приложения используют такие возможности OpenGL в большом объеме, и большое количество кода манипулирует состоянием OpenGL, что по- зволяет добиться различных эффектов визуализиции.
4.3. Встроенные uniform-переменные 111 Язык шейдеров OpenGL облегчает таким приложениям программирование гра- фических операций. В нем есть множество встроенных uniform-переменных, из которых шейдер может получать текущее состояние OpenGL. Таким образом, приложение продолжает использовать OpenGL как управляющий конечный ав- томат и использует шейдеры для чтения этого состояния, чтобы обрабатывать гра- фическую информацию способами, отличающимися от стандартных операций. Так как переменные объявлены как uniform, шейдеры могут считывать из них значе- ния, но не могут их изменить. Встроенные uniform-переменные (листинг 4.1) предоставляют шейдеру теку- щее состояние OpenGL. Их значения могут получать и вершинные, и фрагмент- ные шейдеры. Если приложение не меняет значение состояния OpenGL, эти пе- ременные содержат значения по умолчанию, определенные OpenGL. Листинг 4.1. Встроенные uniform-переменные // Л матрицы состояния Н uniform mat4 glModelViewMatrix: uniform mat4 gl_Project1onMatrix: uniform mat4 gljtodelViewProjectionMatrix; uniform mat3 gl_NormalMatrix: // вторичный параметр uniform mat4 gl_TextureMatrix[gl_MaxTextureCoords]; // // масштабирование нормали И uniform float gl_NormalScale: // //. диапазон глубины в оконных координатах // struct gl_DepthRangeParameters float near; // n float far; // f float cliff; // f - n uniform gl__DepthRangeParameters gl_DepthRange; // // Плоскости отсечения // uniform vec4 gl_ClipPlane[gl_MaxClipPlanes]: // // Размер точки // struct gl_PointParameters { float size: float sizeMin; float sizeMax; float fadeThresholdSize: fl oat di stanceConstantAttenuation; float distanceLinearAttenuation; n л продолжение tV
112 Глава 4. Программируемая часть операций OpenGL Листинг 4.1 (продолжение) float distanceQuadratlcAttenuatioh; }: uniform gl_PointParameters glPoint; И 11 Параметры материала И struct gl-Material Parameters { vec4 emission; /./ Ecm vec4 ambient; // Acm vec4 diffuse; H Dem vec4 specular: // Scm float shininess; // Srm }: uniform gl_MaterialParameters gl-FrontMaterial; uniform gl_MaterialParauieters gl_BackMater1al; // // Параметры освещения // struct gl LightSourceParameters { vec4 ambient; // Acli vec4 diffuse; // Deli vec4 specular; // Scl i vec4 position; И Ppli vec4 halfVector; 11 вторичный параметр: Hi vec3 spotDIrection; // Sdli float spotExponent; // Sri 1 float spotCutoff; // Crli // (диапазон: [0.0, 90.0]. 180.0) float spotCosCutoff: // вторичный параметр: cos(Crli) И (диапазон: [1.0, 0.0]. -1,0) float constantAttenuation; // КО float 11 nearAttenuation; // KI float quadraticAttenuation: // К2 uniform gl_LightSourceParameters gl J.ightSource[g1_MaxL1ghts]: struct gl_LightModelParameters { vec4 ambient; // Acs uniform gl_LightModelParameters gl-LightMode.l; // // Состояние, порожденное от освещения и материала. // struct gltightModelProducts { vec4 sceneColor; } И вторичный параметр. Ecm + Acm * Acs
4.1 Встроенные imiform-переменные 113 uniform gl_LightModelProducts gl_FrontLlghtModelProduct; uniform gl_LigfitModelProducts gl_BackLightModelProduct; struct gl_LightProduct$ vec4 ambient; // Acm * Acli v.ec4 diffuse; // Dem * Deli vec4 specular; .// Scm * Sell }: uniform gl_Li ghtProducts gl FrontLi ghtProduct[gl_MaxLi ghts] uniform gl_LightProducts gl_BackLightProduct[gl_MaxL1ghts]; /7 //. Текстуры // uniform vec4 gl_TextureEnvColor[gl_MaxTextureImagellnits]: uniform vec4 gl_EyePlaneSEgl_MaxTextureCoords]; i. f uniform vec4 gl_EyePlaneT[gl_Hax7extureCoords]: uniform vec4 gl_EyePlaneRLgl_MaxTextureCoords]; 'i uniform vec4 gl_EyePlaneQ[gl_MaxTextureCoords]; uniform vec4 gl_ObjectPlaneS[gl_MaxTextureCoords]; uniform vec4 gl J)bjectPlaneT[gl_MaxTextureCoordsJ; uniform vec4 gl_ObjectPlaneR[gl_MaxTextureCoords]; uniform vec4 glJ)bjectPlaneQ[gl_MaxTextureCoords]; // .// Дымка // struct gl JrogParameters vec4 color; float density; float start; float end; float scale; }; // 1 / (gl_Fog.end - gl_Fog.start) uniform gl_FogParameters gl Fog: Приведенные здесь встроенные uniform-переменные определены таким образом, чтобы получить наибольшее преимущество от возможностей языка. Параметры сгруппированы в структуры, например: параметры диапазона глубины, парамет- ры материала, параметры освещения и дымки. Для определения источников света и плоскостей отсечения используются массивы. Определение uniform-перемен- ных в таком виде улучшает удобочитаемость кода и позволяет шейдерам, напри- мер, обрабатывать такие данные с помощью циклов. В приведенном списке встроенных uniform-переменных (см. листинг 4.1) есть несколько переменных, которые хранят вторичные значения. Это значит, что значения не устанавливаются приложением через функции OpenGL, а реализа- ция OpenGL сама определяет их значения, исходя из ранее установленных величин. Это удобнее, чем если бы пришлось вычислять эти значения самостоятельно в шей- дере. Пример таких значений — матрица нормалей gl_Noста!Matrix. Она получена
114 Глава 4. Программируемая часть операций OpenGL простым обратным транспонированием верхнего левого угла матрицы модели- вида размером 3x3. Но матрица нормали обычно используется так часто, что нет смысла требовать от шейдеров вычислять ее самостоятельно. Вместо этого OpenGL по необходимости сам вычисляет нужное значение, доступное шейдерам через встроенную uniform-переменную. Вот несколько примеров использования подобных переменных. Вершинный шейдер может преобразовать координаты вершины gl_Vertex с помощью матри- цы проекции и модели-вида с помощью следующего кода: gl_Position = gl_M0.delViewProjectioriMatrix * gl J/ertex: Таким же способом вычисляется нормаль: triorm = gl_NormalMatrix * gl Normal: Преобразованная нормаль должна быть дополнительно нормализована для вы- числения освещения. Это можно сделать встроенной функцией norma 1 i ze (см. раз- дел 5.4). Приведем несколько примеров доступа к состоянию OpenGL через описанные переменные: gl_FrontMaterial.emission // значение излучения материала передней грани gl_LightSource[0].ambient // параметр рассеяния источника света #0 gl_ClipPlane[3][0] // первый компонент плоскости отсечения #3 gl_Fog.color.rgb H г-, д- и b-компоненты цвета дымки gl_TextureMatrix[l][2][3] // 3-й столбец. 4-й компонент второй И текстурной матрицы Смысл всех этих переменных и хранимых в них значений очевидны. За более подробной информацией следует обратиться к спецификации языка OpenGL. 4.4. Встроенные константы В языке шейдеров OpenGL определено много встроенных констант, доступных и для фрагментного, и для вершинного шейдеров. Это освещение, плоскости отсечения, модули текстур — все, что может возвратить функция gl Get для конкретной реали- зации. Есть также некоторые значения, появившиеся в OpenGL только с языком шейдеров, — максимальное количество числовых значений, которые могут сохранять- ся как uniform-переменные, доступные и вершинному, и фрагментному шейдерам; такие же значения для varying-переменных; максимальное количество доступных модулей текстур, максимальное количество наборов текстурных координат. Все эти значения можно получить с помощью функции gl Get (см. раздел 7.10). OpenGL определяет минимальные значения, которые в конкретной реализа- ции могут быть изменены в большую сторону. Эти значения — минимальная при- емлемая норма для реализации OpenGL, па которую может рассчитывать любой разработчик: // И Константы, зависящие от реализации. Эти значения - // установленный стандартом минимум. И const Int gl_MaxLights = 8; const int gl_MaxClipPlanes = 6: const int gl_MaxTextureUnits = 2;
4.5. Взаимодействие со стандартными операциями OpenGL 115 const int gl_MaxTextureCoords = 2; const int gl_MaxVertexAttribs = 16; const int gl,MaxVertexUniformComponents = 512; .const int gl_MaxVaryingFloats = 32: const int gl_MaxVertexTexture!mageUnits = 0; const int gl_MaxTextureImageUnits = 2: const int gl_MaxFragmentllnifornnComponents = 64; const int gl_MaxCombinedTextureImageUnits = 2; Перечисленные константы могут понадобиться шейдеру. Например, шейдер может запускать функцию вычисления освещения, которая в цикле будет прохо- дить по всем заданным источникам освещения, учитывая их параметры для вы- числения параметров вершины. Такой цикл можно организовать, используя встро- енную константу gl_MaxLights. Но, скорее всего, приложение будет использовать .дажене эту константу, а функцию gl Get, чтобы можно было определить, следует ли вообще загружать шейдер (см. раздел 7.10). 4.5. Взаимодействие со стандартными операциями OpenGL Этот раздел содержит в основном подробности для опытных разработчиков, ко- торые хорошо знакомы с OpenGL и хотят узнать, как программируемые возмож- ности OpenGL будут взаимодействовать с остальной функциональностью. 4.5.1. Двухсторонний цветовой режим Вершинные шейдеры могут работать в двухстороннем цветовом режиме. Цвет пе- редней и задней сторон вычисляется вершинным шейдером и записывается в пере- менные gl_FrontCol or, gl_BackColor,gl_FrontSecondaryColor Hgl_BackSecondaryColor. Если включен именно этот режим, один из этих цветов в OpenGL будет выбран для дальнейшей обработки. Выбор зависит от вида примитива и знака оконных координат этого примитива (за более точным описанием необходимо обратиться К спецификации). Если двухсторонний режим цвета отключен, OpenGL всегда выбирает цвет передней стороны. Этот режим можно переключать функциями glEnable и glDisablе, передавая константу GL_VERTEX_PROGRAM_TWO_SIDE_ARB. После выбора конкретного цвета (передней или задней стороны) его значение при- водится к значению из диапазона [0, 1 ] и перед интерполяцией переводится в формат числа с фиксированной точкой (обычные операции OpenGL). Если требуется более высокая точность или динамический диапазон цвета, приложение должно использо- вать собственные varying-переменные вместо встроенных gl _Col or. В этом случае цвет Передней/задней стороны выбираться не будет, зато встроенная переменная gl _Front - Facing для фрагментного шейдера будет хранить флаг, обозначающий, к какому имен- но примитиву принадлежит растеризованный фрагмент: переднему или заднему. 4.5.2. Режим размера точки Вершинные шейдеры могут работать в режиме размерности точки. Размеры точки вычисляются в пикселах и записываются во встроенную переменную gl_Poi nt Si ze.
116 Глава 4. Программируемая часть операций OpenGL Если этот режим включен, размер точки берется из указанной переменной и ис- пользуется на этапе растеризации, если же выключен ~ из значения, установлен- ного командой glPointSize. Если gl_PointS1ze не устанавливается шейдером, но режим включен, размер точки получается неопределенным. Включить или отклю- чить этот режим можно функциями gl Enabl е и gl Di sabl е с соответствующей кон- стантой GL_VERTEX_PROGRAM__POINT_SIZE_ARB. Установленный размер точки по умолчанию удобен для приложений, которые не меняют этот размер внутри шейдера. Вообще большинство вершинных шейде- ров не заботятся о размере точки и ничего не записывают в gl_PointSize, в таких случаях при растеризации используется значение, ранее установленное функци- ей gl Pol ntSize. Если примитив отсечен при включенном режиме размерности точки, значе- ния размера точки отсекаются таким же образом, как и значения цвета. Такие зна- чения используются затем в OpenGL-вычислениях как производные (размер точ- ки, зависящий от расстояния). Так что если для приложения требуется, чтобы размер точки становился меньше с увеличением расстояния до фрагмента, при- ложение должно само вычислять это значение. Если режим размерности точки выключен и для вычисления размера точки используется ранее установленное glPointSize значение, изменения размера в зависимости от расстояния не проис- ходит. При этом вторичный, вычисленный размер точки может использоваться для создания эффекта перехода прозрачности при одновременно доступном муль- тисемплинге. Подробности описаны в спецификации. Размер точки в зависимости от расстояния должен вычисляться вершинным шейдером без использования стандартного алгоритма OpenGL. Этот стандартный алгоритм вычисляет размер как функцию от расстояния между точкой обзора (0,0, 0,1) и координатами вершины в пространстве координат обзора. И если коор- динаты вершин вычисляются в вершинном шейдере, эта операция не выполняет- ся. С другой стороны, вычисление прозрачности точки может быть сделано пра- вильно, только если известно, примитиву какого типа принадлежит точка. Эта информация вершинному шейдеру недоступна, так как шейдер выполняется еще до сборки примитивов. Например, есть треугольник, у которого настройки перед- ней поверхности GLJTLL, а задней — GL_POINT. Вершинный шейдер должен вычис- лять эффект перехода только в случае принадлежности вершины заднему треу- гольнику. Но это невозможно, так как шейдеру неизвестен тип примитива. 4.5.3. Отсечение Отсечение, заданное пользователем, может выполняться совместно с вершинным шейдером. Дополнительные плоскости отсечения задаются командой gl Cl i pPl ane. Потом эти плоскости будут преобразованы инвертированием текущей матрицы модели-вида. После выполнения вершинного шейдера полученные координаты вершины оцениваются относительно этих плоскостей отсечения, и шейдер дол- жен представить координаты вершины в том же пространстве координат, в кото- ром они определены для плоскости отсечения (обычно это пространство обзора). Такие координаты вершины задаются в переменной gl_Cl 1 pVertex. Если эта пере- менная не определена или не заполнена шейдером, а пользовательское отсечение включено, результаты могут стать непредсказуемыми.
CS. Взаимодействие co стандартными операциями OpenGL 117 Если вершинный шейдер имитирует стандартные операции OpenGL, он дол- жен и вычислять координаты вершины в пространстве обзора, и сохранять их в gWipVertex, например: gl_ClipVertex = gl MpdelViewMatriх * gl_Vertex; Если все же необходимо делать отсечение в предметном пространстве коорди- нат, плоскости отсечения должны быть преобразованы инверсией матрицы моде- ли-вида. Чтобы получилось правильное отсечение, матрица модели-вида долж- на быть преобразована в единичную матрицу. После пользовательского отсечения вершины, как обычно, отсекаются углом обзора. При этом значения из gl-Position (однородные координаты точки в про- странстве отсечения) сравниваются с углом обзора. 4.5.4. Координаты растра Вершинный шейдер работает с координатами, заданными командой glRasterPos, точно так же, как с координатами, заданными gl Vertex. Вершинный шейдер обя- зан представлять данные для вычисления координат текущего растра. Данные, представляющие текущие координаты растра, состоят из следующих элементов: L Оконных координат, вычисленных из gl_Pos1ti on. Эти координаты восприни- маются как координаты точки, которые прошли отсечение и проецирование на оконное пространство координат. < 2. Бита, обозначающего, была ли точка отбракована. 3. Расстояния до растра, которое устанавливается в varying-переменную gl_Fog- FragCoord вершинного шейдера, 4. Цвета растра, устанавливаемого либо из gl_FrontColor, либо из gl_BackColor, в зависимости от того, передняя или задняя грань была выбрана. 5. Вторичного цвета растра, устанавливаемого из varying-переменных: либо из gl_FrontSecondaryColor, либо из gl_BackSeco.nda ryCol or, в зависимости от того, пе- редняя или задняя грань была выбрана. 6. Одной или больше растровых текстурных координат. Они берутся из varying- переменной вершинного шейдера — массива gl_TexCoord[], 7. Индекса цвета растра. Так как в режиме индексированного цвета результат выполнения вершинного шейдера не определен, значение индекса цвета рас- тра всегда устанавливается в 1. Если переменные, нужные для получения данных, описанных в первых шести пунктах, отсутствуют, значения соответствующих элементов данных останутся не определенными. 4.5.5. Инвариантность координат Для многопроходного рендеринга, подразумевающего, что часть операций выпол- няется вершинным шейдером, а часть — внутри OpenGL, важна инвариантность координат. Это означает, что вычисленные вершинным шейдером и стандартными операциями координаты вершины в пространстве отсечения должны совпадать
118 Глава 4. Программируемая часть операций OpenGL при одинаковых координатах в предметном пространстве и при одних и тех же матрицах проекции и модели-вида. Такая инвариантность может быть достигну- та использованием встроенной функции ftransforrn в вершинном шейдере: gl_Position = ftransformO: А код вершинного шейдера <jl_Position = д1_Мос1е1 ViewProJectMatrlх * gl_Vertex может и не привести к инвариантности из-за возможных оптимизаций компиля- тора и потенциальных различий в аппаратном обеспечении. 4.5.6. Текстурирование С возникновением языка шейдеров OpenGL появилось одно из самых значитель- ных усовершенствований в OpenGL — в области текстурирования. Операции тек- стурирования, в принципе, могут выполняться в вершинном шейдере. Однако при обработке фрагментов также могут выполняться некоторые операции. Фрагмент- ный шейдер потенциально может получить доступ к большему количеству тек- стурных модулей, чем это возможно при стандартных операциях. Это означает, что за один проход рендеринга можно выполнить больше текстурных преобразо- ваний. Разработчик шейдера может запрограммировать, чтобы эти преобразова- ния выполнялись так, как ему представляется удобным. Изменения в процессе обработки изображения в целом требуют разъясне- ния новых возможностей текстурирования. Термин «текстурный модуль» ранее использовался в OpenGL для обозначения разных понятий. Так могла называть- ся и совокупность текстурных координат, которые можно задать для каждой вер- шины (сейчас это называется набором текстурных координат), и совокупность аппаратных модулей, которые можно было использовать для одновременного до- ступа к текстурным картам (сейчас их называют модулями текстурных изобра- жений). Набор текстурных координат включает в себя параметры текстурных координат, такие как набор текстурных матриц и режим создания текстур. Чтобы получить количество этих параметров, нужно вызвать функцию glGet с констан- TofiGL_MAX_TEXTUREJJNITS. Если реализация поддерживает язык шейдеров OpenGL, количество доступ- ных наборов текстурных координат может отличаться от количества доступных модулей текстурных изображений, и это нормально, так как новые текстурные координаты могут, например, вычисляться из текстурных координат, переданных текстурной картой или просто содержащихся в ней. Существует пять различных ограничений, связанных с отображением текстур. 1. Максимальное количество доступных модулей текстурных изображений для вершинного шейдера — GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB. 2, Максимальное количество доступных модулей текстурных изображений для фрагментного шейдера — GL_MAX_TEXTURE_IMAGE_UNITS_ARB. 3. Общее количество модулей текстурных изображений и для обработки вершин, и для обработки фрагментов (фрагментным шейдером или в OpenGL) не долж- но превышать значения GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB. Если и вершин- ный, и фрагментный шейдеры используют одни и тот же модуль, считается, что модулей два.
47. Ссылки 119 О 4, OpenGL может выполнять множественное текстурирование даже без фрагмент- I ного шейдера. В этом случае максимально доступное количество ступеней тек- I стуры определяется константой GL_MAX_TEXTURE_UNITS. I 5. Количество поддерживаемых наборов текстурных координат задается констан- I той GL_MAX_TEXTURE_COORDS_ARB. Это ограничение действует независимо от нали- I чия или отсутствия вершинного шейдера. I Обычные установки для текстур (GL JEXTURE_CUBE_MAP, GL_TEXTURE_3D, GL_TEXTURE_2D I n GL_TEXTl.|RE_lD) шейдерами игнорируются. Например, даже если для какого-либо [ текстурного модуля установлено GL_TEXTUREJLD, шейдер все равно может исполь- I зевать семплер типа GL_TEXTURE_2D для доступа к этому текстурному модулю. Семплеры типов samp! erlDShadow или sampl er2DShadow обычно используются для доступа к текстурам глубины (тем, у которых установлен параметр GL_DEPTH_ COMPONENT). Режим сравнения текстур требует от шейдера использования встро- енной функции shadowlD либо shadow2D для доступа к текстуре (см. раздел 5.7). При попытке использовать эти функции с форматом, отличающимся от GL_DEPTH_ COMPONENT, результат будет непредсказуем. Если шейдер использует семплер для ссылки на неполный текстурный объект (например, одну из mipmap-текстур, имеющую внутренний формат, отличающийся от формата прочих текстур, или рамку, см. спецификацию), текстурный модуль возвратят значения (R, G, В, А) = = (0, 0, 0, 1). 4.6. Итоги В OpenGL были добавлены два новых программируемых модуля: вершинный и фрагментный процессоры. Для написания программ, выполняемых на этих про- цессорах, используется один и тот же язык, но с небольшими различиями. Вер- шинный процессор заменяет стандартные операции над вершинами в OpenGL, и шейдер, который будет выполняться на этом процессоре, называется вершин- ным шейдером. Вершинный шейдер будет выполняться для каждой вершины отдельно. Фрагментный процессор заменяет стандартные операции над фрагмен- тами, и шейдер, выполняющийся на нем, называется фрагментным шейдером. Фрагментный шейдер выполняется для каждого фрагмента отдельно. Огромное внимание было уделено созданию интерфейса между программиру- емыми и стандартными операциями, и теперь можно программировать обработку вершин при стандартной обработке фрагментов, и наоборот. Встроенные пере- менные предоставляют доступ к стандартным атрибутам OpenGL, зависящим от реализации константам и другим параметрам состояния OpenGL. Также эти пе- ременные позволяют шейдеру сообщаться с предыдущими и последующими эта- пами обработки. 4.7. Ссылки Встроенные переменные, описанные в этой главе, будут упоминаться во многих примерах этой книги.
120 Глава 4, Программируемая часть операций OpenGL За подробностями следует обращаться к стандартам OpenGl и OpenGl Shading Language. Литература по OpenGL, упомянутая в конце главы 1, может быть ис- пользована для того, чтобы лучше разобраться в способах использования встро- енных переменных. Элементы статьи [2] после адаптации включены в данную книгу. Полный ее текст доступен на веб-сайте 3Dlabs. 1. Kessenich J., Baldwin D„ Rost R. The OpenGL Shading Language, Version 1.051/ 3Dlabs. 2003 (http://www.3dlabs.com/sLipport/developer/ogL2). 2. Lichtenbelt B. Integrating the OpenGL Shading Language/3Dlabs. 2003. 3. Segal M., Akeley K. The OpenGL Graphics System: A Specification (Version 1.5)/ Ed.: Ch. Frazier (v. LI), J. Leech (v. 1.2-1.5). 2003 (http://opengL.org).
Встроенные функции Б этой главе читатель найдет подробности о функциях, определенных как часть языка шейдеров OpenGL. Ее можно пропустить, торопясь перейти к написанию Собственных шейдеров, так как это в основном справочник функций. Тем не менее он может быть очень полезным при написании шейдеров. Язык шейдеров OpenGL имеет множество удобных встроенных функций для операций над скалярными числами и векторами. Основная часть этих функций относятся к одной из трех категорий. 1. Функции, удобно дополняющие какую-либо аппаратную функциональность, например доступ к текстурным картам. Эти функции нельзя имитировать в шей- дере. 2. Функции для тривиальных операций (преобразование, смешивание и т. д.); они просты в использовании, но очень общие, поэтому должны непосредственно поддерживаться аппаратной базой. 3. Функции для операций, которые в той или иной мере ускоряются с помощью графического ускорителя. Тригонометрические функций тоже относятся к этой категории. Довольно много функций носят такие же имена, как и функции из библиотек языка С, но здесь они могут принимать и векторные аргументы. Так как язык шейдеров OpenGL поддерживает перегрузку функций, встроенные в язык функ- ции обычно имеют несколько вариантов с одним и тем же именем, различающихся типами входных и возвращаемого значений. Большинство функций существует в четырех вариантах: одна получает float-аргументы и возвращает float, вторая получает чес2-аргументы и возвращает vee2, третья получает УесЗ-аргументы и воз- вращает vec3, четвертая получает уес4-аргументы и возвращает vec4. Всякий раз при необходимости выполнять описанные вычисления предпо- чтительно использовать функции, а не писать собственный код. Ожидается, что встроенные функции сделаны наиболее оптимально, иногда даже поддерживают- ся аппаратно. Почти все встроенные функции можно использовать и в вершин- ном, и во фрагментном шейдере, но некоторые из них доступны только для шейде- ров одного вида. Можно также заменить встроенную функцию, объявив ее заново и определив в коде шейдера с теми же именем и списком аргументов. В качестве иллюстраций далее будет приведено графическое представление некоторых функций. Эти функции просты, и читателям не составит труда нари- совать их графики самостоятельно. Но, как будет виД'и. последующих глав,
122 Глава 5. Встроенные функции многие из этих функций можно использовать в шейдерах, чтобы добиться инте- ресных эффектов. При написании кода шейдера иногда бывает полезно нарисо- вать график функции, чтобы понять, каким будет вычисленное значение в дан- ном месте шейдера. По некоторым приведенным здесь графикам будет легче понять, как нарисовать остальные и для чего можно пытаться использовать эти функции. Некоторые из применений таких функций читатель увидит в последующих гла- вах книги. 5.1. Угловые и тригонометрические функции Тригонометрические функции можно использовать и в вершинных, и во фрагмент- ных шейдерах. Если параметр функции — угол, он обозначается в радианах. Ни одна из этих функций не генерирует ошибки деления на ноль. При пулевом дели- теле результаты выполнения функции не определены. Все перечисленные далее функции выполняются покомпонентно (табл. 5.1). Таблица 5.1. Угловые и тригонометрические функции Синтаксис Описание float radians (float degrees) vec2 radians (vec2 degrees) vec3 radians (vec3 degrees) vec4 radians (vec4 degrees) float degrees (float radians) vec2 degrees (vec2 radians) vec3 degrees (vec3 radians) vec4 degrees (vec4 radians) float sin (float radians) vec2 sin (vec2 radians) vec3 sin (vec3 radians) vec4 sin (vec4 radians) float cos (float radians) vec2 cos (vec2 radians) vec3 cos (vec3 radians) vec4 cos (vec4 radians) float tan (float radians) vec2 tan (vec2 radians) vec3 tan (vec3 radians) vec4 tan (vec4 radians) float asin (float x) vec2 asin (vec2 x) vec3 asin (vec3 x) vec4 asin (vec4 x) float acos (float x) vec2 acos (vec2 x) vec3 acos (vec3 x) yec4 acos (vec4 x) Переводит градусы в радианы и возвращает результат, а именно: result = тг/180 degrees Переводит радианы в градусы и возвращает результат, а именно: result = 180/тт radians Стандартная тригонометрическая функция синуса. Значение, которое возвращает эта функция, находится в диапазоне [-1,1] Стандартная тригонометрическая функция косинуса. Значение, которое возвращает эта функция, находится в диапазоне [-1,1] Стандартная тригонометрическая функция тангенса Арксинус. Возвращает угол, синус которого равен х. Диапазон возвращаемых значений [-п/2, л/2]. При | х | > 1 результаты не определены Арккосинус. Возвращает угол, косинус которого равен х. Диапазон возвращаемых значений [0, я]. При | х | > 1 результаты не определены
&2: Экспоненциальные функции 123 Синтаксис Описание float atan (float у. float х) vec2 atan (vec2 у. vec2 x) vec3 atan (vec3 y. vec3 x) Vec4 atan (vec4 y, vec4 x) Арктангенс. Возвращает угол, тангенс которого равен у /х. По знакам входных значений от и /определя- ется, в какой четверти находится угол. Диапазон воз- вращаемых значений [-л, п]. При х= 0 и у = 0 результаты не определены float atan (float y_over_x) vec2 atan (vec2 y_over_x) vec3 atan (vec3 y_over x) yec4 atan (vec4 y_over_x) Арктангенс. Возвращает угол, тангенс которого y_over_x. Диапазон возвращаемых значений [-тг/2, п/2] Приведенные функции используют в основном как тригонометрические, но синус и косинус можно использовать также и для других целей, например как Се- дову для функции сглаживания (рис. 5.1), а также для моделирования волн напо- зерхности предметов, для создания равномерных полос разных материалов, ими- тации качания объекта и для других целей. i.2. Экспоненциальные функции Экспоненциальные функции можно вызывать и из вершинных, и из фрагмент- ных шейдеров. Все перечисленные далее функции выполняются покомпонентно (табл. 5.2). Таблица 5.2. Экспоненциальные функции Синтаксис Описание float рои (float х. float у) vec2 рои (vec2 х. vec2 у) vec3 рои (vec3 х. vec3 у) vec4 рои (vec4 х. vec4 у) float ехр2 (float х) vec2 ехр2 (vec2 х) vec3 ехр2 (vec3 х) vec4 ехр2 (vec4 х) Возвращает значение хв степени у, то есть Xх Возвращает значение 2 в степени х, то есть 2х продолжение &
124 Глава 5. Встроенные функции Таблица 5.2 {продолжение) Синтаксис Описание float log2 (float х) vec2 1од2 (vec2 х) vec3 1од2 (vec3 х) vec4 1од2 (vec4 х) float sqrt (float х) vec2 sqrt (vec2 x) vec3 sqrt (vec3 x) vec4 sqrt Cvec4 x) float inversesqrt (float x) vec2 inversesqrt (vec2 x) vec3 inversesqrt (vec3 x) vec4 inversesqrt (vec4 x) Возвращает логарифм с основанием 2 от х, то есть возвращает значение у, удовлетворяющее уравнению х = 2у Возвращает положительное значение квадратного корня из X Возвращает обратное положительному значение квадратного корня из х 5.3. Общие функции Общие функции могут использоваться и вершинным, и фрагментным шейдерами. Все они работают с аргументами покомпонентно (табл. 5.3). Таблица 5.3. Общие функции Синтаксис Описание float abs (float х) vec2 abs (vec2 x) vec3 abs (vec3 x) vec4 abs (vec4 x) float sign (float x) vec2 sign (vec2 x) vec3 sign (vec3 x) vec4 sign (vec4 x) float floor (float x) vec2 floor (vec2 x) vec3 floor (vec3 x) vec4 floor (vec4 x) float ceil (float x) vec2 ceil (vec2 x) vec.3 ceil (vec3 x) vec4 ceil (vec4 x) float fract (float x) vec2 fract (vec2 x) vec3 fract (vec3 x) vec4 fract (vec4 x) float mod (float x. float y) vec2 mod (vec2 x. float y) vec3 mod (vec3 x. float у) vec4 mod (vec4 x. float y) vec2 mod (vec2 x. vec2 y) vec3 mod (vec3 x, vec3 y) vec4 mod (vec4 x. vec4 y) Возвращает x, если x> 0; в противном случае возвращает -х Возвращает 1,0, если х> 0, 0,0, если х= 0, -1,0, если х< 0 Возвращает значение ближайшего целого числа, меньшего или равного х Возвращает значение ближайшего целого числа, большего или равного х Возвращает х- floor(z) Модуль. Возвращает х- у- f1oor(z/y) для каждого компонента х, используя число с плавающей запятой у Модуль. Возвращает х- у - floor(z/y) для каждого компонента х, используя соответствующий компонент у
5.3. Общие функции 125 Синтаксис Описание 'float min (float х, float у) vec2 min (vec2 x. vec2 y) Vffi3 min (vec3 x, vec3 y) «4 min (vec4 x. vec4 y) vec2 min (vec2 x. float y) vec3 min (vec3 x. float y) vec4 min (vec4 x. float y) float max (float x, float y) ,vec2 max (vec2 x. vec2 y': vec3 max (vec3 x. vec3 y) vec4 max (vec4 x. vec4 y) vec2 max (vec2 x. float y) vec3 max (vec3 x. float y) vec4 max (vec4 x. float y) float clamp (float x. float minVal. float maxVal 1 vec2 clamp (vec2 x. float minVal. float maxVal) vec3 clamp (vec3 x. float minVal, float maxVal) vec4 clamp (vec4 x. float minVal. float maxVal) vec2 clamp (vec2 x. vec2 minVal. vec2 maxVal) vec3 clamp (vec3 x. vec3 minVal. vec3 maxVal) vec4 clamp (vec4 x. vec4 minVal. vec4 maxVal) float mix (float x. float y. float a) vec2 mix (vec2 x. vec2 y. float a) vec3 mix (vec3 x. vec3 y, float a) vec4 mix (vec4 x. vec4 y. float a) vec2 mix (vec2 x. vec2 y. vec2 a) vec3 mix (vec3 x. vec3 y, vec3 a) vec4 mix (vec4 x. vec4 y. vec4 a) Возвращает у, если у < х; в противном случае возвра- щает X Возвращает минимум для каждого компонента х, сопо- ставляемого с числом с плавающей запятой у Возвращает у, если х< у; в противном случае возвра- щает X Возвращает максимум для каждого компонента хг сопо- ставляемого с числом с плавающей запятой у Возвращает min(max(x, minVal), maxVal) для каждого компонента х, используя числа с плавающей запятой minVal и maxVal Возвращает покомпонентный результат min(max(x, minVal), maxVal) Возвращает х- (1,0 - а) + у- а, то есть линейное сочетание хи ус использованием числа с плавающей запятой а. Значение а не ограничено диапазоном [0,1] Возвращает покомпонентный результат х (1,0 - а)+ + у - а, то есть линейное сочетание векторов хи у с использованием вектора а. Значение а не ограничено диапазоном [0,1] float step (float edge, float x) vec2 step (vec2 edge. vec2 x) vec3 step (vec3 edge. vec3 x) vec4 step (vec4 edge. vec4 x) float smoothstep (float edged, float edgel. float x) vec2 smoothstep (vec2 edged. vec2 edgel. vec2 x) vec3 smoothstep (vec3 edged. vec3 edgel. vec3 x) vec4 smoothstep (vec4 edged. vec4 edgel. vec4 x) Возвращает 0, если х< edge; в противном случае возвращает 1,0 Возвращает 0, если x<edge0, и 1,0, если > edgel, и выполняет плавную интерполяцию Хермита между 0 и 1 при edgeO < х< edgel Эти функции полезны в целом, кроме того, многие из них могут пригодиться и для создания интересных шейдеров, как будет видно в последующих главах.
126 Глава 5. Встроенные функции Использование функции abs может придать уверенность в том, что какая-либо функция будет возвращать только положительные значения. С ее помощью также можно вносить неоднородности в функции сглаживания. Как будет видно из раз- дела 12.5, это свойство функции, abs будет использоваться для внесения в. функ- цию шума неоднородностей, создающих эффект, похожий на завихрение. Графи- ческое представление функции abs приведено на рис. 5.2. Функция s 1 gn просто возвращает -1,0 или 1 в зависимости от знака переданно- го в нее числа. Получается бесконечная функция (рис. 5.3). Функция floor является бесконечной ступенчатой функцией (рис. 5.4). Дроб- ная часть каждого входного значения отбрасывается, и выходное значение всегда является ближайшим к входному значению меньшим или равным ему целым. Функция ceil выполняет почти то же самое, что и floor, за исключением того, что возвращаемое целое число тоже будет ближайшим ко входному значению, но большим или равным ему (рис. 5.5). Функция выглядит так же, как и показанная на рис. 5.4, но выходные значения сдвинуты на единицу. (Хотя функции ceil и fl оог всегда возвращают только целые числа, они определены таким образом, чтобы воз- вращать тип данных с плавающей запятой.) Функция fract являет собой бесконечную функцию, где каждый сегмент на- клонен (рис. 5.6).
ЖОбщие функции 127 Рис. 5.6. Функция fract Функция mod очень похожа на fract. фактически, если разделить результат люс1(х,у) на у, результат будет практически таким же, как результат fract. Един- ственное отличие — период (рис. 5.7).
128 Глава 5. Встроенные функции Функция cl amp полезна для приведения какого-либо значения к заданному диа- пазону. Самый распространенный вариант использования: clamptx. 0.0, 1.0): где переменная х будет приведена к диапазону [0, 1 ]. Так как при выполнении этой функции выполняются два сравнения, ее следует использовать только в случае, когда неизвестно, в какую сторону от проверяемого диапазона уходит значение. Если это известно, можно использовать ml п или max, и тогда будет выполняться только одно сравнение. Если известно, что значение будет не меньшее, чем 0, то функция m1n(x. 1.0): наверняка будет выполняться быстрее и потребует меньше машинных ресурсов, чем clamp(x. 0.0. 1.0): так как нет смысла проверять, что полученное значение меньше нуля. Не нужно вызывать функцию с1 arc для значений цвета и глубины фрагментного шейдера, так как это выполняется автоматически после завершения шейдера. Функции mln, max, clamp показаны на рис. 5.8-5.10 соответственно. Функция mi п(х, у) имеет наклон при а; меньшем у, и является прямой горизонтальной линией при х, большем г/. Эта функция часто используется для ограничения окончатель- ного результата, например, чтобы гарантировать, что вычисленный результат ни- когда не превысит 1,0. Функция гсах(х, у) выглядит как прямая линия при х меньшем,, чем у, и как на- клонная при х, большем у. Эта функция, как и mi п, часто используется для ограни- чения окончательного результата, например, чтобы гарантировать, что вычислен- ный результат никогда не будет меньше 0. Функция cl amp(x, minVal, maxVal) не имеет наклона при х, меньшем minVal, и х, большем maxVal, и имеет наклон при х, большем mi nVa 1 и меньшем maxVal. Она функ- ционально эквивалентна выражению min(max(x, minVal), maxVal).
КЗ.1 Общие функции 129 Рис. 5.9. Функция max Функция step может использоваться для создания бесконечной прямой со сме- ной значения в конкретной точке (рис. 5.11). Использование этой функции рас- ематривается в главе 6 на примере создания простого процедурного шейдера. Функция smoothstep (рис, 5.12) может быть полезна в случае, если в определен- ной точке нужен плавный переход от одного значения к другому. Например, если С— float, это эквивалентно float t: t = clamp ((x - edged) / (edgel - edged). 0.0. 1.0): return t * t * (3.0 - 2.0 * t): Случаи использования других параметров — векторов vec2, vec3 и vec4 отли- чаются от приведенного примера только типом данных 4. 5 Зак. 218
130 Глава 5. Встроенные функции Рис. 5.12. Функция smoothstep 5.4. Геометрические функции Геометрические функции, исключая ftransform, можно использовать и из вершин- ного, и из фрагментного шейдера. Эти функции работают с параметрами-вектора- ми как с настоящими векторами, а не поэлементно (табл. 5.4). Таблица 5.4. Геометрические функции Синтаксис Описание float length (float х) float length (vec2 x) float length (vec3 x) float length (vec4 x) float distance (float pO. float pl) float distance (vec2 pO. vec2 pl) float distance (vec3 pO. vec3 pl) float distance (vec4 pO. vec4 pl) float dot (float x. float у) float dot (vec2 x. vec2 y) float dot (vec3 x. vec3 y) float dot (vec4 x. vec4 y) vec3 cross (vee3 x. vee3 y) Возвращает длину вектора х, то есть sqrt(%[0] х[0] + х[1] х[1] + ...) Возвращает расстояние между рОи pl, то есть length (pO- pl) Возвращает скалярное произведение % и у, то есть result = *"[0] - у [0] + *"[1] у[1] +... Возвращает векторное произведение хи у, то есть resulted] = jc [1] - у [2] - у[1] - х[2] resultfl] = х[2] - у [0] - у[2] - %[0] result[2] = х[0] • у[1] - у [0] х[1] float normalize (float x) vec2 normalize (vec2 x) vec3 normalize (vec3 x) vec4 normalize (vec4 x) Возвращает вектор с тем же направлением, что х, но длиной 1
5.5. Матричные функции 131 | Синтаксис Описание | vec4 ftransformO Только для вершинных шейдеров. Эта функция гарантирует, что координаты вершин будут преобразованы таким же способом, что и обыч- ными операциями OpenGL. Предполагаемое при- менение этой функции — вычисление значений для gl Fos L 1 on 1 float faceforward (float N. 1 float I. float Nref) 1 vec2 faceforward (vec2 N, Г vec2 I. vec2 Nref) 1 vec3 faceforward (vec3 N. 1 vec3 1. vec3 Nref) 1 vec4 faceforward (vec4 N. 1 vec4 1. vec4 Nref) 1 float reflect (float I. 1 float N) f vec2 reflect (vec2 I. Г vec2 N) L vec3 reflect (vec3 I. Если скалярное произведение (/Vref, Z) < 0,0, вернуть N; в противном случае вернуть -N Для инцидентного вектора /и ориентации поверх- ности /Vвозвращает направление отражения: result = I- 2,0 dot(/V, I) N /Vдолжно быть нормализовано vec3 N) vec4 reflect (vec4 I. vec4 N) Версия функции di stance с параметрами fl oat, возможно, не очень полезна (это то же самое, что абсолютное значение разницы), но при этом вычисляется расстояние Эвклида между двумя точками. Подобным образом fl oat-версия функции normal 1 ze всегда будет возвращать 1, a fl oat-версия функции 1 ength всегда будет возвращать абсолютное значение входного аргумента. Скалярные формы этих функций по- лезны тем, что типы данных аргументов могут быть изменены без изменения кода, который будет вызывать встроенную функцию. Функция ftransform предназначена для вычисления gl_Position таким образом: gl-Position = ftransformO Она преобразует значение gl^Vertex, используя текущую матрицу модели-вида, чтобы вычислить для gl _Posi ti on значение, идентичное тому, что было бы вычис- лено с помощью стандартных операций OpenGL. Эта функция используется, на- пример, когда приложение обрабатывает одни и те же геометрические фигуры за несколько проходов, и один проход выполняется с помощью стандартных опера- ций, а другой с использованием программируемых процессоров. |5.5. Матричные функции Матричные функции могут вызываться как из вершинных, так и из фрагментных ; шейдеров (табл. 5.5). ПРИМЕЧАНИЕ---------------------------------------------------- Чтобы получить результат математического умножения матриц, следует использовать оператор умножения (*).
132 Глава 5. Встроенные функции Таблица 5.5. Матричные функции Синтаксис Описание mat2 matrlxcotiipmult mats х, mats у) mat3 matrixcosipmult mat3 x. mats y) mat4 matrixcompmult mat4 x. mat4 y) Умножает матрицу хна матрицу /покомпонентно, то есть, result [г][)] является скалярным произведением x[i][y] на у [/][)]. Эти функции выполняют покомпонентное умножение двух матриц. Например, результат вызова функции matrixcompmult с двумя ЗО-матрицами х и у будет вы- глядеть так: mat3 х, у. newmat: newmat = matrixcompmultlx. у): Л'оо х01 *02 хы хн х12 Х20 Х21 Х22 Ут Уо1 У02 Ую Уп Угг .Узо У21 У22 ХООУОО х01У01 хЧ2Уо2 хюУ10 х20^20 т21^21 -г22.г/22 Обычно это не те действия над матрицами, которые необходимы для преобра- зований. Нужно использовать оператор умножения (*), чтобы выполнить математи- ческое умножение матриц: mat3 х. у. newmat: newmat = х * у: При этом будет выполняться следующая операция: х00 х01 ^02 Л'1О А’и А:2 х20 Х21 х22 Ум У\-.\ Ут Ую У-.-. Угг Уж Уг\ Угг -гооУоо +ж01#ю + хо2Уго -г'юЗЛ)О +-г'12^20 хгаУм +-Ъ|У1и + хтУж хмУ01 + 5'01J/il + х02?/21 ^io^oi + хиУи +хаУ.21 -Г 20^01 +-r21Vn +х22Уц ХООУП2 + х01Уг2 +хтУ22 хпУт + хпУгг + хггУгг х20У(>2 +-V21J/12 ^"г’22^22 5.6. Функции отношения векторов Операторы равенства и отношения (<, <=, >, >=, ==,. !=) определены таким образом, чтобы возвращать скалярные логические значения, они могут использоваться как из вершинных, так и из фрагментных шейдеров. Для векторов следует воспользо- ваться одной из перечисленных далее встроенных функций (табл. 5.6).
5Д Функции отношения векторов 133 Таблица 5.6. Функции отношения векторов Синтаксис Описание bvec2 lessThan(vec2 х. vec2 у) bvec3 lessThan(vec3 х. vec3 у) ,bvec4 lessThan(vec4 х. vec4 у) bvee2 lessThan(1vec2 х. 1vec2 у) bvec3 lessThandvec3 x, ivec3 y) bvec4 lessllian(1vec4 x, ivec4 y) byecZ lessThanEqual(v.ec2 x, vec2 y) bvec3 lessThanEqual(vec3 x. vec3 y) bvec4 lessThanEqual(vec4 x. vec4 y) bvec2 lessThanEqual(1vec2 x. ivec2 y) bvecS lessThanEqual (ivec3 x, 1vec3 y) by.ec4 lessThanEqual(ivec4 x. ivec4 y) bvec2 greaterTiian(vec2 x. vec2 y) ,bvec3 greaterThan(vec3 x. vec3 y) b.vec4 greaterThan(vec4 x. vec4 y) bvec2 greaterThant1vec2 x. ivec2 y) bvec3 greaterThan(ivec3 x, ivec3 y) bvec4 greaterThan(1vec4 x. 1vec4 y)„ bvec2 greaterThanEqual(vec2 x. yec2 y) bvec3 greaterThanEqual(vec3 x. vec3 y) brec4 greaterThanEqual(vec4 x. vec4 y) bvec2 greaterThanEqual(ivec2 x. 1 vec2 y) ЬтесЗ greaterThanEqual(1vec3 x.ivec3 y) bvec4 greaterThanEqual(ivec4 x.ivec4 y) bvec2 equal£vec2 x. vec2 y) ЬтесЗ equal(vec3 x. vec3 y) bvec4 equal(vec4 x. vec4 y) byec2 equal(1vec2 x, ivec2 y) bvec3 equal(ivec3 x, ivec3 y) 6vec4 equal(ivec4 x. ivec4 y) bvec2 equal(bvec2 x. bvec2 y) ЬуесЗ equal(ЬуесЗ x. bvec3 yl bvec4 equal (bvec4 x. bvec4 y) bvec2 notEqual(vec2 x. yec2 y) bvec3 notEqual(vec3 x. vec3 y) bree4 notEqual(vec4 x. vec4 y) bvec2 notEqual(1vec2 x. 1vec2 y) bvec3 notEqual(ivec3 x. 1vec3 y) bvec4 notEqual(ivec4 x. 1yec4 y) bye.c2 notEqual (bvec2 x. bvec2 y) bvee3 notEqual(bvec3 x, bvec3 y) bvec4 notEqual(bvec4 x. bvec4 y) bool any(bve.c2 x) bool any(bvec3 x) bool any(bvec4 x) bool all(bvec2 x) bool all(bvec3 x) bool all(bvec4 x) bvec2 not(bvec2 x) bvec3 not(bvec3 x) bvec4 not(bvec4 x) Возвращает результаты покомпонентного сравнения х< у Возвращает результаты покомпонентного сравнения х< у Возвращает результаты покомпонентного сравнения х> у Возвращает результаты покомпонентного сравнения х^ у Возвращает результаты покомпонентного сравнения х= у Возвращает результаты покомпонентного сравнения л! - у Возвращает true, если хотя бы один компонент х Возвращает true, если хотя бы один компонент х является true Возвращает true, только если все компоненты х— true Возвращает результаты покомпонентного отрицания х
134 Глава 5. Встроенные функции 5.7. Функции доступа к текстуре Функции доступа к текстуре видны и вершинному, и фрагментному шейдеру. Каж- дая из этих функций в качестве первого параметра получает переменную типа sampl er. Если заданный семплер — типа sampl er ID, то операция доступа к текстуре будет считывать данные из ID-текстуры, которая ранее была связана с этим семпле- ром. (Было бы неправильно, если бы приложение ассоциировало с переменной sampl erlD не ID-текстуру.) Точно так же переменная sampl er2D используется для доступа к 2В-текстуре и т. д. Правила приоритетности текстур, актуальные для стандартных операций OpenGL, здесь будут проигнорированы. Приложение дол- жно устанавливать параметры состояния для текстур самостоятельно перед вы- полнением шейдера (см. раздел 7.8). Функции доступа к текстуре можно использовать как для многоуровневых, так и для одноуровневых текстур. Так как при использовании вершинных шейде- ров не вычисляется уровень детализации, при выполнении подобных операций существует различие между вершинными и фрагментными функциями доступа. Некоторые свойства текстур, например размер, формат пиксела, размерность, метод отбора, количество уровней множественного отображения, сравнение глубины и т. д., уже определены соответствующими вызовами функций из OpenGL API. Все это учитывается при доступе к текстуре с помощью функций, описанных в этом разделе. Во всех этих функциях параметр Ы as необязателен для фрагментных шейде- ров и не принимается в вершинном шейдере. Если Ы as задается для фрагментного шейдера, он добавляется к вычисленному уровню детализации. Если парамет- ра bi as нет, реализация OpenGL сама выберет уровень детализации. Если текстура не множественная, она используется непосредственно; если же множественная текстура используется фрагментным шейдером, уровень детализации вычисляет- ся автоматически и используется затем для поиска по текстуре. Если же такая тек- стура используется вершинным шейдером, всегда будет прочитана основная текстура. Встроенные функции, заканчивающиеся на Lod, доступны только из вершин- ного шейдера. В них параметр 1 od и есть уровень детализации. Встроенные функ- ции, заканчивающиеся на Proj. можно использовать для текстурирования с уче- том проекции. Это позволяет текстуре быть спроецированной на объект прибли- зительно таким же способом, как это делает проектор со слайдами. Эту возмож- ность, кроме всего прочего, используют также для составления карт теней при рендеринге теней. В последующих главах использование некоторых из этих функций будет ил- люстрировано примерами. С теми возможностями программируемости, которые предоставляет язык шейдеров OpenGL, текстурная память может использоваться не только как хранилище изображений текстур. Приведенные далее функции обес- печивают быстрый и гибкий доступ к этим данным, что позволяет с их помощью реализовывать различные эффекты (табл, 5.7).
5.7. Функции доступа к текстуре 135 Таблица 5.7. Функции доступа к текстуре Синтаксис Описание vec4 texturelD (samplerlD sampler. float coord [. float bias]) vec4 texturelOProj (samplerlD sampler, vec2 coord [. float bias]) vec4 texturelOProj (samplerlD sampler, vec4 coord [, float bias]) vec4 texturelDLod (samplerlD sampler. float coord, float lod) vec4 texturelDProjLod (samplerlD sampler. kc2 coord, float lod) vec4 texturelDProjLod (samplerlD sampler. vec4 coord, float lod) vec4 texture2D (sampler2D sampler. vec2 coord [. float b/as]) vec4 texture2DProj (sampler2D sampler. vec3 coord [. float bias]) vec4 texture2DProj (sampler2D sampler. yec4 coord [, float bias]) vec4 texture2DLod (sampler2D sampler. vec2 coord, float lod) vec4 texture2DProjLod (sampler2D sampler. vec3 coord, float lod) vec4 texture2DProjLod (sampler2D sampler. vec4 coord, float lod) vec4 texture3D (sampler3D sampler. vec3 coord [. float bias]) vec4 texture3DProj sampler3D sampler. vec4 coord [. float bias]) vec4 texture3DLod (sampler3D sampler. vec3 coord. float lod) vec4 texture3DProjLod (samplerSD sampler,vec4 coord, float lod) vec4 textureCube (samplerCube sampler. vec3 coord [. float bias]) vec4 textureCubeLod (samplerCube sampler. vec3 coord, float lod) vec4 shadowlD (samplerlDShadow sampler. vec3 coord [, float bias]) vec4 shadow2D (sampler2DShadow sampler. vec3 coord [, float bias]) vec4 shadowlDProj (samplerlDShadow sampler, vec4 coord [. float bias]) vec4 shadow2DProj (sampler2DShadow sampler. vec4 coord [. float bias]) vec4 shadowlDLod (samplerlDShadow sampler. vec3 coord. float lod) vec4 shadow2DLod (sampler2DShadow sampler. vec3 coord. float lod) vec4 shadowlDProjLod (samplerlDShadow sampler. vec4 coord. float lod) vec4 shadow2DProjLod (sampler2DShadow sampler. vec4 coord, float lod) Текстурная координата coord используется для поиска по 1 D-текстуре, указанной семплером. Для версии функции с проецированием («Proj») текстурная координата coord.s делится на последний компонент coord. При ис- пользовании типа coord vec4 второй и третий компоненты coord игнорируются Текстурная координата coord используется для поиска по 20-текстуре, указанной семплером. Для версии функ- ции с проецированием («Proj») текстур- ная координата (coord, s. coord, t) де- лится на последний компонент coord. При использовании типа coord vec4 третий компонент coord игнорируется Текстурная координата coord использует- ся для поиска по ЗО-текстуре, указанной семплером. Для версии функции с прое- цированием («Proj») текстурная коорди- ната делится на coord.q Текстурная координата coOrd исполь- зуется для поиска по кубической тексту- ре, указанной семплером. Направление coord учитывается при выборе грани, по которой выполняется поиск двухмер- ной текстуры Текстурные координаты coord исполь- зуются для поиска по глубине в текстуре глубины, указанной семплером. Третий компонент coord (coord.р) сравнивается со значением, считанным из текстуры глубины. Текстура, связанная с семпле- ром, должна быть текстурой глубины, иначе результаты будут неопределенны- ми. Для версии функции с проецировани- ем («Proj») текстурная координата де- лится на coord, q, представляя значение глубины coord.p/coord, q. Второй компо- нент coord игнорируется для lD-вариан- тов функции
136 Глава 5. Встроенные функции Если текстурная функция без вычисления глубины вызывается для семплера, текстура которого включает в себя глубину, результаты непредсказуемы. То же самое верно, если текстурная функция с вычислением глубины применяется для текстуры без глубины. 5.8. Функции обработки фрагмента Функции обработки фрагментов доступны только для фрагментных шейдеров, тех, что будут использоваться во фрагментном процессоре. В эту категорию попадают три функции, две из которых используются для получения производных, а тре- тья — для оценки ширины фильтра, используемого в текстурах с антиалиасингом. Функции производных, dFdx и cFdy, определяют скорость изменения результа- та математического выражения (скорость изменения функции в математическом смысле). Функция dFdx(p) вычисляет производную выражения р в направлениях оконных координат, а функция dFdy( р) вычисляет производную выражения р в на- правлении у оконных координат. Эти значения показывают, как быстро меняется значение функции в оконном пространстве, и эта информация оказывается полез- ной для предотвращения неровностей изображения. Например, если текстурные координаты меняются слишком быстро, возможно, будет лучше устанавливать цвет как средний по текстуре, чтобы избежать эффекта наложения. Применение этих функций имеет смысл только в отношении выражений, кото- рые различаются от фрагмента к фрагменту. Так как значение uniform-перемен- ной от одного пиксела к другому не меняется, производная и по х, и по у всегда будет иметь значение 0 (табл. 5.8). Таблица 5.8. Функции обработки фрагмента Синтаксис Описание float dFdx (float р) vec2 dFdx (vec2 p) vec3 dFdx (vec3 p'l vec4 dFdx (vec4 p) float dFdy (float p) vec2 dFdy (vec2 p) vec3 dFdy (vec3 p) vec4 dFdy (vec4 p) float fwidth (float p) vec2 fwidth (Vec2 p) vec3 fwidth (vec3 p) vec4 fwidth (vec4 p) Возвращает значение производной по х для аргумента р Возвращает значение производной по у для аргумента р Возвращает сумму абсолютной производной по х и /для аргумента р, то есть return = abs(dFdx( р)) + abs(dFdy( р)) 5.9. Функции шума Функции шума (табл. 5.9) можно применять как для вершинных, так и для фраг- ментных шейдеров. Эти вероятностные (стохастические) функции, когда-то опи- санные Кеном Перлинем, могут быть использованы для визуального увеличения сложности. Значения этих функций придают изображению эффект случайности, но они не случайные по-настоящему. Более полное описание и назначение этих функций можно найти в главе 12,
ЫО. Итоги 137 Таблица 5.9, Функции шума | Синтаксис Описание I float noisel (float х) float noisel (vec2 x) | float noisel (vec3 x) float noisel (vec4 x) Возвращает значение шума для одномерной координаты, основанное на входном значении х 1 vec2 noise2 (float x) vec2 noise2 (vec2 x) 1 vec2 noise2 (vec3 x) vec2 noise2 (vec4 x) Возвращает значение шума для двухмерной координаты, основанное на входном значении х 1 vec3 noise3 (float x) vec3 noise3 (vec2 x) 1 vec3 noise3 (vec3 x) vec3 noise3 (vec4 x) Возвращает значение шума для трехмерной координаты, основанное на входном значении х 1 vec4 noise4 (float x) vsc4 noise4 (vec2 x) 1 vec4 noise4 (vec3 x) vec4 noise4 (vec4 x) Возвращает значение шума для четырехмерной координаты, основанное на входном значении х I Встроенные функции шума подчиняются следующим правилам: I Q Возвращаемые значения всегда находятся в диапазоне [-1, 1]. 0 Среднее значение результатов некоторого количества вызовов функции долж- [ но приближаться к 0. □ Возвращаемые значения воспроизводимы, то есть при заданном входном зна- чении всегда получается одно и то же выходное значение. О Возвращаемые значения статистически инвариантны к вращению (не важно, как будет вращаться область, значения имеют такой же статистический ха- рактер). □ Возвращаемые значения статистически инвариантны к преобразованиям (не важно, как будет преобразовываться область, значения имеют такой же статис- тический характер). □ Обычно после преобразования возвращаемые значения изменяются. □ У функций есть узкое ограничение по частоте (нет видимых элементов, кото- )• рые выделялись бы сильно в большую или меньшую сторону, выходя за какой- то узкий диапазон). □ функции везде С-непрерывны вместе со своей, первой производной. 5.10. Итоги Язык шейдеров OpenGL содержит богатый набор встроенных функций. Некото- рые из них похожи на аналогичные в С и C++, другие похожи на функции из RenderMan. Эти функции предназначены для расширения аппаратной функцио- нальности (например, доступа к текстурам) или для поддержки стандартных опе- раций (например, вычисления квадратного корня, приведения значения к задан- ному диапазону и т. д.), либо они представляют операции, которые будут ускорены аппаратно в будущих версиях графических ускорителей (тригонометрические функции, шум). Здесь в большом объеме используется перегрузка функций, так как многие из них должны выполнять операции как над векторами, так и над скалярными чи- слами. Производители, поддерживающие язык шейдеров OpenGL, ожидают, что
138 Глава 5. Встроенные функции реализация этих функций будет оптимальной, так как рекомендуется, чтобы раз- работчики использовали эти функции, а не делали вычисления самостоятельно. Встроенные математические функции можно использовать несколько неожи- данным способом, например, для создания текстур, что можно будет увидеть в при- мерах шейдеров, приводимых в следующих главах этой книги. При этом визуали- зация функции может быть жизненно необходима в процессе разработки, так как она позволяет оценить полезность функции для реализации какого-либо эффекта и сделать правильный выбор. 5.11. Ссылки Чтобы рассмотреть примеры применения встроенных функций, описанных в этой главе, просто продолжайте читать книгу. Некоторые дополнительные подробнос- ти можно найти в книге [2]. Различные встроенные функции языка шейдеров OpenGL, включая функции производных и ширины фильтра, были заимствованы из языка RenderMan, смысл применения этих функций обсуждается в книгах [7 и 1], За подробным описанием функций шума обращайтесь к заметкам Перлина [3 и 4], а также к дополнитель- ным ссылкам из главы 12. 1. Apodaca A. A., Gritz L. Advanced RenderMan: Creating CGI for Motion Pictures. San Francisco: Morgan Kaufmann Publishers, 1999 (http://www.bmrt.org/arman/ materials.html). 2. Kessenich J., Baldwin D., Rost R. The OpenGL Shading Language, Version 1.051/ 3Dlabs. 2003 (http://www.3dlabs.com/support/developer/ogl2). 3. Perlin K. An Image Synthesizer//Computer Graphics (Proc. SIGGRAPH-85). 1985. July. P. 287-296. 4. Perlin K. Improving Noise// Computer Graphics (Proc. SIGGRAPH-2002). 2002. July. P. 681-682 (http://mrl.nyu.edu/perlin/paper445.pdf). 5. Pixar, The RenderMan Interface Specification, Version 3.2 / Pixar. 2000 (http:// renderman.pixar.com/products/rispec/index.htm). 6. Segal M., Akeley K. The OpenGL Graphics System: A Specification (Version 1.5)/ Ed.: Ch. Frazier (v. 1.1), J. Leech (v. 1.2-1.5). 2003 (http://opengl.org). 7. Upstill S. The RenderMan Companion: A Programmer’s Guide to Realistic Computer Graphics. Reading, MS: Addison-Wesley, 1990. 8. Zwillinger D. CRC Standard Mathematical Tables and Formulas. 30th ed. CRC Press, 1995 (http://geom.math.uiuc.edu/docs/reference/CRC-formulas).
, Пример простого t шейдера Рассмотрим простой пример. Предположим, необходимо наложить на объект изоб- ражение кирпичной стены. Этот шаблон будет вычисляться фрагментным шей- дером. Если вы хотите побыстрее перейти к обсуждению API для определения шейдеров и управления ими, то эту главу можно пропустить. Пример, как и большинство примеров, приводимых в этой книге, состоит из трех основных компонентов; исходного кода вершинного шейдера, исходного кода фрагментного шейдера и кода приложения, которое инициализирует и использу- ет эти шейдеры. В данной главе основное внимание уделяется именно шейдерам, акод приложения можно найти в разделе 7.11, после обсуждения API языка шей- деров OpenGL. В первом примере будут подробно рассмотрены все детали — это поможет луч- ше усвоить информацию; в последующих примерах подробно будут рассматри- ваться только новые возможности, которых не было в предыдущих примерах. .1. Обзор шейдера кирпичной стены Один из приемов написания шейдеров состоит в том, чтобы сначала описать же- лаемый эффект, а потом определить, какие части функциональности должны быть реализованы в вершинном шейдере, а какие — во фрагментом, и как связать эти части через приложение. В данном примере будет показан шейдер, который применяет шаблон кирпич- ной стены ко всем объектам, рисуемым на экране. Здесь не делается попытка со- здать что-либо, выглядящее как настоящая кирпичная стена, а реализуется про- стой шейдер для иллюстрирования принципов, описанных в предыдущих главах, Так что поверхность будет нарисована алгоритмически, а не взята из текстуры. Если усложнить алгоритм рисования шаблона некоторыми изменяемыми пара- метрами, можно добиться интересных эффектов и некоторой гибкости. Итак, вот описание результата, который нужно получить. □ Простой источник света. □ Характеристики рассеянного и отраженного света. □ Шаблон кирпичной стены, основывающийся на пространственных координа- тах объекта, где координатах обозначает измерение кирпичей по горизонтали, а координата у — измерение по вертикали.
140 Глава 6. Пример простого шейдера □ Каждый следующий ряд кирпичей сдвинут относительно предыдущего на по- ловину ширины кирпича. □ Возможность задавать цвет кирпичей, промежутков между ними, расстояния между кирпичами по горизонтали и вертикали, соотношение горизонтального расстояния и длины кирпича, соотношение вертикального расстояния и высо- ты кирпича. Геометрические параметры кирпичей (рис. 6.1) — некоторые размеры и их со- отношения с другими параметрами — будут храниться в определенных разработчи- ком uniform-переменных типа vec2. Ширина кирпича по горизонтали, включая промежуток между кирпичами, будет обозначаться BnckSize.x. Высота кирпича, включая промежуток, будет обозначаться BrlckSize.у. Эти две величины целесооб- разно сохранять в пространственных координатах объекта. Чтобы определить раз- меры (высоту и ширину) кирпича без промежутков, необходимо знать процент- ное соотношение общих размеров из Вri ckSi ze и размеров кирпича. Это соотношение хранится в BrickPct ,х для горизонтальных размеров и в BrickPct .у для вертикаль- ных размеров. Последние две величины должны входить в диапазон [0,1 ]. И нако- нец, цвет кирпича и промежутков представлен переменными Bri ckCol or и MortarCol or. Рис. 6.1. Параметры кирпича Теперь, когда определены желаемый результат и параметры, можно присту- пить к созданию вершинного и фрагментного шейдеров. 6.2. Вершинный шейдер В вершинном шейдере должны выполняться операции над каждой вершиной. Чтобы создать вершинный шейдер для данного примера, нужно ответить на три вопроса. 1. Какие данные необходимо передавать вершинному шейдеру для каждой вер- шины (в attribute-переменных)?
6,2. Вершинный шейдер 141 2. Какие глобальные переменные состояния понадобятся вершинному шейдеру (uniform-переменные)? 3. Что явится результатом вычислений в вершинном шейдере (varying-переменные)? Рассмотрим эти вопросы по отдельности. Если не задать координаты каждой вершины, то вообще невозможно будет что- либо нарисовать. Более того, освещение поверхности объекта не вычислить, если ие будут указаны нормали для каждой координаты, в которой нужно вычислять освещение. Так что минимальными входными параметрами будут координаты и нормаль для каждой вершины. Эти параметры уже определены в OpenGL как встроенные переменные gl_Vertex и gl_Normal. Если приложение будет использовать стандартные функции для задания координат и нормали, то никаких дополни- тельных attribute-переменных в вершинном шейдере объявлять не нужно, а чтобы получить эти значения в шейдере, следует просто обратиться Kgl_Vertex и gljtormal. Для реализации алгоритма рисования кирпичной стены понадобится доступ к не- которым параметрам состояния OpenGL. Например, таким параметром является те- кущая матрица модели-вида-проекции (gljfodel ViewProjectionMatri x) для преобра- зования координат вершин в пространство координат отсечения. Нужно получить доступ к текущей матрице модели-вида (gl _Model Vi ewMatri x) для преобразования координат вершин в пространство обзора при вычислении освещения. Также необ- ходимо будет преобразовать нормали в пространство обзора, используя матрицу •преобразования нормали (glNc'ml Mat ri х, которая создается просто обратным тран- спонированием верхнего левого угла матрицы gl_Model ViewMatriх размером 3x3). Понадобятся нам и координаты источника освещения. Конечно, можно исполь- зовать обычные переменные OpenGL, но, чтобы продемонстрировать использо- вание uniform-переменных, определим координаты источника освещения как та- кую переменную1: uniform vec3 LightPosltior; Необходимо также определить количество света, рассеиваемого поверхностью, и количество отражаемого света. Можно было бы определить uniform-переменные, чтобы задавать эти величины из приложения, ио здесь они определяются как кон- станты, просто чтобы продемонстрировать дополнительные возможности языка: const float SpecularContrlbution =0.3; const float DiffuseContribution = 1.0 - SpecularContribution; И наконец, нужно определить значения для передачи фрагментному шейдеру. Каждый вершинный шейдер должен вычислить однородные координаты верши- ны и сохранить их во встроенной переменной gl Position, и в данном шейдере тоже нужно это сделать. Шаблон кирпичей будет вычисляться во фрагментном шейдере «на лету» как результат вычисления некоей функции с аргументами х и у — геометрическими значениями координат в модельной системе, специально для этого объявлена varying-переменная MCposition. Чтобы применить эффект освещения к верхней части кирпича, некоторая часть вычислений для освещения будет выполняться во фрагментном шейдере. Но большая часть этих вычислений 1 В шейдерах, рассматриваемых в этой книге, принято писать имена, определенные разработчиком uniform-, varying- и attribute-переменных с большой буквы, чтобы отличать их от локальных или обыч- ных глобальных переменных.
142 Глава б. Пример простого шейдера все же будет сделана в вершинном шейдере, который будет возвращать вычис- ленную силу света фрагментному шейдеру в varying-переменной Lightintensity. Вот как определяются эти две varying-переменные: varying float Lightintensity: varying vec2 MCpositlon; Теперь все готово для написания вершинного шейдера. Начнем с объявления точки входа для вершинного шейдера и вычисления координат вершины в про- странстве обзора: void main(void) { vec3 ecPosition = vec3 (gl_ModelV1ewMatrix * gl_Vertex); В первой строке кода вершинного шейдера объявляется переменная ecPosition, в которую будут занесены вычисленные координаты вершины в пространстве обзора. Эти координаты вычисляются из значений glj/entex, преобразованных с помощью текущей матрицы модели-вида gl _ModeI Vi ewMatrl x. Так как один из опе- рандов матрица, а второй — вектор, оператор * выполняет математическое, а не покомпонентное умножение. Результат умножения матрицы на вектор будет типа vec4, однако ecPosition объявлена как vec3. Так как в языке шейдеров OpenGL нет автоматического пре- образования типов, его приходится выполнять явно с использованием конструк- тора. При этом четвертый компонент вектора отбрасывается и типы операндов оператора присваивания будут совместимыми. (Конструкторы, помимо прочего, выполняют еще и некоторые преобразования типов; более подробно об этом гово- рилось в разделе 3.3.) При дальнейших вычислениях координаты вершины в про- странстве обзора будут использоваться для вычисления освещения. Вычислить освещение в этом примере совсем не сложно. Какая-то часть света из источника будет рассеиваться (то есть отражаться во всех направлениях). В на- правлениях, близких к направлению отражения от источника освещения, будет наблюдаться зеркальное отражение. Чтобы вычислить рассеивание света, нужно определить угол между поступающим лучом света и нормалью поверхности. Что- бы вычислить отражение света, нужно определить угол между направлением от- ражения и направлением обзора. Сначала преобразуем нормаль: vec3 tnorm = normal1ze(gl_NormalMatrix * gl_Normal); Эта строка объявляет новую переменную tnorm для сохранения значений пре- образованной нормали (в OpenGL, как и в C++, можно объявлять переменные непосредственно перед использованием). Входные значения нормали (gl _Normal, встроенная переменная для доступа к значениям нормали) преобразуются с помо- щью матрицы преобразования нормали, определенной в OpenGL (gl_Normal Matri х). Вектор, полученный в результате преобразования, нормализуется, то есть приво- дится к вектору единичной длины, встроенной функцией normalize, и результат нормализации сохраняется в tnorm. Затем вычисляется вектор из заданной точки на поверхности трехмерного объекта до источника освещения. Оба аргумента должны быть определены в про- странстве координат обзора (это значит, что приложение должно задать значения uniform-переменной LightPosI 11 on в пространстве координат обзора). Вычисле- ние вектора направления освещения происходит так: vec3 lightVec = normal ize(LightPositi on - ecPosition):
$.2. Вершинный шейдер 143 Заранее определенные координаты объекта в пространстве обзора находятся в ecPosi tl on. Чтобы вычислить вектор направления освещения, нужно вычесть ко- ординаты объекта из координат источника освещения. Результат вычислений так- же приводится к единичному вектору и сохраняется в переменной 11 ghtVec. Проведенные вычисления — подготовка к использованию встроенной функ- ции reflect. Из преобразованной нормали поверхности и вычисленного вектора освещения можно получить вектор отражения от поверхности объекта; функция refl ect требует вектор освещения (направление от источника освещения к поверх- ности), и так как мы вычислили направление от поверхности к источнику осве- щения, вектор 11ghtVec просто инвертируется: vec3 reflectVec = reflect(-lightVec. tnorm); Так как оба вектора, используемые в этой операции, — единичные, то резуль- тирующий вектор также будет единичным. Чтобы завершить вычисление осве- щения, нужен еще один вектор — единичный вектор, определяющий направле- ние обзора. Так как по определению точка обзора является началом координат (0,0,0) в пространстве координат обзора, нужно всего лишь инвертировать и при- вести к единичному вектору вычисленные координаты в пространстве обзора (ecPosi tl on): vec3 viewVec = normalize(-ecPosltion); С вычисленными векторами уже можно вычислять освещение конкретной вер- шины. Соотношения этих векторов показаны на рис. 6.2. Рис, 6.2. Векторы для вычисления освещения в вершинном шейдере кирпичной стены Рассеянное отражение создается следующим образом: допускается, что падаю- щий на поверхность луч света рассеивается во все стороны по принципу распреде- ления по функции косинуса. Отражение света будет наибольшим при совпадении вектора направления освещения и нормали поверхности. С увеличением угла до 90° рассеянное отражение будет уменьшаться до нуля. Так как оба вектора — единичные, косинус угла между 11 ghtVec и tnorm определяется их скалярным
144 Глава 6. Пример простого шейдера произведением. Чтобы рассеянное отражение было нулевым при угле между на- правлением освещения и нормалью поверхности больше 90° (то есть когда источник освещения расположен с обратной стороны объекта), используется функция max: float diffuse = max(dot(lightVec. tnorm). 0.0): Компонент зеркального отражения для вершины вычисляется так: float spec ’0.0; if (diffuse > 0.0) { spec = max(dot(reflectVec. viewVec), 0.0); spec = pawfspec. 16.0): } Переменная для значения зеркального отражения определена и инициализи- рована нулем. Здесь будет вычисляться только значение отражения, отличное от нуля, если угол между нормалью и направлением освещения больше 90°, так как если источник освещения расположен за объектом, отражение света не происходит. Поскольку оба вектора, reflectVec и viewVec, — единичные, скалярное произведе- ние (dot) этих двух векторов равняется косинусу угла между ними. Если угол бли- зок к нулю (то есть вектор отражения и вектор обзора практически совпадают), результат будет приближаться к 1,0, Возведением результата в степень 16 в сле- дующей строчке кода достигается усиление резкости изображения, и это приводит к тому, что ярко выраженное отражение света будет происходить только в облас- ти, где вектор отражения и вектор обзора практически совпадают. Число 16 в дан- ном случае выбрано произвольно. При увеличении этого значения область отра- жения сужается, при уменьшении — расширяется, и отражение становится не таким концентрированным. Это значение можно сделать параметром шейдера, чтобы в приложении можно было задавать его через uniform-иеременную. Осталось только умножить вычисленные значения рассеянного и прямого отра- жений на константы di ffuseContri bution и specul a rContri but 1 on и сложить результаты: Lightintensity = DiffuseContribution * diffuse + SpecularContribution * spec: Результат будет занесен в переменную Lightintensity и затем интерполирован между вершинами. Вычислить значение еще одой varying-переменной легко: MCposition = cT’Verlex.xy: Когда такой шаблон кирпичей уже наложен на геометрический объект, рисунок не должен сдвигаться по отношению к объекту, как бы ни менял свое расположе- ние объект и как бы ни двигалась точка обзора. Чтобы совместить это требование с алгоритмическим вычислением рисунка поверхности во фрагментом шейдере, необходимо знать координаты каждого фрагмента, которые определяют его поло- жение на поверхности объекта. В этом примере зададим модельные координаты каждой вершины, устанавливая varying-переменную MCpcsi tion в то же значение, что и исходные координаты вершины (которые по определению должны задаваться в пространстве координат объекта). Во фрагментном шейдере не используются координаты г или ®, так что нужен способ извлечь компоненты х и у из gl_Vertex. Мы могли бы использовать здесь конструктор (то есть написать vec2(gl_Vertex)), но, чтобы продемонстрировать еще одну возможность языка, выберем первые два компонента gl_Vertex (.ху) и затем сохраним их в переменной MCposition.
6.3. Фрагментный шейдер 145 Единственное, что нам осталось, — вычислить однородные координаты вер- шины (это должен выполнять каждый вершинный шейдер). Это достигается пре- образованием входных координат вершины с помощью матрицы модели-вида- проекции с использованием встроенной функции ^transform: gl_Position = ftransformO; Полный код созданного вершинного шейдера приведен в листинге 6.1. Листинг 6.1. Исходный код шейдера кирпичной стены uniform vec3 LightPosition; const float SpecularContribution = 0.3: const float DiffuseContribution = 1.0 - SpecularContribution: varying float Ligh.tlntensity; varying vec2 MCposition: void main(vold) { vec3 ecPosition = vec3 (g.l_ModelViewHatrix * gl_Vertex): vec3 tnorm = normalize(gl_NormalMatrix * gl_Normal); vec3 lightVec ~ normalize(LightPosition - ecPositlon); vec3 reflectVec = ref1ect(-1ightVec. tnorm): vec3 viewVec = normalize(-ecPosltion); float diffuse = max(dot(lightVec. tnorm). 0.0): float spec =0.0: if (diffuse > 0.0) spec - max(dot(reflectVec. viewVec). 0.0): spec = powtspec, 16.0); Lightintensity = DiffuseContribution * diffuse + SpecularContribution * spec; HCposition = gl_Vertex.xy; gl_Position = ftransformO: ) 6.3. Фрагментный шейдер Назначение фрагментного шейдера — вычислять цвет для фрагмента, или значе- ние глубины для него, или и то и другое. В данном случае (и на самом деле в боль- шинстве фрагментных шейдеров) будет вычисляться только цвет фрагмента, а значение глубины уже вычислено на этапе растеризации OpenGL. Так что на- значение данного шейдера — вычислить цвет текущего фрагмента. Фрагментный шейдер кирпичной стены начинается с определения некоторых uniform-переменных, отличающихся от определенных в вершинном шейдере. Ри- сунок кирпичей был параметризирован именно для того, чтобы было легче изме- нять его. Эти параметры, являющиеся постоянными для конкретного примитива,
146 Глава 6. Пример простого шейдера задаются в uniform-переменных и инициализированы (а позже изменены) прило- жением. При этом в приложении будет легко сделать так, чтобы эти параметры задавал пользователь — например, ползунком или в окне выбора цвета. Данный фрагментный шейдер использует параметры, показанные на рис. 6.1. Они опреде- лены как uniform-переменные: uniform vec3 BrickColor. MortarColor; uniform vec2 BrickSize; uniform vec2 BrickPct: Для того чтобы рисунок не смещался относительно объекта при движении, нужно «привязать» его к объекту. Начало фрагмента рисунка определяется в про- странстве координат объекта, вычисляется вершинным шейдером и передается через varying-переменную MCposition: varying vec2 MCposition; Эта переменная вычисляется при обработке каждой вершины в вершинном шей- дере, а потом интерполируется в пределах примитива и становится доступной фраг- ментному шейдеру во время работы с каждым фрагментом. Фрагментный шейдер использует это значение, чтобы определить, как положение фрагмента соотносится с рисунком из кирпичей. Еще одна входная varying-переменная объявлена так: varying float Lightintensity; Эта переменная содержит интерполированное значение интенсивности осве- щения, которое вычисляется для каждой вершины в вершинном шейдере. Следу- ет обратить внимание на то, что обе varying-переменные, определенные в этом фрагментном шейдере, определены с точно такими же типами, как соответствую- щие переменные в вершинном шейдере. Если типы не совпадают, происходит ошибка компоновки. После определения всех необходимых uniform- и varying-переменных опреде- ляется код фрагментного шейдера: void main (void) { vec3 color; vec2 position, useBrick; Вычисления, выполненные в этом шейдере, очень похожи на обычные вычис- ления на языке С, а все локальные переменные будут определены до их использо- вания в начале функции mat г. В некоторых случаях это делает код более удобочи- таемым и понятным, но в основном такие определения зависят от предпочтений разработчика. Первая строчка кода в примере будет вычислять значения для ло- кальной переменной position типа vec2: position = MCposition / BrickSize; В этой строке происходит деление координаты х фрагмента в модельных коор- динатах на ширину столбца, а координаты у фрагмента — на ширину строки. По- лучаем «номер ряда» (position.у) и «номер кирпича» (position.х). Это знаковые значения, числа с плавающей запятой, так что можно получить отрицательные номера кирпичей и рядов. Определим, нужно ли рисовать данный ряд со смеще- нием (каждый второй ряд смещается на полкирпича): if (fracttposition.у * 0.5) > 0.5) position.х += 0.5;
6.3. Фрагментный шейдер 147 «Номер ряда» (position .у) умножается на 0,5, и результат сравнивается с 0,5. Через ряд это значение будет получаться true, и «номер кирпича» (position.х) увеличивается на 0,5, чтобы сместить ряд с нечетным номером на половину кир- пичика. Теперь нужно вычислить координаты фрагмента по отношению к теку- щему кирпичу. position = fract(position); Это вычисление дает нам горизонтальную и вертикальную координаты отдель- ного кирпича, которые будут использоваться для определения того, следует ли использовать для закраски цвет кирпичей или цвет промежутка между ними. Чтобы упростить выбор цвета, понадобится также функция, возвращающая 1,0 на самом кирпиче и 0 — на промежутке между кирпичами. Так как у функции рисования кирпичей есть и горизонтальный, и вертикальный компоненты, то функции выбора цвета будет две: для горизонтального и для вертикального ком- понентов. Для получения искомого цвета результаты выполнения обеих функ- ций перемножаются. Если результат хотя бы одной функции 0 (цвет промежут- ка), то окончательный результат — 0; в противном случае результатом будет 1,0 (цвет кирпича). Для получения такой функции можно использовать функцию step (edge. х). Онаполучает два аргумента: край (edge) и параметр для повторной проверки этого края. Если значение параметра меньше или равно значению границы, функция возвращает 0; в противном случае — 1,0 (см. рис. 5.11). Обычно функцию step ис- пользуют для получения импульсов (прямоугольных колебаний), при этом функ- ция начинается с 0 и поднимается до 1 при достижении некоего порога. Для дан- ной задачи можно получить функцию, значения которой начинаются с 1,0 и затем падают до 0. Этого можно достичь перестановкой ее аргументов: useBrick = steptposition. BnckPct); Здесь вычисляются два значения, по которым можно узнать, находится ли дан- ная точка на кирпиче или вне его, позиции прослеживаются в горизонтальном (useBri ck.х)и вертикальном (usеВг 1 ск. у) направлениях. Встроенная функция step будет возвращать 0 при условии Bri ckPct. х <= ppsiti on. x и 1,0 при условии BrickPct. x > posit i on. x. Функция tract обеспечивает попадание значения position.x в диапа- зон [0, 1). Переменная BrickPct — типа uniform, так что ее значение будет посто- янным в пределах одного примитива. Это определяет, что значение useBr i ck. х будет равно 1,0 для кирпича и 0 — для промежутка между кирпичами (по горизонтали). То же самое выполняется для вертикального направления, pos i t i on. у и Bri ck Pct. у используются для того, чтобы вычислить значение useBri ck .у. Умножив useBrick. х на useBrick .у, получим 0 или 1,0, что позволит выбрать цвет фрагмента. Периоди- ческая функция для горизонтального компонента шаблона кирпичной стены при- ведена на рис. 6.3. Значения BrickPct.х и BrickPct .у можно вычислить приложением, чтобы полу- чить ширину промежутков между кирпичами в обоих направлениях, основыва- ясь на соотношении высоты ряда и ширины столбца; эти значения можно выби- рать и произвольно. Остается только вычислить окончательное значение цвета и сохранить его в особой переменной gl _JragCol or: color “ mix(MortarColor. BrickColor. useBrick.x * useBrick.y); color *= Lightintensity:
148 Глава 6. Пример простого шейдера gl_FragColar = vec4 (color. 1.0): } Рис. 6.3. Периодическая функция шага для горизонтального компонента шаблона кирпичной стены Здесь цвет фрагмента вычисляется и сохраняется в локальной переменной col or. Встроенная функция mi х используется для выбора цвета кирпича или про- межутка в зависимости от результата useBrick. х * useBrick.у. Так как useBrick.х и useBri ck .у могут иметь значения только 0 или 1,0, цвет кирпича можно получить, только если оба значения — 1,0, а иначе в результате получится цвет промежутка. Затем вычисленное значение умножается на интенсивность освещения, и ре- зультат сохраняется в локальной переменной color. Эта переменная — типа vec3, a gl_FragColor определен как vec4, так что окончательное значение цвета будет получено с помощью конструктора. Он добавит четвертый компонент (прозрач- ность), равный 1,0, и занесет результат во встроенную переменную gl_FragColor. Весь исходный код фрагментного шейдера приведен в листинге 6.2. Листинг 6.2. Исходный код фрагментного шейдера кирпичной стены uniform vec3 BrickColor. MortarColor; uniform vec2 BrickSize; uniform vec2 BrickPct: varying vec2 MCposition: varying float Lightintensity: void main(void) { vec3 color; vec2 position. useBrick: position = MCposition / BrickSize: if (fract(position,y * 0.5) > 0.5) position.x +- 0.5; position = fract(position); useBrick = step(position. BrickPct);
6,4. Замечания 149 color = nnxCMortarColor. BrickColor. useBrick.x * useBrlck,y).; color *= Lightintensity: gl_FragColor = vec4 (color. 1.0): При сравнении этого шейдера с вершинным шейдером из предыдущего при- мера видно, что основные возможности языка шейдеров OpenGL, использован- ные в них, почти одинаковы. Оба шейдера используют функцию main, некоторые uniform-переменные, некоторые локальные переменные, выражения; встроенные функции вызываются одинаково; конструкторы используются одинаково и т. д. Небольшие различия заключаются в следующем: а) вершинный шейдер работает свстроенными атрибутами gl_Vertex и gl_Normai; б) вершинный шейдер пишет во встроенную переменную gl_Positior, в то время как фрагментный шейдер пишет во встроенную переменную gl FragCo'l or; в) varying-переменные устанавливают- ся в вершинном шейдере и считываются фрагментным шейдером. Код приложения, в котором создаются и используются эти шейдеры, при- веден в разделе 7.11 после описания API языка шейдеров OpenGL. Результат рендеринга нескольких простых объектов показан на рис. 6.4 (см. также цвет- ной рис. 25). Рис. 6.4. Плоский многоугольник, шар и тор, рендеринг которых выполнялся шейдерами кирпичной стенки 6.4. Замечания В приведенном решении задачи есть несколько подводных камней, из-за которых оно может использоваться только в самых простых случаях. Так как шаблон кир- пичной стены вычисляется с использованием модельных координат заданного объекта, видимые размеры кирпичей зависят от размера объекта в модельных ко- ординатах. Некоторые объекты в результате будут выглядеть хорошо, но для дру- гих созданные нами кирпичики будут либо слишком большими, либо слишком маленькими. Чтобы избежать этого, для вершинного шейдера нужно задать пара- метр uniform-переменную, в соответствии со значением которой шаблон будет масштабироваться по отношению к модельным координатам. Приложение могло
150 Глава 6. Пример простого шейдера бы предоставлять пользователю возможность самому задавать масштаб для того, чтобы кирпичный рисунок выглядел хорошо на любом объекте. Так как рисунок привязывается к координатам объекта х и у в модельном про- странстве, изображение может выглядеть нереалистичным на сложных объектах (см. рис. 6.4). Используя только х- и у-координаты объекта, будет сложно рабо- тать, например, с бесконечно глубокими объектами. Кирпичный рисунок выгля- дит хорошо на передней грани объектов, но на боковых гранях глубина кирпичи- ков выглядит несоразмерно большой. Для того чтобы шейдер был по-настоящему трехмерным, в процедуру вычисления нужно добавить третье измерение и исполь- зовать z-компоненту координат в модельном пространстве, чтобы определить цвет точки в измерении z. Если рассмотреть рисунок при увеличении, можно увидеть неровности вдоль границы между кирпичом и промежутком. Они появляются из-за резкого изме- нения значения функции step (с 0 до 1,0) при переходе от кирпичика к промежут- ку. Созданный нами шейдер может выбирать для каждого фрагмента лишь один из двух цветов, и неровности появляются из-за того, что частота семплинга слиш- ком низкая. Чтобы избежать этого, вместо функции step можно использовать встроенную функцию smoothstep. Она похожа на step, за исключением того, что переход от 0 к 1,0 у нее плавный. Это дает эффект размывания границ между кир- пичом и промежутком, отчего деформированный край становится не таким за- метным. Метод аналитического сглаживания описан в разделе 14.4.5. Несмотря на отмеченные недостатки, описанные шейдеры — хороший пример работы Open GL-шейдеров, иллюстрирующий некоторые интересные особеннос- ти языка шейдеров OpenGL. 6.5. Итоги В этой главе некоторые компоненты языка шейдеров OpenGL, описанные в пре- дыдущих главах, были применены для создания шейдеров, накладывающих на объект шаблон кирпичной стенки. Вершинный шейдер преобразует координаты вершин, передаваемые в модельном пространстве координат, и вычисляет интен- сивность освещения в каждой точке. Фрагментный шейдер определяет цвет фраг- мента и совмещает его со значением освещения. После этого окончательное зна- чение помещается в буфер кадров. Исходный код этих двух шейдеров обсуждался строчка за строчкой, чтобы читатели хорошо разобрались в том, как все работает. Эти шейдеры — иллюстрация возможностей языка шейдеров OpenGL, они могут использоваться в качестве исходных для дальнейшей работы. 6.6. Ссылки Шейдер кирпичной стенки, приведенный в данной главе, очень похож на один из шейдеров на языке RenderMan, написанный Дарвином Пичи (Darwyn Peachey) в 2002 г. и представленный в книге [2]. Этот и другие шейдеры можно найти на веб-
6.6. Ссылки 151 сайте компании 3Dlabs, предназначенном для разработчиков. Код OpenGL-шей- деров также доступен. 1, 3Dlabs. Веб-сайт для разработчиков (http://www.3dlabs.com/support/deveLoper). 2. Texturing and Modeling: A Procedural Approach. 3rd ed./D, S. Ebert, J. Hart, B. Mark, at al. San Francisco: Morgan Kaufmann Publishers, 2002 (http://www.tex- turingandmodeling.com). , 3. Kessenich J., Baldwin D., Rost R. The OpenGL Shading Language, Version 1.051 > / 3Dlabs. 2003 (http://www.3dlabs.com/support/developer/ogl2). 4. OpenGL Architecture Review Board: спецификация расширения ARB_vertex_ shader, реестр расширений OpenGL (http://oss.sgi.com/projects/ogL-sampte/registry). 5, OpenGL Architecture Review Board: спецификация расширения ARB_fragrnent_ shader, реестр расширений OpenGL (http://oss.sgi.com/projects/ogL-sampLe/registry). ‘6. OpenGL Architecture Review Board: спецификация расширения ARB_shader_ objects, реестр расширений OpenGL (http://oss.sgi.com/projects/ogL-sampLe/ registry).
API языка шейдеров OpenGL Расширения OpenGL, которые называются ARB_shader^objects, ARB_vertex_shader и ARB_fragment_shader, представляют новые функции и возможности для поддержки шейдеров, написанных на языке шейдеров OpenGL. Этот набор функций в дан- ной книге называется API языка шейдеров OpenGL. Чтобы определить, поддер- живает ли данная реализация OpenGL язык шейдеров, достаточно вызвать функ- цию gl GetString с аргументом GL_EXTENSIONS и проверить, содержит ли возвращаемая строка подстроки GL_ARB_siiader_objects, GL_ARB_yertex_shader и GL_ARB_fragment_shader. Таким образом можно определить и версию языка. Если возвращаемая строка содержит GL_ARB_shading_language_100, значит, версия языка шейдеров — 1.00. В этой главе будут обсуждаться функции для создания, загрузки, компиляции и компоновки шейдеров, а также функции для передачи атрибутов вершины и uniform-переменных шейдерам. Все эти функции перечислены в приложении Б. В конце главы будет обсуждаться код приложения, необходимый для создания и использования шейдера кирпичной стены, рассмотренного в главе 6. Читатель может просмотреть список функций, приведенный в разделе 7.11, а затем вернуть- ся к описаниям функций в деталях. Вот приблизительный сценарий создания и использования шейдеров OpenGL. □ Создать один или несколько пустых шейдерных объектов функцией gl Create- ShaderObjectARB, □ Задать исходный код для этих шейдеров функцией gl ShaderSourceARB. □ Скомпилировать каждый шейдер функцией gl Compi 1 eShaderARB. □ Создать программный объект функцией glCreateProgramObjectARB. □ Присоединить все шейдерные объекты к программному объекту функцией glAttachObjectARB. □ Скомпоновать программный объект функцией gl LinkProgramARB. □ Установить выполняемую программу как часть текущего состояния OpenGL функцией glUseProgramObjectARB. □ Если шейдеры используют uniform-переменные, место их расположения за- прашивается функцией gl Get Un 1 formLocat 1 onARB уже после того, как отработала функция gl LinkProgramARB. Значения uniform-переменных задаются функцией gl Uni formARB. □ Если вершинный шейдер использует определенные разработчиком attribute- переменные, их индексы можно устанавливать до компоновки функцией gl Bl nd-
7.1. Создание шейдерных объектов 153 AttribLocationARB либо они будут назначаться автоматически во время компо- новки и тогда узнать их можно с помощью функции gl Get At t г т bLocati onARB. Про- извольные атрибуты вершин могут быть переданы в вершинный шейдер функ- цией glVertexAttribARB или совместным использованием функций gl Vertex Attгт ЬРо 1 nterARB и gl Enabl eVertexArrayPol nter для массивов вершин. 7.1. Создание шейдерных объектов и API языка шейдеров OpenGL сконструировано так, чтобы использование послед- него походило на работу с языками С и C++: первый этап — создание исходного кода, потом его компиляция и компоновка отдельных модулей и, наконец, выпол- нение кода. Чтобы поддерживать принцип высоко уровневости языка шейдеров OpenGL, нужно обеспечить создание хранилища для исходного кода, скомпилированного и выполняемого кода. Решение проблемы — определение двух новых структур данных (объектов) в OpenGL. В этих объектах могут храниться данные, и опера- ции над объектами (для задания исходного кода, компиляции, компоновки и вы- полнения) уже определены. При создании объекта OpenGL возвращает его иден- тификатор, который затем используется для выполнения операций над объектом, установки или считывания параметров объекта. Сначала нужно создать шейдерный объект. При этом создается структура дан- ных, управляемая OpenGL, которая затем используется для хранения исходного кода шейдера. Шейдер создается функцией GLhandleARB glCreateShaderObjectARBtGLenurn shdderType') Эта функция создает пустой шейдерный объект и возвращает его идентификатор. Шейдерный объект используется для хранения строк исходного кода шейдера. shdderType определяет тип создаваемого шейдера. В данный момент поддержива- ются два типа шейдеров. Шейдер типа GL_VERTEX_SHADER_ARB предназначен для за- пуска на программируемом вершинном процессоре, его выполнение заменяет стандартную обработку вершин в OpenGL. Шейдер типа GL_FRAGMENT_SHADER_ARB предназначен для запуска на программируемом фрагментном процессоре, его выполнение заменяет стандартную обработку фрагментов в OpenGL. При создании шейдерного объекта его параметр GL_OBJECT_TYPE_AR0 устанавлива- ется в GL_SHADER_OBJECT_ARB, a GL_OBJECT_SUBTYPE_ARB устанавливается в значение либо GL_VERTEX_SHADER_ARB, либо в GL_FRAGMENT_SHAOER_ARB — в зависимости от значения shdderType. После того как шейдерный объект создан, в него нужно занести строки с ис- ходным кодом шейдера. Исходный код шейдера представлен в виде массива строк. Команда для определения исходного кода шейдера: void glShaderSourceARBCGLhandleARB shader, GLuint nstrings. const GLcharARB ** strings. GLint * lengths') Эта команда устанавливает исходный код шейдера из массива строк (параметр strings), полностью замещая предыдущий код в объекте. Количество строк в мас- сиве указано в nstrings. Если параметр lengths — NULL, то предполагается, что
154 Глава 7. API языка шейдеров OpenGL каждая строка из strings содержит завершающий символ конца строки. Если же lengths не NU LL, этот параметр указывает на массив, в котором содержатся длины соответствующих строк (не считая нуль-символа), или отрицательное значение (в этом случае соответствующая строка также заканчивается нуль-символом). На данном этапе исходный код шейдера не анализируется, а просто копируется в объ- ект. Приложение может делать все что угодно (модифицировать свою копию исходного кода, удалять ее из strings) сразу же после возврата из этой функции. Передача исходного кода шейдера не в одной, а в нескольких строках имеет следующие преимущества: □ Можно систематизировать некоторые общие части исходного кода. □ Можно использовать совместно для нескольких шейдеров какой-либо «заго- ловочный» код (подобно заголовочным файлам в C++). □ Можно совместно использовать значения #defi пе для контроля процесса ком- пиляции. □ Можно включать в код функции, определенные разработчиком или взятые из других библиотек. 7.2. Компиляция шейдерных объектов После загрузки исходного кода шейдера в шейдерный объект этот исходный код необходимо скомпилировать, чтобы проверить его. Результат компиляции тоже хранится в шейдерном объекте до тех пор, пока не будет выполнена следующая компиляция либо объект не будет удален. Для компиляции шейдерного объекта используется команда void glCompIleShaderARBCGLhandleARB shader) Команда компилирует исходный код, хранящийся в объекте. Результат компи- ляции будет сохранен как часть состояния шейдерного объекта. Это значение будет установлено в GL_TRUE, если не допущено никаких ошибок компиляции, и в GL_FALSE, если ошибки были. Это можно узнать с помощью функции glGet- ObjectParaneterARB с аргументами shader и GL_O0JECT_COMPILE_STATUS_ARB. Шейдер не сможет быть скомпилирован, если его исходный код неправилен лексически, грам- матически или семантически. Независимо от того, была компиляция успешной или нет, информацию о ней можно получить из протокола шейдерного объекта функцией gl GetlnfoLogARB. У языка шейдеров OpenGL есть свои правила компиляции, которые немного различаются в зависимости от типа компилируемого шейдера, так что во время компиляции учитывается, является шейдер вершинным или фрагментный. Информацию о прохождении этой операции можно получить с помощью функ- ции glGetlnfoLogARB (см. раздел 7.5) с параметром shader, но не нужно использовать ее как показатель успешности или неуспешности компиляции. Если компиляция прошла успешно, эта функция возвоатит либо пустую строку, либо информа- цию об операции компиляции. Если же компиляция шейдерного объекта прошла с ошибками, информационный лог будет содержать сообщения о любых лекси- ческих, грамматических или семантических ошибках, а также предупреждения и другую информацию от компилятора.
7,3. Компоновка и использование шейдеров 155 Функция gl Compl 1 eSh a de г ARB возвращает контроль приложению, не ожидая окон- чания компиляции. Все последующие команды, которые влияют на завершение компиляции (например, glLInkPrOgramARB), блокируются до ее завершения. Для того чтобы убедиться, что компиляция прошла успешно, нужно запросить резуль- тат компиляции, воспользовавшись функцией gl GetObjectРа raireterARB. Эта функ- ция будет ожидать окончания компиляции исходного кода и момента, когда ста- туе результата станет доступным. .3. Компоновка и использование шейдеров Каждый шейдерный объект компилируется отдельно. Чтобы создать програм- му, нужен механизм для определения списка шейдерных объектов, которые дол- жны быть скомпонованы в одну программу. Это делается созданием программ- ного объекта и присоединением к нему всех шейдерных объектов для данной программы. Для создания программного объекта следует использовать функцию GLhandleARB glCreateProgramObJectARB(void) Эта функция создает пустой программный объект и возвращает его идентифика- тор. Программным называется объект, к которому могут присоединяться шейдер- ные объекты, чем обозначается, что данные шейдерные объекты будут скомпоно- ваны вместе. Это позволяет также проверить совместимость шейдеров (например, совместимости вершинного шейдера с фрагментным). После того как шейдер- ные объекты больше не нужны как часть программы, их можно отсоединить. После того как программный объект определен, к нему присоединяют шей- дерные объекты. Присоединение в данном случае означает создание ссылки на шейдерный объект, который будет скомпонован. Шейдерные объекты присоеди- няются к программному объекту функцией void glAttachObjectARBCGLhandleARB program, GLhandleARB shader) Функция присоединяет шейдерный объект (shader) к программному объекту (program). Это означает, что шейдер будет обрабатываться операцией компонов- ки данного программного объекта. Не существует ограничения на количество шейдерных объектов, которые мож- но присоединять к программному объекту. Все операции, выполняемые над шей- дерным объектом, действуют независимо от того, присоединен ли шейдерный объект к программному. Шейдерный объект можно присоединять даже перед тем, как загружать в него исходный код, или перед компиляцией. Другими словами, функция glAttachObjectARB просто указывает, какие шейдерные объекты будут компоноваться с программным объектом. Для создания программы все присоединенные шейдерные объекты должны быть скомпилированы, а программный объект скомпонован. Операция компонов- ки определяет расположение uniform-переменных, устанавливает ссылки между скомпилированными шейдерными объектами и проверяет совместимость вершин- ного и фрагментного шейдеров. Для компоновки программного объекта исполь- зуется функция void glLinkProgramARBCGLhandleARB program)
156 Глава 7. API языка шейдеров OpenGL Эта функция компонует указанный программный объект program. Если к объекту присоединены любые шейдерные объекты типа GLJ/ERTEX_SHADER_ARB, они будут использоваться для создания выполняемой программы, которая будет запускаться на программируемом вершинном процессоре. Если к объекту присоединены лю- бые шейдерные объекты типа GL_FRAGMENT_SHADER_ARB, будет создана программа, которая сможет выполняться на программируемом фрагментном процессоре. Результат выполнения компоновки сохраняется как часть состояния программно- го объекта. Это будет 3Ha4eHHe.GL_TRUE при успешной компоновке, когда программ- ный объект уже готов к запуску, или GL FALSE. Этот результат можно запрашивать функцией gl Get Object Ра rameterARB с аргументам и program и GL_OBJECT_LINK_STATUS_ARB. Если операция компоновки выполнена успешно и указанный программный объект уже используется (для него ранее вызывалась функция glUseProgramObjectARB), функция gl Li nkProgramARB установит выполняемые программы как часть текуще- го состояния для рендеринга. В результате успешной компоновки все определенные разработчиком uniform- переменные данной программы program (см. раздел 7.7) устанавливаются в 0, каж- дая из них будет расположена в определенном месте, определить которое можно с помощью функции gl Getllni formLocationARB. Tакже будут назначены индексы тем определенным разработчиком attribute-переменным (см. раздел 7.6), которым они еще не были назначены. Если program содержит шейдерные объекты типа GLVERTEX_SHADER_ARB, но не со- держит шейдерных объектов типа GL_FRAGMENT_SHADER_ARB, вершинный шейдер будет скомпонован как замена неявного интерфейса для стандартной функциональнос- ти. Точно так же, если program содержит шейдерные объекты типа GL_FRAGMENT_ SHADER_ARB, но не содержит шейдерных объектов типа GL_VERTEXJ>HAE)ER_ARB, фраг- ментный шейдер будет скомпонован как замена неявного интерфейса для стан- дартной функциональности. Компоновка программного объекта может оказаться неуспешной по ряду при- чин. Приведем их. □ Количество объявленных attribute-переменных больше, чем поддерживаемых реализацией. □ Количество объявленных uniform-переменных больше, чем поддерживаемых реализацией. □ Функция main отсутствует для вершинного или фрагментного шейдера. □ Varying-переменная, используемая фрагментным шейдером, не объявлена с та- ким же типом или не объявлена вовсе в вершинном шейдере. □ Существует неразрешенная ссылка на функцию или имя переменной (опреде- ление функции или переменной отсутствует). □ Совместно используемая глобальная переменная объявлена с различными ти- пами или начальными значениями. □ Один или более присоединенных шейдерных объектов не скомпилировался. Если операция компоновки прошла успешно, создается программа. Она со- держит выполняемый модуль для вершинного процессора, или для фрагментно- го процессора, или для них обоих. В любом случае вся информация и выполняе- мые программы, задействованные в предыдущей операции компоновки, теряются.
7,3. Компоновка и использование шейдеров 157 После операции компоновки приложения могут изменять присоединенные шей- дерные объекты, компилировать их, отсоединять или присоединять дополнитель- ные объекты, и все это не будет влиять на статус компоновки и информационный журнал до тех пор, пока не будет выполнена очередная операция компоновки. Информацию о результатах компоновки можно получить функцией glGet- InfoLogARB (см. раздел 7.5) с аргументом program. Если программный объект был скомпонован успешно, результат будет содержать либо пустую строку, либо ин- формацию о компоновке. При неудачной компоновке информационный жур- нал содержит информацию об ошибках, предупреждения и другие замечания. Функция gl Link Prog ramARB не ожидает окончания операции компоновки и пере- дает управление приложению. Любая последующая команда, зависящая от результа- та компоновки, будет ожидать ее окончания. Убедиться в том, что операция ком- поновки закончена, можно, вызвав функцию g 1 GetObjectParameterARB, которая будет ожидать ее окончания, а затем выдаст результат. После успешного завершения компоновки можно устанавливать выполняемые файлы как часть текущего состояния для рендеринга функцией void glUseProgramObjectARBCGLhandleARB program) Она устанавливает данный программный объект program как часть текущего со- стояния для рендеринга. Программный объект будет содержать выполняемый модуль для вершинного процессора, если содержит один или несколько успеш- но скомпилированных и скомпонованных шейдерных объектов типа GL_VERTEX_ SHAD£R_ARB. Точно так же программный объект будет содержать выполняемый модуль для вершинного процессора, если содержит один или несколько успеш- но скомпилированных и скомпонованных шейдерных объектов типа GL_FRAGMENT SHADER_ARB. Если program содержит шейдерные объекты типа GLJ/ERTEX_SHADER_ARB, но не со- держит объектов типа GL_FRAGMENT_SHADER_ARB, выполняемый модуль будет уста- новлен только на вершинном процессоре, а для фрагментного будет использо- ваться стандартная функциональность. Точно так же, если program содержит шейдерные объекты типа GL_FRAGMENT_SHADER_ARB, но не содержит объектов типа GL_VERTEX_SHADER_ARB, выполняемый модуль будет установлен только на фрагмент- ном процессоре, а для вершинного будет использоваться стандартная функцио- нальность. Если program — 0, программируемые процессоры не используются, а вершины и фрагменты будут обрабатываться стандартным способом. Успешная установка выполняемого модуля на программируемом процессоре автоматически блокирует соответствующую стандартную функциональность. Если выполняемый модуль устанавливается на вершинном процессоре, блокируется функциональность, описанная в разделе 4.1. Если выполняемый модуль устанав- ливается на фрагментом процессоре, блокируется функциональность, описан- ная в разделе 4.2. После установки программного объекта можно изменять, компилировать, от- соединять присоединенные шейдерные объекты или присоединять новые, удалять любые шейдерные объекты или даже программный объект, и все это не повлияет на выполняемые модули. Но повторная компоновка в случае, если она прошла успешно, переустановит эти программы. После установки программного объекта можно работать с любыми состояниями OpenGL, влияющими на блокированную стандартную функциональность.
158 Глава 7. API языка шейдеров OpenGL 7.4. Удаление данных Если объекты больше не нужны, их необходимо удалять. Это можно сделать функ- цией void glDeleteObjectARBCGLhandleARB object': Эта функция освобождает память и делает недействительным идентификатор объекта object. Она аннулирует объекты, созданные функциями glCreateShader ObjectARB или glCreateProgramObjectARB. Если удаляемый шейдерный объект присоединен к программному объекту, он будет отмечен для удаления, но не удален до тех пор, пока не отсоединится от всех программных объектов, к которым он был присоединен. А если удаляемый программный объект имеет присоединенные шейдерные объекты, они автомати- чески отсоединяются, но не удаляются (если только они не были ранее отмечены для удаления функцией gl DeleteObjectARB). Чтобы определить, помечен ли какой-либо объект для удаления, можно вызвать glGetObjectParameterARB с аргументами object и GL_OBJECT_DELETE_STATUS_ARB. Шейдерный объект отсоединяется от программного объекта функцией void glDetachObjectARBfGLhandleARB program. GLhandleARB shader) Ею программируется отсоединение шейдерного объекта shader от программного объекта program. Эта команда отменяет действие ранее выполненной команды glAttachObjectARB. Интересный программный прием, связанный с удалением объектов, состоит в том, чтобы удалять шейдерные объекты сразу же после присоединения их к про- граммному. Тогда они не будут удаляться сразу, но будут помечены для удале- ния, и чтобы удалить все их сразу, нужно будет всего лишь удалить программный объект, к которому они присоединены. Тогда все шейдерные объекты будут авто- матически отсоединены и удалены. 7.5. Функции запроса состояния API языка шейдеров OpenGL содержит несколько функций для запроса состоя- ния объекта. Чтобы получить тип объекта, подтип, результат выполнения опера- ции над объектом, количество присоединенных объектов, количество активных атрибутов (см. раздел 7,6) и uniform-переменных (см. раздел 7.7) или длину лю- бой строки из объекта, используется одна из следующих функций: void glGetObjectParameterfvARB (GLhandleARB object, GLenum pname, GLfloat ★params) void glGetObjectParameterivARB (GLhandleARB object. GLenum pname. GLint *params) Эти функции возвращают в params значение параметра объекта object. Они могут использоваться для получения информации об объекте. В табл. 7.1 значение pname показано слева, а выполняемая операция — справа.
7,5. Функции запроса состояния 159 Таблица 7.1. Запрашиваемые параметры объекта Параметр Операция GL_OBJECT_TYPE_ARB params возвращает значение GL_PROGRAM_OBJECTJ\RB или GL_SHADER_OBJECT_ARB, в зависимости от того, является ли object идентификатором программ- ного или шейдерного объекта EL OBJECT SUBTYPE ARB params возвращает значение GL VERTEX SHADER ARB или GL_FRAGMENT_SHADER_ARB в зависимости от того, является ли object идентификатором вершинного или фрагментного шейдера GL_OBJECT_D£LETEJTATUS_ ARB params возвращает 1 или l,Of, если объект помечен для удаления, и 0 или 0,0f, если не помечен GL_OBJECT_COMPILE_STATUS_ ARB params возвращает 1 или l,0f, если последняя компиляция указанного шейдерного объекта была успешна, и 0 или 0,0f, если неуспешна GL_OBJ ECT_L IN K_STATUS_ARB params возвращает 1 или l,0f, если последняя компоновка указанного программного объекта была успешна, и 0 или 0,0f, если неуспешна 6U)BJECT_VALIDATE_STATUS_ ARB params возвращает 1 или 1,0f, если последняя проверка программного объекта была успешна., и 0 или 0,0f, если неуспешна GL_OBJECTJNFO_LOG_LENGTH_ ARB params возвращает количество символов в инфор- мационном журнале указанного объекта, включая сим- вол конца строки. Если у объекта нет информаци онного журнала, возвращается значение 0 или 0,0f GL_OBJECT_ATTACHED_OBJECTS_ ARB params возвращает количество объектов, присо- единенных к указанному программному объекту il_OBJECT_ACTIVE_ATTRIBUTES _ARB params возвращает количество активных attribute- переменных заданного программного объекта GL_OBJECT_ACTIVE_ATTRI BUTE_ MAX J_ENGTH_ARB params возвращает длину самого длинного имени активной attribute-переменной, включая символ конца строки, для заданного программного объекта. Если не существует активных attribute-перемен- ных, возвращается 0 или б, Of GLJ®JECT_ACTIVEJJNIFORMS_ ARB params возвращает количество активных uniform- переменных заданного программного объекта GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB params возвращает длину самого длинного имени активной uniform-переменной, включая символ конца строки, для заданного программного объекта. Если не существует активных uniform-перемен- ных, возвращается 0 или 0,0f GL_OBJECT_SHADER_SOURCE_LENGTH_ARB params возвращает общую длину исходного кода шейдера, включая символ конца строки. Если кода нет, возвращается значение 0 или 0,0f Исходный код шейдера можно получить из шейдерного объекта, вызвав функ- цию: void gJGetShaderSourceARB(GLh.ardleARB shader. GLsizei maxLength. GLsizel *length. GLcharARB *source)
160 Глава 7. API языка шейдеров OpenGL Функция возвращает полный исходный код шейдера из шейдерного объекта shader. Этот исходный код — результат предшествующего вызова gl ShaderSourceARB. Возвращаемая строка заканчивается нуль-символом. Для функции glGetShaderSourceARB существует ограничение на количество симво- лов возвращаемой строки, задаваемое maxLength. Количество символов в реально возвращенной строке находится в параметре length, а если ограничивать их коли- чество не требуется, можно передавать в этот параметр N U LL. Необходимый раз- мер буфера для сохранения возвращаемого кода можно узнать с помощью функ- ции glGetObjectParameterARB со значением GLj)BJECT_SHADER_SOURCE_LENGTHj\RB. Результат компиляции будет сохранен в информационном журнале шейдерно- го объекта; а результат компоновки — в информационном журнале программного объекта. Информационный журнал это строка, содержащая диагностические сообщения, предупреждения и другую информацию о последней компиляции (для шейдерных объектов) или компоновке и проверке (для программных объектов). Сразу после создания шейдерного или программного объекта его информацион- ный журнал представляет собой строку длиной 0. Он может содержать полезную информацию даже в случае успешного завершения компиляции или компонов- ки, нужен в основном только во время разработки графического приложения, и разработчик не должен ожидать от различных реализаций OpenGL одинаковых сообщений об ошибках. Получить информационный журнал для шейдерного или программного объекта позволяет функция void glGetlnfoLogARBCGLhandleARB object. GLsizel maxLength. GLsizel * length, GLcharARB *lnfoLog) Эта функция возвращает информационный журнал для указанного объекта object. Журнал обновляется каждый раз после компиляции шейдера, а для программно- го объекта — каждый раз при компоновке или проверке программы. Возвращае- мая строка заканчивается нуль-символом. Функция gl Get InfoLogARB возвращает в InfoLog такую часть журнала, которая туда поместится, до maxLength символов. Количество реально возвращенных символов за- писывается в length; если же эта информация не нужна, следует просто передавать NULL. Размер буфера, способного вместить весь журнал, можно определитьс по- мощью функции gl GetObjectPararneterARB, передав ей значение GL_OBJECT_INFO_LOG_- LENGTH_ARB. Сначала нужно выполнять запрос, чтобы определить длину информационно- го журнала (количество символов в строке). После выделения буфера нужного размера вызывается функция glGetInfoLogARB, для того чтобы поместить строку информационного журнала в предоставленный буфер. Потом можно эту строку, например, напечатать (листинг 7.1). Листинг 7.1. Функция на языке С, печатающая информационный журнал объекта void printInfoLog(GLhandleARB obj) { int infologLength = 0; int charsWritten = 0; GLcharARB *infoLog: printOpenGLErrorO; II Проверка на ошибки OpenGL
7.6. Установка атрибутов вершин 161 I glGetObjectParameterl vARB(obj . GL_OBJEC'T_INFOJ-OG_LENGTH_ARB, I. SinfologLength): I printOpenGLError(); H Проверка на ошибки OpenGL if (infologLength > 0) [ { infoLog = (GLcharARB*)malloc(infcilogLength); I if (infoLog == NULL) I { L prfntfC"ОШИБКА: Невозможно выделить память для InfoLogin"): I exit(l): I" glGetlnfoLogARBCobj. infologLength. fcharsWritten. InfoLog): I. printf ("InfoLog:\n£s\.n\n", infoLog): free(infoLog): printOpenGLErrorO; И Проверка на ошибки OpenGL } Получить текущий программный объект можно с помощью функции I GLhandleARB glGetHandleARB(G.Lenum рпате} I Она возвращает идентификатор текущего объекта состояния. Аргумент рпате оп- I ределяет запрашиваемое состояние. Чтобы получить идентификатор программ- I кого объекта, следует передать GL_PROGRAM_OBJECT ARB. I И наконец, список шейдерных объектов, присоединенных к конкретному про- f фаммному объекту, можно получить функцией Г void glGetAttachedObjectsARB(GLhandleARB program. I GLsizei maxCount. I GLsizei *count. I GLhandleARB *objects) | Эта функция возвращает идентификаторы шейдерных объектов, присоединен- | пых к program. Максимальное количество возвращаемых объектов — maxCount, I Количество реально возвращенных идентификаторов заносится в count, а если [ эта информация не нужна (например, перед этим количество присоединенных г объектов уже было получено с помощью функции glGetObjectParameterARB), мож- f но передавать NULL. Чтобы получить количество присоединенных объектов, Г нужно вызвать функцию glGetObjectParameterARB с параметром GL_OBJECT_ATTACHED_ OBJECTS ARB. Г 7.6. Установка атрибутов вершин ’ 'Один из способов передать данные о вершинах в OpenGL — вызвать gl Begi п, затем «нужной последовательности функции gl Col or/g 1 Normal /gl Vertex и т. д., завершив 1 :эту последовательность вызовом функции gl End. Все эти вызовы продолжают работать и в программируемой среде OpenGL. Как и раньше, функция gl Vertex позволяет сообщить, что данные для некоторой I точки переданы и должны быть обработаны. Однако если функцией. glUsePro- [ grsmObjectARB в среде установлен правильный вершинный шейдер, данные о вер- I шинах будут обрабатываться этим шейдером. Вершинный шейдер работает с дан- I ними о вершине через встроенные переменные 6 Зак. 218
162 Глава 7. API языка шейдеров OpenGL attribute vec4 gi _Color: attribute vec4 9? SecondaryCol or; attribute vec3 gl _Normai ; attribute vec4 gi Vertex: attribute vec4 gl MultiTexCoordO; attribute vec4 gl -MultlTexCoordl: attribute vec4 gi _MultiTexCoord2; attribute vec4 91. _FogCoord: Интерфейс OpenGL «вершина за раз» простой и действенный, но в современ- ных системах является не самым лучшим решением, поскольку требует больших затрат времени для передачи данных графическому ускорителю. Лучше вместо него использовать массивы вершин. Этот интерфейс позволяет заносить данные о вершинах в массивы и затем сообщать OpenGL указатели на эти массивы. Вме- сто того чтобы посылать OpenGL данные для одной вершины, можно передать за один раз целый набор примитивов. Возможно, в целях увеличения производи- тельности массивы для хранения данных о вершинах будут располагаться даже в видеокарте. Интерфейс массивов вершин в программируемой среде OpenGL работает точ- но так же, как и обычно. Когда в OpenGL устанавливается массив вершин, дан- ные о вершинах обрабатываются последовательно, вершина за вершиной, как в ме- тоде «вершина за раз». Если вершинный шейдер активен, каждая вершина будет им обрабатываться. Однако новые возможности программируемости позволяют приложению не ограничиваться обычными атрибутами, определенными в OpenGL. Существует много дополнительных атрибутов вершин, которые, возможно, потребуется пере- дать от приложения вершинному шейдеру. Легко представить, что приложению может потребоваться указывать, напри- мер, тангенс, температуру, давление или еще какую-нибудь величину. Как же по- зволить приложениям передавать «нетрадиционные» атрибуты и работать с ними в вершинных шейдерах? Ответ таков: OpenGL предоставляет некоторое количество переменных для передачи атрибутов вершин. Каждая такая переменная имеет индекс (номер) и ме- сто для хранения до четырех чисел с плавающей запятой (то есть типа vec4). Реа- лизация, поддерживающая 16 таких переменных, будет присваивать им номера от 0 до 15. Приложение может передавать атрибуты вершины через любые такие переменные с помощью функций void glVertexAttrlЬ{1|2|3|4}{s|f|d}ARB(GLuint index. TYPE v) void glVertexAttrib{li2i3}{s|fd}vARB(GLuinc index, const TYPE *r) void glVertexAttrib4{b|s|i|f|d|ub|us|ui}vARB(GLu1nt Index, const TYPE *v) Функции устанавливают атрибуты вершин по индексу index в значение из v. Эта команда имеет до трех суффиксов, которые дифференцируют входные параметры. Первый суффикс может быть 1,2,3 или 4, он показывает, содержит ли v 1,2,3 или 4 компонента. Если второй и третий компоненты не указаны, по умолчанию предпо- лагается значение 0. Если четвертый компонент не указан, по умолчанию пред- полагается значен ие 1. Второй суффикс определяет тип данных и может обозначать byte (b), short (s), Int (1), float (f), double(d),unsigned byte (ub), unsigned short (us) или unsigned int (ui). Третий суффикс необязателен — это буква v, обозначаю- щая, что параметр V является указателем на массив значений этого типа данных.
7.6. Установка атрибутов вершин 163 Приведенный набор команд имеет свои правила для преобразования данных в формат числа с плавающей запятой, то есть во внутреннее представление дан- ных» OpenGL. Числа с плавающей запятой и двойной точности переводятся во внутренние значения OpenGL обычным способом, целые числа преобразуются в числа с плавающей запятой добавлением нулевой дробной части. Таким обра- зом, например, значение 27 для типов byte, тnt, short, unsigned byte, unsigned 1 nt, unsigned short станет значением 27,0 внутри OpenGL. Другой набор функций позволяет передавать нормализованные значения как атрибуты вершин: void glVertexAttrib4NubARB(GLuint index, TYPE к) void glVertexAttrib4N{b|s111f|d|ub|us|ui}vARBtGLuint index, const TYPE ★v') Функции устанавливают дополнительные атрибуты вершин по индексу index в нормализованное значение из V. В дополнение к N (что указывает на нормализо- ванные данные) в названии может быть два суффикса, что позволяет указывать тип передаваемого аргумента. Первый суффикс указывает тип данных v и может обозначать byte (b), short (s), int (1), float (f), double (d), unsigned byte (ub), unsigned short (us) или unsigned int (ui). Второй суффикс v — необязательный и обозна- чает, что v — указатель на массив значений этого типа данных. Команда с N в названии обозначает, что для данных, не являющихся числами с плавающей запятой или числами двойной точности, аргументы будут линейно отображены на нормализованный диапазон точно так же, как данные, передаваемые в целые варианты функций gl Col or и glNormal. То есть для функций, работающих сцелыми числами со знаками, самое большое положительное число отображает- ся на 1,0, а самое маленькое — на -1,0; для беззнаковых целых наибольшее поло- жительное число отображается на 1,0, а наименьшее — на 0. Attribute-переменные могут быть типов mat2, mat3 или mat4. Установить их мож- но с помощью функции gl VertexAttri bARB в последовательные ячейки по столб- цам. То есть чтобы задать атрибут типа mat4, нужно установить первую колонку в attribute-переменную с индексом 1, вторую — в attribute-переменную с индек- сом 1 +1, третью — в attribute-переменную с индексом 1 +2 и четвертую — в attribute- переменную с индексом 1 +3. За одним исключением, атрибуты вершин могут быть любыми. Их можно использовать для передачи всего, чего угодно: дополнительных значений цвета, тангенсов, бинормалей, значений глубины и т. п. Исключение сострит в том, что вершинный атрибут с индексом 0 используется, подобно вызову функции gl Ver- tex, для того, чтобы сообщить OpenGL об окончании передачи данных для кон- кретной вершины. Команды gl Vertex2, gl V ert ехЗ, gl Vertex4 полностью эквивалент- ны соответствующей команде glVertexAttribARB с индексом 0. Для атрибута с индексом 0 не существует текущего значения; при попытке получить его бу- дет сгенерирована ошибка. Только один этот атрибут имеет такие свойства; установка всех прочих стандартных атрибутов может выполняться в произволь- ном порядке, вперемешку с установкой других дополнительных атрибутов. Мож- но также сочетать вызовы gl Vertex с вызовами функции gl VertexAttri bARB с ин- дексом 0. API для работы с вершинными массивами было расширено для того, чтобы задавать дополнительные атрибуты в вершинных массивах. Следующая функция установит вершинный массив для дополнительного атрибута:
164 Глава 7. API языка шейдеров OpenGL void glVertexAttribPointerARBCGLuint index, GLint size. GLenum type, GLboolean normalized, GLsizei stride. const Glvoid ★pointer) Эта функция указывает расположение и формат данных массива дополнительных атрибутов вершин, которые будут использоваться для рендеринга. Доступ к масси- ву происходит по индексу index. Аргумент s ize указывает количество компонентов в атрибуте, которое должно быть 1, 2,3 или 4. Аргумент type определяет тип дан- ных каждого компонента (GLJ3YTE, GL_UNSIGNED_BYTE,GL_SHORT, GL_UNSIGNED_SHORT, Gl. I NT, GL_UNSIGNED_INT, GL_FLOAT или GL_D0.U3LE). Аргумент stride определяет шаг по ин- дексу, что позволяет передавать значения атрибутов как в отдельном массиве, так и вперемешку с другими атрибутами. Аргумент norma 1 ized, установленный в GL_TRUE, показывает, что значения, переданные в виде целых чисел, будут ото- бражены на диапазон [-1,0,1,0] (для знаковых значений) или [0,0,1,0] (для без- знаковых значений) при доступе к ним и приведении к типу чисел с плавающей запятой. При другом значении этого аргумента нормализация выполняться не будет. Аргумент pointer — адрес памяти, где расположен массив с атрибутами вершин. Посте того как информация о вершинах будет занесена в массив дополнитель- ных атрибутов, нужно включить использование массивов. В этом случае допол- нительные атрибуты будут передаваться вместе с обычными данными для вершин при вызове команд работы с массивами, например glDrawArrays. Чтобы включить или выключить работу с массивом дополнительных атрибутов, используются функции void glEnableVertexAttribArrayARBCGLuint index) void glDisableVertexAttribArrayARBCGLuint index) Они включают или выключают работу с массивами дополнительных атрибутов вершин, указанными аргументом index. По умолчанию все подобные возможнос- ти выключены, в том числе и работа со всеми массивами дополнительных атри- бутов вершин. При включении функциональности значения из этих массивов будут извлекаться и использоваться при рендеринге в функциях glArrayElement, glDrawArrays, glDrawElements.glDrawRangeElements, glMultlDrawArrays или gl Multi Draw- Elements. Это решает проблему передачи данных о вершинах в OpenGL. Но возникает вопрос: как потом получить эти данные из вершинного шейдера? Обращение по индексам — не очень хорошее решение, так как такие имена не наглядны и их использование может вызвать многочисленные ошибки. API языка шейдеров OpenGL предоставляет два способа доступа к дополнительным атрибутам вер- шин из вершинного шейдера. Первый способ — компоновщик связывает параметры автоматически. В таком случае после компоновки приложение должно сделать запрос к OpenGL, чтобы определить индексы дополнительных атрибутов, которые в дальнейшем и исполь- зовать для передачи этих атрибутов. Второй способ — приложение выбирает какой-либо индекс в массиве допол- нительных атрибутов и связывает с ним некую attribute-переменную в вершин- ном шейдере до компоновки, используя функцию
7,6. Установка атрибутов вершин 165 void glBindAttribLocationARBlGLhandleARB program. ;GLuint index. । ’ -const GLcharARB *name) | Эта функция устанавливает соответствие заданной в программном объекте program определенной разработчиком attribute-переменной индексу дополнительного атрибута index. Имя определенной разработчиком attribute-переменной переда- ' ется как строка с символом конца строки пате. Если это имя раньше уже было ' связано, предыдущая связь теряется. Нельзя связать одну и ту же переменную . с несколькими индексами, но можно связать разные переменные с одним и тем же индексом. Когда программа с такими связанными переменными активна, зна- чения из массива произвольных атрибутов копируются в связанные с ними опре- деленные разработчиком attribute-переменные. Если name ссылается на attribute-переменную матричного типа, index ссылается на первый столбец матрицы. Другие столбцы матрицы автоматически связыва- 1 ются со следующими номерами: index+l — для типа mat2; index+1 и index-2 — для । матрицы типа mat3; index-r 1, index<2 и index+3 — для матрицы типа mat4. Этой командой нельзя связывать какие-либо имена со стандартными атрибута- ми вершин в OpenGL, так как при необходимости они связываются автоматичес- ки. Все операции связывания, выполненные после компоновки программного i объекта, не будут применяться до следующей компоновки. Функцию glBindAttri bLocationARB можно вызывать и до того, как шейдерные «ЙЪекты присоединены к программному объекту. Также можно связываться с име- нем переменной, не используемой в вершинном шейдере вообще. Приложение может при необходимости связывать более одной переменной ;¥Ьдним и тем же индексом атрибута. Такие переменные называются псевдонимы атрибутов, их можно использовать в случае, если только одно из имен исполь- 'зуется в программе или же если ни один путь выполнения программы не совме- щает в себе нескольких псевдонимов. В этом случае компилятор и компоновщик жгут полагаться на отсутствие псевдонимов и выполнять оптимизацию кода. От (реализации OpenGL не требуется проверять наличие псевдонимов. Так как нельзя (связывать стандартные атрибуты, невозможно сделать псевдонимы для них. Связывание можно выполнить в любой момент с помощью функции gl Bi nd- #tribLocati onARB. На самом деле связывание не выполняется до начала выполне- ния функции gl LinkProgramARB, но перед компоновкой должны быть связаны все йеременные, которые предполагается использовать в шейдере. После компонов- ки программного объекта индексы для attribute-переменных не меняются (и можно долучить значения этих переменных) до следующей компоновки. Чтобы полу- дить индекс атрибута для заданного имени attribute-переменной, можно исполь- зовать функцию GLint glGetAttribLocationARB(GLhancfleARB program. const GLcharARB ★name') Функция запрашивает значение attribute-переменной с именем пате у скомпо- нованного программного о6ъекгарго§гат. Возвращаемое значение — индекс в мас- сиве дополнительных атрибутов, связанный с пате. Если паше — attribute-пере- менная матричного типа, возвращается индекс первого столбца. Если заданная переменная не является активным атрибутом указанного программного объекта или ее имя начинается с зарезервированного префикса gl_, возвращается значе- ние -1.
166 Глава 7. API языка шейдеров OpenGL Функция gl GetAttНbLocat 1 onARB возвращает последнее связывание, заданное до вызова функции дШлкРгодгатАКВдля заданного программного объекта. Те связы- вания, что были заданы после компоновки, не возвращаются gl GetAtt rl bLocat1 onARB. Используя эти функции, создадим вершинный шейдер, который будет содержать определенную разработчиком attribute-переменную Opaci ty (способность закры- вать объекты, расположенные на заднем плане), значение которой будет исполь- зоваться при вычислении освещения. Предположим, значения для этой перемен- ной будет передаваться под индексом 1 и нужно выполнить соответствующее связывание: glBindAttribLocationARBCrnyProgram. 1. "Opacity”); После этого можно вызвать gl VertexAtt ri bARB для передачи значения для каж- дой вершины следующим образом: glVertexAttriblfARB(l. opacity); Функция glVertexAttribARB вызывается между вызовами функций gTBegin и gl End. Она заменяет стандартные OpenGL-функции gl Col or, gl Normal и т. д. Но, как было замечено ранее, для увеличения скорости рисования рекомендуется ис- пользовать вершинные массивы. Профессиональный жаргон в этом разделе может несколько сбить с толку, по- этому следует взглянуть на диаграмму, чтобы удостовериться в правильном пони- мании предмета. Рисунок 7.1 демонстрирует, как команды установки атрибутов вершин могут быть использованы для модификации значений встроенных пере- менных, определенных языком шейдеров OpenGL. Связывание между командами для установки стандартных атрибутов (цвет, нормаль, вершина и т. д.) и встроен- ных переменных (gl_Col or, gl_Normal, gl_Vertex и т. д.) происходит автоматически и таким образом, чтобы не противоречить дополнительным установленным атри- бутам. Каждая из этих функций, за исключением gl Vertex, меняет состояние OpenGL. Значения встроенных attribute-переменных обновляются автоматически. glVertex(...) --------- glColor(...) ---------- glNoimal(...)---------- glSecondaryColor(...) glFodCoord(...)-------- glMultiTexCoord(0,...) glMultiTexCoord(1,...) glMultiTexCoord(2,...) gl_ Vertex gl_Color gl_Normal gl_SecondaryColor gl_FodCoord glJtfultiTexCoordO gl_MultlTexCoord1 gl_MultiTexCoord2 glMultiTexCoord(N, ...)- ]—► gl_MultiTexCoordN Вызовы функций Текущее из приложения значение атрибутов для установки стандартных атрибутов вершин Встроенные переменные атрибутов Рис. 7.1. Отображение стандартных функций установки вершинных атрибутов на встроенные attribute-переменные При наличии дополнительных атрибутов вершин (рис. 7.2) отображение немно- го отличается от рассмотренного ранее. Определенная разработчиком attribute- переменная должна быть связана с определенным индексом в массиве дополни-
7.5. Установка атрибутов вершин 167 тельных атрибутов. Это связывание выполняется явно с помощью функции gl Bi nd- AttHbLocationARB или неявно при компоновке. glVertexAttribARB(O, ...) giVertexAttribARB(1,...) glVertexAttribARB(2, ...) glVertexAttribARB(3,...) glVertexAttribARB(4,...) g)VertexAttribARB(N, ...) Вызовы функций Текущее Определенные из приложения значение атрибутов пользователем для установки переменные стандартных атрибутов атрибутов вершин (кроме gl_Vertex) Рис. 7.2. Отображение дополнительных функций установки вершинных атрибутов на определенные разработчиком attribute-переменные Предположим, есть вершинный шейдер, использующий три определенные разра- ботчиком attribute-переменные: Opaci ty, Вт normal, MyData (см. рис. 7.2). Эти перемен- ные можно связать с каким-либо индексом в массиве дополнительных атрибутов: gIBindAttribLocationARBtmyProgram. 1, "Opacity"): glBindAttribLocationARB(myProgram. 2. "Binormal"): glBindAttrlbLocationARBCmyProgram. 3. "MyData''): Этот код устанавливает отображение данных, так что значения, записанные в мас- сив дополнительных атрибутов с индексами 1,2 и 3, будут автоматически заноситься в attribute-переменные Opacity, Binormal и MyData вершинного шейдера. Атрибуте ин- дексом 0 можно связать с какой-нибудь определенной разработчиком переменной либо получить его значение из встроенной attribute-переменной glVerTez. Ячейка массива дополнительных атрибутов с индексом N не связана с переменной (см. рис. 7.2), Как говорилось ранее, в каждой ячейке массива достаточно места для четырех компонентов — чисел с плавающей запятой. Приложения могут сохранять 1, 2, 3 или 4 компонента в каждой ячейке. Вершинный шейдер может считывать эти зна- чения через определенную разработчиком attribute-переменную типа float, vec2, уесЗ или vec4. Получить значения сразу из двух ячеек можно через attribute-пере- менную типа mat2, из трех — типа mat3, из четырех — типа mat4. Связь индекса и определенной разработчиком attribute-переменной (обозна- чены стрелками в правой части рис. 7.2) является частью состояния программного объекта, тогда как содержимое массива атрибутов само по себе является состоя- нием атрибутов (кроме атрибута с индексом 0). Приложение может задавать для разных шейдеров связи с разными именами переменных, так что если оно не поза- ботилось о новых значениях attribute-переменных для нового шейдера, то шейдер будет считывать значения, оставшиеся от предыдущего. Attribute-переменные, доступ к которым происходит при выполнении вершин- ного шейдера, называются активными атрибутами. Чтобы получить информа- цию об активном атрибуте, нужно воспользоваться функцией void glGetActiveAttritARB(GLhand!eARB program. GLuint index. GLsizei maxLength.
168 Глава 7. API языка шейдеров OpenGl GLsizei * length. GLint *s1ze. GLenum *type. GLcharARB ★name') Эта функция возвращает информацию об активной attribute-переменной в программном объекте program. Размер буфера символов, предоставленного приложением, передается в maxLength, а указатель на этот буфер — в.пате. Attribu- te-переменная (встроенная или определенная разработчиком) считается ак- тивной, если при компоновке выяснилось, что работающая программа может обращаться к этой переменной. Так что перед первым вызовом этой функции должен выполниться хотя бы один вызов функции glLinkProgramARB, не обяза- тельно успешно. Функция glGetActiveAttribARB возвращает имя attribute-переменной, указанной с индексом index, сохраняя его в пате. Возвращаемая строка будет заканчиваться нуль-символом. Реальное количество символов, записанных в буфер (не считая нуль-символа), возвращается в length, но если это несущественно, то можно пе- редать NULL. Количество активных атрибутов в программном объекте можно узнать с помо- щью функции gl GetObjectParameterARB с аргументом GL_OBJECT_ACTIVE_ATTRIBLITES_ARB. Если задавать для index значение 0, возвратятся параметры первой активной attribute-переменной, а если задать значение GL_OBJECT_ACTIVE_ATTRIBUTES_ARB -1, возвратятся параметры последней. Длину самого длинного имени attribute- переменной можно получить функцией glGetObjectParameterARB с параметром GL__ OBJECT_ACTIVE_ATTRIBUTE_MAX_LENGTH_ARB. Аргумент type будет содержать указатель на тип attribute-переменной. Тип ука- зывает одна из символических констант: GL_FLOAT, GL_FL0AT_VEC2_ARB, CLFLOATVEC3ARR, GL_FL0AT_VEC4_ARB, GL_FL0AT_MAT2_ARB, GL_FLOAT_MAT3_ARB и GL_FLOAT_MAT4_ARB. Аргумент size будет содержать размер атрибута в единицах возвращенного в type типа. Эта функция возвращает максимум информации об указанной attribute-перемен- ной. Если нет доступной информации, length будет равен нулю и name будет пус- той строкой. Такое может произойти в случае не состоявшейся компоновки. Описанная команда может быть полезна, если разработка шейдера и разработ- ка приложения выполняются раздельно. Если между разработчиками шейдера и приложения достигнуто соглашение об именах атрибутов, во время выполне- ния можно запрашивать программный объект для получения действительно нуж- ных атрибутов, а затем передавать их. Это придает гибкость процессу разработки шейдеров. Чтобы получить состояние конкретного дополнительного атрибута вершины, нужно вызвать одну из функций: void glGetVertexAttribfvARBCGLuint index. GLenum pname. GLfloat ★params) void glGetVertexAttribivARB(GLuint index, GLenum pname. GLint *params) void glGetVertexAttribdvARBCGLuint index. GLenum pname. GLdouble *params) Эти функции возвращают в params значение дополнительного атрибута, задан- ного индексом index. Запрашиваемый параметр указан в рпате. Список всех параметров и возвращаемых значений приведен в табл. 7.2. Все параметры, кроме GL_CURRENT_VERTEX_ATTRIB_ARB, представляют состояние клиентской час- ти OpenGL.
'17. Установка uniform-переменных 169 Таблица 7.2. Параметры дополнительных атрибутов вершин Параметр Операция GLJERTE Х АИ RIВ ARRAY ENABL ЕD_ARB params возвращает ненулевое значение (GL_TRUE) в случае, если массив атрибутов для данного индекса включен, и 0 (GL_FALSE), если выключен. Начальное значение — GL_FALSE GL_VERTEX_AnRIB_ARRAY_SIZE_ ARB params возвращает размер массива атрибутов для index — количество значений, помещающе- еся в каждый элемент, от 1 до 4. Начальное значение — 4 • GL_VER'TX_ATTRIEARRAY STRIDE_ARB params возвращает шаг индекса (количество байт перед следующим элементом) в массиве атрибутов для данного индекса. Значение 0 означает, что элементы массива хранятся в па- мяти последовательно один за другим. Началь- ное значение — 0 GL^ERTEX^ATTRIB_ARRAY_ TYPE_ARB params возвращает символическую константу, обозначающую тип массива данного атрибута. Возможные значения: GL BYTE, GL UNSIGNED BYTE, GL_SHORT, GL-UNSIGNEDSHORT, GL JNT, GL_UNSIGNED_INT, GL FLOA” hGL_DOUBLE. Началь- ное значение — GL_FLOAT GL VER1 EX A( I'RIB ARR/A NORMALIZED- ARB params возвращает ненулевое значение (GL_TRUE), если типы данных для указанного атрибута были нормализованы при преобразовании, и 0 (GL_FALSE), если не были. Начальное значе- ние — GLFALSE GL_CURRENT_VERTEX_ATTRIB_ ARB params возвращает четыре значения — текущие значения атрибута. При указании индекса 0 в этом случае получится ошибка, так как у вер- шинного атрибута 0 нет текущего состояния. Начальное значение всех других атрибутов вершин — (0, 0, 0,1) void glGetVertexAttribPointervARBIGLuint index. GLerum рпате. GLvoid **pointer) Функция возвращает информацию об указателе. Здесь index указанный индекс атрибута, рпате — символическая константа, обозначающая, какой указатель нужно возвратить, и params — указатель на место сохранения возвращаемых данных. В рпате можно передавать лишь однозначение: GL_VERTEX_ATTRIB_ARRAY_POINTER_ARB. В pointer возвратится значение указателя на массив вершинных атрибутов для дополнительного атрибута с номером index. 7. Установка uniform-переменных Как сказано в предыдущем разделе, attribute-переменные используются для пере- дачи данных, изменяющихся для каждой вершины, в вершинный шейдер. Другие данные, изменяющиеся не так часто, можно задавать через uniform-переменные,
170 Глава 7. API языка шейдеров OpenGL которые объявлены внутри шейдера и могут устанавливаться непосредственно приложением. Это дает приложению возможность передавать в шейдер данные любых типов. Приложения могут устанавливать эти переменные в разные значе- ния для каждого примитива, чтобы влиять на работу шейдера (хотя при этом мо- жет немного пострадать производительность). Обычно uniform-переменные ис- пользуют для хранения состояния, общего для нескольких или даже многих примитивов. Язык шейдеров OpenGL также определяет несколько встроенных переменных со- стояния OpenGL. Приложения могут продолжать управлять состоянием OpenGL с помощью обычных функций, но могут использовать и встроенные uniform-пе- ременные в шейдерах. Если нужно выполнить операцию, не предусмотренную OpenGL, определить собственную uniform-переменную и передать ее значение в шейдер очень просто. Когда программный объект становится текущим, встроенные uniform-перемен- ные инициализируются. Последующие изменения состояния OpenGL могут вли- ять на эти переменные и изменять их значения. Основная схема работы с uniform-переменными отличается от схемы работы с attribute-переменными. Как сказано в предыдущем разделе, приложение может указывать расположение attribute-переменных перед компоновкой. Расположе- ние же uniform-переменных определяется OpenGL во время компоновки, так что приложение вынуждено после компоновки запрашивать их расположение. Чтобы установить значение определенной разработчиком uniform-переменной, приложение должно определить ее расположение и затем присвоить значение. Расположение uniform-переменных определяется на этапе компоновки и не из- меняется до следующей компоновки. С каждой компоновкой расположение uni- form-переменных может меняться, так что приложение должно опять запраши- вать эти данные. Запрос можно делать функцией GLint glGetUniformLocationARBCGLhandleARB program, const GLcharARB ★name') Эта функция возвращает целое число - индекс заданной uniform-переменной. Имя uniform-переменной указано в пате как строка с нуль-символом. Можно ис- пользовать оператор элемента массива «[]*> или оператор элемента структуры «. >> (без пробелов) для выбора элемента массива или структуры. Расположение первого элемента массива можно получить, задав имя массива или имя и «[О]». Результатом использования этих операторов не может быть другая структура, массив структур или компонент вектора или матрицы. Если uniform-переменная не найдена в заданном программном объекте или ее имя начинается с зарезерви- рованного префикса gl_, будет возвращаться значение -1. До компоновки расположение uniform-переменных неизвестно. После компонов- ки данной командой можно получить расположение uniform-переменной. Это значение может передаваться в gl Uni fornARB для установки значения uniform-пе- ременной или в gl GetUni fornARB для запроса текущего состояния uniform-перемен- ной, После успешной компоновки программного объекта место uniform-перемен- ных не меняется до следующей компоновки. Установка значения определенной разработчиком uniform-переменной возмож- на только в случае, если программный объект активен. Все определенные разработ- чиком uniform-переменные при компоновке объекта устанавливаются в 0. Опреде- ленные разработчиком uniform-переменные — часть состояния программного объ-
Jit, Установка uniform-переменных 171 екта, Их значения можно изменять, только если программный объект сам являет- ся частью текущего состояния для рендеринга, но при временном выводе про- граммного объекта из текущего состояния значения остаются неизменными. Для установки uniform-переменных используются следующие несколько функций: void gUJniform{l1213|4){f11}ARBtGLint location. TYPE Эта функция устанавливает определенную разработчиком uniform-перемепнуто или массив переменных в значение v. Суффикс 1, 2, 3 или 4 указывает, сколько компонентов содержит v. Это значение должно совпадать с количеством компо- нентов в ТУРЕ (например, 1 для float, int, bool; 2 для vec2, 1vec2, bvec2 и т. д,). Суффикс f указывает, что тип передаваемых значений число с плавающей за- пятой, а суффикс 1 — что это целое число; этот тип должен совпадать с типом uniform-переменной, i-вариант этой функции должен использоваться для uniform- переменных, объявленных как int, 1vec2, ivec3 и 1vec4 или массивов из них. f-ва- риант должен использоваться для типов float, vec2, vec3 и vec4 или массивов из них. Каждый из них может использоваться для передачи значений в uniform-пе- ременные типов bool, bvec2, bvec3 и bvec4 или массивов из них. Значение uniform- переменной будет установлено в FALSE при входном значении 0 или O.Of или в TRUE — при любом другом значении; void glUniform{l[2|3|4}{f|i}vARB(GLint location, GLuint count, const TYPE v) Функция устанавливает значение v в определенную разработчиком uniform-пе- ременную или массив uniform-переменных, на которые ссылается location. Эти функции передают количество и указатель на значения для установки в uniform- переменную или массив переменных. Если устанавливается значение одной пе- ременной, значение count должно быть 1, а при установке значения для масси- ва — 1 или больше. Номер в названии функции означает количество компонентов каждого элемента в г, и это количество должно совпадать с количеством компо- нентов в указанной uniform-переменной (то есть 1 для float, int, bool; 2 для vec2, ivec2, bvec2 и т, д.). Суффикс v в названии функции обозначает, что передается указатель на вектор значений. Суффиксы f и 1 имеют те же значения, что и для не векторных версий gl Uni form. Для массивов uniform-переменных каждый элемент массива считается принадле- жащим типу, указанному в имени функции (например, gl Uni form3f или gl Uni foraCfv можно использовать для установки значений в массив uniform-переменных типа vec3). Количество элементов массива, которые должны быть установлены, указа- но в count; void glUniformMatrix[21314}fvARB(GLint location. GLuint count. GLboolean transpose. const GLfloat *vt Функция устанавливает определенную разработчиком uniform-переменную мат- ричного типа из location в значение v. Число в названии функции — размерность матрицы; 2 — матрица 2x2 (4 значения), 3 — матрица 3x3 (9 значений), 4 — матри- ца 4x4 (16 значений). Если transpose имеет значение GL_FALSE, матрица ожидается в виде «по столбцам», а если GL_TRUE — «по строкам». Аргумент count обозначает количество передаваемых матриц, если указать 1, значения будут устанавливать- ся для одной матрицы, если больше 1 — предполагается массив матриц. Функции gl Uni formli ARB и gl Uni formll vARB — единственные, которые могут ус- танавливать значения семплеров (см. раздел 7.8). Попытка установить семплер •с помощью другой функции приведет к ошибке.
172 Глава 7. API языка шейдеров OpenGL Ошибки при вызове функции gl Uni formARB могут быть вызваны одной из сле- дующих причин. □ Нет текущего программного объекта. □ 1 ocation указывает неверное расположение uniform-переменной для текущего программного объекта. □ count превосходит размер указанных переменной или массива. □ Тип и размер uniform-переменной, определенной в шейдере, не совпадает с ти- пом и размером, указанными в аргументах функции. Во всех этих случаях изменения значения uniform-переменной не происходит. Нельзя использовать адрес uniform-переменной для чего-либо еще, кроме ус- тановки или запроса ее значения. Например, пусть определена некая uniform-пе- ременная как структура, имеющая три поля типа float. Получив адрес п первого поля с помощью функции gl GetUni formLocatl onARB, нельзя предполагать, что сле- дующее поле будет находиться по адресу п + 1. Можно запросить адрес i-го эле- мента массива и потом установить значение в один или несколько элементов это- го массива, начиная с г-го элемента, с помощью функции gl Uni formARB, но нельзя взять индекс i, добавить к нему целое N и пытаться установить значение (г + lV)-ro элемента массива. Адрес этого элемента нужно запрашивать отдельно перед тем, как устанавливать его значение. Этот адрес (расположение) не обязательно будет представлять реальный физический адрес в памяти, и приложения, предполагаю- щие это, правильно работать не будут. Например, пусть в шейдере определена такая структура: struct { struct { float а; float b[10J: } c[2J: vec2 d; } uniform e; и есть код, определяющий адреса внутри этой структуры: loci = glGetUniformLocationARSlprogObj. "е.сГ): //правильно 1 ос2 = glGetUn1forml_ocationARB(progObj. "e.c[Q]’’): // неправильно 1осЗ = glGetUn1formLocatnonARB(progObj. "e.c[0].b"): // правильно 1ос4 = glGetUniformLocationARBfprogObj. "е.с[0].Ь[2]"); // правильно Адрес 1 ос2 получить нельзя, так как е.С[0] ссылается на структуру. Пусть те- перь нужно установить значение uniform-переменных: glUniform2fARB(loci. l.Of. 2.Of): // правильно glUnifonn2iARB(locl. 1. 2); // неправильно glUniformlfARBdocl, l.Of); // неправильно glUniformlfvAR3(1oc3. 10. floatPtr): // правильно glUniformlfvARB(loc4. 10. floatPtr): // неправильно glUnifonnlfvARB(loc4. 8. floatPtr): // правильно Вторая строка неправильная, так как loci ссылается на uniform-переменную типа vec2, а не 1 vec2. Третья строка неправильная из-за того, что 1 ocl ссылается на vec2, а не на float. Пятая строка неправильная, так как количество заданных эле- ментов таково, что получится выход за конец массива.
I L 7:7. Установка uniform-переменных 173 F M -------------------------------------------------------------------------------------------- Uniform-переменные, и определенные разработчиком, и встроенные, которые могут считываться в процессе работы шейдера, называются активными uniform- гкременными. Предполагается, что компилятор и компоновщик распознают объяц- ^В ленные, но нигде не используемые uniform-переменные. Это позволяет делать код ^В более гибким и модульным — объявлять много uniform-переменных, а те из них, ^В яго не будут использоваться в работе, выбрасывать при компиляции и компоновке. ^В Чтобы получить список активных uniforni-переменных из программного объек- ^В та, используется функция gl GetActi veUni formARB. Приложение может запрашивать ^В значения uniform-переменных программного объекта и предоставлять конечно- ^В МУ пользователю элементы интерфейса для непосредственной работы со значе- ^В ниями определенных разработчиком uniform-переменных: ^В void glGetActiveUniformARBCGLhandleARS program. ^В GLirirt index. ^B~ GLsizei maxLength. ^B* GLsizei * length. ^B’ GLint *s1ze, GLenum ★type. ^B GLcharARB ★name': ^B Эта функция возвращает информацию о uniform-переменной, указанной по ин- ^В дексу index, из программного объекта program. Размер символьного буфера, выде- ^В ленного приложением, указывается в maxLength, а указатель на буфер — в name. ^В Uniform-переменная (встроенная или определенная разработчиком) считается ^В' активной в шейдере, если при компоновке было определено, чтр шейдер может обращаться к ней. Это означает, что program уже должен быть скомпонован функ- цией gl LinkProgramARB, но не обязательно успешно. Список активных uniform-ne- ременных может включать как имена встроенных переменных (начинающихся с префикса gl_), так и имена переменных, определенных разработчиком. Функция gl GetActi veUni fornARB возвращает имя uniform-переменной, указанной index, сохраняя его в пате. Возвращаемая строка будет заканчиваться нуль-сим- волом. Количество реально записанных в буфер символов, не считая нуль-сим- !^В вола, возвращается в length, а если эта информация не нужна, вместо параметра ^В можно передать NULL. ^В Количество активных uniform-переменных можно получить с помощью функ- |^Н ции gIGetObjectParameterARB со значением GL_OBJECT_ACTIVEJJNI FORMS_ARB. Значение О ^В для index означает, что будет возвращаться первая активная uniform-переменная, ^В а значение GL_OBJECT_ACTIVE_UNIFORMS_ARB -1 для индекса выберет последнюю ак- ^В тивную uniform-переменную. Размер символьного буфера, способного вместить ^В самое длинное имя переменной, можно получить функцией gl GetObjectParameterARB М со значением GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB. ^В В type возвращается указатель на тип данных uniform-переменной. Здесь могут воз- ^В вращаться символические константы GL_FLOAT, GL_FL0AT_VEC2_ARB, GL_FL0AT_VEC3_ARB, И GL_FLOAT_VEC4ARB, GL_I NT, GL_INT_VEC2_ARB, GL_INT_VEC3_ARB, GL_INT_VEC4_ARS, GL_BOOL_ARB, В GL_B00L_VEC2_ARB, GL_B00L_VEC3ARB, GLJOOL_VEC4_ARB, GL_FL0AT_MAT2_ARB, GLJLOAT MAT3 ARB M и GL_FLOAT_MAT4_ARB. ^B Uniform-переменные, объявленные как структуры или массивы структур, не будут возвращаться целиком. Вместо этого каждая из этих переменных будет разбита ^В на базовые компоненты, в именах используются операторы «.s> и «[]», так что каждое имя все равно будет принадлежать к одному из перечисленных ранее ти- ггов. Каждый из базовых компонентов считается отдельной активной uniform- переменной, а если хотя бы один элемент массива активен, то активным считает- ся каждый элемент массива.
174 Глава 7. API языка шейдеров OpenGL Размер uniform-переменной будет возвращен в size. Uniform-переменные, не яв- ляющиеся массивами, будут иметь размер 1, а при массивах будет возвращаться размер массива. Структуры и массивы структур разбиваются на компоненты, как описано ранее. Если в результате разбиения получается массив, s1 ze будет содер- жать размер массива, а иначе будет равно 1. Описываемая функция возвращает максимум информации об указанной актив- ной uniform-переменной. Если информация недоступна, length будет равно О и пате — пустая строка. Такое может случиться, если последняя компоновка про- шла неудачно. С помощью функции gl GetActiveUniformARB разработчик приложения может запрашивать только активные uniform-переменные, то есть те, что фактически используются шейдером, и создавать соответствующие элементы интерфейса для конечного пользователя динамически. Если бы у авторов шейдеров существовало особое соглашение об именовании uniform-переменных, было бы удобно стандар- тизировать пользовательский интерфейс: например, если любая uniform-перемен- ная заканчивается на «Color», ее значения можно редактировать с помощью диа- лога выбора цвета. Эта функция также может быть полезна в случае совместного использования разных вершинных и фрагментных шейдеров, чтобы определить, какие у конкретных вершинного и фрагментного шейдеров общие uniform-пере- менные. Проще и безопаснее определять, что именно нужно посылать шейдеру, чем задавать все сочетания значений по умолчанию. 7.8. Семплеры Для установки uniform-переменных типа семплер (то есть sampl er ID, sampl er2D, sampler3D, sampl erCube, sampl erlDShadow или sampl er2DShadow) используются функции gl Uni formliARB nglUni formlivARB. Эти переменные могут быть объявлены как в вер- шинном, так и во фрагментом шейдере. Семплер используется, когда какой-то шейдер обращается к конкретной тек- стурной карте. Значение, записанное в семплер приложением, — это номер текстур- ного модуля для обращения к текстуре. Для вершинных шейдеров оно должно быть меньше константы GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB, которую можно получить с помощью функции gl Get и значение которой зависит от реализации. Для фрагментных шейдеров это значение должно быть меньше константы GL_MAX_ ТЕ XTUREJ MAGEJJNI TS_ARB. Суффикс в имени типа семплера обозначает тип текстуры: 1D — одномерная, 2D - двухмерная, 3D — трехмерная, кубическая, 1D с глубиной, 2D с глубиной. В OpenGL текстурный объект каждого из первых четырех типов может быть связан с отдельным текстурным модулем, и этот суффикс позволяет выбрать весь тек- стурный объект. Текстура 1D с глубиной используется для доступа к одномерной текстуре с включенной проверкой глубины, а текстура 2D с глубиной использу- ется для доступа к двухмерной текстуре с включенной проверкой глубины. Если две uniform-переменные различных типов семплера содержат одно и то же значе- ние, при выполнении следующей команды рендеринга возникнет ошибка. Попытка установить значение семплера функциями, отличающимися от gl Uni formliARB или gl Uni formli vARB, также влечет за собой ошибку.
7.9. Средства диагностики 175 Шейдер не знает, как именно устроены семплеры. Современное API задает номер текстурного модуля, в будущем, возможно, семплер будет ссылаться не- посредственно на текстурный объект. Семплеры, обращение к которым может происходить при выполнении програм- мы, называются активными семплерами. Если количество активных семплеров превысит ограничение, компоновка будет неуспешной. Максимальное количество активных семплеров для вершинного процессора определяется константой GL_MAX_ VERTEX_TEXT1IRE_IMAGE_UNITS_ARB, а для фрагментного процессора — константой GL_MAX _ TEXTUR.E_I H:AGE_UN ITSARB. Максимальное количество активных семплеров для обоих процессоров вместе определяется константой GL_COMBINED_TEXTURE_IMAGE_UNITS_ARB. Более подробное описание использования семплеров в шейдере приводится в разделе 10.1. 7.9. Средства диагностики Иногда бывает сложно определить, почему программа не работает. Случаются си- туации, когда программа не работает из-за неправильного значения семплера. Значения этих переменных можно менять в любое время после компоновки и до выполнения программы. Чтобы поведение программы было надежным, реализа- ция OpenGL должна проводить некоторые проверки во время выполнения про- ' граммы, непосредственно перед началом выполнения шейдера (то есть перед выполнением рендеринга). В этот момент единственный способ сообщить об ошиб- ке— установить флаг ошибки в OpenGL, но приложения обычно не проверяют флаг ошибки в этой критической для производительности точке. Чтобы получать более подробную информацию о подобных проблемах, в API языка шейдеров OpenGL определена новая функция, которая может явно выпол- нить такую проверку и выдать диагностическую информацию: void glValidateProgranARBtGLhandleARB program) Функция проверяет, могут ли выполняемые модули из program выполняться для текущего состояния OpenGL. Диагностическая информация, полученная в ре- зультате этой проверки, будет сохранена в информационном журнале программ- ного объекта. Она может содержать пустую строку или строку с описанием того, как текущий программный объект взаимодействует с остальными параметрами OpenGL. Это позволяет разработчикам определить, почему текущая программа неэффективна, не очень оптимальна, не может выполняться и т. д. Результат выполнения этой операции будет сохранен как часть состояния про- граммного объекта. Это значение будет установлено в GL_TRUE при успешной про- верке или в GL_FALSE — при неудачной проверке, а получить его можно функцией glGetObjectParameterARB с аргументами program и GL_OBJECT_VALIDATE_STATUS_ARB. Эта функция используется только во время разработки приложения. Содержимое информационного журнала (текст диагностических сообщений) обычно зависит от реализации OpenGL, то есть нельзя ожидать от разных реализаций одних и тех же сообщений. Так как функции, описанные в этом разделе, могут значительно ухудшить производительность, рекомендуется применять их только при разработке прило- жения и комментировать или удалять вызовы до окончательного выпуска про- граммного обеспечения.
176 Глава 7, API языка шейдеров OpenGL 7.10. Значения, зависящие от реализации Поведение некоторых возможностей, описанных в предыдущих разделах, зави- сит от реализации OpenGL. Все значения API языка шейдеров OpenGL, которые зависят от реализации, могут быть получены функцией glGet. Приведем их. □ GLJ/AX_VERTEXj\TTRIR$_ARB — определяет максимальное количество активных вер- шинных атрибутов. Минимальное предусмотренное стандартом значение — 16, □ GL_MAX_VERTEX_UNIFORM_COMPONENTS_ARB —- определяет максимальное количество компонентов (то есть значений чисел с плавающей запятой), доступных для uniform-переменных вершинного шейдера. Минимальное предусмотренное стандартом значение — 512. □ GL_MAX_VARYING_FLOATS_ARB — определяет максимальное количество varying-пе- ременных, являющихся числом с плавающей запятой. Минимальное преду- смотренное стандартом значение — 32. □ GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB — определяет максимальное количество аппаратных модулей, которые могут быть использованы для доступа к текстур- ным картам из вершинного процессора. Минимальное предусмотренное стан- дартом значение — 0. □ GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB — определяет максимальное общее ко- личество аппаратных модулей, которые могут быть использованы для доступа к текстурным картам и из вершинного, и из фрагментного процессора. Мини- мальное предусмотренное стандартом значение — 2. □ GL_MAX_TEXTURE_1MAGE_UNITS_ARB — определяет максимальное общее количество аппаратных модулей, которые могут быть использованы для доступа к текстур- ным картам из фрагментного процессора. Минимальное предусмотренное стан- дартом значение —• 2. □ GL_MAX _TEXTURE__CCORCS_ARR — определяет максимальное количество доступных наборов текстурных координат. Минимальное предусмотренное стандартом значение — 2. □ GL_MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB — определяет максимальное количество компонентов (являющихся числами с плавающей запятой) для uniform-пере- менных фрагментного шейдера. Минимальное предусмотренное стандартом значение — 64. 7.11. Код приложения для шейдеров кирпичной стены Каждый шейдер хотя бы немного отличается от других. Например, вершинные шейдеры могут использовать различные наборы атрибутов или uniform-перемен- ных, атрибуты могут быть связаны с различными адресами и т. д. Один из приме- ров программы, исходный код которой доступен для загрузки с веб-сайта компа- нии 3Dlabs, — ogL2exampLe. В этой программе есть функция install для каждого
7;Ц. Код приложения для шейдеров кирпичной стены 177 набора шейдеров, которые предполагается установить и использовать. Приведен- ный далее пример похож, хотя и более прост, и определяет функцию установки ШЙдеров кирпичной стены. Функция установит шейдеры кирпичной стеньг, пред- ставленные в главе 6. Но сначала нужно определить простую функцию, которая сможет задавать значения uniform-переменным: GLint getllniLocCGLhandleARB program, const GLcharARB *name) { GLint loc: loc = glGetUniformLocationARB(prograr. name); if (loc — -1) printf("Uniform-nepeMenHoS с именем не существует^". name): printOpenGLError(): // проверка на ошибки OpenGL return loc; ) Шейдеры передаются в OpenGL как строки. Для создаваемой функции уста- новки шейдера предположим, что каждый шейдер определен одной строкой и указатели на эти строки передаются в функцию. Приведем определение функции и ее локальные переменные: int instal 1 BrickShaders(GLcharARB *brickVertex. GLcharARB *brickFragment) GLhandleARB brickVS. brickFS. brickProg: // идентификаторы объектов GLint vertCompiled. fragCompiled; // статусы GLint linked; Аргумент brickVertex содержит указатель на исходный код вершинного шей- дера кирпичной стены, а аргумент bri ckFragment содержит указатель на исходный код фрагментного шейдера кирпичной стены. Далее определяются переменные Для хранения идентификаторов трех объектов OpenGL: шейдерный объект, кото- рый будет использоваться для хранения и компиляции вершинного шейдера, второй шейдерный объект, который будет использоваться для хранения и компи- ляции фрагментного шейдера, и программный объект, к которому будут присо- единены шейдерные объекты. Потом определяются флаги, показывающие резуль- тат компиляции и компоновки. Первый шаг — создание двух пустых шейдерных объектов, одного для вершин- ного шейдера и одного для фрагментного шейдера: brickVS = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB); brickFS = glCreateShaderObjectARB(.GL_FRAGMENT_SHADER_ARB): Исходный код можно загружать в шейдерные объекты после их создания, ког- да они пустые и код их состоит из пустой строки с нуль-символом. Например, так: glShaderSourceARB(bnckVS. 1, &brickVertex. NULL): glShaderSourceARBtbrickFS. 1. SbrlckFragment. NULL): Теперь шейдеры можно компилировать. Для каждого шейдера вызывается функция glCompi 1 eShaderARB, а потом gl GetObjectParameterARB, чтобы видеть, что происходит. Функция gl Compll eShaderARB установит параметр GL_OBJECT_COMPILE_ STATUS^ARB шейдерного объекта в значение GL_TRLfE при успешном завершении операции или в GL_FALSE — в противном случае. Вне зависимости от результата
178 Глава 7. API языка шейдеров OpenGL операции на экран будет выведен информационный журнал шейдера. Если компи- ляция была неудачной, он будет содержать сообщения об ошибках компиляции. Если компиляция завершилась нормально, журнал все равно может содержать полезную информацию, которая поможет совершенствовать шейдер. Проверка компиляционного журнала необходима только во время разработки программы либо при первом запуске шейдера на новой платформе. Если компиляция не вы- полнена либо шейдер не запускается, приложение должно завершить работу: gl ComplleShederARB(brickVS): printOpenGLErrorO; 1! проверка на ошибки OpenGL gl GetObjectPa rameteri vARB(bri ck VS. GL_OBJECT_COMPILE_STATUS_ARB. &vertCompiПed); printlnfoLog(brickVS): glCompileShaderARB(brickFS); printOpenGLErrorO; // проверка на ошибки OpenGL glGetObJectParameter!vARBCbrickFS. GL_OBJECT_COMPILE_STATUS_ARB. fifragCompiled): printlnfoLog(brickFS): if (!vertCompi1ed || JfragCompIled) return 0: Эта часть кода использует определенную ранее функцию pri nt InfoLog. К этому моменту шейдеры уже успешно скомпилированы и почти готовы к ра- боте. Теперь следует присоединить шейдерные объекты к программному объекту для компоновки: brickProg = glCreatePrograniObjectARBO; glAttachObjectARBtbrickProg. bri ck VS): glAttachObjectARBfbrickPreg. brlckFS); Компоновать программный объект нужно функцией gl Li nkProgramARB. Инфор- мационный журнал программного объекта снова выводится вне зависимости от результатов компоновки. При первом запуске шейдера можно получить из него полезную информацию: glLinkProgramARBtbrickProg); printOpenGLErrorO; И проверка на ошибки OpenGL gl GetObjectParameteri vARBtbrickprog. GL_OBJECT_LINKJTATUS_ARB. Blinked): printlnfoLcg(brlckProg); if ((linked) return 0: Программа создана и может быть установлена в качестве текущей функцией gl Us е Prog ramOb j ect ARB: glUseProgramObjectARB(brickProg); Перед выходом из функции 1 nstal 1 Bri ckShaders неплохо было бы инициализи- ровать uniform-переменные для обоих шейдеров. Чтобы получить адрес перемен- ной после компоновки, каждая переменная запрашивается по имени с помощью определенной ранее функции get Un 1 Loe. Этот адрес сразу используется для уста- новки начального значения uniform-переменной: glUniform3fARB(getUn1Loc(brickProg. "BrickColor"). 1.0. 0.3. 0.2): glUni form3fARBtgetllniLoctbrickPr-og. "MortarColor"). 0.86, 0.86. 0.84);
Z11. Код приложения для шейдеров кирпичной стены 179 glUniform2fARB(getUn1Loc(brickProg, "BrickSize"). 0.30. 0.15): glUniform2fAR0(get(JniLoc(brickProg, "BrickPct"). 0.90. 0.35);. glUniform3fAR0(getUniLoc(brickProg. "LightPosition"), 0.0. 0.0. 4.0); return 1: } После возврата из функции приложение может рисовать геометрические фи- гуры, и установленные шейдеры будут выполнять их рендеринг. Результат при- менения к некоторым простым объектам шейдеров, описанных в главе б, и кода программы показан на рис. 6.4. Функция на языке С приведена полностью в лис- тинге 7.2. Листинг 7.2. Функция на языке С для установки шейдеров кирпичной стены int installBrickShaders(GLcharARB *brickVertex. GLcharARB *brickFragment) GLhandleARB brickVS, brickFS. brickprog: // идентификаторы объектов GLInt vertCompiled. fragCompiled: // статусы GLInt linked; // Создание вершинного и фрагментного шейдерных объектов brickVS = glCreateShaderObjectARB(GL_VERTEX_SHADER_AREI): brickFS = glCreateShaderObjectARB(GL_FRAGMENTJjHADER_ARB); // Загрузка исходного кода шейдеров а шейдерные объекты glShaderSourceARB(brickVS. 1. SbrickVertex. NULL): glShaderSourceARBCbrickFS. 1. SbrickFragment. NULL): // Компиляция вершинного шейдера кирпичной стены И и вывод на экран информационного журнала glCompi1eShaderARB(brickVS); printOpenGLErrprt); // проверка на ошибки OpenGL glGetObjectParameterivARB(brickVS. GL_OBJECT_CQMPILE_STATUS_ARB. VertCompiled): printlnfoLog(brickVS): !! Компиляция фрагментного шейдера кирпичной стены // и вывод на экран информационного журнала glCompileShaderARB(brickFS): printOpenGLErrorO: // проверка на ошибки OpenGL glGetObjectParameterivARB(brickFS. GL_CBJECT_COMPILE_STATUS_ARB. SfragCompiled): printlnfoLog(brickFS): if (!vertCompiled || !fragCompiled) return 0: // Создание программного объекта //и присоединение к нему шейдерных объектов brickProg = glCreateProgramObjectARBO: продолжение
180 Глава 7. API языка шейдеров OpenGL Листинг 7.2 (продолжение} glAt tachObjectARB(br1ckprog. brickVS); glAttachObjectARBCbrickProg. brickFS); l! Компоновка программного объекта //и вывод на экран информационного журнала glLinkProgramARB(brickprog): printOpenGLErrorO; // проверка на ошибки OpenGL glGetObjeetParameteri vARBCbrickProg. GLJBJECT_LINK_STATUS_ARB. &1 inked) : printlnfoLog(brickPr.og); if (!linked) return 0: // Установка программного объекта как текущего glllseProgramObjectARBt bri ckProg): // Установка начальных значений uniform-переменных glUniform3fARB(getUniLoc(brickPrqg. "BrickColor"). 1.0, 0.3. 0.2): glUniformSfARBtgetdniLocCbrickProg, "MortarCdor”). 0.85. 0.86. 0.84); glUniform2fARB(getUniLoc(brickProg. “BrickSize"). 0.30, 0.15): glUniform2fARB(getUniLoc(brickProg, "BrickPct"). 0.90. 0.85): glUniforni3fARB(getUn1LoC(brickProg. "LightPosItion"). 0.0, 0.0, 4.0): return 1: 7.12. Итоги Набор функций для создания шейдеров и управления ими в OpenGL невелик. При создании интерфейса все было сделано для того, чтобы процесс разработки такого программного обеспечения походил на обычное программирование на С и C++. Чтобы установить OpenGL-шейдеры, необходимо выполнить следующее: 1. Создать один или несколько пустых шейдерных объектов функцией gl Create- ShaderObjectARB. 2, Передать в эти шейдеры их исходный код функцией gl ShaderSourceARB. 3. Скомпилировать каждый шейдер функцией gl Сотри 1 eShaderARB. 4. Создать программный объект с помощью glCreateProgramObjectARB. 5. Присоединить шейдерные объекты к программному функцией glAttach- ObjectARB. 6. Скомпоновать программный объект функцией gl LinkProgramARB. 7. Установить выполняемую программу в качестве текущей (активной) функци- ей glUseProgramObjectARB.
7.13. Ссылки 181 Адреса определенных разработчиком uniform-переменных можно получить после компоновки функцией gl GetUni formLocationARB, а значения затем устанав- ливаются функцией gl Uni formARB. Определенные разработчиком attribute-переменные могут быть явно связаны с каким-либо индексом дополнительных атрибутов вершин функцией gl Вт ndAttri b- Locati onARB или такое соответствие устанавливается автоматически при компо- новке и может быть получено функцией gl GetAttri bLocati onARB. Значения дополни- тельных атрибутов вершин можно впоследствии задавать из приложения для каждой вершины отдельно одной из версий функции gl VertexAttribARB или для массива вершин — функциями gl EnablelfortexAttribArrayARB и gl VertexAttribPointerARB, Для получения информации о шейдерных и программных объектах также су- ществует ряд функций. 7.13. Ссылки Ссылки на функции OpenGL, которые работают с шейдерами, приведены в при- ложении Б. Исходный код, подобный приведенному в данной главе, с примерами различных шейдеров можно найти на веб-сайте 3Dlabs для разработчиков. 1. 3Dlabs. Веб-сайт для разработчиков (http://www.3dLabs.eom/support./developer). 2. Texturing and Modeling: A Procedural Approach. 3rd ed./D. S. Ebert, J. Hart, B. Mark, at al. San Francisco: Morgan Kaufmann Publishers, 2002 (http://www.text.u- ringandmodeling.com), 3. Kessenich J., Baldwin D., Rost R. The OpenGL Shading Language, Version 1.051/ 3Dlabs. 2003 (http://www.3dLabs.com/support/developer/ogl2). 4. NeiderJ,, Davis T., Woo M. OpenGL Programming Guide. 3rd ed. Reading, MS: Addison-Wesley, 1999. 5. OpenGL Reference Manual. 3rd ed. Reading, MS: Addison-Wesley, 1999. 6. OpenGL Architecture Review Board: спецификация расширения ARB_vertex_sha- der, реестр расширений OpenGL (http://oss.sgi.com/projects/ogl-sampLe/registry). 7. OpenGL Architecture Review Board: спецификация расширения ARBjf ragment_sha - der, реестр расширений OpenGL (http://oss.sgi.com/projects/ogL-sample/registry). 8. OpenGL Architecture Review Board: спецификация расширения ARB_shader_ob- jects, реестр расширений OpenGL (http://oss.sgi.com/projects/ogl-sample/registry). 9. Segal M„ Akeley K. The OpenGL Graphics System: A Specification (Version 1.5)/ Ed.: Ch. Frazier (v. 1.1), J. Leech (v. 1.2-1.5). 2003 (http://opengl.org).
Разработка шейдера В момент написания этой книги инструменты для разработки программ на языке шейдеров OpenGL находились в процессе становления. И хотя некоторые инст- рументы уже существуют (например, RenderMonkey от ATI), в них нет средств для отладки или профилирования. Ожидается, что скоро эта ситуация изменит- ся, так как производители аппаратного и программного обеспечения создают но- вые инструментальные средства для разработки шейдеров. В этой главе представлены некоторые мысли о процессе разработки шейдеров и описаны инструментальные средства, доступные в данный момент. Обсужда- ются как общие приемы программирования, так и применяемые только при со- здании шейдеров. По всей вероятности, вскоре появятся новые продукты, кото- рые заполнят вакуум в области средств для разработки шейдеров, и со временем разработчики шейдеров будут иметь полный набор инструментов. 8.1. Общие принципы Разработка шейдеров — это просто еще один вид программирования; давно суще- ствующие принципы и приемы могут успешно использоваться и при разработке шейдеров. Перед тем как писать код шейдера, нужно сначала этот шейдер спроекти- ровать, чтобы он был прост настолько, насколько это возможно для задачи, кото- рую он призван выполнять. Если шейдер является частью чего-либо большего, нужно проектировать шейдер с учетом надежности и возможности повторного использования кода. Говоря коротко, разработка шейдера — это такой же процесс, как и создание любой другой программы: определенное время планируется для проектирования, реализации, тестирования и документирования. Далее приводится несколько рекомендаций для разработчиков шейдеров. Это скорее советы, чем правила: вполне могут возникнуть ситуации, в которых со- блюдение таких правил не имеет никакого смысла. 8.1.1. Понимание проблемы Это кажется очевидным, однако необходимо периодически напоминать себе, что успех разработки шейдеров во многом зависит от того, как разработчик разобрал- ся в проблеме, прежде чем писать код.
8.1. Общие принципы 183 Прежде чем приступать к разработке, следует убедиться в том, что вы понима- йте алгоритм рендеринга, планируемого для реализации. Если цель — сделать шейдер с отображением бугристости или шероховатости поверхности, нужно убедиться в понимании соответствующих математических формул еще до коди- рования. Карандаш и бумага очень помогают все обдумать и уточнить основные подробности еще до начала написания кода. Поскольку инструменты для разработки шейдеров пока не такие мощные, как средства разработки кода для запуска на обычных процессорах, лучше попробо- вать сначала реализовать модель алгоритма в программе, которая будет работать (й обычном процессоре, прежде чем кодировать на языке шейдеров OpenGL. Это предоставит в распоряжение разработчика мощные средства отладки: пошаговую отладку кода, точки останова и возможность наблюдать работу кода в действии. Конечно же, скоро все это станет доступным и для работы непосредственно на графическом ускорителе. 8.1.2. Постепенное усложнение Многие шейдеры зависят от большого количества подробностей и данных, помо- гающих достичь ожидаемого эффекта. Разрабатывать шейдер лучше таким обра- зом, чтобы можно было сначала разработать и протестировать самые большие и важные возможности, а после того как базовый шейдер заработает, добавлять другие условия и наращивать сложность. Например, есть задача: разработать шей- дер, который будет сочетать эффект шума с присвоением изображению значений из нескольких текстурных карт, а потом еще создавать какие-нибудь особенные эффекты освещения. Эту задачу можно выполнить несколькими способами. Один из них — сначала создать эффекты освещения, используя простую модель шейде- ра. После успешного тестирования этой части шейдера можно постепенно добав- лять другие эффекты, наложение текстурных карт и опять тщательно тестиро- вать. После этого добавляется эффект шума — и опять выполняется тестирование. Таким образом, сложная задача была разбита на несколько подзадач. После -успешного завершения и тестирования одной подзадачи можно переходить к сле- дующей. 8.1.3. Тестирование и повторение Иногда невозможно представить эффект, создаваемый шейдером, например, в про- цессе работы со сложными математическими функциями. В других случаях его вообще сложно представить — например, шум. В этом случае можно попробовать параметризовать алгоритм и систематически изменять входные параметры. За- метки, сделанные при изменении параметров и наблюдении вызванного этим эф- фекта, станут хорошими комментариями к исходному коду шейдера для тех, кому потом придется разбираться в нем, добавлять новые возможности и отлаживать. После того как параметры для достижения нужного эффекта выбраны, можно упростить шейдер, убрав специально добавленные для отладки параметры и за- менив их константами. Это сделает шейдер менее гибким, но более легким для понимания.
184 Глава 8. Разработка шейдера 8.1.4. Упрощение Про упрощение уже много говорилось в предыдущих главах. Простые шейдеры легче для понимания, их проще поддерживать. Обычно существует не один, а не- сколько алгоритмов, которые могут обеспечить нужный эффект. Выбран ли наи- более простой из них? Зачастую есть даже несколько способов реализации одно- го и того же алгоритма. Был ли выбран для реализации самый простой способ? 8.1.5. Модульность Язык шейдеров OpenGL и его API поддерживает модульные шейдеры, так что можно воспользоваться их преимуществами. Придерживайтесь принципа «раз- деляй и властвуй»: лучше проектировать шейдер как набор маленьких простых модулей, которые будут работать совместно. Например, модули для вычисления освещения могут заменять друг друга и под держивать как стандартные, так и до- полнительно определенные источники освещения. Или есть несколько модулей — разных вариантов дымки. Если они правильно спроектированы, их можно сме- шивать и заменять один другим. Принцип модульности можно применять как для вершинных, так и для фрагментных шейдеров. 8.2. Анализ производительности Разработанный по всем правилам шейдер может быть или не быть эффективным по времени выполнения. Рассмотрим некоторые приемы улучшения производи- тельности хорошо сделанного шейдера. 8.2.1. Частота вычислений Вычисления могут выполняться в трех местах: на основном процессоре, на вер- шинном процессоре и на фрагментном процессоре. Привлекательным было бы выполнять большинство вычислений во фрагментном шейдере, поскольку он ра- ботает с каждым нарисованным пикселом, и поэтому картинка получается самого лучшего качества. Но если важна производительность, можно выполнять те же вычисления не во фрагментном, а в вершинном шейдере и получить приемлемое качество изображения, В результате фрагментный шейдер становится более быс- трым. В некоторых случаях нет ощутимых различий между выполнением вычис- лений в вершинном шейдере или тех же вычислений во фрагментном шейдере, например, при вычислении эффекта дымки. Один из способов улучшить производительность — реализовывать эффекты с быстро меняющимися характеристиками во фрагментном шейдере, а эффекты с плавно меняющимися характеристиками в вершинном. Например, рассеянное освещение меняется плавно, так что можно вычислить его с приемлемым каче- ством в вершинном шейдере. Отражение, наоборот, надо рассчитывать во фрагмент- ном шейдере, чтобы добиться хорошего качества. Общее правило таково: при плав- ном изменении характеристик можно использовать и значения, интерполированные между вершинами, в этом случае качество особо не пострадает. Фрагментный шейдер обычно выполняется большее количество раз, чем вершинный, за ис-
8.2. Анализ производительности 185 1 ключением случаев, когда выполняется рендеринг очень маленьких треугольни- ков; так что будет разумным часть вычислений перенести в вершинный шейдер. । Есть вычисления, которые можно выполнить один раз на обычном процессо- ре, — их результаты остаются постоянными для множества запусков и вершинно- го, и фрагментного шейдеров. За счет этого можно сэкономить память для хране- ния команд шейдера или улучшить производительность (или и то и другое), вычисляя некоторые значения заранее с помощью приложения и передавая их в шейдер через uniform-переменные. Зачастую такие вычисления можно распоз- нать, анализируя код шейдера. Допустим, 1 ength передается как uniform-перемен- ная, и шейдер всегда вычисляет sqrt(l ength), так что выгоднее вычислить это значе- ние заранее и передать в шейдер. Если шейдеру нужны и length и sqrt(l ength), эти значения можно передавать как два отдельных параметра. Принимая решение о месте выполнения вычислений, нужно определить узкие места программного обеспечения для конкретной операции рендеринга. Сначала нуж- но ускорять самую медленную часть системы. Не стоит тратить время на улучше- ние производительности не в узких местах, так как все равно выигрыша не будет. 8.2.2. Анализ алгоритма Если разобраться в математике алгоритма, использующегося в шейдере, можно сделать этот шейдер более эффективным. Например, пусть нужно приводить лю- бое значение переменной final col or к диапазону [0, 1]. Зная, что значение этой переменной может быть только больше 0, можно не проверять f i nal col or на отри- цательные значения. Команда Tin (final col or, 1.0) всегда приведет результат к 1,0, поэтому она гораздо более эффективна, чем cl amp (f i па 1 со! or, 0.0,1.0), так как число нужно сравнивать только с одним значением, а не с двумя. Если для каждой пере- менной в шейдере определить диапазон допустимых значений, будет легче выяв- лять возможности оптимизации. 8.2.3. Использование встроенных функций Встроенные функции лучше использовать везде, где только возможно, — это по- может повысить производительность. Встроенные функции реализованы произ- водителем аппаратного обеспечения, и обычно они работают быстрее, чем те же вычисления в обычном коде. Если вычисления в шейдере все-таки сделаны вруч- ную, без использования встроенных функций, вероятность того, что код будет работать быстрее, довольно мал, а ухудшить производительность вполне реально. 8.2.4. Использование векторов Язык шейдеров OpenGL позволяет выполнять операции с векторами простыми и естественными способами, а графическое аппаратное обеспечение обычно спро- ектировано так, что может работать одновременно со всеми значениями вектора. Так что следует пользоваться этим преимуществом и применять векторы в вы- числениях везде, где это возможно. С другой стороны, не следует использовать векторы, большие размером, чем того требуют вычисления. Это влечет за собой ненужный расход регистров, аппаратных интерполяторов (в случае varying-пере- менных), уменьшает пропускную способность процессора и занимает память.
186 Глава 8. Разработка шейдера 8.2.5. Представление сложных функций в виде текстур Так как обработка фрагментов уже программируема, область применения текстур для рисования изображения значительно расширена. Можно сохранять в виде тек- стур сложные функции, а затем вместо громоздких вычислений просто проводить поиск из фрагментного шейдера. Пример такого использования текстур приве- ден в главе 12, где функция шума представлена в виде BD-текстуры. Этот подход дает преимущество при использовании специализированного высокопроизводи- тельного аппаратного обеспечения, обеспечивающего доступ к текстурам, а так- же аппаратного обеспечения, которое выполняет интерполяцию между значени- ями, хранящимися в виде текстур. 8.2.6. Анализ информационного журнала OpenGL может обеспечивать обратную связь, чтобы сообщать о происходящем, через информационные журналы шейдерного и программного объектов (см. раз- дел 7.5). В процессе разработки шейдера совершенно необходимо просматривать информационные журналы компиляции и компоновки, содержащие сообщения о наличии ошибок, следует также обращать внимание и на предупреждения и дру- гие замечания. Эти журналы — основной способ OpenGL сообщить разработчику о производительности, ограничении ресурсов и т. п. 8.3. Отладка шейдера Инструментальные средства для разработки шейдеров еще очень несовершенны, и отладка шейдеров — трудная задача. Вот несколько практических советов, ко- торые могут оказаться полезными при отладке шейдеров. 8.3.1. Анализ выходных данных вершинного шейдера Чтобы узнать, работает ли код вершинного шейдера так, как ожидалось, можно проверять промежуточные значения для выявления неожиданных результатов. Если таковые будут обнаружены, можно изменить одно из выходных значений шейдера и посмотреть, как это повлияет на результат. Например, если разработ- чик ожидает, что значение foo не превысит 5,0, можно задавать цвет фрагмента черным, розовым или ярко-зеленым при превышении переменной foo этого зна- чения. Если это недостаточно очевидно и преобразованные однородные коорди- наты уже вычислены, можно попробовать сделать что-то вроде следующего: if (foo > 5.0) gl_Position += 1.0; Эта операция добавляет 1 к каждому компоненту преобразованных коорди- нат, для которых foo больше 5,0, и можно увидеть сдвиг объекта на экране. Этим способом можно систематически проверять предположения о промежуточных значениях, поступающие от вершинного шейдера.
8.4. Средства разработки шейдеров 187 -------------------------------------------------------- 8.3.2. Анализ выходных данных фрагментного шейдера Возможные выходные данные фрагментного шейдера цвет фрагмента, его глу- бина, или и то и другое, или признак того, что буфер кадров обновляться не будет из-за ключевого слова discard. Значение глубины, возможно, не будет полезным при отладке, а вот цвет можно использовать, устанавливая определенные цвета для «неправильных» выходных данных. Ключевое слово discard можно исполь- | зовать для отбрасывания фрагментов с какими-либо ненужными качествами, тогда они вообще не будут отображаться на экране. Таким образом, разработчик получит зрительный образ на экране и сможет понять, что происходит внутри шейдера. Например, если нет уверенности в том, что все координаты 2В-текстуры при- надлежат диапазону от 0 до 1,0, можно сделать в шейдере проверку е помощью 1 f иотбросить фрагменты, не удовлетворяющие этому условию. Можно отбрасывать все фрагменты, в которых и и t-координаты текстуры больше 0,5 или любая координата больше 0,99 и т. д. Изображение будет нарисовано с «пропавшими» пикселами там, где фрагменты были отброшены. Ключевое слово discard здесь оказывается полезным, так как его можно помещать в любое место фрагментного шей- дера. Начиная проверку, нужно поместить этот оператор в самом начале шейдера и по мере проверки последующих частей кода постепенно передвигать его вниз. Похожим способом можно заносить в gl_FragCoior отладочную информацию. До- пустим, в шейдере есть математическая функция, значение которой должно при- надлежать диапазону [0, 1] со средним значением 0,5, можно занести в gl_FragCol or зеленый цвет, если значение функции меньше нуля, красный цвет, если значение от 0 до 0,5, синий, если значение от 0,5 до 1,0, и белый, если значение больше 1,0. Такой способ отладки может быстро показать, что же не так. 8.3.3. Простые геометрические фигуры Для отладки вычислений с текстурами можно выполнить рендеринг простого большого прямоугольника с идентичными матрицами модели, вида и проекции и посмотреть, что получится. Применяя в виде текстуры простое изображение, например цветные прямоугольники или плавный переход цвета, можно легко оп- ределить, что идет не так в операциях текстурирования. 8.4. Средства разработки шейдеров В будущем появятся гораздо более мощные средства разработки шейдеров, чем существуют сейчас. В этом разделе описаны средства, доступные во время напи- сания этой книги. 8.4.1. RenderMonkey С тех пор как появились программируемые графические процессоры, стало по- нятно, что, кроме разработки собственно кода для шейдеров, следует обращать внимание и на другие моменты. Шейдеры могут быть настраиваемыми до такой степени, что могут работать так, как от них ожидается, только в простых случаях.
188 Глава 8, Разработка шейдера Исходный код шейдера, текстуры, геометрические фигуры, начальные значения uniform-переменных — все это важные составные части качественного шейдера. Чтобы помочь разработчикам преуспеть, нужно обеспечить их средствами для распознавания, модификации и поддержки всех основных элементов шейдера. Еще один фактор, который нужно учитывать при разработке шейдеров, — это то, что обычно код шейдера и код приложения пишут разные разработчики. Зача- стую дизайн текстур делает вообще художник, и он же принимает участие иди даже контролирует процесс разработки шейдера. Сотрудничество художника и разработчика игровых приложений является существенным влияющим факто- ром, и это тоже должно учитываться при создании инструментов разработки. Комплексная среда для разработки (IDE) позволяет разработчикам и худож- никам разрабатывать шейдеры и экспериментировать с ними вне приложения. Это уменьшает сложность задач, стоящих перед разработчиком, и стимулирует быструю разработку интерфейса и процедуры экспериментирования. Закончен- ные шейдеры интеллектуальная собственность, и ее сопровождение, кросс - платформенность и легкость внедрения на различных платформах очень важны для получения компанией-разработчиком максимальной выгоды. IDE должно работать со всеми элементами законченного шейдера: управлять ими, совместно использовать, инкапсулировать и экспортировать. Самая первая среда для разработки — RenderMonkey — была выпущена ATI в 2002 г. Самая первая версия RenderMonkey поддерживала разработку вершин- ных и пиксельных шейдеров для DirectX. Однако RenderMonkey был спроекти- рован так, что его можно легко расширять и этим обеспечить поддержку других шейдерных языков. В начале 2004 г. компании ATI и 3Dlabs совместно разработа- ли версию RenderMonkey, которая поддерживала высокоуровневую разработку шейдеров в OpenGL на языке шейдеров OpenGL в дополнение к языку шейдеров DirectX. Бесплатную версию RenderMonkey IDE можно найти на веб-сайтах этих компаний: http://www.3dlabs.com и http://www.ati.com. RenderMonkey был спроектирован с учетом расширяемости. Его основа гиб- кая оболочка, которая позволяет встраивать поддержку шейдерных языков. Это среда, ориентированная на язык и позволяющая поддерживать практически лю- бой высокоуровневый шейдерный язык, организованный в виде встраиваемых модулей. Сейчас он поддерживает разработку пиксельных и вершинных шейде- ров из Microsoft DirectX 8.1 и 9.0, высокоуровневый язык шейдеров (HLSL), оп- ределенный в DirectX 9.0, и язык шейдеров OpenGL. Инкапсуляция всех необходимых для создания эффекта данных называется рабочей средой эффекта (effect workspace). Эта среда состоит из узлов группи- ровки эффектов, узлов для переменных и других узлов. Каждая группа эффектов состоит из нескольких узлов эффектов, а каждый узел эффекта может состоять из одного или нескольких проходов рендеринга. Каждый проход рендеринга может включать в себя состояние рендеринга, исходный код для вершинного и фрагмент- ного шейдеров, геометрические фигуры и текстуры. Все данные для создания эф- фектов организованы в виде дерева, видимого в окне рабочей среды. Узлы группировки эффектов используются для группировки родственных эффектов в один контейнер, что повышает удобство работы со сложными эффек- тами. Такую группировку можно использовать также для того, чтобы объединять похожие эффекты с разными характеристиками производительности (например,
8.4. Средства разработки шейдеров 189 «лучшего качества», «быстрый» и «очень быстрый»). Критерии для группировки эффектов полностью зависят от разработчика. Узлы группировки эффектов объединяют всю информацию, необходимую для реализации визуального эффекта, формирование которого может происходить в несколько проходов. Начальное состояние эффекта унаследовано от эффекта по умолчанию, с его помощью задается какое-то известное начальное состояние для всех эффектов. Эффект по умолчанию можно использовать для хранения каких- либо данных, общих для всех эффектов. Узлы проходов определяют операции рисования. Каждый проход наследует данные из предыдущих проходов, создающих этот эффект, а первый проход — из эффекта по умолчанию. Обычно проход состоит из пары «вершинный шейдер — фрагментный шейдер», блока состояния рендеринга, текстур, геометрических фигур и узлов других типов (например, узлов переменных). В этих проходах мо- гут быть использованы различные геометрические фигуры, например, чтобы вы- полнять рендеринг эффекта меха. В узлах переменных определяются параметры, доступные из шейдера. В язы- ке шейдеров OpenGL узлы переменных — это механизм объявления uniform-пе- ременных. Им можно назначать имена и типы, а содержимым узла переменных можно управлять через специальные графические элементы управления. RenderMonkey полностью состоит из подключаемых модулей. Исходный код некоторых модулей находится в свободном доступе, к тому же для конкретных задач можно писать собственные модули, которые будут выполнять импорт и эк- спорт данных, работая с правильными форматами. Далее приведен список моду- лей RenderMonkey. □ Редакторы шейдеров — интуитивный интерфейс смоделирован на примере Microsoft Visual Studio; поддерживают редактирование вершинных и фрагмент- ных шейдеров, подсветку синтаксиса, создание шейдеров на языках OpenGL, HLSL, ассемблера. □ Редакторы переменных — параметры шейдера можно редактировать посред- ством графических элементов управления, вид которых зависит от типа дан- ных редактируемого параметра; есть элементы для цвета, векторов, матриц, ска- лярных чисел; можно также создавать свои элементы управления. □ Редактор для художника — параметры шейдера, с которыми работают дизай- неры, могут быть представлены в виде, удобном для просмотра и редактирова- ния; программисты могут устанавливать флаг, обозначающий, может ли конк- ретный параметр редактироваться художником; все изменения сразу же будут видны на экране. □ Предварительный просмотр — позволяет быстро просмотреть результаты при- менения эффекта; изменения в исходном коде шейдера или в его параметрах немедленно отражаются в окне предварительного просмотра; параметры изоб- ражения можно настраивать; точка обзора задается (спереди, сзади, сбоку); поддерживается предварительный просмотр для DirectX 9.0, Open GL и языка шейдеров Open GL. □ Экспортер — все, что требуется для создания эффекта, записывается в отдель- ный файл в XML-формате.
190 Глава 8, Разработка шейдера □ Импортер — все, что требуется для создания эффекта, может быть прочитано из отдельного файла в XML-формате. XML-формат был выбран основой для хранения информации о шейдере в Ren- derMonkey, так как это промышленный стандарт, формат файла достаточно удо- бочитаем, его можно расширять, и существует большое количество бесплат- ных синтаксических анализаторов в свободном доступе. Файл в XML-формаТе, в котором хранится эффект, содержит весь исходный код шейдера, все состояния для рендеринга, все модели, информацию о текстурах. Поэтому легко создавать, совместно использовать свойства шейдера и управлять ими. 8.4.2. Внешний интерфейс компилятора языка шейдеров OpenGL В июне 2003 г. компания 3Dlabs выпустила открытую версию лексического анали- затора, синтаксического анализатора, семантического анализатора (то есть внеш- ний интерфейс — front-end — компилятора языка шейдеров OpenGL). Эти програм- мы способны считывать шейдер OpenGL и преобразовывать его в поток лексем, то есть проводить лексический анализ. Затем поток лексем проверяется на пра- вильность выражений. Эта процедура называется синтаксический анализ, или пар- синг. Затем выполняется семантический анализ — проверка шейдера на соответ- ствие семантическим правилам, определенным или предполагаемым специфи- кацией языка шейдеров OpenGL. Результат такой обработки преобразуется в пред- ставление исходного кода на высоком уровне. Этот высокоуровневый промежу- точный язык (high-level intermediate language, HIL) — двоичное представление исходного кода, которое может в дальнейшем обрабатываться внутренним аппа- ратно-зависимым модулем компилятора, при этом создается машинный код для конкретного вида графического акселератора. Производители аппаратного обеспечения должны создавать реализацию внут- реннего модуля компилятора (back-end) для конкретного графического акселе- ратора. Обычно в таком модуле содержится специфическая информация о гра- фическом акселераторе, которая является интеллектуальной собственностью про- изводителя. От производителей не ожидают, что они опубликуют исходный код этих модулей. Внешний интерфейс компилятора, созданный 3Dlabs, — полезный инструмент для разработки на языке шейдеров OpenGL, он может использоваться и другими компаниями, желающими создать компилятор языка шейдеров OpenGL. Внешний интерфейс был создан сразу после завершения создания спецификации. За ис- ключением препроцессора (который был создан из другого открытого препроцес- сора), он разрабатывался с нуля с помощью утилит flex и bison. Это послужило хорошим поводом еще раз перепроверить спецификацию и найти дефекты языка перед новым ее выпуском. И действительно, несколько дефектов было найдено именно этим способом, а спецификация была доработана еще перед ее выпуском. Внешний интерфейс компилятора языка шейдеров OpenGL, в силу его чистой реализации, служит дополнительной документацией о языке. Разработчик дол- жен вначале изучить спецификацию, но если нужны подробности, чтобы полу- чить точный ответ, следует обратиться к интерфейсу компилятора.
8.6. Ссылки 191 Создатели реализаций OpenGL, которые основывают свой компилятор на I внешнем интерфейсе компилятора от 3Dlabs, очень помогают своим будущим | пользователям: семантика языка будет проверяться одинаково во всех реализа- I днях, которые будут использовать этот интерфейс. Это будет очень удобно для разработчиков, так как они смогут полагаться на согласованную обработку оши- бок даже в разных реализациях OpenGL. Скорее всего, немногие читатели этой книги будут разрабатывать компилятор языка шейдеров OpenGL, но эта ссылка будет полезна для всех. Исходный код I внешнего интерфейса компилятора можно загрузить с веб-сайта компании 3Dlabs (http://www.3dLabs.com). 8.5. Итоги Создание шейдеров во многих отношениях похоже на другие задачи, решаемые разработчиками. Некоторое количество здравого смысла очень помогает принять верное решение. Принципы обычного программирования тоже можно применять при разработке шейдеров, особенно справедливо это для ранних поколений про- граммируемых графических акселераторов. Так как ранние реализации языка шейдеров OpenGL могут быть в каком-то смысле неполными, компиляторы пока несовершенны, характеристики производительности оборудования от разных производителей значительно различаются, а инструментальные средства для раз- работчиков оставляют желать лучшего. RenderMonkey — единственное доступ- ное на сегодняшний день средство разработки; но, можно надеяться, остальные не заставят себя долго ждать. В то же время написание шейдеров для программируемых графических аксе- лераторов представляет собой нечто уникальное. Нужно принимать правильные решения о разделении вычислений между главным, вершинным и фрагментный процессорами. Не лишними будут хорошая математическая подготовка и знание компьютерной графики, доскональное знание работы OpenGL и понимание ап- паратной базы. Зачастую приходится разрабатывать шейдер совместно с худож- ником. Для разработчика это может послужить стимулом сделать шейдер более параметризируемым, для того чтобы его можно было использовать всесторонне. 8.6. Ссылки Существует множество книг, описывающих принципы и практические приемы разработки программного обеспечения. Две из них предназначены специально для разработки шейдеров: [4 и 2]. Некоторые рассуждения в этих книгах относятся скорее к RenderMan, но большинство принципов применимо и к разработке шей- деров на языке шейдеров OpenGL. Существует интересный способ улучшения производительности. Можно завес- ти дружеские отношения с разработчиками компании (или компаний), производя- щей графические акселераторы. Эти люди могут сообщить некоторые полезные подробности об архитектуре акселератора и относительной производительности
192 Глава 8. Разработка шейдера различных его частей. Так как все идет к созданию нового поколения (или двух) программируемых графических процессоров, возникнет множество различий между разными аппаратными архитектурами в зависимости от компромиссов, на которые согласятся пойти конструкторы и разработчики драйверов. Поиск на веб-сайтах компаний-производителей, посещение их презентаций на выставках и множество вопросов может дать отличные результаты. Веб-сайт ATI содержит ряд презентаций RenderMonkey. Среду для разработ- ки и документацию можно загрузить с веб-сайта либр ATI, либо 3Dlabs. 1. 3Dlabs. Веб-сайт для разработчиков (http://www.3dLabs.conn/support/developer). 2. Apodaca A. A., Gritz L. Advanced RenderMan: Creating CGI for Motion Pictures. San Francisco: Morgan Kaufmann Publishers, 1999 (http://www.bmrt.org/arnnan/ materials.html). 3. ATI. Веб-сайт для разработчиков (http://www.ati.com/na/pages/resource_centre/ dev„rel/devrel.htmL). 4. Texturing and Modeling: A Procedural Approach. 3rd ed./D. S. Ebert, J. Hart, B. Mark, at al. San Francisco: Morgan Kaufmann Publishers, 2002 (http://www.tex- turingandmodeling.com). 5. NVIDIA. Веб-сайт для разработчиков (http://deveLoper.nvidia.com).
Традиционные шейдеры у Программируемость в OpenGL открывает множество новых возможностей для рендеринга, хотя поучительным может быть использование некоторых частей стандартной функциональности OpenGL для рендеринга с помощью шейдеров OpenGL. Эти фрагменты кода в дальнейшем могут быть использованы как от- правные точки для создания чего-то большего и лучшего. В этой главе показано, как код шейдера OpcnGL может имитировать стандарт- ную функциональность OpenGL для обработки вершин и фрагментов. Цель кода, приведенного здесь, — дать точное представление о стандартной функциональ- ности OpenGL 1.5. Примеры кода содержат ссылки на всевозможные состояния OpenGL с помощью встроенных переменных. В собственных шейдерах можно все это передавать через определенные разработчиком uniform-переменные, а не счи- тывать состояние OpenGL. Делая это, читатель будет готов отказаться от исполь- зования машины состояний OpenGL вообще с целью расширения шейдеров и по- иска новых возможностей кода. .1. Преобразования Одна из возможностей языка шейдеров OpenGL — простота преобразования ко- ординат одной системы в координаты другой. Преобразование, необходимое по- чти каждому вершинному шейдеру, было продемонстрировано в предыдущих гла- вах. Входные координаты вершины должны быть преобразованы в координаты пространства отсечения, пригодные для работы со стандартной функциональнос- тью после вершинного шейдера. Это обычно делается одним из двух способов, первый из них таков: // преобразование вершинных координат в координаты пространства отсечения gl_Pos1tion = gl_ModelVtewArojectlonMatriх * gl_Vertex: а второй: gl_Position = ftransform(): Единственное различие между ними в следующем: второй способ гарантирует, что преобразование координат выполнялось точно таким же способом, как это делает стандартная функциональность. Некоторые реализации могут иметь соб- ственные аппаратные усовершенствования, которые будут обусловливать неболь- шие различия в таких преобразованиях. Это может создавать проблемы при использовании многопроходных алгоритмов, где один и тот же алгоритм исполь- зуется для рисования одних и тех же фигур более одного раза. В таких случаях 7 Зак. 218
194 Глава 9. Традиционные шейдеры предпочтительно применять второй метод, поскольку он работает точно так же, I как стандартная функциональность. Координаты источника освещения преобразуются с помощью матрицы моде- ли-вида при передаче их в OpenGL. Это значит, что сохраняются они в координа- | тах обзора. Зачастую удобнее рассчитывать освещение в системе координат обзора, । так что бывает необходимо сделать соответствующие преобразования (листинг 9.1). Листинг 9.1. Вычисление координат точки обзора vec4 ecPosition; vec3 ecPosition3: // in 3 space И Преобразование координат вершины в систему координат обзора if CNeedEyePosItion) { ecPosition = gl_ModelViewMatrix * gl_Vertex; ecPosition3 = (vec3 (ecPosltlon)i / ecPosition.w: } Этот фрагмент кода вычисляет однородную точку в системе координат обзора (vec4), а также неоднородную точку (vec3). Оба значения понадобятся в дальнейшем. Чтобы рассчитать освещение в пространстве обзора, нужно также преобразо- вать нормали поверхности. Для доступа к матрице преобразований нормали су- ществуют встроенные uniform-переменные (листинг 9.2). Листинг 9.2. Преобразования нормали normal = gl Normal Matrix * gl_Normal: Во многих случаях приложение может и не знать ничего о характеристиках нормалей поверхности. Чтобы убедиться в том, что освещение вычислено правиль- но, нужно нормализовать каждую входящую нормаль до единичной длины. В стан- дартной функциональности OpenGL нормализация — это параметр, который мож- но контролировать с помощью передачи символической константы GL_NORMALIZE в функции gl Enablе или glDisable. В шейдере OpenGL необходимая нормализа- ция делается так, как в листинге 9.3. Листинг 9.3. Нормализация нормали normal = normalize(normal); Иногда приложение знает, что всегда будет посылать нормали в единичном виде, а матрица модели-вида будет применяться для масштабирования. В этом случае можно изменять масштаб, чтобы избежать «дорогой» операции извлече- ния квадратного корня. Коэффициенты масштабирования должны быть предо- ставлены приложением, и нормали могут быть масштабированы так, как показа- но в листинге 9.4. Листинг 9.4. Масштабирование нормали normal = normal * gl_NormalScale; Коэффициенты масштабирования сохраняются как состояние OpenGL, и к ним мож- но обращаться из шейдера, используя встроенную uniform-переменную gl _Norma 1 Seal е. Координаты текстур также можно преобразовывать. Для каждого текстурного модуля в OpenGL определена текстурная матрица и к ней можно обращаться че-
9.2. Источники освещения 195 рез встроенную uniform-переменную массива матриц gl_TextureMatriх. Входные координаты текстур преобразуются точно так же, как координаты вершин (лис- тинг 9.5). Листинг 9.5. Преобразование текстурных координат g1_TexCoord[0] = gl_TextureMatr1x[0] * gl_Mult1TexCoordO: 9.2. Источники освещения В OpenGL необходимо вычислять освещение. Начнем с определения функции для каждого типа источников освещения: направленного света, точечных и про- жекторов. Для этого существуют переменные, в которых будут содержаться доли рассеянного, отраженного и зеркального света от всех источников освещения. Эти переменные должны быть инициализированы в 0 перед вычислением освещения. 9.2.1. Источники направленного света Направленный свет — это свет, источник которого находится бесконечно далеко от освещаемых объектов. С таким допущением все лучи света от источника па- раллельны, и для каждой точки можно использовать один и тот же вектор направ- ления освещения. Это упрощает математические вычисления, поэтому код для работы с направленным освещением проще, чем код для работы с другими вида- ми освещения, и быстрее работает. Так как источник света бесконечно далек, на- правление максимального освещения одинаково для каждой точки. Этот вектор можно вычислить для каждого источника света i заранее и сохранить в перемен- ной gl_LightSource[1 ]. hal fVector. Этот тип источника освещения используется для имитации солнечного света. Функция вычисления направленного света (листинг 9.6) определяет косинус угла между нормалью поверхности и направлением освещения, а также косинус угла между нормалью поверхности и половиной угла между направлением осве- щения и направлением обзора. Предыдущее значение умножается на цвет рассе- янного освещения, чтобы вычислить рассеянную часть освещения. Последующее значение возводится в степень из gl_FrontMaterial .shininess, а затем умножается на цвет отраженного освещения. Отраженное или зеркальное освещение присутствует, только если угол меж- ду направлением освещения и нормалью поверхности находится в диапазоне [-90°, 90°]. Это можно определить проверкой значения nDotVF. Это значение уста- навливается в максимальное значение из 0 и косинуса угла между направлением света и нормалью поверхности. Если это значение — 0, значение зеркального от- ражения также устанавливается в 0. Приведенная здесь функция направленного освещения предполагает, что все векторы нормализованы, так что операция dot над двумя векторами дает косинус угла между ними. Листинг 9.6. Вычисления освещения от направленного источника void DI rectionalLight С in int 1. in vec3 normal. Inout vec4 ambient. . продолжение &
196 Глава 9. Традиционные шейдеры Листинг 9.6 {продолжение) Inout vec4 diffuse. Inout vec4 specular) { float nDotVP; // нормаль . направление освещения float nDotHV: // нормаль . половинный вектор float pf; // степень nDotVP = maxtO.O. dottnormal. vec3 (gl_L1ghtSource[i].position))): nDotPV = max(0.0. dot(normal. vec3 (gl_LightSource[i].halfVector))): if (nDotVP == 0.0) pf = 0.0: else pf = pow(nDotHV. gl_FrontMaterial.shininess): ambient += gl_LightSource[iJ.ambient: diffuse += gl_L1ghtSource[i].diffuse * nDotVP; specular += gl_LightSource[i].specular * pf: } 9.2.2. Точечные источники Точечные источники освещения имитируют свет возле или внутри изображения, например лампы, люстры или уличные фонари. Существует два основных отли- чия точечных источников света от источников направленного освещения. Первое состоит в том, что направление максимальной подсветки должно быть вычислено в каждой вершине, а значение из gl_LightSource[iLhalfVector не может быть ис- пользовано. Второе отличие — в том, что свет, полученный поверхностью, умень- шается с удалением источника освещения. Этот эффект называется поглощением. Каждый источник освещения имеет постоянный, линейный и квадратичный ко- эффициенты, которые используются при вычислении доли освещения от точеч- ного источника. Эти различия выделяются уже в первых нескольких строках функции вычис- ления освещения от точечного источника (листинг 9.7). Первый шаг — вычисле- ние вектора от поверхности до источника освещения с помощью функции длины. VP нормализуется и в дальнейшем будет использоваться в операции dot при на- хождении правильного значения косинуса. Коэффициент поглощения и направ- ление максимума освещения вычисляются как обычно. Остальной код остается таким же, как и в предыдущей функции (вычисление света от источника направ- ленного освещения), за исключением того, что отраженные и зеркальные состав- ляющие освещения умножаются на коэффициент поглощения. Оптимизация, которую здесь можно выполнить, состоит в том, чтобы получить две функции для вычисления точечного освещения: одна функция учитывает коэф- фициент поглощения, а другая — нет. Если значения постоянного, линейного и ква- дратичного коэффициентов — (1, 0, 0) (значения по умолчанию), для улучшения производительности можно использовать функцию без вычисления поглощения. Листинг 9.7. Вычисления освещения от точечного источника void PointLighttin int i. in vec3 eye.
9.2. Источники освещения 197 in vec3 ecPositiопЗ, in vec3 normal. inout vec4 ambient, inout vec4 diffuse, incut vec4 specular) float nDotVP: U нормаль . направление освещения float nDotHV; П нормаль . половинный вектор float Pf: // степень float attenuation; // вычисленный коэффициент поглощения float d: 11 расстояние от поверхности до источника уесЗ VP: П направление от поверхности до источника vec3 halfVector: И направление максимального освещения // Вычисление вектора от поверхности до источника VP - vec3 (gl_LightSource[1].position) - ecPositiопЗ; // Вычисление расстояния между поверхностью и источником d = 1engthС VP): И Нормализация вектора от поверхности до источника I VP = normalize(VP): И Вычисление поглощения I attenuation = 1.0/( gl_LightSource[1J-cohstantAttenuation + f gl_LightSource[i].linearAttenuation * d + I gl_L1ghtSource[i].quadraticAttenuation *d*d); halfVector = normalize(VP + eye): ' nD.otVP = maxCO.O. dot(normal. VP)): nDotHV = maxCO.O. dotCnormal. halfVector)): if (nDotVP — 0.0) pf = 0.0: else pf = powCnDotHV. gl JrontMaterial .shininess): ambient += gl_LightSource[i].ambient: diffuse += gl_LightSource[i].diffuse * nDotVP * attenuation: specular += gl_LightSource[il.specular * pf * attenuation: } 9.2.3. Прожекторы В театре и кино прожекторы обеспечивают мощный луч света, который освещает конкретный участок. Освещенный участок может ограничиваться заслонками, расположенными по сторонам источника освещения. В OpenGL существуют ат- рибуты освещения, которые используются для имитации простого типа прожек- тора. Так как обычно точечные источники освещения излучают свет во всех на- правлениях, OpenGL изменяет их таким образом, чтобы в некоторых направлениях этого излучения не происходило, и получается конус света. Начало и окончание функции прожектора (листинг 9.8) выглядят точно так же, как функция точечного освещения (см. листинг 9.7). Различия — в середи- не функции. Вычисляется скалярное произведение центрального направления
198 Глава 9. Традиционные шейдеры прожектора (gl_L1ghtSource[1LspotDi recti on) и вектора от источника освещения до поверхности (-VP). Полученное значение косинуса сравнивается с предва- рительно вычисленным значением косинуса граничного значения (gl_Light- Sou rce El J - spot Cos Cutoff), чтобы определить, где находится данная точка по- верхности — внутри конуса света или снаружи. Если точка находится снаружи, коэффициент поглощения равен 0; в противном случае это значение возводится в степень gl_LightSource[i ] .spotExponent. Этот коэффициент поглощения умножа- ется на тот, что был вычислен предварительно, чтобы получить общий коэффи- циент. Остальные строки кода не отличаются от кода функции для точечного ис- точника освещения. Листинг 9.8. Вычисление освещения от прожектора void Spotlight( in int i. in vec3 eye. in vec3 ecPosition3. in vec3 normal. inout vec4 ambient, inout vec4 diffuse, inout vec4 specular) { float nDotVP: float nDotHV: // нормаль . направление освещения И нормаль . половинный вектор освещения float pf; // степень float spotDot: И косинус угла раскрытия конуса float spotAttenuation: // коэффициент поглощения float attenuation: И вычисленный коэффициент поглощения float d: // расстояние от поверхности до. источника 11 освещения vec3 VP: // направление от поверхности до источника // освещения vec3 ha IfVector: И направление максимума освещения // Вычисление вектора от поверхности до источника освещения VP = vec3 (gl_LlghtSource[i].position) - ecPosition3: /l Вычисление расстояния между поверхностью и источником освещения d = 1ength(VP): И Нормализация вектора от поверхности до источника освещения VP = normalize(VP): // Вычисление коэффициента поглощения attenuation - 1.0 / (gl_LightSource[1].constantAttenuatfon + gl_LightSource[iJ.11 nearAttenuation * d + gl_L1ghtSource[i].quadraticAttenuation * d * d); // Проверка вхождения точки поверхности в конус света spotDot - dOt(-VP. gl _L 1 ghtSou reel ILspotDi recti on): if (spotDot < gl_LiShtSource[i].spotCosCutoff) spotAttenuation = 0.0: И этот источник освещения не участвует else spotAttenuation =* pow(spotDot. gl_LightSource[1 ] .spotExponent): ll Совмещение коэффициентов поглощения и расстояния
9.3. Свойства материала и освещение 199 attenuation *= spotAttenuation; halfVector = normal1ze(VP + eye): nDotVP = maxlO.O. dot(normal. VP)); nDotHV = max(0.0. dot(normal. halfVector)); If (nDotVP “ 0.0) pf = 0.0: else pf = pow(nDotHV. gl_FrontMaterial.shininess); ambient += gl_LightSource[i],ambient: diffuse +“ gl_L1ghtSource[i].diffuse * nDotVP * attenuation: specular += gl_LightSource[1].specular * pf * attenuation: } .3. Свойства материала и освещение Вычисление освещения в OpenGL требует знания направления обзора в системе координат обзора, чтобы вычислить элемент отражения. По умолчанию направ- ление обзора принимается параллельным оси -2. В OpenGL есть режим, требую- щий определения направления обзора от начала координат в системе координат обзора. Чтобы сделать это, можно преобразовать входные координаты вершины в систему координат обзора с помощью текущей матрицы модели - вида. Координа- ты х, у и z точки обзора делятся на однородную координату w для получения зна- чения vec3, которое и используется в дальнейших вычислениях. Вычисление этих координат обзора (ecPosi tiопЗ) показано в разделе 9.1. Для получения единично- го вектора направления обзора нужно нормализовать и инвертировать координа- ты. Код шейдера, который это выполняет, показан в листинге 9.9. Листинг 9.9. Вычисление координат наблюдателя if (Localviewer) eye = -normal1ze(ecPos1tion3): else eye - vec3 (0.0, 0.0. 1.0); После этого можно инициализировать переменные, которые затем будут ис- пользоваться для накопления составляющих рассеянного света, отражения и зер- кального отражения от всех источников освещения. Можно использовать функ- ции из предыдущего раздела, чтобы вычислить вклад в освещение каждого источ- ника. В коде листинга 9.10 предполагается, что все источники освещения с номе- ром, меньшим NumEnabledLlghts, включены. Источники направленного освещения имеют значение однородной координаты w равное 0 во время передачи этих коор- динат в OpenGL (затем все переданные координаты преобразуются с помощью матрицы модели-вида, так что координата w после преобразования останется О, если последний столбец матрицы типичный (ООО 1)). У точечных источников освещения угол ограничения равен 180°. Листинг 9.10. Цикл для вычисления суммарного освещения от всех источников // Инициализация переменных для накопления интенсивности света amb = vec4 (0.0): _ . продолжение У
200 Глава 9. Традиционные шейдеры Листинг 9.10 (продолжение) diff = vec4 (0.0): spec = vec4 (0.0): // Цикл по включенным источникам for (i’O: i< NumEnabledLights: i++) if (gl_LightSource[i].position.w == 0.0) DirectionalLightti. normal, amb, diff. spec): else if (gl_LightSource[i].spotCutoff = 180.0) PointLightd. eye. ecPosition3. normal, amb. diff. spec): else SpotLightii. eye, ecPos1tion3. normal. amb. diff. spec): } Одна из дополнительных возможностей OpenGL 1.2 — функциональность для расчета цвета вершины за два этапа: сначала как обычно вычисляется первичный цвет, включая компоненты излучения, рассеяния и отражения; затем вторичный цвет, который содержит только компонент зеркального отражения. Если этот ре- жим не включен (по умолчанию), первичный цвет будет содержать все компо- ненты: излучения, рассеяния, отражения и зеркального отражения. Вычисление зеркального отражения отдельно от остальных позволяет накла- дывать его после текстурирования. Значение зеркального отражения накладыва- ется на вычисленный цвет после текстурирования для того, чтобы отраженный свет был окрашен в цвет источника освещения, а не в цвет поверхности. Вычисле- ние цвета поверхности без компонента зеркального отображения показано в лис- тинге 9.11. Листинг 9.11. Вычисление цвета поверхности без компонента зеркального отражения color = gl_Frontl_ightModelProduct.sceneColor + amb * gl_FrontMaterial.ambient + diff * gl_FrontMaterial .diffuse; Язык шейдеров OpenGL предоставляет удобную встроенную переменную gl_FrontL1ghtModel Product. sceneCol or, в которой содержится значение излучающе- го свойства материала для передних поверхностей и общее значение рассеянного освещения (то есть gl_FrcntMaterial.emission + gl_FrontMaterial.ambient * gl_LightModel .ambi ent). Можно сложить его с интенсивностью отраженного света и интенсивностью рассеянного света. В дальнейшем нужно поступать в зависи- мости от того, указан ли режим отдельного наложения компонента зеркального отражения (листинг 9.12). Листинг 9.12. Завершающие вычисления цвета поверхности if (SeparateSpecular) gl_FrontSecondaryColor = vec4 (spec *gl_FrontMatenal .specular. 1.0); el se color +- spec * gl_FrontMaterial .specular: gl_FrontColor = color; Нет необходимости ограничивать значения (clamping) gl_FrontSecondaryColor и gl_FrontCol or, так как это будет сделано автоматически.
9.5. Отсутствие освещения 201 4. Двухстороннее освещение Чтобы в OpenGL имитировать двухстороннее освещение, нужно инвертировать нормаль поверхности и выполнить те же самые вычисления, что и в предыдущем разделе, но только со значениями задней поверхности материала. Возможно, при необходимости читатель выполнит такие вычисления более оптимально, а сама идея вычислений содержится в листинге 9.13. Листинг 9.13. Вычисление двухстороннего освещения normal - -normal: /7 Инициализация переменных для накопления интенсивности освещения .amb = vec4 (0.0); diff = vec4 (0.0): spec - vec4 (0.0); // Цикл по всей источникам, вычисление компонентов освещения от каждого for (1 - 0; 1 < NumEnabledLights: i++) if (glLightSourcefil.position.w == .0.0) Di rectional LighW . normal, amb. diff. spec); else if (gl_LightSource[1J.spotCutoff == 180.0) PolntLlghtd. eye. ecPosition3. normal, amb. diff. spec): else SpotLIghtd. eye. ecPosition3. normal, amb. diff. spec): } color = gl_BackLightModelProduct.sceneColor + amb * gl_BackMaterial.ambient + diff * gl BaCkMaterial .diffuse; if (SeparateSpecular) glBackSecondaryColor = vec4 (spec *gl_BackMaterial .specular. 1.0); else color +- spec * gl_BackMaterial.specular; gl_BackColor = color; Нет необходимости ограничивать значения gl_ВаckSeconda ryCol or и gl _BackCol or, потому что это выполняется автоматически. .5. Отсутствие освещения Если ни один источник освещения не включен, можно просто передать первич- ный и вторичный цвета вершины дальше (листинг 9.14). Листинг 9.14. Установка значений цвета без освещения if (SecondaryColor) gl_FrontSecondaryColor = gl_SecondaryColor; // Для gl_FrontColor ограничение будет выполнено автоматически gl_FrontColor “ gl_Color:
202 Глава 9. Традиционные шейдеры 9.6. Эффект дымки В OpenGL эффекты глубины и дымки (туман, а в большинстве изображений про- сто ухудшение видимости с удалением предметов от точки обзора) контролиру- ются параметрами дымки. Коэффициент дымки вычисляется с помощью одного из трех уравнений, а в дальнейшем используется при вычислении плавного пере- хода между цветом дымки и цветом фрагмента. Значение глубины, которое ис- пользуется в выражении для дымки, может быть либо координатой дымки, пе- реданной как стандартный атрибут вершины (gl_FogCoord), либо расстоянием от точки обзора до фрагмента изображения в пространстве координат обзора. Во втором случае обычно достаточно просто аппроксимировать значение глуби- ны, используя абсолютное значение координаты z в пространстве обзора (то есть abs(ecPosition.z)). При очень большом угле обзора такая аппроксимация может спровоцировать заметный дефект изображения (небольшую дымку у края). В этом случае можно вычислять г как расстояние от точки обзора до фрагмента (1 ength(ecPosi 11 on)). (Этот метод предполагает вычисление квадратного корня, что немного ухудшит производительность.) Выбрать способ вычисления в вер- шинном шейдере можно так: if (UseFogCoordinate) gl_FogFragCoord = gl_FogCoord: else gl_FogFragCoord - abs(ecPosition.z); Линейные вычисления, соответствующие обычным вычислениям глубины, можно выбрать в OpenGL с помощью константы GL_LINEAR. В этом случае коэф- фициент дымки/вычисляется следующим уравнением: $ _ end - z end - start; где start, end иг — расстояния в координатах пространства обзора: start — рассто- яние до начала дымки; end — расстояние до окончания действия эффекта; z — зна- чение из gl _FogFragCoord. Начальную и конечную координаты можно задавать явно в uniform-переменных или использовать в качестве них текущие значения из со- стояния OpenGL (встроенные переменныеgl_Fog .start и gl_Fog.end). Код шейде- ра для вычисления коэффициента дымки с помощью встроенных переменных показан в листинге 9.15. Листинг 9.15. Вычисления дымки по GL_LINEAR fog = (gl_Fog.end - gl_FogFragCoord)) * gl_Fog.scale; Так как 1.0 / (gl_Fog.end - gl_Fog.start) не зависит от состояния вершины или фрагмента, это значение можно вычислить заранее и сделать доступным в виде встроенной переменной gl_Fog. sea 1 е. Более реалистичного эффекта дымки можно достичь с помощью экспонен- циальной функции. Используя отрицательное значение экспоненты, экспонен- циальная функция моделирует убывание изначального цвета в зависимости от расстояния. Простая экспоненциальная функция дымки выбирается в OpenGL с помощью константы GL_EXP. Этой функции соответствует формула:
9.6. Эффект дымки 203 f = . Здесь density — это плотность дымки, a г вычисляется так, как описано в пре- дыдущей функции, density можно представить в виде uniform-переменной или встроенной переменной gi_Fog.density. Чем больше это значение, тем насыщен- нее дымка. Чтобы эта функция работала правильно, density должно быть больше или равно 0. Язык шейдеров OpenGL не имеет встроенной функции expfbase е), но в нем есть экспоненциальная функция с основанием 2, ехр2. Учитывая, что ехр(.т) = ехр 2{х/ lpg(2)), можно получить нужный результат. Выражение 1 /log(2) равно 1,442695. Код шейдера OpenGL для вычисления приведенного уравнения показан в листинге 9.16. Листинг 9.16. Вычисление дымки по GL_EXP const float L0G2E = 1.442695: // = 1 / log(2) fog • exp2(-gl_Fog.density * gl_FogFragCoord * L0G2E): Этот код иллюстрирует формулу, определенную для стандартной функцио- нальности OpenGL 1.5. Приняв решение об использовании этой формулы для вычисления дымки, разработчик может усовершенствовать код — например, ум- ножить константу L0G2E на -gl_Feg.density в приложении и передать результат в определенной разработчиком uniform-переменной. Этим достигается экономия на операции умножения в приведенном ранее выражении. Окончательная функция дымки, определенная OpenGL, выбирается с помо- щью константы GL_EXP2: У _ е-(Ал.ч7уг)2 . Эта функция меняет крутизну экспоненциального затухания функции возве- дением экспоненты в степень 2. Код шейдера OpenGL, реализующий это выраже- ние, очень похож на предыдущую функцию (листинг 9.17). Листинг 9.17. Вычисление дымки по GL_EXP2 const float L0G2E = 1.442695: // = 1 / log(.2) fog = exp2(-gl_Fog.density * gl_Fog.density * gl_FogFragCoord * glFocFragCoord * L0G2E); Производительность этого кода можно улучшить, передавая определенную раз- работчиком uniform-переменную, которая содержит значение gl_Fog.density * gl_Fog.density * L0G2E. OpenGL требует также, чтобы окончательное значение ко- эффициента дымки было ограничено диапазоном [0,1]. Это можно сделать так, как показано в листинге 9.18. Листинг 9.18. Ограничение коэффициента дымки fog = clamp(fog. 0.0. 1.0): Любую из этих трех функций дымки можно вычислить либо в вершинном, либо во фрагментном шейдере — когда в изображении нет очень больших многоугольни- ков, разница вряд ли будет заметна, если коэффициент дымки будет вычисляться в вершинном шейдере и передаваться во фрагментный шейдер как varying-пере- менная. Это может повлиять на производительность кода в целом. Во фрагментном
204 Глава 9. Традиционные шейдеры шейдере, когда окончательный цвет почти вычислен, коэффициент дымки можно использовать для вычисления перехода между цветом дымки и окончательным цветом фрагмента. Код шейдера (листинг 9.19) использует значение цвета дымки из состояния OpenGL. Листинг 9.19. Наложение дымки на окончательное значение цвета color - mix(vec3 (gl_Fog.color). color, fog); Код, представленный в этом разделе, можно использовать для вычислении, результат которых будет тем же, что и полученный стандартными вычислениями OpenGL. Но, учитывая возможность программируемости, разработчик может вычислять дымку совершенно иначе. 9.7. Формирование текстурных координат OpenGL можно настроить для автоматического вычисления текстурных коорди- нат на основании только входных координат вершин. Существует пять способов такой настройки, и каждый из них имеет свои преимущества в определенных ус- ловиях. Режим GL_OBJECT_LINEAR применяется в случаях, когда текстура будет неподвижной на геометрической модели, например, при моделировании местно- сти. GL_EYE_LINEAR применяется для выполнения динамических контурных линий объекта. С ее помощью, например, создаются топографические карты или интер- претируются сейсмические данные. GL_SPHERE_MAP можно использовать для тек- стурных координат, применяемых для простого отображения. GL_REFLECTIONJW и GL_NORMAL_MAP можно использовать для кубических текстур: GL_REFLECTION_MAP пе- редает вектор отражения как координаты текстуры, GL_NORMAL_MAP просто переда- ет вычисленную нормаль в системе координат обзора как текстурную координату. Функция, которая будет формировать коорди наты GL_SPHERE_MAP в соответствии со спецификацией OpenGL, приведена в листинге 9.20. Листинг 9.20. Вычисление GL_SPHERE_MAP vec2 SphereMapdn vec3 ecPositionS. in vec3 normal) { float m: vec3 r. u: u “ normalize(ecPosit!on3): r = reflect(u. normal): m = 2.0 * sqrttr.x * r.x + r.y * r.y + (r.z + 1.0) * (r.z + 1.0)): return vec2 (r.x / m + 0.5. r.y / m + 0.5): Функция для GL_REFLECTIONJW выглядит почти так же, как предыдущая, за исключением того, что возвращает вектор отражения (листинг 9.21). Листинг 9.21. Вычисление GL_REFLECTION_MAP vec3 Reflect!onMap(1n vec3 ecPosition3. in vec3 normal) { float NdotU. m: vec3 u:
9,7. Формирование текстурных координат 205 и = normalTze(ecPos1tion3); return (reflecttu. normal)]: } Выбор нужного метода формирования текстурных координат и вычисление окончательных значений показаны в листинге 9.22. Листинг 9.22. Вычисление текстурных координат // Вычислить координаты сферического отображения при необходимости if (TexGenSphere) sphereMap = SphereMaptecpositionS. normal): И Вычислить координаты отражения при необходимости If (TexGenReflectlon) reflection = ReflectionMap(ecposition3. normal): /./ Вычислить текстурные координаты для каждого активного текстурного модуля for (1 = 0; 1 < NumEnabledTextureUnits; i++) if (TexGenObject) gl TexCoordCi].s = dot(gl_Vertex, gl JDbjectPlaneSEil); gl_TexCoord[i].t = ddttgljiertex. gl_ObjectPlaneT[f]); gl_TexCoord[i].p = dot(gl_Vertex, gl_ObjectPlaneR[i]); gl_TexCoordE1].q = dot(gl_Vertex. gl_ObjectPlaneQLi]): } if (TexGenEye) { gl_TexCoorcf[i].s = dottecPosition. gl_TexCoordEi].t = dottecPosition, gl_TexCoord[i].p = dottecPosition. gl_TexCoord[i].q = dottecPosition. } gl_EyePlaneS[i]): gl_EyePlaneT[i]): gl_EyePlaneR[i]); glEyePlaneQ[i]); if (TexGenSphere) gl_TexCoord[1] = vec4(sphereMap. 0.0, 1.0); if (TexGenReflectlon) gl_TeKCoord[i] = vec4(reflection. 1.0): if (TexGenNormal) gl_TexCoord[i] = vec4(normal. 1.0); Предполагается, что каждый текстурный модуль этого кода имеет номер мень- ший, чем NumEnaN edTextureUnits. Если это значение равно 0, цикл по модулям бу- дет пропущен. В противном случае все необходимые текстурные координаты бу- дут вычислены внутри цикла. Так как вычисления сферического отображения и отражения не зависят от состояния заданного текстурного модуля, их можно выполнять отдельно, а резуль- тат будет использоваться для всех текстурных модулей. Для способов, реализуе- мых с использованием GL_OBJECT_LINEAR и GL_EYE_LINEA.R, существует уравнение плос- кости для каждого компонента каждого набора текстурных координат. В первом примере компоненты gl_TexCoord[0] формируются умножением коэффициентов
206 Глава 9. Традиционные шейдеры уравнения плоскости для указанного компонента на входные вершинные коор- динаты, Во втором примере компоненты gl_TexCoord[0] вычисляются умножени- ем коэффициентов уравнения плоскости на координаты вершины в пространстве координат обзора. В зависимости от типа обращения к текстуре на этапе обработ- ки фрагментов вычислять £, у?1 и q не обязательно. 9.8. Произвольное отсечение В OpenGL, чтобы применять произвольное отсечение (оно остается стандартной функциональностью между вершинным и фрагментным шейдерами), вершинный шейдер должен преобразовать входные вершинные координаты в то же самое про- странство координат, в котором определены плоскости отсечения. Обычно они определены в пространстве обзора, и код шейдера, приведенный в листинге 9.23, используется для преобразования координат вершин. Листинг 9.23. Вычисление произвольного отсечения gl_ClIpVertex = gl_ModelViewMatrix * glj/ertex: 9.9. Наложение текстуры Чтение значений из текстурной памяти выполняется с помощью встроенных тек- стурных функций. Эти значения нужны для разных целей. Стандартная функци- ональность OpenGL включает поддержку формул для наложения текстур, опре- деляемых символическими константами GL_REPLACE, GL_MODULATE, Gl. DECAL, GLJ3LEND и GL_ADD. Эти режимы работают по отдельности, в зависимости от формата тексту- ры, к которой обращаются. Следующий код — иллюстрация случая обращения к текстуре RGBA с помощью семплера texO. Переменная col or инициализирована в gl_Col or и изменена таким образом, что содержит значение цвета после наложе- ния текстуры. GL_REPLACE — самый простой режим наложения текстуры. При этом происходит простая замена цвета текущего фрагмента цветом из текстуры (лис- тинг 9.24). Листинг 9.24. Вычисление GL^REPLACE color = texture2D(texO. gljrexCoard[0].xy); В режиме GL_MODULATE входной цвет фрагмента умножается на значение, про- читанное из текстурной памяти. Этот режим применяется при вычислении осве- щения перед наложением текстуры (то есть вершинный шейдер вычисляет осве- щение, а фрагментный шейдер выполняет текстурирование). Белый цвет объекта здесь будет основным, а текстура задает рассеянный цвет (листинг 9.25). 1 Исторически сложилось так, что компоненты текстурных координат в OpenGL называются ,v, t, г и q. В языке шейдеров OpenGL они тоже называются одной буквой (это требование): s, с, р и q. Буква г нс используется — опа оставлена для обозначения цвета в наборе компонентов цвета г, g, b и а.
9.10. Итоги 207 Листинг 9.25. Вычисление GL_MODULATE color *= texture2D(texO. gl_TexCoord[0].ху): Режим GL_DECAL используется для наложения непрозрачного изображения на часть объекта. Например, нужно наложить на гоночную машину ряд логотипов фирмы-спонсора или татуировки — на кожу персонажей игры. При доступе к RGBA-текстуре значение прозрачности каждого элемента текстуры применяется для линейной интерполяции между входными значениями RGB и такими же зна- чениями текстуры. Значение прозрачности остается без изменений. Применение .этого режима иллюстрируется листингом 9.26. Листинг 9.26. Вычисление GL..DECAL vec4 texture - texture2D(texO. gl_TexCoord[0].xy): vec3 col = nrix(color.rgjt). texture.rgb. texture.a): color - vec4 (col. color.a): Режим GLJ3LEND — единственный, который принимает во внимание цвет среды текущей текстуры. Значения RGB, прочитанные из текстуры, используются для линейной интерполяции между RGB-значениями входного фрагмента и цветом среды текстуры. Новое значение прозрачности вычисляется умножением преды- дущего значения прозрачности входного фрагмента на значение прозрачности текстуры. Код шейдера, иллюстрирующий этот метод, приведен в листинге 9.27. Листинг 9.27. Вычисление GL_BLEND vec4 texture = texture2D(texO. gl_TexCoord[01.xy): vec3 col = mlxtcolor. rgb. g1_TextureEnvCoJor[0L rgb. texture.rgb); color = vec4 (col. color.a * texture.a): В режиме GI.ADD вычисляется сумма цвета входящего фрагмента и значения, прочитанного из текстуры. Значения прозрачности перемножаются. Это един- ственный традиционный режим, в котором вычисленные значения могут выхо- дить за пределы диапазона [0,1], так что результат нужно привести к этому диа- пазону (листинг 9.28). Листинг 9.28. Вычисление GL_ADD vec4 texture = texture2D(texO. gl_JexCoord[0].xy); col or.rgb +- texture.rgb; col or.a *= texture.a; color = clamp (color. 0.0. 1.0); Объединенный режим, добавленный в OpenGL 1.3 и расширенный в OpenGL 1.4, определяет ряд новых простых способов наложения текстуры. Для него существует множество новых формул, исходных значений и операндов. Отображение этих дополнительных режимов в коде шейдера OpenGL в этой книге не приводится: это утомительно и не очень интересно. 9.10. Итоги Формулы для рендеринга, представленные OpenGL, реализованы как стандарт- ная функциональность в аппаратном обеспечении, но не обязательно они будут
208 Глава 9. Традиционные шейдеры лучшими для шейдеров. Однако все равно было полезно посмотреть, как эти фор- мулы выглядят в шейдерах, написанных на языке шейдеров OpenGL. Примеры шейдеров, представленные в этой главе, отображают стандартную функциональ- ность и формулы, но не следует считать эти способы оптимальными. Разработчи- кам следует лишь учитывать идеи, приведенные в примерах, и адаптировать их для собственных нужд. 9.11. Ссылки Работающие шейдеры с кодом, описанным в этой главе, можно загрузить с веб- сайта этой книги: http://www.3dshaders.conn. Книга [5] содержит более подробные описания формул, приведенных в этой главе. Функциональность определяется спецификацией OpenGL [7]. Основные принципы машинной графики, такие как преобразование координат, вычисление освещения, эффекты дымки и текстурирования, описаны в книгах [2 и 3]. 1. 3Dlabs. Веб-сайт для разработчиков (http://www.3dLabs.com/support/deveLoper). 2. Akenine-МцПег Т., Haines Е. Real-Time Rendering. 2nd ed. Natick, MS: A К Peters, Ltd., 2002 (http://www.reaLtimerendering.com). 3. Introduction to Computer Graphics/J. D. Foley, A. van Dam, S. K. Feiner, at al. Reading, MS: Addison-Wesley, 1994. 4. Computer Graphics: Principles and Practice. 2nd ed./J. D. Foley, A. van Dam, S. K. Feiner, J. H. Hughes. Reading, MS: Addison-Wesley, 1996. 5. Neider J., Davis T., Woo M. OpenGL Programming Guide. 3rd ed. Reading, MS: Addison-Wesley, 1999. 6. OpenGL Reference Manual: The Official Document to OpenGL, Version 1.2.3rd ed. Reading, MS: Addison-Wesley, 1999. 7. Segal M., Akeley K. The OpenGL Graphics System: A Specification (Version 1.5)/ Ed.: Ch. Frazier (v, 1.1), J. Leech (v. 1.2-1.5). 2003 (http://opengL.org).
F Шейдеры ! с сохранением данных в текстурах Работа с текстурами — очень мощный механизм, встроенный в OpenGL. Когда OpenGL только формировался (1992 г.), аппаратное обеспечение для работы стек- етурами начинало появляться в коммерческих продуктах. В настоящее время тек- стурное отображение доступно во всех графических акселераторах, даже самых Простых. В версии OpenGL 1.0 текстурное отображение было определено с узкими огра- ничениями. Это просто был способ наложить изображение на поверхность объекта. G тех пор аппаратное обеспечение стало гораздо более совершенным, и было при- думано много способов использования текстур. Определение текстуры в OpenGL также было расширено. Текстурные объекты — одно из ключевых дополнений к OpenGL 1.1. Трехмерные текстуры стали частью стандарта в OpenGL 1.2. Воз- можность аппаратного обеспечения обращаться к двум и более текстурам од- новременно проявились в OpenGL 1.3 вместе с кубическими текстурами и воз- можностью поддержки форматов сжатия текстур. В OpenGL 1.4 была добавлена поддержка текстур глубины и тени, автоматическое множественное отображение текстур и другие возможности (например, зеркальное повторение). Краткий об- зор текстур, используемых в OpenGL, читатель может найти в разделе 1.10. Программируемость, появившаяся с созданием языка шейдеров OpenGL, по- зволяет пользоваться текстурами более широко. Используя программируемые шейдеры, приложение может считывать значения с любого количества текстур к применять их как угодно. Можно работать по сложным алгоритмам, в которых результаты обращения к одной текстуре используются для определения парамет- ров доступа к другой текстуре. Также можно хранить в текстурах промежуточные результаты рендеринга, использовать их как таблицы для поиска (представление сложных функций), для хранения нормалей, их отклонений, значений блеска, ин- формации о видимости, полиномиальных коэффициентов и многих других дан- ных. Делать это, конечно, не так легко, как работать с текстурами обычным спосо- бом, и такая гибкость обозначает также, что текстурные карты в некотором смысле уже являются памятью общего назначения, в которой можно хранить что угодно. (Фильтрация и наложение все еще отличают доступ к текстурной карте от доступа к обычной памяти.) В этой главе описано несколько шейдеров, которые читают значения из тек- стурной памяти и используют их для формирования интересных эффектов. Нач- нем с разговора о том, как обратиться к текстуре из шейдера, а потом рассмотрим несколько примеров шейдеров, которые обращаются к текстурной памяти с целью не только наложения текстуры на трехмерный объект.
210 Глава 10. Шейдеры с сохранением данных в текстурах 10.1. Обращение к текстуре из шейдера Приложения должны правильно инициализировать текстуры перед выполнением шейдера, который работает с ними. Для инициализации текстуры приложение дол- жно выполнить следующие действия. 1. Выбрать текстурный модуль и сделать его активным функцией gl Act i veTexture. 2. Создать текстурный объект и связать его с активным текстурным модулем функ- цией glBlndTexture. 3. Установить различные параметры (наложение, фильтрация и т. д.) текстурно- го объекта функцией glTexParameter. 4. Определить текстуру функцией gl Teximage. При использовании стандартной функциональности OpenGL необходимы еще два дополнительных шага: разблокирование нужной текстуры, входящей в тек- стурный модуль, функцией gl Ena bl е и установка текстурной функции для текстур- ного модуля (модулирование, шаблон, замена и т. д.) функцией glTexEnv. (Эти шаги не нужны для шейдера, так как блокирование/разблокирование текстур игнори- руется, а тфсстурная функция содержится в самом коде шейдера.) Когда эти шаги выполнены, текстуру можно использовать в OpenGL-шейДере. К текстурам можно обращаться из шейдера сразу же, как только состояние тек- стуры задано приложением. Для этого у языка шейдеров OpenGL есть встроенные типы данных (см. раздел 3.2.4) и встроенные функции (см. раздел 5.7). Uniform-переменная типа sampler используется для обращения к текстуре из шейдера. Как структурная единица шейдера sampler считается закрытым типом данных, содержащим значение для обращения к конкретной текстуре. Шейдер должен объявить такую uniform-переменную для каждой текстуры, которая ему нужна. Приложение должно задать семплеру значение перед выполнением шей- дера, как описано в разделе 7.8. Тип семплера показывает тип соответствующей текстуры. Переменная типа samplerlD используется для доступа к ID-текстуре, переменная типа sampl er2D — для доступа к 2П-текстуре, переменная типа sampl er3D — для доступа к ЗО-тексту- ре, переменная типа sampl erCube — для доступа к кубической текстуре, а перемен- ные типов samplerShadowlD и samplerShadow2D — для доступа к ID- и 2В-текстурам глубины. Например, если приложение предполагает использовать текстурный модуль 4, чтобы хранить 2В-текстуру, шейдер, должен объявить uniform-перемен- ную типа sampler2D, а приложение должно установить значение 4 в эту перемен- ную перед выполнением шейдера. Встроенные функции texturelD, texture2D, textureSD, textureCube, shadowlD и др. обращаются к текстуре из шейдера. Первый аргумент в каждой из этих встроен- ных функций — семплер, и тип семплера должен совпадать с именем функции. Например, семплер.типа samplerlD должен быть первым аргументом функции texturelD, семплер THna.sainpler2D — первым аргументом функции texture2D и т. д. Несовпадения приведут к ошибкам компилятора. Каждая из этих встроенных функций доступа к текстурам также получает в ка- честве аргумента текстурные координаты, которые затем используются аппарат- ным обеспечением для того, чтобы определить, к какой части текстурной карты происходит обращение. Аргумент для 2В-текстуры будет типа vec2, для ЗВ-тек-
10.2. Простой пример текстурирования 211 Стуры — типа vec3. Существуют также проекционные версии функций доступа к текстурам. В этих функциях компоненты текстурных координат делятся на послед- ит компонент, а результат используется при обращении к текстуре. Существуют различия между доступом к текстуре из вершинного и из фраг- 1/ментного шейдеров (язык шейдеров OpenGL позволяет делать и то И другое). Уровень детализации при доступе к множественной текстуре вычисляется стан- дартной функциональностью между вершинным и фрагментным процессорами. Поэтому это значение известно фрагментному процессору, но неизвестно вершинно- му. По этой причине в языке шейдеров OpenGL определены специальные встроен- ные функции, которые можно вызывать только из вершинного шейдера. Эти функ- ции позволяют получить уровень детализации текстуры и подставить его в качестве аргумента. Язык шейдеров OpenGL содержит также встроенные функции, предна- значенные только для фрагментного шейдера, которые принимают значение сдвига уровня детализации текстуры, и это значение добавляется к автоматически вычи- сляемому уровню детализации. Таким образом, автор шейдера может сделать изоб- ражение более резким или размытым. Если эти функции применяются к текстуре, не являющейся множественной, значение уровня детализации игнорируется. Встроенные функции доступа к кубическим текстурам (textureCube и texture- CubeLod) работают так, как это определено для стандартной функциональности. Переданные текстурные координаты интерпретируются как вектор направления, выходящий из центра куба. Это значение используется для выбора одной из двух- мерных текстур куба на основе координат с наибольшим модулем. Другие две коор- динаты делятся на абсолютное значение этой координаты, затем масштабируются и смещаются для вычисления двухмерной координаты, которая и будет использо- ваться для применения кубической текстуры к выбранной грани. Встроенные функции для доступа к текстурам глубины (shadowlD, shadow2D и др.) тоже работают так, как это определено для стандартной функциональности. Фор- мат текстуры, к которой обращаются, должен быть GL_DEPTH_COMPONENT. Возвращаемое значение зависит от режима сравнения текстуры, функции сравнения и режима глубины. Каждое из этих значений можно задать с помощью функции glTexPararreter. Встроенные функции доступа к текстуре работают в соответствии с текущим состоянием заданного текстурного модуля и с параметрами текстурного объекта, присоединенного к этому текстурному модулю. Другими словами, при определе- нии возвращаемого значения встроенной функции учитываются состояния тек- стурного модуля и текстурного объекта, в том числе режим переноса, уменьшение и увеличение, цвет рамки, минимальный/максимальный уровень детализации, режим сравнения текстур и т. д. 0.2. Простой пример текстурирования Используя встроенные функции для доступа к текстурам, легко написать простой шейдер, который будет выполнять наложение текстуры. Чтобы достичь результатов, начнем с очень хороших текстур. На цветном рис. 2 показан пример двухмерной текстурной карты — цилиндрическая проекция по- верхности земного шара с облаками. Это изображение, как и другие, о которых говорится в этом разделе, взято с веб-сайта NASA1 и создано Рето Стекли из центра
212 Глава 10. Шейдеры с сохранением данных в текстурах космических летательных аппаратов (NASA/Goddard Space Flight Center). Эти изо- бражения Земли — часть постепенно обрабатываемой последовательности данных (суша, морской лед и облака) от спутника-наблюдателя NASA, спектрорадиометра со средней разрешающей способностью. Данные от этого спутника совмещаются с другими изображениями (топографические карты, земной покров и огни горо- дов), все с разрешением до 1 км. Результат — изображение исключительно высо- кого разрешения (43 200 х 21 600 пикселов). Для наших целей хватит и менее подробной текстуры, так что мы будем использовать версии изображения с умень- шенным разрешением: 2048 х 1024. 10.2.1 . Настройка приложения Предположим, что изображение уже занесено в наше приложение и его ширина, высота, указатель на изображение и имя текстуры можно передать в функцию ини- циализации текстуры: 1nit2DTexture( GLInt texName. GLInt texWidth. GLInt texHeight. GLubyte *texPtr) { glB1ndTexture(GL_TEXTURE_2D. texName); glTexParameteri(GL_TEXTURE_2D. GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D. GL_TEXTURE WRAP_T. GL_REPEAT): glTexParameteri(GL_TEXTURE_2D. GL_TEXTURE_MAG_FILTER. GL_LINEAR); glTexParameteri (GLJEXTURE_2D. GL_TEXTURE_HIN_FILTER. GLJ.INEAR): glTexImage2Q(GL_TEXTUREJ?D. 0. GL_RGB. texWidth, texHeight. 0. GL_RGB. GL_UNSIGNED_BYTE. texPtr); } Этим создается текстурный объект с именем texName. Вызов функции glTex Parameter устанавливает режимы фильтрации и наложения. Выбираем повторение и линейную фильтрацию. Данные для текстуры задаются функцией gl TexImage2D (значения, передаваемые в нее, будут зависеть от того, как данные изображения сохраняются в памяти). После того как текстура готова к использованию, можно выполнить следующее: gl Acti veTextwe(GL_TEXTUREO); g 1ВI ncJText u re С GL_TEXTURE_2D. ea rthTexName): Эта последовательность вызовов сделает текстурный модуль 0 активным, при- соединит текстуру с изображением земли к этому модулю и активизирует ее. Нужно также задать значения двум uniform-переменным. Вершинному шейдеру необхо- димо знать координаты источника освещения, а фрагментному шейдеру — к како- му текстурному модулю нужно обратиться. Координаты источника освещения будут определяться как vec3 в вершинном шейдере под именем light Position, а текстурный модуль — как sampl егЗО во фрагментном шейдере под именем Earth Texture. Код приложения должен узнать, где находятся эти uniform-переменные, и задать им правильные значения. Предположим, что шейдеры уже скомпилирова- ны, скомпонованы в программном объекте programObj и установлены как часть теку- щего состояния. Для инициализации uniform-переменных выполним следующий код: UghtLoc = glGetUniformLocationARBfprogramObj. "LightPosition"): glUniTorrn3fARB(lightLoc, 0.0. 0.0. 4.0): 1 National Aeronautics and Space Administration (NASA) — государственная организация США, зани- мающаяся исследованием космоса. — Примеч. перев.
Г 10.2. Простой пример текстурирования 213 texLoc = glGetUn1fDrmLocat1onARB(programObq. "EarthTexture"): glUniformliAR3(texLoc, 0); Координаты источника освещения соответствуют точке, расположенной на линии । обзора перед объектом. Пусть для текстуры изображения земли используется тек- [ стурный модуль 0, тогда именно это значение нужно занести в переменную-семплер. Приложение может вызывать функции OpenGL для рисования сферы, потом на нее будет наложена текстура с изображением Земли. Для каждой вершины дол- жны быть заданы нормаль поверхности, двухмерные координаты текстуры и ко- ординаты вершины. 10.2.2 . Вершинный шейдер Вершинный шейдер, используемый в этом простом примере, очень похож на про- стой шейдер кирпичной стены (см. раздел 6.2). Главное отличие в том, что здесь текстурные координаты передаются как вершинные атрибуты, через встроенную varying-переменную gl_TexCoord[0] (листинг 10.1). Листинг 10.1. Вершинный шейдер для простого наложения текстуры varying float Lightintensity: uniform vec3 LightPosttion: const float Specular-Contribution = 0.1: const float diffuseContributipn = 1.0 - specularCantribution: void main(void) vec3 ecPosition = vec3 (gl JlodelViewMatrix * gl_Vertex); v.ec3 tnorm = normalizetgl^NomalMatrix * gl_Normal); vec3 lightVec = normalizeG-ightPosition - ecPosition).; vec3 reflectVec = reflect(-lightVec. tnorm): vec3 viewVec = normal Ize(-ecPositioii): float spec = clamptdot(reflectVec. viewVec). 0.0. 1.0): spec = powtspec. 16.0).: Lightintensity = diffuseContribution * max(dot(11ghtVec, tnorm). 0.0) + specularContribution * spec; gl_TexCoordl0] = gl JiultiTexCoordO: gl_Position = ftranSformQ: } 10.2.3 . Фрагментный шейдер Фрагментный шейдер (листинг 10.2) будет накладывать текстуру на входные геоме- трические фигуры. Например, если определена сфера, где текстурная координатам обозначает долготу (то есть 0° долготы — 5= О, 360° долготы — s = 1,0), а текстурная координата t обозначает широту (90° южной широты — t = 0,0, а 90° северной широ- ты — t = 1,0), можно наложить текстуру на сферу так, как показано на цветном рис. 3. В данном фрагментном шейдере входные текстурные координаты $ й t (компо- ненты встроенной varying-переменной g1_TexCoordO) используются для поиска значения в текстуре, связанной с текстурным модулем 0. Результат умножает- ся на значение интенсивности освещения, вычисленное вершинным шейдером
214 Глава 10. Шейдерыссохранением данных в текстурах и переданное в varying-переменной. Затем значение цвета приводится к опреде- ленному диапазону и учитывается значение прозрачности 1,0 для получения окон- чательного цвета фрагмента, отправляемого на завершающую обработку, включая проверку глубины. Полученное изображение сферы показано на цветном рис. 3. Листинг 10.2, Пример фрагментного шейдера для простого наложения текстуры varying float Lightintensity: uniform sampler2D EarthTexture; void main (void) { vec3 lightColor = vec3 (texture2D(EarthTexture. gl_TexCoord[0].st)): gl_FragColor = vec'- (lightColor * Lightintensity. 1.0): 1 10.3. Пример множественной текстуры Окончательное изображение выглядит неплохо, однако, приложив немного боль- ше усилий, можно его улучшить. Известно, что на нашей планете огромное коли- чество искусственных источников освещения, и в ночное время большие мегапо- лисы и города будут видны как пятна света даже из космоса. Используем угол между направлением света и нормалью в каждой точке поверхности и определим, на днев- ной или ночной стороне находится точка. Для точек, расположенных на дневной поверхности, будет использована «дневная» текстура (обычное изображение, без городского освещения), а для точек на ночной поверхности — «ночная» текстура (без освещения обычным источником освещения, но с учетом городского освеще- ния). Дневная и ночная текстуры показаны на цветном рис. 5. Другая не совсем реалистичная особенность — отсутствие отражения солнеч- ного света от поверхности океанов и больших озер. Вода очень хорошо отражает свет, и когда линия падения и отражения почти совпадают, мы должны видеть от- раженное освещение. Но у пустынь, травы и деревьев такого свойства нет, так что нужно подумать, как создать отражение от воды, но не от суши. Выйти из положения поможет применение способа, называемого карта отра- жений (gloss map). Это будет измененная исходная текстура, в которой области, представляющие воду, будут иметь значения 1,0, а все остальные — значения 0. Значение для каждого фрагмента будет считано из этой текстуры, а затем умноже- но на отражающую составляющую применяемого уравнения освещения. Такую карту очень просто создать почти в любом графическом редакторе. Самый про- стой способ это сделать — отредактировать красный канал дневного изображения без облаков. В этом канале все водные области будут черными или почти черны- ми, так как красного цвета в них очень мало. Можно применить специальный ин- струмент для выбора всех черных (вода) областей. После этого все выбранные об- ласти окрашиваются в белый цвет. Выборка инвертируется, и все остальные области заполняются черным цветом. В результате получится текстура, в которой значе- ние 1,0 (белый цвет) имеют отражающие области, а значение 0 (черный цвет) — не отражающие. Значение отражения будет множителем при вычислении отражения, так что области с водой будут иметь составляющую отражения, а остальные обла- сти — нет. Такая карта отражений представлена на рис. 10.1.
10.3. Пример множественной текстуры 215 Рис, 10.1. Карта отражений для создания эффекта отражения от водной поверхности Как видно на цветном рис. 5, текстуры для дневного и ночного времени больше не имеют покрытия облаками. Нужно сохранить облачное покрытие как отдель- ную текстуру, показанную на рис. 10.2. Это придаст дополнительную гибкость со- четанию изображения облаков с рисунками поверхности Земли. Для дневной по- верхности облака должны быть с рассеянным отражением, но без зеркального отражения. Более того, облака скрывают поверхность Земли, и значение 1,0 этой текстуры означает, что облака в этой точке полностью закрывают поверхность. Облака над ночной поверхностью ничего не отражают и не рассеивают, но все рав- но должны закрывать поверхность. Для удобства это одноканальное изображение сохраняется в красном канале RGB-текстуры, а карта отражений — в зеленом ка- нале. Синий канал остается неиспользованным. (Есть варианты — можно сохра- нять карту отражений как канал прозрачности в «дневной» текстуре, а облака — как канал прозрачности в «ночной» текстуре.) Рис. 10.2. Текстурная карта с изображением облачного покрытия (мраморно-голубое изображение от Рето Стекли)
216 Глава 10. Шейдеры с сохранением данных в текстурах 10,3.1. Подготовка приложения Подготовка приложения к множественным текстурам почти не отличается от про- стой подготовки, за исключением того факта, что теперь имеется три текстуры вместо одной. Можно три раза вызвать функцию jnl t2Dtextиге, описанную в раз- деле 10.2.1, по одному разу для каждой текстуры: дневной, ночной и с облаками и отражением. Сделать эти текстуры активными можно таким образом: glActi veTexture(GL_TEXTUREO); glBindTexture(GL_TEXTURE_2D. earthDayTexName): glActiveTexture(GL_TEXTUREl): glBindTexture(GL_TEXTURE_2D. earthNightTexName): glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D. earthCloudsTexName): Необходимые uniform-переменные инициализируются так: TightLoc - glGetUniformLocationARBCprogramOhj. "LightPosition"); glUniform3fARB(lightLoc. 0.0. 0.0. 4.0): texLoc = glGetUniformLocationARBCprogramObj, "EarthDay"); glUniforml1ARB(texLoc. 0); texLoc = glGetUnifonrLocationAREKprogramObj. "EarthNight"); glUniformllARBttexLoc. 1): texLoc = glGetUniformLocationARBCprogramObj. "EarthCloudGloss"): glUniformliARB(texLoc. 2): Теперь приложение может рисовать сферу. Для каждой вершины должны быть указаны нормаль поверхности, двухмерные координаты текстуры и координаты вершины. 10.3.2. Вершинный шейдер Вершинный шейдер для этого примера множественной текстуры очень похож на простой шейдер, описанный в разделе 10.2.2, за исключением того, что коэффици- енты рассеянного и зеркального отражения вычисляются вершинным шейдером и передаются в отдельных varying-переменных во фрагментный шейдер. Вычис- ленное значение отражения умножается на постоянный вектор (1,0, 0,941, 0,898), что приближает его к солнечному свету (листинг 10.3). Листинг 10.3. Вершинный шейдер для множественных текстур varying float Diffuse: varying vec3 Specular; varying vec2 TexCoord: uniform vec3 LightPosition: void main(void) vec3 ecPosition = vec3 (gl__Model Vi ewMatri x * gl_Vertex): vec3 tnorm = normalize(gl_NormalMatrix * .gi_Norntal).; vec3. lightVec = normal ize'CLightPositlon - ecPosition): vec3 reflectVec = reflect(-lightVec. tnorm); vec3 viewVec = normalize(-ecPosition): float spec = clamp(dot(reflectVec. viewVec). 0.0. 1.0):
,10.3. Пример множественной текстуры 217 spec = powlspec. 8.0): Specular = vec3 (spec) * vec3 (1.0. 0.941. 0.898) * 0.3; Diffuse = max(dot(lightVec. tnorm). 0.0); TexCcord = gl_MultiTexCoord0.st; gl_Position = ftransformO; 10.3.3. Фрагментный шейдер Фрагментный шейдер, который работает с множественными текстурами, приве- ден в листинге 10.4. Приложение загрузило «дневную» текстуру в текстурный мо- дуль с именем EarthDay, «ночную» текстуру — в текстурный модуль EarthNight, а тек- стуру с облаками и отражением — в текстурный модуль EarthCloudGloss. Вычисление освещения выполняется в вершинном шейдере, учитываются рассеянное и зер- кальное отражения, и результат передается во фрагментный шейдер. Текстурные координаты, передаваемые приложениям, также передаются во фрагментный шей- дер, и на основании этих координат происходит дальнейшая работа с текстурами. Во фрагментном шейдере сначала нужно обратиться к текстуре облаков/отраже- ния — эти значения пригодятся при дальнейших вычислениях. Потом считывается значение из «дневной» текстуры, умножается на коэффициент рассеянного освеще- ния и добавляется к коэффициенту отраженного освещения, умноженному на зна- чение из текстуры зеркального отражения. Если фрагмент не скрыт облаками, эти вычисления дают необходимый эффект рассеянного освещения по всей поверхно- сти Земли и эффект зеркального отражения от водной поверхности. Это значение затем умножается на 1,0 минус коэффициент облачности. И наконец, эффект об- лака получается умножением коэффициента облачности назначение рассеянного освещения, и результат добавляется к результату всех предыдущих вычислений. Вычисления для ночной стороны проще. Нужно просто прочитать значение из «ночной» текстуры и умножить результат на 1,0 минус коэффициент облачности. Так как данный фрагмент находится в тени, компоненты рассеянного и зеркального отражения отсутствуют. Затем можно вычислить значение для каждого фрагмента. Существует коэффициент рассеянного освещения, больший нуля в освещенных областях, меньший нуля в затененных областях, и около нуля возле терминатора. Используя условные выражения, значение daytime подставляется для освещенных областей, ni ghtti те — для неосвещенных, а возле терминатора задается постепенный переход. Затем для получения окончательного цвета фрагмента добавляется значение про- зрачности 1,0. Полученное изображение с нескольких точек обзора показано на цвет- ном рис. 6. На первом изображении рисунка можно увидеть замечательное зеркальное отражение Мексиканского залива; а если присмотреться к третьему (ночному) изоб- ражению, можно увидеть облака, скрывающие центральную часть восточного по- бережья Соединенных Штатов и северо-западную часть Бразилии. Этот шейдер не является универсальным, так как он учитывает тип геометри- ческой фигуры (сфера). Изображение, нарисованное с использованием данного шейдера, будет «правильным», только если сфера имеет правильные текстурные координаты. Его можно еще больше усовершенствовать, чтобы добиться большей реалистичности изображения. Цвет атмосферы может различаться в зависимости
218 Глава 10. Шейдеры с сохранением данных в текстурах от точки обзора и координат источника освещения (Солнце). Он более красный ближе к границе тени — тот цвет, который мы часто наблюдаем при восходах и за- катах. Читатель может обратиться по ссылкам в конце главы и узнать, как полу- чить более реалистичные эффекты, например рэлеевское рассеяние. Листинг 10.4. Фрагментный шейдер «как вращается мир» uniform sampler2D EarthDay: uniform sampler2D EarthNlght; uniform samplerZD EarthCloudGloss: varying float Diffuse: varying vec3 Specular: varying vec2 TexCoord: void main (void) { Н Монохромное значение облачного покрытия будет в clouds.г И Значение отражения от воды будет в clouds.g /7 clouds.b не будет использоваться vec3 clouds = texture2D(EarthCloudGloss. TexCoord).stp: vec3 daytime = (texture2D(EarthDay. TexCoord).stp * Diffuse + Specular * clouds.g) * (1.0 - clouds.r) + clouds.r * Diffuse: vec3 nighttime = texture2D(EarthNight. TexCoord).stp * (1.0 - clouds.r): vec3 color - daytime: if (Diffuse < -0.1) color - nighttime: if (abs(Diffuse) < 0.1) color = mixfnighttime. daytime. (Diffuse + 0.1) * 5.0): gl_FragColor = vec4 (color. 1.0): } 10.4. Пример наложения карты среды Способ моделирования отражения в сложной среде без использования трассиров- ки лучей называется наложение карты окружающей среды. Чтобы реализовать этот способ, нужпа одна или несколько текстурных карт, имитирующих отражение сре- ды вокруг объекта, над которым выполняется рендеринг. Лучше всего использо- вать этот способ на объектах, которые имеют хорошие зеркальные свойства поверх- ности. Существует несколько вариантов наложения карты среды, в том числе кубическое наложение и сферическое наложение, и оба они поддерживаются стан- дартом OpenGL. Если от нужного эффекта не требуется совершенство изображения, самый про- стой способ выполнить наложение карты среды — использовать простую текстурную карту из равных прямоугольников. Можно получить этот тип текстуры с помо- щью фотографий реальной окружающей среды или создать имитирующее изо- бражение. Пример показан на цветном рис. 7.
10.4. Пример наложения карты среды 219 Как бы ни было получено изображение, результатом является простая картин- ка, охватывающая 360° по горизонтали и 180° по вертикали. Изображение искрив- лено, чтобы после наложения на объект оно получилось пропорциональным. Основная идея наложения карты среды — в том, чтобы определить цвет отра- жения из «среды», хранимой в текстуре, используя вектор отражения от поверх- ности объекта. Если все сделать правильно, результат выглядит так, как будто объект блестит и отражает свое окружение. Для того чтобы использовать текстуру из равных прямоугольников как карту окружения, нужно вычислить пару углов, которые затем использовать для досту- пактекстуре. Угол возвышенности вычисляется определением угла между направ- лением отражения и плоскостью XZ. Значение угла возвышенности должно ока- заться в диапазоне от 180 (отражение прямо вверх) до -180° (отражение прямо вниз). Синус этого угла будет колебаться от 1,0 до -1,0, и его можно использовать, чтобы получить текстурную координату в диапазоне [0,1]. Угол азимута определяется проецированием направления отражения на плос- кость XZ. Значение этого угла будет колебаться от 0 до 360°, и по этому значению можно определить вторую текстурную координату в диапазоне [0, 1]. Следующие OpenGL-шейдеры будут совместно выполнять наложение карты среды на объект. Углы возвышенности и азимута вычисляются для определения значений s и t, которые, в свою очередь, используются для доступа к текстуре сре- ды. Предполагается, что у объекта есть дополнительный слой, который рассеивает часть освещения. Результат этого рассеивания сочетается с отражением окружаю- щей среды для получения окончательного значения каждого пиксела. 10.4.1. Подготовка приложения Для использования этого шейдера приложение должно сделать не очень мно- гое. Используется очень простая модель освещения, так что нужно передавать только координаты одного источника освещения. В данном примере карта ок- ружения будет занесена в текстурный модуль 4. Режим наложения для тексту- ры устанавливается и для $, и для t (это необходимо для небольшого трюка во фрагментном шейдере). Цвет слоя, который рассеивает освещение, для данно- го объекта определяется в baseCcl ос, а соотношение основного цвета к отраже- нию среды устанавливается в mi xRati о. Вот определения необходимых uniform- переменных. LightPos 0.0. 0.0. 4.0 BaseColor 0.4. 0.4. 1.0 MixRatio 0.8 EnvMap 4 После того как шейдеры, а также значения uniform-переменных установлены, приложение должно задать нормаль и координаты для каждой.вершины. Текущие значения матрицы модели-вида, матрицы проекции-модели-вида и матрицы нор- мали будут получены непосредственно из шейдера. 10.4.2. Вершинный шейдер В листинге 10.5 показан вершинный шейдер, который используется для наложе- ния карты среды.
220 Глава 10. Шейдеры с сохранением данных в текстурах Листинг 10.5. Вершинный шейдер для наложения карты среды varying vec3 Normal; varying vec3 EyeDir; varying float Lightintensity; uniform vec3 LightPos: void main(void) { gl_Position - ftransformO: Normal = normalizeCglJtormalMatrix * gl_Normal): vec4 pos = gl_Model ViewMatrix * gVVertex: EyeDir = pos.xyz; Lightlntensity = max(dot(nonualize(LightPos - EyeDir). Normal). 0.0): } Цель данного вершинного шейдера — получить три значения, которые будут интерполироваться для каждого примитива: значения рассеянного освещения, нормали поверхности и направление обзора. Два последних значения позволяют вычислить точное направление отражения для каждого фрагмента, а отсюда мож- но вычислить требуемые углы возвышенности и азимута. Преобразованные координаты вершины вычисляются в первой строке про- граммы обычным способом: преобразуется и нормализуется нормаль, затем с по- мощью текущей матрицы модели- вида и входных значений для вершины вы- числяется направление обзора. Эти два значения потом будут использоваться во фрагментном шейдере, чтобы определить вектор отражения. И наконец, вычис- ляется значение рассеянного освещения таким же способом, как и в предыдущих примерах. 10.4.3. Фрагментный шейдер В листинге 10.6 показан фрагментный шейдер, который используется для наложе- ния карты среды. Листинг 10.6. Фрагментный шейдер для наложения карты среды const vec3 Xunitvec = vec3 (1.0. 0.0. 0.0): const vec3 Yunitvec = vec3 (0.0. 1.0. 0.0): uniform vec3 BaseColor; uniform float MIxRatio: uniform sampler2D EnvMap: i! = 4 varying vec3 Normal; varying vec3 EyeDir: varying float Lightintensity; void main (void) { // Вычислить вектор отражения vec3 reflectDir = reflect(EyeD1r. Normal): // Вычислить углы возвышенности и азимута
Ю.4. Пример наложения карты среды 221 vec2 Index: index.у = dot (normalize (refl ectDir). Yunitvec); reflectDir.y =0,0: Index.x = dot(normalizetreflectDir). Xunitvec) * 0.5: // Привести значения index к правильному диапазону if (reflectDir.z >= 0.0) index = (index +1.0) * 0.5; else index.t - (index.t + 1.0) * 0.5: index.s ” (-index.s) * 0.Б + 1.0: // если reflectDir.z > 0.0. s будет находиться в диапазоне от 0.25 // до 0.75 // если reflectDir.z < 0.0. s будет находиться в диапазоне от 0.75 до 1.25. // и это нормально, так как установки текстуры позволяют это // Выполнить поиск в карте среды. vec3 envColor = vec3 (texture2D(EnvMap. index)): ll Добавить освещение в базовый цвет и смешать vec3 base = Lightintensity * BaseColor; envColor = mixtenvColor. base. MixRatio); glJreagColor = vec4 (envColor. 1.0); Varying-переменные Normal и EyeDi r — это значения, вычисленные вершинным шейдером и затем интерполированные в пределах примитива. Чтобы получить действительно точные результаты, нужно опять нормализовать значения во фраг- ментном шейдере. Исключение нормализации немного улучшит производитель- ность, но ухудшит качество; для некоторых объектов это приемлемо. Константы Xunitvec и Yunitvec установлены в значения, необходимые для вы- числения углов возвышения и азимута. Сначала нужно вычислить угол возвыше- ния путем нормализации вектора reflectionDi г и выполнения операции dot над Yunitvec. Так как оба вектора являются единичными, это дает значение косинуса для нужного угла, значение которого колеблется в пределах [-1,1]. Установка ком- понента у вектора отражения в 0 приведет к отображению этого вектора на плос- кость XZ. Нормализация нового вектора дает косинус угла азимута, значение ко- торого колеблется в пределах [-1,1]. Так как горизонтальное направление текстуры среды охватывает все 360°, умножаем это значение на 0,5, так что результат будет находиться в пределах половины карты среды. Теперь нужно определить, что же это за половина. Если компонент г направления отражения является положительным, направ- ление отражения — от передней стороны, и вычисленное значение будет исполь- зоваться непосредственно. Значения масштабируются и смещаются, так что при доступе к текстуре значения s будут находиться в диапазоне [0,25, 0,75], а значе- ния t — в диапазоне [0, 1 ].
222 Глава 10. Шейдеры с сохранением данных в текстурах Если компонент z отрицательный, расчеты будут несколько иные. Значение Г вычисляется таким же образом, как и в предыдущем случае, но значение 5 масшта- бируется и сдвигается так, что оказывается в диапазоне [0,75, 1,25]. Впоследствии эти значения можно будет использовать непосредственно, так как для текстуры установлен режим GL REPFAT. Значения 5, лежащие в промежутке от 1,0 до 1,25, бу- дут отображаться на s, находящиеся в промежутке от 0 до 0,25 в настоящей тексту- ре (трюк, упомянутый ранее). Таким образом, доступ ктекстуре в целом будет орга- низован правильно, в зависимости от направления отражения. Можно сравнивать s с 1,0 и, если его значение больше 1,0, вычитать из него 1,0, но для этого нужно . будет выполнять дополнительные операции, что повлияет на производительность. Используя трюк режима повтора, можно оставить эти вычисления для аппаратно- го обеспечения. Все, что нужно сделать с установленными значениями индекса, — найти нуж- ное значение в текстурной карте. Основное значение диффузно отраженного цве- та вычисляется умножением интенсивности освещения на BaseColor. Для созда- ния эффекта керамики это значение смешивается со значением на карте среды. Затем добавлением значения прозрачности 1,0 создается vec4 и окончательный цвет фрагмента передается для дальнейшей обработки. Окончательный результат по- казан на цветном рис. 8. На изображении видны ветки деревьев на спине и задней части трицератопса. В этом примере использовались цвет (0,4, 0,4, 1,0) (то есть светло-синий) и коэффициент смешивания 0,8 (то есть 80 % рассеянного цвета, 20 % значения из текстурной карты среды). Пример наложения карты среды на зеркальную поверхность и добавление про- цедурных «шишечек» показан па цветном рис. 9. 10.5. Полиномное отображение текстуры с BRDF-данными Чтобы моделировать более реалистичные поверхности, нужно выйти за пределы простой модели освещения/отражения, встроенной в OpenGL и предполагающей, что все поверхности имеют зеркальные свойства отражения. С некоторого време- ни исследователи в области компьютерной графики выполняют рендеринг изоб- ражений с помощью более реалистичной модели отражения, которая называется функция распределения двунаправленного отражения, или BRDF1. Эта модель для вычисления отражения от поверхности учитывает и направление падающего ос- вещения, и направление отраженного освещения. Углы возвышения и азимута этих векторов направления используются для вычисления относительного количества отраженного света (модель из стандартной функциональности OpenGL учитыва- ет только угол возвышения). Эта модель может использоваться для рендеринга поверхностей со свойством анизотропного отражения (поверхности не инвариант- ны в отражении света при вращении). Для измерения BRDF-реальных поверхно- стей были придуманы специальные инструменты. Измерения BRDF можно диск- ретизировать, чтобы создавать текстурные карты для воспроизведения функции 1 Bi-directional reflectance distribution function. —Примеч. перво.
Полиномное отображение текстуры с BRDF-данными 223 WDF во время выполнения программы. Разработано множество различных ме- тодов дискретизации и воспроизведения для рендеринга таких материалов. Этот раздел описывает BRDF-шейдеры на языке шейдеров OpenGL, которые используют метод полиномного отображения текстуры, разработанный в Hewlett- Packard. Шейдеры любезно предоставлены Брэдом Риттером из Hewlett-Packard, Данные BRDF предоставлены Корнелльским университетом. Там их получили, измеряя отражение от нескольких видов автомобильных покрытий, предоставлен- ных Ford Motor Со. Одна из причин, по которой этот вид рендеринга считается важным, — достиже- ние эффекта реалистичности материалов, характеристики отражения которых зави- сят от угла обзора и направления освещения, например автомобильных покрытий. Для конструктора автомобилей, несомненно, важно увидеть окончательное изоб- ражение автомобиля, даже если он окрашен краской, которая имеет разные харак- теристики отражения в зависимости от угла обзора и направления освещения. Один из образцов, испытанных в Корнеллъском университете, Mystique Lacquer, имел особенное свойство: цвет отраженного света менялся в зависимости от угла обзо- ра. Этот материал нельзя правильно воспроизвести стандартными методами. Текстуры, используемые в этом примере, называются полипомными картами текстур, или РТМ’. Вид этих текстур зависит от освещения. Описаны они Т. Маль- збевдером, Д. Гелбом и X. Уолтерсом в документе [6]. РТМ можно использовать для воспроизведения цвета поверхности при различных условиях освещения. Если рендеринг поверхности выполняется с помощью РТМ, в зависимости от направ- ления освещения получаются разные характеристики. Это помогает зрителям вос- принимать геометрию поверхности и повторяющиеся рельефные детали. РТМ могут помочь также при взаимном отражении близких поверхностей сложных объектов и отбрасывании тени на самого себя. РТМ создаются в результате иссле- дования реальных материалов и предназначены для представления их визуаль- ных характеристик. Полиномные карты текстур — это методика, основанная на изображениях, которые не имеют рельефных деталей или сложной геометрии. На цветном рис. 10 показаны два треугольника из демонстрационной програм- мы РТМ, разработанной компанией Hewlett-Packard. Рендеринг верхнего право- го треугольника был выполнен с помощью полиномной текстурной карты, а рен- деринг нижнего левого треугольника — с помощью альтернативной двухмерной текстурной карты. Для создания текстурной карты использованы металлическая панель с логотипом Hewlett-Packard и окрашенное металлическое покрытие с ре- льефным логотипом 3Dlabs. Альтернативная текстура выглядит плоско и не очень реалистично, в то время как РТМ-текстура точно воспроизводит отражающие свой- ства реальных объектов и затенение поверхностей, которое на них наблюдается. В демонстрационной программе источник света передвигается, и полученные эффек- ты хорошо видны. На приведенном здесь изображении источник освещения рас- положен немного впереди и вверху объекта. РТМ показывает реалистичные отра- жения, а альтернативная текстура может только воспроизводить эффект освещения под каким-то конкретным углом (как если бы источник света находился прямо перед объектом). 1 Polynomial texture maps. — Примеч. перев.
224 Глава 10. Шейдеры с сохранением данных в текстурах Разработанный HP метод РТМ для своей реализации требует набора входных изображений желаемого объекта, на каждом изображении данный объект должен, рассматриваться с одной и той же точки обзора и быть освещен под другим углом. Для каждого тексела РТМ эти исходные изображения дискретизируются и приво- дятся к кривой по методу наименьших квадратов. В результате получается поли- ном, который представляет собой функцию освещения для данного тексела. Эта часть работы — частично наука, а частично искусство (небольшое вмешательство в автоматический процесс может значительно улучшить конечный результат). Таким образом получается биквадратное уравнение, которое позволит воспроиз- вести функцию освещения для заданного материала. Коэффициенты, которые со- храняются в РТМ — А, В, С, D, EhF, — показаны в уравнении: Au2 + Bv1 + Cuv + Du + Ev + F. Одно из предполагаемых назначений РТМ — представление материалов с та- кими свойствами поверхности, различающимися на разных ее участках. Напри- мер, окрашенный металл, ткань, дерево и камень — это все материалы, по-разному отражающие свет в зависимости от угла обзора и направления освещения. Они также могут создавать отражение и отбрасывать тень на самих себя. С помощью метода РТМ можно запомнить эти подробности и затем воспроизвести их при ри- совании изображения. Существует два способа реализации этого метода: яркость (LRGB1) и RGB. LRGB РТМ использует биквадратные полиномы для определе- ния яркости каждого показываемого пиксела. Так как каждому текселу LRGB РТМ соответствует собственная биквадратная полиномиальная функция, его яркость будет уникальной. RGB РТМ использует отдельный биквадратный полином для каждого из трех цветов: красного, зеленого и синего, — так что объекты, закрашен- ные таким способом, будут менять цвет там, где освещение сдвигается. Материалы с такими цветовыми характеристиками и можно воспроизвести методом RGB РТМ. Для создания полиномной карты текстуры таких материалов нужно делать снимки этих материалов, освещенных под разными углами. Инженеры компании Hewlett-Packard разработали устройство — колпак с множеством источников ос- вещения и фотокамерой на самом верху. Это устройство (рис. 10.3) может автома- тически создавать до 50 снимков материала, освещенного разными источниками света. Данные об изображении, захваченные этим устройством, — основа для созда- ния РТМ настоящей текстуры материала (например, автомобильного покрытия). Эти виды РТМ имеют четыре степени свободы. Две из них используются для пред- ставления изменяемых в пространстве характеристик материала и контролируют- ся двухмерными текстурными координатами. Другие две степени свободы нужны для представления направления освещения. Это две независимые переменные биквадратного полинома. BRDF РТМ немного отличается от описанного метода. Она используется для моделирования однородных материалов, которые не изменяются в пространстве. BRDF РТМ использует две степени свободы для представления направления ос- вещения и еще две — для представления направления обзора. Параметризован- ное направление освещения (Lu, Lv) используется для независимых переменных L - luminance (яркость). — Примеч. перев.
10.5. Полиномное отображение текстуры с BRDF-данными 225 биквадратного полинома, а параметризованное направление обзора (Vu, Vv) — для координат двухмерной текстуры. Рис. 10.3. Устройство для захвата изображений материала для создания полиномных карт текстур (© Hewlett-Packard, 2003, воспроизведено с разрешения компании) С Не существует простой параметризации, которая хорошо подходит для всех I BRDF-материалов. Для дальнейшей детализации (улучшения качества) нужна I повторная параметризация векторов освещения и обзора как векторов половины [ угла и приращения (Ни, Но) и (Du, Dy). В шейдерах BRDF РТМ, обсуждаемых Р в следующем разделе, Ни и Но представлены в виде независимых переменных би- квадратного полинома, a Du и Dv — в виде координат двухмерной текстуры. В ос- новном в вершинном шейдере вычисляются (Ни, Но) и (Du, Dv). BRDF РТМ может быть создана как методом LRGB, так и методом RGB. При- мер, приведенный далее, демонстрирует рендеринг с использованием RGB BRDF I РТМ. В примере использованы 8-битные текстуры RGBA, так как формат файла 1 РТМ, разработанный компанией HP, основан именно на таком формате текстуры. 110.5.1. Подготовка приложения I Чтобы выполнить рендеринг BRDF-поверхностей с использованием приведенных R в примере шейдеров, приложение должно установить значения нескольких uniform- [ переменных. Вершинному шейдеру нужно передать значения, описывающие на- I правление обзора (бесконечно удаленный зритель) и координаты простого источпи- [ ка освещения (точечный источник освещения). Фрагментному шейдеру требуются | значения для масштабирования и смещения шести полиномных коэффициентов. [ (Эти значения уже были предварительно масштабированы при создании РТМ для [ сохранения точности, а теперь нужно их масштабировать снова с помощью коэф- I фициентов масштабирования и смещения для данного РТМ.) | Приложение должно передать для каждой вершины четыре атрибута. Два из г них — стандартные атрибуты OpenGL: g._Vertcx (координаты) и gl_Normal (нор- маль). Другие два — тангенциальный вектор и вектор бинормали, которые должны быть вычислены приложением. Нужно передавать их в OpenGL либо с помощью 8 Зак. 218
226 Глава 10. Шейдеры с сохранением данных в текстурах функции gl VertexAttribARB, либо через произвольный массив вершин. Адрес этих произвольных атрибутов можно присоединить к нужным атрибутам в вершинном шейдере с помощью функции gl Bi ndAttri bLocati onARB. Например, если выбрать для значения тангенса адрес атрибута 3, а для значений бинормали — адрес атрибута 4, то присоединение будет происходить таким способом: glBindAttribLocationARBCprogramObj. 3. "Tangent"): glBindAttribLocationARB(programObj. 4. "Binormal"): Если и переменная tangent, и переменная binormal определены как массивы из трех чисел с плавающей запятой, эти атрибуты можно передавать такими функци- ями: gl VertexAttrib3fvARB(3. tangent): glVertexAttrib3fvARB(4. binormal): или использовать произвольные массивы вершин. Перед рендерингом приложение должно установить семь текстурных карт: три двухмерные текстурные карты будут хранить коэффициенты А, В и С для красно- го, зеленого и синего компонентов РТМ, еще три — коэффициенты D, Е и F для красного, зеленого и синего компонентов РТМ, а одномерная текстура будет хра- нить функцию освещения. Эта последняя текстура устанавливается приложением всякий раз, когда со- стояние освещения меняется. Текстура коэффициента освещения решает четыре проблемы. 1. Распознавание передней и задней поверхностей. Эта текстура индексируется LdotN, значение положительное для вершин лицевой поверхности и отрицатель- ное для вершин обратной поверхности. На первом уровне сложности текстура коэффициента освещения может решить проблему лицевой/обратной поверх- ности хранением значения 1,0 для лицевой и 0,0 — для обратной поверхности. 2. Иногда желательно освещать объекты цветным светом. На втором уровне слож- ности текстура освещения (в которой есть три канала, R, G и В) будет хранить цвет освещения вместо значения 1,0 для лицевой поверхности, 3. Резкий переход с лицевой поверхности на обратную выглядит некрасиво и не очень реалистично. На третьем уровне сложности применяется постепенное из- менение значений текстуры от 0 до 1,0. Чтобы определить нужные значения, можно использовать кривую синуса или косинуса. 4. Для рендеринга с помощью РТМ не существует понятия общего освещения. Если выполнить рендеринг обратной поверхности со значениями (0, 0, 0), она может выглядеть очень нереалистично. Вместо того чтобы использовать значе- ния 0 для отрицательных индексов, лучше подставлять значения 0,1, 10.5.2. Вершинный шейдер Вершинный шейдер по методу BRDF РТМ приведен в листинге 10.7. Цель этого шейдера — вычислить значения пяти varying-переменных: □ gi. Position — требуется от каждого вершинного шейдера; □ TexCoord — будут использованы для обращения к текстуре, чтобы получить два набора коэффициентов полинома;
10.5. Полиномное отображение текстуры с BRDF-данными 227 Du — косинус угла между направлением освещения и тангенциальным векто- ром (число с плавающей запятой); □ Dv, — косинус угла между направлением освещения и вектором бинормали (чис- ло с плавающей запятой); LdotN — косинус угла между нормалью поверхности и направлением освеще- ния (число с плавающей запятой). Предполагается, что наблюдатель бесконечно удален, а источник освещения точечный и единственный. Листинг 10.7. Вершинный шейдер для рендеринга с помощью полиномных текстур // // Вершинный шейдер РТМ от Брэда Риттера. Hewlett-Packard. // и Рэнди Роста. SDlabs. // // 0 3D1abs. Inc. и Hewlett-Packard. L.P.. 2003. // опубликовано с разрешения компаний // uniform vec3 LightPos; uniform vec3 EyeDir; attribute vec3 Tangent: attribute vec3 Binormal: varying float Du; varying float Dv: t varying float LdotN: varying vec2 TexCoord: void main(void) { vec3 lightTemp: vec3 halfAngleTemp: vec3 tPrime: vec3 bPrime: // Преобразование вершины gl_Position = ftransformO: lightTemp = normalizetLightPos - gl_Vertex.xyz): // Вычисление вектора половины угла halfAngleTemp = normalizetEyeDir + lightTemp): // Вычисление Г и В' // Т' - |T - (Т.Н)Н| tPrime = Tangent - (halfAngleTemp * dot(Tangent, halfAngleTemp)): tPrime = normalize(tPrime): // B' - HxT' bPrime = crossdialfAngleTemp, tPrime); Du = dot(lightTemp. tPrime): Dv = dot(lightTemp. bPrime); // Умножение половинного угла на NOISE_FACTOR, И чтобы избежать помех в BRDF-данных л продолжение
228 Глава 10. Шейдеры с сохранением данных в текстурах Листинг 10.7 {продолжение) HalfAngleTemp = ha1fAngleTemp * 0.9; // Hu = DottHalfAngle. T) // Hv = DottHalfAngle. B) // Remap [-1.0..1.0] to [0.0..1.0] TexCoord.s = dotCTangent. halfAngleTemp) * 0.5 + 0.5; TexCoord.t = dotfBInormal. ha 1fAngleTemp) * 0.5 + 0.5; // "S" Text Coord3; Dot(Light. Normal); LdotN = dot(lightTemp. gljtormal) * 0.5 + 0.5; } Координаты источника освещения и направление обзора передаются в шейдер приложением как uniform-переменные. В дополнение к стандартным атрибутам вершины — координатам и нормали — приложение передает тангенс и бинормаль для каждой вершины, как описано в предыдущем разделе. Эти два дополнительных атрибута определены с соответствующими именами в данном вершинном шейдере. Первая строчка вершинного шейдера преобразует входные координаты с по- мощью проекционной матрицы модели-вида. Следующая строчка вычисляет на- правление освещения вычитанием координат вершины из координат источника освещения. Так как переменная LightPos — типа vec3, а встроенная переменная gl_Vertex — типа vec4, необходимо использовать компоненты . xyz, чтобы получить первые три элемента gl_Vertex перед вычитанием векторов. Результат вычитания векторов нормализуется и сохраняется как направление освещения. Следующая строчка кода вычисляет половинный угол следующим образом: вектор обзора и вектор освещения складываются и результат нормализуется. Следующие несколько строк кода выполняют двухмерную параметризацию по- ловинного угла и вектора приращения. Это нужно для вычисления значений для u (Du) и v (Dm), которые подставляются в биквадратное уравнение во фрагментном шей- дере (так называемый метод ортонормирования Грама—Шмидта). Здесь Н (поло- винный угол), Т' и В' — ортогональные оси системы координат, Т’ и В' поддержи- вают общее выравнивание с исходными векторами Т (тангенциальный! вектор) и В (вектор бинормали). В то время как Т и В лежат на плоскости треугольника, ренде- ринг которого выполняется, Т' и В' находятся в плоскости, перпендикулярной век- тору половинного угла. Обоснование для определения системы координат через Н, Г и В' содержится в документе «Interactive Rendering with Arbitrary BRDFs Using Separable Approximations» (авторы Ян Куц и Майкл МакКул, 1999 г.). BRDF-данные часто содержат шум и помехи, если инцидентные углы очень большие (почти 180°), так что в следующей строчке кода эта проблема устраняет- ся наложением коэффициента на половинный угол. Затем код данного вершинного шейдера вычисляет значения для Ни и Hv и по- мещает их в varying-переменпую TexCoord. Эти коэффициенты будут подставлены в биквадратное уравнение во фрагментном шейдере как значения и и v. Этими зна- чениями определяется параметризованный вектор приращения, который будет использоваться для поиска требуемых коэффициентов полинома из текстуры, так что значения приводятся к диапазону [0, 1]. И наконец, вычисляется окончательное значение освещения. Это значение — косинус угла между нормалью поверхности и направлением освещения, он тоже
10.5, Полиномное отображение текстуры с BRDF-данными 229 приводится к диапазону [0, 1], так как потом будет использоваться в качестве тек- стурной координаты для обращения к 1 D-тскстуре, чтобы получить коэффициент освещения. 10.5.3. Фрагментный шейдер Фрагментный шейдер, иллюстрирующий метод рендеринга BRDF РТМ, приве- ден в листинге 10.8. Листинг 10.8. Фрагментный шейдер для рендеринга с помощью полиномных текстур // // Фрагментный шейдер РТМ от Брэда Риттера. Hewlett-Packard. // и Рэнди Роста. 3Dlabs. II И ® 3Dlabs. Inc. и Hewlett-Packard. L.P.. 2003. // опубликовано с разрешения компаний // uniform sampler2D ABCred: // - 0 uni form sampler2D DEFred; И = 1 uniform sampler2D ABCgrn: // = 2 uniform sampler2D DEFgrn: // = 3 uniform sampler2D ABCblu: // - 4 uniform sampler2D DEFblu: // = 5 uniform samplerlD Lighttexture: // = 6 uniform vec3 ABCscale. ABCbias: uniform vec3 DEFscale. DEFbias: varying float Du: ll передает вычисленное значение L*tPrime varying float Dv; // передает вычисленное значение L*bPrime varying float LdotN: // передает вычисленное значение L*Normal varying vec2 TexCoord: // передает текстурные координаты s и t void main(void) { vec3 ABCcoef. DEFcoef: vec3. ptvec: // Чтение коэффициентов для красного цвета И и применение коэффициентов масштабирования и сдвига ABCcoef = (texture2D(ABCred. TexCoord).rgb - ABCbias) * ABCscale: DEFcoef = (texturB2D(DEFred, TexCoord).rgb - DEFbias) * DEFscale: // Вычисление полинома для красного цвета ptvec.г = ABCcoef[0] * Du * Du + ABCcoef[1] * Dv * Dv + ABCcoefL2J * Du * Dv + DEFcoeffQ] * Du + DEFcoeftl] * Dv + DEFcoef[2]: // Чтение коэффициентов для зеленого цвета // и применение коэффициентов' масштабирования и сдвига продолжение
230 Глава 10. Шейдеры с сохранением данных в текстурах Листинг 10.8 {продолжение} ABCcoef я (texture2D(ABCgrn. TexCoord).rgb - ABCblas) * ABCscale: DEFcoef = (texture2D(DEFgrn. TexCoord).rgb - DEFbias) * DEFscale: И Вычисление полинома для зеленого цвета ptvec. g = ABCcoeftO] * Du * Du + ABCcoeftl] * Dv * Dv + ABCcoef[2] * Du * Dv + DEFcoef[0] * Du + DEFcoeftl] * Dv + DEFcoef[2]; // Чтение коэффициентов для синего цвета И и применение коэффициентов масштабирования и сдвига ABCcoef - (texture2D(ABCblu, TexCoord).rgb - ABCblas) * ABCscale; DEFcoef = Ctexture2D(DEFblu. TexCoord).rgb - DEFbias) * DEFscale: // Вычисление полинома для синего цвета ptvec.b = ABCcoefГО] ’ Du * Du + ABCcoef[l] * Dv * Dv + ABCcoef[2] * Du * Dv + DEFcoefEO] * Du + DEFcoefEl] * Dv + DEFcoef[2]: // Умножение результата на коэффициент освещения ptvec *- texturelD(L1ghttexture. LdotN).rgb. // Занесение результата в переменную gl_FragColor gl_FragColor = vec4 (ptvec. 1.0): } Этот шейдер относительно прост для читателя, усвоившего предыдущие три раздела. Значения компонентов $ и t переменной TexCoord являются двухмерными параметризациями вектора приращения. Эти значения используются для поиска по текстуре с коэффициентами и получения коэффициентов Л, В, С, D, Е и F. BRDF РТМ сохраняется как многоуровневая текстура, и, так как смещение по много- уровневой текстуре не задано, просто используется вычисленный уровень детали- зации. С помощью векторных операций шесть коэффициентов масштабируются и сдвигаются по значениям, заданным приложениям в виде uniform-переменных. Такие преобразованные коэффициенты, как и Du и Dv, подставляются в би- квадратный полином, чтобы вычислить красную составляющую поверхности. Этот процесс повторяется для зеленой и красной составляющих. Коэффициент ос- вещения вычисляется обращением к ID-текстуре освещения по косинусу угла между направлением освещения и нормалью поверхности. Этот коэффициент ос- вещения умножается на полиномиальный вектор, затем добавляется значение про- зрачности 1,0, в результате чего получается окончательный цвет фрагмента. Цветной рис. 11 демонстрирует, как BRDF РТМ-шейдеры выполнили рендеринг тора, используя текстуру для автомобильного покрытия под названием Mystique Lacquer. Основной цвет этой краски — черный, но зеркальное отражение в основ- ном белого и красно-коричневого цветов с.освещенной стороны и синеватого цве- та — с противоположной. При вращении объекта или источника освещения при- веденные шейдеры будут правильно вычислять сдвиг цветов.
10,7. Ссылки 231 ЦО.6. Итоги В этой главе обсуждалось несколько шейдеров, которые используют информацию из текстурных карт. Программируемость OpenGL открывает много возможностей I использования текстурной памяти. В первом примере в текстурах хранились два обычных изображения и еще одна текстура использовалась как карта прозрачнос- ти и отражений. Во втором примере в текстуре было задано обычное цветное изоб- ражение, но шейдер интересным образом его использовал. В третьем примере ис- пользовались шесть текстур для хранения коэффициентов полинома и седьмая — для таблицы вычисления освещения. В дальнейших примерах, приводимых в книге, читатель увидит, как мож- но использовать текстуры для хранения нормалей и функций шума. Возмож- ности создания уникальных эффектов с использованием текстур поистине без- граничны. 10.7. Ссылки Основы работы с текстурами OpenGL более подробно описаны в книге [10]. Изображения Земли, использованные в разделе 10.2, можно найти на веб-сай- reNASA по адресу http://earthobservatory.nasa.gov/Newsroom/BLueMarble. Хороший обзор методик наложения карты среды доступен в документе [3], ко- торый является частью заметок для «SIGGRAPH-2000», курс 27, который называ- ется «Procedural Shading on Graphics Hardware». Этот материал в соответствую- щей переработке можно найди в книге [9]. «SIGGRAPH-2001» также предоставляет документ [6]. Дополнительную инфор- мацию можно найти на веб-сайте Hewlett-Packard по адресу http://www.hpL.hp.com/ ptm/. На этом сайте можно найти примеры файлов данных, программу для про- смотра РТМ, спецификацию формата файла РТМ и утилиты для создания РТМ- текстур. 1. Blinn J. Light Reflection Functions for Simulation of Clouds and Dusty Surfaces // Computer Graphics (Proc. SIGGRAPH-82). 1982. July. P. 2129. 2. Heidrich W., Seidel H.-P. View-Independent Environment Maps//ACM SIGGRAPH. Eurographics Workshop on Graphics Hardware. 1998. P. 39-45. 3. Heidrich W. Environment Maps and Their Applications//Proc. SIGGRAPH-2000. Course 27, course notes (http://www.csee.umbc.edu/~oLano/s2000c27/envmap.pdf). 4. Hewlett-Packard. Polynomial Texture Mapping. Веб-сайт (http://www.hpL.hp.com/ptm). 5. Kautz J., McCool M. D. Interactive Rendering with Arbitrary BRDFs Using Sepa- rable Approximations//10th Eurographics Workshop on Rendering, 1999. June. P. 281-292 (http://www.mpisb.mpg.de/~jnkautz/pubLications). 6. Malzbender T., Gelb D., Wolters H. Polynomial Texture Maps//Computer Graphics (Proc. SIGGRAPH-2001). 2001. August. P. 519-528. 7. NASA, Earth Observatory. Веб-сайт (http://earthobservatory.nasa.gov/Newsroom/ BLueMarbLe).
232 Глава 10. Шейдеры с сохранением данных в текстурах 8. Display of the Earth Taking Into Account Atmospheric Scattering/T. Nishit.a, T. Sirai, K. Tadamura, E. Nakamae//Computer Graphics (Proc. SIGGRAPH-93). 1993. August. P. 175-182 (http://nis-lab.is-s. u-tokyo.ac.jp/~nis/abs_sig. htmL#sig93), 9. Real-Time Shading/М. Olano, J. Hart, W. Heidrich, M. McCool. Natick, MS: A К Peters, Ltd., 2002. 10. NeiderJ., Davis T., Woo M. OpenGL Programming Guide. 3rd ed. Reading, MS: Addison-Wesley, 1999. 11. Sloup J. Physically-based Simulation: A Survey of the Modeling and Rendering of the Earth’s Atmosphere//Proc. of the 18th Spring Conference on Computer Graphics. April 2002, P. 141-150 (http://sgi.felk,cvut.cz/~sLoup/htmL/research/project).
Процедурные текстурные шейдеры Имея полнофункциональный высокоуровневый язык программирования для об- работки каждого фрагмента, можно алгоритмически вычислять рисунок на по- верхности объекта. Это позволяет создавать множество эффектов визуализации, которые иначе создать было бы невозможно. В предыдущей главе обсуждались шейдеры, которые должны считывать дан- ные из текстурной памяти, чтобы получить требуемый эффект. В этой же главе описываются шейдеры, которые работают с интересными эффектами в основном с помощью алгоритмов, определенных в этих шейдерах. Изображения, построен- ные с помощью таких шейдеров, создаются алгоритмом, а не основываются на предварительно вычисленных значениях, таких как оцифрованный рисунок или фотография. Иногда такие шейдеры называют процедурными текстурными шей- дерами, а выполнение вычислений с помощью такого шейдера — процедурным текстурированием. Зачастую текстурных координат или координат каждой точ- ки объекта бывает достаточно для рисования поверхности объекта процедурным текстурным шейдером. Теоретически процедурные текстурные шейдеры можно использовать для тех же задач, которые выполняют шейдеры с доступом к готовым текстурам. На прак- тике же иногда удобнее пользоваться процедурными текстурными шейдерами, а иногда — шейдерами готовых текстур. Для того чтобы выбрать, какой шейдер написать для какой-либо конкретной задачи, нужно знать некоторые особеннос- ти этих шейдеров. Перечислим основные преимущества процедурных текстур- ных шейдеров. □ Процедурно созданные текстуры требуют немного памяти по сравнению с хра- нимыми текстурами. Единственное представление текстуры — в алгоритме, оп- ределенном кодом процедурного текстурного шейдера. По сравнению с разме- рами хранимых 2О-текстур, такое представление является очень компактным. Обычно это несколько команд, которые занимают гораздо меньше памяти, чем изображения (сравните несколько килобайт кода в процедурном шейдере и не- сколько сотен килобайт или даже мегабайты для 2О-текстуры высокого каче- ства). Это означает, что процедурные текстурные шейдеры требуют меньше памяти графического ускорителя. Такие шейдеры еще экономнее, если на объект необходимо наложить ЗЭ-текстуру (сравните несколько килобайт и де- сятки или даже больше мегабайт для хранимой ЗП-текстуры). □ Текстуры, созданные процедурными шейдерами, не фиксированы и не имеют какого-либо конкретного разрешения. Их можно накладывать на любые объек- ты любого масштаба и получать хорошее изображение, так как эти текстуры
234 Глава 11. Процедурные текстурные шейдеры определены алгоритмически. А вот двухмерное изображение не всегда можно хорошо наложить на трехмерный объект — иногда нужно его растягивать или повторять, что не лучшим образом сказывается на качестве: при приближении объекта к точке обзора иногда видны куски повторяемого изображения или дефекты растягивания. Для алгоритмически созданной текстуры таких про- блем не существует. □ Процедурные текстурные шейдеры можно параметризовать. Входные парамет- ры можно легко изменять, получая таким образом множество разнообразных эффектов. А хранимую текстуру если и можно изменить, то очень незначи- тельно. А теперь некоторые недостатки процедурных текстурных шейдеров. □ Процедурные шейдеры требуют, чтобы алгоритм был воплощен в программу. Не у всех достаточно знаний для создания такой программы, тогда как нарисо- вать обычную двухмерную или трехмерную текстуру может почти каждый. □ Выполнение алгоритма, реализованного в процедурном текстурном шейдере, для каждой точки объекта может происходить намного медленнее, чем нало- жение обычной хранимой текстуры. □ Процедурные текстурные шейдеры могут создавать серьезные неровности на изображении, с которыми трудно бороться, в то время как сглаживание храни- мых текстур может выполняться на основе возможностей современного гра- фического аппаратного обеспечения. □ Из-за различий в точности выполнения арифметических операций и в реали- зации встроенных функций (например, noise) процедурные текстурные шей- деры могут давать различные результаты на разных платформах. Окончательный выбор способа работы с текстурами нужно делать с точки зре- ния его целесообразности. Произведения искусства и фотографии (рисунки, афи- ши, надписи) лучше воспроизводить с помощью хранимых текстур. Объекты, для которых важен окончательный вид изображения (лица, костюмы, значимые рек- визиты), тоже желательно хранить в текстурах — с ними художнику проще рабо- тать. Если же что-либо не слишком влияет на реалистичность окончательного изображения и покрывает большую часть поверхности, тогда визуализация вы- полняется с помощью процедурных шейдеров (стены, полы, земля). Часто применяется и гибридная технология. Например, сначала можно выпол- нить рендеринг мяча для гольфа в основном цвете, а потом наложить нарисован- ную текстуру с потертостями и логотипом и процедурно сгенерированную рель- ефную поверхность. С помощью хранимых текстур также можно контролировать или ограничивать процедурные эффекты. Если на мяче для гольфа есть травяные пятна и важно правильно отобразить их, художник может нарисовать полутоно- вую карту, с помощью которой шейдер сможет определить, где эти пятна накла- дывать (например, там, где на карте есть черные области), а где не накладывать (белые области). Шейдер может считывать эту управляющую текстуру и исполь- зовать ее для выполнения плавного перехода от нетронутой поверхности к поверх- ности, испачканной травой. Давайте рассмотрим несколько примеров полностью процедурных шей- деров.
11.1. Повторяющиеся шаблоны 235 11.1. Повторяющиеся шаблоны В главе 6 был описан процедурный шейдер для рисования кирпичной стены. Пер- вый пример этой главы тоже будет простым. Попытаемся создать шейдер, кото- рый визуализирует полосы на объекте. С помощью такого шейдера можно выпол- нять рендеринг множества реальных объектов: детских игрушек, обоев, оберточной бумаги, флагов, тканей и т. п. Объект, изображенный на цветном рис. 13, представляет собой неполный тор, рендеринг которого выполнен шейдером полосок. Этот шейдер и приложение, использующее этот шейдер, разработаны в 2002 г. компанией LightWork Design, которая разрабатывает программное обеспечение для наложения фотореалистич- ных эффектов1 на объекты, созданные коммерческими автоматизированными пакетами (CAD/CAM). Приложения, разработанные LightWork Design, имеют графический интерфейс, который позволяет пользователю менять параметры шей- дера. Имена доступных шейдеров перечислены в правом верхнем углу экрана, а па- раметры выделенного шейдера, которые можно изменять, — в правом нижнем. В нашем случае параметры для шейдера полосок — это цвет полоски (синий), цвет фона (оранжевый), масштаб (количество полосок) и ширина полоски (соотноше- ние ширины полоски к ширине фонового просвета; если это значение, например, 0,5, синие и оранжевые полоски будут одинаковой ширины). На цветном рис. 12 показан снимок экрана с большим количеством объектов, рендеринг которых вы- полнен процедурными шейдерами OpenGL. Для шейдера полосок приложение должно посылать только параметры вер- шин и текстурные координаты каждой вершины. Для того чтобы определить, ка- кой цвет использовать во фрагменте — фона или полоски, используется текстур- ная координата t, а текстурная координата s не используется вообще. Приложение должно также передавать значения в вершинный шейдер для вычисления осве- щения. Указанные параметры — цвет полоски, фона, масштаб и ширина полос- ки — должны передаваться во фрагментный шейдер, так что полоски вычисляют- ся для каждого фрагмента. 11.1.1. Вершинный шейдер полосок Вершинный шейдер для эффекта полосок приведен в листинге 11.1. । Листинг 11.1. Вершинный шейдер для рисования полосок uniform vec3 LightPosi11 on: uniform vec3 LightColor: uniform vec3 EyePosition; uniform vec3 Specular: uniform vec3 Ambient; uniform float Kd; varying vec3 DiffuseColor; продолжение & В дальнейшем фотореалистичными будем называть изображения, воспроизводящие окружающую действительность с максимально возможной точностью. Понятно, что в некоторых случаях это не требуется, например при воспроизведении анимации. — Примем, науч.ред.
236 Глава 11. Процедурные текстурные шейдеры Листинг 11.1 {продолжение) varying vec3 SpecularColor: void main(void) { vec3 ecPosition = vec3 (gl_ModelViewMatrix * gl_Vertex); vec3 tnorm = normalize(gl_NormalMatrix * gl_NormaD: vec3 lightVec = normal izeCLightPositwn - ecPosition): vec3 viewVec = normalize(EyePosition - ecPosition): vec3 twee = normalize(viewVec + lightVec): float spec = clamp(dot(hvec. tnorm). 0.0. 1.0); spec = powlspec. 16.0): DiffuseColor = LightColor * vec3 (Kd * dot(lightVec. tnorm)): DiffuseColor = cl amp(Ambient + DiffuseColor. 0.0. 1.0): SpecularColor = clamp((LightColor * Specular * spec). 0.0. 1.0): gl_TexCoord[0] = gl_Mult1TexCoord0: gl_Position = ftransform(): } В этом шейдере есть несколько интересных особенностей, но действительно ничего необычного. Это хороший пример такого вычисления освещения, которое было бы совместимо со множеством фрагментных шейдеров. Как уже упоминалось, значения для вычисления освещения (LightPosition, LightColor, EyePosItion, Specular, Ambient и Kd) передаются приложением через uni- form-перемеппые. Назначение этого шейдера — вычислять Di ffuseCol or и Specul ar- Col or, две varying-переменные, которые затем будут интерполированы для каждого примитива и переданы фрагментпому шейдеру для каждого фрагмента. Эти значе- ния вычисляются обычным способом, с небольшой оптимизацией — Ambi ent добав- ляется к вычисленному значению рассеянного отражения, чтобы посылать во фраг- ментный шейдер через varying-переменную на одно значение меньше. Входные текстурные координаты передаются во фрагментный шейдер как встроенная vary- ing-перемеппая gl _TexCoord [ 0 ], и координаты вершины преобразуются как обычно. 11.1.2. Фрагментный шейдер полосок Фрагментный шейдер содержит алгоритм для рисования полосок процедурным способом. Он приведен в листинге 11.2. Листинг 11.2. Фрагментный шейдер для рисования полосок uniform vec3 StripeColor; uniform vec3 BackColor: uniform float Width: uniform float Fuzz: uniform float Scale: varying vec3 DiffuseColor: varying vec3 SpecularColor; void maln(void) {
11.1. Повторяющиеся шаблоны 237 float scaled? ’ fract(gl_?exCoord[OJ.t * Scale): float fracl = clamp(scaled? / Fuzz, 0.0. 1.0); float frac2 = clampt(scaled? - Width) / Fuzz. 0.0. 1,0); fracl = fracl * (1.0 - frac2); fracl = fracl * fracl * (3.0 - (2.0 * fracl)): vec3 firalColor = mixtBackColor. StripeColor. fracl); finalColor = finalColor * DiffuseColor + Speculated or: gl_FragColor = vec4 (flnalCol or. 1.0); Еще одна uniform-переменная, передаваемая приложением, — Fuzz. Ее значение используется для создания плавности перехода (то есть сглаживания) между цве- том полоски и цветом фона. При значении Scale 10,0 приемлемое значение Fuzz будет 0,1. С изменением размеров объекта значение Fuzz можно изменять, чтобы не допустить лишней расплывчатости при большом увеличении. Это значение на самом деле не должно быть больше 0,5 (максимальное размытие края полоски). Сначала в шейдере выполняется умножение входной текстурной координаты t на масштаб полосок и отделение дробной части результата. Так можно вычислить координаты фрагмента внутри шаблона полосок. Чем больше значение Scale, тем больше полосок получится. Окончательное значение локальной переменной scaled? будет находиться в диапазоне [0, 1). Переходы между цветами должны быть хорошо сглажены. Один из способов это сделать — использовать функцию smoothstep при переходе от Stri peCol or кBackColor, а потом еще раз — при переходе от BackColor к StripeColor. Но шейдер учитывает тот факт, что эти переходы симметричны, и совмещает два перехода в одни. Чтобы выполнить совмещение, значение scaled? используется для вычисле- ния двух других значений: fracl и f гас2. Эти два значения сообщают, где находит- ся текущая точка по отношению к переходам от BackCol or в St ri peCol or и обратно. Если sea 1 edT/Fuzz больше 1, для fracl это означает, что точка находится вне зоны перехода, и значение приводится к 1, Если scaled? меньше Fuzz, то sealedT/Fuzz обозначает относительное расстояние от фрагмента до границы зоны перехода. Такое же значение вычисляется для другой стороны полоски вычитанием Width из scaled?, деленного на Fuzz, приведением результата к ширине зоны перехода и сохранением его в frac2. Полученные значения обозначают ширину размытой области. На одной из сто- рон полоски frac2 будет равно 0, a fracl — соответствовать относительному рассто- янию до границы зоны перехода. На другой стороне полоски fracl будет равно 1, a frac2 — соответствовать относительному расстоянию до границы зоны перехо- да. Следующая строчка кода (fracl = fracl* (1.0 - fra с2)) выдает промежуточ- ное значение для дальнейшего вычисления плавного перехода между BackColor и St ri peCol or. Но вообще-то неплохо было бы создать что-нибудь лучшее, чем про- стой плавный линейный переход. Следующая строчка кода выполняет интерпо- ляцию Эрмита, так, как это делает функция smoothstep. Окончательное значение fracl в дальнейшем используется для перехода между BackCol or и St г 1 peCol or. Результатом всех этих вычислений является плавно размытый край в зоне пере- хода между цветами. Без этого эффекта переход между цветами был бы довольно
238 Глава 11. Процедурные текстурные шейдеры грубым и перемещался при движении объекта. Выполнение плавного размытия устраняет эти проблемы. Увеличенное изображение перехода между цветами по- казано на цветном рис. 14. (О процедурных шейдерах сглаживания можно прочи- тать в главе 14.) Теперь осталось только применить эффекты рассеянного и отражающего ос- вещения, вычисленные вершинным шейдером, и добавить значение прозрачнос- ти 1,0 для получения окончательного цвета фрагмента. Изменяя пять основных параметров фрагментного шейдера, можно одним и тем же шейдером создавать довольно интересные разновидности полосок. 11.2. Игрушечный шар С помощью программируемости можно процедурно определять все виды текстур- ных шаблонов. Следующий шейдер делает немного больше, чем предыдущий, за- полняя сферу процедурно определенным шаблоном звезды и процедурно опреде- ленной полосой. Автор этого шейдера, Билл Лайси-Кейн из ATI Research, Inc., хотел создать шар, похожий на одну из ранних анимаций от Pixar — Luxo Jr. Этот шейдер очень специализированный. Как бы сказал Билл, «он заполняет любую поверхность, если это поверхность сферы». Причина этого заключается в особен- ности фрагментного шейдера, который использует одно из свойств сферы — то, что нормаль любой точки поверхности указывает в том же направлении, что и век- тор от центра сферы до этой точки поверхности. При этом нормаль поверхности вычисляется аналитически для дальнейшего использования внутри фрагментно- го шейдера. Звезда определяется коэффициентами для пяти полупространств, которые определяют форму звезды. Эти коэффициенты выбирают, исходя из размера шара. Точки на сфере классифицируются как внутренние и наружные по отношению к каждому полупространству. Точки в самом центре звезды будут однозначно клас- сифицированы как внутренние по отношению ко всем пяти полупространствам. Точки просто внутри звезды будут внутренними по отношению к четырем или пяти полупространствам. Все остальные точки, не принадлежащие звезде, будут внутренними по отношению к трем или меньшему количеству полупространств. Фрагменты в полоске вычислить легче. После того как каждая точка поверх- ности определена как «звезда», «полоска» или «иная», можно соответственно за- красить каждый фрагмент. Вычисления цвета происходят в порядке, который га- рантирует приемлемый результат, даже если шар удален от точки обзора. Нормаль поверхности вычисляется во фрагментном шейдере. Вычисленные значения ос- вещения, включая значения зеркального отражения, также накладываются на каж- дый фрагмент. 11.2.1. Подготовка приложения Приложение должно лишь задать координаты вершин для нормальной работы этого шейдера. И цвет, и нормали будут вычисляться алгоритмически во фраг- ментном шейдере. Единственное, что требуется, чтобы этот шейдер работал пра- вильно, — все вершины должны определять сферу. Сфера может быть любого раз-
11.2. Игрушечный шар 239 мера, а фрагментный шейдер сделает необходимые вычисления, основываясь на известной геометрии сферы. Параметры этому шейдеру передаются через uniform-переменные. Рисунок, показывающий, как будет меняться изображение при изменении параметров, мож- до увидеть далее в этом разделе, а код шейдера приведен в листинге 11.3. Листинг 11.3. Значения uniform-переменных для шейдера игрушечного шара LightDir 0.57735. 0.57735. 0.57735. 0.0 HVector 0.32506. 0.32505. 0.88808. 0.0 Ball Center 0.0. 0.0. 0.0. 1.0 SpecularColor 0.4, 0.4. 0.4. 60-0 Red 0.5. 0.0. 0.0. 1.0 Blue 0.0. 0.3. 0.6. 1.0 Yellow 0.5. 0.5. 0.0. 1.0 HalfSpaceO 1.0. 0.0. 0.0. 0.2 HalfSpacel 0.309015994. 0.951055516. 0.0. 0.2 HalfSpace2 -0.809016994, 0.587785252. 0.0. 0.2 HalfSpace3 -0.809016994. -0.587785252. 0.0. 0.2 HalfSpace4 0.309016994. -0.951056516. 0.0. 0.2 InOrOutlnit -3.0 StripeWidth 0.3 FW-idth 0.005 11.2.2. Вершинный шейдер Фрагментный шейдер в описываемой паре шейдеров — «рабочая лошадка», а вер- шинный шейдер должен только вычислить координаты центра шара в простран- стве координат обзора и координаты пространства отсечения для каждой верши- ны. Приложение может передавать координаты центра сферы в пространстве координат обзора, но вершинный шейдер и так делает немного работы, и выпол- нение в нем таких вычислений означает, что приложению теперь не нужно знать о матрице модели-вида. Это значение можно легко вычислить во фрагментном шейдере, но тогда пострадает производительность. Поэтому в нашем случае луч- ше всего выполнить эти вычисления в вершинном шейдере и передать результат как varying-переменную (листинг 11.4). Листинг 11.4. Вершинный шейдер игрушечного шара varying vec4 ECposItlon: varying vec4 uniform vec4 ECbal 1 Center Bal 1 Center: // координаты поверхности в пространстве И координат обзора // центр шара в пространстве координат обзора // центр шара в модельных координатах void main(void) { ECposition = gl_ModelViewMatrix * gl_Vertex; ECballCenter = gl_Mo.de!VIewMatrix * BallCenter: gl_Position “ftransformO:
240 Глава И. Процедурные текстурные шейдеры 11.2.3. Фрагментный шейдер Фрагментный шейдер игрушечного мяча немного больше, чем рассмотренный в предыдущих примерах, так что будем разбирать код по строкам и показывать промежуточные результаты. Вот определения локальных переменных, которые будут использоваться в нашем фрагментном шейдере. vec4 normal: vec4 р; vec4 surfColor: // Вычисленная нормаль // Точка // Вычисленный цвет поверхности float intensity: // Вычисленная интенсивность освещения vec4 distance; U Вычисленные значения расстояния float inorout: // Счетчик для вычисления шаблона звезды Сперва преобразуем точку на поверхности, которую нужно закрасить, в точку на сфере радиусом 1,0. Сделать это можно с помощью функции normalize: p.xyz = normalizetECposition.xyz - ECballCenter.xyz); p.w =1.0: Координата w в вычислениях не используется, поэтому применяется оператор выбора компонентов . xyz для выбора первыхтрех компонентов ECposi ti on и ECbal 1 - Center. Этот нормализованный вектор затем сохраняется в первых трех компо- нентах р. После этих вычислений р представляет точку на сфере с радиусом 1, так что все три компонента р будут находиться в диапазоне [-1, 1]. Координата wздесь не нужна, но для последующих вычислений она инициализируется в 1,0. Затем определяется положение точек звезды по отношению к полупростран- ствам. Счетчик 1 norout будет инициализирован в значение -3. Он будет увеличи- ваться все время, если точка на поверхности является внутренней по отношению к полупространству. Так как определено пять полупространств, окончательное значение счетчика будет находиться в диапазоне [-3, 2]. Значения 1 или 2 будут означать, что фрагмент находится внутри звезды. При значениях 0 или меньше фрагмент находится вне звезды: inorout = InOrOutlnlt: // инициализация inorout в -3 Мы могли бы определить полупространства как массив из пяти значений vec4, затем выполнить вычисления, а результаты сохранить в массиве из пяти значе- ний fl oat. Но здесь можно получить некоторое преимущество в скорости, исполь- зуя параллельность графического аппаратного обеспечения. Рассмотрим эту про- цедуру. Сначала с помощью встроенной функции dot вычисляется расстояние между р и первыми четырьмя полупространствами: distance!!}] = dot(p. HalfSpaceO): distance[l] = dot(p. HalfSpacel): distance!?] = dot(p. HalfSpace2): distance!?] = dottp. HalfSpace3): Результаты этих вычислений показаны на рис, 11,1, а-г. Точки на поверхнос- ти, которые определены как внутренние, показаны серым цветом, а определенные как внешние — черным цветом. Наверное, читатель удивлен, почему счетчик был определен как fl oat, а не как int. Но счетчик является всего лишь основанием для сглаживания перехода меж- ду цветом звезды и цветом остальной поверхности шара. Для этого используется функция smoothstep, устанавливающая расстояние в 0, если вычисленное рассто- яние меньше -FWidth, в 1, если вычисленное расстояние больше FWidth, и в ровно
11.2. Игрушечный шар 241 интерполированное значение между 0 и 1, если вычисленное расстояние нахо- дится между этими двумя значениями. Определив di stance как vec4, эти вычисле- ния можно производить над четырьмя значениями параллельно, srnoothstep вы- полняет операцию деления, и так как FWidth является значением типа fl oat, нужно выполнить всего одну операцию: distance = smoothstepC-FWIdth, FWidth. distance); Рис. 11.1. Результаты вычисления принадлежности точек полупространству (ATI Research, Inc.) Теперь можно быстро добавлять значения в distance, выполняя операцию dot над di stance и vec4, содержащим все единицы: inorout += dot(distance. vec4(1.0)): Так как Inorout инициализирована числом -3, добавим результат dot к преды- дущему значению inorout. Эта переменная теперь содержит значение, лежащее в диапазоне [-3, 1], и нужно вычислить расстояние по отношению к еще одному полупространству. Функция srnoothstep используется для выполнения такой же операции, как и с предыдущими полупространствами. Счетчик inorout обновля- ется: к нему добавляется результат вычисления отношения расстояния к пятому полупространству. Вычисленное расстояние до пятого полупространства показа- но на рис. 11.1, Э: distance.х = dot(p. HalfSpace4); distance.у = StripeWldth - abstp.z); distance = smoothstep(-FWidth. FWidth. distance); Inorout += distance.x; (В нашем случае операция srnoothstep выполняется над vec4, по по-настоящему нужны только два компонента. Производительность будет лучше на графических устройствах, которые спроектированы так, чтобы обрабатывать значения vec4 парал- лельно, но это может быть несколько неэффективно на графических устройствах с. обычной архитектурой. В последнем случае компилятор языка шейдеров OpenGL может быть достаточно умным для того, чтобы понять, что результаты расчета третьего и четвертого компонентов никогда не будут использоваться в программе, и оптимизировать команды так, чтобы вычислялись только два значения.) Значение Inorout находится в диапазоне [-3, 2]. Этот промежуточный резуль- тат показан на рис. 11.2, а. Приведением значения inorout к диапазону [0, 1] полу- чаем результат, показанный на рис. 11.2, б: Inorout = clamp(Inorout, 0.0. 1.0); На этом этапе можно вычислять цвет поверхности фрагмента. Мы используем вычисленное значение 1 norout для достижения плавного перехода между желтым и красным цветами, чтобы определить звезду. Если остановиться здесь, результат
242 Глава 11. Процедурные текстурные шейдеры будет таков, как на цветном рис. 16, а. Если же выполнить плавный переход к цве- ту полосы, результат будет таким, как на цветном рис. 16, б. Так как использова- лась функция smoothstep, значения 1 norout и di stance .у позволят получить хоро- шо сглаженный край между цветами- surfColor = mix(¥ellow. Red, inorout); surfColor = ml x(surfColor, Blue, distance.y); a 6 Рис. 11.2. Промежуточные результаты вычислений: а — белым цветом обозначены точки на поверхности, находящиеся внутри по отношению ко всем пяти полуплоскостям, а серым — внутри по отношению только к четырем полуплоскостям; б— значение inorout приведено к диапазону [0, 1] (ATI Research, Inc.) На этом этапе результат получается плоский и нереалистичный, и чтобы улуч- шить его, нужно рассчитать параметры освещения. Первый шаг — аналитически вычислить нормаль для данного фрагмента, что мы можем сделать, поскольку известны координаты центра шара в пространстве координат обзора (их можно получить из varying-переменной ECba 11 Center) и известны координаты фрагмента в пространстве координат обзора (из ECposI tl on). (Этот подход можно использо- вать также в шейдере земного шара, создаваемого в разделе 10.2, чтобы не пере- давать нормаль поверхности и использовать интерполированные результаты.) На самом деле мы уже вычислили это значение и сохранили в р: // normal = точка на поверхности сферы (0, 0, 0) normal = р; Рассеянная часть освещения вычисляется, следующими тремя строками кода: intensity - 0.2: // ambient intensity += 0.8 * clamp(dot(LightDir, normal). 0.0. 1.0): surfColor *= intensity: Результат вычисления только рассеянного освещения показан на цветном рис. 7, а. Последний шаг — добавление зеркального компонента следующими тре- мя строками кода: intensity = clamptdottHVector. normal), 0.0, 1.0): intensity = pow(intensity. SpecularColor.a): surfColor += SpecularColor * intensity: Как видно на цветном рис. 17, б, зеркальное освещение идеально. Так как нор- маль поверхности вычисляется очень точно для каждого фрагмента, здесь нет эффекта мозаики, который обычно наблюдается. На последнем шаге значение за- писывается в gl_FragColor и посылается для окончательной обработки перед тем, как отправиться в буфер кадров.
11.2. Игрушечный шар 243 gl_FragColor = vec4 (surfColor, 1.0); Вот и все, игрушечный шар создан. Полностью код фрагментного шейдера при- веден в листинге 11.5. Листинг 11.5. Фрагментный шейдер для рисования игрушечного шара varying vec4 ECposItlon; И Координаты поверхности в пространстве обзора varying vec4 ECbal1 Center; U Центр шара в пространстве обзора uniform vec4 LightDir; И направление освещения, нужна нормализация uniform vec4 HVector; // вектор отражения для освещения uniform vec4 SpecularColor; uniform vec4 Red, Yellow. Blue: uniform vec4 HalfSpaceO; // uniform vec4 HalfSpacel: uniform vec4 HalfSpace2: uniform vec4 HalfSpaceB; uniform vec4 HalfSpace4: полупространства, определяющие звезду uniform float InOrOutlnit: // = -3 uniform float StripeWidth; // = 0.4 uniform float FWidth; ll = TBD void main(void) { vec4 normal: vec4 p; vec4 surfColor: float intensity; vec4 distance; float inorout; // Аналитически вычисленная нормаль // Точка II Вычисленный цвет поверхности // Вычисленная интенсивность освещения // Вычисленные значения расстояния И Счетчик для вычисления звезды p.xyz = normalizeCECposition.xyz - ECbal1 Center,xyz); p.w ’ 1.0; Inorout = InOrOutlnit: // инициализация inorout a -3 distanceEO] - dottp, HalfSpaceO): distance[l] = dottp, HalfSpacel); distance[2] = dottp. HalfSpace2); distance[3] = dottp. HalfSpace3); distance = smoothstept-FWidth, FWidth. distance): inorout += dottdistance. vec4(1.0)); distance.x = dottp. HalfSpace4); distance.y = StripeWidth - abstp.z); distance = smoothstept-FWidth. FWidth. distance): inorout += di stance, x; inorout = clamptinorout. 0.0, 1.0): surfColor = mix(Yellow, Red. inorout); продолжение
244 Глава 11. Процедурные текстурные шейдеры Листинг 11.5 {продолжение) surfColor = m1x(surfColor. Blue, distance.у): // normal - точка на поверхности сферы (0.0.0) normal = р; // Вычисление рассеянного освещения для каждого фрагмента intensity = 0.2; И ambient intensity += 0.3 * clampfdot(LIghtDIr, normal)., 0.0, 1.0); surfColor *= Intensity; Il Вычисления отраженного освещения для каждого фрагмента intensity = clampCdot(HVector. normal). 0.0. 1.0); intensity = powCintensity. SpecularColor.a)-. surfColor += SpecularColor * intensity: gl_FragColor - vec4 (surfColor, 1.0); } 11.3. Сетка Рассмотрим своего рода хитрость. Здесь будет показано, как не рисовать объект процедурно. В этом примере читатель увидит, как можно использовать команду di scar'd во фрагментном шейдере, чтобы получить интересные эффекты. Команда discard означает, что фрагмент будет отброшен и не попадет в буфер кадров вообще. Ис- пользуем этот прием для рисования геометрических фигур с «дырками». Вершин- ный шейдер будет таким же, как вершинный шейдер полосок (см. раздел 11.1.1). Фрагментный шейдер приведен в листинге 11.6. Листинг 11.6. Фрагментный шейдер для процедурного отбрасывания частей объекта varying vec3 DiffuseColor: varying vec3 SpecularColor; uniform vec2 Scale; uniform vec2 Threshold; uniform vec3 SurfaceColor; void main (void) { float ss = fract(gl_TexCoord[0].s * Scale.s); float tt = fract(gl_TexCoord[0].t * Scale.t); if ((ss > Threshold.s) (tt > Threshold.!)) discard; vec3 finalColor = SurfaceColor * DiffuseColor + SpecularColor; gl_FragColor = vec4 (finalColor, 1.0); } Отбрасываемая часть объекта определяется значениями 5 и t текстурных ко- ординат. Коэффициент масштаба применяется для того, чтобы определить часто- ту сетки. Затем вычисляется дробная часть отмасштабированного значения тек-
11.4. Бугристая поверхность 245 йгурных координат, чтобы в итоге получилось значение, принадлежащее диапа- зону [0,1]. Значения 5 и t сравниваются с заданным граничным значением. Если оба значения превышают граничное, фрагмент отбрасывается. В противном слу- чае вычисляется освещение и выполняется рендеринг фрагмента. На цветном рис. 15 оба граничных значения были установлены в 0,13. Это оз- иачает, что больше чем три четверти фрагментов было отброшено! Получилась «дырявая корова1». 1.4. Бугристая поверхность Читатель уже видел процедурные шейдеры, которые меняют цвет (кирпичная стенка, полоски) и непрозрачность (сетка). Еще один интересный эффект можно получить, используя метод бугристой поверхности. Этот метод подразумевает изменение нормали поверхности перед наложением освещения. Изменение нор- мали может быть сделано алгоритмически, если накладывается какой-либо по- вторяющийся шаблон; можно просто добавлять шум к компонентам нормали, а можно брать значения из текстурной карты. Метод бугристой поверхности -- эффективный способ сделать изображение более реалистичным, не увеличивая сложность геометрических фигур. Этот метод можно использовать для имитации деталей поверхности или ее неровностей. Метод бугристой поверхности не изменяет саму поверхность, а просто хитро меняет ее освещение. Поэтому неровности не будут видны на краях силуэта объек- та. Например, нужно сделать модель планеты как сферы и закрасить ее с помо- щью метода бугристой поверхности, чтобы показать горы, большие относительно диаметра планеты. Так как в геометрии исходного объекта (сферы) ничего не ме- няется — она всегда будет идеально круглой, — то гор не будет видно на самом краю изображения, хотя посередине они будут рельефно выделяться. Но в реаль- ных объектах рельеф поверхности обычно проявляется и на краях силуэта. По- этому данный метод используется для наложения эффекта на небольшие по срав- нению со всей поверхностью участки. Апельсиновая корочка, рельефные логотипы, кирпичи с неровной поверхностью — это хорошие примеры применения метода бугристой поверхности. Метод бугристой поверхности создает видимое усложнение поверхности во время обработки фрагментов, так что обрабатываться она будет во фрагментном шейдере. Это означает, что вычислять освещение нужно не в вершинном шейде- ре, как это делается обычно, а во фрагментном. Так проявляется еще одно пре- имущество программируемости, доступной с помощью языка шейдеров OpenGL, — возможность выполнять операции в тех местах, где они нужны, — в вершинном или фрагментном шейдере. Не обязательно полагаться на обычную последова- тельность выполнения операций. Для реализации метода бугристой поверхности нужны будут правильные нор- мали поверхности для каждого фрагмента, а также вектор направления освещения 1 Ища слов: «holy cow» можно перевести как «дырявая корова», в то время как это устоявшееся нефор- мальное выражение «ну и дела!». — Примеч. перев.
246 Глава 11. Процедурные текстурные шейдеры и вектор обзора; эти значения должны быть доступны из фрагментного шейдера, В таком случае можно будет процедурно изменить нормаль перед вычислением освещения, чтобы добиться эффекта неровности поверхности. В данном примере на поверхность будут накладываться «шишечки», маленькие сферические бугорки. Источник освещения обычно рассчитывается с помощью функции dot. Чтобы результат имел какое-то значение, его нужно преобразовать так, чтобы все компо- ненты, необходимые для вычисления освещения, были определены в одном и том же пространстве координат. Обычно при вычислении освещения в вершинном шейдере векторы направлений или источники освещения задаются в простран- стве координат обзора, а входные нормали и значения вершин преобразуются в это пространство координат. На самом деле не обязательно выполнять все вычисления именно в этом про- странстве координат. Можно нормализовать направление освещения и нормаль поверхности после преобразования их в пространство координат обзора, а затем передать их во фрагментный шейдер через varying-переменные, После интерпо- ляции вектор направления освещения должен быть перенормализован, что по- зволяет получить более точные результаты. Какой бы способ ни применялся для вычисления изменяющей нормали, ее нужно преобразовать в пространство коор- динат обзора и добавить к нормали поверхности, вектор которой также должен быть нормализован. Если не выполнить перенормализацию, могут появиться за- метные дефекты изображения. Выполнение всех этих преобразований для каж- дого фрагмента может значительно уменьшить производительность. Но вообще- то есть способ лучше. Существует еще одно пространство координат, называемое локальное простран- ство координат поверхности. Это система координат, которая меняется с каждой точкой поверхности объекта и предполагает, что в текущей точке поверхности будут (0, 0, 0), а пока не измененная нормаль поверхности в каждой точке имеет координаты (0, 0,1). Это очень удобная система координат для метода бугристой поверхности. Но перед вычислением освещения нужно убедиться в том, что и на- правление освещения, и направление обзора, и вычисленная изменяющая нор- маль также определены в этой системе координат. Если изменяющая нормаль оп- ределена в локальном пространстве координат поверхности, это означает, что нужно преобразовать векторы освещения и обзора в это пространство координат. Как это сделать, рассказывается в дальнейшем. Здесь понадобится матрица преобразования, которая будет преобразовывать каждую входную вершину в локальное пространство координат поверхности (то есть входная вершина (х, у, z) будет преобразована в (0, 0, 0)). Нужно создать та- кую матрицу преобразования для каждой точки. Затем в каждой точке эта матри- ца будет использоваться для преобразования направления освещения и направ- ления обзора. Координаты будут вычисляться отдельно для каждой вершины и затем интерполироваться для каждого примитива. В каждом фрагменте можно использовать эти значения для вычисления освещения, применяя полученную ранее изменяющую нормаль. Но как именно создавать такую матрицу преобразования? Существует беско- нечное множество преобразований, которые могут переводить заданную точку в (0, 0, 0). Нужен способ преобразования входных значений вершин, который будет давать согласованные результаты, чтобы потом их можно было интерполировать.
11.4. Бугристая поверхность 247 Для этого приложение должно посылать еще один атрибут вершины, значение тангенса. Значения тангенсов должны быть согласованы по всей поверхности объекта. По определению, вектор тангенса будет лежать в плоскости поверхности иперпендикулярно нормали поверхности. Если тангенсы согласованы, они помо- гут согласовывать и локальные пространства координат. Вычислив векторное про- изведение вектора тангенса и нормали поверхности, получим третий вектор, пер- пендикулярный первым двум. Третий вектор называется бинормаль и его можно вычислить в вершинном шейдере. Вместе эти три вектора являются ортонормиро- ванными, и на их основе можно выполнять преобразования из пространства коор- динат объекта в локальное пространство координат поверхности. Так как данная конкретная система координат определена с тангенциальным вектором как одним из базовых, эту систему координат иногда называют касательное пространство. Преобразование из пространства координат объекта в локальное пространство ко- ординат поверхности показано на рис. 11.3. Вектор пространства координат объек- та (О.,, Оу, Ог) преобразуется в локальное пространство умножением на матрицу, в которой содержится тангенциальный вектор (Тп Ту, Т2) в первой строке, вектор би- нормали (В.г, Ву, во второй строке и нормаль поверхности (М, ЛГУ, М) в третьей строке. Можно использовать этот способ для преобразования и вектора освещения, и вектора обзора. Преобразованные векторы будут интерполированы в пределах примитива, а интерполированные векторы затем используются во фрагментном шейдере для вычисления отражения с помощью уже измененной нормали. Рис. 11.3. Преобразование из пространства координат объекта в локальное пространство координат поверхности 11.4.1. Подготовка приложения Чтобы шейдер бугристой поверхности работал правильно, приложение должно задать координаты вершины, нормаль поверхности и тангенциальный вектор в плоскости поверхности. Тангенциальный вектор передается как произвольный вершинный атрибут, а индекс атрибута должен быть связан с переменной вер- шинного шейдера tangent с помощью вызова функции gl Bl ndAttri bLocati onARB. Приложение также должно задать значения для uniform-переменных Li ghtPosi tl on, SurfaceColor, BumpDensity, BumpSize и SpecularFactor. Необходимо позаботиться о том, чтобы тангенциальные векторы были согласова- ны; в противном случае преобразования в локальные пространства также будут не- согласованными, что сделает результаты вычисления освещения непредсказуемы- ми. Согласованные тангенциальные векторы могут быть вычислены алгоритмически для поверхностей, определенных математическими формулами. Согласованные тан- генциальные векторы для сложных объектов можно вычислить с помощью соседних вершин, согласованно упорядочивая их с помощью текстурных координат объекта. Проблема, связанная с несогласованно определенными нормалями, иллюстри- руется рис. 11.4. На диаграмме показаны два треугольника: один с согласованными
248 Глава 11. Процедурные текстурные шейдеры тангенсами, второй — с несогласованными. Серые стрелки указывают направление тангенциального вектора и вектора бинормали (нормаль поверхности направле- на прямо вверх перпендикулярно странице). Белые стрелки показывают направ- ление навстречу источнику освещения (в данном случае это направленный ис- точник освещения). 1. Пространство у поверхности вершины 1 1. Пространство у поверхности вершины 2 2, Пространство у поверхности вершины 1 2. Пространство у поверхности вершины 2 1. Небольшая интерполяция векторов освещения Рис. 11.4. Несогласованные тангенсы могут вызвать серьезные ошибки при вычислении освещения 2. Значительная интерполяция векторов освещения
11.4. Бугристая поверхность 249 В обоих случаях при преобразовании вершины 1 в локальное пространство f координат поверхности получается один и тот же результат. Но при преобразова- i нии вершины 2 наблюдается большое отличие, так как тангенциальные векторы этих двух вершин очень разные. Если бы они были определены согласованно, этого бы пе случилось, разве что поверхность стала бы очень изогнутой. И если это дей- ствительно так, можно разбить поверхность объекта на ячейки, чтобы предотвра- , тить возможные проблемы. В первом случае вектор направления освещения будет ровно интерполирован от первой до второй вершины, и все интерполированные векторы будут прибли- зительно одной длины. Если нормализовать этот вектор освещения в каждой вер- шине, его длина будет очень близка к единичной. Но во втором случае векторы после интерполяции будут иметь разную длину, некоторые из них почти нулевые. Это может привести к дефектам изображения. OpenGL не определяет конкретный вершинный атрибут для тангенциального вектора. Следует использовать произвольный атрибут вершины для передачи этого значения, также, как это сделано в примере, приведенном в разделе 10.5.1. Един- ственное различие в том, что не приложение будет передавать бинормаль, а вер- шинный шейдер сам будет вычислять ее. 11.4.2. Вершинный шейдер Вершинный шейдер для метода бугристой поверхности приведен в листинге 11.7. Этот шейдер должен вычислять направления освещения и обзора в локальном пространстве координат поверхности. Входные данные для шейдера — координа- ты вершины, нормаль поверхности, тангенциальный вектор, Шейдер вычисляет бинормаль и преобразует координаты с помощью созданной матрицы преобразо- вания. Текстурные координаты тоже передаются в вершинный шейдер, так как текстура используется для задания координат процедурных бугорков. Листинг 11.7. Вершинный шейдер для процедурного метода бугристой поверхности varying vec3 LightDir; varying vec3 EyeDir; uniform vec3 LightPosition; attribute vec3 Tangent; void main(void) { EyeDir = vec3 (gl_Model ViewMatrix * gl_Vertex); glPosition - ftransformO; gl_TexCoord[0] = glMultlTexCoordO: vec3 n = normalize(gl_No™aiMatrix * gljtormal); vec3 t = normalize(gl_NormalMatrix * Tangent); vec3 b = crossCn. t): vec3 v: v.x = dot(L1ghtPosit1en, t); v.y = dotCLightPosition. b); продолжение zy
250 Глава 11. Процедурные текстурные шейдеры Листинг 11.7 {продолжение) v.z = dot(LightPosltion, и); LightDir = normal 1 ze с v); v.x = dot(EyeDir, t); v.y = doUEyeDir, b); v.z = dotCEyeDir. n): EyeDlr ~ normalize(v): 11.4.3. Фрагментный шейдер Фрагментный шейдер для метода бугристой поверхности приведен в листинге 11.8. Через uniform-переменные передаются некоторые характеристики шаблона бу- горков, а именно: BumpQensity (количество бугорков в заданной области) и Bump- Size (размеры бугорков). Также задаются характеристики самой поверхности: SurfaceColor (основной цвет поверхности) и SpecularFactor (свойство рассеянного отражения поверхности). Вычисляемые бугорки будут круглыми. Так как для вычисления расположе- ния бугорков используются текстурные координаты, сначала нужно умножить входные текстурные координаты на значение density — от этого зависит количе- ство бугорков на поверхности. Получив некую сетку, вычисляем бугорок в каж- дой ее ячейке. Компоненты вектора р вычисляются как расстояние от центра бугорка в направлениях х и у. (Нам нужно всего лишь изменить нормаль в на- правлениях .г и у. Значение г для преобразованной нормали всегда будет 1,0.) «Ложное расстояние» d вычисляется возведением в квадрат компонентов р и по- следующим их сложением. (Настоящее расстояние можно вычислить извлечени- ем квадратного корня, но обычно это не требуется, если BumpSi ле является не абсо- лютным, а относительным значением.) Чтобы правильно вычислить отражение позже, нужно предварительно норма- лизовать изменяющую нормаль. Эта нормаль должна быть единичным вектором, чтобы потом можно было выполнить операцию dot и получить точные значения косинуса для использования при вычислении освещения. Вектор нормализуется умножением каждого компонента нормали на 1,0/sqrt(jc2 + y2 + z2). При вычисле- нии d часть необходимых данных уже получена (то естьх2 + у2). Так каки вообще не меняется, мы знаем, что г2 всегда будет 1,0. Чтобы уменьшить количество вы- числений, коэффициент нормализации в данном случае будет вычисляться по формуле l,O/sqrt(rf + 1,0). Далее сравниваем d с BurnpSi ze, чтобы узнать, есть в данной точке бугорок или нет. Если нет, изменяющий вектор устанавливается в 0, а коэффициент нормали- зации - в 1,0. В следующих нескольких строках вычисляется освещение. Норма- лизованный изменяющий вектор умножается на коэффициент нормализации f. Рассеянное и зеркальное отражения вычисляются как обычно, за исключением того, что используются векторы освещения и обзора в локальном пространстве координат поверхности. Неплохие результаты получаются и без нормализации этих двух векторов, если значения их не будут сильно различаться от вершины к вершине.
11.4. Бугристая поверхность 251 Листинг 11.8. Фрагментный шейдер для процедурного метода бугристой поверхности varying vec3 LightDi г: varying vec3 EyeDir: uniform vec3 SurfaceColor: H = CO.7, 0.6. 0.18) uniform float BumpDensity; // = 16,0 uniform float BumpSize; И = 0.15 uniform float SpecularFactor; // = 0.5 void main (void) есЗ 1itColor; ec2 c = BumpDensity * glJTexCoordCO].st; ec2 p = fract(c) vec2 (0.5); loat d. f: = p.x * p.x + p.y * p.y; =1.0/ sqrtCd + 1.0); f (d >= BumpSize) { p = vec2(0.0); f = 1.0; } vec3 normDelta = vec3 (p.x, p.y, 1.0) * f; litColor = SurfaceColor * max(dot(normDelta, LightDir), 0.0); vee3 reflectDir = reflecttLightDir. normDelta): float spec = max(dot(EyeU1r. reflectDir), 0.0); spec = pow(spe.c. 6.0) spec *= SpecularFactor; litColor = mintlitColor + spec. vec3 (1.0)): glFragColor = vec4 (litColor, 1.0); } Результаты применения такого шейдера к двум объектам, простому блоку и то- ру, показаны на цветном рис. 18. Текстурные координаты используются для оп- ределения расположения бугорков, и, так как текстурные координаты изменяют- ся от 0,0 до 1,0 четыре раза по окружности тора, бугорки выглядят на этом объекте расположенными гораздо ближе друг к другу. 11.4.4. Карты нормали Рассматриваемый шейдер можно легко изменить, чтобы он получал значения для изменения нормали из текстуры, а не вычислял их самостоятельно. Текстура, ко- торая содержит значения изменяемой нормали, называется карта нормали. Пример карты нормали и результаты ее применения на простом блоке показаны на цветном рис. 19. Отдельные компоненты нормалей могут располагаться в диа- пазоне [-1,1 ]. Карта нормали изображена синим цветом, так как вектор изменений по умолчанию (0, 0, 1) занесен в карту нормали как (0,5, 0,5, 1,0). Поддержка тек- стур со значениями с плавающей запятой, с 16 битами на каждый компонент по- является все в большем количестве графических акселераторов, но еще не счита- ется используемой повсеместно, Если для хранения нормали выбран формат
252 Глава 11. Процедурные текстурные шейдеры текстуры с плавающей запятой, это улучшает качество изображения (например, снижает «волосатость» при отражении освещения). Но при этом производитель- ность может пострадать, так как здесь используется 16 бит на компонент вместо 8. Вершинный шейдер будет таким же, как описанный в разделе 11.4.2, Фрагмен- тный шейдер — почти таким же, как описанный ранее, за исключением того, что изменяющая нормаль не вычисляется, а считывается из карты нормали, находя- щейся в текстурной памяти. 11.5. Итоги Искусный фокусник может создать иллюзию того, как из простого воздуха полу- чается нечто. С помощью процедурных текстур вы как создатель шейдера можете превращать простые серые поверхности в цветные, узорчатые, бугристые или зер- кальные, Все, что для этого нужно, — лишь придумать алгоритм, описывающий свойства воображаемой текстуры. Закодировав этот алгоритм как OpenGL-шей- дер, вы тоже можете создать нечто из воздуха. В этой главе мы лишь коснулись того, что можно сделать с помощью проце- дурных текстур. Нами разработан шейдер полосок, и теперь создание разного рода сеток, шахматных досок и узоров «в горошек» не должно представлять для вас никаких трудностей. Мы нарисовали игрушечный мячик со звездой, но это мог быть и пляжный мяч со снежинками. С помощью шейдеров можно добавлять или скрывать части объектов, а также создавать холмы или выемки. В следующей гла- ве будет показано, как с помощью нерегулярных функций (шума) и процедурных текстур можно получать различные эффекты. Шейдеры, генерирующие текстуры по сложным математическим функциям (множества Мандельброта и Джулии), а также создающие не фотореалистичные эффекты, также будут описаны далее в этой книге. Процедурные текстуры являются математически точными, легко параметри- зуемыми и не требуют больших объемов памяти. Конечной целью создания вер- шинного и фрагментного шейдеров является вычисление значений для цвета (и возможно, глубины), которые будут записаны в буфер кадров. Так как язык шейдеров OpenGL является процедурным языком, единственным ограничением для таких вычислений может стать только ваше воображение. 11.6. Ссылки Книга [3] полностью посвящена созданию изображений с помощью процедур. Она содержит множество информации о создании и применении процедурных моде- лей и текстур. Шейдеры, написанные на языке шейдеров RenderMan, в большинстве своем также являются процедурными. Книги [1 и 6] содержат замечательные примеры таких программ. Идея бугристых поверхностей принадлежит Джиму Блинну, она описана в ра- боте [2]. Очень хороший обзор приемов создания бугристых поверхностей содер- жится в книге [4].
11.6. Ссылки 253 Плагин для Photoshop для создания карты нормалей на основе изображения можно скачать с веб-сайта для разработчиков компании NVIDIA, адрес http:// developer.nvidia.com/view.asp?IO=ps_texture_compression_plugin. 1. Apodaca A. A., Gritz L Advanced RenderMan: Creating CGI for Motion Pictures. San Francisco: Morgan Kaufmann Publishers, 1999 (http://www.bmrt.org/arrnan/ materials.html). 2. BlinnJ. Simulation of Wrinkled Surfaces//Computer Graphics (Proc. SIG- GRAPH 78). 1978. August. P. 286-292. 3. Texturing and Modeling: A Procedural Approach. 3rd ed./D, S. Ebert, J. Hart, B. Mark, at al. San Francisco: Morgan Kaufmann Publishers, 2002 (http://www. texturin'gandmodeling.com). 4. Kilgard M.J. A Practical and Robust Bump-mapping Technique for Today's GPUs, Game Developers Conference//NVIDIA White Paper. 2000 (http://deve- loper.nvidia.com). 5. NVIDIA. Веб-сайт для разработчиков (http://developef.nvidia.com), 6. Upstill S. The RenderMan Companion; A Programmer’s Guide to Realistic Computer Graphics. Reading, MS: Addison-Wesley, 1990.
Шум В компьютерной графике легко создавать идеальные изображения — все геомет- рические фигуры нарисованы и закрашены ровно и красиво. Но если нужно изоб- разить реалистичный объект, из преимущества это превращается в недостаток. У объектов реального мира зачастую есть вмятины, потертости, царапины. Иног- да они изношенные и дырявые. Художники, работающие с компьютерной графи- кой, прилагают немало усилий для того, чтобы кегля выглядела так, будто она использовалась в кегельбане на протяжении 20 лет, а космический корабль ка- зался вернувшимся из многолетнего путешествия. Эту проблему пытался решить Кен Перлин (Ken Perlin), когда работал в ком- пании Magi в начале 80-х гг. Magi и компания Disney в то время работали над художественным фильмом «Трон». Перлин заметил «несовершенство» идеально закрашенных объектов в фильме и попытался разрешить эту проблему. В 1985 г. Перлин опубликовал содержащую интересные идеи статью, в кото- рой описал свою программу для рендеринга. В ней использовалась технология, названная Перлином шум. Его определение шума немного отличается оттого, что обычно называется этим термином. Обычно, когда говорится про шум, то подра- зумевается нечто вроде случайного набора пикселов на телевизионном экране при отсутствии сигнала (так называемый «снег») или радиопомех на частоте, которая не используется ни одной ближайшей станцией. Но такая действительно случайная функция не годится для компьютерной графики. Нужна функция с повторениями, позволяющая рисовать объект с раз- ных углов обзора. Бывает необходимо также рисовать объект одним и тем же спо- собом кадр за кадром, создавая иллюзию движения. По-настоящему случайные функции не зависят от входных значений, поэтому объект будет выглядеть по- разному при каждой перерисовке, а когда объект на экране движется, это выгля- дит некрасиво. Здесь нужна функция, которая всегда будет выдавать одно и то же выходное значение для каждого входного значения, но в то же время создавать иллюзию случайности. Эта функция должна быть непрерывной на всех уровнях детализации. Перлин первым разработал такую функцию. С тех пор создано множество по- хожих функций шума, которые используются совместно для получения различ- ных эффектов визуализации, например: □ рендеринга природных явлений (облака, огонь, дым, ветер и т. д.); □ рендеринга природных материалов (мрамор, гранит, дерево, горы и т. д.); □ рендеринга произведенных человеком материалов (штукатурка, асфальт, це- мент и т. д.);
12.1. Определение шума 255 I '□ наложения неоднородностей на идеальные модели (ржавчина, грязь, пятна, вы- I боины и т. д.); I □ наложения неоднородностей на идеальные шаблоны (волны, бугорки, переход I цвета и т. д.); I □ наложения неоднородностей на периоды времени (период мерцания, время I между сменой кадров и т. д,); [ □ наложения неоднородностей на движение (колебания, дрожание, тряска и т. д.). р Вообще-то этот список можно продолжать до бесконечности. Но большинство современных библиотек для рендеринга включает поддержку для шума Перлина или эквивалентных функций. Эти функции — основа реалистичного рендеринга, и сейчас, они очень широко используются в работе с компьютерными изображе- ниями, особенно при производстве фильмов. Кен Перлин проделал значительную работу в этой области, и в 1997 г. ему вручили премию «Оскар» за технические достижения в кинематографии. Так как наложение шума является важной технологией компьютерной графи- ки, в языке шейдеров OpenGL для него предусмотрены встроенные функции. Есть несколько способов наложения шума во фрагментном шейдере. После общего описания технологии шума в этой главе будут приведены несколько примеров шейдеров, демонстрирующих, как можно получить интересные эффекты с исполь- зованием этой технологии. 12.1. Определение шума Цель этого раздела - не описывать математически функции шума, а дать читате- лю достаточно информации для понимания шейдеров, использующих шум, и со- здания новых эффектов на основе этой технологии. Чтобы глубже разобраться в технологии шума, следует обратиться к литературе, ссылки на которую приве- дены в конце этой главы, особенно к книге [2], в которой обсуждается технология шума, в том числе и самая первая функция шума, созданная К. Пердином. В этой книге Дарвин Пичи (Darwyn Peachey) систематизирует функции шума. Наложе- ние функций шума и различные их сочетания описаны Кеном Масгрейвом (Кеп Musgrave) в главе о построении изображений планет процедурно. Перлин описывает шум как «приправы» для графики. Эти функции помогают добавлять изображению небольшие дефекты, но их влияние на внешний вид объек- та не исчерпывается только этим. Идеальная модель должна выглядеть не иде- ально (иметь погрешности и дефекты) и поэтому более реалистично, так как на нее накладываются едва различимые эффекты шума, Идеальная функция шума должна обладать несколькими важными качест- вами. □ Это непрерывная функция, которая создает впечатление случайной. □ Это воспроизводящаяся функция (будет выдавать всякий раз одно и то же зна- чение при задании одного и того же входного параметра). □ Функция имеет определенный диапазон выходных значений (обычно [-1, 1] или [0, 1]).
256 Глава 12. Шум □ Значения функции не должны формировать какого-либо очевидного регуляр- ного шаблона или периода. □ Функция не зависит от масштабирования. □ Это изотропная функция (при статистическом повторении она должна быть инвариантной). □ Функция может быть определена для 1,2, 3,4 или больше измерений. Такое определение шума подразумевает создание неровного примитива, кото- рый затем можно использовать для наложения видимых случайных элементов на повторяющийся шаблон, а также для моделирования, рендеринга или анимации. Характеристики функции шума позволяют получить с ее помощью весьма инте- ресные эффекты. Алгоритмы для создания функций шума подразумевают комп- ромисс качества и скорости, и приведенные ранее критерии способствуют выбо- ру такого компромисса. Простую функцию шума (называемую Пичи значение шума) можно создать, присваивая псевдослучайное число из диапазона [-1,1] каждому целому числу на оси.г (рис. 12.1), а затем плавно интерполируя значения между этими точками (рис. 12.2). Функция является повторяемой, то есть для определенного значения входного параметра все время возвращается одно и то же значение функции. « О-----1—I—I—I—I—I—I—НЧ—I—i—Н -1 -- Рис. 12.1. Дискретная ID-функция шума -1 1 Рис. 12.2. Непрерывная ID-функция шума Выбор метода интерполяции между дискретными точками очень важен для этой разновидности функций шума. Линейная интерполяция недостаточно хоро- ша, так как в результате наложения такого шума на изображении появляются де-
42.1. Определение шума 257 фекты, Для получения плавных переходов обычно используется метод кубичес- С.кой интерполяции. Изменяя частоту и амплитуду, можно добиться большого разнообразия функ- ций шума (рис. 12.3). Частота = 4 Амплитуда = 1,0 Частота = 8 Амплитуда = 0,5 Частота =16 Амплитуда = 0,25 Частота = 32 Амплитуда = 0,125 Частота = 64 Амплитуда = 0,0625 Рис. 12.3. Изменение частоты и амплитуды функции шума Как можно видеть на рисунке, с увеличением частоты и уменьшением ам- плитуды «помехи» становятся мельче. Отношение двух частот друг к другу в мас- штабе 2:1 называется октава. На рис, 12.3 показаны пять октав одномерной функ- ции шума. Сами по себе эти функции не слишком полезны, но применение их в шейдерах помогает получить некоторые интересные эффекты. Если сложить вместе функции различных частот, результат кажется гораздо более интересным (рис. 12.4). 9 Зак. 218
258 Глава 12, Шум Рис. 12.4. Результат сложения функций шума различной частоты и амплитуды В результате получается функция, которая содержит «помехи» различных раз- меров. Большие выпуклости на низкочастотных функциях обеспечивают общую форму, а меньшие выпуклости на высокочастотных — детализацию, заметную при уменьшении масштаба. Перлин назвал функцию суммы последовательных октав с разницей в половину амплитуды функцией шума 1//, но сейчас более популяр- но другое название — «отрывочное броуновское движение» («fractional Brownian motion», или «fBm»).
J 12.1. Определение шума 259 Если различные октавы шума суммируются в процедурном шейдере, наступа- ет момент, когда некоторые частоты могут вызвать нежелательные дефекты изоб’ ражеиия. Алгоритмы, учитывающие необходимость сглаживания функций шума, обычно прекращают наложение октав раньше, чем частота станет настолько вы- сокой, чтобы вызвать дефекты. Вместо этого можно сделать функцию более плав- ной в месте, где появляется дефект. Функцию шума, разработанную Перлином (шум Перлина), иногда называют градиентным шумом. Она определена как функция со значением 0 для каждого целого входного значения; для каждой из этих точек определяется псевдослу- чайный градиентный вектор. Характеристики этой функции шума позволяют ис- пользовать ее для некоторых эффектов [2]. Именно эта функция используется sRenderMan как функция шума, и она же, скорее всего, будет использоваться в реализациях встроенной функции noise языка шейдеров OpenGL. Существует много других функций шума. Сочетая их или используя попере- менно, можно добиться интересных эффектов. Но не так-то просто сразу предста- вить себе результат применения этих функций, и нужная функция обычно под- бирается методом проб и ошибок. 12.1.1. 20-шум Итак, основная идея функции шума была рассмотрена на примере одномерной функции шума. А теперь приведем описание двухмерной функции шума и при- меры изображений с наложенным двухмерным шумом Перлина на разных часто- тах, приведенных к диапазону [0, 1] и показанных как полутоновое изображение (рис. 12.5). На каждом следующем изображении частота вдвое больше, чем на предыдущем. Контрастность изображений специально была усилена, для того чтобы зернистость была лучше заметна. Обычно в реальных изображениях каж- > дая следующая картинка имеет еще и половинную амплитуду предыдущей, но 1 если бы здесь использовался этот же способ, картинки были бы более серыми и чи- , татель не смог увидеть хороший пример двухмерного шума. Рис. 12.5. Базовый двухмерный шум на частотах 4, 8, 16 и 32 (контраст усилен) I Как и в случае одномерных функций шума, сложение различных частот иног- да дает более интересные изображения (рис. 12.6). Первое изображение (см. рис. 12.6) точно такое же, как первое изображение предыдущего рисунка (см. рис. 12.5). Второе изображение — это сумма первого изображения и половины второго изображения предыдущего рисунка, сдвину- того таким образом, что средняя интенсивность равна 0. Получается, что в не- которых областях интенсивность усиливается, а в некоторых — уменьшается.
260 Глава 12. Шум На третьем изображении к первым двум октавам добавлена третья, а на четвер- том — четвертая октава. Четвертое изображение уже немного похоже на облака. Рис. 12.6. Сложенный шум 1, 2, 3 и 4 октав (контраст усилен) 12.1.2. Шум большей размерности Трехмерные и четырехмерные функции шума — очевидные расширения одно- мерных и двухмерных функций. Довольно сложно показать в книге изображе- ние трехмерного шума, но можно представить себе, что двухмерные изображения (см. рис. 12.5) — это срезы трехмерных функций шума. Между соседними среза- ми всегда будет плавный переход изображения. Зачастую шум большей размерности используется для того, чтобы плавно ме- нять во времени значения шума меньшей размерности. Например, одномерный шум можно использовать для внесения неровностей в линию, а двухмерный шум - для анимации (от кадра к кадру расположение неровностей плавно меняется). Точно так же двухмерная функция шума используется для рисования облаков, а трехмерная — для анимации изображения облаков. Используя четырехмерную функцию шума, можно создавать трехмерные объекты (например, планеты), а чет- вертое измерение применять для равномерного изменения объектов. 12.1.3. Шум в шейдерах OpenGL Существует три способа использования шума в шейдере OpenGL. 1. Использование встроенной функции moi se языка шейдеров OpenGL. 2. Создание собственной функции шума на языке шейдеров OpenGL. 3. Хранение предварительно вычисленной функции шума в текстурной карте. К сожалению, на время написания этой книги существует только одна доступ- ная реализация языка шейдеров OpenGL, и в ней нет ни встроенной функции noise, ни возможности реализации определенных разработчиком функций. Но шум является важным аспектом компьютерной графики, так что в приведенных при- мерах будут использовать третий способ, а именно сохранение функции шума в текстурной карте. 12.2. Текстуры шума Возможности программируемости в языке шейдеров OpenGL открывают совер- шенно новые возможности использования текстурной памяти. Можно заранее
12.2. Текстуры шума 261 'Вычислить функцию шума и сохранить ее в одномерной, двухмерной или трех- мерной текстурной карте. Эта текстурная карта (или карты) затем читается из шейдера. Так как текстуры могут содержать до 4 компонентов, можно использо- вать одну текстурную карту для хранения четырех октав шума или четырех со- вершенно отдельных функций шума. В листинге 12.1 приведена функция на языке С, которая создает трехмерную текстуру шума. Эта функция создает текстуру RGBA, причем первая октава шума .сохраняется в красном компоненте текстуры, вторая — в зеленом компоненте, тре- тья - в синем компоненте, а четвертая — в компоненте прозрачности. Каждая ок- тава отличается от предыдущей вдвое большей частотой и половинной амплитудой. Эта функция предполагает существование функции пот se3, которая выдает трехмерные значения шума в диапазоне [-1, 1]. Можно попробовать использовать в своей программе реализацию этой функции на языке С от Перлина (http:// www.texturingandniodeLing.com/CODE/PERLIN/PERLIN.С). Джон Кессепич сделал в этом коде несколько изменений (добавил функцию setNolseFrequency), обеспечиваю- щих плавное изменение значений шума на краях массива. В этом случае при по- вторении тех же фрагментов (режим GL_REPEAT) переход на краях фрагментов плав- ный, а не резкий. Измененную версию кода можно найти на веб-сайте этой книги, ; http://3dshaders.com. Листинг 12.1. Функция на языке С для создания трехмерной текстуры шума int noise3DTexSize - 128: GLuint noiseSDTexNarne = 0; GLubyte *noise3DTexPtr: void make3DNoiseTexture(void) int f, 1, j. k, inc; Int startFrequency = 4: int numOctaves = 4: double ni[3]: double inci. inej. inek: int frequency = startFrequency: GLubyte *ptr; double amp =0.5; if ((noiseSDTexPtr = (GLubyte *) mallocC no1se3DTexS1ze * noise30TexSize * noise30TexSize * 4)) = NULL) fprjntf(stderr. "Ошибка: невозможно выделить память\п"); exit(l); for (f = 0. inc - 0; f < numOctaves: ++f. frequency ★= 2, ++inc, amp *= 0.5) setNolseFrequency(frequency); ptr = noise3DTexPtr; ni[0] = ni[1] = ni[2] = 0; inci = 1.0 / (noise3DTexSize / frequency); for (i - 0; i < noise3DTexS1ze; ++i, ni[0] += irici) продолжение d?'
262 Глава 12. Шум Листинг 12.1 {продолжение} { incj = 1.0 / (noise3DTexSize / frequency); for (j = 0; j < noiseSDTexSize: ++j. ni[l] += incj) { inck = 1.0 / (noise3DTexSize / frequency): for (k = 0; к < noise3DTexSize: ++k. ni[2] += inck. ptr+= 4) { *(ptr+inc) = (GLubyte)(((noise3(ni)+l.0) * amp)*128.0): } } } } Эта функция вычисляет значения шума для четырех октав и сохраняет их в трехмерной текстуре RGBA размером 128 х 128 х 128. Данный код также предполагает, что каждый компонент текстуры сохраняется как 8-битное це- лое значение. В первой октаве частота равна 4, а амплитуда — 0,5. В самом внут- реннем цикле вызывается функция noise3 для получения значения шума, ос- нованного на текущем значении ni. Функция noi se3 всегда возвращает значение в диапазоне [-1, 1]; добавив единицу, получим значение шума в диапазоне [0, 2]. Умножив результат на значение амплитуды 0,5, получим значение в диапазо- не [0, 1]. И наконец, умножив последнее значение на 128, получим целое чис- ло в диапазоне [0, 128], которое'сохраняется в красном компоненте текстуры (при доступе из шейдера это значение будет числом с плавающей запятой в диа- пазоне [0, 0,5]). При каждом следующем прохождении цикла частота увеличивается вдвое, а амплитуда вдвое уменьшается. В зеленом компоненте текстуры шума будут со- храняться целые значения из диапазона [0, 64], в синем компоненте — целые зна- чения из диапазона [0, 32], а в компоненте прозрачности — целые значения из диа- пазона [0, 16]. Изображения, приведенные на рис. 12.5, сформированы отдельно из каждого из этих каналов, после того как значения были приведены к нужному диапазону интенсивности (целые числа в диапазоне [0, 255] или числа с плаваю- щей запятой в диапазоне [0, 1]). После вычисления значений для текстуры шума их следует занести в текстур- ный модуль с помощью кода, приведенного в листинге 12.2. Текстурный модуль сначала выбирается, а потом связывается с ранее созданной SD-текстурой. Для параметров края задан плавный переход во всех трех измерениях, и поэтому функ- ция шума всегда будет возвращать правильные значения вне зависимости от пе- реданного входного параметра. Однако следует заботиться о том, чтобы повто- рения фрагментов текстуры не были заметны. Следующие две строчки устанавливают для текстуры линейный режим фильтрования, так как по умолчанию текстура является и множественной, и линейной, а множественность здесь не нужна. (Ис- пользование множественной текстуры в некоторых случаях оправдано, но здесь масштабирование будет выполняться внутри шейдера шума, и простой текстуры будет достаточно.) После установки всех параметров текстуру можно загружать в текстурный модуль функцией glTexImage3D.
12.3. Выбор метода создания шума 263 Листинг 12.2. Функция для создания трехмерной текстуры шума void init3DNoiseTexture() glGenTextures(1. &noise3DTexName): glActiveTexture(GL_TEXTURE6): glBindTexture(GL_TEXTURE_3D. noise3DTexName); glTexParameterf(GL_TEXTURE_3D. GL_TEXTURE_WRAP_S. GL_REPEAT); glTexParameterf(GL_TEXTURE_3D. GL_TEXTURE_WRAP_T. GL_REPEAT); glTexParameterf(GL_TEXTURE_3D. GL_TEXTURE_WRAP_R. GL_REPEAT); glTexParameterf(GL_TEXTURE_3D. GL_TEXTURE_MAG_FILTER. GL_LINEAR): glTexParameterf(GL_TEXTURE_3D. GL_TEXTURE_MIN_FILTFR. GLJJNEAR); glTex!mage3D(GL_TEXTURE_3D. 0. GL_RGBA. noise3DTexSize. noise3DTexSize. noise3DTexSize. 0. GL_RGBA. GL_UNSIGNED_BYTE. noise3DTexPtr); } Этот способ хорош, если в окончательном изображении не будет повторе- ний или если они будут не очень заметны. Один из способов избежать повто- рений — проследить, чтобы каждое значение текстуры было прочитано только один раз. Например, если текстура имеет размеры 128 х 128 х 128, а входными значениями для функции шума являются координаты объекта, то повторений не будет при условии, что поверхность объекта полностью покрывается дан- ной текстурой. 2.3. Выбор метода создания шума После того как графические ускорители станут поддерживать встроенную функ- цию по1 se и позволять разработчикам добавлять собственные функции, будут до- ступны три метода создания шума в шейдере и придется выбирать, какой именно метод использовать в приложении. Очень многое зависит от реализации OpenGL, Яо если нужно вычислять шум без использования текстур, то лучше всего подой- дет встроенная функция noise языка шейдеров OpenGL. Далее перечислены пре- имущества ее использования. р Не расходуется текстурная память (к примеру, текстурная карта в RGBA с 8 би- 1 тами на компонент занимает 8 Мбайт текстурной памяти). р Не расходуется текстурный модуль (это может быть важно для разработчиков ' в случае, если графический ускоритель предоставляет только 2 или 4 текстур- ' ных модуля). Р Функция noise непрерывная, а не дискретная, поэтому при любом масштабе i на изображении с шумом не будет пикселизации1. Пикселизация — появление на растровом изображении нежелательного регулярного геометрическо- го узора, например, в виде маленьких квадратиков. — Примеч. перев.
264 Глава 12. Шум □ Эта функция не периодическая и не повторяющаяся, особенно для двухмер- ного и трехмерного шума (но это зависит от реализации). □ Шейдеры, использующие встроенную функцию noi se, не зависят от приложе- ния (как это было бы в случае использования текстурного метода). Преимущества метода текстурных карт таковы: □ Так как функция шума вычисляется приложением, то разработчик приложе- ния может самостоятельно контролировать ее и быть уверенным, что эта функ- ция одинаково реализуется на любом графическом аппаратном обеспечении. □ В каждой текстуре можно хранить до четырех значений шума (по одному для каждого канала текстуры R, G, В и А). Таким образом, можно вычислять четы- ре октавы шума и получать все четыре значения за одно обращение к текстуре. □ Обращение к текстуре происходит быстрее, чем выполнение встроенной функ- ции noise. Возможность определять свои функции шума тоже необходима — используя их, можно получить совсем другое изображение, чем в результате использования встроенной функции noise. Зато определенная разработчиком функция шума потенциально может работать по-разному на разных графических ускорителях и платформах, в то время как встроенная функция шума работает везде одинако- во (по крайней мере, пока все производители аппаратного обеспечения будут ре- ализовывать ее одинаково). Встроенную функцию noise можно оптимизировать для каждого графического ускорителя, возможно, даже сделав полную аппарат- ную реализацию, так что, скорее всего, встроенная функция noise будет работать быстрее функции, определенной разработчиком шейдера. В целом ожидается, что приложения будут использовать либо встроенную, либо определенную функцию шума, чтобы шум не повторялся (как это может произойти при использовании текстуры), значения шума были более точными, а ресурсы текстур могли применяться для других целей. А если приложению нужно полно- стью контролировать работу функции шума и разработчиков устраивают ограниче- ния в диапазоне входных значений текстурной функции шума, то используется текстурный метод создания шума. На данном этапе развития аппаратного обеспе- чения текстуры шума также работают быстрее и требуют меньше команд в шейдере. 12.4. Простой шейдер шума Теперь посмотрим, как можно использовать разные способы создания шума в шей- дерах OpenGL, которые создают интересные эффекты. Первый шейдер использу- ет шум очень простым способом и рисует облака. 12.4.1. Настройка приложения Шейдеры, описанные здесь и в разделах 12.5 и 12.6, требуют не очень много вход- ных параметров. Как и всегда, нужно передавать координаты вершин, а нормаль поверхности нужна для вычислений освещения. Различные цвета и коэффициен- ты масштабирования передаются как uniform-переменные в различные шейдеры.
L12.4. Простой шейдер шума 265 2.4.2. Вершинный шейдер В листинге 12.3 приведен код вершинного шейдера, который работает совместно |С четырьмя фрагментными шейдерами шума, описанными далее. Это простой ^шейдер, так как в нем должны проводиться всего три операции. L Как и во всех вершинных шейдерах, вершинные координаты должны быть [ преобразованы и записаны во встроенную varying-переменную gl_Position. В. Входные значения нормали и uniform-переменная LightPos участвуют в вычис- J лении интенсивности освещения от простого белого источника. Для увеличе- f ния освещенности результат умножается на коэффициент 1,5. Входные вершинные координаты сохраняются в varying-переменной MCposition, и эти значения становятся доступными фрагментному шейдеру как координа- ты каждого фрагмента объекта в модельном пространстве. Эти координаты — идеальное входное значение для поиска по текстуре. Не имеет значения, в каком положении нарисован объект; у фрагментов будут всегда одни и те же (или очень близкие) координаты, поэтому значения шума в каждой точке поверхнос- ти объекта всегда будут одинаковые (или с очень небольшой погрешностью). Uniform-переменная Sea 1 е устанавливается приложением, чтобы наиболее оп- тимально масштабировать объект по отношению к размеру текстуры шума. 1истинг 12.3. Вершинный шейдер для рисования облаков varying float Lightintensity: varying vec3 MCposition: uniform vec3 LightPos: uniform float Scale: void main(void) ' vec3 ECposition = vec3 (gl_ModelViewMatrix * gl_Vertex): MCposition = vec3 (gl_Vertex) * Scale: vec3 tnorm = normalize(vec3 (gl_NormalMatrix * gl_Normal)): : Lightintensity = dot(normalize(LightPos - ECposition). tnorm): ! Lightintensity *= 1.5: gl_Position = ftransformO: 2.4.3. Фрагментный шейдер После вычисления текстуры шума и вызова функций OpenGL для загрузки тек- Ьтуры в графический акселератор можно применять описанный ранее вершин- ный шейдер совместно с очень простым фрагментным шейдером (листинг 12.4). В результате обработки изображения получится эффект неба, покрытого облака- ии. Можно немного поэкспериментировать с цветом, чтобы изображение стало Колее реалистичным. 1 Входные значения для этого вершинного шейдера передаются в двух varying-ne- Ьеменных, а вычисляются в вершинном шейдере, описанном ранее: Light intensity hMCposi11 on. Эти значения вычисляются для каждой вершины вершинным шейде- ром и затем интерполируются по целому примитиву соответствующим аппаратным
266 Глава 12. Шум модулем. В данном фрагментном шейдере можно обращаться к интерполирован- ному значению каждой из этих переменных для каждого фрагмента. Первая строка кода шейдера выполняет поиск по трехмерной текстуре шума и получает результат, состоящий из четырех компонентов. Значение intensity вы- числяется сложением четырех компонентов текстуры шума. Результат затем ум- ножается на коэффициент масштабирования 1,5 и используется для плавного линейного перехода от белого цвета к небесно-голубому. Четыре канала текстуры шума имеют определенные значения: 0,25, 0,125, 0,0625 и 0,03125. Дополнитель- ное значение 0,03125 добавляется к каждому среднему значению всех октав с бо- лее высокой частотой. Можно считать это «обесцвечиванием» средних значений на всех октавах с более высокой частотой, которые не включаются в вычисления (см. раздел 12.1). Умножение на коэффициент масштабирования 1,5 дает возмож- ность «растянуть» полученное значение, чтобы выйти за рамки диапазона [0,1]. Затем на вычисленный цвет накладывается значение Lightintensity, чтобы сымитировать диффузно отражающую поверхность, освещенную одним источ- ником. Каждый компонент цвета затем приводится к диапазону [0, 1], результат заносится во встроенную переменную gl_FragColor со значением прозрачности 1,0 и передается для окончательной обработки. Объект, рендеринг которого выпол- нен данным шейдером, показан на цветном рис. 20, причем текстура на чайничке очень напоминает окончательное изображение рис. 12.6. Листинг 12.4. Фрагментный шейдер для создания эффекта облачного неба varying float Lightintensity: varying vec3 MCposition: uniform sampler3D Noise: uniform vec3 SkyColor: // (0.0. 0.0. 0.8) uniform vec3 CloudColor: // (0.8. 0.8. 0.8) void main (void) { vec4 noisevec = texture3D(Noise. MCposition): float intensity = ( noisevec[0] + noisevec[l] + noisevec[2] + noisevec[3] + 0.03125) * 1.5: vec3 color = mix(SkyColor. CloudColor. intensity) * Lightintensity: gl_FragColor = vec4 (color. 1.0): } 12.5. Завихрения Интересные эффекты получаются при использовании абсолютного значения функции шума. Производная функции в этом случае будет разрывной, так как функция имеет излом в точке 0 (см. рис. 5.2). Если выполнить такую операцию над функциями шума на разных частотах и сложить результаты, результат функ- ции будет иметь перегибы и изломы. Перлин называет этот вид шума завихре- ние (турбулентность), так как он напоминает турбулентный поток. В природе такие потоки встречаются довольно часто, и с помощью завихрения можно ими-
12.5. Завихрения 267 тировать, например, огонь или лаву. Двухмерный вариант такого шума показан на рис. 12.7. Рис. 12.7. Абсолютное значение шума, или турбулентность 2.5.1. Шейдер поверхности солнца Описываемый далее эффект похож на впадины в расплавленной лаве или на по- верхности солнца, и его можно получить с помощью все того же вершинного шей- дера и немного измененного фрагментного шейдера. Основное его отличие в том, что каждое из значений шума масштабируется и сдвигается симметрично относи- тельно 0, а потом вычисляется абсолютное значение. После сложения всех значе- ний результат опять масштабируется, чтобы приблизительно помещаться в диа- пазон [0, 1]. В дальнейшем это значение используется для смешивания желтого и красного цветов, а результат такого смешивания показан на цветном рис. 20 (ли- стинг 12.5). (В главе 13 будут рассмотрены некоторые способы анимации таких текстур, для того чтобы поверхность выглядела более интересно и реалистично.) Листинг 12.5. Фрагментный шейдер поверхности солнца varying float Lightintensity; varying vec3 MCposition: uniform sampler3D Noise: uniform vec3 Colorl: uniform vec3 Color2: uniform float NoiseScale: // (0.8. 0.7. 0.0) // (0.6. 0.1. 0.0) // 1.2 void main (void) { vec4 noisevec = texture3D(Noise. MCposition * NoiseScale): float intensity = abs(noisevec[0] - 0.25) + abs(noisevec[l] - 0.125) + abs(noisevec[2] - 0.0625) + abs(noisevec[3] - 0.03125): intensity = clamp(intensity * 6.0, 0.0, 1.0): vec3 color = mixtColorl. Color2. intensity) * Lightintensity: gl_FragColor = vec4 (color. 1.0):
268 Глава 12. Шум 12.5.2. Мрамор Еще одна разновидность функции шума — часть периодической функции, напри- мер синуса. Добавив значение шума к аргументу функции синуса, получим функ- цию колебаний с шумом. Получается нечто вроде прожилок на некоторых видах мрамора. В листинге 12.6 приведен фрагментный шейдер для создания этого эф- фекта. Вершинный шейдер остается все тем же, а результаты работы шейдеров показаны на цветном рис. 20. Листинг 12.6. Фрагментный шейдер для создания эффекта мрамора varying float Lightintensity: varying vec3 MCposition; uniform sampler3D Noise; uniform vec3 MarbleColor; uniform vec3 VeinColor: void main (void) { vec4 noisevec = texture3D(Noise. MCposition): float intensity = abs(noisevec[0] - 0.25) + abs(noisevec[l] - 0.125) + abs(noisevec[2] - 0.0625) + abs(noisevec[3] - 0.03125): float sineval = sin(MCposition.y * 6.0 + intensity * 12.0) * 0.5 + 0.5: vec3 color = mix(VeinColor. MarbleColor. sineval) * Lightintensity: gl_FragColor = vec4 (color. 1.0): 12.6. Гранит С помощью шума легко имитировать практически любой материал. В данном при- мере сымитирован серый каменистый материал с небольшими черными вкрапле- ниями. Чтобы создать относительно высокочастотную текстуру шума, использу- ется только четвертый компонент (с наивысшей частотой). Он масштабируется с определенным коэффициентом, чтобы получить определенный уровень интен- сивности, а затем это значение подставляется для всех остальных компонентов (красного, зеленого и синего). Результат приведен в листинге 12.7 — так создает- ся поверхность, похожая на гранит (см. цветной рис. 20). Листинг 12.7. Фрагментный шейдер гранитной поверхности varying float Lightintensity: varying vec3 MCposition; uniform sampler3D Noise: uniform float NoiseScale:
Дерево 269 void main(void) vec4 noisevec = texture3D(Noise. NoiseScale * MCposition): float intensity = minfl.O, noisevec[3] * 18.0): vec3 color = vec3 (intensity * Lightintensity): gl_FragColor = vec4 (color. 1.0): .7. Дерево 'одобным созданию гранитной поверхности способом можно получить и нечто схожее на дерево ости (листинг 12.8). Вот основные идеи фрагментного шейдера деревянной поверх - Дерево состоит из чередующихся светлых и темных участков поверхности в форме концентрических цилиндров, окружающих центральную ось. Шум применяется для деформирования цилиндров так, чтобы они выглядели более реалистично. Центр «дерева» находится на координатной оси у. На всю поверхность накладывается высокочастотный «волокнистый» шаблон, чтобы создать впечатление распиленного дерева, показывая волокнистую струк- туру этого материала. Фрагментный шейдер деревянной поверхности работает совместно все с тем :е вершинным шейдером. L7.1. Настройка приложения [ейдеру деревянной поверхности не требуется много информации. Приложение олжно передавать вершинные координаты и нормаль с помощью обычных функ- ий OpenGL. Также вершинный шейдер получает координаты источника осве- щения и коэффициент масштабирования, которые передаются как uniform-nepe- енные. Фрагментный шейдер получает несколько uniform-переменных для того, тобы контролировать вид деревянной поверхности. Перечислим uniform-переменные, используемые шейдером деревянной поверх- ости: LightPos 0.0. 0.0, 4.0 Scale 2.0 LightWood 0.6. 0.3. 0.1 DarkWood 0.4. 0.2. 0.07 RingFreq 4.0 LightGrains 1.0 DarkGrains 0.0 GrainThreshold 0.5 NoiseScale 0.5. 0.1, 0.1 Noisiness 3.0 GrainScale 27.0
270 Глава 12. Шум 12.7.2. Фрагментный шейдер В листинге 12.8 приведен фрагментный шейдер деревянной поверхности. Листинг 12.8. Фрагментный шейдер деревянной поверхности varying float Lightintensity: varying vec3 MCposition: uniform sampler3D Noise: uniform vec3 Lightwood: uniform vec3 DarkWood: uniform float RingFreq: uniform float LightGrains: uniform float DarkGrains: uniform float GrainThreshold uniform vec3 1 MoiseScale: uniform float Noisiness: uniform float GrainScale: void main(void) { vec3 noisevec = vec3 (texture3D(Noise. MCposition * NoiseScale) * Noisiness): vec3 location = MCposition + noisevec: float dist = sqrtdocation.x * location.x + location.z * location.z): di st *= RingFreq: float r = fracttdist + noisevecLO] + noisevecfl] + noisevec[2]) * 2.0: if (r > 1.0) r = 2.0 - r: vec3 color - mix(LightWood. DarkWood. r): r = fract((MCposition.x + MCposition.z) * GrainScale + 0.5): noisevec[2] *= r; if (r < GrainThreshold) color += Lightwood * LightGrains * noisevec[2]: else color -= Lightwood * DarkGrains * noisevec[2]: color *= Lightintensity: gl_FragColor = vec4 (color. 1.0): Как видно из листинга, несколько параметров, заданных с помощью uniform- переменных, помогают манипулировать видом поверхности из приложения. Как и в большинстве процедурных шейдеров, здесь для наложения текстуры исполь- зуются координаты объекта. В данном случае координаты объекта умножаются на NoiseScale (переменная типа vec3, с помощью которой можно масштабировать изображение отдельно в направлениях х, у и г), а результат становится индексом в заданной трехмерной текстуре шума. Значения шума, полученные из текстуры, масштабируются по значению Noisiness, что позволяет увеличить или уменьшить влияние шума на изображение.
12.7. Дерево 271 Здесь дерево будет считаться последовательностью чередующихся темных и светлых концентрических колец. Чтобы поверхность выглядела более реалис- тично, нужно добавить к координатам объекта вектор шума. Первая (низкочас- тотная) октава добавляется к х-координате, третья октава добавляется к 2-коор- динате (а у-координата вообще не используется). В результате получатся кольца, которые будут несколько отличаться по ширине и расстоянию от центра дерева. Для вычисления текущих координат относительно центра дерева компоненты х и 2 возводятся в квадрат, а из полученных значений извлекается квадратный корень. Так вычисляется расстояние от центра дерева до текущего кольца. Затем оно умножается на RingFreq — коэффициент масштабирования, посредством ко- торого можно задавать количество (большее или меньшее) колец на поверхности материала. Затем создается функция, значение которой увеличивается от 0 до 1,0, а потом опять уменьшается до 0. К расстоянию прибавляются еще три октавы шума, что- бы материал выглядел волокнистым. Здесь можно вычислить и другие значения шума, но уже полученные тоже годятся. Получим дробную часть вычисленного значения, результат будет находиться в диапазоне [0, 1). Умножим это значение на 2,0 и получим функцию, значения которой будут входить в диапазон [0, 2). И наконец, вычтем 1,0 из тех значений, что больше 1,0, и получим искомую функ- цию, увеличивающуюся от 0 до 1,0, а потом уменьшающуюся обратно до нуля. Эта «треугольная» функция понадобится для вычисления основного цвета фрагмента с помощью встроенной функции mix. Функция будет обеспечивать плавный линейный переход между цветами LightWood и DarkWood, основанный на вычисленном значении г. Уже в результате этих вычислений получается неплохая функция, но попро- буем еще улучшить ее имитацией незначительного эффекта зернистости распи- ленного дерева. (Читатель может и не разглядеть этот эффект на объектах, изоб- раженных на цветном рис. 22.) Нужно создать полоски, приблизительно параллельные оси у. Это делается сложением координат х и 2, умножением результата на коэффициент GrainScale (еще одна uniform-переменная, значение которой можно изменять из приложе- ния, чтобы контролировать частоту этого эффекта), добавлением 0,5 и отделени- ем дробной части результата. Получается функция, значение которой находится в диапазоне [0, 1), но для значений по умолчанию GrainScale (27,0) и RingFreq (4,0) эта функция для г будет изменяться от 0 до 1,0 более часто, чем предыдущая. Можно было бы создать «волокна» просто линейным переходом от светлого цве- та к темному, но попробуем применить более искусный способ. Значение г умно- жается на третью октаву шума — так оно увеличивается нелинейно — и сравнива- ется со значением GrainThreshold (по умолчанию 0,5). Если значение г меньше, чем GrainThreshold, текущий цвет меняется добавлением к нему значения, вычис- ленного перемножением значений LightWood, LightGrains и измененного значения шума. И наоборот, если значение г больше, чем GrainThreshold, текущий цвет ме- няется вычитанием из него произведения DarkWood, DarkGrains и измененного зна- чения шума. (По умолчанию значение LightGrains 1,0, a DarkGrains 0, так что если г больше, чем GrainThreshold, цвет не изменится вообще.) Можно поразвлечься и посмотреть, в результате чего изображение меняется. Возможно, есть простой способ добиться лучшего эффекта.
272 Глава 12. Шум После того как цвет вычислен, остается умножить его на интерполированный коэффициент диффузного освещения и добавить значение прозрачности 1,0, что- бы получить окончательный цвет фрагмента. Результаты применения данного шейдера к трем различным объектам показаны на цветном рис. 22. 12.8. Итоги В этой главе описаны основные разновидности шума — невероятно полезной функции для добавления неоднородностей к изображению в процедурных шей- дерах. После краткого описания математического определения функции шума приводятся шейдеры, воспроизводящие облака, завихрения, мрамор, гранит и де- рево. Функция шума является встроенной функцией языка шейдеров OpenGL. Разработчики также могут создавать собственные функции шума или работать с текстурами. Однако, как бы ни был реализован шум, он применяется для дос- тижения большей реалистичности изображения или создания мультипликации с помощью добавления к изображениям дефектов, отклонений и видимых нере- гулярностей. 12.9. Ссылки Кен Перлин рассказывает историю создания функции шума, представляет учеб- ное пособие по ее применению, а также последнюю реализацию функции на язы- ке Java на своем веб-сайте (http://www.noisemachine.com). Множество интересной информации доступно на его домашней страничке NYU (http://mrL. nyu.edu/~perLin). Его доклад [4] был прочитан на SIGGRAPH в 1985 г., а об улучшениях первично- го алгоритма говорилось в докладе [5] на SIGGRAPH-2002. Книга [2] содержит несколько замечательных описаний функции шума, а на веб-сайте этой книги, http://www.texturingandmodeLing.com, можно найти исходный код множества функций шума на языке С, в том числе и первую функцию шума. Кен Перлин написал для этой книги главу, в которой его алгоритм шума описан в подробностях, а Кен Масгрейв рассматривает потрясающе красивые, созданные математически с помощью функций шума, миры. (Программы для создания изоб- ражений различных планет, использующие эти идеи, можно найти на веб-сайте компании Pandromeda, http://www.pandromeda.com.) В главе 2 этой книги Дарвин Пичи приводит множество функций шума, а в главе 7 Дэвид Эберт описывает метод, похожий на метод трехмерного текстурирования. Книга [1] также содер- жит описание шума и представляет несколько замечательных шейдеров шума на языке RenderMan. 1. Apodaca A. A., Gritz L. Advanced RenderMan: Creating CGI for Motion Pictures. San Francisco: Morgan Kaufmann Publishers, 1999 (http://www.bmrt.org/arman/ materiaLs.htmL). 2. Texturing and Modeling: A Procedural Approach. 3rd ed./D. S. Ebert, J. Hart, B. Mark, at al. San Francisco: Morgan Kaufmann Publishers, 2002 (http:// www.texturingandmodeLing.com).
12.9. Ссылки 273 3. Hart J. C. Perlin Noise Pixel Shaders//ACM SIGGRAPH/Eurographics Workshop on Graphics Hardware. 2001. August. P. 87-94 (http://graphics.cs.uiuc.edu/~jch/ papers/pixelnoise.pdf). 4. Perlin K. An Image Synthesizer//Computer Graphics (Proc. SIGGRAPH-85). 1985. July. P. 287-296. 5. Perlin K. Improving Noise//Computer Graphics (Proc. SIGGRAPH-2002). 2002. July. P. 681-682 (http://mrL.nyu.edu/perlin/paper445.pdf). 6. Perlin К. Личный веб-сайт (http://www.noisemachine.com). 7. Perlin К. Личный веб-сайт (http://mrl.nyu.edu/~perLin).
Анимированные шейдеры С помощью анимации можно показать все, что только человек может себе представить. Эта возможность делает анимацию очень гибкой, придуманной для понимания чего-либо сразу многими. Уолт Дисней Как точно заметил Уолт Дисней, анимация предоставляет зрителю множество информации. Вращение объекта позволяет получить представление о его очерта- ниях и форме. Имитация пребывания внутри здания дает человеку, находящему- ся в нем, представление о том, как здание будет выглядеть изнутри. Увидев мими- ку лица на экране, можно понять, какие эмоции испытывает человек. При помощи языка шейдеров OpenGL и последних разработок в области гра- фического аппаратного обеспечения можно создавать реалистичные изображе- ния в реальном времени на более дешевых графических ускорителях. Приятной неожиданностью стало обнаружение того, как легко выполняется анимация с по- мощью программируемых шейдеров. Если разрабатывать шейдеры, которые ме- няют свое поведение в течение времени, то на графические ускорители можно переложить еще большее количество работы. В этой главе рассказывается, как написать шейдер, создающий эффект анима- ции. Обычно эффект анимации контролируется одной или несколькими uniform- переменными, через которые передается время от приложения. Можно переда- вать номер кадра, количество прошедшего времени или любое другое значение, с помощью которого можно контролировать время. Если изображены несколько объектов, которые должны двигаться по-разному, каждый объект будет нарисо- ван своим шейдером. Если же у объектов одинаковы все характеристики, кроме параметров анимации, исходные коды шейдеров будут различаться только фраг- ментом, определяющим эффект анимации для конкретного объекта. Шейдер можно написать так, что он будет рисовать объект по-разному всякий раз при новом значении времени. Чтобы избежать дефектов изображения и раз- рывов при движении объекта, в конце временного цикла объект должен возвра- титься в первоначальное положение. Если приложение использует для создания эффекта анимации шейдер, ему уже не нужно выполнять сложные вычисления — лишь обновлять значения времени при смене кадров и перерисовывать объект. Но иногда приложение может выполнять и некоторые дополнительные вычисле- ния для достижения интересных эффектов анимации. Интересные эффекты анимации создаются с помощью хранимых и процедур- ных текстур, шума и другими способами. В этой главе будут описаны некоторые
13.3. Преобразования 275 простые эффекты анимации и шейдеры для изменения формы и расположения объекта, а также для имитации колебательных движений. 3.1. Два режима рисования объекта Пусть существует объект, который нужно рисовать по-разному в зависимости от какого-либо условия: например, мигающая неоновая реклама в некоторые моменты времени включена, а в некоторые — выключена. В принципе, можно написать два разных шейдера, один для включенного состояния, а второй — для выключенно- го. Но в данном случае будет проще написать один шейдер, который анализирует состояние некоторой переменной и определяет, в каком состоянии находится объект, включенном или выключенном, а потом рисует объект в соответствии с этими данными. Чтобы это сделать, в приложении устанавливается значение uniform-перемен- ной, которое показывает текущее состояние объекта. Приложение должно вовре- мя обновлять эту переменную; например, для того чтобы неоновая реклама была 3 секунды включена и одну секунду выключена, приложение должно записать в переменную значение «включено», через 3 секунды записать «выключено», еще через одну секунду записать «включено» и т. д. В промежутках нужно всего лишь постоянно перерисовывать изображение. Разделение рендеринга и эффекта ани- мации делает приложение более простым, его легче поддерживать и легче вно- сить в него изменения. 3.2. Пределы Можно сделать некоторое усовершенствование описанного ранее способа — пе- редавать значение, которое в шейдере будет сравниваться с другими значениями (пределами). Одно управляющее значение и два пороговых значения позволят шейдеру рисовать объект тремя способами: одним, если управляющее значение меньше порогового значения, вторым — если управляющее значение находится между двумя пороговыми значениями, и третьим — если управляющее значение больше порогового значения. В приведенном примере с рекламой есть одна неточность: на самом деле нео- новые лампы не гаснут и не загораются сразу, это происходит постепенно. При- менив описанный способ, можно усовершенствовать пример, сглаживая переход между двумя крайними состояниями. Плавный переход можно вычислять с по- мощью функции smoothstep. 3.3. Преобразования Для хранимых или процедурных текстур самая простая анимация — сдвиг при считывании значений из текстуры. Например, если нужно изобразить облака, плывущие по небу, можно взять шейдер, описанный в разделе 12.4, и внести в него небольшие изменения. К индексу для трехмерной текстуры шума (координатам
276 Глава 13. Анимированные шейдеры объекта) добавляется смещение, значение которого берется из uniform-перемен- ной, а переменная обновляется из приложения с каждым новым кадром. Если нужно добиться медленного движения облаков слева направо, из т-компоненты этой uniform-переменной при смене кадров вычитается небольшое постоянное значение. Если же, допустим, облака должны двигаться быстро снизу вверх, нуж- но вычитать большее постоянное значение из ^-компонентов. Для получения бо- лее сложного эффекта изменяются все три координаты. Чтобы вычислить это смещение, можно использовать функцию шума, делающую движение облаков более реалистичным и не похожим на обычную прокрутку. Шейдер облаков, измененный с учетом последних замечаний, приведен в лис- тинге 13.1. Листинг 13.1. Фрагментный шейдер для анимации облачного неба varying float Lightintensity: varying vec3 MCposition: uniform sampler3D Noise: uniform vec3 SkyColor: // (0.0. 0.0. 0.8) uniform vec3 CloudColor: // (0.8. 0.8. 0.8) uniform vec3 Offset: // обновляется приложением для каждого кадра void main (void) { vec4 noisevec = texture3D(Noise, MCposition + Offset): float intensity = ( noisevecLO] + noisevecLl] + noisevec[2] + noisevec[3]) * 1.5: vec3 color = mix(SkyColor, CloudColor. intensity) * Lightintensity: gl_FragColor = vec4 (color, 1.0): 13.4. Интерполяция ключевых кадров Рассмотрим еще один интересный эффект анимации — постепенный переход меж- ду двумя объектами. Его можно применять также для перехода от одного эффек- та к другому за определенное количество кадров. Вместо того чтобы выполнять в приложении сложные вычисления перехода между объектами, можно делать это автоматически с помощью шейдера. Этим способом можно задавать переход либо между разными формами объек- та, либо между цветами, текстурами, процедурными рисунками и т. д. Все, что для этого нужно, — создать шейдер, который будет получать значение времени (или смены кадров). В некоторых случаях бывает достаточно линейного перехо- да. А для получения эффекта колебаний приложение должно вычислять коэффи- циент интерполяции с помощью функции сплайна, чтобы избежать прерывисто- сти анимации. (Можно, конечно, делать эти вычисления и в шейдере, но лучше, чтобы приложение выполняло вычисления один раз для каждого кадра, чем для каждой вершины или каждого фрагмента, как было бы в шейдере.)
13.4. Интерполяция ключевых кадров 277 Через дополнительные атрибуты вершин можно передавать в шейдер координа- ты точек не одного, а двух объектов. Координаты первого объекта будут передавать- ся как обычно (gl Vertex, gl Col or, gl Normal и т. д.). А координаты второго объекта будут передаваться через дополнительные атрибуты вершин 0, 1, 2 и т. д. Прило- жение может также передавать через uniform-переменную коэффициент сглажи- вания, а вершинный шейдер использует его для вычисления среднего значения из двух наборов параметров вершин. Вычисленные координаты вершины в даль- нейшем передаются для преобразования, а вычисленная средняя нормаль исполь- зуется при вычислении освещения. Чтобы переход выглядел реалистично, нужно выбрать правильное количество ключевых кадров, а также промежуточных точек на переходе между объектами. В [8, с. 64—65] этот принцип описывается как «распределение интервалов време- ни» (тайминг); принципы его работы объясняются так: «Пусть заданы два ключе- вых кадра рисунка головы, в первом голова повернута к правому плечу, а во вто- ром — к левому и подбородок слегка приподнят. Можно создать множество разных эффектов, зависящих только от количества промежуточных кадров. Каждый до- полнительный кадр придает движению новое значение. Нет промежуточных кадров Один промежуточный кадр Два промежуточных кадра Три промежуточных кадра Четыре промежуточных кадра Пять промежуточных кадров Шесть промежуточных кадров Семь промежуточных кадров Восемь промежуточных кадров Девять промежуточных кадров Десять промежуточных кадров Персонаж ударили с огромной силой, и у него чуть не оторвалась голова Персонаж ударили кирпичом, скалкой или сковородкой У персонажа нервный тик, мышечный спазм или судорога Персонаж уклоняется от кирпича, скалки, сковородки Персонаж дает резкий приказ («Начинай!») Жест персонажа более дружественный («Поторопись!») Персонаж видит хорошенькую девушку или спортивную машину, о которой мечтает Персонаж рассматривает какой-либо предмет Персонаж ищет ореховую скорлупку на кухонной полке Персонаж вдумчиво оценивает какой-либо предмет У персонажа при повороте головы растягивается больная мышца .3.4.1. Вершинный шейдер с интерполяцией лючевых кадров Рассматриваемый здесь вершинный шейдер (листинг 13.2) очень похож на шей- дер, представленный ранее, за исключением двух моментов: 1) второго набора вершинных данных; 2) вычисления промежуточного значения между двумя на- борами вершинных данных, позволяющего получить именно те данные, которые потом будут преобразовываться и использоваться в дальнейших вычислениях. Листинг 13.2. Вершинный шейдер для интерполяции между двумя объектами varying float Lightintensity: varying vec2 TexCoord: продолжение
278 Глава 13. Анимированные шейдеры Листинг 13.2 {продолжение) uniform vec3 LightPosition: uniform float SpecularContribution: uniform float DiffuseContribution; uniform float Weight: // обновляется приложением для каждого кадра attribute vec4 VertexB: attribute vec3 NormalB: void main(void) { vec4 vert = mix(gl_Vertex. VertexB. Weight): vec3 ecPosition = vec3 (gl_ModelViewMatrix * vert): vec3 tnorm = mix(gl_Normal. NormalB. Weight): tnorm = normalize(gl_NormalMatrix * tnorm): vec3 lightVec = normalizetLightPosition - ecPosition): vec3 reflectVec = reflect!-1ightVec. tnorm): vec3 viewVec = normalize(-ecPosition): float spec spec = max(dot(reflectVec. viewVec). 0.0): = powfspec, 16.0): Lightintensity = DiffuseContribution * maxtdottlightVec. tnorm), 0.0) + SpecularContribution * spec: TexCoord gl_Position = gl_MultiTexCoord0.st: = gl_ModelViewProjectionMatrix * vert: 13.5. Другие эффекты перехода Еще одним эффектом перехода является постепенное исчезновение объекта. В этом случае управляющее значение будет значением прозрачности объекта. Объект может быть нарисован полностью непрозрачным (значение прозрачнос- ти 1,0), полностью прозрачным (прозрачность 0) и полупрозрачным (значение прозрачности между 0 и 1,0). Можно также скрыть части объекта директивой discard. Для этого в шейдере полосок, описанном в разделе 11.3, при перерисовке объекта отбрасывается опре- деленное количество пикселов. Коэффициент отбрасывания пикселов может плав- но меняться от 0 до 1,0 при появлении объекта и от 1,0 до 0 при его исчезновении. Вместо такого коэффициента можно использовать значение функции шума для каждой точки поверхности. Таким образом можно создать эффект разъедания или ржавления. 13.6. Системы частиц В начале 80-х гг. Билл Ривз и его коллеги изобрели новый способ рендеринга при- митивов. Традиционные методы рендеринга больше подходят для рисования ров- ных, четко определенных поверхностей. Ривз придумал способ рендеринга объек-
13.6. Системы частиц 279 тов, называемых нечеткими — огня, дыма, распыленной жидкости, хвостов комет, фейерверков и других природных явлений. Эти явления нечеткие, так как не имеют определенных границ, а вид объектов меняется с течением времени. Способ, которым Ривз решил проблему, описан в статье [5]. Системы частиц и раньше использовались при рендеринге, но по-другому. Ривз стал задавать каж- дой частице набор начальных условий и устанавливать набор вероятностных пра- вил поведения частиц с течением времени. Существует три основных отличия метода создания системы частиц от тради- ционного метода рисования поверхности объекта. 1. Объект представлен скоплением частиц, определяющих объем, а не многоуголь- ников или кривых, определяющих поверхность. 2. Объект считается динамическим, а не статическим. Частицы возникают, ме- няются и пропадают, а за время своего существования могут менять располо- жение и вид. 3. Объекты определены не совсем четко, то есть задан только набор начальных условий и правила возникновения, изменения и исчезновения частиц. На всех этапах существования частиц на них влияют вероятностные процес- сы, поэтому форма и вид объекта в каждый момент времени точно не определены. Чтобы упростить рендеринг систем частиц, обычно делают следующие пред- положения: □ Частицы не могут перекрываться. □ Частицы не отражают свет, а излучают его. □ Частицы не отбрасывают теней на другие частицы. В набор атрибутов частиц обычно входят координаты, цвет, прозрачность, ско- рость, размер, форма и время жизни. При рендеринге системы частиц атрибуты каждой частицы сопоставляются с некоторыми общими параметрами, что позво- ляет правильно обновлять координаты частиц и их вид в каждом кадре. Все коор- динаты частиц должны обновляться в зависимости от начального вектора движе- ния, влияния гравитации, ветра, трения и других факторов. Цвет каждой частицы (включая прозрачность), их размер и форма могут меняться как функция от вре- мени, а время жизни частицы, ее высоту, скорость и любые другие параметры можно вычислить. Преимущества данной технологии рендеринга в том, что с ее помощью можно создавать довольно сложные системы с минимальными усилиями. С другой сто- роны, их можно усложнять или, наоборот, упрощать. Билл Ривз в статье [8] отме- чает: «Самое важное в системах частиц — то, что они меняются: такое поведение делает объекты очень реалистичными». 13.6.1. Настройка приложения Шейдер в следующем примере работает, как хлопушка, начиненная конфетти, — откуда-то появляется множество маленьких ярких кусочков бумаги. Они появ- ляются не все сразу, а постепенно. Их начальные скорости случайны, но установ- лено общее направление от начальной точки. На частицы будет влиять гравита- ция, и в конце концов все они опустятся вниз.
280 Глава 13. Анимированные шейдеры Код, приведенный в листинге 13.3, представляет собой программу на языке С, со- здающую начальные значения для нашей системы частиц. Для каждой частицы за- даются начальные координаты, выбранный случайным образом цвет, случайное (с некоторыми ограничениями) значение скорости и случайное время появления. Функция createPoints позволяет создавать двухмерную сетку из точек произ- вольного размера. В принципе, в сетке нет необходимости, но эффект разлета ча- стиц из сетки, как кусочков поп-корна, выглядит интересно. Проще всего опреде- лить систему частиц в одномерном массиве, тогда все координаты вершин будут иметь одинаковое начальное значение (например, (0, 0, 0)). Однако в этом шейдере частицы определены в двухмерном массиве, размеры которого и определяют количество создаваемых частиц. После выделения памя- ти под массивы во вложенном цикле вычисляются значения для каждого из атри- бутов частицы в каждой ячейке сетки. В каждом наборе координат г/-компонент будет равен 0, а х- и 2-компоненты будут различными для каждой новой ячейки. Каждое значение цвета определяется случайным образом из диапазона [0,5, 1,0] — для того чтобы цвета были светлыми, пастельными. Векторы направления дви- жения тоже случайны, но имеют строгий уклон вниз — для этого иху-координата умножается на 10. В общем движение частиц направлено в сторону добавлением числа 3 к х- и 2-координатам. И наконец, каждой частице назначается начальное значение времени из диапазона [0, 10]. Листинг 13.3. Программа на языке С для создания начальных данных частиц static GLint arraywidth, arrayHeight; static GLfloat *verts = NULL: static GLfloat *colors = NULL: static GLfloat Velocities = NULL: static GLfloat *startTimes = NULL: void createPoints(GLint w. GLint h) GLfloat *vptr, *cptr. *velptr, *stptr: GLfloat i. j: if (verts != NULL) free(verts): verts = malloc(w * h * 3 * sizeof(float)): colors = malloctw * h * 3 * sizeof(f1 oat)): velocities = malloc(w * h * 3 * sizeof(fl oat)); startTimes = malloc(w * h * sizeof(float)): vptr = verts: cptr = colors: velptr = velocities: stptr = startTimes: for (i = 0.5 / w - 0.5: i < 0.5: i = i + 1.0/w) for (j = 0.5 / h - 0.5: j < 0.5: j = j + 1.0/h) { vptr = i: (vptr + 1) = 0.0: (vptr + 2) = j:
13.6. Системы частиц 281 vptr += 3: * cptr = ((float) rand О / RAND_MAX) * 0.5 + 0.5: * (cptr + 1) = ((float) randO / RAND_MAX) * 0.5 + 0.5: * (cptr + 2) = ((float) randO / RAND_MAX) * 0.5 + 0.5: cptr += 3: * velptr = (((float) randO / RAND_MAX)) +3.0: * (velptr + 1) = ((float) randO / RAND_MAX) * 10.0: * (velptr + 2) = (((float) randO / RAND_MAX)) + 3.0: velptr += 3: * stptr = ((float) randO / RAND_MAX) * 10.0: stptr++: } arrayWidth = w: arrayHeight = h: У OpenGL есть встроенные атрибуты координат и цвета вершины, которые и будут использоваться для передачи координат и цвета частицы. Начальную ско- рость и начальное время придется передавать через дополнительные атрибуты. Определим необходимые для этого константы: #define VELOCITY_ARRAY 3 #define START_TIME_ARRAY 4 После создания программного объекта необходимо связать индексы дополни- тельных атрибутов с соответствующими именами переменных. (Это можно сделать даже до того, как вершинный шейдер связывается с программным объектом.) Все эти связи будут проверены и вступят в силу во время вызова функции glLink- ProgramARB. Чтобы связать индекс дополнительного атрибута с переменной вер- шинного шейдера, сделаем следующее: glBindAttribLocationARB(ProgramObject. VELOCITY_ARRAY. "Velocity"): glBindAttribLocationARB(ProgramObject, START_TIME_ARRAY, "StartTime"): После того как шейдеры скомпилированы, связаны с программным объектом и скомпонованы, можно рисовать систему частиц. Все, что для этого нужно сде- лать, — вызвать функцию drawPoi nts, приведенную в листинге 13.4. В этой функции размер точки устанавливается в 2, чтобы выполнять рендеринг немного больших точек. Следующие четыре строки кода устанавливают указатели на используемые массивы вершин. В данном шейдере используются четыре массива: для коорди- нат вершин (начальные координаты частиц), для цвета частиц, для начальной скорости и начального времени (времени рождения) частиц. После этого вызыва- ются функции gl ЕпаЫ еС1 т ent St a te для стандартных атрибутов вершин и gl ЕпаЫ е- VertexAttri bArrayARB — для дополнительных атрибутов вершин, чтобы массивы стали доступными. Потом вызывается функция glDrawArrays для рендеринга то- чек, и, наконец, доступ к массивам закрывается. Листинг 13.4. Программа на языке С для рисования частиц как точек void drawPointsO glPointSize(2.0): glVertexPointer(3. GL_FLOAT, 0, verts): . продолжение
282 Глава 13. Анимированные шейдеры Листинг 13.4 {продолжение) glColorPointer (3. GL_FLOAT. 0. colors): glVertex4ftribPointerARB(VEL0CITY_ARRAY, 3. GL_FLOAT. GL_FALSE. 0. velocities); glVertexAttribPointerARB(START_TIME-ARRAY. 1, GL_FLOAT. GL_FALSE. 0. startTimes); glEnableClientState(GL_VERTEX_ARRAY): glEnableClientState(GL_C0L0R_ARRAY); glEnableVertexAttribArrayARB(VELOCITY_ARRAY); glEnableVertexAttribArrayARB(START_TIME_ARRAY); glDrawArrays(GL_POINTS. 0. arrayWidth * arrayHeight): glDisableClientState(GL_VERTEX_ARRAY): gl DisableClientState(GL_COLOR_ARRAY): glDisableVertexAttribArrayARB(VELOCITY_ARRAY): glDisableVertexAttribArrayARB(START_TIME_ARRAY): } Чтобы получить эффект анимации, приложение должно передавать свое значе- ние времени вершинному шейдеру (листинг 13.5). Переменная Раrticl eTime увели- чивается на единицу при смене кадров, и это значение заносится в uniform-пере- менную Time. Это позволяет вершинному шейдеру учитывать значение времени в вычислениях. Листинг 13.5. Фрагмент кода на языке С для обновления переменной с новым кадром if (DoingPartiс 1es) { location = glGetUniformLocationARB(ProgramObject. "Time"): ParticleTime += O.OOlf; glUm formlfARB( location, ParticleTime): CheckOgl ErrorO: } 13.6.2 . Вершинный шейдер хлопушки с конфетти Вершинный шейдер (листинг 13.6) выполняет основную работу в примере рен- деринга методом создания систем частиц. Вместо того чтобы просто преобразо- вать входные вершинные координаты, шейдер считает эти координаты началь- ными для вычисления новых координат с учетом значения uniform-переменной Time. И уже эти новые координаты будут преобразовываться и передаваться для рендеринга. В вершинном шейдере определены attribute-переменные Velocity и StartTime (определение массивов дополнительных атрибутов вершин и связывание их с со- ответствующими переменными шейдера описано в предыдущем разделе). Поэто- му у шейдера всегда будут обновленные значения переменных Vel ocity, StartTime, а также стандартных вершинных атрибутов gl-Vertex и gl _Col or. Вершинный шейдер сначала вычисляет время жизни частицы. Если это значе- ние меньше нуля, частица еще не создана, и тогда ей просто присваивается цвет фона через uniform-переменную Background. (Можно показать еще не «родившие- ся» частицы, присвоив им их собственный цвет. Можно также передавать значе-
13.6. Системы частиц 283 ние t во фрагментный шейдер как varying-переменную, чтобы затем отбрасывать фрагменты с С меньшим 0. Но здесь этого делать не обязательно.) Если стартовое время частицы меньше, чем текущее время, для определения текущих координат частицы используется следующее кинематическое уравнение: 1 ? Р - P +vt + —at1- ‘ 2 В этом уравнении Р, — начальные координаты частицы, v — начальная скорость, t- время движения частицы, а — ускорение, Р — окончательные координаты ча- стицы. Значение ускорения — обычное ускорение земного притяжения, 9,8 м/с. В нашей упрощенной модели считается, что гравитация влияет только на г/-коор- динату частицы, а ускорение негативно (то есть скорость частицы постепенно уменьшается при падении на землю). Поэтому коэффициент а для t2 в уравнении будет константой, равной -4,9, и применяться будет только для «/-координаты. Осталось преобразовать координаты и сохранить результат gl-Position. Листинг 13.6. Вершинный шейдер хлопушки с конфетти, использующий метод систем частиц uniform float Time; uniform vec4 Background; // обновляется приложением с каждым кадром // постоянный цвет фона attribute vec3 Velocity: // начальная скорость attribute float StartTime; // время активации частицы varying vec4 Color; void main(void) { vec4 vert: float t = Time - StartTime: if (t >= 0.0) { vert = gl_Vertex + vec4 (Velocity * t. 0.0): vert.у -= 4.9 * t * t; Color = gl_Color: } else { vert = gl_Vertex: // начальное положение Color = Background; // цвет частицы до "рождения" } gl_Position = gl_ModelViewProjectionMatrix * vert: } 13.6.3 . Фрагментный шейдер хлопушки : конфетти Фрагментный шейдер для создания изображений, приведенных на цветном рис. 24 (листинг 13.7), здесь описываться не будет, что даст читателю возможность поуп- ражняться в понимании кода шейдеров.
284 Глава 13. Анимированные шейдеры Листинг 13.7. Фрагментный шейдер хлопушки с конфетти, использующий метод систем частиц varying vec4 Color; void main (void) { gl_FragColor = Color; } 13.6.4 . Улучшение шейдеров Рассматриваемый шейдер можно сделать и более интересным. Передавая значе- ние t из вершинного шейдера во фрагментный, можно менять цвет частицы с те- чением времени. Например, чтобы имитировать вспышку, цвет нужно изменить с желтого до красного и затем с красного до черного. При изменении значения прозрачности тоже получается интересный эффект — постепенное появление или исчезновение частицы. Можно задавать время, за которое частица полностью исчезает, или расстоя- ние от исходной точки до точки, в которой частица начинает исчезать. Также вме- сто рисования частиц в виде точек их можно рисовать короткими линиями. Это даст возможность применить к каждой частице эффект размытости. Размер точ- ки или линии тоже может меняться с течением времени, и частицы будут расти или сжиматься. Модель можно сделать намного более сложной, чем описанная здесь. Чтобы частицы лучше выглядели, их можно рисовать как четырехугольни- ки с текстурой, повернутые лицевой стороной к зрителю. (Существует расшире- ние ARB для точечных спрайтов1, которое тоже можно применять.) Настоящая прелесть работы с системами частиц из шейдера заключается в том, что вычисления выполняются в основном на графическом акселераторе, а не на основном процессоре. Если данные для создания системы частиц хранятся в бу- ферном объекте, скорее всего, они будут храниться в памяти графического уско- рителя, и для рендеринга даже не будет использоваться шина передачи данных. Однако уравнение для обновления каждой частицы, написанное на языке шейде- ров OpenGL, может оказаться довольно сложным. Так как рендеринг системы частиц выполняется так же, как рендеринг любого другого трехмерного объекта, то во время анимации можно поворачивать каждую частицу как угодно. Исполь- зуя метод систем частиц, можно применять любое количество эффектов. 13.7. Колебания В двух предыдущих примерах анимация объекта выполнялась в основном вершин- ным процессором (так как форма объекта не может изменяться фрагментным про- цессором). Но фрагментный процессор также можно использовать для создания эффектов анимации. Основное назначение большинства фрагментных шейдеров — вычислять цвет фрагмента, и любые коэффициенты, влияющие на цвет, могут зави- 1 Небольшое изображение, переносимое по экрану независимо от других. — Примеч. науч. ред.
13.7. Колебания 285 сеть от времени. В этом разделе будет представлен шейдер, который вносит завися- щие от времени возмущения в текстурные координаты, создающие эффект колеба- ний или возмущений. Если выбрана правильная текстура, с помощью этого эффекта можно создавать «желатиновую» поверхность или «танцующий» логотип. Этот шейдер был разработан для того, чтобы воспроизвести двухмерные коле- бательные эффекты, показанные в некоторых демонстрационных программах ре- ального времени на веб-сайте http://www.scene.org. Автор этих программ, Анто- нио Тихада из компании 3Dlabs, хотел создать нечто подобное с помощью языка шейдеров OpenGL. Для создания возмущений, вносимых фрагментным шейдером в текстурные координаты перед доступом к текстуре, используется функция синуса. Размеры и частота возмущений контролируются с помощью uniform-переменных, устанав- ливаемых приложением. В данном случае точность вычисления функции синуса не имеет особого значения, а сама функция синуса на время написания шейдера еще не была реализована, поэтому Антонио применил первые два члена последо- вательности Тейлора для синуса. Фрагментный шейдер был бы проще, если бы Антонио использовал встроенную функцию sin, но способ с последовательнос- тью демонстрирует применение численных методов в шейдерах. (Сложно сказать, будет ли использование первых двух членов последовательности Тейлора рабо- тать быстрее встроенной функции sin. Возможно, это зависит от производителя аппаратного обеспечения, а именно от реализации функции sin.) Чтобы шейдер правильно работал, приложение должно задавать частоту и ам- плитуду колебаний и координаты источника освещения. Нужно также при смене кадра увеличивать uniform-переменную StartRad. Это значение будет использо- ваться для вычисления возмущений во фрагментном шейдере. Если увеличивать его с каждым кадром, получится движение возмущений. Приложение должно пе- редавать координаты вершины, нормаль поверхности и текстурные координаты для каждой вершины объекта. Вершинный шейдер для создания эффекта возмущений должен вычислять освещение на основании нормали поверхности и координат освещения. Текстур- ные координаты передаются без изменений. Вершинный шейдер для создания изображения Земли, описанный в разделе 10.2.2, полностью повторяет эти вы- числения, так что можно использовать его. Фрагментный шейдер для создания эффекта возмущений приведен в листин- ге 13.8. В него передается varying-переменная Light Intensity, вычисленная вершин- ным шейдером. Эта переменная будет использоваться в конце процедуры для нало- жения на фрагмент освещения. Uniform-переменная StartRad содержит начальную точку для вычисления возмущений в радианах, приложение увеличивает ее на еди- ницу с каждым новым кадром, чтобы эффект возмущений был анимационным. Изображение будет двигаться быстрее, если всего лишь увеличивать эту перемен- ную на большее значение, и медленнее, если увеличивать переменную на меньшее значение. Экспериментальным путем установлено, что наилучшее значение — 1°. Частота и амплитуда колебаний устанавливаются приложением через uniform- переменные Freq и Ampl itude. Они определены как переменные типа vec2, и их ком- поненты х и у можно изменять по отдельности. Во фрагментном шейдере для обо- значения текстурного модуля, в котором хранится нужная текстура, определена переменная Wobbl eTex.
286 Глава 13. Анимированные шейдеры Чтобы аппроксимация последовательности Тейлора для синуса давала более точные результаты, следует убедиться в том, что значение, для которого вычисля- ется синус, находится в диапазоне [—тг/2, л/2]. Для упрощения в шейдере опреде- лены константы С_Р1 (л), С_2РКтг), С_2Р1_1(1/2л) и С_Р1_2(тг/2). В первой части фрагментного шейдера вычисляется коэффициент возмуще- ния для направления х. Необходимо, чтобы коэффициент возмущений зависел от $- и Г-компонентов текстурных координат. Для этого вычисляется локальная пе- ременная rad, которая линейно зависит от значений s и t. (Похожее, но немного другое выражение будет вычислять коэффициент возмущения для направления у во второй половине шейдера.) К результату добавляется значение St a rt Rad. И на- конец, по х-компоненту Freq результат масштабируется. Значение rad будет увеличиваться с увеличением St a rt Rad, а частота возмуще- ний увеличивается с ростом коэффициента масштабирования Freq. х. Коэффици- ент масштабирования должен увеличиваться при увеличении размера текстуры на экране, чтобы видимая частота возмущений не зависела от масштаба. Можно представлять uniform-переменную Freq как шкалу Рихтера для возмущений. Если ее значение будет равно 0, то возмущений вообще не будет. Значение 1,0 вызовет легкое покачивание, 2,0 — тряску, 4,0 — биение, а 8,0 — эффект, похожий на зем- летрясение. Следующие семь строк кода приводят значение rad к диапазону [-л/2, тг/2]. После этого можно вычислять sin (rad) с помощью первых двух членов последо- вательности Тейлора для синуса, это всего лишь выражение х - х‘'/3!. Результат вычисления умножается нах-компонент переменной Amplitude. Значение вычис- ленного синуса будет находиться в диапазоне [-1, 1]. Если просто прибавить это значение к текстурной координате как коэффициент возмущения, координата сильно изменится. Но в данном случае нужно колебание, а не эффект взрыва, по- этому вычисленное значение синуса умножается на 0,05, в результате чего полу- чаются возмущения небольшого размера. Увеличивая этот коэффициент, можно задавать возмущения большего размера, а уменьшая — меньшего размера. Пред- ставить себе действие коэффициента можно так: он определяет, как далеко «ухо- дит» текстурная координата от первоначального значения. Значение коэффици- ента 0,05 означает, что текстурная координата изменится не больше, чем на ±0,05. Потом все эти вычисления повторяются для коэффициента возмущений в на- правлении у. Вычисления также линейно зависят от значений текстурных коор- динат $ и t, но проявляется зависимость немного иначе, что позволяет избежать симметрии в разных направлениях. После вычисления коэффициентов возмущения можно обращаться к тексту- ре. Для получения окончательного цвета фрагмента значение цвета, полученное из текстурной карты, умножается на Lightintensity. Несколько кадров из такой анимации приведены на цветном рис. 21. Эти кадры показывают, как шейдер ра- ботает с неподвижным логотипом. Этот эффект интересно применять также на поверхности воды, лавы, тины или даже шкуре животного или чудовища. Листинг 13.8. Фрагментный шейдер для создания эффекта возмущений // Константы const float C_PI = 3.1415: const float C_2PI =2.0* C_PI: const float C 2PI I = 1.0 / (2.0 * C PI):
13.7. Колебания 287 const float C_PI_2 = C_PI /2.0: varying float Lightintensity: uniform float StartRad: uniform vec2 Freq: uniform vec2 Amplitude: uniform sampler2D WobbleTex: void main (void) { vec2 perturb: float rad: vec3 color: // Вычисление коэффициента возмущений для направления х rad = (gl_TexCoord[0]. s + gl_TexGoord[0].t - 1.0 + StartRad) * Freq.x: // Приведение к диапазону -2.0*PI. 2*PI rad = rad * C_2PI_I: rad = fract(rad): rad = rad * C_2PI: // -PI. PI if (rad > C_PI) rad = rad - C_2PI: if (rad < -C_PI) rad = rad + C_2PI // -PI/2. PI/2 if (rad > C_PI_2) rad = C_PI - rad; if (rad < -C PI 2) rad = -C PI - rad perturb.x = (rad - (rad * rad * rad / 6.0)) * Amplitude.x: // Вычисление коэффициента возмущений для направления у rad = (gl_TexCoord[0].s - gl_TexCoord[0].t + StartRad) * Freq.y: // Приведение к диапазону -2*PI. 2*PI rad = rad * C_2PI_I: rad = fract(rad): rad = rad * C_2PI: // -PI. PI if (rad > C_PI) rad = rad - C_2PI: if (rad < -C_PI) rad = rad + C_2PI: // -PI/2. PI/2 if (rad > C_PI_2) rad = C_PI - rad: if (rad < -C_PI_2) rad = -C_PI - rad: perturb.y = (rad - (rad * rad * rad / 6.0)) * Amplitude.y: color = vec3 (texture2D(WobbleTex. perturb + gl_TexCoord[0].st)); gl_FragColor = vec4 (color * Lightintensity, 1.0):
288 Глава 13. Анимированные шейдеры 13.8. Итоги В прежних версиях OpenGL эффекты анимации должны были вычисляться только приложением и занимать ресурсы центрального процессора. С введением про- граммируемости эту работу можно переложить на графический ускоритель и вы- полнять любые вычисления в шейдере. От текущего времени могут зависеть ко- ординаты, форма, текстурные координаты, освещение объекта и многие другие параметры. При разработке шейдера для движущегося объекта следует часть эффекта ани- мации вычислять в шейдере. Это разгружает главный процессор и упрощает код приложения. В этой главе были описаны некоторые простые способы анимации объектов с помощью шейдеров. Интерполяция ключевых кадров довольно про- ста. Частицы также можно двигать, изменяя их координаты, цвет, скорость и лю- бые другие параметры. Объекты и текстуры могут вибрировать, двигаться, расти или меняться по сложным математическим формулам. Анимация — мощный инструмент, способный донести до зрителя любую ин- формацию, а язык шейдеров OpenGL предоставляет новый эффективный способ анимации объектов. 13.9. Ссылки Для читателей, особо интересующихся анимационными эффектами, будет полез- на книга [7]. Авторы книги обнаруживают глубокое понимание принципов рабо- ты с анимацией, происходящей на студии Диснея, в ней много цветных иллюст- раций. Часть материалов этой книги была опубликована в докладе [3]. В книге [4] описано множество алгоритмов для компьютерной анимации. Книга [1] также содержит несколько разделов об анимации. Системы частиц впервые были представлены Биллом Ривзом в докладе [5]. Джефф Ландер написал хорошее руководство по системам частиц [2]. Он также выложил несколько примеров исходного кода — демонстрационные программы создания систем частиц на языке шейдеров OpenGL. 1. DeLoura М. Game Programming Gems. Hingham, MS: Charles River Media, 2000. 2. Lander J. The Ocean Spray in Your Face//Game Developer Magazine. 1998. Vol. 5, № 7. P. 13-19 (http://www.darwin3d.com/gdml998.htm). 3. Lasseter J. Principles of Traditional Animation Applied to 3D Computer Animation// Computer Graphics (Proc. SIGGRAPH-87). 1987. July. P. 35-44. 4. Parent R. Computer Animation: Algorithms and Techniques. San Francisco: Morgan Kaufmann Publishers, 2001 (http://www.cis.ohio-state.edu/~parent/ book/outline.html). 5. Reeves W. T. Particle Systems — A Technique for Modeling a Class of Fuzzy Objects//ACM Transactions on Graphics. 1983. Vol. 2, № 2. P. 91-108.
13.9. Ссылки 289 6. Reeves W. T., Blau R. Approximate and Probabilistic Algorithms for Shading and Rendering Structured Particle Systems//Computer Graphics (Proc. SIGGRAPH- 85). 1985. July. P. 313-322. 7. Thomas F., Johnston O. Disney Animation — The Illusion of Life. New York: Abbeville Press, 1981. 8. Thomas F., Johnston O. The Illusion of Life — Disney Animation. Revised Ed. Hyperion, 1995. 9. Watt A. H., Watt M. Advanced Animation and Rendering Techniques: Theory and Practice. Reading, MS: Addison-Wesley, 1992. IO Зак. 218
Сглаживание процедурных текстур Неровности, дефекты изображения, отблески, «ступеньки», стробоскопический эффект и «бегущие муравьи» — все это проявления основного проклятия компь- ютерной графики — алиасинга1. Любой пользователь часто видит такие дефекты изображения. В неподвижных изображениях алиасинг сложнее заметить, и он редко причиняет неудобства. Но при движении объекта зубчатый край бросается в глаза, отвлекая внимание от всего остального. С самого рождения компьютер- ной графики прилагаются колоссальные усилия для устранения этой проблемы, что выражается в разработке программ сглаживания (антиалиасинга). В этой главе не описаны в подробностях все причины возникновения алиасин- га и методы борьбы с ним. Здесь рассматриваются ситуации, при которых мо- жет возникнуть алиасинг, и средства языка шейдеров OpenGL для борьбы с ним. Зная все это, можно с легкостью избавляться от алиасинга в собственных шей- дерах. 14.1. Причины алиасинга Человеческий глаз устроен так, что хорошо различает края объектов на изобра- жении. Благодаря этому свойству мы оцениваем форму и представление объекта, распознаем буквы и слова, и при этом зрение работает очень хорошо, так как по- стоянно упражняется. Компьютерный экран довольно сильно ограничен и состоит из конечного числа дискретных элементов (пикселов). В каждый момент времени пиксел может быть только одного цвета. Поэтому на экране невозможно точно показывать части изоб- ражения, размер которых меньше экранного пиксела, — например, края объекта. При сочетании этих двух свойств — способности глаза различать края и невоз- можности правильного представления края на экране — и возникает проблема алиасинга. Вообще-то алиасинг появляется при воспроизведении сигнала с недо- статочной частотой дискретизации. На компьютерном экране, где есть ограниче- ние по частоте (пикселы), практически всегда частота будет недостаточной для отображения некоторых частей изображения, поэтому алиасинг на экране будет всегда, выраженный в большей или меньшей степени. Его можно сделать неза- метным или превратить в какую-либо менее заметную проблему, например в раз- мытость или шум. 1 Зубцеобразный дефект, неровность, ступенчатость края изображения. — Примеч. перев.
14.2. Избежание алиасинга 291 На диаграмме (рис. 14.1) показаны результаты попытки нарисовать серый объект. Предполагаемая форма объекта показана на рис. 14.1, а. Из-за ограничения компьютерного экрана на объект накладывается сетка, каждая ячейка которой может быть окрашена только в один цвет (точечная дискретизация). Обозначим два разных цвета кружочками (рис. 14.1, б). В результате получатся некрасивые зубцы (рис. 14.1, в). (Рисунок немного упрощен, так как обычный CRT-монитор не показывает пикселы правильными квадратиками, но дефекты изображения видны и в том случае, если пикселы показаны перекрывающимися кругами.) □□ □□иояива □□□□□□□□ □□□□□□□□ Рис. 14.1. Дефекты изображения (алиасинг): а — серой областью обозначена форма объекта; б— изображение на экране показано в виде пикселов, на объект наложена сетка; в — результат наложения сетки показан в виде зубцов Алиасинг вызывают и другие причины. Если создается анимация и объекты неправильно дискретизированы в движении, можно говорить о временном алиа- синге. Так получается, если движение объекта слишком быстрое для выбранной частоты дискретизации. Объекты могут дергаться или мигать. Классический при- мер временного алиасинга — движущийся вперед автомобиль (карета, фургон, велосипед), колеса которого крутятся назад. Это происходит потому, что частота кадров (количество в секунду) слишком низкая по сравнению со скоростью вра- щения колес. В реальности колесо может делать пол-оборота или три четверти оборота вперед за один кадр, но на экране это будет выглядеть так, как будто ко- лесо повернулось на четверть оборота назад. Чтобы выполнять рендеринг изображений более реалистично, требуется раз- работать способы избежания алиасинга на компьютерном экране. 4.2. Избежание алиасинга Один из способов добиться хорошего качества изображения без алиасинга — из- бегать ситуаций, при которых алиасинг возникает. Например, если известно, что какой-либо объект в окончательном изображе- нии всегда будет одного и того же размера, можно создать шейдер, который будет хорошо рисовать объект именно этого размера. Такой способ был описан ранее в этой книге при демонстрации некоторых шейдеров. Функции smoothstep, mix и cl amp особенно полезны, так как позволяют избавиться от резких переходов и по- могают показывать процедурную текстуру корректно при любом масштабе. Алиасинг зачастую возникает, если рендеринг объекта выполняется для разных его размеров. Множественные текстуры придумали именно для этого, и нечто подобное можно сделать и в шейдерах. Если точно известно, что на изображении объект будет появляться с разными размерами, можно создать отдельный шейдер
292 Глава 14. Сглаживание процедурных текстур для каждого размера. Каждый из этих шейдеров будет рисовать объект с правиль- ным уровнем детализации, что позволит избежать алиасинга. Чтобы этот способ работал, приложение должно знать приблизительный размер объекта еще до на- чала рисования и в соответствии с этим устанавливать нужный шейдер. Даже если размеры объекта на анимации плавно увеличиваются или уменьшаются, некото- рые погрешности при смене уровня детализации все равно возникнут. В некоторых случаях алиасинга можно избежать, используя текстуру вместо процедурных вычислений (см. главу 11). Это позволяет использовать возможно- сти антиалиасинга, заложенные в текстурные аппаратные модули. 14.3. Увеличение разрешения Эффект алиасинга можно уменьшить с помощью довольно грубого метода, называ- емого супердискретизацией, при котором анализируются несколько значений цве- та для каждой ячейки наложенной сетки (пиксела), а потом выбирается среднее значение. Метод используется в современных графических ускорителях в буфере мультисемплинга. Этот метод сглаживания заменяет семплинг одной точки сем- плингом нескольких, поэтому некоторый алиасинг все равно остается, но он на- много менее заметен. В принципе, если шейдеры используются совместно с буфе- ром мультисемплинга, то оставшийся небольшой алиасинг можно игнорировать. Так как этот способ незначительно использует ресурсы графического ускори- теля (например, память), он может работать медленнее, чем применение анти- алиасинга в процедурной текстуре. Мы не полностью избавляемся от алиасинга, но проявляется он уже на более высокой частоте. Описанная процедура иллюстрируется рис. 14.2. Для каждого из пикселов цвет определяется четыре раза, затем вычисляется среднее значение. Конечно, рису- нок стал выглядеть лучше, но еще недостаточно хорошо, так как мелкие детали все равно не видны. Рис. 14.2. Супердискретизация с четырьмя пробами цвета на пиксел дает хорошие результаты, но полностью от алиасинга не избавляет: a — форма объекта для рендеринга; б— семплинг выполняется четыре раза для одного пиксела; в — средний результат. Некоторые пикселы, на которые объект попал только одним семплом, считаются имеющими цвет фона Суперсемплинг также можно реализовать во фрагментном шейдере. Код для вычисления цвета фрагмента можно написать как функцию, которая вызывается несколько раз из главной функции и получает цвет нескольких мест фрагмента. Затем вычисляется окончательный цвет фрагмента как среднее значение. Недо- статок такого метода — вызов функции Npa3, где N — количество семплов, вы- числяемых для каждого фрагмента.
14.4. Пример сглаживания полосок 293 Иногда случается, что алиасинга избежать невозможно, а суперсемплинг не- допустим. Если нужно выполнять процедурное текстурирование и для этого должен использоваться единственный шейдер, тут мало что можно сделать, но лучше постараться написать шейдеры таким образом, чтобы сглаживание все же выполнялось. ,4.4. Пример сглаживания полосок Алиасинг возникает при попытке представить непрерывное изображение на эк- ране. Это происходит при растеризации, и попытки смягчить дефекты выполня- ются во фрагментном шейдере. В языке шейдеров OpenGL для этого есть несколько функций, доступных только из фрагментного шейдера. Чтобы продемонстриро- вать в этом примере несколько приемов борьбы с алиасингом, придумаем наихуд- ший вариант — чередование черных и белых полос на сфере. Разработка фраг- ментного шейдера, выполняющего сглаживание, поможет проиллюстрировать проблему алиасинга и методы борьбы с ним. 14.4.1. Рисование полос Фрагментный шейдер для сглаживания будет определять цвет фрагмента (чер- ный или белый) для создания полос на поверхности объекта. Сначала нужно оп- ределить метод рисования линий. Для этого можно передавать один параметр, пусть это будет координата s текстурных координат объекта. Вершинный шейдер будет передавать это значение как varying-переменную V, представляющую собой число с плавающей запятой, что позволит использовать текстурную координату s как интенсивность цвета (полутон) на поверхности сферы (рис. 14.3, а). Точка обзора находится чуть выше сферы, и зритель смотрит на «северный полюс». Тек- стурная координата s начинается с 0 (черный цвет) и увеличивается до 1 (белый цвет), и так вокруг всей сферы. Черно-белый край виден возле полюса; передний край сферы в основном серый, но интенсивность увеличивается слева направо. При умножении текстурной координаты s на 16 и отбрасывании целой части возникает пилообразный эффект (рис. 14.3, б), вызванный тем, что значение ин- тенсивности сначала 0, потом резко поднимается до 1, а потом так же резко опуска- ется до 0. (Чтобы понять, как выглядит пилообразный эффект, можно посмотреть на иллюстрации встроенных функций fract (см. рис. 5.6) и mod (см. рис. 5.7).) Это повторяется 16 раз. Код шейдера выглядит так: float sawtooth = fract(V * 16.0): Полученные полоски — это не совсем то, чего мы ожидали. Теперь нужно взять абсолютное значение функции (рис. 14.3, в). Умножая значение sawtooth на 2 и вы- читая 1, получаем функцию, значение которой находится в диапазоне [-1, 1]. Если получится абсолютное значение функции, то оно будет меняться с 1 до 0 и потом опять до 1 (колебания треугольной формы). Код выглядит так: float triangle = abs(2.0 * sawtooth - 1.0): Полоски уже появились, но они все еще слишком расплывчаты. Сделаем их чисто черными и чисто белыми с помощью функции step. Если сравнить
294 Глава 14. Сглаживание процедурных текстур переменную triangle со значением 0,5, функция будет всегда возвращать 0 при triangle, меньшем или равном 0,5, и 1 при triangle, большем 0,5. Вот этот код: float square = step(0.5. triangle); Рис. 14.3. Рисование полосок на сфере с помощью текстурной координаты s: а — s принимается как непосредственное значение интенсивности; б— пилообразный эффект; в — абсолютное значение функции, обеспечивающее пилообразную функцию в виде треугольников1 Результат показан на рис. 14.4, а. Относительный размер полосок можно ме- нять изменением порогового значения для функции step. 14.4.2. Аналитический отбор На рис. 14.4, а уже видны отчетливые полоски, но алиасинг на краях тоже хорошо заметен. Функция step может возвращать значения 0 или 1, а не промежуточные, поэтому у полосок и получается зубчатый край. Увеличив разрешение изображе- ния, мы добьемся только того, что размер зубцов уменьшится относительно разме- ра объекта, но они не пропадут окончательно. Проблема в том, что функция step может выполнить только прямой переход от белого цвета к черному (см. рис. 5.11), поэтому невозможно избавиться от алиасинга путем увеличения частоты. Чтобы получить красивые полоски, нужно выполнить сглаживание в шейдере. Рис. 14.4. Сглаживание краев полосок: а—функция step вызывает появление алиасинга; б— функция smoothstep с жестко заданной шириной вызывает сильную расплывчатость на экваторе, но недостаточную на полюсе; в— адаптивный метод позволяет выполнить сглаживание везде1 Рисунок любезно предоставлен Бертом Фройденбергом, университет Магдебурга, 2002.
14.4. Пример сглаживания полосок 295 Множество методов антиалиасинга основано на удалении высоких частот пе- ред семплингом. Такой прием называется фильтрация верхних частот, так как верхние частоты удаляются, а нижние — остаются. Основной видимый эффект такой фильтрации — размытое изображение. Чтобы избавиться от высоких частот в шаблоне полосок, воспользуемся фун- кцией smoothstep. Получится плавный переход между белым и черным цветами. Чтобы добиться этого, нужно задавать две линии края, и плавный переход будет выполняться между этими линиями. На рис. 14.4, б показан результат выполне- ния следующей строки кода: float square = smoothstep(0.4, 0.6. triangle): 14.4.3. Адаптивный аналитический предварительный отбор Описанный в предыдущем подразделе способ сглаживания дает приемлемые ре- зультаты не во всех областях сферы. Размер фильтра сглаживания (0,2) опре- делен как параметр. Но при постоянном размере экрана этот параметр не изме- няется, а текстурная координата s меняется слишком быстро возле полюсов и слишком медленно — на экваторе. Из-за применения фильтра фиксирован- ной ширины размывка затрагивает несколько пикселов на экваторе, но совсем не видна на полюсах. Нужно найти способ определять размер фильтра сглажи- вания в зависимости от координат на сфере, чтобы рисунок выглядел лучше. Для этого требуется знать, с какой скоростью должна меняться функция при изменении экранных координат. К счастью, в языке шейдеров OpenGL есть встроенная функция скорости из- менения (производная) любого параметра пространства. Функция dFdx опреде- ляет скорость изменения в направлении х, а функция dFdy — скорость изменения в направлении у. Так как эти функции работают с экранными координатами, они доступны только из фрагментного шейдера. С их помощью можно вычислить век- тор-градиент нужных координат. Для заданной функции f(x, у) градиент f в координатах (х, у) определен как вектор: G[/(x, г/)] = дх V ду Проще говоря, вектор-градиент включает в себя частную производную функ- ции /по отношению к х (скорость изменения/в направлениих) и частную произ- водную функции f по отношению к у (скорость изменения / в направлении у). Вектор-градиент всегда указывает в сторону наибольшей скорости увеличения функции f(x, у) (направление градиента), а модуль вектора представляет собой скорость увеличения функции в направлении градиента. (Эти значения тоже при- годятся для обработки изображения.) Встроенные функции dFdx и dFdy как раз и нужны для вычисления вектора-градиента функций фрагментного шейдера.
296 Глава 14. Сглаживание процедурных текстур Значение модуля градиентного вектора функции f(x, у) обычно называется градиентом функции /(х, у). Он определен как mag[G[/(*> ?/)]] = sqrt((5//a.r)2 +(9//3z/)2)- На самом деле не всегда нужно выполнять громоздкие вычисления квадратно- го корня. Градиент можно подсчитать приблизительно, вычислив абсолютные значения: mag[G[/(x, г/)]] = abs(/(.г, г/)- /(х +1, г/)) + abs(/(х, г/)-/(х, у + !))• В результате получится значение, которое возвращает обычная встроенная функция fwidth. Сумма абсолютных значений является верхней границей фильт- ра. Если это значение слишком велико, окончательное изображение будет размы- тым больше, чем нужно, но обычно степень размытия вполне приемлема. Два метода вычисления градиента сравниваются на рис. 14.5. Как можно заме- тить, существует небольшое видимое различие результата. Так как значение гра- диента слишком маленькое для функции этого объекта, значения были масшта- бированы. Рис. 14.5. Визуализация градиента: а — модуль вектора градиента используется как значение интенсивности цвета; б— градиент вычислен приближенно (реальные значения градиента масштабированы)1 Чтобы вычислить фактический градиент для varying-переменной V во фраг- ментном шейдере, используем следующий код: float width = length(vec2 (dFdx(V). dFdy(V))): Вычислим более приблизительное значение, но быстрее: float width = fwidth(V): Ширина фильтра затем передается в функцию smoothstep таким образом: float edge = width * 32.0: float square = smoothstep(0.5 - edge. 0.5 + edge, triangle); Фрагментный шейдер, выполняющий все это, приведен в листинге 14.1. ’ Рисунок любезно предоставлен Бертом Фрейденбергом, университет Магдебурга, 2002,
14.4. Пример сглаживания полосок 297 Листинг 14.1. Фрагментный шейдер для адаптивного аналитического сглаживания varying float V: // произвольное число varying float Lightintensity: uniform float Frequency: // частота полосок равно 6 void main (void) { float sawtooth = fract(V * Frequency): float triangle = abs(2.0 * sawtooth - 1.0): float dp = length(vec2 (dFdx(V). dFdy(V))): float edge = dp * Frequency * 2.0: float square = smoothstep(0.5 - edge. 0.5 + edge, triangle); gl_FragColor = vec4 (vec3 (square). 1.0): } При масштабировании частоты текстуры соответственно должна увеличивать- ся ширина фильтра. Вычисленное значение функции записывается в красный, зеленый и синий компоненты вектора типа vec3 и используется в качестве цвета фрагмента. Результат применения метода показан на рис. 14.4, в, размытость бо- лее равномерно распределена по сфере. Затем проводятся несложные вычисле- ния освещения, и окончательное изображение накладывается на чайник (рис. 14.6). Рис. 14.6. Эффект адаптивного аналитического сглаживания на чайниках разных размеров. Для сравнения верхние чайники нарисованы без сглаживания. При рисовании нижних чайников использовался описанный шейдер. Сглаживание краев полосок выглядит хорошо в обоих масштабах. Но на краях объектов дефекты изображения все же видны Метод работает хорошо, пока ширина фильтра не превысит частоту, что и про- исходит на северном полюсе сферы. Полоски возле полюса получаются намного
298 Глава 14. Сглаживание процедурных текстур тоньше одного пиксела, и функция step не может здесь иметь нормальное значе- ние. В таких областях объекта нужно применять интегрирование или ограниче- ние частоты (оба метода будут описаны в дальнейшем). 14.4.4. Аналитическое интегрирование Средневзвешенное значение функции в заданном интервале называется свёрт- кой. Исходные данные для вычисления средневзвешенного значения называются ядро свёртки или свёрточный фильтр. В некоторых случаях можно определить свёртку функции заранее и в дальнейшем использовать для семплинга результат этих вычислений, а не функцию-оригинал. Свёртку в вычислениях можно вы- полнять через определенный промежуток, как если бы свёртка входной функции происходила через прямоугольный фильтр. Этот способ далек от идеала, но прост, в нем легко выполнять вычисления, и зачастую этого достаточно для улучшения качества изображения. Описанный метод называется методом зонального семплинга-, в нем некоторые вычисления зависят от того, в какой зоне заданного объекта находятся текущие координаты. Если этот метод будет использоваться применительно к изображе- нию, приведенному на рис. 14.2, то для каждого конкретного пиксела будут вы- числены более точные значения. В книге [2] рассказывается, как выполнять аналитическое сглаживание пери- одической функции step, иногда этот процесс называют серией импульсов. Исполь- зуем этот метод для сглаживания кирпичной стенки в процедурном шейдере, опи- санном в главе 6 этой книги. В шейдере функция step используется для того, чтобы рисунок был периодическим. Функция, вычисляющая рисунок кирпичей в гори- зонтальном направлении, изображена на рис. 14.7. От точки 0 до точки BrickPct.x (ширина кирпича) значение функции равно 1,0. В точке BrickPct. х находится край с бесконечным наклоном функции к 0. В точке 1 функция поднимается обратно в 1,0, и все повторяется для следующих кирпичей. Рис. 14.7. Периодическая функция step (серия импульсов), определяющая горизонтальный компонент процедурной текстуры для кирпичной стены Для сглаживания этой функции вычисляется ее интегральное значение. В слож- ных областях ширина фильтра, вычисляемая функцией fwidth, будет соответство- вать нескольким таким импульсам. Выполнив семплинг интегрального значения функции, получим средневзвешенное значение без высоких частот.
14.4. Пример сглаживания полосок 299 Интегральное значение данной функции показано на рис. 14.8. От точки 0 до точки BrickPct.х значение функции равно 1,0, так что крутизна функции 1,0. От точки Bri ckPct. х до точки 1 функция имеет значение 0, и теперь интегральное зна- чение постоянное. В точке 1 функция возвращается обратно в 1,0, так что интег- рал увеличивается до тех пор, пока функция не достигнет значения Bri ckPct. х + 1. В этой точке значение крутизны меняется до 0, потом все повторяется сначала. Рис. 14.8. Периодическая функция step (серия импульсов) и ее интегральное значение Для сглаживания нужно определять интегральное значение вне области фильтра. При этом проверяется значение на краях фильтра, и из него вычитаются два зна- чения. Интегральное значение этой функции состоит из двух частей: суммы обла- стей всех импульсов перед краем и области, включающей часть импульса на краю. Для процедурного шейдера кирпичной стенки переменная pos 11 i on. x принята за основу для функции импульсов в горизонтальном направлении. Количество пол- ных импульсов будет равно f 1 oor (pos i t i on. x). Так как высота каждого импульса 1.0, область каждого полного импульса будет BrickPct. х. Перемножив эти два значе- ния, получаем область для всех полных импульсов. Край может располагаться в той части функции, где ее значение 1, или в той части, где значение 0. Это можно выяснить, вычислив разность fract(position.x) - (1.0 - BrickPct ,х). Если результат такого вычитания меньше нуля, то точка находится там, где значение функции равно 0, и больше ничего вычислять не требуется. Но если значение больше нуля, точка находится там, где значение функции равно 1,0. Так как высота импульса равна 1,0, область такого «частичного» импульса будет fract(position.x) - (1.0 - BrickPct.х). Вторая часть интегрального значения — выражение max(fract(posi- tion.x) - (1.0 - BrickPct.х), 0.0). Вычисленное значение можно использовать для горизонтальных и вертикаль- ных компонентов процедурного шаблона кирпичной стенки. Так как приложение знает ширину и высоту кирпичей (Bri ckPct. х и Bri ckPct. у), выражение 1.0 - Bri ckPct. х and 1.0 - BrickPct .у легко вычислить и передать полученное значение во фрагментный
300 Глава 14. Сглаживание процедурных текстур шейдер, что избавит от необходимости рассчитывать его для каждого фрагмента отдельно. Так как выражение будет вычисляться два раза с разными аргумента- ми, удобно определить его как макрос или функцию: #define IntegraKx. р. notp) ((flоог(х)*(р)) + max(tract(х)-(notp). 0.0)) Параметр р содержит значение части импульса (при значении функции 1,0), а параметр notp содержит значение, не являющееся частью импульса (при значе- нии функции 0). С помощью этого макроса можно написать код для вычисления интегрального значения по всей ширине фильтра: vec2 fw. useBrick; fw = fwidth(position): useBrick = (Integral(position + fw. BrickPct. MortarPct) - Integral(position. BrickPct. MortarPct)) / fw; Результат делится на площадь фильтра (в данном случае имеется в виду прямо- угольный блок), чтобы получить среднее значение функции в выбранном промежутке. 14.4.5. Фрагментный шейдер кирпичной стенки со сглаживанием Попробуем заставить описанную процедуру работать. Простая технология семп- линга, использованная в примере в главе 6, заменяется аналитической интегра- цией. Окончательный шейдер приведен в листинге 14.2. Разница между резуль- татами работы шейдера без сглаживания и шейдера со сглаживанием показана на цветном рис. 25. Листинг 14.2. Исходный код фрагментного шейдера кирпичной стенки со сглаживанием uniform vec3 BrickColor. MortarColor: uniform vec2 BrickSize; uniform vec2 BrickPct; uniform vec2 MortarPct; varying vec2 MCposition; varying float Lightintensity; #define IntegraKx. p. notp) ((floor(x)*(p)) + max(fract(x)-(notp). 0.0)) void main(void) { vec2 position, fw. useBrick: vec3 color; // Определение координат в шаблоне кирпичной стенки position = MCposition / BrickSize: // Каждая следующая строка сдвигается на половину кирпича if (fract(position.у * 0.5) > 0.5) position.x += 0.5: // Вычисление размера фильтра fw = fwidth(position): // Фильтрация с помощью интеграции двухмерного импульса //в шаблоне кирпичной стенки по ширине и высоте фильтра
14.5. Отбрасывание частот 301 useBrick = (Integral(position + fw. BrickPct. MortarPct) - Integral(posltion. BrickPct. MortarPct)) / fw; // Определение окончательного цвета color = mix(MortarColor. BrickColor, useBrick.x * useBrick.y); color *= Lightintensity; gl_FragColor = vec4 (color, 1.0); } .5. Отбрасывание частот Для некоторых функций рассмотренный в предыдущих разделах способ вычис- лений не подходит, и тут можно попробовать другой метод, называемый отбра- сывание частот. Для замены конкретного значения функции в случае излишней ширины фильтра используется среднее значение функции. Это удобно для функ- ций, среднее значение которых уже известно, например синуса и функции шума. ,4.5.1. Фрагментный шейдер шахматной |оски со сглаживанием Рисунок шахматной доски — удобный критерий проверки качества метода сгла- живания (рис. 14.9). В листинге 14.3 приведен фрагментный шейдер для про- цедурного рисования шахматной доски со сглаживанием. Вершинный шейдер всего лишь преобразует координаты вершин и передает текстурные координаты. Приложение передает значения двух цветов шахматной доски, среднее значение этих двух цветов (приложение может вычислить его один раз и передать через uniform-переменную, чтобы не вычислять в шейдере для каждого фрагмента) и ча- стоту чередования клеток. Рис. 14.9. Шаблон шахматной доски. Рендеринг выполнен с помощью шейдера шахматной доски со сглаживанием. Слева ширина фильтра выставляется в 0, и получаются неровности. Справа ширина фильтра вычисляется функцией fwidth
302 Глава 14. Сглаживание процедурных текстур Фрагментный шейдер вычисляет размер фильтра, а затем выполняет гладкое интерполирование между соседними клетками доски. Если фильтр слишком ши- рокий (переменный параметр изменяется слишком быстро, чтобы можно было правильно использовать фильтр), подставляется среднее значение. Даже если во фрагментном шейдере есть условное выражение, все равно выполняется гладкое интерполирование между вычисленным и средним значениями цвета. Листинг 14.3. Исходный код шейдера шахматной доски со сглаживанием uniform vec3 Colorl: uniform vec3 Color2: uniform vec3 AvgColor; uniform float Frequency: varying vec2 TexCoord; void maln(vold) { vec3 color; // Определение ширины проекции одного пиксела в пространстве s-t vec2 fw = fwidth(TexCoord): // Определение степени размытости vec2 fuzz = fw * Frequency * 2.0; float fuzzMax = max(fuzz.s, fuzz.t): // Определение координат шаблона шахматной доски vec2 checkPos = fract(TexCoord * Frequency): If (fuzzMax <0.5) { // Если ширина фильтра слишком маленькая, вычисляется цвет шаблона vec2 р = smoothstep(vec2 (0.5). fuzz + vec2 (0.5), checkPos) + (1.0 - smoothstep(vec2(0.0). fuzz. checkPos)): color = mix(Colorl, Color2. p.x * p.y + (1.0 - p.x) * (1.0 - p.y)): // Постепенное изменение цвета к среднему при приближении к краю color = m1x(color. AvgColor, smoothstep(0.125. 0.5. FuzzMax)); } else // В противном случае используется только средний цвет color = AvgColor: } gl_FragColor = vec4(color. 1.0): } 14.6. Итоги Больше свободы — больше ответственности. Язык шейдеров OpenGL позволяет вычислять любые процедурные текстуры. Можно легко написать шейдер, который
14.7. Ссылки 303 будет рисовать изображение с уродливыми неровностями и дефектами изобра- [ жения (обычно в нем используются только условные выражения и функция step), но избавиться от этих проблем будет куда труднее. После общего описания не- скольких проблем в этой главе были рассмотрены методы сглаживания проце- i дурных текстур. Некоторые возможности языка помогают выполнять сглажива- | ние, например встроенные функции для гладкой интерполяции (smoothstep), для I производных (dFdx, dFdy) и для вычисления ширины фильтра (fwidth). Эти функ- [ ции широко используются в шейдерах, выполняющих сглаживание с помощью I фильтрации, адаптивной фильтрации, интегрирования и отбрасывания частот. 14.7. Ссылки : В большинстве книг по обработке сигналов и изображений обсуждаются принци- пы семплинга, преобразований и дефектов изображения. Книги [10, 12 и 16] мо- гут служить дополнительными справочниками по этой теме. Технические записи Альви Рэя Смита[13-15] описывают проблему дефектов изображения непосред- ственно для компьютерной графики. Книга [2] содержит главу, описывающую сглаживание в шейдерах в понятиях языка шейдеров RenderMan, но большинство методов могут применяться и в языке шейдеров OpenGL. Дарвин Пичи тоже пишет о методах сглаживания в книге [7]. Берт Фрейденберг разработал шейдер на языке шейдеров OpenGL для выпол- нения адаптивного сглаживания и представил эту работу на выставке SIGGRAPH- 2002 в Сан-Антонио, штат Техас. В этой главе я просто воспроизвел его примеры, но Берт заслуживает особой похвалы за иллюстрации к некоторым темам этой книги. 1. 3Dlabs. Веб-сайт для разработчиков (http://www.3dlabs.com/support/developer). 2. Apodaca A. A., Gritz L. Advanced RenderMan: Creating CGI for Motion Pictures. San Francisco: Morgan Kaufmann Publishers, 1999 (http://www.bmrt.org/arman/ materials.html). 3. Cook R. L. Stochastic Sampling in Computer Graphics//ACM Transactions on Graphics. 1986. Vol. 5, № 1. P. 51-72. 4. Crow F. C. The Aliasing Problem in Computer-Generated Shaded Images//Comm. of the ACM. 1977. № 20 (11). P. 799-805. 5. Crow F. C. Summed-Area Tables for Texture Mapping//Computer Graphics (Proc. SIGGRAPH-84). 1984. July. P. 207-212. 6. Dippft M. A. Z., Wold E. H. Antialiasing Through Stochastic Sampling//Computer Graphics (Proc. SIGGRAPH-85). 1985. July. P. 69-78. 7. Texturing and Modeling: A Procedural Approach. 3rd ed.//D. S. Ebert, J. Hart, B. Mark, at al. San Francisco: Morgan Kaufmann Publishers, 2002 (http:// www.texturingandmodeling.com). 8. Freudenberg B.ANon-Photorealistic Fragment Shader in OpenGL 2.0 //Presented at the SIGGRAPH 2002 Exhibition in San Antonio. 2002. July (http://isgwww.cs.uni- magdeburg.de/~bert/publications).
304 Глава 14. Сглаживание процедурных текстур 9. Freudenberg В. Real-Time Stroke-based Halftoning//Ph. D. thesis/University of Magdeburg. 2003. 10. Glassner A. S. Principles of Digital Image Synthesis: Vol. 1. San Francisco: Morgan Kaufmann Publishers, 1995. 11. Glassner A. S. Principles of Digital Image Synthesis: Vol. 2. San Francisco: Morgan Kaufmann Publishers, 1995. 12. Gonzalez R. C., Woods R. E. Digital Image Processing. 2nd ed. Upper Saddle River, NJ: Prentice Hall, 2002. 13. Smith A.R. Digital Filtering Tutorial for Computer Graphics//Lucasfilm Technical Memo 27. 1983. March. http://www.alvyray.com/Memos/default.htm) 14. Smith A. R. Digital Filtering Tutorial. Part II//Lucasfilm Technical Memo 27.1983. March (http://www.alvyray.com/Memos/defauit.htm). 15. Smith A. R. A Pixel Is Not a Little Square, a Pixel Is Not a Little Square, a Pixel Is Not a Little Square! (And a Voxel Is Not a Little Cube)//Technical Memo 6, Microsoft Research. 1995. July (http://www.aLvyray.com/memos/default.htm). 16. Wolberg G. Digital Image Warping. Wiley-IEEE Press, 2002.
I Нефотореалистичные | шейдеры [ Значительное количество исследований в компьютерной графике проводится для [ того, чтобы создавать более реалистичные изображения. В перспективе цель та- [ких исследований — обеспечить возможность рисовать настолько хорошо, что изображение будет неотличимо от фотографии реального мира, — это называется [фотореализм. На графическом оборудовании, использующем самые последние разработки, некоторые фотореалистичные эффекты уже достижимы. Такая погоня за реализмом, конечно же, отразилась на графических API, в том [числе и OpenGL. Спецификация OpenGL определяет формулы для вычисления [эффектов освещения, свойств материалов и дымки. Эти формулы предполагают [определение настолько реалистичных эффектов, какие только возможно поду- рить на конкретном оборудовании. I Но опыт создания произведений искусства многими поколениями художни- ков показывает, что фотореалистичность — не единственный возможный стиль [создания изображений. Доступность недорогого программируемого графическо- I го оборудования привела к развитию нефотореалистичного рендеринга, или NPR. [Исследователи и специалисты-практики пытаются воссоздать множество худо- жественных эффектов в компьютерной графике. В этой главе читатель увидит [несколько примеров реализующих их шейдеров. Б.1. Штриховка [Берт Фрейденберг из университета г. Магдебурга (Германия) - один из первых [программистов, не работающих в компании 3Dlabs, создал уникальный шейдер [OpenGL. Область его исследований — использование программируемого графи- гческого оборудования для воспроизведения NPR-эффектов (штриховка и полу- тона) в реальном времени. Он экспериментировал с тестовой реализацией языка [шейдеров OpenGL летом 2002 г. и создал шейдер штриховки, который согласил- ся представить для этой книги. I Этот шейдер содержит несколько уникальных возможностей, а процесс его со- Ездания описан в кандидатской диссертации Фрейденберга [6]. Шейдер штрихов- ой основан на шейдере деревянных полосок, разработанном Скоттом Джонсто- I ном и описанном в книге «Углубленный RenderMan: создание CGI для анимации». I Шейдер штриховки должен создавать поверхность объекта так, чтобы она вы- глядела нарисованной вручную, например, чтобы видны были полоски от черниль- [.ной ручки. Каждый штрих способствует зрительному восприятию тона, текстуры
306 Глава 15. Нефотореалистичные шейдеры и формы объекта. Эффект, получаемый с помощью этого шейдера, — гравюра, то есть рисунок, имитирующий оттиск изображения, вырезанного на деревянной доске. Если на дереве в каком-нибудь месте была бороздка, на объекте в этом ме- сте не будет чернил. Освещение имитируется изменением ширины бороздок в со- ответствии с интенсивностью освещения. У разработчика, желающего создать такой шейдер, сразу возникает несколько проблем. Первая из них — та, что нужно рисовать штрихи, с помощью которых определяются тон и текстура объекта. Простые чередующиеся черные и белые линии создают контрастность на краях, то есть источник неровностей, так что сглаживание придется выполнять в первую очередь. Штрихи на объекте долж- ны оставаться в одном и том же положении при анимации. И наконец, штрихи на гравюре не всегда идеально прямые и одинаковые, как их рисует компьютер. Поэтому нужно создать впечатление, что изображение нарисовано настоящим художником. 15.1.1. Настройка приложения Приложение должно задавать координаты вершин, нормали и простой набор тек- стурных координат. Нормали нужны для довольно простого вычисления освеще- ния, а текстурные координаты — в качестве основы для процедурного шаблона штрихов. Координаты источника освещения передаются в uniform-переменной, приложение также должно обновлять значение переменной Т1 те для каждого кадра, так что поведение шейдера можно менять от кадра к кадру. Для придания каждо- му штриху индивидуальности можно использовать функцию шума. В этом слу- чае приложение будет создавать несколько значений шума и сохранять результа- ты в трехмерной текстуре. Значение uniform-переменной типа sampler3D будет устанавливаться приложением, чтобы фрагментный шейдер обращался к соот- ветствующему текстурному модулю, чтобы получить значения шума. 15.1.2. Вершинный шейдер Вершинный шейдер штриховки приведен в листинге 15.1. Varying-переменная ObjPos нужна как основа для вычислений штриховки во фрагментном шейдере. Чтобы выполнять анимацию штрихов, вершинный шейдер прибавляет uniform- переменную Time к координате z входных координат вершины. Это создаст впе- чатление, что штрихи «плывут» вдоль оси/. Коэффициент масштабирования учи- тывает видимый размер объекта на экране. (Чтобы применять его к различным объектам, константное значение нужно заменить uniform-переменной.) Осталь- ная часть вершинного шейдера выполняет простые вычисления диффузного ос- вещения, копирует координату t текстурных координат в varying-переменную V и вычисляет значение встроенной переменной gl_Pos1tion. Листинг 15.1. Вершинный шейдер штриховки uniform vec3 LightPosition: uniform float Time; varying vec3 ObjPos; varying float V;
15.1. Штриховка 307 varying float Lightintensity; void main(vold) { ObjPos = (vec3 (gl_Vertex) + vec3 (0.0. 0.0. Time)) * 0.2; vec3 pos = vec3 (gl_ModelViewMatr1x * gl_Vertex); vec3 tnorm = normal1ze(gl_NormalMatr1x * gl_Normal); vec3 1ightVec = normal1ze(L1ghtPos1t1on - pos); Lightintensity = max(dot(llghtVec, tnorm), 0.0); V = gl_Mult1TexCoord0.t; // используйте .s для вертикальных полос gl_Pos1tion = ftransformO; } .5.1.3. Рисование штриховых полосок Рассматриваемый фрагментный шейдер должен определить цвет фрагмента — белый или черный — таким образом, чтобы получились линии на поверхности объекта. Здесь есть несколько особенностей. Покажем применение нескольких методов на простом объекте — сфере. Начнем с такого же кода, который представлен в разделе 14.4.1 для вертикаль- ных полосок, а именно: float sawtooth = fract(V * 16.0): float triangle = abs(2.0 * sawtooth - 1.0); float square = step(0.5. triangle): Здесь V — varying-переменная, передаваемая из вершинного шейдера, она рав- на текстурной координате s при рисовании вертикальных штрихов и текстурной координате t при рисовании горизонтальных штрихов. Число 16 означает, что будут нарисованы 16 белых и 16 черных полосок. Результат выполнения этого кода показан на рис. 15.1. Относительный размер белых и черных полосок можно из- менять с помощью изменения порогового значения функции step. Рис. 15.1. Сфера с полосками, созданными процедурно с помощью текстурной координаты s1 Рисунок любезно предоставлен Бертом Фройденбергом, университет Магдебурга, 2002.
308 Глава 15. Нефотореалистичные шейдеры 15.1.4. Вычисление толщины штриха Сейчас полоски выглядят довольно хорошо, но они не одинаковой ширины: ши- рокие возле экватора и узкие возле полюса. Нужно, чтобы линии были приблизи- тельно одинаковой ширины, а для этого понадобятся функции dFdx и dFdy: float dp = length(vec2 (dFdx(V), dFdy(V))): в г Рис. 15.2. Выравнивание частоты полосок: а — целая часть логарифма градиента используется для определения частоты полосок; б— сфера с высокой частотой полосок; в — целая часть логарифма используется для выравнивания частоты полосок; г— полоски с конусовидными окончаниями1 В результате получается значение градиента, то есть скорость изменения V в за- данной точке поверхности (см. раздел 14.4.3). С помощью этого значения можно выровнять ширину линий. (Теперь приблизительное значение градиента, описан- ное в разделе 14.4.3, не годится, так как здесь вычисляется не ширина фильтра для сглаживания, а частота полосок. Значения должны быть инвариантны при вращении объекта. Поэтому нужно знать не сумму абсолютных значений двух компонентов вектора-градиента, а его реальную длину.) Логарифм (с основой 2) этого значения нужен для согласования ширины по шагам: каждый раз dp удваива- Рисунок любезно предоставлен Бертом Фрейденбсргом, университет Магдебурга, 2002.
15.1. Штриховка 309 ется, количество линий — тоже (результат показан на рис. 15.2, а). Линии полу- чаются слишком тонкие или слишком жирные. Чтобы избежать этого (так как мы добиваемся постоянной ширины линий), нужно уменьшить количество линий при значительном увеличении их ширины. Это достигается отрицанием логарифма: float logdp = -1og2(dp): float ilogdp = floor(logdp); float frequency = exp2(ilogdp): float sawtooth = fract(V * 16.0 * frequency): Сфера с большей частотой линий показана на рис. 15.2, б. Видно, что в нижней части сферы линии выглядят нормально, но на полюсе их слишком много. Вне- сем изменения в частоту линий, и получится, что по всей сфере полоски практи- чески одинаковы (рис. 15.2, в). (Заметьте, как соотносятся рис. 15.2, а и в.) Следующая особенность процедуры — сглаживание изменений, происходящих при смене частоты. Глаза видят явный край между этими переходами и нужно его как-нибудь смягчить. Можно использовать дробную часть 1 ogdp для задания плав- ного перехода между частотами. Это значение будет равно 0 в начале одной час- тоты и увеличится до 1,0 в точке изменения частоты: float transition = logdp - ilogdp: Полоски с двойной частотой можно получить с помощью формулы abs (2.0 * t -1.0). Можно использовать значение transi ti on для линейной интерполяции меж- ду t и abs (2.0 * t - 1.0), вычислив выражение (1.0 - transition) * t + transition * abs(2.0 * t -1.0) (это то же самое, что выражение mi x(abs( 2.0*t -1.0),t, transition)). Но вместо использования mix возьмем его эквивалент abs ((1.0 - transition) *t - tran- sition). Используя вычисленное ранее значение для основной частоты (tri angle), получим такой код: triangle = abs((1.0 - transition) * triangle - transition): Результат рисования сферы с полосками одинаковой ширины и заостренны- ми краями показан на рис. 15.2, г. .5.1.5. Освещение Чтобы имитировать освещение, нужно сделать темные штрихи более заметными в тени, а белые — более заметными в освещенных областях. Сделать это можно с помощью вычисленного значения интенсивности освещения, изменяя порого- вое значение функции step. В освещенных областях пороговое значение умень- шается, и черные полоски будут тоньше, а в затененных областях, наоборот, поро- говое значение увеличивается, и черные полоски будут шире: const float edgew =0.2: // ширина шага float edgeO = cl amp(LightIntensnty - edgew. 0.0. 1.0): float edgel = clamp(Light!ntensity. 0.0, 1.0): float square = 1.0 - smoothstep(edgeO. edgel. triangle): Чтобы сглаживать переход, нужна функция smoothstep. Так как полоски в про- странстве экрана приблизительно одинаковой ширины, можно использовать фильтр постоянной ширины. Результаты наложения освещения показаны на рис. 15.3.
310 Глава 15. Нефотореалистичные шейдеры Рис. 15.3. Наложение освещения на сферу: а — сфера освещена простым источником и на ней нет полосок; б— интенсивность освещения влияет на толщину полосок1 15.1.6 . Имитация ручной работы Если бы гравюра была выполнена не компьютером, а человеком, то штрихи не были бы идеально ровными. Нужно внести некоторое несовершенство в изоб- ражение с помощью шума, как описано в главе 12. Для этого шейдера не нуж- но придумывать ничего особенного: немного волнистости, частично разорван- ные линии. Трехмерная текстура с шумом Перлина вполне годится для этих целей. Чтобы сделать штрихи волнистыми, можно изменить функцию float sawtooth = fract((V + noise * 0.1) * frequency * stripes): Результат добавления шума к штрихам показан на рис. 15.4. Рис. 15.4. Наложение шума на штрихи: а — функция шума Перлина накладывается непосредственно на поверхность сферы; б— функция шума используется для корректирования параметров частоты штриховки1 1 Рисунок любезно предоставлен Бертом Фрейдепбсргом, университет Магдебурга, 2002,
15.1. Штриховка 311 15.1.7 . Фрагментный шейдер штриховки Фрагменты кода, описанные в предыдущих разделах, складываются в шейдер, приведенный в листинге 15.2. Так как частота вычисляется по текстурной коор- динате t, полоски будут вертикальными, а не горизонтальными. Результат при- менения этого шейдера к модели чайника показан на рис. 15.5. Рис. 15.5. Чайник в стиле гравюры, рендеринг которого выполнен шейдером штриховки1 Листинг 15.2. Фрагментный шейдер для имитации гравюры const float frequency =1.0; varying vec3 ObjPos: // varying float V: // varying float Lightintensity: координаты в пространстве объекта (для шума) произвольное число uniform sampler3D Noise: // значение Noise = 3; void main (void) ( float dp = length(vec2 (dFdx(V), dFdy(V))): float logdp = -log2(dp * 8.0): float ilogdp = floor(logdp): float stripes = exp2(ilogdp): float noise = texture3D(Noise. ObjPos).x: float sawtooth = fract((V + noise * 0.1) * frequency * stripes): float triangle = abs(2.0 * sawtooth - 1.0): // Выравнивание ширины штриха продолжение 1 Рисунок любезно предоставлен Бертом Фройденбергом, университет Магдебурга, 2002.
312 Глава 15. Нефотореалистичные шейдеры Листинг 15.2 {продолжение) float transition = logdp - llogdp: // Сужение кончиков triangle = abs((1.0 + transition) * triangle - transition): const float edgew = 0.3: // ширина шага float edgeO = clamp(LightIntensity - edgew. 0.0, 1.0): float edgel = clampdlghtlntensity. 0.0, 1.0): float square = 1.0 - smoothstep(edge0. edgel. triangle): gl_FragColor = vec4 (vec3 (square). 1.0): } 15.2. Примеры технических иллюстраций В любом руководстве пользователя, технической книге или энциклопедии есть множество иллюстраций, не являющихся фотографиями или фотореалистич- ными изображениями. Технические художники-оформители годами совершен- ствовали различные методы, чтобы показать информацию так понятно, как это только возможно. На рисунках нужно избежать излишней детализации и в то же время показать все самое важное. Этот стиль работы несколько отличается от основного направления компьютерной графики, в котором, наоборот, большая реалистичность изображения достигается использованием большого количества деталей. Специалисты-практики разрабатывают алгоритмы для создания техничес- ких иллюстраций. Цёль — максимально упростить или даже полностью автомати- зировать процесс создания высококачественных технических иллюстраций в соот- ветствии с методами их создания. Зритель получает информацию о форме объекта с. помощью его освещения. Традиционного вычисления освещения бы- вает недостаточно для оценки тех областей объекта, что находятся в тени. В этих областях есть только постоянное рассеянное освещение. На технических иллюстрациях некоторые части объекта также изображают на черном фоне, что- бы сделать их более различимыми. Если же часть объекта, изображенная на чер- ном фоне, сама по себе почти черная, она будет неразличима. В 1998 г. Брюс и Ами Гуш, Петер Ширли и Элейн Кохен провели анализ и составили список общих характеристик цветных иллюстраций, выполненных с помощью аэрографа и ка- рандаша. □ Края поверхностей, силуэт и неоднородности поверхности объекта обычно ри- суют черными кривыми линиями. □ Для освещения используется один простой источник, который излучает на объекты белый свет. □ Источник освещения обычно находится сверху объекта, и рассеянное отраже- ние от видимой поверхности объекта находится в диапазоне [0, 1]. □ Сложные и реалистичные эффекты, а именно тени, отражения и многочислен- ные источники освещения, не показаны.
15.2. Примеры технических иллюстраций 313 □ Матовые объекты затеняются с интенсивностью, не близкой ни к белому, ни к черному цвету, так что их цвет будет отличаться и от черных линий края, и от белой подсветки. □ Теплота или холодность цвета показывает нормаль поверхности (и кривизну поверхности). Эти характеристики были использованы в алгоритме, называемом заливка Гуча. Один из важных аспектов заливки Гуча — выделение нужных граней черными линиями. Есть несколько методов рисования таких линий. Лучше всего опреде- лить эти линии сможет человек, который будет создавать модель. В этом случае края можно рисовать как сглаженные черные линии, которые нанесены непос- редственно на поверхность объекта (то есть проводится второй рендеринг, кото- рые рисует линии уже после завершения основного рендеринга). Если контурные линии все же не были заданы при моделировании, есть не- сколько способов создать их автоматически. Качество результата во многом зави- сит от использованного метода и от характеристик объектов. Внутренние края или сгибы также нужно прорисовывать, иногда это очень важно для восприятия. Метод определения внутренних краев и сгибов включает в себя использование вершинных и фрагментных шейдеров для записи нормалей и значений глубины в буфер кадров. Результат сохраняется в текстуре, и последующий рендеринг дру- гими вершинным и фрагментным шейдерами может использовать алгоритм оп- ределения края на этом «изображении». Метод рисования силуэтных линий для простых объектов, описанный Джеф- фом Дендером в книге [И], требует двойного прохода при рисовании. Сначала рисуют только многоугольники передних граней, а режим глубины устанавлива- ется в GL_LESS. Шейдеры Гуча при этом активны. Затем рисуют задние многоуголь- ники простыми линиями, а режим глубины при этом установлен в GL_LEQUAL. Эти линии будут нарисованы черным с помощью стандартной функциональности OpenGL, а как сделать правильные вызовы, показано в листинге 15.3. Листинг 15.3. Код на языке С для рисования силуэтов простых объектов // Включить отбраковку gl Enable(GL_CULL_FACE): // Рисовать передние грани как заполненные с помощью шейдера Гуча glPolygonMode(GL_FRONT. GL_FILL): glDepthFunc(GL_LESS): glCul 1 Face(GL_BACK): gl UseProgramObjectARB(ProgramObject): drawSphere(0.6f. 64): // Рисовать задние грани черными контурами обычными вызовами OpenGL gl LI neWI dth(5.0): glPolygonMode(GL_BACK. GL_LINE): glDepthFunc(GL_LEQUAL): glCul1Face(GL_FRONT): glColor3f(0.0. 0.0, 0.0): glUseProgramObjectARB(O): drawSphere(0.6f. 64); Второй аспект заливки Гуча — то, что рассеянное освещение вычисляется по ме- тоду Фонга, и его цветом является белый. Освещение дает информацию о кривизне
314 Глава 15. Нефотореалистичные шейдеры поверхности, и выбор белого цвета гарантирует, что освещенные области будут отличаться от контурных черных линий и от цветов заливки объекта (обычно они не черные и не белые). Третий аспект алгоритма — ограниченный диапазон значений яркости, исполь- зуемых для подчеркивания кривизны поверхности объекта. Эта часть заливки выполняется с использованием цвета объекта, и обычно это среднее значение, которое не сливается ни с черным, ни с белым цветом. Так как диапазон значений яркости ограничен, переход цвета от теплого к хо- лодному тоже используется для передачи информации о поверхности объекта. Художники применяют теплые (желтый, красный, оранжевый) и холодные (си- ний, фиолетовый, зеленый) цвета, чтобы правильно передать глубину. Теплые цвета, визуально приближающие объект, больше подходят для ближних объек- тов. Холодные цвета, визуально отдаляющие объект, больше подходят для объек- тов, расположенных на заднем плане. Заливка объектов зависит от двух коэффициентов. Коэффициент рассеянного отражения нужен для вычисления значений в ограниченном диапазоне для за- ливки. Вторым коэффициентом является полоска перехода между двумя разны- ми цветами. Один из этих цветов будет холодный, имитирующий удаление по- верхности, второй — теплый, имитирующий приближение поверхности. Полоса перехода цветов от холодного к теплому получается полутоновая, вне зависимос- ти от основного цвета объекта. Приведем формулы для вычислений цветов в заливке Гуча: ^cool — ^blue + ^^diffuse J ^warrn — ^yellow + P^diffuse fl + NL'] f 1 + XlY "final — I 2 I «cool I 1 2 I "warm . Здесь kcoo\ — цвет неосвещенных областей. Это значение вычисляется добавле- нием голубого полутона и рассеянного цвета объекта, diffuse Значение а является переменной, определяющей, как много рассеянного цвета будет добавлено к го- лубому полутону. Awa™ — цвет полностью освещенных областей. Это значение вычисляется как сумма желтого полутона и рассеянного цвета объекта с коэффи- циентом р. Окончательный цвет получается в результате линейного перехода между цве- тами £cooi и £warm, основанного на диффузном отражении NL, где N— нормализо- ванная нормаль поверхности, а £ — единичный вектор, направленный к источни- ку освещения. Так как NL может меняться в диапазоне [-1, 1], добавим к нему 1 и разделим результат на 2, чтобы получить значение в диапазоне [0, 1]. Затем по этому значению определяются пропорции /гсоо| и для вычисления окончатель- ного цвета. 1 5.2.1. Настройка приложения Этот алгоритм является двухпроходным (все геометрические объекты рисуются дважды). Используем в нем метод Лендера для рисования черных силуэтных ли-
15.2. Примеры технических иллюстраций 315 ний. За первый проход многоугольники задней поверхности отбрасываются, а рен- деринг многоугольников передней поверхности выполняется шейдером Гуча. За второй проход отбрасываются многоугольники передней поверхности и с помо- щью стандартной функциональности OpenGL черным цветом рисуются контуры задних прямоугольников. Если рисовать контуры линией толще одного пиксела, они будут выходить за края объекта. Чтобы этот шейдер работал правильно, нуж- но передавать координаты вершин и нормали поверхности в вершинный шейдер. 1 5.2.2. Вершинный шейдер Назначение вершинного шейдера Гуча — выдавать значение для выражения (1 +NL)/2 и передавать вектор отражения и вектор обзора, так что рассеянное отражение можно вычислять во фрагментном шейдере (листинг 15.4). Другие эле- менты шейдера такие же, как в предыдущих. Листинг 15.4. Вершинный шейдер для матовой заливки Гуча uniform vec3 LightPositlon: // (0,0. 10.0, 4,0) varying float NdotL; varying vec3 ReflectVec: varying vec3 ViewVec: void main(void) vec3 ecPos = vec3 (gl_ModelViewMatrix * gl_Vertex); vec3 tnorm = normalize(gl_NormalMatrix * gl_Normal): vec3 lightVec = normalizeCLightPosition - ecPos): ReflectVec = normalize(reflect(-lightVec, tnorm)): ViewVec = normalize(-ecPos): NdotL = (dot(1ightVec. tnorm) + 1.0) * 0.5: gl_Position = ftransform(): } 1 5.2.3. Фрагментный шейдер Фрагментный шейдер выполняет полутоновую заливку алгоритма Гуча и добав- ляет изображению компонент рассеянного отражения (листинг 15.5). Цвета и их пропорции заданы как uniform-переменные, и приложение может легко их ме- нять. Единичные векторы отражения и обзора нормализуются во фрагментном шейдере, так как их длина после интерполяции может немного измениться. Ре- зультат рендеринга с помощью шейдера Гуча и алгоритма рисования силуэта Ден- дера показан на цветном рис. 23. Листинг 15.5. Фрагментный шейдер для матовой заливки Гуча uniform vec3 SurfaceColor: // (0.75, 0,75, 0,75) uniform vec3 WarmColor: // (0,6, 0,6, 0.0) uni form vec3 Cool Col or: // (0,0, 0.0. 0.6) uniform float DiffuseWarm: // 0,45 uni form float DiffuseCool: // 0,45 varying float NdotL; продолжение
316 Глава 15. Нефотореалистичные шейдеры Листинг 15.5 {продолжение) varying vec3 ReflectVec: varying vec3 ViewVec: void main (void) { vec3 kcool vec3 kwarm vec3 kflnal = m1n(CoolColor + DlffuseCool * SurfaceColor, 1.0): = min(WarmColor + DlffuseWarm * SurfaceColor. 1.0): = m1x(kcool. kwarm, NdotL): vec3 nreflect vec3 nview = normal1ze(ReflectVec): = normallze(VlewVec): float spec spec = max(dot(nreflect. nview), 0.0): = pow(spec, 32.0); gl_FragColor = vec4 (m)n(kflnal + spec, 1.0). 1.0): } 15.3. Пример Мандельброта Ни одна книга по программированию графики не обходится без примера рисова- ния изображений Мандельброта. Последний шейдер этой главы не попадает в кате- горию художественного эффекта, но может служить примером выполнения общих вычислений на оборудовании для создания графики для изучения приемов визуа- лизации. В данном случае оборудование для создания графики позволяет изучать известную математическую функцию, вычисляющую множество Мандельброта. Этот шейдер включен в книгу как еще один пример возможностей языка шейде- ров OpenGL: вычисления, ранее возможные только на основном процессоре, сего- дня можно выполнять на графическом акселераторе, что дает большой выигрыш в производительности, так как могут обрабатываться несколько пикселов одно- временно. Визуализация сложной математической функции, подобной множеству Мандельброта, — только верхушка айсберга в использовании программируемых шейдеров для визуализации математических формул и научных объектов. 15.3.1. О множестве Мандельброта Чтобы понять, что такое множество Мандельброта, нужно вспомнить несколько понятий высшей математики. Существует число, называемое мнимым и обозна- чаемое г, определенное как результат квадратного корня из -1. С использованием мнимого числа может быть описан результат квадратного корня любого отрицательного числа. Например, (Зг)2 = -9, и наоборот, V-9 = Зг • Числа, состоящие из вещественной и мнимой частей, например 6 + 4г, называ- ются комплексными числами. Над комплексными числами можно выполнять обычные арифметические операции. Результат умножения двух комплексных чисел следующий: х = а + Ы', у = с + di', ху = ас + adi + cbi -bd= (ас - bd) (ad + bc)i.
15.3. Пример Мандельброта 317 Так как комплексные числа состоят из двух частей, множество комплексных чисел является двухмерным. Их можно обозначить на плоскости, где горизон- тальная ось представляет вещественную часть числа, а вертикальная — мнимую (рис. 15.6). Рис. 15.6. Пример обозначения комплексных чисел на плоскости На рис. 15.6 показаны три точки на плоскости: небольшой квадрат в точке 2 + г, небольшой круг в точке -1 + 2г и небольшой треугольник в точке -1,5 - 0,5г. С помощью компьютерных технологий 70-х гг. математик Бенойт Мандель- брот начал изучение рекурсивной функции с комплексными числами: Zo = 0 + 0г; Zn+1 = Z„2 + с • В этой функции первым значением Z является 0 + 0г. В каждой итерации зна- чение Z возводится в квадрат и добавляется к комплексной константе с для опре- деления нового значения Z. Эта простая итеративная формула вычисляет пара- метры наиболее сложного математического объекта и, возможно, самой сложной структуры. Для некоторых значений с функция уходит в бесконечность, для некоторых — нет. Все очень просто: значения с, уходящие в бесконечность, не являются частью множества Мандельброта. Используя компьютер (или графический ускоритель с поддержкой языка шейдеров OpenGL) для проверки тысяч точек на плоскости комплексных чисел и помечая серым цветом точки, уходящие в бесконечность, и черным цветом — все остальные точки, получим геометрическую фигуру серд- цевидной формы (рис. 15.7). Как же проверить, какие значения уходят в бесконечность? Мандельброт оп- ределил это. Он показал, что если модуль Z (расстояния от начала координат) больше 2, значение функции всегда бесконечно. Чтобы запрограммировать эту функцию, нужно остановить итерации, когда модуль Zпревысит значение 2. И да- же проще, так как мы все время работаем с Z2, сравнивать это значение с 4. Значения внутри черной фигуры на рис. 15.7 не уходят в бесконечность за некоторое разумное количество итераций. Чтобы не зацикливать графический
318 Глава 15. Нефотореалистичные шейдеры акселератор, нужно определить максимальное количество итераций, после про- ведения которых предполагается, что точка принадлежит множеству Мандель- брота. На основе этих двух критериев можно написать простой цикл, вычисляю- щий множество Мандельброта. Рис. 15.7. Графическое представление множества Мандельброта Изображение будет более красочным, если закодировать количество итераций разными цветами. Значения, которые оказались вне множества после первой итерации, обозначаются одним цветом; значения, «выброшенные» на следующей итерации, обозначаются другим цветом и т. д. На рис. 15.7 вместо разных цветов использованы оттенки серого. Белые точки вдоль края были вычислены за 20 ите- раций. Края множества Мандельброта бесконечно разнообразны при разном количе- стве итераций. Продолжая вычисления, можно выявить точки вне множества на сотых или даже тысячных итерациях. Вот несколько соображений по поводу рисования множества Мандельброта. □ Длина края бесконечна. □ Все области внутри множества Мандельброта (обозначенные черным цветом) сообщаются. □ Для каждой итерации существует только одна полоса, огибающая множество Мандельброта. Полосы полностью окружают изображение, не пересекаются и не прерываются. При увеличении края получается интересное изображение. □ Существует бесконечное количество «мини-Мандельбротов» (частей, по фор- ме похожих на искаженное или трансформированное изображение основного множества). 15.3.2. Вершинный шейдер Вершинный шейдер для множества Мандельброта (листинг 15.6) почти такой же, как вершинный шейдер кирпичной стенки из раздела 6.2. Единственное отличие — текстурные координаты в диапазоне [0, 1] представляются и для s, и для t. Эти значения отображаются на диапазон [-2,5, 2,5], и результат сохраняется в varying-
15.3. Пример Мандельброта 319 переменной Position. Это дает фрагментному шейдеру возможность рисовать точки непосредственно в системе координат, центром которой является точка (0, 0). Если приложение рисует прямоугольник, размер которого совпадает с размером экра- на и имеет текстурные координаты левого нижнего угла (0, 0) и правого верхнего угла (1, 1), в результате получится стандартное представление множества Ман- дельброта. С помощью шейдера Мандельброта на языке шейдеров OpenGL обла- сти можно «текстурировать» любым образом и даже накладывать простое осве- щение. Листинг 15.6. Вершинный шейдер множества Мандельброта uniform vec3 LlghtPositlon: uniform float SpecularContribution; uniform float DiffuseContribution; uniform float Shininess; varying float Lightintensity; varying vec3 Position; void mai n(void) vec3 ecPosition = vec3 (gl_ModelV1ewMatrix * gl_Vertex); vec3 tnorm = normalize(gl_NormalMatrix * gl_Normal): vec3 lightVec = normalize(LightPos1tion - ecPosition); vec3 reflectVec = reflect(-11ghtVec, tnorm); vec3 viewVec = normalize(-ecPosition): float spec = max(dot(reflectVec. viewVec). 0.0): spec = pow(spec. Shininess): Lightintensity = DiffuseContribution * max(dot(lightVec. tnorm). 0.0) + SpecularContribution * spec: Position = vec3(gl_Mult1TexCoordO - 0.5) * 5.0; gl_Position = ftransform(); } 15.3.3. Фрагментный шейдер Во фрагментном шейдере реализован алгоритм, описанный в предыдущем разде- ле. Uniform-переменные определяют максимальное количество итераций и началь- ную точку множества Мандельброта (центр и коэффициент масштабирования). Приложение может устанавливать один цвет для внутренних точек и два цвета — для внешних точек. Для значений внешних точек переход от цвета OuterCol orl до цвета 0uterColor2 будет разделен на 20 отдельных полос, и цвета начнут повто- ряться, если количество итераций превысит 20, потом 40 и т. д. Этот шейдер соотносит координату х с вычисленными координатами на плос- кости комплексных чисел (Position./) с действительным числом в итеративной : функции, а координату у — с мнимым числом. После установки начальных усло- - вий шейдер начинает цикл, используя два критерия выхода: достижение макси- ; мального количества итераций или выход точки за пределы множества. После выхода из цикла вычисляется цвет фрагмента. Если точка расположена внутри [ множества, используется внутренний цвет. Если точка на краю или вне множества,
320 Глава 15. Нефотореалистичные шейдеры происходит плавный переход от цвета края к внешнему цвету в зависимости от количества итераций. Полный код фрагментного шейдера приведен в листинге 15.7. Листинг 15.7. Фрагментный шейдер множества Мандельброта varying varying vec3 Position; fl oat Lightintensity uniform float Maxiterations; uni form float Zoom: uni form float /center: uni form float Ycenter: uniform vec3 InnerColor; uni form vec3 OuterColorl; uniform vec3 0uterColor2: void main(void) ( float real = Position./ * Zoom + /center: float imag = Position.у * Zoom + Ycenter: float Creal = real; // Эту строку нужно поменять... float Cimag = imag: // ...и эту тоже для множества Джулии float г2 = 0.0: float iter; for (iter = 0.0; iter < Maxiterations && r2 < 4.0; ++iter) ( float tempreal = real; real = (tempreal * tempreal) - (imag * imag) + Creal; imag = 2.0 * tempreal * imag + Cimag: r2 = (real * real) + (imag * imag): } // Цвет зависит от количества итераций vec3 color; if (r2 < 4.0) color = InnerColor: el se color = mix(OuterColorl, 0uterColor2. fract(iter * 0.05)): color *= Lightintensity: gl_FragColor = vec4 (color. 1.0); } Очевидно, что этот шейдер можно совершенствовать. Сначала можно усовер- шенствовать алгоритм выбора цвета, например, используя одномерную текстуру для хранения значений цвета. Количество итераций в этом случае может служить индексом в текстурной таблице. После выбора приятного набора цветов можно рассмотреть несколько популяр- ных частей множества Мандельброта. Различные книги и веб-сайты публикуют ко-
15.3. Пример Мандельброта 321 ординаты интересных частей множества, и эти шейдеры исполнены таким образом, чтобы можно было самому ввести координаты и сразу увидеть результаты (рис. 15.8). Скипетр Х = -1,36 Y = 0,005 Завитки X = -0,0002 Y = 0,7383 Рис. 15.8. Результаты работы шейдера Мандельброта Мини-Мандельброт Х = -1,75 Y = 0,0 15.3.4. Множества Джулии Множества Джулии немного похожи на множество Мандельброта. Каждая точка в множестве Мандельброта может использоваться для создания множества Джу- лии, и эти новые множества не менее интересны. Единственное их отличие в том, что константа с в уравнении Z2 + с устанавливается в значение какой-нибудь точ- ки, входящей в множество Мандельброта (кроме той, которую рисуют в данный момент). Чтобы изменить фрагментный шейдер соответствующим образом, нуж- но поменять две строки кода, инициализирующие значение с: float Creal = -1.36: // Теперь мы увидим интересное множество float Cimag = 0.11; На рис. 15.9 показано несколько примеров множеств Джулии, которые можно создавать с помощью этого шейдера. Для этого можно параметризовать значения Creal и Ci mag, передавая их через uniform-переменные. Х = 0,0 Y = 0,7383 real = -1,5 imag = О.-О Х = -1,36 Y = 0,005 real = -0,765 imag = 0,11 Рис. 15.9. Множества Джулии, нарисованные шейдером Мандельброта Х = 0,0 Y = 0,0 real = —0,32 imag = 0,043 И Зак. 218
322 Глава 15. Нефотореалистичные шейдеры 15.4. Итоги Далеко не во всех интерактивных графических программах нужен реализм. Ис- пользуя современные возможности графического аппаратного обеспечения, лег- ко можно создавать любые нужные эффекты. С помощью высокоуровневого про- цедурного языка, такого как язык шейдеров OpenGL, художники и создатели графических программ могут программировать алгоритмы для рисования в худо- жественных стилях, имитируя, например, перо, гравюру, живопись. В этой главе был представлен процедурный шейдер штриховки, который иллюстрирует про- цесс создания таких шейдеров. Различные виды технических иллюстраций мож- но создавать с помощью шейдера Гуча, описанного в этой главе. Можно также создавать шейдеры для визуализации математических функций, например шей- деры Мандельброта и Джулии. История искусства давно доказала, что существует бесконечное множество ху- дожественных стилей. С помощью языка шейдеров OpenGL можно создавать шей- деры, имитирующие некоторые из этих стилей и, возможно, создающие новые сти- ли. 15.5. Ссылки Нефотореалистичному рендерингу посвящены книги [10 и 16]. Алгоритм Гуча определен и описан в докладе [8]. Доклад основан на принци- пах, описанных Эдвардом Тафте в 1997 г. Небольшое обсуждение NPR приводит- ся в книге [1]. Классическое описание фракталов и множества Мандельброта приведено в книге [12]. В 1986 г. Хайнц-Отто Питжен и Петер Рихтер написали книгу [14]. 1. Akenine-МцНег Т., Haines Е. Real-Time Rendering. 2nd ed. Natick, MS: A К Peters, Ltd., 2002 (http://www.reaLtimerendering.com). 2. Личный веб-сайт Поля Дербишира. Краткая информация по множеству Ман- дельброта (http://www.globalserve.net/~derbyshire/manguide.html). 3. Freudenberg В., Masuch М., StrothotteT. Walk-Through Illustrations: Frame- Coherent Pen-and-ink Style in a Game Engine//Computer Graphics Forum. 2001. Vol. 20, № 3 (http://isgwww.cs.uni-magdeburg.de/~bert/publications). 4. Freudenberg B„ Masuch M., Strothotte T. Real-Time Halftoning: A Primitive For Non-Photorealistic Shading // Rendering Techniques 2002, Proceedings 13th Eurographics Workshop. 2002. P. 227-231 (http://isgwww.cs.uni-magdeburg.de/ -bert/publications). 5. Freudenberg B., Masuch M„ Non-Photorealistic Shading in an Educational Game En- gine//SIGGRAPH and Eurographics Campfire. 1-4 June. 2002 (http://isgwww.cs.uni- magdeburg.de/ ~bert/pubLications). 6. Freudenberg B. Real-Time Stroke-based Halftoning//Ph. D. thesis/University of Magdeburg. 2003. 7. Gooch A. Interactive Non-Photorealistic Technical Illustration//Master’s thesis/ University of Utah. 1998. December (http://www.cs.utah.edu/~gooch/publication.htmL).
15.5. Ссылки 323 8. ANon-Photorealistic Lighting Model for Automatic Technical Illustration/А. Gooch, B. Gooch, P. Shirley, E. Cohen//Computer Graphics (Proc. SIGGRAPH-98). 1998. July. P. 447-452 (http://www.cs.utah.edu/~gooch/pubLication.html). 9. Interactive Technical Illustration / B. Gooch, P.-P. J. Sloan, A. Gooch, at al.// Proc. Symposium on Interactive 3D Graphics. April. 1999. P. 31-38 (http:// www.cs.utah.edu/~gooch/publication.html). 10. Gooch B., Gooch A. Non-Photorealistic Rendering. Natick, MS: A К Peters, Ltd., 2001 (http://www.cs.utah.edu/~gooch/book.html). 11. Lander J. Under the Shade of the Rendering Tree//Game Developer Magazine. 2000. Vol. 7, № 2. P. 17-21 (http://www.darwin3d.com/gdm2000.htm). 12. Mandelbrot В. B. The Fractal Geometry of Nature, Updated and Augmented. New York: W. H. Freeman and Company, 1983. 13. Mitchell J. L. Image Processing with Pixel Shaders in Direct3D / Ed. by W. Engel // ShaderX, Wordware. 2002. May (http://www.pixelmaven.com/jason). 14. Peitgen H.-О., Richter P. H. The Beauty of Fractals, Images of Complex Dynamical Systems. Berlin; Heidelberg: Springer-Verlag, 1986. 15. The Science of Fractal Images/H.-O. Peitgen, D. Saupe, M. F. Barnsley, at al. New York: Springer-Verlag, 1988. 16. Strothotte T., Schlectweg S. Non-Photorealistic Computer Graphics, Modeling, Rendering, and Animation. San Francisco: Morgan Kaufmann Publishers, 2002. 17. Tufte E. Visual Explanations. Cheshire: Graphics Press, 1997.
Шейдеры для обработки изображения Одно из основных преимуществ OpenGL над другими графическими API состо- ит в том, что в OpenGL всегда были возможности и для обработки изображений, и для рендеринга трехмерных объектов. Например, приложение для создания ви- деоэффектов задает последовательность изображений (видеопоток), и она накла- дывается на трехмерный объект в виде текстуры. OpenGL может дополнительно обрабатывать изображения, например, преобразованием цвета, а также наклады- вать дополнительные эффекты освещения, дымки. Первая версия реализации языка шейдеров OpenGL выполняла в основном трех- мерный рендеринг. Дополнительные возможности обработки изображений плани- руются в следующей версии. Однако и в существующей версии уже есть некото- рые операции обработки изображения, которые можно использовать в шейдерах. В этой главе будут описаны шейдеры, обрабатывающие не трехмерные геомет- рические примитивы, а двухмерные изображения. Этап растеризации в OpenGL может быть разделен на пять разных частей в за- висимости от типа примитива. Это растеризация точки, растеризация линии, ра- стеризация многоугольника, растеризация пиксельного прямоугольника и расте- ризация изображения. Результат каждой из этих операций может передаваться во фрагментный шейдер, поэтому программируемые операции можно выполнять пе только над геометрическими данными, но и над изображениями. Во фрагментном шейдере можно обрабатывать как фрагменты, созданные gl Bitmap или gl DrawPI xel s, так и значения, полученные из текстурной карты. Опе- рации работы с изображением можно разделить на две основные категории: один пиксел за раз и несколько пикселов за раз. Операции, относящиеся к первой кате- гории, можно реализовать во фрагментном шейдере на языке шейдеров OpenGL довольно просто. Из varying-переменной gl_Col or шейдер получает цвет фрагмен- тов, созданных gl Bitmap или glDrawPIxel s. Операции первой категории тоже мож- но выполнять, но для этого сначала нужно сохранить изображение в текстурной памяти, к которой затем выполняется несколько обращений из шейдера. Преимущество использования шейдера OpenGL для операций с изображени- ями состоит в том, что обычно они выполняются на графическом оборудовании значительно быстрее, чем на основном процессоре, так как в современных графи- ческих акселераторах многие операции могут выполняться параллельно. К тому же основной процессор при этом освобождается для других задач. Если изобра- жение сохраняется как текстура, над ним в реальном времени можно выполнять операции коррекции цвета, удаления шума, увеличения резкости и пр. с очень небольшой нагрузкой шины ввода-вывода.
16.2. Математические отображения 325 6.1. Геометрические преобразования зображения В стандартном OpenGL определена только одна операция над геометрическими размерами изображения: пиксельное масштабирование. Эта операция масштаби- рует изображение перед отображением на экране. Для того чтобы повернуть изображение, предлагалось загрузить его в тек- стурную память, наложить как текстуру на прямоугольник и затем поворачи- вать прямоугольник. Эти операции требуют обращения к текстурному моду- лю, но все равно скорость работы на современном оборудовании для большинства приложений является приемлемой. На языке шейдеров OpenGL даже можно задавать одни и те же координаты прямоугольника при рисовании изображе- ния, чтобы вершинный шейдер выполнял масштабирование, преобразование, поворот прямоугольника. Деформировать изображение можно, накладывая его на многоугольник сложной формы вместо простого прямоугольника. Аппарат- ная поддержка этих операций позволяет получать высококачественные ре- зультаты. 6.2. Математические отображения Самые общие операции обработки изображения, поддерживаемые стандарт- ным OpenGL, — это масштабирование и смещение. При этом каждый компонент цвета умножается на коэффициент масштабирования, а значение смещения к не- му прибавляется. С помощью этих операций можно отображать значения цвета с одной линейной последовательности на другую, а делается все это стандартны- ми арифметическими операциями. Также можно выполнять более сложные мате- матические отображения пиксельных значений встроенными функциями pow, ехр2, 1од2. Встроенная функция dot может возвращать значение интенсивности, используя значение цвета. Чтобы вычислить значение яркости по цветовой системе Между- народной комиссии по освещению (МКО, или CIE) на основе обычных RGB-зна- чений, определенных в соответствии с цветовым стандартом телевидения повы- шенной четкости (HDTV), можно сделать так: float luminance = dot(vec3 (0.2125. 0.7154, 0.0721), rgbColor): HDTV — это стандарт для современных промышленных мониторов, который определяет линейное цветовое пространство RGB. Коэффициенты 0,299, 0,587, 0,114 часто используются для преобразования значений RGB в значения ярко- сти. Эти значения были установлены Национальным комитетом по телевизион- ным системам США (НТСЦ, NTSC) в 1953 г., чтобы вычислять яркость для мо- ниторов и преобразовывать нелинейные значения RGB в значения яркости. Для современных мониторов эти значения вычисляют яркость не совсем точно, но вполне подходят для вычисления нелинейных значений яркости из нелинейных значений RGB: float luma = dot(vec3 (0.299, 0.587. 0.114). rgbColor):
326 Глава 16. Шейдеры для обработки изображения 16.3. Операции поиска в таблице В OpenGL определено несколько таблиц, и часть из них используется для работы с изображениями. Таблицы — это простой способ отображения входного значения на одно или несколько значений, что помогает при обработке цвета. Так как в OpenGL появилась возможность работы с программируемыми процессорами, вход- ные значения можно вычислять в шейдере, затем выполнять поиск в таблице и ис- пользовать полученные значения при дальнейших вычислениях в том же шейде- ре. Это открывает множество новых возможностей использования таблиц поиска. Один из эффективных способов выполнить операцию поиска в шейдере OpenGL — использовать одномерную текстурную карту. Таблица поиска может быть произ- вольного размера (хотя во многих реализациях OpenGL этот размер ограничен 256 записями), и в ней можно отображать входное значение как на одно выходное значение, так и на двух-, трех- или четырехкомпонентное значение. Если возвра- щаемые значения должны быть дискретными, режим одномерной текстуры уста- навливается в GL_NEAREST. Если же нужна интерполяция между соседними значе- ниями, устанавливается GL_LINEAR. Значение интенсивности в диапазоне [0,1] можно принять в качестве текстурной координаты для доступа к одномерной текстуре. Встроенные функции работы с текстурами всегда возвращают значение RGBA. Если одноканальная текстура связана с текстурным модулем, на который указывает семплер, значение тексту- ры будет храниться во всех каналах (красном, зеленом и синем), и можно взять значение из любого канала: float color = texturelD(lut. intensity).г: // GL_PIXEL_MAP_I_TO_I Поиск по таблице отображения интенсивности на RGBA-значение можно вы- полнить простым обращением к текстуре: vec4 color = texturelDdut. intensity); // GL_PIXEL_MAP_I_TO_R.G.B.A Поиск по таблице отображения RGBA-значения на RGBA-значение требует четырех обращений к текстуре: vec4 colorOut: colorOut.r = texturelDdut. colorln.r).r; II GL_PIXEL_MAP_R_TO_R colorOut.g = texturelDdut. colorin.g) .g; // GL_PIXEL_MAP_G_TO_G colorOut.b = texturelDdut. colorin.b) .b: // GL_PIXEL_MAP_B_TO_B colorOut.a = texturelDdut. colorin.a) .a; // GL_PIXEL_MAP_A_TO_A 16.4. Преобразования цвета В шейдерах OpenGL можно реализовать все разнообразие преобразований цветов. В этом разделе читатель увидит, как преобразовать CIE в RGB, и наоборот. Преоб- разования между другими системами цвета можно выполнить похожим способом. Система CIE была определена в 1931 г. Международной комиссией по осве- щению. Эта цветовая система не зависит от каких-либо устройств и основана на восприятии цвета человеческим зрением. Комиссия установила некий первичный набор, XKZ, соответствующий восприятию цвета сетчаткой глаза. После опытов с наблюдателями-людьми первичный набор был определен так, что весь видимый
16.5. Интерполяция и экстраполяция изображения 327 цвет попадает в сочетания положительных значений X, У и Z, а от У зависит, на- сколько светлым будет оттенок цвета. В CIE-системе может быть определен лю- бой видимый цвет. Цвета можно преобразовывать из систем, зависящих от конк- ретного устройства, в системы, зависящие от других устройств, или в какую-либо общую систему. Представление данных в виде таблицы как раз подходит для выполнения та- ких преобразований. Стандарт HDTV имеет следующий набор первичных дан- ных и использует белую точку D65 (природное солнечное освещение). Координата R G В White X 0,640 0,300 0,150 0,3127 У 0,330 0,600 0,060 0,3290 z 0,030 0,100 0,790 0,3582 В листинге 16.1 приведен шейдер OpenGL, который преобразует значения цвета из системы CIE в RGB-значения стандарта HDTV с помощью точки Des, а в лис- тинге 16.2 показано обратное преобразование. Таблицы, взятые из специальной литературы, выглядят транспонированными, так как в языке шейдеров OpenGL таблицы передаются по столбцам. Листинг 16.1. Код шейдера OpenGL для преобразования значений CIE в RGB const mat3 CIEtoRGBmat = mat3 ( 3.240479, -0.969256. 0.055648. -1.537150. 1.875992. -0.204043. -0.498535. 0.041556. 1.057311); vec3 rgbColor = cieColor * CIEtoRGBmat: Листинг 16.2. Код шейдера OpenGL для преобразования значений RGB в CIE const mat3 RGBtoCIEmat = mat3 ( 0.412453. 0.212671. 0.019334. 0.357580. 0.715160. 0.119193. 0.180423. 0.072169. 0.950227); vec3 cieColor = rgbColor * RGBtoCIEmat: 16.5. Интерполяция и экстраполяция изображения В 1994 г. Поль Хиберли и Дуглас Вухис опубликовали интересную статью, в ней рас- сказывается про операции с изображением, которые можно выполнять с помощью операций интерполяции и экстраполяции. Вообще-то эти операции программи- руются на профессиональных графических системах, но сейчас их легко выпол- нять и на обычных графических ускорителях с помощью языка шейдеров OpenGL. Этот метод очень прост. Нужно определить конечное изображение (target), которое можно использовать совместно с исходным изображением (source) для интерполяции и экстраполяции. Уравнение является простой линейной интер- поляцией, простой переход между двумя изображениями: Imageout =(1-ос) Image target + oclmagesource.
328 Глава 16. Шейдеры для обработки изображения Конечное изображение — то, с которым нужно интерполировать или экстра- полировать исходное изображение. Значения 0 < ос < 1 интерполируются между двумя изображениями, а значения ос > 1 экстраполируются. Если ос = 1, результа- том является исходное изображение. Если ос = 0, в результате все пикселы полу- чаются черными. Если 0 < ос < 1, результатом является линейный переход от ис- ходного изображения к черному, то есть плавное затемнение изображения. Если а > 1, изображение, наоборот, постепенно осветляется. Такие операции можно выполнять с изображениями (в терминах OpenGL — пик- сельными прямоугольниками) во фрагментном шейдере в том порядке, в котором они отправляются на экран. В случае если конечное изображение действительно су- ществует (а как мы видели, во многих случаях его нет), его можно сохранить в тек- стуре и затем обращаться к этой текстуре из фрагментного шейдера. Если исходное и конечное изображения хранятся в памяти графического акселератора (то есть в текстурной памяти), эти операции выполняются довольно быстро, скорость ог- раничивается только временем доступа к памяти и скоростью выполнения опера- ций на графическом оборудовании. Это значительно быстрее выполнения тех же операций на основном процессоре и загрузки каждый раз по шине ввода-вывода. 16.5.1. Яркость Самый простой пример работы с изображениями — изменение яркости. Так как конечное изображение состоит только из черных пикселов (значение (0, 0, 0)), первая половина уравнения интерполяции стремится к нулю, и уравнение сво- дится к простому масштабированию исходных значений. Это реализовано во фраг- ментном шейдере, приведенном в листинге 16.3, а результаты для нескольких зна- чений Al pha показаны на цветном рис. 26. Листинг 16.3. Фрагментный шейдер для изменения яркости uniform float Alpha: void main (void) { gl_FragColor = gl_Color * Alpha: } 16.5.2. Контраст Более интересный пример — изменение контраста (листинг 16.4). Конечное изоб- ражение является серым, каждый пиксел содержит среднее значение яркости. Это значение и Al pha вычисляются приложением и передаются в шейдер как uniform-пе- ременные. Результаты работы шейдера контраста показаны на цветном рис. 27. Листинг 16.4. Фрагментный шейдер для изменения контраста uniform vec3 AvgLuminance: uniform float Alpha; void main (void) { vec3 color = mix(AvgLuminance. gl_Color, Alpha); gl_FragColor = vec4 (color. 1.0):
16.5. Интерполяция и экстраполяция изображения 329 16.5.3. Насыщенность цвета При изменении насыщенности цвета конечное изображение содержит только информацию о яркости (то есть является полутоновой версией исходного изображения). Его можно вычислить попиксельно, получив значение яркости из каждого значения RGB. Чтобы выполнить вычисления правильно, необходи- мо знать, в какой цветовой системе заданы значения RGB. Если они заданы в стандарте HDTV, можно использовать коэффициенты из шейдера (лис- тинг 16.5). Результат работы этого шейдера показан на цветном рис. 28. Как можно заметить, экстраполяция дает хорошие результаты для значений, боль- ших 1,0. Листинг 16.5. Фрагментный шейдер для изменения насыщенности цвета const vec3 lumCoeff = vec3 (0.2125. 0.7154. 0.0721): uniform float Alpha: void main (void) { vec3 intensity = vec3 (dot(gl_Color.rgb. lumCoeff)): vec3 color = mix(intensity. gl_Color.rgb. Alpha): gl_FragColor = vec4 (color. 1.0): 16.5.4. Резкость Метод уменьшения резкости приведен в листинге 16.6. Конечное изображение можно создать размыванием исходного изображения или другим способом. Ин- терполяция между изображениями уменьшит высокие частоты, а экстраполяция (Alpha больше 1) увеличит. В результате будет создана маска уменьшения контра- стности. Результаты выполнения этого фрагментного шейдера показаны на цвет- ном рис. 29. Листинг 16.6. Фрагментный шейдер для уменьшения резкости uniform sampler2D Blurry: uniform float Alpha: void main (void) { vec3 blurred = vec3 (texture2D(Blurry. gl_TexCoordLO].st)): vec3 color = gl_Color.rgb * Alpha + blurred * (1.0 - Alpha): gl_FragColor = vec4 (color. 1.0): } На этих примерах показан простой случай изменения целого изображения с из- менением простого значения Alpha. Возможна и более сложная обработка — на- пример, Alpha может быть значением функции от других переменных. Можно в какой-либо текстуре определить сложную форму, ограничивающую область изображения для внесения изменений, или выбрать несколько небольших облас- тей на изображении с помощью какого-либо шаблона, маски. Операцию можно выполнять выборочно, для пикселов с конкретным диапазоном яркости (тени, об- ласти повышенной яркости или средние тона).
330 Глава 16. Шейдеры для обработки изображения Фрагментные шейдеры можно использовать также для интерполяции более чем двух изображений, и не обязательно эта интерполяция будет линейной. Воз- можно интерполировать вдоль нескольких осей одновременно. Размытая полу- тоновая версия исходного изображения совместно с исходным изображением могут служить основой для создания за одну операцию насыщенного цветного изображения с хорошей резкостью. 16.6. Режимы плавного перехода Высокоуровневый язык программирования достаточно выразителен для того, что- бы можно было сочетать два изображения самыми разными способами. В нем либо оба изображения сохраняются в текстурной памяти, либо одно из них загружается приложением с помощью функции gl DrawPI xel s. Приведем несколько фрагментов кода шейдера OpenGL для плавного попиксельного перехода в разных режимах: □ base — vec4 — содержит значение цвета RGBA из исходного изображения; □ bl end — vec4 — содержит значение цвета RGBA из изображения, которое пере- ходит в исходное; □ result — vec4 — содержит значение цвета RGBA, результат операции плавного перехода; □ если понадобится для вычислений, white является vec4 и содержит (1,0,1,0,1,0,1,0); □ если понадобится для вычислений, 1 umCoeff является vec4 и содержит (0,2125, 0,7154, 0,0721, 1,0); □ для окончательных вычислений base и resul t сочетаются с помощью числа с пла- вающей запятой opaci ty (непрозрачность), которое определяет участие каждо- го компонента. Вероятно, результат выполнения приведенного кода не будет идентичен ре- зультату, полученному в излюбленной программе читателя, но эффекты будут очень похожими. Код шейдера OpenGL для некоторых режимов плавного пере- хода был создан с помощью информации, содержащейся в статье Дженса Груше- ля «Режимы плавного перехода», доступной по адресу http://www.pegtop.net/delphi/ blendmodes. Если явно не указано что-то иное, операции плавного перехода не ком- мутативные (если поменять местами исходное и конечное изображения, резуль- тат изменится). Результаты выполнения шейдеров плавного перехода в различных режимах показаны на цветном рис. 30. 16.6.1. Обычный Этот режим работы с изображением часто предполагается по умолчанию. Конеч- ное изображение помещается над исходным. Окончательное изображение такое же, как конечное, если значение прозрачности 1,0 (исходное изображение полно- стью закрыто). При других значениях прозрачности результатом будет сочетание двух изображений, основанное на значении opacity: result = blend:
16.6. Режимы плавного перехода 331 16.6.2. Среднее значение В этом режиме два изображения складываются и результат делится пополам. Сде- лать это — все равно что применить обычный режим со значением opacity, рав- ным 0,5. Операция коммутативна: result = (base + blend) * 0.5: 16.6.3. Растворение В каждом пикселе Ы end или base выбирается случайным образом. Значение opaci ty используется как коэффициент вероятности выбора значения blend. Чем ближе opacity к 1,0, тем выше вероятность выбора blend, а не base. Если изображение нарисовано как текстура на прямоугольнике, значения текстурных координат можно использовать как аргумент функции nolselD, чтобы получить значение, которое можно использовать для выбора псевдослучайного числа, периодически встречающегося в последующих выборках. Здесь можно применять и коэффици- ент масштабирования для того, чтобы получить шум большей частоты и создать иллюзию случайности. Значение функции шума будет расположено в диапазоне [-1, 1 ], так что нужно добавить к нему 1 и умножить на 0,5, чтобы получить число в диапазоне [0, 1]: float noise = (no1sel(vec2 (gl_TexCoord[0] * noiseScale)) + 1.0) * 0.5: result = (noise < opacity) ? blend : base: 16.6.4. «Позади» В этом режиме значение bl end выбирается только тогда, когда основное изображе- ние полностью прозрачно (base. а = 0,0). При использовании этого режима создает- ся эффект изображения, нарисованного на обратной стороне прозрачного полот- на: будут видны только области, расположенные напротив прозрачных пикселов: result = (base.а == 0.0) ? blend : base: 16.6.5. Очистка Здесь всегда используется значение bl end и прозрачность результата установлена в 0 (прозрачность). Этот режим больше подходит для различных инструментов рисования, чем для целых изображений: result.rgb = blend.rgb: result.a =0.0: 16.6.6. Затемнение В режиме затемнения сравниваются два значения и для каждого компонента вы- бирается минимальное из них. Эта операция сделает изображения темнее. Цвет совершенно белого изображения (RGB = 1,0,1,0,1,0) не изменится. При обработ- ке областей черного цвета в любом изображении (0, 0, 0) получатся такие же чер- ные области. Это коммутативная операция — результат не изменится, если изоб- ражения поменять местами: result = m1n(blend. base):
332 Глава 16. Шейдеры для обработки изображения 16.6.7. Осветление Этот режим противоположен затемнению. Вместо того чтобы выбирать минималь- ное значение для каждого компонента, выбирается максимальное значение. Эта операция тоже коммутативна: result = max(blend. base): 16.6.8. Умножение В этом режиме значения яркости двух изображений перемножаются. Окончатель- ное изображение получится темнее везде, где ни одно изображение не белое. Бе- лый цвет здесь является оператором прозрачности, так как любой цвет, умножен- ный на белый, не изменится. Области черного цвета (0, 0, 0) в любом изображении дадут в результате черный цвет. Это похоже на наложение двух полупрозрачных цветов на диапроекторе. Операция коммутативна: result = blend * base: 16.6.9. Экран Режим экрана противоположен умножению, поскольку здесь умножаются значе- ния, противоположные исходным. Оператором прозрачности является черный цвет. Этот режим коммутативен: result = white - ((white - blend) * (white - base)): 16.6.10. Усиление цвета В режиме усиления цвета основной цвет затемняется дополнительным цветом (уменьшается яркость). Если значение Ы end — белый цвет, эффекта в этой точке не будет. При вычислениях иногда могут получиться отрицательные значения, которые будут отброшены при приведении окончательного цвета к конкретному диапазону: result = white - (white - base) / Mend: 16.6.11. Ослабление цвета Этот режим делает цвета более светлыми. Если значение blend — черный цвет, эф- фекта в этой точке не будет. При вычислении могут получиться значения больше 1, которые затем отбрасываются так же, как и при реализации предыдущего режима: result = base / (white - blend): 16.6.12. Наложение В этом режиме сначала вычисляется яркость основного изображения. Если зна- чение яркости меньше 0,5, значения blend и base умножаются. Если значение яр- кости больше 0,5, выполняется та же операция, что и в режиме экрана. Эффект состоит в том, что значение base смешивается со значением Ы end, а не заменяется им. Это позволяет шаблонам и цветам перекрывать основное изображение, со-
16.6. Режимы плавного перехода 333 храняя тени и освещенные участки. При значении яркости равном 0,5 получается разрыв, поэтому нужно создать в этих местах плавный переход между уравнения- ми для яркости в диапазоне [0,45, 0,55]: float luminance = dot(base. lumCoeff): if (luminance < 0.45) result = 2.0 * Mend * base: else if (luminance > 0.55) result = white - 2.0 * (white - blend) * (white - base): el se vec4 resultl = 2.0 * blend * base: vec4 result2 = white - 2.0 * (white - blend) * (white - base): result = mix(resultl. result2. (luminance - 0.45) * 10.0): 16.6.13. Мягкое освещение При использовании этого режима получается эффект, похожий на мягкий свет, проходящий через вспомогательное изображение и падающий на исходное. В ре- зультате получится сочетание двух изображений, освещенное приглушенным светом: result = 2.0 * base * blend + base * base - 2.0 * base * base * blend: 16.6.14. Жесткое освещение Этот режим похож на наложение, за исключением того, что яркость вычисляется на основе значения blend, а не base. Пикселы во вспомогательном изображении с яркостью 0,5 не будут влиять на основное изображение. Этот режим часто ис- пользуется при создании эффектов рельефа. Для получения плавного перехода яркости в диапазоне [0,45, 0,55] используется функция mix: float luminance = dot(blend. lumCoeff); if (luminance < 0.45) result = 2.0 * blend * base: else if (luminance > 0.55) result = white - 2.0 * (white - blend) * (white - base): el se vec4 resultl = 2.0 * Mend * base: vec4 result2 = white - 2.0 * (white - blend) * (white - base): result = mix(resultl. result2. (luminance - 0.45) * 10.0): 16.6.15. Сложение В этом режиме происходит сложение исходного и вспомогательного изображе- ний. Так как в результате может получиться значение, большее 1,0, происходит отсечение. Если изображения поменять местами, результат не изменится: result = Mend + base:
334 Глава 16. Шейдеры для обработки изображения 16.6.16. Вычитание В этом режиме вспомогательное изображение вычитается из основного. Если зна- чение получается меньше 0, происходит отсечение: result = base - blend: 16.6.17. Разность Результат применения этого режима — абсолютное значение разности между bl end и base. Черный цвет результата означает, что изначальные значения одинаковы, а белый цвет — что они противоположны. Еще один вариант использования этого режима — сравнение изображений, так как полная их идентичность даст полнос- тью черный цвет. А совершенно белое вспомогательное изображение может ис- пользоваться для инвертирования исходного изображения. Этот режим является коммутативным: result = abs(blend - base): 16.6.18. Инверсная разность В этом режиме выполняется операция, обратная разности. Белое и черное значе- ния bl end дают те же значения, что и для режима разности (белый цвет инверти- рует, а черный не влияет на результат), но цвета между белым и черным становят- ся светлее, а не темнее. Эта операция является коммутативной: result = white - abs(white - base blend): 16.6.19. Исключение Этот режим похож на разность, но результат получается более мягким, немного менее контрастным. Это нечто среднее между разностью и инверсной разностью. Этот режим также является коммутативным: result = base + blend - (2.0 * base * blend): 16.6.20. Прозрачность Значение прозрачности в диапазоне [0, 1] также можно использовать для опреде- ления участия основного изображения в формировании результата. Значение result из любой приведенной ранее формулы можно впоследствии изменить та- ким образом: finalColor = m1x(base. result, opacity): 16.7. Свертка Свёртка — это общая операция обработки изображения, которая может исполь- зоваться для фильтрации изображения. Это происходит с помощью вычисления суммы исходного изображения и меньшего изображения, называемого ядром свёртки или свёрточным фильтром. В зависимости от выбора значений в ядре
16.7. Свертка 335 операция свертки может выполнять размывку, увеличение резкости, уменьшение шума, выделение краев и другие полезные операции. Математически дискретная двухмерная операция свёртки определена так: height—1 width-1 Я (*,*/) = Е Е F (х + i’ у + >)G0> J) • у=0 i=0 В этом уравнении функция Fпредставляет основное изображение, a G — ядро свёртки. Двойная сумма основана на ширине и высоте ядра свёртки. Значение конкретного пиксела в окончательном изображении вычисляется совмещением центра ядра свёртки с пикселом в той же точке, что и в основном изображении, и умножением значений из основного изображения на значения из ядра. Затем полученные значения суммируются. Набор функций для работы с изображениями в OpenGL 1.5 включает в себя функции для свёртки, но реализация этой функциональности всегда ограничива- ет размер ядра (обычно это 3 х 3) и точность промежуточных операций. Гибкость языка шейдеров OpenGL позволяет задавать ядро практически любого размера и точность, соответствующую операциям над числами с плавающей запятой. Бо- лее того, операция свёртки может быть написана в шейдере OpenGL таким обра- зом, чтобы выполнялось минимальное количество умножений (например, нуле- вые значения ядра вообще исключаются из вычислений). Здесь, однако, есть несколько препятствий. Эта операция кажется обычной для реализации во фрагментном шейдере, но фрагментный шейдер не может полу- чать данные о соседних фрагментах. Решением проблемы может быть сохранение основного изображения в текстурной памяти и обращение из фрагментного шей- дера к текстуре. Чтобы хорошо выполнить рендеринг изображения, можно взять основное изображение в виде прямоугольника, размер которого равен размеру экрана, и включить текстурирование. В этом процессе вполне может участвовать фрагментный шейдер, и в этом случае можно получить доступ к любому значе- нию ядра и вычислять результат для каждого пиксела. Еще одна проблема состоит в том, что язык шейдеров OpenGL хотя и поддер- живает циклы, даже вложенные, но не поддерживает двухмерные массивы. В прин- ципе можно «раскрутить» ядро свёртки и хранить его в одномерном массиве, в каж- дой ячейке которого будут храниться х- и г/-координаты (сдвиг) от центра ядра и значение в этой точке. Во фрагментном шейдере можно обработать весь массив в одном цикле, прибавляя указанный сдвиг к текущим текстурным координатам, обращаясь к основному изображению и умножая полученное значение пиксела на значение из ядра. Если ядро свёртки будет храниться в таком виде, значения можно записывать в массив как угодно: по строкам, по столбцам, в обратном по- рядке или вообще в произвольном порядке. В массив даже не нужно включать нулевые значения ядра, так как они никак не влияют на результат. Вершинный шейдер здесь должен выполнять тождественное отображение вход- ных геометрических фигур (а именно прямоугольника, размер которого по гори- зонтали соответствует размеру основного изображения) и передавать текстурные координаты. Текстурная координата (0, 0) должна ассоциироваться с нижним левым углом прямоугольника, а текстурная координата (1, 1) — с верхним пра- вым углом прямоугольника.
336 Глава 16. Шейдеры для обработки изображения В случае если ядро свёртки выходит за край основного изображения, требуется особое решение. Можно использовать один из побочных эффектов текстурных операций OpenGL: если приложение хочет нарисовать край так, чтобы получился результат в режиме GL_CONSTANT_BORDER, текстура с основным изображением должна содержать в GL_TEXTURE_WRAP_S nGL_TEXTURE_WRAP_T параметр GL_CLAMP_TO_BORDER. Если же нужен край вида GL_REPLICATE_BORDER (при выходе ядра за край берется крайний пиксел изображения), то параметры текстуры GL_TEXTURE_WRAP_S и GL_TEXTURE_WRAP_T должны быть установлены в GL_CLAMP_TO_EDGE. Л если нужен режим GL_REDUCE, при- ложение должно рисовать прямоугольник, немного меньший свертываемого изоб- ражения. Для размера ядра 3x3 прямоугольник должен быть меньше на 2 пиксела в ширину и на 2 пиксела в высоту. В этом случае текстурная координата нижнего левого угла будет (1/width, 1/height), а текстурная координата верхнего правого угла —(1 - 1/width, 1 - 1/height). Режим текстуры нужно установить в GL_NEAREST, чтобы избежать ненужной интерполяции. Сложность заключается также в том, что OpenGL требует, чтобы текстуры имели размеры, кратные степени двойки и по ширине, и по высоте (такие изобра- жения в наше время — редкость). Чтобы обойти это ограничение, можно увели- чить изображение до следующей ближайшей степени двойки, а затем соответствен- но сдвинуть текстурные координаты на прямоугольнике. Это может привести (а может и не привести) к излишнему расходованию текстурной памяти, что за- висит от того, поддерживает ли графический ускоритель виртуальное текстури- рование (загрузка части текстур в графическое оборудование по запросу). Суще- ствуют расширения от разных производителей, с помощью которых эту проблему пытаются решить. OpenGL ARB работает над тем, чтобы создать общее решение и включить его в стандарт OpenGL. 16.7.1. Сглаживание Операции сглаживания изображений нужны для того, чтобы приглушить высо- кие частоты. Общая операция сглаживания называется усреднение соседних эле- ментов. Этот метод использует ядро свертки со значением 1,0 в каждой точке. Окончательная сумма делится на количество точек в ядре. Например, сверточный фильтр для усреднения размером 3x3 выглядит примерно так: 1 1 1 1 1 1 1 1 1 Окончательная сумма делится на 9 (или умножается на 1/9). Этим методом вычисляется среднее значение окружающих заданную точку пикселов. В резуль- тате изображение получается более мягким, размытым. Так как все элементы ядра равны 1, можно написать упрощенный фрагмент- ный шейдер для усреднения (листинг 16.7). Этот шейдер можно использовать для ядра любого размера, где width height < 25 (например, 5 х 5). Результаты работы этого шейдера показаны на рис. 16.1, б. Листинг 16.7. Фрагментный шейдер для свертки методом усреднения // Максимальный размер для данного шейдера const int MaxKernelSize = 25:
16.7. Свертка 337 // Массив со значениями сдвигов для доступа к основному изображению uniform vec2 Offset[MaxKernelSize]: // Размеры ядра (width * height) uniform int Kernel Size: // Масштаб uniform vec4 ScaleFactor; // Основное изображение uniform sampler2D Baseimage: void main(void) int i: vec4 sum = vec4 (0.0); for (i = 0: i < KernelSize: i++) sum += texture2D(BaseImage. gl_TexCoord[0].st + Offset!!]): gl_FragColor = sum * ScaleFactor; Этот способ сглаживания обычно применяется для уменьшения шума. Прием хоро- шо работает в областях сплошного цвета или интенсивности, однако получается побоч- ный эффект размывания краев областей. Избежать этой проблемы поможет сверточ- ный фильтр, например фильтр Гаусса, который можно задать в ядре таким образом: 1/273 4/273 7/273 4/273 1/273 4/273 16/273 26/273 16/273 4/273 7/273 26/273 41/273 26/273 7/273 4/273 16/273 26/273 16/273 4/273 1/273 4/273 7/273 4/273 1/273 В листинге 16.8 приведен код для более общего шейдера свертки. Этот шейдер может работать с ядрами, содержащими до 25 элементов. В этом шейдере каждый элемент ядра должен умножаться на коэффициент масштабирования, поэтому не нужно масштабировать сумму. Листинг 16.8. Фрагментный шейдер для общих вычислений свертки // Максимальный размер ядра, поддерживаемый этим шейдером const int MaxKernelSize = 25: // Массив со значениями сдвигов для доступа к основному изображению uniform vec2 OffsetEMaxKernelSize]; // Размеры ядра (width * height) uniform int KernelSize; // Значение для каждой точки ядра uniform vec4 Kernel Vaiue[MaxKernelSize]: // Основное изображение uniform sampler2D Baseimage; продолжение &
338 Глава 16. Шейдеры для обработки изображения Листинг 16.8 {продолжение) void main(void) int Г. vec4 sum = vec4 (0.0); for (1 = 0; 1 < KernelSize: 1++) ( vec4 tmp = texture2D(Base!mage. gl_TexCoord[0].st + Offset[i]); sum += tmp * Kernel Vaiue[i]: gl_FragColor = sum; К исходному изображению (рис. 16.1, а) добавили шум, и получилось то, что изображено на рис. 16.1, в. Затем шум был удален с помощью фильтра Гаусса, ре- зультат показан на рис. 16.1, г. Следует заметить, что шум значительно уменьшился в областях с относительно постоянной интенсивностью. Рис. 16.1. Результаты различных операций: а— исходное черно-белое изображение 512 х 512; б — усреднение 5x5 соседних пикселов; в — добавление шума к оригиналу; г — понижение шума фильтром Гаусса С увеличением размеров ядра свертки количество необходимых обращений к текстуре увеличивается, как квадрат этих размеров. При использовании боль-
16.7. Свертка 339 ших ядер это может сильно повлиять на производительность. Некоторые ядра можно назвать делимыми, так как все действия с ядром размером widthxheight можно выполнить за два прохода, одномерными операциями widthxl и Ixheight. При этом для каждого пиксела будет сделана лишняя запись, но количество обра- щений к текстуре уменьшается с width • height до width + height. 16.7.2. Выделение края Еще одно довольно распространенное применение операции свёртки — выделе- ние края, то есть поиск разрывов в изменении уровня интенсивности. Один из методов выделения края подразумевает использование оператора Лапласа О 1 О 1 -4 1 О 1 О Можно поместить лапласовское ядро свертки во фрагментный шейдер, приве- денный в листинге 16.8. Результат работы этого шейдера показан на рис. 16.2. Рис. 16.2. Выделение края с помощью лапласовского ядра свертки (изображение масштабировано) 16.7.3. Увеличение резкости Распространенный способ увеличения резкости изображения — добавление ре- зультата наложения фильтра выделения края на исходное изображение. Для уп- равления степенью резкости используется коэффициент. Один из способов увеличения резкости изображения — применение отрица- тельного оператора Лапласа. Фильтр будет выглядеть так: 0-10 -1 4 -1 0-10 Фрагментный шейдер, реализующий этот способ, почти совпадает с шейдером из предыдущего раздела (листинг 16.9). Единственное отличие в том, что результат вы- полнения операции добавляется к исходному изображению. Перед добавлением
340 Глава 16. Шейдеры для обработки изображения он корректируется с помощью коэффициента, передаваемого приложением через uniform-переменную. Результаты изменения контрастности показаны на рис. 16.3. Листинг 16.9. Фрагментный шейдер для изменения резкости (контрастности) // Максимальный размер ядра, поддерживаемый шейдером const Int MaxKernelSize = 25; // Массив значений сдвигов для доступа к основному изображению uniform vec2 Offset[MaxKernelSIze]: // Размеры ядра (width * height) uniform Int KernelSize; // Значение для каждой точки ядра uniform vec4 Kernel Vaiue[MaxKernelSize]: // Коэффициент uniform vec4 ScaleFactor; // Исходное изображение uniform sampler2D Baseimage; void main(void) int i; vec4 sum = vec4 (0.0): for (i = 0; i < KernelSize; i++) vec4 tmp = texture2D(BaseImage. gl_TexCoord[0].st + OffsetEi]): sum += tmp * Kernel Vaiue[i]; vec4 baseColor = texture2D(BaseImage. vec2(gl_TexCoord[0])); gl_FragColor = ScaleFactor * sum + baseColor; } Рис. 16.3. Результат работы шейдера для изменения резкости (лапласовское изображение в центре масштабировано) 16.8. Итоги В дополнение к работе с трехмерными объектами, OpenGL может работать и с изображениями. Язык шейдеров OpenGL расширил возможности стандарт-
16.9. Ссылки 341 ного OpenGL, позволив выполнять программируемую обработку изображений. Программируемость и возможность параллельной обработки информации на гра- фическом аппаратном обеспечении позволяют выполнять операции над изобра- жениями гораздо быстрее, чем на обычном процессоре. Можно запрограммиро- вать множество операций обработки изображений: размывание, контрастность, удаление шума; цветокоррекцию; изменение насыщенности цвета, яркости, кон- траста, — а также геометрические преобразования, такие как повороты и дефор- мирование, смещение и множество других операций. Приложения больше не ог- раничены в манипулировании цветными и одноцветными изображениями. Можно выполнять также их спектральный анализ и обработку. Мир цветных образов, существующий в компьютерных системах, — это резуль- тат быстрого развития цифровой фотографии и цифрового видео. Язык шейде- ров OpenGL, несомненно, является одним из основных инструментов для под- держки этой революции в будущем. 6.9. Ссылки В литературе по OpenGL не всегда хорошо описаны возможности обработки изобра- жений. Автор этой книги в 1996 г. опубликовал статью [14], где попытался описать возможности стандартного OpenGL, включая несколько наиболее известных расши- рений. Эта статья доступна на веб-сайте http://3dshaders.com/pubs. Еще одна хоро- шая статья — [9] — была написана для SIGGRAPH-1999:. Эта статья доступна на веб-сайте http://www.opengL.org/developers/code/sig99/advanced99/notes/notes.html. Чарльз Пойнтон (1997) является одним из светил в технологии цвета, и его работы [ 12 и 13] очень информативны. Вот ссылка: http://www.poynton.com/Poynton- coLor.htm L. Краткая версия статьи Поля Хиберли и Дугласа Вухи [3] находится по адресу http://www.sgi.com/grafica/interp/index.html. 1. Gonzalez R. С., Woods R. Е. Digital Image Processing. 2nd ed. Upper Saddle River, NJ: Prentice Hall, 2002. 2. Gruschel J., Modes В. Веб-сайт для разработчиков (http://www.pegtop.net/delphi/ blendmodes). 3. Haeberli P., Voorhies D. Image Processing by Interpolation and Extrapolation// IRIS Universe Magazine. 1994. № 28 (http:/www.sgi.com/grafica/interp/index.html). 4. Hall R. Illumination and Color in Computer Generated Imagery. New York: Sprin- ger-Verlag, 1989. 5. International Lighting Vocabulary, Publication CIE№ 17.4//Joint publication IEC and CIE. Geneva, 1987 (http://www.cie.co.at/framepublications.htmL). 6. ITU-R Recommendation BT.709, Basic Parameter Values for the HDTV Standard for the Studio and for International Programme Exchange [formerly CCIR Rec. 709]. Geneva: ITU, 1990. 7. Lindbloom B. J. Accurate Color Reproduction for Computer Graphics Applications// Computer Graphics (Proc. SIGGRAPH-89). 1989. July. P. 117-126.
342 Глава 16. Шейдеры для обработки изображения 8. Lindbloom В. J. Личный веб-сайт (http://www.brucelindbloom.com). 9. Advanced Graphics Programming Techniques Using OpenGL/Т. McReynolds, D. Blythe, B. Grantham, S. Nelson//SIGGRAPH-99 course notes. 1999 (http:// www.opengl.org/developers/code/sig99/index.html). 10. Myler H. R., Weeks A. R. The Pocket Handbook of Image Processing Algorithms in C. Upper Saddle River, NJ: Prentice Hall, 1993. 11. Poynton Ch. A. A Technical Introduction to Digital Video. New York: John Wiley & Sons, 1996. 12. Poynton Ch. A. Frequently Asked Questions about Color. 1997 (http://www.poyn- ton.com/Poynton-color.html). 13. Poynton Ch. A. Frequently Asked Questions about Gamma. 1997 (http://www.poyn- ton.com/Poynton-color.html). 14. Rost R. Using OpenGL for Imaging // SPIE Medical Imaging-96 Image Display Conference. February. 1996 (http://3dshaders.com/pubs). 15. Wolberg G. Digital Image Warping. Wiley-IEEE Press, 2002.
Сравнение языков программирования Язык шейдеров OpenGL, без сомнения, является ведущим языком шейдеров. Рас- смотрим в этой главе несколько других языков шейдеров и сравним их с языком шейдеров Open GL. Диаграммы, приведенные в этой главе, можно сравнивать с мо- делью работы языка шейдеров OpenGL (см. рис. 2.4). 7.1. Хронология возникновения 1ейдерных языков Роб Кук и Кен Перлин обычно всегда принимают участие в разработке языков шейдеров, они работают с неинтерактивными системами. Перлин работал над функцией шума и управляющими структурами. Кук, работая в компании Lucasfilm (позже в Pixar), классифицировал шейдеры на шейдеры поверхности, шейдеры освещения, шейдеры атмосферы и т. д. и описал, как работает каждый, с помощью математических выражений. В результате был разработан полноценный язык программирования для описания вычислений, работу над которым продолжил Пат Хенрехен. Эта работа завершилась в 1988 г. выпуском компанией Pixar пер- вой версии спецификации языка RenderMan. Впоследствии RenderMan факти- чески стал стандартом для систем рендеринга, используемых в отрасли развлече- ний. Он и сейчас широко используется. Первый интерактивный язык шейдеров был представлен в университете Се- верной Каролины (UNC) на параллельной графической архитектуре, называемой PixelFlow, которая была разработана в 90-х гг. Этот язык шейдеров мог выпол- нять рендеринг сцен процедурно в количестве 30 кадров в секунду или даже боль- ше. Язык шейдеров этой системы был описан Марком Олано в 1998 г. После ухода из UNC Марк Олано присоединился к группе разработчиков ком- пании SGI, занимающейся определением и реализацией интерактивного языка шейдеров, который бы работал на основе OpenGL и использовал многопроходные методы рендеринга для выполнения шейдеров. Эта работа завершилась в 2000 г. выпуском продукта от SGI, названного OpenGL Shader, который стал первым ком- мерческим продуктом подобного рода. В июне 1999 г. лаборатория компьютерной графики в Стэнфорде начала рабо- ту над определением языка шейдеров реального времени, код которого мог бы выполняться на графическом акселераторе. Этот язык был назван «Стэнфорд- ский язык шейдеров реального времени». Результаты были показаны в 2001 г.
344 Глава 17. Сравнение языков программирования Язык шейдеров OpenGL, HLSL от Microsoft, Cg от NVIDIA являются приме- рами коммерчески жизнеспособных высокоуровневых языков шейдеров реаль- ного времени. Доклад о языке, который позже и стал языком шейдеров OpenGL, опубликован в октябре 2001 г. Дэйвом Болдуином. Спецификация Cg была опуб- ликована в июне 2002 г., а спецификация HLSL — в ноябре 2002 г. как часть бета- версии DirectX 9.0 SDK. При этом происходил и некоторый обмен идеями между компаниями, так что языки в чем-то похожи. В следующих разделах сравним язык шейдеров OpenGL с другими коммер- ческими высокоуровневыми языками шейдеров. 17.2. Язык RenderMan В 1998 г., после нескольких лет разработки, компания Pixar опубликовала специ- фикацию языка RenderMan (рис. 17.1). Это был интерфейс, в котором определя- лись формат передаваемых сообщений, соглашения и правила сообщения про- грамм моделирования и программ рендеринга, конечной целью применения которого является получение фотореалистичных изображений. Эта спецификация сразу нашла при-менение, и очень успешное, в мультипликации. Интерфейс RenderMan использовался для создания спецэффектов в фильмах «Парк юрского периода», «Звездные войны. Эпизод 1», «Властелин колец: две башни» и др., а также в муль- тфильмах «В поисках Немо», «Сказка об игрушках», «Жизнь насекомых» и «Кор- порация монстров». Одно из основных различий между языком шейдеров OpenGL и RenderMan — в том, что RenderMan полностью определяет интерфейс между программами мо- делирования и рендеринга, без каких-либо частей «стандартной функционально- сти», и не имеет никакого отношения к OpenGL. RenderMan включает поддержку описания геометрических примитивов, иерархическое моделирование, наложе- ние геометрических преобразований, параметры камеры, затенения и т. д. Мно- гие из этих возможностей уже реализованы в OpenGL, так что нет необходимости ссылаться на них при описании языка шейдеров OpenGL. Самая интересная часть RenderMan называется языком шейдеров RenderMan. Этот язык позволяет описать полностью произвольные шейдеры, которые можно передавать для рендеринга через интерфейс RenderMan. Этот язык также осно- ван на языке программирования С, поэтому немного похож и на язык шейдеров OpenGL. В целом, интерфейс RenderMan напоминает OpenGL, а язык шейдеров RenderMan — язык шейдеров OpenGL. И интерфейс RenderMan, и OpenGL по- зволяют определять характеристики сцены (параметры обзора, примитивы для рендеринга и т. д.). И в одном и в другом можно вычислять цвет, координаты, прозрачность и другие характеристики точек сцены. Одно из основных различий между языками OpenGL и RenderMan — в степе- ни абстракции. Язык шейдеров OpenGL спроектирован так, чтобы максимально просто «вписаться» в современное графическое оборудование, поэтому в нем есть два вида шейдеров: вершинные и фрагментные. Язык шейдеров RenderMan слу- жит, прежде всего, для создания высококачественных изображений, поэтому в нем целых пять видов шейдеров: освещения, сдвига, поверхности, объема и работы с изображением. Типы шейдеров RenderMan идеально подходят для создания
17.2. Язык RenderMan 345 удачных реализаций рендеринга, но не очень хорошо совмещаются с современ- ным оборудованием, спроектированным для поддержки интерактивного ренде- ринга на OpenGL. В результате практически все реализации RenderMan являют- ся программными, а не аппаратными; однако попытки ускорить работу RenderMan предпринимались [12]. Язык шейдеров OpenGL с самого начала проектировался для выполнения на графическом оборудовании. I Приложение I Описание сцены’ | и шейдеры 1 RenderMan I RenderMan API Реализация RenderMan Видеобуфер или файл изображения — — От разработчика приложения —От производителя реализации Рис. 17.1. Модель работы языка RenderMan В этих двух языках различаются и типы данных. RenderMan поддерживает типы, близкие к предметной области: цвет, точку, нормаль, — в то время как язык шейдеров OpenGL оперирует более абстрактными типами: векторами чисел с пла- вающей запятой размерностью от 1 до 4. RenderMan оперирует терминами, при- сущими скорее для графики, — «объект», «мир», «камера», «растр», «экран». RenderMan поддерживает большое количество предопределенных переменных для шейдеров поверхности, освещения, объема, сдвига и изображений. Язык шейдеров OpenGL содержит встроенные переменные, аналогичные неко- торым состояниям OpenGL, часть из них по смыслу похожа на переменные, опреде- ленные в RenderMan. Так как RenderMan используется в основном в анимации, в нем есть встроенные переменные для отсчета времени. В языке шейдеров OpenGL
346 Глава 17. Сравнение языков программирования таких переменных нет, но существует возможность передавать нужные значения в шейдеры через uniform-переменные. И все же эти два языка имеют много общего. Язык шейдеров OpenGL может даже рассматриваться как некое ответвление языка шейдеров RenderMan. Типы переменных uniform и varying были придуманы для RenderMan и затем позаим- ствованы для языка шейдеров OpenGL. Выражения, приоритетность операторов в обоих языках очень похожи на используемые в языке С. Ключевые слова 1 f, el se, whl le, for, break, return в них совпадают. Список встроенных математических функций языка шейдеров OpenGL практически совпадает с таким же списком языка шейдеров RenderMan. 17.3. Язык OpenGL Shader (ISL) Язык OpenGL Shader является некоммерческим и его можно загрузить с веб-сай- та компании SGI. Версия 3.0 этого пакета совместима с IRIX 6.5 или выше и с RedHat Linux 8.0. OpenGL Shader определяет и язык шейдеров (интерактивный язык шейдеров, или ISL), и набор функций, с помощью которых можно опреде- лять шейдеры и использовать их для рендеринга. Этот набор функций стал час- тью продуктов SGI, таких как SGI OpenGL Performer и SGI OpenGL Volumizer. Основное преимущество OpenGL Shader в том, что OpenGL API можно ис- пользовать как язык ассемблера для выполнения программируемых шейдеров (рис. 17.2). Если графическое аппаратное обеспечение позволяет использовать больше возможностей (например, многотекстурность и программируемость на уровне фрагментов), то считается, что язык ассемблера является более мощным. Последовательность выражений в шейдере ISL может затем транслироваться в один или больше проходов рендеринга. На каждом проходе может происходить обработка геометрической информации (операции над вершинами, фрагмента- ми, растеризация), или копирование (копируется часть буфера кадров), или ко- пирование текстуры (часть буфера кадров копируется в текстуру для операций, выполняемых раздельно с каждым пикселом). Для определения типа прохода последовательности команд и уменьшения количества проходов используется оптимизация компилятора. Текущая версия OpenGL Shader оптимизирована под многие графические ускорители и платформы и позволяет для каждой из них уменьшать количество проходов. Подобно любому языку шейдеров, ISL основан на языке С. Но шейдеры ISL выглядят совсем не так, как шейдеры OpenGL. Множество команд в шейдере ISL подразумевают какой-либо конкретный вид прохода. Например, вот фрагмент кода на ISL: varying color b: FB = diffused : FB *= color(.5. .2. 0. 1): b = FB: FB = specular(30.0): FB += b: Идентификатор FB обозначает, что результат должен сохраняться в буфере кадров. Эта последовательность операций сначала вызывает шейдерную подпрог-
17.3. Язык OpenGL Shader (ISL) 347 рамму, которая выполняет шейдер освещения для вычисления рассеянного цве- та. Полученное значение умножается на значение цвета (0.5, 0.2, 0, 1), затем ре- зультат сохраняется в области текстурной памяти Ь. После этого вычисляется зер- кальное отражение и, наконец, компоненты рассеянного и зеркального отражений складываются. Хотя этот код и выглядит требующим многопроходности, после- довательность команд может быть выполнена за один проход на множестве раз- ных графических акселераторов. Приложение I-----------------------1 I Исходный код ISL I Графический акселератор — — От разработчика приложения □ От SGI «—• От призводителя аппаратного обеспечения Рис. 17.2. Модель выполнения OpenGL Shader (ISL) ISL поддерживает шейдеры поверхности и освещения, которые затем объеди- няются и компилируются. В этом смысле ISL больше похож на RenderMan, чем на OpenGL.
348 Глава 17. Сравнение языков программирования Другое различие между языком шейдеров OpenGL и ISL заключается в том, что ISL был спроектирован с возможностью переносимости с устаревшего на со- временное оборудование, a OpenGL опирается на современное оборудование и обо- рудование будущего. Язык шейдеров OpenGL не может работать на аппаратном обеспечении без программируемости, a ISL будет выполнять шейдеры на совер- шенно разном оборудовании с одинаковым результатом. Еще одно различие языков в том, что ISL создавался с ограничениями: OpenGL API используется в нем как язык ассемблера, без учета графического оборудования, а язык шейдеров OpenGL как раз и создавался для того, чтобы ис- пользовать самые новые возможности графического оборудования, и он поддер- живает более естественный синтаксис для реализации алгоритмов работы с гра- фикой. Высокоуровневый код на языке шейдеров OpenGL можно преобразовывать в машинный код, выполняющийся на определенном графическом оборудовании и оптимизированный именно под него. 17.4. Язык HLSL HLSL расшифровывается как High-Level Shader Language (язык шейдеров высо- кого уровня), он был создан компанией Microsoft и реализован в DirectX 9 в 2002 г. По синтаксису и функциональности HLSL значительно ближе к OpenGL, чем RenderMan и ISL. HLSL поддерживает парадигму программируемости на уровне вершин и фрагментов точно так же, как язык шейдеров OpenGL. Вершинный шей- дер HLSL соответствует вершинному шейдеру OpenGL, а пиксельный шейдер HLSL соответствует фрагментному шейдеру OpenGL. Одно из основных различий этих языков — в модели выполнения. HLSL со- здан как транслятор из исходного кода в другой исходный код (рис. 17.3). Компи- лятор HLSL на самом деле является транслятором, который не зависит от DirectX, поэтому HLSL-программы не передаются для выполнения через DirectX 9 API. Вместо этого компилятор HLSL транслирует исходный код в более низкоуровне- вый код, и получаются программы, называемые вершинными шейдерами и пик- сельными шейдерами. Эти шейдеры могут различаться номером версии (напри- мер, Vertex Shader 1.0, 2.0, 3.0; Pixel Shader 1.1, 1.4, 2.0, 3.0). Преимущество такого подхода заключается в том, что программы HLSL мож- но транслировать перед выполнением программы. Получаются программы на языке ассемблера в строковом представлении, которые анализируются уже во время работы программы. Этим HLSL отличается от языка шейдеров OpenGL, где компилятор является частью драйвера, и его должен предоставить произво- дитель аппаратного обеспечения. При этом открываются широкие возможности для оптимизации выполнения шейдеров на конкретном графическом оборудо- вании. HLSL создан для облегчения работы разработчиков программ, для того чтобы те писали шейдеры на языке более высокого уровня. При этом нужно понимать, что шейдеры будут запускаться на разном оборудовании с разными возможнос- тями.
17.4. Язык HLSL 349 Не гарантируется, что шейдеры HLSL будут работать на любой платформе. Разработчики шейдеров должны выбрать: либо написать шейдер, который мо- жет выполняться и на оборудовании с очень небольшой программируемостью, либо ориентироваться только на определенный ряд графического оборудова- ния (для чего существует возможность языка, называемая профайл). Компа- ния Microsoft предоставляет программу DirectX Effects Framework для разра- ботчиков шейдеров. Графический акселератор — — - От разработчика приложений От Microsoft — От производителя аппаратного обеспечения Рис. 17.3. Модель выполнения Microsoft HLSL Типы данных в HLSL почти такие же, как в языке шейдеров OpenGL, с не- большими отличиями. HLSL включает поддержку чисел с плавающей запятой
350 Глава 17. Сравнение языков программирования половинной и двойной точности. Как и в языке шейдеров OpenGL, поддержива- ются векторы, матрицы, структуры и массивы. Выражения в HLSL такие же, как в языках С и C++. Определенные разработчиком функции и условные операторы поддерживаются так же, как и в языке шейдеров OpenGL. Операторы циклов (for, do, while) определены, но, согласно последней документации, не реализованы. HLSL поддерживает гораздо большее количество встроенных функций, чем язык шейдеров OpenGL, а те, что имеются в обоих списках, практически совпадают. Еще одно отличие — в способе передачи данных между вершинным и пиксель- ным (HLSL) или фрагментным (язык шейдеров OpenGL) шейдерами. HLSL оп- ределяет семантику и входных, и выходных данных как для вершинного, так и для пиксельного шейдеров. Это то же самое, что varying- и встроенные переменные в языке шейдеров OpenGL. Можно передавать любые данные в шейдеры и из них, но нужно записывать эти данные в определенные области памяти: POSITION, COLORE 1 ], TEXCOORDЕ1 ] и т. д. Например, если направление освещения содержится в перемен- ной lightdir, ее нужно записать в «гнездо» TEXC00RDE1 ] — это, вообще, довольно странный прием для высокоуровневого языка программирования. Язык шейде- ров OpenGL позволяет использовать произвольно выбранные имена для переда- чи значений между шейдерами. HLSL был спроектирован для DirectX, графического API от компании Microsoft, а язык шейдеров OpenGL — для OpenGL. Microsoft может изменять DirectX как вздумается, a OpenGL является открытым кроссплатформенным стан- дартом, который меняется более медленно, но сохраняет совместимость с преды- дущими версиями. 17.5. Язык Сд Cg является высокоуровневым языком шейдеров, очень похожим на HLSL. Cg определен, реализован и поддерживается компанией NVIDIA. Можно сравнить Cg с языком шейдеров OpenGL почти так же, как последний — с HLSL. Между Cg и HLSL существует несколько небольших различий (например, в HLSL есть тип данных двойной точности, а в Cg — нет), но эти языки разрабатывались ком- паниями совместно, так что и результаты их работы получились практически оди- наковыми. Однако у Cg есть преимущества перед другими языками. Транслятор Cg мо- жет выдавать код как для DirectX, так и для OpenGL. Таким образом, шейдеры Cg могут использоваться и на базе DirectX, и на базе OpenGL (рис. 17.4). При этом требуется, чтобы приложение обращалось к промежуточной библиотеке от NVIDIA — посреднику между приложением и соответствующим API (OpenGL или DirectX). Эта библиотека называется Cg Runtime. Для простых приложений использование библиотеки довольно просто, но в более сложных приложениях управлять шейдерами становится нелегко. NVIDIA предоставляет собственную версию надстройки над языком. CgFX — это спецификация шейдеров и формат данных, а формат файлов там такой же, как и .fx-формат, поддерживаемый DirectX 9. Библиотека CgFX, как и Cg Runtime, включает поддержку и OpenGL и DirectX.
17.6. Итоги 351 Графический акселератор — — От разработчика приложения Q От NVIDIA —От производителя аппаратного обеспечения Рис. 17.4. Модель выполнения Сд 17.6. Итоги Сейчас языки шейдеров очень популярны. Первые языки шейдеров создавались не для реального режима времени, а просто для создания фотореалистичных статических изображений. Графическое оборудование, способное поддерживать интерактивный язык шейдеров, появилось в 1990-х гг., а сейчас программируе- мые графические акселераторы доступны практически всем. Это стимулировало разработку нескольких коммерческих языков шейдеров: ISL, языка шейдеров OpenGL, HLSL, Cg.
352 Глава 17. Сравнение языков программирования При всем разнообразии языков программирования, последние три упомяну- тых языка очень похожи между собой. Каждый из них был создан для отображе- ния функциональности RenderMan на основании синтаксиса С и C++. В резуль- тате все три языка получились очень похожими по синтаксису и возможностям. Самое большое их различие в том, что HLSL и Cg были созданы как надстройки над стандартными интерфейсами DirectX и OpenGL, а трансляция с исходного кода в команды этих API может происходить когда угодно. Код на языке шейде- ров OpenGL преобразуется прямо в машинный код драйвером OpenGL. Спецификации HLSL и Cg контролируются компаниями Microsoft и NVIDIA соответственно. Язык шейдеров OpenGL контролируется группой OpenGL ARB, созданной представителями различных производителей графического аппарат- ного обеспечения. HLSL создан для использования в среде Microsoft DirectX, а язык шейдеров OpenGL — для использования совместно с OpenGL в множестве различных сред. Cg может работать как в среде DirectX, так и в среде OpenGL. 17.7. Ссылки Язык шейдеров RenderMan описан в спецификации [14], а его применение опи- сано в книгах [17 и 1]. OpenGL Shader и ISL описаны в документации, разработанной в компании SGI и доступной на ее веб-сайте и в докладе [12]. [И] содержит информацию о не- скольких языках шейдеров, включая RenderMan, ISL и языки шейдеров, опреде- ленные и реализованные исследователями из компании UNC, Стэнфорда и уни- верситета Ватерлоо. Стэнфордский язык шейдеров реального времени описан в докладе [15]. Книг, содержащих описание Microsoft HLSL, в момент написания данной книги найти не удалось, но есть документация, разработанная Microsoft: http://www.micro- soft. com/directx. Cg описан в документации, разработанной в компании NVIDIA, в книге [4] и в докладе [7]. 1. Apodaca A. A., Gritz L., Advanced RenderMan: Creating CGI for Motion Pictures. San Francisco: Morgan Kaufmann Publishers, 1999 (http://www.bmrt.org/arman/ materials.html). 2. Baldwin D. OpenGL 2.0 Shading Language White Paper, Version 1.2/3Dlabs. 2002 (http://www.3dlabs.com/support/developer/ogl2). 3. Cook R. L. Shade Trees, Computer Graphics//Proc. SIGGRAPH-84.1984. P. 223— 231. 4. Fernando R., Kilgard M. The Cg Tutorial, the Definitive Guide to Programmable Real-Time Graphics. Boston, MS: Addison-Wesley, 2003. 5. Kessenich J., Baldwin D., Rost R. The OpenGL Shading Language, Version 1.051/ 3Dlabs. 2003 (http://www.3dlabs.com/support/developer/ogl2). 6. Mark W. R. Real-Time Shading: Stanford Real-Time Procedural Shading System// Proc. SIGGRAPH-2001.2001 (http://graphics.stanford.edu/projects/shading/pubs/ sigcourse2001.pdf).
17.7. Ссылки 353 7. Cg: A System for Programming Graphics Hardware in a C-like Language / W. R. Mark, S. R. Glanville, K. Akeley, M. Kilgard//Computer Graphics (Proc. SIGGRAPH-2003). 2003. P. 896-907 (http://www.cs.utexas.edu/users/biLlrnark/papers/cg). 8. DirectX 9.0 SDK / Microsoft. 2003 (http://msdn.microsoft.com/directx). 9. Cg Toolkit, Release 1.1, software and documentation/NVIDIA Corporation (http:// developer.nvidia.com/Cg). 10. Olano M., Lastra A. A Shading Language on Graphics Hardware: The PixelFlow Shading System//Computer Graphics (Proc. SIGGRAPH-98). 1998. P. 159-168 (http://www.csee.umbc.edu/~olano/papers). 11. Real-Time Shading/M. Olano, J. Hart, W. Heidrich, M. McCool. Natick, MS: A К Peters, Ltd., 2002. 12. Interactive Multi-Pass Programmable Shading / M. S. Peercy, M. Olano, J. Airey, P. J. Ungar//Computer Graphics (Proc. SIGGRAPH-2000). 2000. P. 425-432 (http://www.csee.umbc.edu/~olano/papers). 13. Perlin K. An Image Synthesizer//Computer Graphics (Proc. SIGGRAPH-85). 1985. P. 287-296. 14. Pixar, The RenderMan Interface Specification, Version 3.2/Pixar. 2000 (http:// renderman.pixar.com/products/rispec/index.htm). 15. A Real-Time Procedural Shading System for Programmable Graphics Hardware/ K. Proudfoot, W. R. Mark, S. Tzvetkov, P. Hanrahan//Computer Graphics (Proc. SIGGRAPH-2001). 2001. P. 159-170 (http://graphics.stanford.edu/projects/ shading/pubs/sig2001). 16. SGI OpenGL Shader Web site (http://www.sgi.com/software/shader). 17. Upstill S. The RenderMan Companion: A Programmer’s Guide to Realistic Compu- ter Graphics. Reading, MS: Addison-Wesley, 1990. 12 Зак 218
Послесловие Работа над книгой отнимает много времени и сил. Иногда авторы называют уже законченную книгу «работа любви». Для меня написание этой книги было «рабо- той радости». Мне посчастливилось принимать участие в большой революции архитектуры графического аппаратного обеспечения. За последние несколько лет графичес- кие акселераторы претерпели множество изменений — от простой стандартной функциональности до почти полной программируемости. Это напоминает конец 70-х и начало 80-х гг., когда в компьютерной графике также происходили значи- тельные изменения (разработки велись в основном в университете штата Юта, а также в NYU, Lucasfilm, JPL, UNC, Cornell). Сейчас графические акселераторы стали дешевы настолько, что для работы с ними не нужно работать в исследова- тельском институте или учиться в престижном высшем учебном заведении. Но- вый мир можно постигать, работая на собственном персональном компьютере. Участвовать в разработке даже одного промышленного стандарта — большая удача, а мне посчастливилось работать с тремя важными графическими стандар- тами. Первым был РЕХ в конце 80-х. Затем в начале 90-х — OpenGL и, наконец, в первые годы нового тысячелетия — язык шейдеров OpenGL. Я рад, что работал над этими проектами, обеспечивающими дополнительные возможности графи- ческих акселераторов на уровне промышленных стандартов. Написанные в соот- ветствии со стандартом приложения переносимы, а технология, на которой они построены, доступна широкой аудитории. Действительно, очень интересно и замечательно быть одним из первых, кто реализовал классические алгоритмы рендеринга на высокоуровневом языке для недорогого высокопроизводительного графического оборудования. Когда наша группа впервые запустила шейдер кирпичной стенки на графичес- ком оборудовании 3Dlabs Wildcat VP, это было потрясающе. Что-то подобное я чувствовал, когда мой или еще чей-либо шейдер первый раз успешно запускал- ся. Мне кажется, что это похоже на чувства, испытываемые первыми разработчи- ками графических алгоритмов 20-25 лет назад. И то же самое испытывают разра- ботчики шейдеров при опробовании новых возможностей. Благодаря новой революции в архитектуре графического аппаратного обеспе- чения многие разработчики могут писать шейдеры, используя алгоритмы, приду- манные еще 20 лет назад. Создавая шейдер бугристой поверхности, мы руковод- ствовались алгоритмом Блайна (1978 г.), а шейдер систем частиц выполнен на основании алгоритма Ривза (1983 г.). Я порадовался, увидев нарисованные вруч- ную диаграммы в записях Альви Рея Смита от 1983 г. Рендеринг изображений,
Послесловие 355 сейчас занимающий миллисекунды, в то время продолжался часы. Код, ранее раз- рабатываемый месяцами, сегодня можно легко написать за несколько минут на специально разработанном для подобных задач высокоуровневом языке. Стано- вится не по себе, когда подумаешь, как усердно работал над созданием своих зна- менитых изображений Мандельброт в конце 70-х гг. и как легко это сделать сей- час на языке шейдеров OpenGL. В этой книге обсуждается несколько значительных новых достижений в обла- сти компьютерной графики, что стало для меня дополнительным удовольствием при написании этой книги. Если кто-то вроде меня может легко и просто реализо- вать любой алгоритм рендеринга, который раньше мог выполняться только с ис- пользованием программного обеспечения на основном процессоре, представьте себе, как просто это сделать сегодня, имея программируемое аппаратное обеспе- чение. Современные графические акселераторы для персональных компьютеров дают возможность разработчикам экспериментировать с новыми технологиями рендеринга, создавать и использовать гораздо более сложные алгоритмы, чем рань- ше. Я уверен, что скоро появятся еще более новые технологии рендеринга, в кото- рых предполагается использование языка шейдеров OpenGL. Я написал эту книгу для того, чтобы разработчики познакомились с новым языком и новыми технологиями и, что более важно, увидели новые возможности рендеринга, которые все это время скрывались за стандартной функциональнос- тью графических акселераторов. Как мне кажется, нет смысла продолжать исполь- зовать стандартную функциональность. Разработчики будут создавать шейдеры, выполняющие рендеринг так, как необходимо, а не тем единственным способом, который позволяет реализовать графический акселератор. Думайте над новыми способами рендеринга и делитесь находками с другими. Если хотите, присылайте их мне по адресу randi@3dshaders.com. Самые лучшие попадут в следующее изда- ние этой книги. Удачи во всех ваших графических начинаниях! Рэнди Рост, Форт-Коллинз, СО Июнь 2003
Приложение А Грамматика языка Грамматика языка шейдеров OpenGL взята из лексического анализатора. Вот спи- сок ключевых слов: ATTRIBUTE CONST BOOL FLOAT INT BREAK CONTINUE DO ELSE FOR IF DISCARD RETURN BVEC2 BVEC3 BVEC4 IVEC2 IVEC3 IVEC4 VEC2 VEC3 VEC4 MAT2 MAT3 MAT4 IN OUT INOUT UNIFORM VARYING SAMPLER1D SAMPLER2D SAMPLER3D SAMPLERCUBE SAMPLER1DSHADOW SAMPLER2DSHADOW STRUCT VOID WHILE IDENTIFIER TYPEJJAME FLOATCONSTANT INTCONSTANT BOOLCONSTANT FIELD-SELECTION LEFT_OP RIGHT_OP INC_OP DEC_OP LE_OP GE_OP EQ_OP NE_DP AND_OP OR_OP XOR_OP MUL_ASSIGN DIV_ASSIGN ADD_ASSIGN MOD_ASSIGN LEFT_ASSIGN RIGHT_ASSIGN AND_ASSIGN XOR_ASSIGN OR_ASSIGN SUB_ASSIGN LEFT_PAREN RIGHT_PAREN LEFTJ3RACKET RIGHT_BRACKET LEFT_BRACE RIGHT_BRACE DOT COMMA COLON EQUAL SEMICOLON BANG DASH TILDA PLUS STAR SLASH PERCENT LEFT_ANGLE RIGHT_ANGLE VERTICAL_BAR CARET AMPERSAND QUESTION Выражения на языке шейдеров OpenGL выглядят так: variable_identifier: IDENTIFIER priтагу_expression: variable_identifier INTCONSTANT FLOATCONSTANT BOOLCONSTANT LEFT_PAREN expression RIGHT_PAREN postfix_expression: primary-expression postfix_expression LEFT_BRACKET integer_expression RIGHT_BRACKET function_call postfix_expression DOT FIELD-SELECTION postfix_expression INC_OP postfix_expression DEC_OP integer_expression: expression function_cal1:
Грамматика языка 357 function_call_gener1c funct1on_call_gener1c: funct 1 on_ca11_header_wi th_parameters RIGHT_PAREN funct1on_call_header_no_parameters RIGHT_PAREN funct1on_call_header_no_paranieters: function_call_header VOID function_call_header function_cal1_header_with_parameters: function_call_header ass1gnment_express1on function_call_header_with_parameters COMMA ass1gnment_express1on f u net1on_ca11-header: function_1dent1f1er LEFT_PAREN function-1dentlfier: constructor_1dent1f1er IDENTIFIER // Конструкторы похожи на функции, но лексический анализатор // рассматривает большинство из них как ключевые слова. constructor_1dent1f1er: FLOAT INT BOOL VEC2 VEC3 VEC4 BVEC2 BVEC3 BVEC4 IVEC2 IVEC3 IVEC4 MAT2 МАТЗ MAT4 unary_expression: postflx_expression INC_OP unary_expression DEC_OP unary_expression unary_operator unary_expression // Традиционные преобразования типов не поддерживаются unary-Operator: PLUS DASH BANG TILDA // зарезервировано // Унарные операторы и указатели не поддерживаются mult1pl1cative_expression:
358 Приложение А. Грамматика языка unary_expression mult1pl1cative_express1on STAR unary_express1on mult1pl1cat1ve_expression SLASH unary_express1on mult1plicative_expression PERCENT unary_express1on // зарезервировано additive_expression: multiplIcative_express1on additive_express1on PLUS mult1pl1cat1ve_expression add1tive_expression DASH mult1pl1cat1ve_expression sh1ft_express1on: add1tive_expression sh1ft_express1on LEFT_OP add1t1ve_express1on // зарезервировано sh1ft_express1on RIGHT_OP add1tive_express1on // зарезервировано relational_express1on: shift_expression relational_expression LEFT_ANGLE sh1ft_expression relational_express1on RIGHT_ANGLE sh1ft_express1on relat1onal_expression LE_OP shift_express1on relat1onal_expression GE_OP shift_express1on equality_expression: relational-expression equal1ty_expression EQ_OP relational_express1on equal1ty_expression NE_OP relat1onal_express1on and_expression: equa11ty_expressi on and_expression AMPERSAND equality_expression // зарезервировано excl us 1ve_or_expressi on: and_expression exclusive_or_expression CARET and_expression // зарезервировано incluslve_or_expression: exclu sive_or_expressi on 1nclusive_or_express1on VERTICAL_BAR exclusive_or_expression // зарезервировано 1oglcal_and_expressi on: 1nclus1ve_or_expression logical_and_expression AND_OP 1nclus1ve_or_express1on logical_xor_expression: logical_and_expression Iog1cal_xor_express1on XOR_OP logical_and_expression logical_or_expression: logical_xor_expression logical_or_expression OR_OP logical_xor_expression conditional_expression: logical_or_express1on Iog1cal_or_express1on QUESTION expression COLON
Грамматика языка 359 cond1t1onal_express1on ass1gnment_express1on: conditional-expression unary-expression ass1gnment_operator assignment_express1on assi gnment-Operator: EQUAL MUL-ASSIGN DIV ASSIGN MOD_ASSIGN ADD_ASSIGN SUB_ASSIGN LEFT_ASSIGN // зарезервировано RIGHT_ASSIGN // зарезервировано AND_ASSIGN // зарезервировано XOR_ASSIGN // зарезервировано OR_ASSIGN // зарезервировано expression: assi gnment_expressi on expression COMMA assignment_expression constant_expression: conditional-expression declaration: funct1on_prototype SEMICOLON 1n1t_declarator_11st SEMICOLON function_prototype: funct1on_declarator RIGHT_PAREN function_declarator: functlonjieader funct1on_header_wi th_pa rameters function_header_with_parameters: funct1on_header parameter_declarat1on function_header_with_parameters COMMA parameter_declaration funct1on_header: fully_specif1ed_type IDENTIFIER LEFT_PAREN parameter_declarator: type_specifier IDENTIFIER type_specifler IDENTIFIER LEFT_BRACKET RIGHT_BRACKET parameter_decla rati on: type_qualifier parameter_qualifier parameter_declarator pa rameter-qua11 fl er parameter_decla rator type_qualifier parameter_qualifier parameter_type_specifier parameter_qualIfier parameter_type_specifier parameter_qual1f1er: /* пусто */
360 Приложение А. Грамматика языка IN OUT INOUT parameter_type_specifier: type_specifier type_specifier LEFT-BRACKET RIGHT_BRACKET init_declarator_list: single_declarat1on init_declarator_list COMMA IDENTIFIER init_declarator_list COMMA IDENTIFIER LEFT_BRACKET RIGHT_BRACKET init_declarator_l1st COMMA IDENTIFIER LEFT_BRACKET constant_expression RIGHT_BRACKET 1n1t_declarator_11st COMMA IDENTIFIER EQUAL Initializer single_declaration: f u11y_spec i fi ed_type fully_specified_type IDENTIFIER fully_specified_type IDENTIFIER LEFT_BRACKET RIGHT-BRACKET fully_specified_type IDENTIFIER LEFT_BRACKET constant_expression RIGHT_BRACKET fully_specified_type IDENTIFIER EQUAL initializer // 'enum' и 'typedef' не поддерживаются f u 11 у _s pec i f i ed_ty pe: type_specifier type_qualifier type_specifier CONST ATTRIBUTE // только для вершины VARYING UNIFORM type_specifier: VOID FLOAT INT BOOL VEC2 VEC3 VEC4 BVEC2 BVEC3 BVEC4 IVEC2 IVEC3 IVEC4 MAT2 MAT3 MAT4 SAMPLER1D SAMPLER2D SAMPLER3D SAMPLERCUBE SAMPLER1DSHAD0W
Грамматика языка 361 SAMPLER2DSHAD0W struct_specifl er TYPE-NAME struct_specifier: STRUCT IDENTIFIER LEFT_BRACE struct_declaration_l1 st RIGHTJ3RACE . STRUCT LEFT_BRACE struct_declaration_l 1 st RIGHT_BRACE struct_declaration_l1st: struct_declaration struct_declaration_list struct_declaration struct_declaration: type_specifier struct_declarator_l1st SEMICOLON struct_declarator_list: struct_declarator struct_declarator_11st COMMA struct_declarator struct_declarator: IDENTIFIER IDENTIFIER LEFTJ3RACKET constant_express1on RIGHT_BRACKET initializer: assignment_expression declaration_statement: declaration statement: compound_statement simple_statement 11 оператор 'goto' и метки не поддерживаются simple_statement: declaration_statement expression_statement select1on_statement 1terat1on_statement Jump-Statement compound_statement: LEFT_BRACE RIGHT_BRACE LEFT_BRACE statement-list RIGHT_BRACE statement_no_new_scope: compound_statement_no_new_scope simple_statement compound_statement_no_new_scope: LEFT_BRACE RIGHT_BRACE LEFT_BRACE statement-list RIGHT_BRACE statement_list: statement
362 Приложение А. Грамматика языка statement_list statement expressi on_statement: SEMICOLON expression SEMICOLON select1on_statement: IF LEFT_PAREN expression RIGHT_PAREN selection_rest_statement selection_rest_statement: statement ELSE statement statement // оператор 'switch' не поддерживается condition: expression fully_specified_type IDENTIFIER EQUAL initializer iteration_statement: WHILE LEFT_PAREN condition RIGHT_PAREN statement_no_new_scope DO statement WHILE LEFT_PAREN expression RIGHT_PAREN SEMICOLON FOR LEFT_PAREN for_init_statement for_rest_statement RIGHT_PAREN statement_no_new_scope for_init_statement: expressi on_statement declaration_statement conditionopt: condition /* пусто */ for_rest_statement: conditionopt SEMICOLON conditionopt SEMICOLON expression jump_statement: CONTINUE SEMICOLON BREAK SEMICOLON RETURN SEMICOLON RETURN expression SEMICOLON DISCARD SEMICOLON // только для фрагментного шейдера // оператор 'goto' не поддерживается translationjjnit: external_declaration translationjjnit external jiecla ration external_declaration: function_definition declaration function_definition: function_prototype compound_statement_no_new_scope
Приложение Б Справочник функций API В этом разделе содержатся подробное описание функций OpenGL для создания, компиляции, компоновки шейдеров, написанных на языке шейдеров OpenGL, и манипулирования ими, а также описание функций для работы с дополнитель- ными атрибутами вершин и uniform-переменными. Права на эти материалы принадлежат компании 3Dlabs, Inc., Ltd. Перепечата- но с разрешения 3Dlabs. glAttachObjectARB Название glAttachObjectARB — связывает шейдерный объект с программным объектом. Прототип функции void glAttachObjectARB(GLhandleARB program. GLhandleARB shader) Параметры program Дескриптор программного объекта, с которым связывается шейдерный объект. shader Дескриптор связываемого шейдерного объекта. Описание Программный объект предоставляет механизм, с помощью которого можно ука- зать все шейдерные объекты — компоненты программы. glAttachObjectARB связы- вает шейдерный объект shader с программным объектом program. Это означает, что shader будет участвовать во всех операциях компоновки, выполняемых над program. Действия, выполняемые над шейдерным объектом, всегда успешно выполня- ются вне зависимости от того, связан ли шейдерный объект с программным. Можно связывать их перед загрузкой исходного кода в шейдерный объект или непосред- ственно перед компиляцией шейдерного объекта. Не будет ошибкой связывание нескольких шейдерных объектов одного типа с одним и тем же программным объектом, так как в них могут содержаться только части целого шейдера. Можно
364 Приложение Б. Справочник функций API связывать один шейдерный объект с несколькими программными объектами. Связанный шейдерный объект, который попытались удалить, на самом деле не удаляется, а просто помечается для удаления. Он будет удален после отсоедине- ния шейдерного объекта от всех программных объектов с помощью функции gl DetachObj ectARB. Ошибки GL_INVALID_VALUE возникает, если program или shader не являются дескрипторами объектов OpenGL. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ shader не является объектом типа GL_SHADER_OBJECT_ARB; □ shader уже присоединен к program; □ gl AttachObjectARB вызывается между соответствующими вызовами gl Begiп и gl End. Соответствующие GET-функции glGetAttachedObjectsARB, в которую передается дескриптор программного объекта. Смотри также glCompi leShaderARB, gl DetachObjectARB, glLinkProgramARB, glShaderSourceARB gIBindAttribLocationARB Название gIBindAttribLocationARB — устанавливает соответствие индекса дополнительных атрибутов вершин с переменной. Прототип функции void gIBindAttribLocationARB! GLhandleARB program. GLuint index. const GLcharARB *name) Параметры program Дескриптор программного объекта, в котором устанавлива- ется соответствие индекса дополнительных атрибутов вер- шин с переменной. i ndex Индекс дополнительного атрибута вершины. name Строка с завершающим нулем, в которой содержится имя переменной вершинного шейдера, с которой связывается index.
glBindAttribLocationARB 365 Описание Функция gl Bi ndAttri bLocati onARB устанавливает соответствие определенной разра- ботчиком attribute-переменной в программном объекте program с дополнительным атрибутом вершины 1 ndex. Имя определенной разработчиком attribute-перемен- ной передается в строке с завершающим нулем name, index — индекс дополнитель- ного атрибута вершины, соответствующий этой переменной. Когда program станет частью текущего состояния, изменение значений дополнительного атрибута вер- шины по index автоматически изменит значение определенной разработчиком attribute-переменной name. Если name ссылается на attribute-переменную типа матрицы, index ссылается на первый столбец матрицы, а остальным столбцам соответствуют: i ndex+1 — для матрицы типа mat2; 1 ndex+1 и index+2 — для матрицы типа mat3; i ndex+1, index+2 и i ndex+3 — для матрицы типа mat4. Эта функция позволяет устанавливать для attribute-переменных значимые имена вместо номеров, находящихся в диапазоне от 0 до GL_MAX_VERTEX_ATTRIBS-1. Значение каждой дополнительной attribute-переменной является частью текущего состояния, как и значения основных атрибутов вершин — цвета, нормали и коор- динат вершины. Если функцией gl UseProgramObjectARB в текущее состояние уста- навливается другой программный объект, значения дополнительных атрибутов вершин для него сохраняются и имена все так же соответствуют индексам. Соответствие имени attribute-переменной индексу дополнительного атрибута можно устанавливать в любой момент функцией glBindAttribLocationARB. Эти со- ответствия становятся действительными только после вызова функции glLink- ProgramARB. После успешной компоновки программного объекта эти соответствия сохраняются до следующей операции компоновки, и их значения можно полу- чить средствами OpenGL. Этой функцией нельзя устанавливать соответствия со стандартными атрибу- тами вершин OpenGL — это происходит автоматически при необходимости. Ре- зультаты любых вызовов функции glBindAttribLocationARB проявляются только после следующей операции компоновки. Примечания Функцию glBindAttribLocationARB можно вызывать перед связыванием шейдер- ных объектов с программным объектом. Установка соответствия индекса имени attribute-переменной выполняется без ошибок, даже если эта переменная не ис- пользуется в вершинном шейдере. Если name раньше уже соответствовала какому-либо индексу, эта связь теряет- ся. Это означает, что несколько индексов не могут соответствовать одной attribute- переменной, но возможно обратное: один индекс может соответствовать несколь- ким attribute-переменным и в каждую из них будет записываться одно и то же значение. Соответствие одного индекса нескольким attribute-переменным является ис- пользованием псевдонимов, и эту возможность можно применять лишь в случаях, если только один из атрибутов является активным в выполняемой программе либо каждый возможный путь выполнения шейдера использует один из псевдонимов. Благодаря этому компилятор и компоновщик могут полагаться на отсутствие
366 Приложение Б. Справочник функций API псевдонимов в шейдере и выполнять более качественную оптимизацию. Реализа- ции OpenGL не всегда выполняют проверку на наличие псевдонимов. Дополни- тельные атрибуты не могут быть псевдонимами стандартных (за исключением дополнительного атрибута 0). Активные атрибуты, для которых соответствие не установлено непосредствен- но, связываются при вызове функции glLinkProgramARB, а индексы в дальнейшем можно узнать с помощью функции gl GetAttrl bLocati onARB. Во время работы функции glBindAttribLocationARB OpenGL копирует строку name, а после завершения работы приложение должно освобождать выделенную для копии память. Ошибки GL_INVALID_VALUE возникает: □ если значение i ndex больше или равно GL_MAX_VERTEX_ATTRIBS_ARB; □ program не является дескриптором объекта OpenGL. GL_INVALID_OPERATION возникает в следующих случаях: □ name начинается с зарезервированного префикса gl_; □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ gl Bi ndAttri bLocati onARB выполняется между соответствующими вызовами gl Begi n и glEnd. Соответствующие GET-функции glGetActiveAttribARB с аргументом program. glGetAttrl bLocationARB с аргументами program и name. glGet с аргументом GL_MAX_VERTEX_ATTRIBS_ARB. Смотри таже gl Di sabl eVertexAttri bArrayARB, gl Enabl eVertexAttri bArrayARB, gl UseProgramObjectARB, gl VertexAttri bARB, gl VertexAttribPointerARB glCompileShaderARB Название gl Compi 1 eShaderARB — выполняет компиляцию шейдерного объекта. Прототип функции void glCompi1eShaderARBIGLhandleARB shader) Параметры shader Дескриптор шейдерного объекта для компиляции.
glCreateProgramObjectARB 367 Описание Функция gl Comp 11 eShaderARB выполняет компиляцию строк исходного кода, кото- рые содержатся в шейдерном объекте shader. Результат компиляции сохраняется в текущем состоянии шейдерного объекта. Это значение устанавливается в GL TRUE, если компиляция прошла без ошибок и объект можно использовать, в противном случае значение будет равно GL_FALSE. Результат компиляции можно получить с помощью функции gl GetObjectParameterARB с аргументами shader и GL_OBJECT_COMPILE_STATUS_ARB. Причины неудачной компиляции описаны в спецификации языка шейдеров OpenGL. Более полную информацию о результатах компиляции можно получить с помощью функции glGetInfoLogARB. Примечания Функция gl Compi 1 eShaderARB не ожидает окончания компиляции и немедленно возвра- щает управление приложению. Любая следующая операция, зависящая от резуль- тата компиляции (например, gl Li nkProgramARB), будет ожидать ее окончания. Если же необходимо убедиться в том, что компиляция уже завершена, вызывается функция glGetObjectParameterARB. Этот вызов не завершается до окончания компиляции. Ошибки GL_INVALID_VALUE возникает, если shader не является дескриптором объекта OpenGL. GL_INVALID_OPERATION возникает в следующих случаях: □ shader не является объектом типа GL_SHADER_OBJECT_ARB; □ функция gl Compi 1 eShaderARB выполняется между соответствующими вызовами glBegiп и gl End. Соответствующие GET-функции glGet InfoLogARB с аргументом shader. glGetObjectParameterARB с аргументами shader и GL_OBJECT_COMPILE_STATUS_ARB. Смотри также gl CreateShaderObjectARB, glLinkProgramARB, glShaderSourceARB glCreateProgramObjectARB Название glCreateProgramObjectARB — создает программный объект. Прототип функции GLhandleARB glCreateProgramObjectARB(void)
368 Приложение Б. Справочник функций API Описание Функция glCreateProgramObjectARB создает пустой программный объект и возвра- щает его дескриптор. Программным называется объект, с которым связываются шейдерные объекты, при этом шейдеры проверяются на совместимость (например, могут ли вершинный и фрагментный шейдеры работать вместе). Если шейдер- ный объект больше не нужен программному, его можно отсоединить. Выполняемая программа создается после компоновки программного объекта. Эта программа становится частью состояния OpenGL после вызова функции gl UseProgramObjectARB. Программные объекты удаляются функцией gl Del eteObJectARB. Память, выделяе- мая программному объекту, освобождается после того, как объект перестает быть частью текущего состояния рендеринга в любом контексте. Примечания Как и для таблиц состояния и текстурных объектов, область видимости дескрип- торов всех объектов может распространяться на несколько контекстов. Изменения в программном объекте, сделанные в одном контексте рендеринга, не появляются в другом контексте рендеринга до тех пор, пока для этого контек- ста не будет вызвана функция gl UseProgramObjectARB. Ошибки GL_INVALID_OPERATION возникает при вызове функции gl CreateProgramObjectARB между соответствующими вызовами функций gl Begj п и gl End. Соответствующие GET-функции gl GetHandl eARB с аргументом GL_PROGRAM_OBJECT_ARB. gl GetAttachedObjectsARB, в которую передается дескриптор программного объекта, gl Get InfoLogARB, в которую передается дескриптор программного объекта. gl GetObjectParameterARB, в которую передается дескриптор программного объекта. Смотри также gl AttachObjectARB, glCreateShaderObjectARB, glDeleteObjectARB, glDetachObjectARB, glLinkProgramARB, glUseProgramObjectARB,glVaiidateProgramARB glCreateShaderObjectARB Название glCreateShaderObjectARB — создает шейдерный объект. Прототип функции GLhandleARB gl CreateShaderObjectARBtGLenum shaderType)
glCreateShaderObjectARB 369 Параметры shaderType Тип создаваемого шейдера. Возможные значения аргумен- та - GL_VERTEX_SHADER_ARB и GL_FRAGMENT_SHADER_ARB. Описание Функция glCreateShaderObjectARB создает пустой шейдерный объект и возвращает его дескриптор. Затем шейдерный объект используется для хранения строк исход- ного кода шейдера и работы с ними. Параметр shaderType указывает тип создавае- мого шейдера; в данный момент поддерживается два типа шейдеров. Шейдер типа GL_VERTEX_SHADER_ARB будет запускаться па программируемом вершинном процессо- ре и заменит стандартную обработку вершин OpenGL. Шейдер типа GL_FRAGMENT_ SHADER_ARB будет запускаться на программируемом фрагментном процессоре и за- менит стандартную обработку OpenGL. При создании шейдерного объекта его параметр GL_OBJECT_TYPE_ARB уста- навливается в GL_SHADER_OBJECT_ARB, а параметр GL_OBJECT_SUBTYPE_ARB — либо в GL_VERTEX_SHADER_ARB, либо в GL_FRAGMENT_SHADER_ARB, в зависимости от значения shaderType. Примечания Как и для таблиц состояния и текстурных объектов, область видимости дескрип- торов всех объектов может распространяться на несколько контекстов. Если дес- крипторы видимы из нескольких контекстов, любые связанные с этими объекта- ми данные тоже доступны. Изменения в шейдерном объекте, сделанные в одном контексте рендеринга, не появляются в другом контексте рендеринга до тех пор, пока для этого контекста не будет вызвана функция gl UseProgramObjectARB. Ошибки GL_INVALID_ENUM возникает, если в shaderType содержится некорректное значение. GL_INVALID_OPERATION возникает при выполнении glCreateShaderObjectARB между соответствующими вызовами функций gl Begi п и gl End. Соответствующие GET-функции glGetlnfoLogARB, в которую передается дескриптор шейдерного объекта. glGetObjectРаrameterARB, в которую передаются дескриптор шейдерного объекта и одно из значений, GL_OBJECT_TYPE_ARB или GL_OBJECT_SUBTYPE_ARB. glGetShaderSourceARB, в которую передается дескриптор шейдерного объ- екта. Смотри также gl AttachObjectARB, gl Compi 1 eShaderARB, gl CreateProgramObjectARB, gl Del eteObjectARB, glDetachObjectARB,glShaderSourceARB 13 Зак 218
370 Приложение Б. Справочник функций API gIDeleteObjectARB Название gl Del eteObJectARB — уничтожает объект, находящийся под управлением OpenGL. Прототип функции void glDeleteObjectARB(GLhandleARB object) Параметры object Дескриптор объекта OpenGL, который будет удаляться. Описание gIDeleteObjectARB освобождает память и делает недействительным дескриптор объекта object. Эта функция уничтожает результаты вызова функций glCreate- ShaderObjectARB и glCreateProgramObjectARB. Если удаляемый шейдерный объект связан с программным объектом, он лишь помечается для удаления, пока не будет отсоединен от всех программных объек- тов в любом контексте рендеринга. Если удаляемый программный объект являет- ся частью текущего состояния, он лишь помечается для удаления, пока не выйдет из текущего состояния всех контекстов. После этого помеченные объекты удаля- ются автоматически. Если с удаляемым программным объектом связаны какие- либо шейдерные объекты, они не удаляются, а лишь отсоединяются, за исключе- нием тех случаев, когда они уже помечены для удаления предыдущими вызовами gIDeleteObjectARB. Чтобы узнать, помечен ли объект для удаления, вызывается функция glGet- ObjectParameterARB с аргументами object и GL_OBJECT_DELETE_STATUS_ARB. Ошибки GL_INVALID_VALUE возникает, если object не является дескриптором объекта OpenGL. GL_INVALID_OPERATION возникает при выполнении функции gl Del eteObjectARB меж- ду соответствующими вызовами gl Begl n и gl End. Соответствующие GET-функции glGetHandleARB с аргументом GL_PROGRAM_OBJECT_ARB. glGetObjectParameterARB с аргументами object и GL_OBJECT_DELETE_STATUS_ARB. Смотри также gl CreateProgramObjectARB,glCreateShaderObjectARB, glDetachObjectARB, glUseProgramObjectARB
glEnableVertexAttribArrayARB 371 gIDetachObjectARB Название gl DetachObjectARB — отсоединяет шейдерный объект от программного объекта. Прототип функции void glDetachObjectARB; GLhandleARB program. GLhandleARB shader) Параметры program Дескриптор программного объекта. shader Дескриптор шейдерного объекта. Описание Функция gl DetachObjectARB отсоединяет шейдерный объект shader от программного объекта program, ее действие является обратным действию функции gl AttachObjectARB. Ошибки GL_INVALID_VALUE возникает, если program или shader не является дескриптором объек- та OpenGL. GL_INVALIDJ3PERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ shader не связан с program; □ функция gIDetachObjectARB выполняется между соответствующими вызовами glBegin и gl End. Соответствующие GET-функции gl GetAttachedObjectsARB, в которую передается дескриптор программного объекта. Смотри также glAttachObjectARB glEnableVertexAttribArrayARB Название gl Enabl eVertexAttri bArrayARB, gl Di sabl eVertexAttri bArrayARB — предоставляют дос- туп или лишают доступа к массиву дополнительных атрибутов вершины.
372 Приложение Б. Справочник функций API Прототип функции void glЕпаЫeVertexAttribArrayARBtGLuint index) void glDisableVertexAttribArrayARB(GLuint index) Параметры i ndex Индекс дополнительного атрибута вершины. Описание Функция gl Enabl eVertexAttri bArrayARB предоставляет доступ к массиву дополнитель- ных атрибутов вершины, на который указывает index. Функция glDisableVertex At t г 1 bArrayARB лишает приложение доступа к массиву дополнительных атрибутов вершины, на который указывает index. По умолчанию ко всем параметрам, вклю- чая дополнительные атрибуты вершины, доступа нет. После вызова функции gl Enabl eVertexAttri bArrayARB с массивом можно работать с помощью функций gl DrawArrays, gl DrawElements,glDrawRangeElements,glArrayElement,glMuitiDrawElements или gl Mui ti DrawArrays. Ошибки GL_INVALID_VALUE возникает, если значение index больше или равно GL_MAX_VERTEX_ ATTRIBS_ARB. Соответствующие GET-функции glGetVertexAttribARB с аргументом index. glGetVertexAttribPointerARB с аргументом index, gl Get с аргументом GL_MAX_VERTEX_ATTRIBS_ARB. Смотри также gl ArrayEl ement, gl Bi ndAttri bLocati onARB, gl DrawArrays, gl DrawEl ements, gl DrawRangeEl ements, glMui ti DrawArrays, gl Mui tiDrawEl ements, gl PopCl IentAttrib, gl PushClientAttrib, gl VertexAttribARB, glVertexAttribPointerARB g IGet Acti veAttr i bARB Название glGetActiveAttribARB — возвращает информацию об активной attribute-перемен- ной для заданного программного объекта. Прототип функции void glGetActiveAttribARB! GLhandleARB program, GLuint index.
gIGetActiveAttribARB 373 GLsizei maxLength. GLsizei *length. GLint *size, GLenum *type, GLcharARB *name) Параметры program Дескриптор программного объекта, к которому выполняет- ся запрос. index maxLength Индекс attribute-переменной, для которой выполняется запрос. Максимальное количество символов, которое OpenGL мо- жет записать в буфер name. length Возвращает количество символов, записанных в буфер name (без учета завершающего нуля), если в функцию передается не NULL. size type name Возвращает размер attribute-переменной. Возвращает тип attribute-переменной. Возвращает строку с завершающим нулем, в которой содер- жится имя attribute-переменной. Описание Функция gl GetActi veAttri bARB возвращает информацию об активной attribute-пе- ременной из программного объекта program. Размер буфера символов, который выделяется приложением, указан в maxLength, а указатель на этот буфер передается в name. Attribute-переменная (встроенная или определенная разработчиком) счита- ется активной, если во время операции компоновки было определено, что програм- ма может обращаться к переменной во время выполнения. Это означает, что для program должна хотя бы раз выполниться операция компоновки gl Li nkProgramARB, не обязательно успешно. Функция g 1 Get Act i veAttri bARB возвращает имя attribute-переменной, указанной no i ndex, сохраняя его в буфере name. Возвращаемая строка завершается нуль-симво- лом. Количество реально записанных символов без учета нуль-символа возвраща- ется в 1 ength. Если приложению это значение нетребуется, можно передавать NULL. Количество активных атрибутов можно узнать с помощью функции glGet- Obj ect Ра ra meterARB с параметром GL_OBJ ECT_ACTI VE_ATTR I BUTES_ARB. При значении i ndex, равном 0, возвращается информация о первой активной attribute-переменной, а при index, равном GL_OBJECT_ACTIVE_ATTRIBUTES_ARB-1, возвращается информация о последней активной attribute-переменной. Длину символьного буфера, требующую- ся для сохранения самого длинного имени переменной из program, можно получить с помощью функции glGetObjectParameterARB с параметром GL_OBJECT_ACTIVE_ ATTRIBUTE_MAX_LENGTH_ARB. В type возвращается указатель на значение типа данных attribute-переменной. Возможные типы данных: GL_FLOAT, GL_FL0AT_VEC2_ARB, GL_FL0AT_VEC3_ARB, GL_FLOAT_- VEC4_ARB, GL_FL0AT_MAT2_ARB, GL_FL0AT_MAT3_ARB, GL_FL0AT_MAT4_ARB. В size возвраща- ется размер атрибута в единицах типа из type.
374 Приложение Б. Справочник функций API Эта функция возвращает максимум информации об указанной attribute-пере- менной, а если таковой нет, значение 1 ength будет равно 0, a name — пустой строке. Это может произойти в случае неудачного выполнения операции компоновки. Ошибки GL_INVALID_VALUE возникает в следующих случаях: □ program не является дескриптором объекта OpenGL; □ значение i ndex больше или равно GL_OBJECT_ACTIVE_ATTRIBUTES_ARB. □ значение maxLength меньше 0. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ функция gl Get Act 1 veAttri bARB выполняется между соответствующими вызовами gl Begiп и gl End. Смотри также gl Bi ndAttribLocati onARB, glLi nkProgramARB, gl VertexAttrl bARB, 1VertexAttrlbPoi nterARB glGetActiveUniformARB Название glGetActiveUniformARB — возвращает информацию об активной uniform-перемен- ной из указанного программного объекта. Прототип функции void glGetActiveUniformARB( GLhandleARB program, GLuint index, GLsizei maxLength. GLsizei *length. GLint *size, GLenum *type. GLcharARB *name) Параметры program Дескриптор программного объекта, к которому выполняется запрос. index Индекс uniform-переменной, для которой выполняется запрос. maxLength Максимальное количество символов, которое OpenGL может записать в буфер name. 1 ength Возвращает количество символов, записанных в буфер name (без учета завершающего нуля), если в функцию передается не NULL.
gIGetActiveUniformARB 375 si ze Возвращает размер uniform-переменной. type Возвращает тип uniform-переменной. name Возвращает строку с завершающим нулем, в которой содержит- ся имя uniform-переменной. Описание Функция gl Get Act 1 vellni formARB возвращает информацию об активной uniform-пе- ременной из программного объекта program. Размер буфера символов, который выделяется приложением, указан в maxLength, ауказатель на этот буфер передается в name. Uniform-переменная (встроенная или определенная разработчиком) счита- ется активной, если во время операции компоновки было определено, что програм- ма может обращаться к переменной во время выполнения. Это означает, что для program должна хотя бы раз выполниться операция компоновки gl Li nkProgramARB, не обязательно успешно. Функция gl Get Act 1 vellni formARB возвращает имя uniform-переменной, указанной по I ndex, сохраняя его в буфере name. Возвращаемая строка завершается нуль-симво- лом. Количество реально записанных символов без учета нуль-символа возвраща- ется в 1 ength. Если приложению это значение не требуется, можно передавать NULL. Количество активных uniform-переменных можно узнать с помощью функции gl Get Ob j ect Pa ramet erARB с параметром GL_OBJ ECT_ACT IV E_UN I FORMS_ARB. При значении index, равном 0, возвращается информация о первой активной uniform-перемен- ной, а при 1 ndex, равном GL_OBJECT_ACTIVE_UNIFORMS_ARB-1, возвращается информация о последней активной uniform-переменной. Длину символьного буфера, требующу- юся для сохранения самого длинного имени uniform-переменной из program, можно получить с помощью функции glGetObjectParameterARB с параметром GL_OBJECT_ ACTIVEJJNIFORM_MAX_LENGTH_ARB. В type возвращается указатель на значение типа данных uniform-перемен- ной. Возможные типы данных: GL_FLOAT, GL_FL0AT_VEC2_ARB, GL_FL0AT_VEC3_ARB, GL_FL0AT_VEC4_ARB, GL_INT, GL_INT_VEC2_ARB, GL_INT_VEC3_ARB, GL_INT_VEC4_ARB, GL_BOOL_ARB, GL_B00L_VEC2_ARB,GL_B00L_VEC3_ARB, GL_B00L_VEC4_ARB, GL_FL0AT_MAT2_ARB, GL_FL0AT_MAT3_ARB, GL_FL0AT_MAT4_ARB. Uniform-переменные, объявленные как структуры или массивы структур, не- посредственно не возвращаются. Вместо этого каждая переменная сводится к ее фундаментальным компонентам, содержащим операторы «.» и «[]», и каждое имя будет являться корректным для вызова функции gl Get Un 1 formLocati onARB. Это имя не может быть именем структуры, массива структур, компонента матрицы или вектора. Каждый из этих фундаментальных компонентов считается отдельной активной uniform-переменной с соответствующим индексом. Если активная uni- form-переменная сводится к элементу массива, все элементы этого массива счи- таются активными. Размер uniform-переменной возвращается в size. Uniform-переменные, не явля- ющиеся массивами, имеют размер 1, а массив uniform-переменных имеет размер этого массива. Структуры и массивы структур сводятся к компонентам, как описа- но ранее, и каждое возвращаемое имя имеет тип данных из приведенного списка. Ес- ли в результате такого приведения получается массив, возвращаемый размер бу- дет размером этого массива, в противном случае возвращаемый размер будет равен 1.
376 Приложение Б. Справочник функций API Перед выполнением этой операции для program должна обязательно вызываться функция gl Li nkProgramARB, не обязательно успешно. Uniform-переменная (встроен- ная или определенная разработчиком) считается активной в программном объек- те, если за время выполнения программы возможны обращения к этой переменной. Список активных uniform-переменных включает как встроенные (начина- ются с gl_), так и определенные разработчиком переменные. Эта функция возвращает максимум доступной информации. Если информа- ция отсутствует, значение length будет равно 0, а в name возвратится пустая стро- ка. Это может произойти в случае неудачного выполнения компоновки. Ошибки GL_INVALID_VALUE возникает в следующих случаях: □ program не является дескриптором объекта OpenGL; □ значение index больше или равно GL_OBJECT_ACTIVE_UNIFORMS_ARB; □ значение maxLength меньше 0. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ функция gl GetActi veUni formARB выполняется между соответствующими вызо- вами glBegin иglEnd. Соответствующие GET-функции glGetObjectParameterARB co значением аргумента GL_OBJECT_ACTIVE_UNIFORMS_ARB или GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB. Смотри также gl GetUni formARB, gl GetUni formLocat i onARB, gl Li nkProgramARB, 1 Um formARB, gl UseProgramObjectARB gIGetAttachedObjectsARB Название gl GetAttachedObjectsARB — возвращает дескрипторы шейдерных объектов, связан- ных с программным объектом. Пррототип функции void gIGetAttachedObjectsARB! GLhandleARB program, GLsizei maxCount.
gIGetAttachedObjectsARB 377 GLsizei *count, GLhandleARB ^objects) Параметры program Дескриптор программного объекта, к которому выполняет- ся запрос. maxCount Максимальный размер массива для списка возвращаемых дескрипторов. count objects Возвращает количество дескрипторов. Массив дескрипторов объектов, в котором возвращается спи- сок связанных шейдерных объектов. Описание Функция gIGetAttachedObjectsARB возвращает список дескрипторов шейдерных объектов, связанных с программным объектом program. В objects возвращается список дескрипторов шейдерных объектов, ограниченный maxCount (если количе- ство связанных шейдерных объектов превышает maxCount, часть из них не будет возвращена). Количество возвращенных дескрипторов записывается в count. Если же это значение не нужно (например, уже известно после вызова функции glGet- ObjectParameterARB), в count можно передавать NULL. Если к program не присоеди- нен ни один шейдерный объект, в count возвращается значение 0. Реальное коли- чество связанных шейдеров можно получить функцией glGetObjectParameterARB с параметром GL_OBJECT_ATTACHED_OBJECTS_ARB. Ошибки GL_INVALID_VALUE возникает: □ если program не является дескриптором объекта OpenGL; □ значение maxCount меньше 0. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ функция gIGetAttachedObjectsARB выполняется между соответствующими вы- зовами glBegi п и gl End. Соответствующие GET-функции glGetObjectParameterARB с аргументом GL_OBJECT_ATTACHED_OBJECTS_ARB. Смотри также glAttachObjectARB, gIDetachObjectARB
378 Приложение Б. Справочник функций API gIGetAttribLocationARB Название gIGetAttribLocationARB — возвращает место расположения attribute-переменной. Прототип функции GLint glGetAttribLocationARB( GLhandleARB program, const GLcharARB *name) Параметры program Дескриптор программного объекта, к которому выполняет- ся запрос. name Указатель на строку с завершающимся нулем, содержащую имя attribute-переменной, место расположения которой не- обходимо определить. Описание Функция gl GetAttг 1 bLocati onARB запрашивает программный объект program о рас- положении attribute-переменной, имя которой передается в name, и возвращает индекс дополнительного атрибута вершины, соответствующего этой attribute-пе- ременной. Если name является именем attribute-переменной матричного типа, воз- вращается индекс первого столбца матрицы. В случае если указанная attribute- переменная не активна в program или если name начинается с зарезервированного префикса gl_, возвращается значение -1. Соответствие имени переменной индексу дополнительного атрибута можно установить в любое время функцией gl Bi ndAttri bLocati onARB. Такие соответствия считаются действительными только после следующего вызова glLinkProgramARB. После успешной компоновки программного объекта значения индексов для attri- bute-переменных остаются постоянными до следующей команды компоновки. Функция gl GetAttri bLocationARB возвращает только действительные соответствия. Соответствия, установленные после последней операции компоновки, не воз- вращаются функцией gIGetAttribLocationARB. Ошибки GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ program не была успешно скомпонована; □ функция gIGetAttribLocationARB выполняется между соответствующими вызо- вами glBegin и glEnd.
glGetlnfoLogARB 379 Смотри также gl Bi ndAttri bLocationARB, gl Li nkProgramARB, gl VertexAttribARB, glVertexAttribPointerARB gIGetHandleARB Название gl Get Handl eARB — возвращает дескриптор объекта, который является частью теку- щего состояния. Прототип функции GLhandleARB glGetHandleARB(GLenum pname) Параметры pname Тип запрашиваемого объекта. Его значение должно быть GL_PROGRAM_OBJECT_ARB. Описание Функция gl GetHandl eARB возвращает дескриптор объекта, который является частью текущего состояния. В аргументе pname передается тип запрашиваемого объекта. В данное время в этом аргументе может передаваться только GL_PROGRAM_OBJECT_ARB. Ошибки GL_INVALID_ENUM возникает, если pname содержит неправильное значение. GL_INVALID_OPERATION возникает при выполнении функции gIGetHandleARB меж- ду соответствующими вызовами gl Begin и gl End. Смотри также glUseProgramObjectARB glGetlnfoLogARB Название glGetlnfoLogARB — возвращает информационный журнал объекта (максимально полные результаты выполнения некоторых операций).
380 Приложение Б. Справочник функций API Прототип функции void glGetlnfoLogARBt GLhandleARB object. GLsizei maxLength. GLsizei *length, GLcharARB *infoLog) Параметры object Дескриптор объекта, к которому выполняется запрос. maxLength Размер символьного буфера, принимающего информацион- ный журнал. 1 ength Возвращает длину строки из InfoLog без учета нуль-символа. j nfoLog Указывает на массив символов, в котором возвращается ин- формационный журнал. Описание Функция gl Get InfoLogARB возвращает информационный журнал указанного объекта OpenGL. Информационный журнал шейдерного объекта обновляется во время компиляции шейдера, а информационный журнал программного объекта — при компоновке или проверке корректности программного объекта. Возвращаемая строка заканчивается нуль-символом. Функция gl Get'In foLogARB возвращает в infoLog максимум доступной информации, которая помещается в буфер, ограниченный maxLength. Количество реально запи- санных символов без учета нуль-символа возвращается в 1 ength. Если это значение не требуется, можно передавать NULL. Размер буфера, требующийся для сохра- нения полного журнала, можно узнать с помощью функции gl GetObj ect Pa rameterARB с аргументом GL_OBJECT_INFO_LOG_LENGTH_ARB. Информационный журнал является строкой и может содержать диагностичес- кие сообщения, предупреждения и другую информацию о последней компиля- ции (для шейдерных объектов) или компоновке или проверке корректности (для программных объектов). Сразу после создания шейдерного или программного объекта его информационный журнал будет строкой длиной в 0 символов. Примечания Информационный журнал создавался только как механизм отладки шейдерных программ, но не для использования при нормальной работе программ. Разработ- чики приложений не должны ожидать наличия в различных реализациях OpenGL одинаковых информационных журналов. Ошибки GL_INVALID_VALUE возникает: □ если object не является дескриптором объекта OpenGL; □ значение maxLength меньше 0.
gIGetObjectParameterARB 381 GL_INVALID_OPERATION возникает в следующих случаях: □ object не является объектом типа GL_PROGRAM_OBJECT_ARB или GL_SHADER_ OBJECT_ARB; □ функция gl Get I nfoLogARB выполняется между соответствующими вызовами glBegin и gl End. Соответствующие GET-функции gIGetObjectParameterARB с аргументом GL_OBJECT_INFO_LOG_LENGTH_ARB. Смотри также gl Compi1eShaderARB,glLinkProgramARB, glVai idateProgramARB gIGetObjectParameterARB Названия glGetObjectParameterfvARB, glGetObjectParameterivARB — возвращает параметр ука- занного объекта. Прототип функции void glGetObjectParameterfvARBl GLhandleARB object. GLenum pname. GLfloat *params) void glGetObjectParameterivARB( GLhandleARB object, GLenum pname. GLint *params) Параметры object Дескриптор запрашиваемого объекта. pname Параметр объекта. Доступны следующие значения: GL_OBJECT_TYPE_ARB, GL_OBJECT_SUBTYPE_ARB, GL_OBJECT_DELETE_STATUS_ARB, GL_OBJECT_LINK_STATUS_ARB, GL_OBJECT_VALIDATE_STATUS_ARB, GL_OBJECT_COMPILE_STATUS_ARB, GL_OBJECT_INFO_LOG_LENGTH_ARB, GL_OBJECT_ATTACHED_OBJECTS_ARB, GL_OBJ ECT_ACTIVE_ATTRIBUTES_ARB, GL_OBJECT_ACTIVE_ATTRIBUTE_MAX_LENGTH_ARB, GL_OBJECT_ACTIVE_UNIFORMS_ARB, GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB, GL_OBJECT_SHADER_SOURCE_LENGTH_ARB. params Возвращается запрашиваемый параметр объекта.
382 Приложение Б. Справочник функций API Описание Функция glGetObjectParameterARB возвращает в params значение параметра объек- та. Определены следующие параметры: GL_OBJECT_TYPE_ARB params возвращает либо GL_PROGRAM_OBJECT_ARB, либо GL_SHADER_OBJECT_ARB, в за- висимости от типа object. GL_OBJECT_SUBTYPE_ARB params возвращает либо GL_VERTEX_SHADER_ARB, либо GL_FRAGMENT_SHADER_ARB, в зависимости от типа object. GL_OBJECT_DELETE_STATUS_ARB params возвращает 1 или l,0f, если объект уже отмечен для удаления, в про- тивном случае возвращается 0 или 0,Of. GL_OBJECT_COMPILE_STATUS_ARB params возвращает 1 или l,0f, если последняя операция компиляции шей- дерного объекта была успешной, в противном случае возвращается О или 0,Of. GL_OBJECT_LINK_STATUS_ARB params возвращает 1 или l,0f, если последняя операция компоновки про- граммного объекта была успешной, в противном случае возвращается О или 0,Of. GL_OBJECT_VALIDATE_STATUS_ARB params возвращает 1 или l,0f, если последняя операция проверки коррект- ности программного объекта была успешной, в противном случае возвра- щается 0 или 0,Of. GL_OBJECT_INFO_LOG_LENGTH_ARB params возвращает количество символов информационного журнала указан- ного объекта, включая нуль-символ (то есть полный размер буфера для со- хранения информационного журнала). Если у объекта нет информацион- ного журнала, то возвращается значение 0 или 0,Of. GL_OBJECT_ATTACHED_OBJECTS_ARB params возвращает количество объектов, связанных с заданным программ- ным объектом. G L_OBJЕСТ_ACTIVE_ATTRIВ UTE S_AR В params возвращает количество активных атрибутов заданного программно- го объекта. G L_OB J Е СТ_АС ТI VE_ATTR I BUTE_MAX_L Е N GTH_ARB params возвращает длину самого длинного имени атрибута заданного про- граммного объекта, включая нуль-символ. Если не существует активных атрибутов, возвращается 0 или 0,Of. GL_OBJЕ СТ_AC ТIV Е_U NIFORMS_ARB params возвращает количество активных uniform-переменных заданного программного объекта.
glGetObjectParameterARB 383 GL_OBJ ECT_ACTIV E_UNIFORM_MAX_LENGTH_ARB params возвращает длину самого длинного имени uniform-переменной за- данного программного объекта, включая нуль-символ. Если не существует активных uniform-переменных, возвращается 0 или 0,Of. GL_OBJECT_SHADER_SOURCE_LENGTH_ARB params возвращает общую длину исходного кода заданного шейдерного объекта, включая нуль-символ. Если исходный код не задан, возвращается О или 0,Of. Примечания Если во время выполнения этой функции возникает ошибка, в params ничего не записывается. Ошибки GL_INVAL ID_VALUE возникает, если object не является дескриптором объекта OpenGL. GL_INVALID_ENUM возникает, если pname содержит некорректное значение. GL_INVALID_OPERATION возникает в следующих случаях: □ pname содержит одно из значений, GL_OBJECT_TYPE_ARB, GL_OBJECT_DELETE_STATUS_ARB или GL_OBJECT_INFO_LOG_LENGTH_ARB, a object не является объектом типа GL_PROGRAM_ OBJECT_ARB или GL_SHADER_OBJECT_ARB; □ pname содержит одно из значений, GL_OBJECT_SUBTYPE_ARB, GL_OBJECT_COMPILE_ STATUS_ARB или GL_OBJECT_SHADER_SOURCE_LENGTH_ARB, a object не является объек- том типа GL_SHADER_OBJECT_ARB; □ pname содержит одно из значений, GL_OBJECT_LINK_STATUS_ARB, GL_OBJECT_ V ALIDATE_STATUS_ARB,G L_OBJ ECT_ATTACHED_OBJECTS_ARB, GL_OBJ ECT_ACTIV E_ATTRIBUTES_ARB, GL_OBJECT_ACTIVE_ATTRIBUTE_MAX_LENGTH_ARB, GL_OBJECT_ACTIVE_UNIFORMS_ARB или GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB, a object не является объектом типа GL_PROGRAM_OBJECT_ARB; □ функция glGetObjectParameterARB выполняется между соответствующими вы- зовами glBeg 1п и gl End. Соответствующие GET-функции glGetActiveAttribARB с аргументом object. glGetActl veUnl formARB с аргументом object. glGetAttachedObjectsARB с аргументом object. glGetlnfoLogARB с аргументом object. glGetShaderSourceARB с аргументом object. Смотри также gl AttachObjectARB, gl Compi 1 eShaderARB, glCreateProgramObjectARB, glCreateShaderObjectARB,
384 Приложение Б. Справочник функций API glDel eteObjectARB, gl LInkProgramARB, glShaderSourceARB, glVaiIdateProgramARB gIGetShaderSourceARB Название gIGetShaderSourceARB — возвращает строку исходного кода из заданного шейдер- ного объекта. Прототип функции void glGetShaderSourceARB( GLhandleARB shader. GLsizei maxLength. GLsizei *length. GLcharARB *source) Параметры shader Дескриптор шейдерного объекта, к которому выполняется запрос. maxLength Размер символьного буфера для сохранения возвращаемой строки кода. 1 ength Возвращает длину строки кода. source Указывает на символьный буфер, в котором возвращается исходный код. Описание Функция gIGetShaderSourceARB возвращает последовательность строк исходного кода шейдерного объекта shader, соединенных в одну строку. Этот код появляется в шейдерном объекте после вызова функции gl ShaderSourceARB. Возвращаемая стро- ка завершается нуль-символом. Функция gIGetShaderSourceARB возвращает в source ту часть кода, которая мо- жет поместиться в maxLength символах. Количество реально записанных символов без учета нуль-символа возвращается в length. Необходимый размер буфера для сохранения всего кода можно получить с помощью функции gl GetOb ject Pa rameterARB с параметром GL_OBJECT_SHADER_SOURCE_LENGTH_ARB. Ошибки GL_INVALID_VALUE возникает: □ если shader не является дескриптором объекта OpenGL; □ значение maxLength меньше 0.
glGetllniformARB 385 GL_INVALID_OPERATION возникает в следующих случаях: □ shader не является объектом типа GL_SHADER_OBJECT_ARB; □ функция gl GetShaderSourceARB выполняется между соответствующими вызовами glBegin и glEnd. Соответствующие GET-функции gIGetObjectParameterARB с аргументом GL_OBJECT_SHADER_SOURCE_LENGTH_ARB. Смотри также glCreateShaderObjectARB, gIGetObjectParameterARB, glShaderSourceARB glGetllniformARB Название glGetUni formfvARB, glGetUniformivARB — возвращает значение uniform-переменной. Прототип функции void glGetUniformfvARB! GLhandleARB program. GLint location. GLfloat *params) void glGetUniformivARB! GLhandleARB program. GLint location. GLint *params) Параметры program Дескриптор программного объекта, к которому выполняет- ся запрос. 1 осаti on Расположение запрашиваемой uniform-переменной. params Возвращает значение указанной uniform-переменной. Описание Функция glGetUniformARB возвращает в params значение указанной uniform-пере- менной. Uniform-переменная указывается с помощью location, а количество воз- вращаемых значений зависит от типа переменной. Если uniform-переменная оп- ределена в шейдере как boolean, int или float, возвращается одно значение. Если же переменная типа vec2, ivec2 или bvec2, возвращаются два значения. При типе переменной vec3, 1 vec3 или bvec3 возвращаются три значения и т. д.
386 Приложение Б. Справочник функций API Расположение uniform-переменной до компоновки программного объекта неиз- вестно. После компоновки его можно получить функцией gl Get Un 1 f ormLoca t i onARB. Это значение затем передается в gl Get Un 1 formARB, чтобы получить текущее значение uniform- переменной. После успешной компоновки программного объекта расположение лю- бой uniform-переменной остается постоянным до следующей операции компоновки. Примечания Если во время выполнения этой функции возникает ошибка, в params ничего не записывается. Ошибки GL_INVALID_VALUE возникает, если program не является дескриптором объекта OpenGL. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ program не был успешно скомпонован; □ 1 оса11 on не указывает на корректную uniform-переменную программного объекта; □ функция glGetUnl formARB выполняется между соответствующими вызовами glBegin и gl End. Смотри также gl Get Act 1 vellni formARB, gl GetUnl formLocati onARB, gl Li nkProgramARB, gl Uni formARB gIGetUniformLocationARB Название gIGetUniformLocationARB — возвращает расположение uniform-переменной. Прототип функции GLint gIGetUniformLocationARB! GLhandleARB program, const GLcharARB *name) Параметры program Дескриптор программного объекта, к которому выполняет- ся запрос. name Указатель на строку с именем uniform-переменной, закан- чивающуюся нуль-символом. Описание Функция gl Get Un i formLocati onARB возвращает целое число, представляющее собой расположение заданной uniform-переменной. С помощью операторов «[]» и «.»
g IGetVertexAttri bARB 387 можно задавать в name компоненты — элементы массива или поля структуры (без пробелов). Расположение первого элемента массива можно получить двумя спо- собами: передавая в name только имя массива или передавая выражение, указыва- ющее на первый элемент массива. Во втором случае элемент массива не может быть структурой, массивом структур, компонентом вектора или матрицы. Значе- ние-1 возвращается, если заданная переменная не найдена в программном объекте, если name не соответствует активной uniform-переменной или начинается с заре- зервированного префикса gl_. Расположение uniform-переменной до компоновки программного объекта неиз- вестно. После компоновки его можно получить функцией gl GetUni formLocat 1 onARB. Это значение затем передается в gl Uni formARB для установки значения uniform- переменной или в gl GetUni formARB, чтобы получить ее текущее значение. После успешной компоновки программного объекта расположение любой uniform-пе- ременной остается постоянным до следующей операции компоновки. Ошибки GL_INVALID_VALUE возникает, если ргодгатнеявляется дескриптором объекта OpenGL. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ program не был успешно скомпонован; □ функция gl GetUni formLocatl onARB выполняется между соответствующими вы- зовами glBegin и gl End. Смотри также gl GetActi veUni formARB, gl GetUni formARB, gl Li nkProgramARB, gl Uni formARB gIGetVertexAttribARB Название gl GetVertexAttri bARB — возвращает параметр дополнительного атрибута вершины. Прототип функции void glGetVertexAttrlbfvARB(GLuint index, GLenum pname. GLfloat *params) void glGetVertexAttribivARB(GLuint index, GLenum pname, GLint *params) void glGetVertexAttr1bdvARB(GLu1nt index, GLenum pname. GLdouble *params)
388 Приложение Б. Справочник функций API Параметры index pname params Индекс дополнительного атрибута вершины. Символическое имя параметра атрибута вершины. Возможны следующие значения: GL_VERTEX_ATTRIB_ARRAY_ENABLED_ARB, GL_VERTEXJTTRIB_ARRAYJIZE_ARB, GL_VERTEX_ATTRIB_ARRAY_STRIDE_ARB, GL_VERTEX_ATTRIB_ARRAY_TYPE_ARB, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED_ARB, GL_CURRENT_V ERTEX_ATTRIB_ARB Возвращает данные. Описание Функция gl GetVertexAttribARB возвращает в params значение параметра дополни- тельного атрибута вершины. Дополнительный атрибут вершины указан в 1 ndex, а запрашиваемый параметр — в pname. Допустимые имена параметров перечислены далее: GL_VERTEX_ATTRIB_ARRAYJNABLED_ARB params возвращает одно значение, которое равно true, если массив атрибу- тов вершин Index доступен, и false, если недоступен. Значение по.умолча- нию — GL_FALSE. GL_VERTEX_ATTRIB_ARRAY_SIZE_ARB pa rams возвращает одно значение — размер массива атрибутов вершин 1 ndex. Он содержит количество значений для каждого элемента в массиве атрибу- тов вершин и может быть 1, 2, 3 или 4. Значение по умолчанию — 4. GL_VERTEX_ATTR I B_ARRAY J5TRI DE_ARB params возвращает одно значение — размер шага по индексу (количество байтов между двумя последовательно расположенными элементами) мас- сива атрибутов вершин для Index. Значение 0 означает, что элементы мас- сива хранятся в памяти последовательно один за другим. Значение по умол- чанию — 0. GL_VERTEX_ATTRIB_ARRAY_TYPE_ARB pa rams возвращает одно значение — константу, указывающую тип элемента мас- сива. Возможные значения: GL_BYTE,GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT, GL_INT,GL_UNSIGNED_INT,GL_FLOAT,GL_DOUBLE. Значение по умолчанию -GLJLOAT. GL_VERTEX_ATTRIB_ARRAY_NORMALIZED_ARB params возвращает одно значение, которое равно true, если типы данных с фиксированной запятой при преобразовании в типы с плавающей запя- той нормализуются, и false — в противном случае. Значение по умолчанию — GLJALSE. GL_CURRENT_VERTEX_ATTRIB_ARB params возвращает четыре значения, представляющих текущее значение до- полнительного атрибута вершины, за исключением атрибута 0 — у него нет текущего состояния. При попытке получить значение атрибута 0 возника-
glGetVertexAttribPointervARB 389 ет ошибка. Значение по умолчанию для всех остальных дополнительных атрибутов вершины — (0, 0, 0, 1). Все параметры, за исключением GL_CURRENT_VERTEX_ATTRIB_ARB, представляют собой параметры клиента OpenGL. Примечания Если во время выполнения этой функции возникает ошибка, в params ничего не записывается. Ошибки GL_INVALID_VALUE возникает, если значение index больше либо равно GL_MAX_VERTEX_ ATTRIBS_ARB. GL_INVALID_ENUM возникает, если в pname задано некорректное значение. GL_INVALID_OPERATION возникает, если значение index равно 0, а значение pname равно GL_CURRENT_VERTEX_ATTRIB_ARB. Смотри также glBindAttribLocationARB, gl DisableVertexAttribArrayARB, gl Enabl eVertexAttri bArrayARB, gl VertexAttri bARB, gl VertexAttribPointerARB glGetVertexAttribPointervARB Название gl GetVertexAttribPointervARB — возвращает адрес заданного указателя. Прототип функции void glGetVertexAttribPointervARB! GLuint index. GLenum pname. GLvoid **pointer) Параметры i ndex Дополнительный атрибут вершины, к которому выполняет- ся запрос. pname Тип запрашиваемого параметра. Должен быть равен GL_VER- TEX_ATTRIB_ARRAY_POINTER_ARB. pointer Возвращает указатель. Описание Функция glGetVertexAttribPointervARB возвращает информацию об указателе, index — дополнительный атрибут вершины, pname — константа, обозначающая,
390 Приложение Б. Справочник функций API что возвращаться должен именно указатель, роi nter — указатель на область памя- ти, где будут сохраняться возвращаемые данные (указатель). Допустимые имена параметров: GL_VERTEX_ATTRIB_ARRAY_POINTER_ARB pointer возвращает одно значение, являющееся указателем на массив атри- бутов вершины для дополнительного атрибута вершины, на который ука- зывает Index. Примечания Возвращаемый указатель является частью клиентского состояния OpenGL. Зна- чение по умолчанию для каждого указателя равно 0. Ошибки GL_INVALID_VALUE возникает, если значение 1 ndex больше либо равно GL_MAX_VERTEX_ ATTRIBS_ARB. GL_INVALID_ENUM возникает, если в pname содержится некорректное значение. Смотри также glVertexAttri bPoi nterARB gILinkProgramARB Название gl LinkProgramARB — выполняет компоновку программного объекта. Прототип функции void glLinkProgramARB(GLhandleARB program) Параметры program Дескриптор программного объекта для компоновки. Описание Функция gl Li nkProgramARB выполняет компоновку программного объекта program. Если с program связаны любые шейдерные объекты типа GL_VERTEX_SHADER_ARB, из них создается выполняемая программа, которая будет запускаться на программи- руемом вершинном процессоре. Если с program связаны любые шейдерные объек- ты типа GL_FRAGMENT_SHADER_ARB, из них создается выполняемая программа, кото- рая будет запускаться на программируемом фрагментном процессоре. Статус компоновки после ее завершения становится частью состояния про- граммного объекта. Это значение устанавливается в GL_TRUE, если компоновка про-
gILinkProgramARB 391 шла без ошибок и выполняемые программы готовы, или в GL_FALSE — в противном случае. Статус компоновки можно получить функцией gl GetObjectParameterARB с ар- гументами program и GL_OBJECT_LINK_STATUS_ARB. После успешной операции компоновки все активные определенные разработ- чиком uniform-переменные в program устанавливаются в 0, и для каждой из актив- ных uniform-переменных устанавливается соответствие с определенным адресом, который можно получить вызовом функции gIGetUniformLocationARB. Любые ак- тивные, определенные разработчиком attribute-переменные, еще не связанные с каким-либо индексом дополнительного атрибута вершины, связываются в мо- мент компоновки. Компоновка программного объекта может завершиться с ошибками по ряду причин, указанных в спецификации языка шейдеров OpenGL. Перечислим не- сколько ситуаций, которые могут спровоцировать ошибку компоновки. □ Превышено количество активных attribute-переменных, поддерживаемых дан- ной реализацией. □ Превышен объем памяти, выделяемой для хранения uniform-переменных. □ Превышено количество активных uniform-переменных, поддерживаемых дан- ной реализацией. □ В вершинном или фрагментном шейдере отсутствует функция main. □ Varying-переменные, используемые во фрагментном шейдере, не объявлены соответствующим образом или вообще не объявлены в вершинном шейдере. □ Код шейдера содержит ссылки на несуществующие или недоступные функ- ции или переменные. □ Глобальная переменная объявлена в разных местах с разными типами или на- чальными значениями. □ Один или более связанных шейдерных объектов был не скомпилирован или скомпилирован с ошибками. □ Некоторые строки матрицы дополнительных атрибутов превышают разрешен- ный максимум GL_MAX_VERTEX_ATTRIBS_ARB. □ Для матрицы дополнительных атрибутов не найдено достаточно свободных смежных участков памяти в массиве атрибутов вершин. После успешной компоновки программный объект становится частью теку- щего состояния с помощью функции gl UseProgramObjectARB. Вне зависимости от успеха компоновки информационный журнал можно получить с помощью функ- ции gl Get Inf oLogARB. Функция gl Li nkProgramARB после успешной компоновки устанавливает создан- ные выполняемые программы, но только в том случае, если заданный программ- ный объект ранее уже устанавливался в текущее состояние функцией gl UseProgram- ObjectARB. Если с program связаны шейдерные объекты типа GL_VERTEX_SHADER_ARB, но нет ни одного шейдерного объекта типа GL_FRAGMENT_SHADER_ARB, созданный вершинный шейдер будет работать совместно со стандартной функциональнос- тью обработки фрагментов OpenGL. И наоборот, если с program связаны шейдер- ные объекты типа GL_FRAGMENT_SHADER_ARB, но нет ни одного шейдерного объекта
392 Приложение Б. Справочник функций API типа GL_VERTEX_SHADER_ARB, созданный фрагментный шейдер будет работать совме- стно со стандартной функциональностью обработки вершин OpenGL. Информационный лог программного объекта обновляется с каждой компонов- кой, и в это же время создаются выполняемые программы. После операции компо- новки приложение может изменять связанные шейдерные объекты, компилиро- вать их, отсоединять, удалять, присоединять дополнительные шейдерные объекты. До следующей операции компоновки это никак не повлияет ни на информацион- ный журнал, ни на выполняемые программы. Примечания Функция gILinkProgramARB не ожидает окончания операции компоновки и сразу же возвращает управление приложению. Все следующие вызовы функций, завися- щие от результатов компоновки (например, gl UseProgramObjectARB), будут ожидать окончания компоновки. Ожидая завершения компоновки, для получения статуса компоновки приложение может использовать функцию gl GetObjectРаrameterARB, которая будет ожидать появления результатов компоновки. Ошибки GL_I NVAL ID_VALUE возникает, если program не является дескриптором объекта OpenGL. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ функция glLI nkProgramARB выполняется между соответствующими вызовами gl Begin и gl End. Соответствующие GET-функции glGetActi veAttri bARB с аргументом program. glGetActi veUni formARB с аргументом program. glGetlnfoLogARB с аргументом program. glGetObjectParameterARB с аргументами program и GL_OBJECT_LINK_STATUS_ARB. glGetUni formARB с аргументом program и адресом uniform-переменной. gl GetUni formLocati onARB с аргументом program и именем uniform-переменной. gIGetHandleARB с параметром GL_PROGRAM_OBJECT_ARB. Смотри также glAttachObjectARB, gl Compi 1 eShaderARB, gl DetachObjectARB, gl Uni formARB, gl UseProgramObjectARB, glVal IdateProgramARB gIShaderSourceARB Название gIShaderSourceARB — устанавливает шейдерному объекту новый исходный код.
gIShaderSourceARB 393 Прототип функции void glShaderSourceARB( GLhandleARB shader. GLsizei nstrings. const GLcharARB **strings. const GLint *lengths) Параметры shader Дескриптор шейдерного объекта, в который будет устанав- ливаться новый исходный код. nstrings strings Количество элементов в массивах strings и lengths. Массив указателей на строки, содержащие части исходного кода для загрузки в шейдер. lengths Массив, содержащий значения длин строк. Описание Функция gIShaderSourceARB загружает исходный код из массива строк strings в shader. При этом старый код шейдерного объекта теряется. Количество строк в массиве strings указано в nstrings. Если значение lengths равно NULL, считает- ся, что каждая строка из strings заканчивается нуль-символом; если же lengths не равно NULL, то указывает на массив со значениями длины соответствующих строк из strings. Каждый элемент этого массива содержит либо количество символов в соответствующей строке без учета нуль-символа, либо значение меньше 0, если соответствующая строка завершается нуль-символом. На данном этапе строки исходного кода никак не анализируются, а просто копируются в указанный шей- дерный объект. Примечания Во время вызова функции gl ShaderSourceARB OpenGL копирует строки исходного ко- да, и приложение может освобождать свою копию сразу после завершения функции. Ошибки GL_INVALID_VALUE возникает: □ если shader не является дескриптором объекта OpenGL; □ значение nstrings меньше 0. GL_INVALID_OPERATION возникает в следующих случаях: □ shader не является объектом типа GL_SHADER_OBJECT_ARB; □ функция gl ShaderSourceARB выполняется между соответствующими вызовами glBegin и gl End. Соответствующие GET-функции gl GetShaderSourceARB с аргументом shader.
394 Приложение Б. Справочник функций API Смотри также glCompi1eShaderARB,glCreateShaderObjectARB, glDeleteObjectARB glUniformARB Название glUniformARB — задает значение uniform-переменной. Прототип функции void glUniformlfARBCGLint location, GLfloat vO) void glUniform2fARB(GLint location. GLfloat vO. GLfloat vl) void glUniform3fARB(GLint location, GLfloat vO. GLfloat vl. GLfloat v2) void glUniform4fARB(GLint location. GLfloat vO. GLfloat vl. GLfloat v2. GLfloat v3) void glUniformliARB(GLint location. GLint vO) void glUniform2iARB(GLint location. GLint vO. GLint vl) void glUniform3iARB(GLint location. GLint vO, GLint vl, GLint v2) void glUniform4iARBCGLint location. GLint vO. GLint vl. GLint v2. GLint v3) Параметры location vO, vl, v2, v3 Адрес изменяемой uniform-переменной. Новые значения uniform-переменной. Прототип функции void glUniformlfvARB( GLint location. GLsizei count, const GLfloat *value)
glUniformARB 395 void glUniform2fvARB( GLint location. GLsizei count. const GLfloat *value) void glUniform3fvARB( GLsizei count. const GLfloat *value) GLint location. void glUniform4fvARB( GLsizei count. const GLfloat *value) GLint location. void glUniformlivARBC GLsizei count. const GLint *value) GLint location. void glUniform2ivARB( GLsizei count. const GLint *value) GLint location, void glUniform3ivARB( GLsizei count. const GLint *value) GLint location, void glUniform4ivARB( GLsizei count. const GLint *value) GLint location, Параметры location count Адрес изменяемой uniform-переменной. Количество элементов, значения которых будут меняться (равно 1, если переменная не является массивом, и равно 1 или больше, если переменная является массивом). value Указатель на массив значений в количестве count, из кото- рого берется новое значение uniform-переменной. Прототип функции void glUniformMatrix2fvARB( GLint location, GLsizei count. GLboolean transpose. const GLfloat *value) void glUniformMatrix3fvARB( GLint location. GLsizei count. GLboolean transpose. const GLfloat *value) void glUniformMatrix4fvARB( GLint location. GLsizei count. GLboolean transpose. const GLfloat *value) Параметры location count Адрес изменяемой uniform-переменной. Количество элементов, значения которых будут меняться (равно 1, если переменная не является массивом, и равно 1 или больше, если переменная является массивом).
396 Приложение Б. Справочник функций API transpose Флаг, указывающий, транспонировать ли матрицу при ус- тановке значений в uniform-переменную. value Указатель на массив значений в количестве count, из кото- рого берется новое значение uniform-переменной. Описание Функция gl Uni formARB устанавливает значение uniform-переменной или массива uniform-переменных. Адрес нужной uniform-переменной задается в 1 оса11 on, а по- лучить его можно функцией glGetUniformLocatlonARB. Функция gl Uni formARB рабо- тает с программным объектом текущего состояния, который был установлен по- следним вызовом gl UseProgramObjectARB. Функции gl Um form{ 112|3|4}{f|i }ARB задают значение uniform-переменной из зна- чений, передаваемых в аргументах. Номер, присутствующий в имени функции, должен совпадать с количеством компонентов типа данных указанной uniform- переменной (то есть 1 для fl oat, 1 nt, bool; 2 для vec2,1 vec2, bvec2 и т. д.). Суффикс f означает, что передаются значения с плавающей запятой; суффикс 1 означает, что передаются целые значения, и эти типы также должны совпадать с типом uniform- переменной. Все 1 -варианты этой функции используются для uniform-переменных типов Int, 1 vec2, 1 vec3,1vec4 или соответствующих массивов, f-варианты исполь- зуются для uniform-переменных типов fl oat, vec2, vec3, vec4 или соответствующих массивов. Для типов bool, bvec2, bvec3, bvec4 или соответствующих массивов мож- но использовать любой из этих вариантов. Uniform-переменная логического типа установится в false при входном значении 0 или 0,Of или в true — при любом дру- гом значении. Все активные uniform-переменные программного объекта инициализируются в 0 после его успешной компоновки. Все значения, установленные функцией gl Uni formARB, сохраняются до следующей успешной компоновки, где они опять сбрасываются в 0. С помощью функций glUniform{l|2|3|4}{f|l}vARB можно менять значения одной uniform-переменной или целого массива. В эти функции передаются количество и указатель на список значений для uniform-переменной или массива перемен- ных. Значение 1 в count соответствует одной uniform-переменной, а значение 1 или больше — целому массиву или части массива. Номер, присутствующий в име- ни функции, должен совпадать с количеством компонентов типа данных указан- ной uniform-переменной (то есть 1 для fl oat, Int, bool; 2 для vec2,1vec2, bvec2 и т. д.). Тип данных, присутствующий в имени функции, должен совпадать с типом соот- ветствующей uniform-переменной, как и в функции gl Um form{ 112|3|4}{f|l}ARB. В массивах uniform-переменных каждый элемент считается того типа, что ука- зан в имени функции (например, функция gl Uni form3f или gl Uni form3fv может ра- ботать с массивом uniform-переменных типа vec3). Количество элементов масси- ва указано в count. Функции gl Uni formFl oatMatrl x{2|3|4}fvARB используются для матриц или масси- вов матриц. Номер в имени функции означает размерность матрицы. Номер 2 оз- начает матрицу 2x2 (4 значения), номер 3 — матрицу 3x3 (9 значений), номер 4 — матрицу 4x4 (16 значений). Если параметр transpose равен GL_FALSE, функция счи- тает, что матрицы передаются по столбцам. Если параметр transpose равен GL_TRUE,
glUniformARB 397 функция считает, что матрицы передаются по строкам. Аргумент count содержит количество передаваемых матриц: 1 — для изменения значения одной матрицы и большее количество — для изменения значений массива матриц. Примечания Для установки значения в uniform-переменную типа семплера годятся только функции glUniformliARB и glUniformllvARB. При попытке использования любой другой функции возникает ошибка GL_INVALID_OPERATION. Если число значений, заданное в count, превышает величину указанной uniform- переменной, возникает ошибка GL_INVALID_VALUE, а значение uniform-переменной не меняется. Если тип и размер uniform-переменной шейдера отличаются от типа и разме- ра, указанных в имени функции, возникает ошибка GL_INVALID_OPERATION, а значе- ние uniform-переменной не меняется. Если location не указывает на uniform-переменную текущего программного объекта, возникает ошибка и значение uniform-переменной не меняется. Ошибки GL_INVALID_OPERATION возникает в следующих случаях: □ текущий программный объект не задан; □ размер uniform-переменной, объявленной в шейдере, не совпадает с размером, указанным в вызове функции glUniformARB; □ один из целочисленных вариантов этой функции используется для установки значения float, vec2, vec3, vec4 или массивов из них, или один из f-вариантов (с плавающей запятой) этой функции используется для установки значения 1 nt, 1 vec2,1 vесЗ, 1 vec4 или массивов из них; □ 1 оса11 on не является корректным адресом uniform-переменной в текущем про- граммном объекте; □ количество задаваемых значений превышает объявленный размер указанной uniform-переменной; □ семплер устанавливается не функциями glUniformliARB и gl Uniforml 1 vARB; □ функция gl Uni formARB выполняется между соответствующими вызовами gl Begl n и gl End. Соответствующие GET-функции glGetActlveUnl formARB, в которую передается дескриптор программного объекта, gl GetUnl formLocati onARB, в которую передаются дескриптор программного объекта и имя uniform-переменной. gl GetUnl formARB, в которую передаются дескриптор программного объекта и ад- рес uniform-переменной. Смотри также glDisable, glEnable, glLinkProgramARB, glUseProgramObjectARB
398 Приложение Б. Справочник функций API glUseProgramObjectARB Название gl UseProgramObjectARB — устанавливает программный объект в текущее состояние для рендеринга. Прототип функции void glUseProgramObjectARBCGLhandleARB program) Параметры program Дескриптор программного объекта, чьи выполняемые про- граммы устанавливаются для рендеринга. Описание Функция gl UseProgramObjectARB устанавливает программный объект program в теку- щее состояние. После успешного связывания шейдерных объектов с программ- ным функцией glAttachObjectARB, успешной их компиляции функцией glCompi 1 е- ShaderARB, успешной компоновки функцией gl LI nkProgramARB в программном объекте создается одна или больше выполняемых программ. Если в программном объекте содержится один или больше успешно скомпи- лированных и скомпонованных шейдерных объектов типа GL_VERTEX_SHADER_ARB, он будет содержать выполняемую программу для вершинного процессора. А если в программном объекте содержится один или больше успешно скомпилирован- ных и скомпонованных шейдерных объектов типа GL_FRAGMENT_SHADER_ARB, он бу- дет содержать выполняемую программу для фрагментного процессора. После успешной установки выполняемых программ на программируемых про- цессорах соответствующая стандартная функциональность уже не будет работать. А именно, если программа установлена на вершинном процессоре, следующие стандартные операции не выполняются: □ на координаты вершин не накладывается матрица модели - вида; □ на координаты вершин не накладывается проекционная матрица; □ на координаты вершин не накладываются матрицы текстур; □ нормали не преобразуются в пространство координат обзора; □ не выполняются масштабирование и нормализация нормалей; □ не выполняется нормализация нормалей по GL_AUTO_NORMAL; □ текстурные координаты не создаются автоматически; □ не вычисляется освещение для вершин; □ не вычисляется цвет материала; □ не вычисляется цвет освещения; □ все перечисленное не выполняется также при установке текущих координат.
glUseProgramObjectARB 399 Вместо этого необходимые операции должны быть реализованы в самом вер- шинном шейдере. Если программа установлена на фрагментном процессоре, следующие стан- дартные операции не выполняются: □ не накладываются текстурное окружение и текстурные функции; □ не используются текстуры; □ не выполняется сложение цветов; □ не вычисляется дымка. Вместо этого необходимые операции должны быть реализованы в самом фраг- ментном шейдере. В то время когда программный объект является частью текущего состояния, приложение может менять связанные шейдерные объекты, компилировать их, присоединять дополнительные шейдерные объекты, отсоединять или удалять их. Все эти операции не влияют на уже установленные выполняемые программы. Однако повторная успешная компоновка текущего программного объекта функ- цией gl Li nkProgramARB повлечет за собой установку новых выполняемых программ. Несмотря на то что часть стандартной функциональности при этом недоступна, параметры внутреннего состояния OpenGL, контролирующие ее, можно изменять обычными вызовами функций OpenGL API. Если program содержит шейдерные объекты исключительно типа GL_VERTEX_ SHADER_ARB, для обработки фрагментов будет использоваться стандартная функци- ональность. И наоборот, если program содержит шейдерные объекты исключительно типа GL_FRAGMENT_SHADER_ARB, стандартная функциональность будет использоваться для обработки вершин. Если program равно 0, программируемые процессоры не будут принимать участия в обработке вершин и фрагментов. Примечания Изменения программного объекта в одном контексте рендеринга не всегда влияют на другой контекст, пока для последнего не будет вызвана функция gl Us eProg га mOb j ectARB. Ошибки GL_INVALID_VALUE возникает, если program не равно 0 и не является дескриптором объекта OpenGL. GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ program нельзя установить в текущее состояние; □ функция glUseProgramObjectARB выполняется между соответствующими вызо- вами glBegin и gl End. Соответствующие GET-функции gl Get На nd 1 eARB с аргументом program. glGetAttachedObjectsARB с аргументом program. glGetActiveAttribARB с аргументом program.
400 Приложение Б. Справочник функций API Смотри также glAttachObjectARB, glCompl 1 eShaderARB, gIDetachObjectARB, glLinkProgramARB, glValIdateProgramARB gIValidateProgramARB Название gl Val 1 dateProgramARB — выполняет проверку программного объекта на корректность. Прототип функции void glVal1dateProgramARB(GLhandleARB program) Параметры program Дескриптор программного объекта для проверки корректности. Описание Функция gIValidateProgramARB проверяет, могут ли выполняемые программы из program работать при текущем состоянии OpenGL. Результаты проверки сохраняют- ся в информационном журнале программного объекта; это может быть пустая строка или текстовая информация о взаимодействии состояния текущего программного объекта с общим состоянием OpenGL. С помощью такой проверки корректности производители различных реализаций OpenGL могут предоставлять разработчику дополнительные средства диагностики: информацию о причинах неэффективно- сти или отказов программы, о возможности дополнительной оптимизации и т. д. Статус операции проверки корректности затем сохраняется в текущем состоянии. Он равен GL_TRUE при успешной проверке или GL_FALSE при обнаружении каких- либо ошибок. Статус можно получить с помощью вызова функции gl Get Ob J ect Ра rameter ARB с аргументами program и GL_OBJECT_VALIDATE_STATUS_ARB. Обычно эта функция требуется только во время процесса разработки приложения. Содержание информационного журнала полностью зависит от реализации OpenGL. Ошибки GL_INVALID_OPERATION возникает в следующих случаях: □ program не является объектом типа GL_PROGRAM_OBJECT_ARB; □ функция gl Val IdateProgramARB выполняется между соответствующими вызовами glBegin иglEnd. Соответствующие GET-функции glGetInfoLogARB с аргументом program. gIGetObjectParameterARB с аргументами program и GL_OBJECT_VALIDATE_STATUS_ARB.
gIVertexAttribARB 401 Смотри также glLInkProgramARB, glUseProgramObjectARB gIVertexAttribARB Название gIVertexAttribARB — устанавливает значение дополнительного атрибута вершины. Прототип функции void glVertexAttriblfARBC GLfloat vO) GLuint index, void glVertexAttriblsARBt GLshort vO) GLuint index. void glVertexAttribldARB( GLdouble vO) GLuint index. void glVertexAttrib2fARB( GLfloat vO. GLfloat vl) GLuint index. void glVertexAttrib2sARB( GLshort vO. GLshort vl) GLuint index, void glVertexAttrib2dARB( GLdouble vO. GLdouble vl) GLuint index. void glVertexAttrib3fARB( GLfloat vO. GLfloat vl, GLfloat v2) GLuint index. void glVertexAttrib3sARB( GLshort vO. GLshort vl. GLshort v2) GLuint index. void glVertexAttrib3dARB( GLdouble vO. GLdouble vl. GLdouble v2) GLuint index. void glVertexAttrib4fARB( GLfloat vO, GLfloat vl. GLfloat v2. GLfloat v3) GLuint index. void glVertexAttrib4sARB( GLshort vO. GLshort vl. GLshort v2. GLshort v3) GLuint index, void glVertexAttrib4dARB( GLdouble vO. GLuint index. 14 Зак. 218
402 Приложение Б. Справочник функций API GLdouble vl. GLdouble v2. GLdouble v3) void glVertexAttrib4NubARB( GLuint index. GLubyte vO. GLubyte vl. GLubyte v2. GLubyte v3) Параметры 1 ndex Индекс изменяемого дополнительного атрибута вершины. vO, vl, v2, v3 Новые значения дополнительного атрибута вершины. Прототип функции void glVertexAttriblfvARBCGLuint index, index, index, const GLfloat * rv) rv) *v) void void glVertexAttri blsvARBCGLui nt glVertexAttri bldvARB(GL u int const GLshort * const GLdouble void glVertexAttrib2fvARB(GLuint index. const GLfloat * rv) void glVertexAttrib2svARB(GLuint index. const GLshort * rv) void glVertexAttri b2dvARB(GLui nt index. const GLdouble *v) void glVertexAttri b3fvARB(GLui nt index. const GLfloat * rv) void glVertexAttri b3svARB(GLui nt index. const GLshort * rv) void glVertexAttrib3dvARB(GLuint index. const GLdouble *v) void glVertexAttrib4fvARB(GLuint index. const GLfloat * 'V) void glVertexAttrib4svARB(GLuint index. const GLshort * ;v) void glVertexAttri b4dvARB(GLui nt index. const GLdouble *v) void void glVertexAttrib4ivARB(GLuint glVertexAttri b4bvARB(GLui nt index, index. const GLint *v) const GLbyte *v) void glVertexAttri b4ubvARB(GLui nt index. const GLubyte *v) void glVertexAttri b4usvARB(GLui nt index. const GLushort : *v void glVertexAttri b4ui vARB(GLuint index. const GLuint * rv) void glVertexAttrib4NbvARB(GLuint index. const GLbyte * rv) void glVertexAttrib4NsvARB(GLuint index, const GLshort *v) void glVertexAttri b4Ni vARB(GLui nt index. const GLint *v) void glVertexAttrib4NubvARB(GLuint index, const GLubyte *v) void glVertexAttrib4NusvARB(GLuint index, const GLushort *v) void glVertexAttrib4NuivARB(GLuint index, const GLuint *v) Параметры Index Индекс изменяемого дополнительного атрибута вершины. V Указатель на массив новых значений дополнительного ат- рибута вершины. Описание В OpenGL определен ряд стандартных атрибутов вершин (цвет, нормаль, текстур- ные координаты и др.). Приложение может задавать их обычными функциями
gIVertexAttribARB 403 OpenGL API. Семейство функций gl VertexAttrl bARB позволяет приложению зада- вать значения дополнительных атрибутов вершины. Дополнительные атрибуты организованы как массив четырехкомпонентных значений. Первый элемент массива имеет индекс 0, а размер массива зависит от реализации, его значение находится в константе GL_MAX_VERTEX_ATTRIBS_ARB. Значе- ния отдельных элементов этого массива дополнительных атрибутов можно зада- вать одной из функций семейства gIVertexAttribARB. За один вызов функции можно задавать значение одного, двух, трех или всех четырех компонентов настраиваемого атрибута вершины, указанного в 1 ndex. Если передается только одно значение, используется функция, в имени которой есть 1, и при этом изменяется значение первого компонента дополнительного атрибута вершины, а второй, третий и четвертый компоненты устанавливаются в 0, 0 и 1 соответственно. Если в имени функции есть 2, значения задаются для первых двух компонентов, третий компонент устанавливается в 0, а четвертый — в 1. Если ис- пользуется функция, в имени которой есть 3, значения задаются для первых трех компонентов, а четвертый компонент устанавливается в 1. Цифра 4 в имени фун- кции означает, что значения задаются для всех четырех компонентов. Буквы s, f, 1, d, ub, us, ui означают типы аргументов short, float, int, double, unsigned byte, unsigned short, unsigned int соответственно. Если к имени добавляется еще и v, функция как аргумент получает указатель на массив таких значений. Если в имени функции присутствует N, функция считает, что ей передаются аргументы в виде чисел с фиксированной запятой, масштабированных к нормализованному диапазону в соответствии с правилами преобразования компонентов, определен- ными в спецификации OpenGL. Значения знаковых типов считаются значения- ми с фиксированной запятой из диапазона [-1, 1 ], а значения беззнаковых типов — значениями с фиксированной запятой из диапазона [0, 1]. Attribute-переменные языка шейдеров OpenGL бывают матричных типов: mat2, mat3 или mat4. Значения переменных этих типов тоже задаются функциями се- мейства gIVertexAttribARB. Матрицы должны быть записаны в последовательные ячейки массива дополнительных атрибутов по столбцам: один столбец матрицы в каждой ячейке. Определенную разработчиком в вершинном шейдере attribute-переменную можно связать с индексом в массиве дополнительных атрибутов вершины функцией glBindAttribLocationARB. Это позволяет обращаться к дополнительным атрибутам не по индексу, а по имени переменной. Изменение значения дополнительного атрибу- та немедленно отражается в связанной attribute-переменной вершинного шейдера. Соответствие индекса в массиве дополнительных атрибутов вершины с опре- деленной разработчиком attribute-переменной вершинного шейдера является ча- стью состояния программного объекта, но текущее значение дополнительного атрибута вершины — нет. Зато это значение является частью текущего состояния OpenGL, как и для стандартных атрибутов вершины, и сохраняется даже после установки другого программного объекта. Приложение может задавать дополнительные атрибуты вершины даже в том случае, если они не соответствуют каким-либо переменным вершинного шейдера. Эти значения просто будут являться частью текущего состояния, и вершинный шейдер не будет иметь к ним доступа. Если во время выполнения вершинного шейдера значение дополнительного атрибута вершины, связанного с переменной, не меняется, шейдер будет использовать одно и то же значение повторно.
404 Приложение Б. Справочник функций API Дополнительный атрибут вершины с индексом 0 — то же самое, что опреде- ленные в OpenGL координаты вершины. Функции gl VertexS, glVertex3, gl Vertex4 полностью эквивалентны соответствующим функциям gIVertexAttribARB с аргу- ментом 1 ndex, равным 0. Вершинный шейдер может обращаться к этому атрибуту с помощью встроенной attribute-переменной gl_Vertex. Для дополнительного ат- рибута вершины с индексом 0 не существует текущих значений — это единствен- ный атрибут с такими свойствами. Все остальные вызовы функций для установ- ки стандартных атрибутов вершины можно чередовать с вызовами функций для установки дополнительных атрибутов. Примечания Значения дополнительных атрибутов вершины можно задавать в любое время. В частности, функция gIVertexAttribARB может вызываться между соответствую- щими вызовами gl Begi п и gl End. Можно устанавливать соответствие одного индекса с несколькими именами attribute-переменных. Эти имена будут являться псевдонимами, что возможно только в том случае, если в вершинном шейдере активна только одна из перемен- ных или каждый путь выполнения шейдера содержит не больше одной такой пе- ременной. Реализации OpenGL не будут проверять наличие псевдонимов для выполнения оптимизации. Не существует возможности создания псевдонимов для стандартных атрибу- тов вершины. Ошибки GL_INVALID_VALUE возникает, если значение index больше или равно GL_MAX_VERTEX_ ATTRIBS_ARB. Соответствующие GET-функции glGetVertexAttribARB с аргументами GL_CURRENT_VERTEX_ATTRIB_ARB и index. glGetAttrlbLocationARB с аргументами program и name. glGetActiveAttri bARB с аргументом program. gl Get с аргументом GL_MAX_VERTEX_ATTRIBS_ARB. Смотри также gl Bi ndAttri bLocationARB,glVertex,glVertexAttribPointerARB g I VertexAttri b Poi nterARB Название gl VertexAttri bPoi nterARB — задает массив дополнительных атрибутов вершин.
glVertexAttribPointerARB 405 Прототип функции void glVertexAttrlbPointerARBC GLuint index. GLint size. GLenum type. GLboolean normalized. GLsizei stride. const GLvoid *pointer) Параметры index Индекс дополнительного атрибута вершины, который будет изменяться. size Количество значений каждого элемента массива дополни- тельных атрибутов вершины. Допустимые значения 1, 2, 3 или 4. type Тип данных каждого компонента массива. Допустимые зна- чения: GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GLJLOAT, GL_DOUBLE. normalized Флаг, определяющий, должны ли значения с фиксирован- ной запятой быть нормализованы (GL_TRUE) или непосред- ственно преобразованы (GL_FALSE). stride Разница в байтах между смежными атрибутами. Если этот параметр равен 0 (значение по умолчанию), считается, что значения расположены в массиве без промежутков. pointer Указатель на первый компонент первого атрибута в массиве. Описание Функция glVertexAttribPointerARB задает адрес и формат данных массива значе- ний дополнительного атрибута вершины. В параметре size указывается количе- ство компонентов каждого атрибута — 1, 2, 3 или 4. Параметр type задает тип дан- ных каждого компонента, astride— расстояние между атрибутами, благодаря чему значения атрибутов могут быть перемешаны с другими значениями или, наобо- рот, храниться в отдельном массиве. При значении normalize, равном GL_TRUE, це- лочисленные значения преобразуются в значения с плавающей запятой и приво- дятся к диапазону [-1, 1] для знаковых и [0, 1] для беззнаковых переменных. В противном случае целочисленные значения преобразуются в значения с плава- ющей запятой без нормализации. После установки массива дополнительных атрибутов вершины size, type, normalized, stride и pointer сохраняются в клиентском состоянии OpenGL. Чтобы открыть или запретить доступ к массиву дополнительных атрибутов вершин, функции glEnableVertexAttribArrayARB и glDisableVertexAttribArrayARB вызываются с аргументом index. При использовании массива дополнительных атрибутов вершины он используется в функциях glDrawArrays, glDrawElements, glDrawRangeElements, glArrayElement, glMuitiDrawElements, glMultiDrawArrays.
406 Приложение Б. Справочник функций API Примечания По умолчанию массив дополнительных атрибутов вершины недоступен и не участвует в вызовах функций glDrawArrays, gl DrawEl ements, glDrawRangeEl ements, glArrayElement, glMultiDrawElements, glMultiDrawArrays. Выполнение функции gl VertexAttri bPointerARB между соответствующими вы- зовами glBegin и glEnd не разрешается, но не всегда при этом возникает ошибка. Если ошибка не возникает, результат выполнения функции не определен. Функция gl VertexAttri bPointerARB обычно реализована в клиенте OpenGL. Параметры массива дополнительных атрибутов вершины являются частью клиентского состояния, и поэтому функции gl PushAttri b и gl PopAttrib с ними не работают. Вместо этого следует использовать функции gl PushCl i entAttrib и gl PopCl lent Attrib. Ошибки GL_INVALID_VALUE возникает в следующих случаях: □ значение index больше или равно GL_MAX_VERTEX_ATTRIBS_ARB; □ значение si ze не равно 1, 2, 3 или 4; □ значение stride отрицательное. GL_INVALID_ENUM возникает, если type содержит некорректное значение. Соответствующие GET-функции glGetVertexAttribARB с аргументом index. glGetVertexAttribPointerARB с аргументом index. glGet с аргументом GL_MAX_VERTEX_ATTRIBS_ARB. Смотри также gl ArrayEl ement, gl BindAttri bLocationARB, gl Di s a bl eVertexAttri bArrayARB, gl DrawArrays, gl DrawEl ements, gl DrawRangeEl ements, gl Enabl eVertexAttri bArrayARB, glMultiDrawArrays, glMultiDrawElements, gl PopCl i entAttri b, gl PushCl ientAttri b, gl VertexAttri bARB
Приложение В Перевод подписей к цветным рисункам Таблица В.1. Перевод подписей к цветным рисункам Номер цветного рисунка Перевод подписи 1 Снимок экрана с интегрированной средой разработки RenderMonkey. В ок- не исходного кода показан процедурный шейдер множества Джулии, при- меняемый к изображению слона. Показаны инструменты для выбора цве- та и интерфейс манипулирования uniform-переменными 2 Полноцветное изображение Земли для текстурной карты шейдера, опи- санного в разделе 10.2 (мраморно-голубое изображение от Рето Стекли, Центр космических полетов NASA) 3 Текстура с изображением Земли, наложенная на сферу с помощью фраг- ментного шейдера, описанного в разделе 10.2 (3Dlabs, Inc.; мраморно- голубое изображение от Рето Стекли, Центр космических полетов NASA) 4 Результат работы с приложением Solid Works 2004, рендеринг изображе- ния выполнен с помощью шейдеров OpenGL: хромированный корпус, оцинкованные стальные детали, чугунное лезвие (любезно предостав- лено корпорацией SolidWorks) 5 «Дневная» и «ночная» текстурные карты для фрагментного шейдера, описанного в разделе 10.3 (мраморно-голубое изображение от Рето Стекли, Центр космических полетов NASA) 6 Дневной, рассветный и ночной виды Земли, созданные шейдерами, описанными в разделе 10.3 (3Dlabs, Inc.; мраморно-голубое изображение от Рето Стекли, Центр космических полетов NASA) 7 Развернутая в прямоугольник текстурная карта кругового обзора из дома. Это изображение используется как карта окружающей среды в шейдерах, описанных в разделе 10.4 (любезно предоставлено Джероми Диварстом, jerome@photographica.co.uk, http://www.photographica.co.uk) 8 Пример наложения карты окружающей среды. Карта, изображенная на цветном рис. 7, используется в шейдерах, описанных в разделе 10.4 (3Dlabs, Inc.) 9 Наложение карты окружающей среды, изображенной на цветном рис. 7, на отражающую поверхность с процедурными «шишечками». «Шишечки» наложены так, как описано в разделе 11.4 (3Dlabs, Inc.) 10 Разница между обычной (внизу слева) и полиномной (вверху справа) текстурными картами. Обычная текстура выглядит плоской и нереалис- тичной при изменении направления освещения, а полиномная карта хо- рошо воспроизводит изменения освещения и самозатенение (© 2003 Hewlett-Packard Development Company, воспроизведено с разрешения) продолжение
408 Приложение В. Перевод подписей к цветным рисункам Таблица В.1 (продолжение) Номер цветного рисунка Перевод подписи 11 Рендеринг тора выполнен с помощью BRDF РТМ шейдеров, описанных в разделе 10.5. Хотя тор в целом черного цвета, следует заметить изме- нения в цвете освещения (от синевато-фиолетового в дальней части до красновато-коричневого вблизи) при изменении угла отражения (© 2003 Hewlett-Packard Development Company, воспроизведено с разрешения) 12 Рендеринг всех этих объектов выполнен с помощью процедурных шейде- ров OpenGL в демонстрационном приложении от LightWork Design. Прило- жение демонстрирует пользовательский интерфейс управления проце- дурными шейдерами (любезно предоставлено LightWork Design) 13 Крупный план разрезанного тора, рендеринг которого выполнен с по- мощью шейдера полосок, описанного в разделе 11.1 (любезно предостав- лено LightWork Design) 14 Крупный план одной из полосок разрезанного тора: эффект перехода цвета (вычисления Fuzz в шейдере, описанном в разделе 11.1) (любезно предоставлено LightWork Design) 15 Шейдер сетки, описанный в разделе 11.3, применен к модели коровы (3Dlabs, Inc.) 16 Промежуточный результат шейдера игрушечного шара, описанного в раз- деле 11.2. На фрагменте А процедурно определенная область окрашена в красный цвет, а вся остальная сфера — в желтый. На фрагменте В по- является голубая полоса (любезно предоставлено ATI Research, Inc.) 17 Освещение игрушечного шара. На фрагменте А изображено наложение рассеянного отражения. На фрагменте Ддля наложения зеркального от- ражения используется аналитически вычисленная нормаль (любезно пре- доставлено ATI Research, Inc.) 18 Простой блок и тор с «шишечками», созданными процедурным способом, описанным в разделе 11.4 (3Dlabs, Inc.) 19 Карта нормали (слева) и результат рендеринга простого блока и сферы с использованием этой карты (справа). Способ наложения карты нормали описан в разделе 11.4.4 (3Dlabs, Inc.) 20 Рендеринг чайников выполнен с помощью шейдеров шума, описанных в главе 12. По часовой стрелке, начиная сверху слева: шейдер облаков, который суммирует четыре октавы шума и выдает результат в виде цветового перехода от синего к белому; шейдер поверхности солнца, который использует функцию определения абсолютного значения для вычисления завихрений; шейдер гранита, использующий простой высокочастотный шум для создания черно-белого перехода; мраморный шейдер, который использует шум для изменения функции синуса, чтобы получить дополнительные прожилки на изображении (3Dlabs, Inc.) 21 Несколько кадров анимации, выполненной шейдером, описанным в раз- деле 13.7 (3Dlabs, Inc.) 22 Рендеринг различных объектов выполнен с помощью шейдера деревян- ной поверхности, описанного в разделе 12.7 (3Dlabs, Inc.) 23 Заливка Гуча выполнена для трех объектов (см. раздел 15.2). Для кон- траста с цветом контурных линий (черный) и освещения (белый) были выбраны цвет фона (серый), теплый цвет (желтый) и холодный цвет (голубой). При этом рендеринге не теряются некоторые детали объектов, которые при обычном рендеринге потерялись бы в тени (3Dlabs, Inc.)
Перевод подписей к цветным рисункам 409 Номер цветного рисунка Перевод подписи 24 Несколько кадров анимационной последовательности, выполненной шейдером системы частиц, описанным в разделе 13.6. Данная система содержит 10 000 частиц со случайно выбранными временем старта и на- чальной скоростью. Координаты частиц в каждом кадре вычисляются в вершинном шейдере по формуле с имитацией эффекта гравитации (3Dlabs, Inc.) 25 Шейдер кирпичной стенки со сглаживанием и без него. Слева показан результат работы шейдера, представленного в главе 6. Справа показаны результаты сглаживания с помощью шейдера, описанного в раз- деле 14.4.5 (3Dlabs, Inc.) 26 Результаты работы шейдера изменения яркости, описанного в разде- ле 16.5.1, со значениями а, равными 0,4; 0,6; 0,8; 1,0 и 1,2 (слева напра- во). Изображение со значением а = 1,0 — то же самое, что и начальное изображение 27 Результаты работы шейдера изменения контраста, описанного в разде- ле 16.5.2, со значениями а, равными 0,4; 0.6; 0,8; 1,0 и 1,2 (слева напра- во). Изображение со значением а = 1,0 — то же самое, что и начальное изображение 28 Результаты работы шейдера изменения насыщенности цвета, описанного в разделе 16.5.3, со значениями а, равными 0,0; 0,5; 0,75; 1,0 и 1,25 (слева направо). Изображение со значением а = 0.0 — то же самое, что и конечное изображение. Изображение со значением а = 1,0 — то же самое, что и начальное изображение 29 Результаты работы шейдера изменения резкости, описанного в разде- ле 16.5.4, со значениями а, равными 0,0; 0,5; 1,0; 1,5 и 2,0 (слева направо). Изображение со значением а = 0,0 — то же самое, что и конечное изображение. Изображение со значением а = 1,0 — то же самое, что и начальное изображение 30 Иллюстрация шейдеров, описанных в разделе 16.6, — реализация раз- личных способов цветовых переходов Таблица В.2. Перевод подписей на цветном рисунке 30 Оригинальная подпись Перевод Base Image Blend Image Darken Основное исходное изображение Дополнительное исходное изображение Затемнение Multiply Color Burn Умножение Усиление цвета Dissolve, 50% Opacity Lighten Screen Color Dodge Add Overlay Растворение, прозрачность 50 % Осветление Экран Ослабление цвета Сложение Наложение продолжение
410 Приложение В. Перевод подписей к Цветным рисункам Таблица В.2 {продолжение) Оригинальная подпись Перевод Soft Light Мягкое освещение Hard Light Жесткое освещение Normal, 50% Opacity Обычный, прозрачность 50 % Difference Разность Exclusion Исключение Inverse Difference Инверсная разность Substract Вычитание
Словарь терминов ID-текстура — одномерный (только ширина) массив значений в текстурной памяти. 2В-текстура — двухмерный (ширина и высота) массив значений в текстурной памяти. ЗВ-текстура — трехмерный (ширина, высота и глубина) массив значений в тек- стурной памяти. API языка шейдеров OpenGL — набор функций OpenGL для создания, уда- ления, компиляции, компоновки и использования шейдеров OpenGL. Attribute-переменная — переменная языка шейдеров OpenGL, которая опре- деляется с ключевым словом attribute. В этих переменных содержатся значения, передаваемые приложением через OpenGL API с помощью массива дополнитель- ных вершинных атрибутов. С помощью attribute-переменных вершинный шей- дер может получать данные для каждой отдельной вершины. Эти данные пред- назначены только для чтения и доступны только из вершинного шейдера. Через attribute-переменные приложение передает в вершинный шейдер уникальные для каждой вершины данные. BRBF — см. Функция распределения двунаправленного отражения. L-значение — выражение, обозначающее некий объект в памяти, доступный для записи. Например, переменные, значения которых можно менять, являются 1-значениями. Индексирование массива и выбор члена структуры являются вы- ражениями, определяемыми как 1-значения. NPR — см. Нефотореалистичный рендеринг. РТМ — см. Полиномная текстурная карта. R-значение — выражение, обозначающее объявленный или временный объект в памяти, доступный для чтения, — например, имена переменных являются г-зна- чениями, а имена функций — нет. Выражения обычно являются г-значениями. Uniform-переменная — переменная языка шейдеров OpenGL, которая опре- деляется ключевым словом uni form. Значения таких переменных задаются прило- жением или через состояние OpenGL. Из шейдера, как вершинного, так и фраг- ментного, такие переменные доступны только для чтения. Uniform-переменные используются для передачи данных, которые изменяются относительно редко. Varying-переменная — переменная языка шейдеров OpenGL, которая опреде- ляется с помощью ключевого слова varying. Новое значение такой переменной задается для каждой вершины и затем интерполируется для целого графического примитива для вычисления значения каждого фрагмента с учетом перспективы. Эти переменные должны быть объявлены с одинаковыми типами и в вершинном,
412 Словарь терминов и во фрагментном шейдере. Значения этих переменных являются выходными для вершинного и входными — для фрагментного шейдера. Активные attribute-переменные — attribute-переменные, к которым обраща- ется вершинный шейдер, включая встроенные и определенные разработчиком attribute-переменные. (Можно определять также attribute-переменные, которые впоследствии не будут использоваться вершинным шейдером.) Активные uniform-переменные — uniform-переменные, к которым обращается шейдер. Существуют как встроенные, так и определенные разработчиком uniform- переменные. (Можно определять также uniform-переменные, которые впослед- ствии не будут использоваться вершинным шейдером.) Активные семплеры — семплеры, к которым обращается выполняющаяся про- грамма. Алиасинг — дефекты изображения при недостаточном семплинге или неадек- ватном представлении высокочастотных компонентов графического компьютер- ного изображения. Эти дефекты еще называют ступеньками. Альфа-компонент — четвертый компонент значения цвета (после красного, зеленого и синего). Он определяет прозрачность пиксела (значение 1,0 говорит о полной непрозрачности, значение 00 — наоборот, о полной прозрачности). Значе- ние альфа-компонента используется в операциях плавного перехода цвета, сгла- живания. Амплитуда -- разница между максимумом или минимумом и средним значе- нием функции. Анизотропность — свойство, значение которого меняется при измерении в раз- личных направлениях, например свойство материала (анизотропное отражение) или характеристика алгоритма (анизотропная фильтрация текстур). Атрибуты вершины — значения, соответствующие определенной вершине. В OpenGL существуют как стандартные, так и дополнительные атрибуты вер- шины. Стандартные атрибуты включают в себя координаты вершины, цвет, нор- маль и текстурные координаты. Другие данные для вершин можно задавать как дополнительные атрибуты, которые передаются в OpenGL для каждой вершины отдельно. Бугристость поверхности — двухмерный массив со значениями возмущения поверхности, который можно сохранить в текстурной памяти. Буфер глубины — внеэкранный буфер памяти OpenGL, в котором хранятся значения глубины. В нем можно хранить глубину примитивов передней поверх- ности в каждом пикселе. Этот буфер можно использовать в операции проверки глубины, чтобы отбрасывать скрытые поверхности. Буфер кадров — часть графической памяти для хранения результатов выпол- нения операций рендеринга OpenGL. Часть буфера кадров (первичный буфер) является видимой на экране, а часть (например, вторичный буфер) — нет. Буфер кадров, операции — этап обработки в последовательности операций OpenGL, на котором выполняются операции, влияющие на буфер кадров в целом (например, маскирование, очистка и т д.). Буфер шаблона — внеэкранная область памяти буфера кадров, которую мож- но использовать при проверке шаблона. В буфере шаблона можно сохранить лю-
Словарь терминов 413 бую сложную фигуру и впоследствии обновлять только пикселы буфера кадров, попадающие в эту фигуру. Вершина — точка в трехмерном пространстве. Вершинный процессор — программируемый модуль, заменяющий стандарт- ную функциональность обработки вершин в OpenGL. На вершинном процессоре выполняются вершинные шейдеры. Вершинный шейдер — программа, написанная на языке шейдеров OpenGL, которая выполняется на вершинном процессоре. Вершинный шейдер выполняет- ся один раз для каждой вершины, и в нем можно запрограммировать как обычные (преобразование координат, вычисление освещения), так и дополнительные опе- рации над вершинами. Видимая память — память кадрового буфера, выделенная для хранения изоб- ражения, видимого на экране. Видимая память считывается много раз за секунду (частота обновления), и именно из нее обновляется экранное изображение. Внешний интерфейс компилятора — часть компилятора, выполняющая лек- сический, синтаксический и семантический анализ исходного кода и генерирую- щая промежуточное двоичное представление кода, которое используется в даль- нейшем процессе компиляции. Внеэкранная память — память кадрового буфера, в которой хранятся данные, не видимые на экране. Например, буфер глубины, текстуры (в отличие от экран- ной памяти). Временной алиасинг — дефекты изображения, которые появились из-за не- правильной дискретизации объекта в движении или неадекватного представле- ния движущегося объекта. Вызов с передачей параметров по значению — соглашение о вызове функ- ции, при котором входные параметры копируются перед началом выполнения функции, а выходные параметры — перед выходом из функции. Выполняемая программа — машинный код, который будет выполняться на вершинном процессоре или на фрагментном процессоре. Геометрический примитив — точка, линия или многоугольник. Градиент — численная характеристика скорости изменения функции в задан- ном направлении. Это вектор (см. Градиент, вектор). Значение вектора градиен- та функции/(х, у) в заданном направлении считается градиентом функцииf(x, у) в этом направлении. Градиент, вектор — вектор, определяющий скорость изменения функции во всех направлениях. Такой вектор для функции f(x, у) содержит два компонента: частную производную f по переменной х и частную производную f по перемен- ной у. Градиентный шум — см. Шум Перлина. Графическая последовательность операций — последовательность операций, выполняемых над геометрическими данными или изображениями. Результатом выполнения последовательности являются данные для буфера кадров. Эта по- следовательность разделена на несколько этапов, которые выполняются в задан- ном порядке. На каждом этапе существуют определенные входные и выходные данные.
414 Словарь терминов Графический акселератор — аппаратный модуль, предназначенный для вы- полнения рендеринга и отображения графики. Графический контекст — структура данных OpenGL, которая содержит состо- яние для управления операциями — компонентами процесса рендеринга. Группа пикселов — значение для обновления буфера кадров (цвет, глубина или шаблон). Двойная буферизация — метод рендеринга, при котором во время вывода на экран переднего буфера вычисляется следующий кадр и записывается в задний буфер. После завершения рендеринга два буфера меняются местами. При исполь- зовании этого метода пользователь никогда не видит незавершенные изображе- ния. Анимация при этом выглядит более реалистично. Дисплейный список — последовательность команд OpenGL, хранимых в па- мяти, управляемой OpenGL, и затем в какой-то момент выполняемых. Драйвер — программное обеспечение, которое взаимодействует с установлен- ной операционной системой и управляет каким-либо аппаратным обеспечением. Дымка — имитация атмосферных эффектов тумана и смога. Чем дальше объект от точки обзора, тем ярче проявляется этот эффект. Зависимое чтение текстуры — операция доступа к текстуре, выполнение которой зависит от значений, полученных от предыдущей операции доступа к тек- стуре. Завихрения — разновидность шума Перлина. Здесь складываются функции шума на различных частотах, включая абсолютное значение функции, чтобы вне- сти в функцию прерывистость и создать впечатление завихрений. Заливка методом Гуро — см. Плавное затенение. Заливка методом Гуча — нефотореалистичный метод рендеринга, с помощью которого выполняют технические иллюстрации. Изотропность — неизменность свойств вдоль пары ортогональных осей (ин- вариантность при вращении). Противоположна анизотропности. Интерполяция ключевых кадров — метод анимации, при котором промежу- точные кадры вычисляются на основании заданных ключевых кадров. Эта проце- дура поможет сохранить время и усилия, так как объекты прорисовываются толь- ко на ключевых кадрах, а не на всех. Карта нормали — текстурная карта, содержащая нормали вместо обычных текстур. Карта отражений — текстурная карта, в которой хранятся отражательные ха- рактеристики поверхности. Касательное пространство — локальное пространство координат поверхнос- ти, определенное с тангенциальным вектором как одним из основных векторов. Конструктор — средство языка программирования для инициализации агре- гатных типов данных или преобразования типов. Кубическая текстура — текстурная карта, состоящая из шести 2О-текстур, соответствующих сторонам куба. Стороны определяются по направлению осей (±х, +у, ±z), и при доступе к текстуре автоматически выбирается нужная сторона. Кубическое наложение — выбор нужной стороны куба из кубической тексту- ры для доступа к конкретному значению текстуры. Кубическое наложение явля- ется одним из методов наложения карты окружающей среды.
Словарь терминов 415 Лексический анализ — процесс обработки входного текста, при котором фор- мируется последовательность лексем для следующего этапа — синтаксического анализа. Символы или лексемы, которые не являются частью заданного языка программирования, могут считаться ошибками. Иногда лексический анализ на- зывают сканированием. Локальное пространство координат поверхности — система координат, пред- полагающая, что каждая точка поверхности имеет координаты (0, 0, 0), а нормаль поверхности — координаты (0, 0, 1). Маскировка уменьшения контрастности — метод увеличения резкости изоб- ражения путем вычитания размытого варианта изображения из самого себя. Матрица вида — матрица для преобразования координат из мировой системы координат в систему координат обзора. В OpenGL эта матрица недоступна сама по себе, а всегда является частью матрицы модели-вида. Матрица модели-вида — матрица для преобразования координат из модель- ной системы координат в систему координат обзора. Матрица проекции — матрица для преобразования координат из системы ко- ординат обзора в систему координат отсечения. Мировая система координат — система координат, в которой удобно распола- гать и ориентировать все объекты сцены. Многовыборочный буфер (буфер мультисемплинга) — область внеэкранной памяти, которую можно использовать для выполнения супердискретизации: хра- нить для каждого пиксела несколько единиц информации, а потом для буфера кадров вычислять среднее значение. Многоуровневая текстура — набор массивов текселов, представляющих со- бой одно и то же изображение. Обычно каждый следующий массив имеет разре- шение в половину предыдущего по каждой размерности. Моделирование — процесс создания численного представления объекта, ко- торое затем будет использоваться для рендеринга. Например, определение кри- вых Безье для изображения чайника или координаты вершин, нормали поверх- ности и текстурные координаты для определения кегли. Модельная матрица преобразования — матрица для преобразования координат из модельной системы координат в мировую систему координат. В OpenGL эта мат- рица недоступна сама по себе — она всегда является частью матрицы модели—вида. Модельная система координат — система координат, определенная таким об- разом, чтобы было удобно определять и ориентировать один объект. Модельное преобразование — преобразование координат из модельной сис- темы координат в мировую систему координат. Набор дополнительных операций над изображением — содержит поддержку матриц цвета, свертки, гистограмм и различных операций смешивания. Произво- дители аппаратного обеспечения не должны поддерживать в обязательном по- рядке эту функциональность. Накопительный буфер — буфер памяти OpenGL, не отображаемый на экран (закадровый), в котором можно накапливать результаты нескольких операций рендеринга. Зачастую в этом буфере для одного пиксела зарезервировано больше битов на пиксел, чем в других закадровых буферах, чтобы накапливать результа- ты большего количества операций рендеринга.
416 Словарь терминов Наложение карты окружающей среды — метод рендеринга, подразумеваю- щий сохранение изображения окружающего мира объекта как текстурной карты и последующее использование этой текстурной карты при рендеринге для вычис- ления имитации отражения среды. Наложение текстуры — использование данных из текстурной памяти для вы- числения цвета фрагмента. В стандартной функциональности OpenGL есть соот- ветствующие формулы, но с новыми возможностями программируемости это вычисление может быть каким угодно. Непосредственный режим — режим рендеринга, в котором графические ко- манды выполняются сразу после того, как они задаются, а не сохраняются в дисп- лейном списке. Нефотореалистичный рендеринг (NPR) — набор методов рендеринга, целью которых является достижение не реалистичного изображения, а эффекта ручного рисования, или технической иллюстрации, или мультфильма. Штриховка и за- ливка Гуча являются примерами методов нефотореалистичного рендеринга. Нормализованное пространство приборных координат — пространство ко- ординат, в котором находится отображаемый объем куба, выровненного по осям, от угла (-1, -1,-1) до угла (1, 1, 1). Обработка вершин — этап рендеринга OpenGL, на котором выполняются опе- рации над каждой вершиной от момента задания вершины до этапа сборки при- митивов. В стандартной функциональности OpenGL этот этап включает преоб- разования координат, вычисление освещения, вычисление текстурных координат и другие операции. На программируемом вершинном процессоре можно выпол- нять любые операции над вершинами. Обработка фрагментов — см. Фрагменты, обработка. Обращение к компонентам вектора (свиззлинг) — выполняется, чтобы ско- пировать или поменять местами компоненты вектора, например: создать значе- ние прозрачность, зеленый, синий, красный вместо значения красный, зеленый, синий, прозрачность. Обращение к текстуре — чтение данных из текстурной карты в текстурную память, включая фильтрацию, вычисление уровня детализации для многоуров- невых текстур и т. д. Оконное преобразование — преобразование координат из нормализованного пространства приборных координат в оконную систему координат. Оконные координаты — система координат для определения пикселов внут- ри окна на экране. В этой системе координат диапазон для х от 0 до ширины окна минус 1, а диапазон для у от 0 до высоты окна минус 1. OpenGL определяет пик- сел с координатами (0, 0) как пиксел нижнего левого угла окна. Октава — две частоты в соотношении 2:1. Операции с буфером кадров — см. Буфер кадров, операции. Отбраковка — процесс отбрасывания графических примитивов по определен- ным признакам, например: являются ли они примитивами задней поверхности по отношению к текущей точке обзора. Отображаемый объем — объем в пространстве координат отсечения, коорди- наты которого х, у, z и w будут удовлетворять всем условиям -w <x<w, -w<y <w, -w <z<w. Любые части примитива за пределами этого объема будут отсекаться.
Словарь терминов 417 Отображение бугристости поверхности — метод рендеринга, при котором имитируется поверхность с «шишечками», складками или другими неровностя- ми поверхности с помощью внесения возмущений в значения нормалей поверх- ности перед вычислением освещения. Отображение текстуры — сочетание обращения к текстуре и наложения тек- стуры. При этом данные о текстуре, взятые из текстурной карты, считываются из текстурной памяти и используются для вычисления цвета фрагмента. С новыми возможностями программируемости эти вычисления могут быть какими угодно. Отсечение по прямоугольнику — необязательный этап рендеринга в OpenGL. Если этот режим включен, операции рисования происходят только внутри задан- ной прямоугольной области, определенной в оконной системе координат. Отсечение усеченным конусом — см. Усеченный конус, отсечение. Отсечение — процесс сравнения входных графических примитивов с одной или несколькими заданными плоскостями и отбрасывания примитивов, которые находятся вне отсекаемой плоскостями области. Паковка пикселов — этап рендеринга в OpenGL, в котором пикселы записы- ваются в память, управляемую приложением. Перемещение пикселов — этап рендеринга в OpenGL, в котором пиксельные данные обрабатываются и передаются внутри OpenGL. Обработка может вклю- чать в себя масштабирование и смещение, поиск по таблице, свертку, гистограм- му, матрицу цветов и т. д. Пиксельный прямоугольник — прямоугольный массив пикселов (растровое изображение). Плавное затенение — наложение линейно интерполированных значений цве- та на примитив (в отличие от постоянного затенения). Также называется затене- нием методом Гуро. Поглощение — эффект, при котором интенсивность освещения уменьшается с увеличением расстояния от источника освещения. Полиномная текстурная карта (РТМ) — зависимая от освещения текстурная карта, которую можно использовать для воспроизведения цвета поверхности в раз- личных условиях освещения. Положение растра — часть состояния OpenGL для операций записи растро- вых и других изображений. Последовательность операций рендеринга — см. Графическая последователь- ность операций. Постоянное затенение — термин для обозначения заливки примитива одним цветом, в отличие от плавного затенения (метод Гуро). Пофрагментные операции — операции OpenGL, которые выполняются после обработки фрагментов и перед операциями с буфером кадров. Это, например, множество проверок того, нужен ли фрагмент для буфера кадров: проверки шаб- лона, прозрачности и глубины. Преобразование вида — преобразование координат из мировой системы ко- ординат в систему координат обзора. Преобразование и освещение — процесс преобразования координат вершин из системы координат объекта в оконную систему координат и цвета вершин — в экранные цвета, учитывая эффект освещения.
418 Словарь терминов Примитивы — в понятиях OpenGL это объекты, для которых можно выпол- нить рендеринг: точки, линии, многоугольники, растровые и другие изображения. Проверка глубины — этап рендеринга OpenGL, в котором сравниваются глу- бина обрабатываемого фрагмента и глубина из буфера кадров. Если проверка по- казывает, что фрагмент не виден, он отбрасывается. Проверка на видимость — этап рендеринга в OpenGL, в котором проверяется, нужен ли заданный фрагмент для обновления буфера кадров или он принадле- жит другому окну или другому графическому контексту OpenGL. Проверка прозрачности — часть последовательности операций рендеринга в OpenGL, в которой некоторые фрагменты отбрасываются в зависимости от зна- чения прозрачности текущего фрагмента и заданного значения прозрачности. Проверка чтения — этап в OpenGL, содержащий состояние для определе- ния области памяти буфера кадров, считываемой во время операций чтения пикселов. Проверка шаблона — этап рендеринга в OpenGL, на котором отбрасываются пикселы, которые не проходят сравнения с соответствующими значениями буфе- ра шаблона. Приложение может задавать действие, которое будет происходить в случае неудачной проверки шаблона, либо успешной проверки шаблона и не- удачной проверки глубины, либо в обоих случаях. Программный интерфейс приложения (API) — набор функций, который при- ложения могут использовать для своих нужд. Программный объект — управляемая OpenGL структура данных, используе- мая как контейнер для одного или нескольких шейдерных объектов. Программ- ные объекты используются при компоновке шейдеров. Результат компоновки программного объекта — одна или несколько выполняемых программ, которые станут частью программного объекта и которые можно будет установить в теку- щее состояние. Проекционная матрица модели-вида — матрица для преобразования коор- динат из модельной системы координат в систему координат отсечения. Проецирование — преобразование координат из системы координат обзора в систему координат отсечения. Произвольное отсечение — операция OpenGL, в которой положение графи- ческих примитивов сравнивается с положением заданных плоскостей отсечения. Любые примитивы вне области отсечения отбрасываются. Пространство координат отсечения — система координат, в которой проис- ходит отсечение. Графические примитивы преобразуются из пространства коор- динат обзора в пространство координат отсечения с помощью проекционной мат- рицы. Пространство моделей — см. Модельная система координат. Пространство обзора — см. Система координат обзора. Пространство объекта — см. Модельная система координат. Пространство отсечения — см. Пространство координат отсечения. Процедурное текстурирование — процесс вычисления текстуры, в отличие от чтения текстуры из текстурного модуля. Процедурный текстурный шейдер — шейдер, который вычисляет результа- ты, а не получает их из заранее заданных значений.
Словарь терминов 419 Псевдонимы атрибутов — сответствие более одной attribute-переменной одно- му и тому же индексу дополнительного вершинного атрибута. Такое разрешается лишь в случае, если только одна из этих переменных активна или не существует пу- тей выполнения шейдера, в которых используется более одной из этих переменных. Распаковка пикселов — этап рендеринга в OpenGL, в котором пикселы счи- тываются из памяти, управляемой приложением, и отправляются на дальнейшую обработку. Растеризация — процесс преобразования графических примитивов (точек, линий, многоугольников, растровых и других изображений) во фрагменты. Режим дисплейного списка — режим рендеринга, при котором команды OpenGL не выполняются сразу, а сохраняются в дисплейном списке, который позже запускается на выполнение. Рендеринг — процесс преобразования геометрических данных или изображе- ния, заданных приложением, в приемлемые для буфера кадров данные. Сборка примитивов — этап рендеринга в OpenGL, выполняющийся после об- работки вершин. Сборка значений отдельных вершин в примитив. Основное на- значение этого этапа — собирать значения вершин, пока их не будет достаточно для определения примитива. Для точки нужна одна вершина, для линии — две, для треугольника — три и т. д. Свертка — средневзвешенное значение функции в заданном диапазоне. Сверточный фильтр — см. Ядро свертки. Сглаживание (антиалиасинг) — приемы для уменьшения дефектов изображе- ния, вызванных недостаточным семплингом или неадекватным представлением высокочастотных компонентов графического компьютерного изображения. Сглаживающие фильтры — см. Фильтрация высоких частот. Семантический анализ — процесс определения того, соответствует ли вход- ной текст семантическим правилам, заданным в языке программирования. Се- мантические ошибки во входном тексте можно определить на этапе компиляции. Серия импульсов — периодическая функция, прерывисто изменяющаяся меж- ду двумя значениями (например, прямоугольные колебания). Синтаксический анализ — процесс проверки грамматики языка программи- рования. Синтаксические ошибки во входном тексте можно определить на этапе компиляции. Система координат обзора — система координат, определенная относительно точки обзора. Координаты входных графических примитивов всегда преобразу- ются из системы координат модели (объекта) в систему координат обзора с помо- щью матрицы модели-вида. Система координат объекта — см. Модельная система координат. Система частиц — примитив, состоящий из большого количества точек или коротких линий, для рендеринга объектов с нечеткими краями (огня, искр, рас- пыленной жидкости). Сканирование — см. Лексический анализ. Сложение цветов — часть функциональности OpenGL, во время которой скла- дываются первичный и вторичный цвета. Это происходит после наложения тек- стуры, чтобы рассеянное освещение цвета источника освещения накладывалось на текстурированную поверхность.
420 Словарь терминов Стандартная функциональность — здесь этот термин используется для обо- значения непрограммируемых частей из последовательности операций OpenGL. Управлять поведением таких частей можно только изменением состояния OpenGL с помощью OpenGL API. Супердискретизация — метод рендеринга, в котором задействованы несколь- ко единиц информации на пиксел для формирования значения этого пиксела. Этот метод не может полностью гарантировать отсутствие дефектов изображения, но теперь эти дефекты становятся малозаметными. Сферическое наложение — метод наложения карты окружающей среды, который имитирует отражение среды от сферы, окружающей объект, рендеринг которого выполняется в данный момент. Карта окружающей среды является двух- мерной текстурной картой, обращение к которой происходит по полярным коор- динатам вектора отражения. Семплер — «непрозрачный» тип данных языка шейдеров OpenGL, в котором хранится информация для доступа к конкретной текстуре из шейдера. Семплинг по зонам — методы сглаживания, в которых учитывается зона за- данного примитива. Такие методы обычно дают лучшие результаты, чем точеч- ная дискретизация или супердискретизация, но зато занимают больше ресурсов при вычислениях. Тексел — пиксел текстурной карты. Текстурирование — см. Отображение текстуры. Текстурная память — часть памяти графического акселератора для хранения текстур. Текстурный модуль — абстракция OpenGL для графического оборудования, выполняющая обращение к текстуре и наложение текстуры. В версии OpenGL 1.2 появилась возможность иметь более одного текстурного модуля и, таким обра- зом, одновременно обращаться к нескольким текстурам. Текстурный объект — управляемая OpenGL структура данных, содержащая информацию о текстурной карте, включая текселы, определяющие текстуру, ме- тод фильтрации и т. д. Точечная дискретизация — процесс определения значения каждого пиксела с помощью дискретизации функции в одной точке. Так обычно работает графи- ческое оборудование и множество графических алгоритмов, но применение этого способа может создавать зубцеобразные дефекты изображения. Противополож- на супердискретизации и дискретизации по зонам. Управляющая текстура — текстурная карта, в которой хранятся значения, являющиеся не данными для процесса рендеринга, а управляющими значения- ми, определяющими поведение алгоритма рендеринга. Уровень детализации — значения для выбора нужного уровня из многоуров- невой текстуры. Увеличивая уровень детализации на 1, получаем уровень тексту- ры, имеющий половинное разрешение предыдущей. Увеличивая уровень детали- зации, получим текстуры все меньшего размера (для объектов меньшего размера). Уровень текстуры — массив текселов с отдельной текстурой из многоуровне- вой текстуры. Усеченный конус обзора — объем пространства, видимый с заданной точки обзора после вычисления перспективы.
Словарь терминов 421 Усеченный конус, отсечение — этап обработки OpenGL, на котором прими- тивы, не видимые в усеченном конусе, отбрасываются. Усреднение соседних элементов — метод обработки изображений для сгла- живания изображения вычислением средних значений соседних пикселов. Фильтрация верхних частот — метод, в котором отбрасываются верхние час- тоты, а нижние остаются неизменными. Такие фильтры иногда называются филь- трами сглаживания, так как высокие частоты размываются (сглаживаются). Фильтрация — вычисление одного значения, основанное на нескольких сосед- них значениях. Фотореалистичность — моделирование, рендеринг и анимация в компьютер- ной графике, выполненные таким образом, что результат не отличается от реаль- ных фотографии или фильма. Фрагмент — набор данных, полученный после растеризации и представляю- щий собой информацию об одной точке буфера кадров. Данными фрагмента яв- ляются оконные координаты, цвет, глубина, текстурные координаты и другие па- раметры. Фрагментный процессор — программируемый модуль, заменяющий стандарт- ную обработку фрагментов в OpenGL. На фрагментном процессоре выполняются фрагментные шейдеры. Фрагментный шейдер — программа, написанная на языке шейдеров OpenGL, которая выполняется на фрагментном процессоре. Функция main фрагментного шейдера выполняется один раз для каждого входного фрагмента и может быть запрограммирована для выполнения как обычных операций (доступ к текстуре, наложение текстуры, дымка), так и любых других операций. Фрагменты, обработка — этап обработки в последовательности операций OpenGL, в котором выполняются вычисления для фрагмента. Стандартная функциональ- ность обработки фрагментов OpenGL включает обращение к текстуре, наложе- ние текстуры, вычисление дымки и сложение цветов. На программируемом фраг- ментном процессоре OpenGL можно выполнять любые операции над фрагментами. Функция распределения двунаправленного отражения, BRDF — модель вы- числения отражения от поверхности с анизотропными отражающими свойства- ми. Для вычисления интенсивности отраженного освещения используются углы возвышенности и азимута входного и выходного направлений. Чтобы изображе- ние выглядело более реалистично, для вычислений можно использовать резуль- таты измерения настоящих материалов. Частота — измерение периодичности функции (определение того, как часто повторяются одинаковые отрезки значений функции). Шейдер OpenGL — название шейдера, написанного на языке шейдеров OpenGL, отличающее его от шейдеров, написанных на других языках. Шейдер — исходный код, написанный на языке шейдеров OpenGL и предна- значенный для выполнения на одном из программируемых процессоров OpenGL. Шейдерный объект — управляемая OpenGL структура данных для хранения исходного кода и скомпилированного кода шейдера, написанного на языке шей- деров OpenGL. Шейдерные объекты можно компилировать, а успешно скомпи- лированные — компоновать, чтобы создать выполняемую программу (см. Про- граммный объект).
422 Словарь терминов Шум Перлина — функция шума, имеющая значение 0 для целых входных зна- чений; для каждого из них определяется псевдослучайный градиентный вектор. Также называется градиентным шумом. Шум — непрерывная неравномерная функция в определенном диапазоне для создания сложных и интересных изображений. Эффект глубины — метод рендеринга, использование которого изменяет вне- шний вид примитива при удалении или приближении к зрителю. Зачастую при этом цвет удаленных примитивов приближается к цвету фона. Ядро свертки — значения для вычисления средневзвешенного значения функ- ции в операции свертки. Язык шейдеров OpenGL — высокоуровневый язык программирования, позво- ляющий разработчикам приложений писать программы, выполняемые на програм- мируемых процессорах, определенных в стандарте OpenGL.
Алфавитный указатель А attribute-переменная активная, 168 алиасинг, 290 временной, 291 анизотропное отражение, 222 В антиалиасинг, 40, 290 BRDF, 222 атрибуты активные, 167 С вершин, 36, 104 Cg, 350 установка, 161 н Б HLSL, 348 базовая функциональность, 34 бинормаль, 247 I бугристая поверхность, 245 ISL, 346 буфер 0 глубины, 33 накопительный, 33 OpenGL, 28 OpenGL Shader, 346 OpenGL-шейдер, 55 шаблона, 33 буфер кадров, 32 В R вектор-градиент, 295 RenderMan, 344 вершинный процессор, 58, 102 u видимая память, 32 виртуальное текстурирование, 336 uniform-переменные установка, 170 временной алиасинг, 291 встроенные константы, 114 A выражения препроцессора, 99 вычисление освещения, 195 активные атрибуты, 167 активные семплеры, 175 двухстороннее, 201 направленный свет, 195
424 Алфавитный указатель вычисление освещения {продолжение) при отсутствии освещения, 201 прожекторы, 197 точечные источники, 196 Г геометрические функции, 130 глобальное пространство, 45 градиент, 296 графические примитивы, 35 графический контекст, 32, 34 графический ускоритель, 31 д данные удаление, 158 двойная буферизация, 32 двухсторонний цветовой режим, 115 дискретизатор, 68, 84 драйвер, 70 3 закадровая память, 32 заливка Гуча, 313 затенение плавное, 40 постоянное, 40 значение шума, 256 зональный семплинг, 298 И изображения геометрические преобразования, 325 интерполяция и экстраполяция, 327 изменение насыщенности цвета, 329 контраст, 328 изображения {продолжение) уменьшение резкости, 329 яркость, 328 обработка выделение края, 339 свёртка, 334 сглаживание, 336 увеличение резкости, 339 инициализаторы, 87 интерполяция ключевых кадров, 276 интерфейс шейдера, 89 К карта нормали, 251 отражений, 214 касательное пространство, 247 клиентские состояния, 31 ключевые кадры интерполяция, 276 колебания, 285 конструкторы, 87 коэффициент дымки, 202 Л лексический анализ, 190 локальное пространство координат поверхности, 246 м маска уменьшения контрастности, 329 масштабирование, 325 матрица визуализации, 46 модели-вида, 46 матричные функции, 131 моделирование, 45 модельная матрица преобразования, 46
Алфавитный указатель 425 модуль текстур, 49 модульность, 184 н наложение карты окружающей среды, 218 кубическое, 218 сферическое, 218 наложение текстур, 52, 206 нефотореалистичный рендеринг, 305 О обработка вершин, 38 обработка примитивов отбраковка, 40 отсечение, 39 расчет перспективы, 40 обработка фрагментов, 41 общие функции, 124 октава, 257 операции, 95 обработки вершин, 38 обработки фрагментов, 41 обращение к компонентам, 96 обращение по индексу, 95 отображение текстуры, 48 покомпонентные, 97 с буфером кадров, 41 операция наложение текстур, 52 отбрасывание частот, 301 отладка шейдера, 186 отображаемый объем, 39 отражение анизотропное, 222 отсечение, 116 произвольное, 48 усеченным конусом, 48 п память видимая, 32 закадровая, 32 парсинг, 190 переменные атрибутов, 59 пиксел, 32 пиксельные прямоугольники, 42 плавное затенение, 40 поглощение света, 196 полиномное отображение текстуры, 223 полиномные карты текстур, 223 постоянное затенение, 40 пределы, 275 преобразование типов, 87 преобразование типов данных, 89 препроцессор, 98 примитивы, 31 проверка чтения, 44 программный объект, 71,155 проецирование, 47 произвольное отсечение, 48,206 пространство касательное, 247 координат поверхности локальное, 246 моделей, 45 обзора, 46 отсечения, 47 процедурное текстурирование, 233 процедурные текстурные шейдеры, 233 процессор вершинный, 58, 102 фрагментный, 62,107 псевдонимы атрибутов, 165 Р рабочая среда эффекта, 188 распаковка пикселов, 43
426 Алфавитный указатель растеризация, 40 редактор. См. AppBrowser режимы плавного перехода, 330 вычитание, 334 жесткое освещение, 333 затемнение, 331 инверсная разность, 334 исключение, 334 мягкое освещение, 333 наложение, 332 обычный, 330 осветление, 332 ослабление цвета, 332 очистка, 331 прозрачность, 334 разность, 334 растворение, 331 сложение, 333 среднее значение, 331 умножение, 332 усиление цвета, 332 экран, 332 режимы рисования объекта, 275 рендеринг, 31 многопроходный, 117 нефотореалистичный, 305 С сборка примитивов, 39 свёртка, 298, 334 свёрточный фильтр, 298, 334 сглаживание, 290 семантический анализ, 190 семплер, 84 семплеры, 174 активные, 175 серверные состояния, 31 серия импульсов, 298 синтаксический анализ, 190 системы частиц, 279 смещение, 325 согласование типов, 87 спецификаторы, 89 attribute, 90 const, 91 uniform, 90 varying, 91 отсутствие, 92 стандарт OpenGL, 28 супердискретизация, 292 т таблицы, 326 текселы, 50 текстура обращение из шейдера, 210 текстурирование, 41 процедурное, 233 текстурные карты, 103 текстурные координаты формирование, 204 текстуры наложение, 206 типы данных void, 86 векторы, 82 дискретизаторы, 84 массивы, 85 матрицы, 83 объявления переменных, 86 преобразование, 87 скалярные, 81 согласование, 87 структуры, 85 точечная дискретизация, 291 тригонометрические функции, 122
Алфавитный указатель 427 У удаление данных, 158 узлы группировки эффектов, 188 переменных, 189 проходов, 189 управляющая текстура, 234 уровень уменьшенное™ текстуры, 50 усреднение соседних элементов, 336 Ф фильтрация верхних частот, 295 формирование текстурных координат, 204 фотореализм, 305 фрагмент, 40 фрагментный процессор, 62, 107 функции, 93 встроенные, 94 геометрические, 130 доступа к текстуре, 134 запроса состояния, 158 матричные, 131 обработки фрагмента, 136 общие, 124 отношения векторов, 132 тригонометрические, 122 шума, 136 экспоненциальные, 123 функция распределения двунаправленного отражения, 222 ш шейдерные объекты, 70 компиляция, 154 создание, 153 шейдеры анализ производительности, 184 вершинные, 56, 59 компоновка, 155 отладка, 186 разработка, 182 модульность, 184 тестирование и повторение, 183 упрощение, 184 средства разработки, 187 RenderMonkey, 188 фрагментные, 56 шум, 254 большой размерности, 260 завихрение, 266 малой размерности, 259 Перлина, 259 текстуры, 260 часть периодической функции, 268 э экспоненциальные функции, 123 эффект глубины, 202 дымки, 202 эффекты анимации два режима рисования, 275 колебания, 285 постепенное исчезновение, 278 постепенный переход, 276 преобразования, 275 Я ядро свёртки, 298, 334 язык шейдеров Cg, 350 HLSL, 348 ISL, 346 OpenGL Shader, 346 RenderMan, 344
ДЛЯ ПРОФЕССИОНАЛОВ * Язык шейдеров OpenGL, высокоуровневый процедурный язык, является важнейшей новой разработкой в программировании графики. OpenGL — один из лучших кроссплатформенных APL для трехмерной графики — и язык шейдеров OpenGL позволяют разработчикам контролировать самые важные этапы процесса обработки графики. OpenGL и шейдеры, написанные на соответствующем языке, дают возможность приложениям достигать ошеломляющих графических эффектов, используя и графический ускоритель, и центральный процессор. В этой книге читатель найдет подробное введение в язык шейдеров OpenGL, описание новых функций и возможностей языка, подробные примеры и лежащие в их основе алгоритмы, иллюстрирующие разнообразные приемы работы с графикой: • процедурное текстурирование; • использование шума; • создание системы частиц; • штриховку; • аналитическое сглаживание; • обработку изображений. Уровень пользователя: Опытный/эксперт Серия: ppin профессионалов С^ППТЕР Заказ книг: 197198, Санкт-Петербург, а/я 619 тел.: (812) 703-73-74, postbook@piter.com 61093, Харьков-93, а/я 9130 тел.: (057) 712-27-05, piter@kharkov.piter.com www.piter.com — вся информация о книгах и веб-магазин ... . . . . .. - . • . .. . . ... . . :<- ... Код: Ц|на 177524 106.81 грн. ( 4)OpenGL Трехмерная графика и язык профамми 4» "Ж в 1® W