Использование Direct3D с высокоуровневыми библиотеками компонентов VCL/LCL

от автора

Данная публикация адресуется новичкам в области программирования компьютерной графики, желающим использовать графическую библиотеку Microsoft DirectX. Сразу оговорюсь:
— затронутая тема, наверняка, относится и к OpenGL, но я это не проверял опытным путём (созданием приложений под OpenGL), поэтому в заголовке упоминаю только Direct3D;
— приводимые здесь примеры кода относятся к языкам Delphi/FreePascal, но перечисленные «рецепты» по большому счету универсальны в пределах целевой ОС (Windows) — их можно применять к любому языку программирования и, с высокой вероятностью — к любой высокоуровневой библиотеке компонентов, помимо VCL (Delphi) и LCL (Lazarus);
— данная публикация не затрагивает тему создания каркасного приложения Direct3D и методов работы с графическими библиотеками DirectX и OpenGL; все эти вещи хорошо освещены в других источниках, и мне практически нечего к этому добавлять.

Итак, ближе к теме. При разработке приложений с трехмерной графикой для построения каркаса учебного (а тем более — рабочего) приложения обычно рекомендуется использовать чистый Win32 API… Но если очень хочется использовать в своих приложениях ещё и преимущества высокоуровневых библиотек компонентов, тогда добро пожаловать под кат.

Введение в проблему

При использовании чистого Win32 API цикл обработки поступающих оконных сообщений приходится прописывать «вручную», и обычно это выглядит примерно так:

repeat   if ( PeekMessage(msg, 0, 0, 0, PM_REMOVE) ) then   // если есть какое-то сообщение в очереди - получаем его и обрабатываем   begin     TranslateMessage(msg);     DispatchMessage(msg);   end   // иначе немедленно выполняем очередную отрисовку 3D-сцены   else     RenderScene();    // и вот так повторять до завершения работы приложения until ( msg.message = WM_QUIT ); 

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

Однак, высокоуровневые библиотеки компонентов, такие как VCL и LCL, не требуют от программиста реализации подобного цикла обработки сообщений. В недрах подобных библиотек уже имеется, в том или ином виде, реализация подобного цикла, поэтому возникает вопрос: как реализовать бесконечный цикл отрисовки, не нарушая принципов работы с этими библиотеками, и при этом обеспечивая корректную работу всего обвязочного кода этих библиотек? Именно этот вопрос я и намерен в дальнейшем осветить в меру своего собственного понимания.

Отступление по поводу маскировки исключений

Я был удивлен, когда не смог нормально запустить откомпилированный в Lazarus проект с использованием Direct3D, стабильно получая при запуске программы исключения, «кивающие» на вычисления с плавающей запятой. Потратив энное время на изучение проблемы так и не нашел в интернете прямых сведений об этой проблеме, но обратил внимание, что если компилировать проект в Delphi для 64-разрядной архитектуры, то при выполнении получаю весьма похожую по сути ошибку. Изучение содержимого окон Debug-режима в Delphi показало, что для FPU-расширения процессора регистр маскировки исключений MXCSR имеет различные значения во всех рассмотренных случаях. Даже после этого нагуглить ничего стоящего тоже не удалось, кроме упоминания вскользь о том, что модуль OpenGL из стандартной поставки Delphi содержит в секции «initialization» строку, которая устанавливает маскировку исключения на все возможные случаи.

Маскировка исключений FPU не относится к теме этой публикации, поэтому не буду сильно заострять на ней внимание. Приведу только самый простой пример: когда умножение очень больших чисел с плавающей запятой приводит к переполнению, то в этом случае происходит одно из двух: результат умножения становится равным INFINITY (или -INFINITY), если включена маскировка соответствующего исключения; либо процессор генерирует исключительную ситуацию «floating point overflow» (которая должна быть обработана программой в блоке «try except»), если маскировка соответствующего исключения отключена.

В итоге, попробовав установить в своих проектах маскировку исключений так, как это сделано в стандартном модуле OpenGL, я добился того, чтобы мои Direct3D-приложения работали как в Lazarus, так и в Delphi (включая 64-битную платформу) без проблем.

К сожалению, мне не удалось найти в MSDN или других источниках (может, плохо искал?) указаний на то, что нужно делать именно так и никак иначе, но тем не менее, рекомендую читателям в своих Direct3D-проектах прописывать следующий код:

uses Math; ... INITIALIZATION   Math.SetExceptionMask([exInvalidOp..exPrecision]); END. 

При этом должен заметить, что маскировка исключений будет иметь определенные побочные эффекты, которые обязательно следует учитывать. Например, становится «возможным» деление на ноль (с такой проблемой люди столкнулись, например, здесь), поэтому при выполнении вычислений с плавающей запятой нужно обязательно проверять промежуточные результаты.

Впрочем, если вы хотите получать исключительные ситуации при вычислениях с плавающей запятой, как привыкли ранее, то ничто не мешает в нужных местах использовать конструкцию примерно такого вида:

var   mask: TArithmeticExceptionMask; begin   mask := SetExceptionMask([]);  // отключаем всю (или как Вам нужно) маскировку исключений   try     // все необходимые вычисления с плавающей запятой   finally     SetExceptionMask(mask);  // возвращаем обратно предыдущие флаги маскировки исключений   end; end; 

На этом я закругляюсь с вопросом маскировки исключений.

Ещё одно отступление — а зачем нам это нужно?

Целей для создания Direct3D-приложений с использованием высокоуровневых библиотек компонентов может быть много. Например, отладка каких-то моментов, таких как шейдеры и эффекты. А может быть, вы создаёте собственный 3D-движок и нуждаетесь в редакторе файлов определений, на основе которых движок будет выполнять загрузку ресурсов и отрисовку сцен? В таких случаях хотелось бы иметь возможность видеть сразу результат, а при необходимости — редактировать что-то и «на лету» при помощи «вменяемого» пользовательского интерфейса со строками меню, модальными диалогами и т.д. и т.п.

К данной публикации я подготовил относительно примитивную программу, которая выводит на отрисовку в главном окне один-единственный треугольник (используется API DirectX 11), и при этом позволяет во время выполнения менять вершинный и пиксельный шейдеры, применяемые к этому треугольнику. Для этого понадобилось поместить на главную форму приложения необходимый набор компонентов — многострочное поле ввода и кнопку. Сразу предупреждаю — программа исключительно демонстрационная (для данной публикации), поэтому не следует от неё ожидать чего-то особенного. Ссылка на исходные коды приводится в конце текста данной публикации.

На этом отступления заканчиваются, и я перехожу к основной теме.

Способ тривиальный — событие TForm.OnPaint, функция Windows.InvalidateRect()

Программисты, знакомые не только с высокоуровневыми библиотеками компонентов, но и с чистым Win32 API, наверняка уже сложили в голове простую схему: «надо отрисовывать Direct3D-сцену в обработчике события формы (или другого компонента), именуемом OnPaint, и там же, по окончанию отрисовки, вызывать функцию InvalidateRect() из Win32 API, чтобы спровоцировать систему на отправку нового сообщения WM_PAINT, которое приведёт к повторному вызову обработчика OnPaint, и так мы далее пойдём по кругу в бесконечном цикле отрисовки, не забывая по ходу дела реагировать и на остальные оконные сообщения».

В общем-то, всё верно.

Вот примерный план кода для обработчика OnPaint:

procedure TFormMain.FormPaint(Sender: TObject); begin   // отрисовка Direct3D-сцены   // ...    // вывод результатов на экран с помощью интерфейса IDXGISwapChain   pSwapChain.Present ( 0, 0 );    // генерация следующего события WM_PAINT для бесконечного цикла отрисовки   InvalidateRect ( Self.Handle, nil, FALSE ); end; 

Но, как говорится, «гладко было на бумаге».

Давайте посмотрим, что получится (напоминаю, что в конце текста будет ссылка на исходные коды — скачав их, читатель может найти подкаталог «01 — OnPaint + InvalidateRect», скомпилировать и запустить программы и убедиться в не очень корректной работе примера).

Проблема 1: при компиляции приложения в Delphi и последующем запуске Direct3D-сцена отрисовывается как ожидается, но контролы пользовательского интерфейса нормально отображаться не хотят. Пока не изменишь расположение или размер окна программы, не хотят нормально отображаться ни надписи, ни содержимое многострочного поля редактирования, ни статус-бар, ни кнопка… Ну, положим, многострочное поле редактирования более-менее нормально перерисовывается, когда мы начинаем его прокручивать и редактировать содержимое, но в целом результат неудовлетворительный. А если программа в процессе работы открывает диалоговые окна (или хотя бы примитивный MessageBox), то они либо закрываться нормально не хотят, либо отображаться на экране (MessageBox можно закрыть и вслепую кнопкой «пробел», но диалоговое окно, унаследованное от TForm, закрыть у меня уже никак не получается). Для наглядной демонстрации этой проблемы я добавил в главное меню программы-примера пункты «Дополнительно -> О программе (MessageBox)» и «Дополнительно -> О программе (TForm)».

Проблема 2: при компиляции приложения в Lazarus и последующем запуске, впридачу к описанным выше проблемам (как будто их недостаточно), добавляется невозможность завершить работу программы — она не реагирует ни на стандартную кнопку закрытия в заголовке (“X”), ни на пункт меню «Выход»… Чтобы программа завершилась сама, без «помощи» диспетчера задач или комбинации «Ctrl+F2» в IDE, необходимо свернуть программу в таскбар (интересно, почему так?) после нажатия на кнопку закрытия окна.

Избавиться от последней проблемы на самом деле очень просто, нужно всего лишь дописать дополнительное условие перед вызовом функции InvalidateRect(), примерно так:

  if ( not ( Application.Terminated ) ) then     InvalidateRect(Self.Handle, nil, FALSE); 

Но вот решить первую проблему так легко, увы, не получится.

Вывод: описанный в этом подзаголовке способ нарушает нормальную работу очереди оконных сообщений Windows, мешая ряду оконных сообщений быть обработанными вовремя, и особенно это видно в случае использования высокоуровневой библиотеки компонентов (по крайней мере, это относится к VCL и LCL в их версиях на момент написания публикации).

Примечание: в MSDN можно найти описание функции GetMessage, где упоминается, что WM_PAINT имеет низкий приоритет по сравнению с другими оконными сообщениями (кроме WM_TIMER — его приоритет ещё ниже), и обрабатывается после всех остальных оконных сообщений.

Итого: факт, как говорится, налицо. Если и не во всех версия ОС, то как минимум в популярной ныне операционной системе Windows 7 (в которой я запускал все приложенные к публикации программы-примеры), ситуация с приоритетом в обработке сообщения WM_PAINT будет несколько посложнее, особенно если приложение использует высокоуровневую библиотеку компонентов, и поэтому полагаться на указанный в MSDN приоритет нельзя.

На этом можно было бы перейти к следующему способу организации бесконечного цикла отрисовки, но я сделаю ещё одно короткое отступление, на маленький абзац.

Библиотеки VCL и LCL предлагают программисту в классах, унаследованных от TWinControl, метод Invalidate(). В библиотеке VCL его вызов сводится к вызову вышеозначенной функции InvalidateRect() чистого Win32 API, но в общем случае поведение этого метода зависит от реализации в конкретной библиотеке. Так, в LCL этот метод приводит к вызову другой функции Win32 API, имеющей имя RedrawWindow() — эта функция даёт примерно тот же результат (будет выполнена повторная отрисовка окна), но кое-какие нюансы отличаются. Поэтому, чтобы не акцентировать внимание на нюансах, я сразу предложил обратиться к функции InvalidateRect() из Win32 API.

Способ более удачный — задействуем событие Application.OnIdle

Раз предыдущий способ неудачен, поскольку нарушает нормальную работу очереди оконных сообщений Windows, то логично попытаться сделать так, чтобы отрисовка окна приложения выполнялась строго после обработки всех остальных оконных приложений. На первый взгляд (по крайней мере, если детально не вглядываться во внутренности библиотек) эта задача может показаться невозможной без модификации цикла обработки оконных сообщений, скрытого в недрах библиотек VCL и LCL, но на самом деле это не так.

У объекта Application есть событие OnIdle, которое вызывается каждый раз, когда обнаруживается факт отсутствия новых оконных сообщений, и более того — обработчик этого события может сообщить, что он хочет обрабатывать это событие повторно (в цикле) до тех пор, пока не появятся наконец новые сообщения. После того, как будут обработаны новые сообщения, будет снова вызван обработчик события Application.OnIdle, поэтому он вполне подходит для организации бесконечного цикла отрисовки, хотя и со своими нюансами (для получения более подробной информации по этому событию, советую обращаться к справке в используемой Вами среде разработки).

Подобные сведения позволяют убрать из обработчика OnPaint вызов API-функции InvalidateRect() и перенести его в обработчик события Application.OnIdle.

В итоге получается код по примерно такой схеме:

procedure TFormMain.FormCreate(Sender: TObject); begin   Application.OnIdle := OnApplicationIdle;   // прочий код инициализации   // ... end;  procedure TFormMain.FormPaint(Sender: TObject); begin   // отрисовка Direct3D-сцены   // ...    // вывод результатов на экран с помощью интерфейса IDXGISwapChain   pSwapChain.Present ( 0, 0 );    // генерации следующего события WM_PAINT здесь больше нет — она перенесена в OnApplicationIdle() end;  procedure TFormMain.OnApplicationIdle(Sender: TObject; var Done: Boolean); begin   if ( Application.Terminated )  // выполняется завершение работы приложения      or ( { другие условия, при которых не нужно продолжать бесконечный цикл отрисовки } ) then   begin     // перерисовка не нужна, завершаем цикл обработки OnIdle()     Done := TRUE;     Exit;   end;    // будем обрабатывать OnIdle() повторно для обеспечения бесконечного цикла отрисовки   Done := FALSE;    // обеспечить сообщение WM_PAINT для последующей отрисовки   InvalidateRect ( Self.Handle, nil, FALSE ); end; 

В приложенных к публикации исходниках можно найти подкаталог «02 — OnPaint + OnApplicationIdle» и убедиться, что программа работает намного лучше, обновляя содержимое всех контролов своевременно и корректно отображая все модальные диалоговые окна.

К вышесказанному хочу добавить ещё вот что: если свернуть окно программы в таскбар и открыть диспетчер задач, то можно увидеть, что программа «кушает» как минимум одно ядро процессора полностью, и это несмотря на то, что рисовать программе по большому счету нечего и незачем. Если вы хотите, чтобы ваша программа уступала ресурсы CPU другим приложениям в подобных случаях, а также не вызывала глюков в открытых модальных окнах (я такое заметил только в Lazarus), то можно модифицировать обработчик события Application.OnIdle следующим способом:

procedure TFormMain.OnApplicationIdle(Sender: TObject; var Done: Boolean); begin   if ( Application.Terminated )  // выполняется завершение работы приложения      or ( Application.ModalLevel > 0 )  // открыты модальные окна      or ( Self.WindowState = wsMinimized )  // окно программы свернуто      or ( { другие условия, при которых не нужно продолжать бесконечный цикл отрисовки } ) then   begin     // перерисовка не нужна, завершаем цикл обработки OnIdle()     Done := TRUE;     Exit;   end;    // будем обрабатывать OnIdle() повторно   Done := FALSE;    // обеспечить сообщение WM_PAINT для последующей отрисовки   InvalidateRect ( Self.Handle, nil, FALSE ); end; 

Однако, даже в случае обработки события Application.OnIdle невозможно добиться идеального бесконечного цикла отрисовки. Например, когда открыто главное меню окна, то в процессе навигации по нему событие Application.OnIdle не будет вызываться и, соответственно, анимация Direct3D-сцены «остановится». То же самое произойдёт и в случае открытия программой модального диалога или окна MessageBox.

Конечно, с такими проблемками тоже можно побороться. Например, положить на форму объект TTimer, настроить его на срабатывание каждые 50 миллисекунд, и вызывать в его обработчике события всё ту же функцию InvalidateRect() — тогда можно будет надеяться, что и при навигации по главному меню, и при работе с модальными диалогами цикл отрисовки будет продолжать свою работу, но в эти моменты уже не будет возможности адекватно оценивать FPS и производительность отрисовки 3D-сцены в целом. Впрочем, это вряд ли будет интересовать пользователя в те моменты, когда он полезет в главное меню и станет открывает дополнительные диалоговые окна, поэтому я и не акцентирую внимание на непрерывности бесконечного цикла отрисовки — главное чтобы он был и работал в те моменты, когда внимание пользователя сосредоточено на окне с Direct3D-сценой, а остальное уже не так важно и отдается на откуп читателю — желающие могут реализовать момент с TTimer самостоятельно и убедиться, что это работает вполне ожидаемым образом.

Отрисовка 3D-сцены в отдельный контрол

Когда часть окна программы отведена под отрисовку Direct3D-сцены, а другая — под контролы пользовательского интерфейса, то будет не совсем правильно выделять видеопамять под всё окно программы целиком.

Логичнее будет создать панельку (или иной контрол), которая при необходимости будет менять свои размеры вместе с окном программы (удобно использовать свойство Align=alClient) и избавит от «шаманства» с матрицами преобразований при отрисовке Direct3D-сцены.

К сожалению, мне не удалось найти стандартных малофункциональных контролов типа TPanel, которые бы имели «публичный» обработчик события OnPaint, поэтому пришлось реализовать компонент-наследник от TCustomControl (можно и от других классов) и перегрузить его метод Paint().

Подобная реализация предельно проста, и приложенные к публикации исходные коды содержат такой пример в подкаталоге «03 — TCustomControl descendant».

Исходные коды

Программы-примеры к данной публикации доступны по следующей ссылке:
github.com/yizraor/PubDX_VCL_LCL

спойлер

(гитхабом раньше не пользовался, надеюсь, что получилось правильно)

ссылка на оригинал статьи http://habrahabr.ru/post/256871/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *