Visual Studio, а также библиотека GLUT (OpenGL Utility Toolkit, http://user.xmission.com/nate/glut.html ), включающая как упрощённый интерфейс к функциям OpenGL, так и функции работы с окнами в системе и функции взаимодействия с пользователем через клавиатуру и мышь. На других возможных вариантах (например, Qt, или wxWidgets) останавливаться здесь не будем, так как они выходят за рамки данного курса. Использование GLUT Рассмотрим простенький пример работы с GLUT — файл ExampleGLUT.cpp . Для того, чтобы откомпилировать его, понадобится заголовочный файл glut.h , для сборки программы — статические библиотеки glut32.lib (можно просто включить её в проект) и OpenGL32.lib (скорее всего, имеется — в составе Windows SDK; у меня, например, эта библиотека находится в каталоге C:\Program Files\Microsoft SDKs\Windows\v6.1\Lib ). Для запуска программы также надо убедиться, что будут обнаружены glut32.dll (проще расположить её в одном каталоге с исполняемым файлом) и opengl32.dll (почти наверняка есть в C:\WINDOWS\system32 ). 9 39 #include "glut.h" void reshape(int,int) {} void display() {} void keyboard(unsigned char,int,int) {} void mouse(int,int,int,int) {} void main(int argc, char *argv[]) { // Инициализация GLUT glutInit(&argc,argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA); glutInitWindowSize(512,512); // Создание окна OpenGL glutCreateWindow("Simple Window"); // init ( ); // какая - нибудь другая инициализация // Регистрация функций " обратного вызова " (callback) glutReshapeFunc(reshape); glutDisplayFunc(display); glutKeyboardFunc(keyboard); glutMouseFunc(mouse); // Цикл опроса событий glutMainLoop(); } Листинг программы даёт простой пример применения GLUT. После базовой инициализации создаётся окно и регистрируются обработчики событий: отрисовки окна ( display() ) и изменения его размеров ( reshape() ), а также событий клавиатуры ( keyboard() ) и мыши ( mouse() ). В данном примере обработчики пусты, поэтому программа ничего полезного сделать не может, даже окно не перерисовывается — хотя и воспроизводится. Дополняя обработчики отрисовки окна и изменения его размеров кодом отображения, а остальные обработчики — кодом изменения параметров отображаемого, мы можем получить несложную вычислительную программу с графическим интерфейсом. Более сложные программы, использующие OpenGL, можно найти в NVIDIA CUDA SDK. Рассмотрим, к примеру, программу nbody, иллюстрирующую поведение большого числа гравитирующих тел. Открываем файл nbody_vs2008.sln (только не двойным нажатием, поскольку файл по умолчанию "норовит" открыться в VS2010, а вовсе не в VS2008!), выбирая программу VS2008 для его открытия в меню по нажатию правой кнопки мыши, и пробуем создать исполняемый файл. Легко заметить, что эта демонстрационная программа тоже использует GLUT, только здесь обработчики отрисовки, клавиатуры и мыши — вполне реальные, поэтому их можно взять в качестве примера. В остальном это — добротная CUDA-программа, к которой добавлен графический интерфейс, что позволяет качественно исследовать поведение многих гравитирующих тел. Описание теоретической составляющей этой программы можно посмотреть в документе nbody_gems3_ch31.pdf, также входящем в состав CUDA SDK. Вообще надо сказать, что OpenGL — большая и довольно сложная библиотека, "объять" которую за короткое время трудно, поэтому здесь такая попытка даже не делается. Если есть необходимость использовать OpenGL — имеет смысл обратиться к руководствам, особенно таким, 40 где всё изложено подробно, с большим количеством иллюстраций и примеров; вот лишь несколько ссылок: An intro to modern OpenGL. Table of Contents http://duriansoftware.com/joe/An-intro-to-modern-OpenGL.-Table-of-Contents.html Anton's OpenGL 4 Tutorials http://www.antongerdelan.net/opengl/ OpenGL Step by Step — OpenGL Development on Linux http://ogldev.atspace.co.uk/ GLSL (OpenGL Shading Language) Если внимательно просмотреть исходные тексты упомянутой выше демонстрации nbody, то можно заметить (см. файл render_particles.cpp), что там присутствуют вроде бы и символьные строки, но в то же время и какие-то фрагменты программного кода... Эти странные данные в упомянутом файле имеют названия vertexShader и pixelShader. И это действительно программы, причём на специальном ( C-подобном) языке GLSL! Программы на этом языке не используются сами по себе, они являются частью более сложной OpenGL-программы. Или можно сказать так: с некоторых пор каждая OpenGL-программа "внутри" (возможно — невидимым для нас образом) использует мини-программы, написанные на языке GLSL. Далее мы будем называть эти программы по-русски шейдерами ( shaders), несмотря на то, что это слово ещё не получило широкого распространения. Как правило, эти программы (шейдеры) исполняются на GPU. Имеется два основных вида шейдеров: • vertex shader — исполняется для каждой вершины в сцене отрисовки • fragment shader — исполняется для каждого пиксела, видимого на экране Код каждого шейдера что-то принимает на вход и что-то генерирует на выходе. В первом случае ( vertex shader) принимаются позиции каждой вершины, координаты текстуры, которую необходимо отобразить в каждой вершине, и преобразование для них (сдвиг, масштаб, поворот); на выходе получаются экранные координаты вершин, координаты текстуры вершины. Далее fragment shader будет использовать эти данные. attribute vec4 a_position; attribute vec2 a_texCoord; uniform mat4 u_MVPMatrix; varying mediump vec2 v_texCoord; void main() { gl_Position = u_MVPMatrix * a_position; v_texCoord = a_texCoord; } 41 Шейдеру вершин надо заполнить значение встроенной переменной gl_Position положением преобразованной вершины, т.е. входная позиция умножается на ModelViewProjection-матрицу. Ключевое слово attribute говорит о том, что это входная переменная, сопоставляемая каждой вершине; vec2 и vec4 говорят о том, данные являются векторами вещественных чисел ( float). Если нужно передать шейдеру из кода некоторую внешнюю переменную, она объявляется uniform . Тип mat4 говорит о том, что это матрица размера 4x4 из вещественных значений. Переменные, которые должны из шейдера вершин ( vertex shader) попасть в шейдер фрагментов ( fragment shader), помечены ключевым словом varying . Самое интересное, что такие переменные интерполируются, т.е., для каждого пиксела в промежутке между вершинами устанавливается значение, получаемое в результате линейной интерполяции между значениями в вершинах. mediump — это спецификатор точности (наряду с highp и lowp ), он определяет качество вычислений и размер при хранении величины. Для большей точности будут использованы более точные типы данных, но скорость вычислений может быть меньше. Кроме того, каждый шейдер должен иметь (ничего не возвращающую) функцию main() без каких-либо параметров. Цель шейдера фрагментов, напомним, — определить цвет каждого пиксела. precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); } Всё, что шейдер вершин передаёт на выход, должно быть определено здесь как вход ( v_texCoord ). Шейдер фрагментов тоже может иметь uniform -переменные, здесь это u_texture . Встроенная переменная gl_FragColor заполняется результирующим цветом пиксела: берётся цвет из u_texture с использованием координаты v_texCoord Высокоуровневые shading-языки появились в 2004 году: GLSL (в рамках OpenGL), Cg от nVidia, HLSL от Microsoft. В 2006 был введён так называемый geometry shader, а все три шейдера были привязаны к одному и тому же набору инструкций. Появилась так называемая Unified Shader Architecture. С этого момента GPU становится не только параллельным сопроцессором для CPU в графических программах, но и параллельным процессором общего назначения. Это привело к появлению не- графических языков ( CUDA, OpenCL), позволяющих эффективно использовать параллельность и вычислительную мощь современных GPU. 42 Собственно, об этом и идёт речь в рамках этого краткого спецкурса: практически все устройства с процессорами общего назначения ( CPU) сейчас имеют также и графические процессоры ( GPU), зачастую заметно превосходящие по вычислительным возможностям основные процессоры. Поэтому важно знать, какие средства имеются в нашем распоряжении, и каким образом мы могли бы использовать эти новые возможности. Если необходимо узнать, с какой версией OpenGL и GLSL мы имеем дело, можно использовать простую (но полезную) программу GetVerGLSL.cpp; для её построения в свойства проекта следует добавить путь поиска заголовочных файлов в NVIDIA CUDA SDK ($(NVSDKCOMPUTE_ROOT)\C\common\inc). Для запуска программе понадобятся динамические библиотеки OpenGL32.dll и glut32.dllOpenGL в браузере — WebGL Говоря об OpenGL, нельзя не сказать, что повсеместное распространение этой библиотеки и поддержка её подавляющим числом графических карт закономерно привели к появлению новых возможностей в интернет-браузерах — как стационарных, так и мобильных устройств. WebGL — это программный интерфейс для доступа к графическому оборудованию (т.е. GPU) в рамках браузера, причём без установки каких-либо дополнительных расширений. Основанный на OpenGL ES 2.0 и HTML5, он позволяет программисту определить объекты и операции для создания высококачественных графических изображений, в частности цветных изображений трёхмерных объектов. Поддерживает браузер WebGL или нет, можно узнать с помощью специальных страниц в Сети (например, http://get.webgl.org/ — это самая авторитетная, от создателей). Можно также (используя JavaScript) создать простой HTML-документ для этой цели (см. самодостаточный файл Test_WebGL.html). Иногда браузер может поддерживать WebGL, но эта поддержка — не включена. Это связано с тем, что считается, будто реализации WebGL могут быть подвержены уязвимостям для вредоносного кода (по этой причине Internet Explorer от Microsoft пока не поддерживает WebGL). Однако на современном телефоне такая поддержка часто есть и включена по умолчанию. Примеры использования WebGL для отображения графики и анимации в браузере есть в файлах WebGL-DrawCheckerboard.html, WebGL-AnimatedStar.htmlЕсли заглянуть в исходный текст этих HTML-документов, мы легко обнаружим там "старых знакомых": шейдеры вершин и шейдеры фрагментов, а также JavaScript-код, сильно напоминающий программу на OpenGL обилием функций с уже привычными именами... В заключение — интересная ссылка на демонстрации WebGL: https://www.khronos.org/webgl/wiki/Demo_Repository 43 Вспомогательные средства при работе с CUDA и смежными библиотеками Поскольку типы данных, употребляемые в современных программах, имеют тенденцию к усложнению, ясно, насколько важно в процессе отладки ёмко и информативно отображать текущие значения "сложноогранизованных" величин. К счастью, используемая нами среда разработки программ Visual Studio позволяет "подстроить" отображение таких величин желательным образом. Визуализаторы — это компоненты пользовательского интерфейса отладчика среды Visual Studio. Они создают диалоги или другие интерфейсные единицы — чтобы отобразить переменную или объект в присущем этому типу данных стиле. Visual Studio использует несколько стандартных визуализаторов: для текста, для HTML, для XML и другие, а также позволяет создавать дополнительные визуализаторы. Самое приятное для "обычного" пользователя — это то, что он может повлиять на то, как показываются величины, используемые в программе, при отладке: в тултипах и Watch-окошках. Visual Studio 2005, 2008, 2010 Изменение представления различных величин в отладчике возможно путём модификации файла autoexp.dat, причём такая возможность имеется, начиная с версии Visual Studio 2005 и далее. Этот файл находится (при установке по умолчанию) в таких каталогах: VS 2008 C:\Program Files\Microsoft Visual Studio 9.0\Common7\Packages\DebuggerVS 2010 C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Packages\Debuggerили в более общем виде — %VSINSTALLDIR%\Common7\Packages\Debugger, хотя сама переменная окружения VSINSTALLDIR (или аналогичная) может и отсутствовать. Форматирующие правила располагаются в файле autoexp.dat в секции [Visualizer] , куда можно добавить также описание своих собственных или других необходимых для работы типов. Всего в этом файле имеются три секции: [AutoExpand] , [Visualizer] , [hresult] , однако нас пока более всего интересует именно секция [Visualizer] Размещать свои визуализаторы надо, помня о способе поиска визуализатора отладчиком: он обнаруживает первый подходящий по типу визуализатор из файла. Это значит, что более конкретные типы следует располагать перед более общими. Кроме того, расположение визуализаторов "впереди" позволяет заменять уже существующие правила — собственными, причём без какого-либо удаления существующих. Вроде бы можно также расположить свой собственный файл, начинающийся со строки[Visualizer] , в произвольном месте и указать его местоположение в переменной окружения _vcee_autoexp , однако это не проверялось. Описание вида отображения величин какого-либо типа (или нескольких сходных типов) начинается с имени этого типа (или нескольких типов, разделяемых вертикальной чертой). Далее следуют поименованные блоки с выражениями в круглых скобках. Эти выражения определяют, как показываются величины упомянутых типов в разных ситуациях. 10 44 Условная схема описания выглядит так (здесь квадратные скобки обозначают необязательную часть заголовка, а многоточие – возможность повторения): ИмяТипа[|ИмяТипа...] { preview ( ВыражениеСтрокиПредварительногоПросмотра ) stringview ( ВыражениеВизуализатораТекста ) children ( ВыражениеРазворачиваемогоСодержимого ) } Никакие выражения не являются обязательными, однако блоки preview и children часто присутствуют вместе. Можно описывать также шаблонные типы, используя синтаксис полного совпадения ИмяТипа<*> или частичного совпадения ИмяТипа<КонкретныйТип, *> и т.п. Параметры шаблона далее можно использовать под именами $T1, $T2 и т.д., $e — значение визуализируемой переменной. ВыражениеСтрокиПредварительногоПросмотра(блок preview ) состоит из одной строки, выводимой в окошках Watch, QuickWatch, Command Window. Квадратные скобки в ней используются для того, чтобы вместе с выражением, значение которого будет выведено, указать и форматирующий спецификатор. Поскольку одного выражения обычно недостаточно, лучше использовать обрамление #( и ) для списка (разделяемого запятыми) строк и выражений. Блок children тоже содержит либо одно выражение, либо список выражений через запятую в обрамлении #( и )Некоторые синтаксические конструкции autoexp.dat Наиболее часто используемые синтаксические конструкции в таких описаниях – это условный оператор (с одним или несколькими условиями), переключатель (в котором отдельные случаи содержат константные выражения), а также оператор конкатенации. Условный оператор #if ( ... ) ( . . . ) #else ( . . . ) Условный оператор с несколькими условиями #if ( ... ) ( . . . ) #elif ( ... ) ( . . . ) #else ( . . . ) Переключатель #switch( ... ) #case K1 ( . . . ) #case K2 ( . . . ) . . . #default ( . . . ) Оператор конкатенации #(..., ..., ..., . . . ...) 45 Для того, чтобы освоиться с синтаксисом оформления визуализаторов, полезно посмотреть уже имеющиеся в этом файле для каких-нибудь общеизвестных типов данных. Вот, например, фрагмент, касающийся отображения комплексных величин: ;------------------------------------------------------------------------------ ; std::complex ;------------------------------------------------------------------------------ std::complex<*>{ children ( #( real: $e._Val[0], imaginary: $e._Val[1] ) ) preview ( #if($e._Val[1] != 0) ( #if ($e._Val[0] != 0) ( ; Real and Imaginary components #if ($e._Val[1] >= 0) ( #($e._Val[0],"+i*", $e._Val[1])) #else ( #($e._Val[0],"-i*", -$e._Val[1])) ) #else ( ; Purely imaginary #if ($e._Val[1] >= 0.0) ( #("i*", $e._Val[1])) #else ( #("-i*", -$e._Val[1])) ) ) #else ( ; Purely real $e._Val[0] ) ) } Кроме того, в тексте визуализаторов могут встречаться более сложные конструкции: |