OpenGL и Direct3D - две основные на сегодняшний день аппаратно-ускоряемые библиотеки для создания компьютерной трехмерной графики. Перед каждым начинающим 3D-программистом встает вопрос: какой из двух вариантов API выбрать? Вопрос этот совсем не прост, собственного опыта для принятия осознанного решения, как правило, не хватает. И хотя на форумах такие темы обсуждаются регулярно, почерпнуть из них информацию порой непросто, поскольку «советы бывалых» обычно основываются на личных предпочтениях.
Итак, что же выбрать начинающему «тридешнику»? Ответ зависит от трех факторов: от поставленной задачи, от умений программиста и от того, чем он намерен заниматься в дальнейшем. С первым фактором все более или менее просто: начинающий 3D-программист может быть или совсем зеленым новичком, или обладать каким-то опытом в других областях. Второй фактор более расплывчат и во многом зависит от первого, ведь только-только ступая на стезю программирования, очень трудно четко поставить задачу, обычно хочется написать что-то такое, чтоб было красиво и все двигалось и блестело; из этого мы и будем исходить, советуя ту или иную библиотеку. В случае же с опытным программистом дела обстоят иначе; тут может потребоваться или визуализация результатов какой-то вычислительной программы, или внезапно появился выгодный заказ, да вот беда, с необходимостью 3D-графики. Ну а третий фактор очень грубо можно разделить на два направления: первое - разработка игр и все что с этим связано; второе - написание приложений для работы с 3D-графикой и визуализация научных данных (скромно назовем все это научной графикой). Третий фактор тоже тесно связан с первым: практически все «молодые и перспективные» хотят писать игры, ну так уж повелось; и в общем-то они правы - игровая отрасль не только интересная и быстро развивающаяся, но и гораздо лучше представлена на территории нашей страны. В самом деле, все слышали об отечественных играх и компаниях их разрабатывающих, но мало что известно об успехах россиян на поприще научной графики.
Теперь подробнее остановимся на истории каждой из технологий. Начнем с Direct3D. В те давние времена, когда Windows только входила в нашу жизнь, абсолютное большинство игр писалось еще под DOS, а Windows 3.1 не предоставляла эффективных средств доступа к видео- и звуковым устройством. Такое положение дел не устраивало Microsoft, и корпорация вознамерилась сделать 3D API, который стал бы удобным инструментом в руках разработчиков игр. То ли из-за нехватки времени и ресурсов, то ли по другим причинам Microsoft решила не изобретать велосипед, а использовать наработки компании RenderMorphics. Это был совсем маленький проект, на его базе была создана небольшая библиотека Game SDK, которая и стала впоследствии именоваться DirectX 1.0. И как это ни прискорбно для Microsoft, первому DirectX не суждено было стать распространенным средством разработки игр. Причин тому было множество, и если с плохой структурой и сложностью использования еще можно было мириться, то многочисленные ошибки и неповоротливость написанных игр ставили крест на широком распространении этой технологии. Но не в правилах Microsoft сдаваться, и она немедленно принялась совершенствовать свое неудачное детище. Первой по-настоящему принятой программистским сообществом версией DirectX стала 7.0. Этот продукт был лишен большинства из «детских болезней» своих предшественников: научился почти полностью использовать потенциал видеокарт, его интерфейс стал гораздо приятнее, и, главное, DirectX 7.0 был стабилен (сейчас существует DirectX 9.0 - мощное и удобное средство разработки). Вы, наверное, обратили внимание, что я периодически называю вроде бы одно и то же по-разному - то DirectX, то Direct3D. На самом деле все просто: Direct3D - это часть DirectX. До седьмой версии в DirectX было два графических инструмента: Direct3D и DirectDraw, для трех- и двухмерной графики соответственно. DirectX содержит и другие полезные компоненты - например, DirectSound и DirectPlay. Первый используется для воспроизведения звуков, а второй облегчает жизнь разработчику игр: он берет на себя работу с вводом и даже включает сетевые интерфейсы.
История OpenGL ведется с 1992 года. Компания Silicon Graphics создала его как открытый стандарт. Отсюда и первая часть названия, а GL означает Graphic Library (графическая библиотека). На данный момент OpenGL находится под контролем комитета Architectural Review Board (ARB), куда входят представители наиболее влиятельных в 3D-секторе корпораций - nVidia, ATI, SGI, Apple, Intel, id Software и, что особенно интересно, Microsoft. ARB является по сути некой бюрократической машиной, чья задача - вводить в стандарт OpenGL новые возможности. Для этого был создан механизм расширений. Как и любая бюрократическая машина, ARB работает медленно и неповоротливо, что стало главной проблемой OpenGL, который просто не может угнаться за стремительным развитием графических технологий. Достаточно сказать, что между выходом версий 1.0 и 1.4 прошло больше десяти лет! Сейчас этот стандарт морально устарел. Хотя все новые возможности современных видеоускорителей доступны через расширения ARB, пользоваться ими, прямо скажем, неудобно.
OpenGL, создававшийся для профессионального сектора, прочно в нем закрепился во многом благодаря своей переносимости, а вот нишу PC-игр практически полностью уступил своему конкуренту. Изменится ли эта ситуация? Чтобы существенно потеснить DirectX в game-секторе, ARB необходимо вывести OpenGL на современный уровень, и, надо сказать, комитет не сидит сложа руки: грядет так долго ожидаемая вторая версия GL, ее создатели хотят снова создать стандарт на десятилетие вперед. А что же Microsoft? Попытается ли компания захватить и профессиональный сектор? Это возможно только с выходом DirectX под другие операционные системы, что вряд ли случится.
Перейдем к сравнению технологий. DirectX построен по объектно-ориентированной схеме, а OpenGL по процедурной. Что лучше? Программа на GL одинаково «хорошо» выглядит и на C++, и на чистом C, чего не скажешь о DX (хотя это надуманное преимущество, сейчас мало кто пишет на C, особенно под Windows). А вот простота архитектуры GL - неоспоримый плюс: GL работает исключительно с примитивами (треугольники, отрезки и точки) и управляется набором булевых переменных, которые позволяют включать или отключать некоторые функции - например, накладывать текстуру или нет, использовать ли освещение и т. д. Код для отображения «вашего первого треугольника» занимает примерно пятьдесят строк. В DX эта цифра куда больше. С одним-то треугольником у GL все хорошо, но как только захочется использовать что-нибудь из современных 3D-эффектов - появляются расширения GL, и еще недавно простой и понятный код тонет в непонятных и ничего не значащих для человека, не посвященного в тайны 3D-графики, строках.
Как ни странно, ведутся споры о производительности. Проверить это проще простого, но зачем? - ведь результат легко предсказуем. Он будет одинаков для обеих библиотек, поскольку сейчас практически все функции реализуются напрямую через аппаратные ускорители. Расхождение результатов может быть только из-за погрешности измерений; сюда я отношу и оптимизацию тестирующего кода, и специфику конкретных драйверов, но в целом для выполнения кода на GL и DX видеокарта должна выполнять одни и те же действия, а значит, и дискуссии о быстродействии беспочвенны. Конечно, можно копать глубже и спорить о качестве шейдерных компиляторов HLSL[High-level Shader Language - С-подобный язык для написания шейдеров в DirectX 9.0] и GLSL[OpenGL Shading Language - аналог HLSL для OpenGL], но мы не будем лезть в такие дебри, тем более что оба решения далеки от совершенства.
Что мы имеем: львиная доля создателей игр выбирает DirectX за удобную поддержку современных возможностей, остальные предпочитают OpenGL - за переносимость и простоту написания программ. Ситуация в общем-то нормальная, если вспомнить, для чего создавалась каждая из технологий. Но если мы обратим внимание, что Doom III, самая нашумевшая игра последнего времени, написана на OpenGL, то поневоле закрадывается подозрение, что не все так просто. Может быть, в id Software знают какой-то секрет, делающий применение OpenGL предпочтительнее? Нет, просто команда разработчиков уже привыкла работать на нем. Это еще раз красноречиво говорит о том, что GL мало чем уступает DirectX.
Я много говорил о возможностях современного железа, но ни разу не упомянул о том, что же это за возможности и что для их использования предоставляют OpenGL и DirectX. Прежде всего, нынешние видеокарты - это программируемые устройства. Теперь некоторый код можно выполнять прямо на графическом процессоре (GPU). Эти программы называют шейдерами. Они позволяют создавать сложнейшие эффекты в реальном времени. Изначально эти программы можно было писать только на «фирменных» языках производителей видеокарт - ATI и nVidia. Оба языка очень похожи на ассемблер, но, увы, несовместимы. А кому захочется писать два разных кода для реализации одного и того же эффекта? И вот в DirectX 8.0 появляется универсальный язык программирования шейдеров. Он тоже похож на ассемблер, но позволяет обойтись одним шейдером для обоих типов видеокарт. И снова закавыка: много ли людей любят программировать на языках низкого уровня? И вот Microsoft представляет в своем DX 9.0 уже C-подобный язык HLSL. ARB в ответ выкатил аналогичную технологию GLSL. Как уже говорилось, обе эти технологии далеки от совершенства, прежде всего из-за слабой оптимизации компиляторов. Например, создатели игры Chronicles of Riddick (кстати, использовавшие OpenGL) для увеличения производительности написали все шейдеры в двух вариантах, на родных ассемблероподобных языках от ATI и nVidia. Это выглядит странным, так как обычные компиляторы показывают совсем неплохие результаты.
Итак, у нас две библиотеки с практически одинаковыми возможностями и быстродействием. Отличия лишь в сложности написания кода. И даже здесь явного лидерства нет. Все зависит от уровня используемых в программе эффектов. Если нам достаточно базовой функциональности OpenGL (то есть мы не используем расширений), то разумнее использовать именно GL, а если дело дошло до продвинутых эффектов, тут предпочтительнее Direct3D. Но с появлением второго GL ситуация может в корне измениться, так как переход на новую версию избавит GL от большинства недостатков. Но Microsoft тоже не будет стоять на месте, и вполне возможно, что к моменту своего выхода OGL 2.0 уже устареет. Да и в дальнейшем для успешной конкуренции с DX ARB понадобится существенно переработать механизм внесения изменений в библиотеку, чтобы сделать OpenGL более динамично развивающимся.
Так что же выбрать начинающему 3D-программисту? Молодому поколению я, скрепя сердце, посоветую DirectX. Прежде всего потому, что знание DX поможет стать создателем игр. Все российские компании, разрабатывающие игры, используют эту технологию. А вот совет программистам с опытом не так однозначен. Чтобы ваша программа работала не только на Windows, но и на других операционных системах, следует выбрать GL. Далее следует решить, какие эффекты вам необходимы: можете ли вы обойтись стандартной функциональностью GL, или вам непременно нужно использовать более «продвинутые» эффекты. И, наконец, на сцену выходят личные предпочтения: одним придется по вкусу объектная структура DirectX, другим же она покажется слишком громоздкой. И, конечно, не стоит забывать старый анекдот: «Какой дистрибутив Linux ставить? Тот, который стоит у ближайшего гуру».
Простейшая программа на OpenGL и на Direct3D
Сейчас мы подробно разберем написание простейшей программы на этих двух библиотеках. Наши программы будут делать одно и то же, а именно рисовать один-единственный треугольник c углами разных цветов. Мы сознательно опустим всю подготовительную работу и сосредоточимся на рисовании графического примитива.
Для начала очистим фон. В OpenGL для этого потребуется написать следующую строку:
glClear(GL_COLOR_BUFFER_BIT);
где glClear() - функция, предназначенная для очистки буферов, а константа GL_COLOR_BUFFER_BIT указывает, что именно нужно очистить.
Для аналогичной операции в D3D нужно написать
d3d_Device-›Clear (0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB (0, 0, 0), 0, 0);
Clear обладает большей функциональностью и, как следствие, более сложным синтаксисом. Поскольку нам эта функциональность не нужна - среди параметров так много нулей. Первые два отвечают за возможность очищения не всего кадра, а некоторого количества прямоугольников. Последняя пара отвечает за очистку только тех точек, удаление которых от камеры (координата Z) находится в определенных пределах. Константа D3DCLEAR_TARGET указывает на то, что мы будем очищать буфер неким цветом, указанным в четвертом параметре (черный цвет).
Вот рабочее пространство и готово, можно приступать к рисованию. В OGL все примитивы для обрисовки должны находиться между вызовами glBegin() и glEnd(), некоторым их аналогом в D3D являются d3d_Device-›BeginScene () и d3d_Device-›EndScene (). В случае с OGL уже можно привести фрагмент кода, выполняющий нашу задачу:
glBegin(GL_TRIANGLES);
glColor3d(1,0,0);
glVertex3d(1,2,3);
glColor3d(0,1,0);
glVertex3d(4,5,6);
glColor3d(0,0,1);
glVertex3d(7,8,9);
glVertex3d - это вершины нашего треугольника, Название функции интуитивно понятно, но 3d, как ни странно, не означает «трехмерный»; точнее, 3 - как раз означает, а вот d - это тип аргументов (double). GL_TRIANGLES указывает, что все последующие вершины будут последовательно образовывать треугольники (если бы у нас было не три, а шесть вершин - получилось бы два треугольника). glColor3d принимает на вход цветовые компоненты RGB, и все вершины, идущие после него и до следующего вызова glColor, будут иметь соответствующий цвет.
В D3D перед рисованием необходимо подготовить специальную структуру данных:
struct CUSTOMVERTEX { FLOAT x, y, z, rhw; DWORD color;};
CUSTOMVERTEX g_Vertices[] =
{1, 2, 3, 1, 0xffff0000},
{4, 5, 6, 1, 0xff00ff00},
{7, 8, 9, 1, 0xff0000ff},
LPDIRECT3DVERTEXBUFFER8 p_VertexBuffer = NULL;
Эта структура содержит и координаты вершин, и их цвета. p_VertexBuffer - указатель на нашу структуру, для хранения вершин.
Теперь напишем следующее:
d3d_Device-›CreateVertexBuffer (3*sizeof(CUSTOMVERTEX),
0, D3DFVF_XYZRHW | D3DFVF_DIFFUSE, D3DPOOL_DEFAULT, amp;p_VertexBuffer);
VOID* pVertices;
p_VertexBuffer-›Lock (0, sizeof(g_Vertices), (BYTE**) amp;pVertices, 0);
memcpy (pVertices, g_Vertices, sizeof(g_Vertices));
p_VertexBuffer-›Unlock();
CreateVertexBuffer - выделяет место на устройстве под три вершины (3*sizeof(CUSTOMVERTEX),) и присваивает его нашему указателю (p_VertexBuffer). Далее «запираем» (Lock()) буфер вершин, чтобы случайно его не повредить. Копируем весь наш буфер (memcpy()) в буфер видеокарты. И снова разрешаем доступ (Unlock();).
И наконец, мы готовы к рисованию:
d3d_Device-›BeginScene ();
d3d_Device-›SetVertexShader (D3DFVF_CUSTOMVERTEX);
d3d_Device-›SetStreamSource (0, p_VertexBuffer, sizeof(CUSTOMVERTEX));
d3d_Device-›DrawPrimitive (D3DFVF_XYZRHW | D3DFVF_DIFFUSE, 0, 1);
d3d_Device-›EndScene ();
SetVertexShader() определяет тип вершин. SetStreamSource() задает поток для дальнейшей работы. Здесь 0 - номер потока, второй параметр - данные, которые присваиваются к этому потоку, и третий параметр - размер, занимаемый в памяти одной вершиной. DrawPrimitive - рисует примитивы на экране, принимает на вход (в порядке следования) тип примитивов для отрисовки, индекс первой вершины, с которой нужно начать рисование, и количество примитивов в последовательности.