Наконец-то появилось время реализовать ранее написанный obj-ридер в OpenGL. В процессе выяснилось, что с момента моего последнего общения с OpenGL прошло слишком много времени. Совершенно не устраивал результат из-за гадских «ступенек». Мне нужно сглаживание…
Проблема
Открыть OBJ-модель и отобразить в OpenGL проблем не вызвало. Проблема оказалась в том, что моих знаний не хватало, чтобы избавится от этого убогого «ступенчатого» вида

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

Постановка задачи
Необходимо научиться отображать 3D-объект со сглаживанием без использования движков, сторонних библиотек и последних версий Delphi (Skia, знаете, тот ещё монстр). Также, хочется рисовать не в окне, специально созданном для контекста OpenGL, а на какой-нибудь скромной панельке внутри формы.
Чтение информации о расширениях ARB и вопросы к ИИ выработали стойкое убеждение, что без шейдеров не обойтись. Что все старые функции OpenGL канули в лету и, даже если всё таки включить ARB, они работать не будут. Мне это кажется крайне нелогичным, поэтому хочу опровергнуть.
Что плохого в движках? Ничего. Просто они большие, и для ряда задач не нужны. И потом, они меняются, и порой весьма кардинально. И в один прекрасный момент можно просто не скомпилировать проект. На текущий момент мне известен только один выживший, это GLScene. Но он вызывает у меня крайне противоречивые чувства и плохие воспоминания.
Не хочется таскать с проектом файлы библиотек. Проект желательно иметь в одном каталоге, без привязок, настроек путей, наличия определённых компонент. Чтобы открыл, скомпилировал и улыбнулся.
И, наконец, почему не в последних версиях Delphi. Потому что, во-первых, они есть далеко не у каждого. Во-вторых, лично у меня последняя Delphi (сейчас это XE 12) — основной рабочий инструмент, где настроены пути, переписаны модули, где всё не так, как в кристально чистой, только что установленной версии. Поэтому, для экспериментов и статей у меня есть XE 7, которая никак не пересекается с 12-ой.
Плюс ко всему, мне кажется, что понимая, как всё это работает, легче использовать движки и прочие прелести. Ничто не совершенно. Когда понимаешь, в чём может быть проблема, можешь залезть и сам починить. Потом написать автору — дескать, посмотри в этом месте, возможно бага.
Создание контекста GL
Стандартный порядок действий для инициализации работы в OpenGL такой. Нам нужно получить контекст устройства, на котором собираемся рисовать, настроить формат пикселя и получить контекст GL.
Предположим, что у нас есть класс, который отвечает за отрисовку в OpenGL. Мы каким-то образом, например, через конструктор, передали Handle того окна (панели), на котором хотим рисовать и после этого вызываем процедуру инициализации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
TSceneGL = class //... // Описатель окна (панели, другого windows-объекта) FHandle: HWND; // Контекст устройства для FHandle FDC: HDC; // Контекст отрисовки OpenGL FRC: HGLRC; // Инициализация OpenGL procedure InitializeGL; // Завершение работы с OpenGL procedure FinalizeGL; //.... end; |
Метод инициализации очень прост и буквально воспроизводит порядок действий, описанный в первом абзаце:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
procedure TSceneGL.InitializeGL; begin // Получаем контекст устройства FDC := GetDC(FHandle); try // Пытаемся настроить формат пикселя SetupPixelFormat(FDC); // Получаем контекст отрисовки OpenGL FRC := wglCreateContext(FDC); if FRC = 0 then RaiseLastOSError; // Связываем контексты if not wglMakeCurrent(FDC, FRC) then RaiseLastOSError; except // Если не получилось, всё завершаем FinalizeGL; // И пропускаем исключение наружу raise; end; end; |
Функция wglCreateContext создает новый контекст отрисовки OpenGL, который подходит для рисования на указанном контексте устройства. Контекст отрисовки имеет тот же формат пикселей, что и контекст устройства.
Функция wglMakeCurrent делает заданный контекст отрисовки OpenGL текущим контекстом отрисовки вызывающего потока. Все последующие вызовы OpenGL, выполняемые потоком, рисуются на заданном контексте устройства.
Метод для завершения работы с OpenGL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TSceneGL.FinalizeGL; begin try // Отсоединяем текущий контекст GL wglMakeCurrent(0, 0); // Освобождаем контекст отрисовки OpenGL if FRC <> 0 then wglDeleteContext(FRC); // Освобождаем контекст устройства if FDC <> 0 then ReleaseDC(FHandle, FDC); finally // Чтобы ни случилось, обнуляем контексты FDC := 0; FRC := 0; end; end; |
Пояснения к записи wglMakeCurrent(0, 0): «Если hglrc имеет значение NULL, функция делает текущий контекст отрисовки вызывающего потока не актуальным. В этом случае hdc игнорируется«. То есть в первом параметре может быть всё, что угодно.
Если всё сделано в одном потоке, делать этот вызов необязательно, wglDeleteContext сделает контекст не текущим и так. Но мы перестраховываемся.
Настройку формата пикселей можно сделать следующей процедурой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
procedure SetupPixelFormat(DC: HDC); var PFD: TPixelFormatDescriptor; PixelFormat: Integer; begin // Инициализируем структуру // В основном, в ней везде будут нули FillChar(PFD, SizeOf(PFD), 0); with PFD do begin // Укажем размер структуры nSize := SizeOf(TPixelFormatDescriptor); // Номер версии (а других и нет) nVersion := 1; // Настраиваем свойства буфера пикселей dwFlags := // Буфер может рисовать на поверхности окна или устройства PFD_DRAW_TO_WINDOW or // Буфер поддерживает рисование OpenGL PFD_SUPPORT_OPENGL or // Буфер имеет двойную буферизацию PFD_DOUBLEBUFFER; // Каждый пиксель имеет четыре компонента в порядке: // красный, зеленый, синий и альфа-канал iPixelType := PFD_TYPE_RGBA; // Количество цветовых битовых плоскостей в каждом буфере. // Для типов пикселей RGBA это размер буфера цвета, // за исключением альфа-битовых плоскостей. cColorBits := 32; // Глубина Z-буфера (ось Z) cDepthBits := 24; // Остальное либо больше не поддерживается, // либо всегда равно нулю end; // Пытаемся сопоставить формат пикселей, // поддерживаемый контекстом устройства, // с заданной спецификацией формата пикселей PixelFormat := ChoosePixelFormat(DC, @PFD); if PixelFormat = 0 then RaiseLastOSError; // Пытаемся настроить новый формат пикселя if GetPixelFormat(DC) <> PixelFormat then if not SetPixelFormat(DC, PixelFormat, @PFD) then RaiseLastOSError; end; |
Функция ChoosePixelFormat пытается сопоставить соответствующий формат пикселей, поддерживаемый контекстом устройства, с заданной спецификацией формата пикселей. Проще говоря, мы спрашиваем, ты такое потянешь? И если да, то вернёт что-то, отличное от нуля.

И всё работает. Но видя такое, опускаются руки…
Мультисэмплинг и сглаживание
Для того, чтобы получить сглаживание, нам нужен мультисэмплинг. Знающие люди говорят, что все старые методики сглаживания не работают. Например, glEnable(GL_MULTISAMPLE) ни на что не влияет. И они действительно не работают. И не факт, что когда-либо работали. Для реализации нормального сглаживания, говорят знающие люди, необходимо использовать ARB-Multisample.
Описание технологии мультисэмплинга, и что это такое, в большинстве случаев общие слова и перепечатка друг у друга. Но вот тут мне понравилось: Как работает рендеринг в 3D-играх: сглаживание. Несмотря на большое количество брюзжащих комментариев.
Если тезисно, то: 1) суперсэмплинг (Supersampling anti-aliasing, SSAA) — это рендер одной и той же сцены в разных разрешениях и подсчёт среднего арифметического для каждого пикселя. 2) мультисэмплинг (Multisample anti-aliasing, MSAA) — это почти как SSAA, но только работает он на краях проблемных областей. Край проблемной области — это края примитива на фоне, ступенчатая 🤬 грань куба. Всё на самом деле несколько сложнее, но не об том статья.
Таким образом, чтобы получить сглаживание, нам надо, чтобы движок мог анализировать более одного сэмпла.
Включить Multisample оказалось задачей нетривиальной. Дело в том, что описанные выше функции ChoosePixelFormat и wglCreateContext появились давно и абсолютно нерасширяемы. В структуре PIXELFORMATDESCRIPTOR нет поля, в котором можно было бы указать количество сэмплов для сглаживания. Чтобы получить возможность использовать другие функции, взамен указанных выше, нам нужны ARB-расширения.
ARB-расширения OpenGL
Неуёмный креатив производителей видеокарт приводит к появлению новых плюшек. В архитектуре OpenGL эти плюшки доступны как расширения. Есть расширения, которые принадлежат только конкретному производителю. Например, расширения от NVIDIA имеют в названии NV (WGL_NV_DX_interop).
Расширения, имеющие в названии ARB (WGL_ARB_pixel_format), одобрены Советом по обзору архитектуры OpenGL и рекомендованы к использованию. Совет почил в бозе, но ARB мы верим. Вот тут ребята разъясняют про расширения: Что такое расширения OpenGL и каковы преимущества/компромиссы их использования?
Невозможно описать все псевдонимы функций всех расширений. Однако, есть возможность получить функцию по названию. Этим целям служит функция wglGetProcAddress:
1 |
function wglGetProcAddress(ProcName: PAnsiChar): Pointer; stdcall; |
ProcName — это название функции. То есть, если мне нужна функция wglGetExtensionsStringARB, то я так и попрошу:
1 2 |
wglGetExtensionsStringARB := wglGetProcAddress('wglGetExtensionsStringARB'); |
Чтобы использовать эти функции, разработчик должен знать их псевдонимы. Потому что функция wglGetProcAddress возвращает просто указатель. И что там под этим указателем, должен рассказать тип переменной, которой мы этот указатель присваиваем.
1 2 3 4 5 |
// WGL_ARB_extensions_string type TwglGetExtensionsStringARB = function(hdc: HDC): PAnsiChar; stdcall; var wglGetExtensionsStringARB: TwglGetExtensionsStringARB; |
Функция wglGetExtensionsStringARB, которую мы только что запросили, возвращает строку с перечнем расширений для заданного контекста устройства. Расширения разделены пробелом. Строка заканчивается символом #0. Вид возвращаемой строки примерно такой:
‘WGL_EXT_depth_float WGL_ARB_buffer_region WGL_ARB_extensions_string WGL_ARB_make_current_read WGL_ARB_pixel_format WGL_ARB_pbuffer WGL_EXT_extensions_string WGL_EXT_swap_control WGL_ARB_multisample WGL_ARB_pixel_format_float WGL_ARB_framebuffer_sRGB WGL_ARB_create_context WGL_ARB_create_context_profile WGL_EXT_pixel_format_packed_float WGL_EXT_create_context_es_profile WGL_EXT_create_context_es2_profile WGL_NV_DX_interop WGL_NV_DX_interop2 WGL_ARB_robustness_application_isolation WGL_ARB_robustness_share_group_isolation WGL_ARB_create_context_robustness WGL_ARB_context_flush_control ‘
Давайте напишем функцию, которая будет определять наличие расширения в системе по имени:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
uses ..., System.AnsiStrings, ...; function HasExtension(const DC: HDC; const Name: AnsiString): Boolean; var Exts: PAnsiChar; begin if not Assigned(wglGetExtensionsStringARB) then exit(False); Exts := wglGetExtensionsStringARB(DC); if Exts = nil then exit(False); Result := ContainsText(Exts, Trim(Name)+' '); end; |
Есть более навороченный вариант функции. Подсмотрен в Skia:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function HasExtension(const DC: HDC; const Name: MarshaledAString): Boolean; var LEnd: MarshaledAString; LExtensions: MarshaledAString; begin if not Assigned(wglGetExtensionsStringARB) then exit(False); if System.AnsiStrings.StrComp(Name, 'WGL_ARB_extensions_string') = 0 then exit(True); LExtensions := wglGetExtensionsStringARB(DC); if LExtensions <> nil then begin while LExtensions^ <> #0 do begin LEnd := LExtensions; while (LEnd^ <> ' ') and (LEnd^ <> #0) do Inc(LEnd); if (LEnd - LExtensions = Length(Name)) and (System.AnsiStrings.StrLIComp(LExtensions, Name, LEnd - LExtensions) = 0) then exit(True); if LEnd^ = #0 then break; LExtensions := LEnd + 1; end; end; Result := False; end; |
Пока всё хорошо. Но есть одна проблема. В стандартной поставке Delphi нет ни типов, ни переменных для таких функций. Их надо прописывать руками.
Или нет?
Модуль dglOpenGL
Хотелось бы иметь некий модуль, в котором прописан максимально возможный набор псевдонимов функций. Потому что, в противном случае, придётся каждую функцию объявлять самому. В стандартной поставке Delphi такого нет. Зато есть в сети.
Это довольно популярный модуль dglOpenGL.pas. Немецкие программисты были недовольны родными заголовочными модулями Delphi и написали свой. Модуль содержит большой набор псевдонимов функций различных расширений и позволяет их подключать частями.
Возможно, модуль устарел, скажете вы. Вовсе нет, не соглашусь я. На текущий момент последняя версия OpenGL — 4.6, выпущена 31 июля 2017 года. Версия 4.6. в модуле представлена.
Чтобы его использовать, надо убрать из предложения uses модули Winapi.OpenGL, Winapi.OpenGLext и вместо них объявить dglOpenGL. В этом случае инициализация и завершение работы с GL будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TSceneGL.InitializeGL; begin FDC := GetDC(FHandle); FRC := CreateRenderingContext(FDC, [opDoubleBuffered], 24, 32, 0, 0, 0, 0); ActivateRenderingContext(FDC, FRC); //... end; procedure TSceneGL.FinalizeGL; begin //... DeactivateRenderingContext; if FRC <> 0 then wglDeleteContext(FRC); if FDC <> 0 then ReleaseDC(FHandle, FDC); FDC := 0; FRC := 0; end; |
Как видим, всё предельно просто. Несколько строк и всё работает.
Модуль имеет лицензию MPL 2.0. Если это каким-то образом не устраивает, модуль можно использовать как справочный материал и переносить нужное к себе. Грамотный копипаст — основа мироздания.
Включаем ARB-Multisample
По сути, нам нужно иметь больше настроек для формата пикселей и функцию, которая сможет создать контекст GL, понимая эти настройки.
Чтобы определить, существует ли вообще ARB-Multisample в системе, необходимо запросить расширение WGL_ARB_multisample. Если такой существует, нам понадобится расширение WGL_ARB_create_context, которое реализуется функцией wglCreateContextAttribsARB, и WGL_ARB_pixel_format, чтобы получить функцию wglChoosePixelFormatARB.
Проблема состоит в том, что получить эти функции можно только при наличии созданного контекста OpenGL. Казалось бы, вначале создали простой контекст. Получили нужные функции, отсоединили контекст. И создали уже правильный, с новыми настройками. Но для Windows такой финт не получится. Мы можем задать формат пикселей для окна только один раз. Очевидно, что для инициализации промежуточного контекста, нам придётся создавать какое-то временное окно.
Сценарий решения проблемы описан тут — OpenGL: Proper Context Creation.
Подготовка
Опишем директиву, переключающую на использование либо dglOpenGL, либо родные модули Delphi. Связано с тем, что кто-то может иметь неприязнь к чему-то стороннему, недолюбливать лицензию MPL, или же поставлена задача не использовать ничего стороннего.
1 2 3 4 5 6 7 8 9 10 11 12 |
interface {$DEFINE dglOpenGL} uses ..., {$IFDEF dglOpenGL} dglOpenGL, // Заголовочный модуль для OpenGL от немецких товарищей {$ELSE} Winapi.OpenGL, Winapi.OpenGLext, // Родные модули Delphi {$ENDIF} ...; |
Если мы не используем dglOpenGL, то заберём из него всё то полезное, что нам нужно и оформим следующим блоком
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
{$Region 'dglOpenGL.pas'} {$IFDEF dglOpenGL} type GLUquadricObj = PGLUquadricObj; {$ELSE} const // WGL_ARB_multisample WGL_SAMPLE_BUFFERS_ARB = $2041; WGL_SAMPLES_ARB = $2042; // WGL_ARB_pixel_format WGL_DRAW_TO_WINDOW_ARB = $2001; WGL_ACCELERATION_ARB = $2003; WGL_SUPPORT_OPENGL_ARB = $2010; WGL_DOUBLE_BUFFER_ARB = $2011; WGL_COLOR_BITS_ARB = $2014; WGL_ALPHA_BITS_ARB = $201B; WGL_DEPTH_BITS_ARB = $2022; WGL_STENCIL_BITS_ARB = $2023; WGL_FULL_ACCELERATION_ARB = $2027; // WGL_ARB_create_context WGL_CONTEXT_MAJOR_VERSION_ARB = $2091; WGL_CONTEXT_MINOR_VERSION_ARB = $2092; // WGL_ARB_create_context_profile WGL_CONTEXT_PROFILE_MASK_ARB = $9126; WGL_CONTEXT_CORE_PROFILE_BIT_ARB = $00000001; var // WGL_ARB_extensions_string wglGetExtensionsStringARB: function(hDC: HDC): PAnsiChar; stdcall; // WGL_ARB_pixel_format wglChoosePixelFormatARB: function(hDC: HDC; const piAttribIList: PGLint; const pfAttribFList: PGLfloat; nMaxFormats: GLuint; piFormats: PGLint; nNumFormats: PGLuint): BOOL; stdcall; // WGL_ARB_create_context wglCreateContextAttribsARB: function(hDC: HDC; hShareContext: HGLRC; const attribList: PGLint): HGLRC; stdcall; procedure Read_WGL_ARB_extensions_string; begin wglGetExtensionsStringARB := wglGetProcAddress('wglGetExtensionsStringARB'); end; procedure Read_WGL_ARB_pixel_format; begin wglChoosePixelFormatARB := wglGetProcAddress('wglChoosePixelFormatARB'); end; procedure Read_WGL_ARB_create_context; begin wglCreateContextAttribsARB := wglGetProcAddress('wglCreateContextAttribsARB'); end; {$ENDIF} {$EndRegion} |
Добавим класс исключения. Чтобы идентифицировать в будущем, что это точно наше.
1 2 3 |
type EScene3D = class(Exception); |
Временное окно и фиктивный контекст
Чтобы не испортить формат пикселя основного окна, в котором собираемся рисовать, создадим временное окно. В нём создадим временный контекст, способом, который описан выше. При успешном создании контекста, получим функции нужных нам расширений. Затем уничтожим и контекст, и окно.
Структура TPixelFormatDescriptor уже создана в вызывающей стороне. Листинг с заполнением структуры и вызовом будет ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
function InitializeARB(const PFD: TPixelFormatDescriptor): Boolean; const WindowClassName = '_GlTemp'; var LDC: HDC; LRC: HGLRC; LClass: TWndClass; LWindow: HWND; PixelFormat: Integer; begin Result := False; // Инициализируем структуру для создания окна FillChar(LClass, SizeOf(TWndClass), 0); LClass.lpfnWndProc := @DefWindowProc; LClass.hInstance := HInstance; LClass.lpszClassName := WindowClassName; // Регистрируем класс временного окна if Winapi.Windows.RegisterClass(LClass) = 0 then raise EScene3D.CreateFmt( '01. Could not register class: %s', [SysErrorMessage(GetLastError)]); try // Создаём окно LWindow := CreateWindowEx(WS_EX_TOOLWINDOW, WindowClassName, nil, WS_POPUP, 0, 0, 0, 0, 0, 0, HInstance, nil); if LWindow = 0 then raise EScene3D.CreateFmt( '02. Could not create temporary window: %s', [SysErrorMessage(GetLastError)]); try // Получаем контекст устройства LDC := GetDC(LWindow); if LDC = 0 then raise EScene3D.CreateFmt( '03. Could not get temporary device context: %s', [SysErrorMessage(GetLastError)]); try // Пытаемся установить формат пикселя PixelFormat := ChoosePixelFormat(LDC, @PFD); if (PixelFormat = 0) or (not SetPixelFormat(LDC, PixelFormat, @PFD)) then raise EScene3D.CreateFmt( '04. Could not set pixel format for temporary device context: %s', [SysErrorMessage(GetLastError)]); // Пытаемся создать контекст GL LRC := wglCreateContext(LDC); if LRC = 0 then raise EScene3D.CreateFmt( '05. Could not create temporary context: %s', [SysErrorMessage(GetLastError)]); try // Назначаем текущий контекст if not wglMakeCurrent(LDC, LRC) then raise EScene3D.CreateFmt( '06. Could not make temporary context as current: %s', [SysErrorMessage(GetLastError)]); try // Получаем функции расширений Read_WGL_ARB_extensions_string; Read_WGL_ARB_pixel_format; Read_WGL_ARB_create_context; // У нас всё получилось! Result := True; finally // Отвязываемся от текущего контекста wglMakeCurrent(0, 0); end; finally // Удаляем контекст GL wglDeleteContext(LRC); end; finally // Контекст устройства больше не нужен ReleaseDC(LWindow, LDC); end; finally // Уничтожаем временное окно DestroyWindow(LWindow); end; except // Если что-то пошло не так, убираем регистрацию класса Winapi.Windows.UnregisterClass(WindowClassName, HInstance); // Пропускаем исключение наружу raise; end; end; |
Настройка формата пикселя
В настройках формата пикселей нам не хватало только указания количества сэмплов для сглаживания. Теперь у нас такая возможность есть посредством функции wglChoosePixelFormatARB.
1 2 3 4 5 6 7 8 9 |
wglChoosePixelFormatARB: function( hDC: HDC; // Контекст устройства const piAttribIList: PGLint; // Список целочисленных атрибутов const pfAttribFList: PGLfloat; // Список вещественных атрибутов nMaxFormats: GLuint; // Максимальное количество форматов piFormats: PGLint; // Список форматов nNumFormats: PGLuint // Количество форматов в списке ): BOOL; stdcall; // Успех/неуспех операции |
Вместо того, чтобы брать фиксированную структуру PFD, функция берет список атрибутов и значений. Многие из этих атрибутов имеют прямые аналоги полей структуры PFD.
hDC: HDC |
Контекст устройства, с которым связан контекст GL. |
const piAttribIList: PGLint |
Список целочисленных атрибутов. Каждые два элемента в списке — это пара атрибут/значение. Атрибут «0» означает конец списка, после него не требуется значение. Допустимо значение NIL. |
const pfAttribFList: PGLfloat |
Cписок атрибутов с плавающей точкой. Каждые два элемента в списке — это пара атрибут/значение. |
nMaxFormats: GLuint |
Максимальное количество форматов, которые будут сохранены в piFormats. |
piFormats: PGLint |
Список форматов. Функция может возвращать несколько форматов. Их порядок — от наилучшего соответствия к худшему. |
nNumFormats: PGLuint |
Возвращаемое значение, означающее, сколько записей на самом деле в списке piFormats. |
Если функция возвращает FALSE, то код не смог найти подходящий формат пикселей. Для выяснения причин необходимо получить код последней ошибки Windows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function SelectPixelFormat(DC: HDC; const Attributes: TArray<Integer>; SampleCount: Integer): Integer; var AttrF: TArray<Single>; AttrI: TArray<Integer>; Count: Cardinal; begin AttrF := [0, 0]; Result := 0; if not Assigned(wglChoosePixelFormatARB) then exit; if SampleCount = 1 then begin AttrI := Attributes + // Буфер сэмплов нам не нужен, тут пусто [0, 0]; if (not wglChoosePixelFormatARB(DC, Pointer(AttrI), Pointer(AttrF), 1, @Result, @Count)) or (Count = 0) then exit(0); end else begin AttrI := Attributes + [WGL_SAMPLE_BUFFERS_ARB, 1, // Хотим буфер сэмплов WGL_SAMPLES_ARB, SampleCount, // Вот в таком количестве 0, 0]; if (not wglChoosePixelFormatARB(DC, Pointer(AttrI), Pointer(AttrF), 1, @Result, @Count)) or (Count = 0) then exit(0); end; end; |
Непосредственно установки формата пикселя тут нет. Она происходит в листинге ниже.
В функцию передаётся заранее сформированный массив атрибутов. Это неизменная часть настройки формата пикселя, дублирующая PFD. Мы просто дописываем в настройку количество сэмплов, которые хотим использовать.
Создание контекста GL с мультисэмплом
За создание контекста OpenGL с дополнительными атрибутами отвечает функция wglCreateContextAttribsARB.
1 2 3 4 5 6 |
wglCreateContextAttribsARB: function( hDC: HDC; // Контекст устройства hShareContext: HGLRC; // Контекст GL const attribList: PGLint // Список целочисленных атрибутов ): HGLRC; stdcall; // Успех/неуспех операции |
Поле hShareContext является специальным. Его назначение пока для меня туманно, поэтому равно нулю.
В поле attribList я указал версию 3.1:
1 2 3 4 5 |
ContextAttributes := [ WGL_CONTEXT_MAJOR_VERSION_ARB, 3, WGL_CONTEXT_MINOR_VERSION_ARB, 1, WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, 0]; |
Указывая более высокую версию, терял возможность рисовать старыми функциями. То есть контекст создавался без ошибок, что-то там происходило, но он оставался белым листом.
Возможно, объяснение в том, что «…расширение, ARB_compatibility, было представлено, когда был представлен OpenGL 3.1. Наличие этого расширения является сигналом для пользователя о том, что устаревшие или удаленные функции все еще доступны через исходные точки входа и перечисления.» Источник.
Вопрос, а зачем мне вообще старые функции. Ну, новые возможности я ещё досконально не изучил 😇. Во-вторых, хотелось изучить вопрос совместимости. Возможна ли она в принципе? Возможна.
Если серьезно, то вот ситуация. У меня есть просмотр 3D-модели. Меня на этом этапе всё устраивает, кроме ступенек. Я просто хочу включить нормальное сглаживание, не переписывая ничего.
Включил, доволен, дальше буду изучать шейдеры.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
function CreateRenderingContextARB(DC: HDC; ARBMultisample: Boolean=True): HGLRC; var ContextAttributes: TArray<Integer>; PixelFormatAttributes: TArray<Integer>; PFD: TPixelFormatDescriptor; PixelFormat: Integer; begin Result := 0; if DC = 0 then raise EScene3D.Create( '07. Could not get shared device context.'); // Инициализация библиотеки OpenGL {$IFDEF dglOpenGL} if GL_LibHandle = nil then InitOpenGL; if not Assigned(GL_LibHandle) then raise EScene3D.Create( '08. GL_LibHandle is NIL. Could not load OpenGL library!'); {$ENDIF} // Инициализация структуры формата пикселя FillChar(PFD, SizeOf(TPixelFormatDescriptor), 0); with PFD do begin nSize := SizeOf(TPixelFormatDescriptor); nVersion := 1; dwFlags := PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER; iPixelType := PFD_TYPE_RGBA; cColorBits := 32; cDepthBits := 24; end; // Если заказан мультисэмплинг, происходит попытка // создать временное окно и временный контекст GL, // во время которой происходит инициализация ARB-расширений if ARBMultisample and InitializeARB(PFD) and Assigned(wglCreateContextAttribsARB) then begin // Если инbциализация ARB-расширений произошла успешно // формируем массив настроек для подбора формата пикселя PixelFormatAttributes := [ WGL_DRAW_TO_WINDOW_ARB, 1, WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, WGL_SUPPORT_OPENGL_ARB, 1, WGL_DOUBLE_BUFFER_ARB, 1, WGL_DEPTH_BITS_ARB, 24, WGL_COLOR_BITS_ARB, 32, WGL_ALPHA_BITS_ARB, 8, WGL_STENCIL_BITS_ARB, 8]; PixelFormat := 0; // Если ОС поддерживает мультисэмплинг, пытаемся получить // необходимый формат пикселя if HasExtension(DC, 'WGL_ARB_multisample') then begin PixelFormat := SelectPixelFormat(DC, PixelFormatAttributes, 4); if PixelFormat = 0 then PixelFormat := SelectPixelFormat(DC, PixelFormatAttributes, 2); end; // Если нет мультисэмплинга, либо что-то не получилось // на предыдущих этапах, пытаемся инициализировать 1 сэмпл if PixelFormat = 0 then PixelFormat := SelectPixelFormat(DC, PixelFormatAttributes, 1); // Если формат пикселя подобран if PixelFormat <> 0 then begin // Пытаемся установить этот формат пикселя if not SetPixelFormat(DC, PixelFormat, @PFD) then raise EScene3D.CreateFmt( '09. Could not set pixel format for shared device context: %s', [SysErrorMessage(GetLastError)]); // Формируем набор атрибутов для получения контекста GL ContextAttributes := [ WGL_CONTEXT_MAJOR_VERSION_ARB, 3, WGL_CONTEXT_MINOR_VERSION_ARB, 1, WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, 0]; // Пытаемся получить контекст GL Result := wglCreateContextAttribsARB(DC, 0, Pointer(ContextAttributes)); end; end; // Если не требуется, либо не получилось создать мультисэмпл if Result = 0 then begin // Пытаемся создать обычный контекст GL PixelFormat := ChoosePixelFormat(DC, @PFD); if (PixelFormat=0) or not SetPixelFormat(DC, PixelFormat, @PFD) then raise EScene3D.CreateFmt( '10. Could not set pixel format for device context: %s', [SysErrorMessage(GetLastError)]); Result := wglCreateContext(DC); if Result = 0 then raise EScene3D.CreateFmt( '11. Could not create shared context: %s', [SysErrorMessage(GetLastError)]); end; end; |
Использование
Инициализация и завершение сеанса OpenGL теперь выглядят следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
procedure TScene3D.InitializeGL; begin FDC := GetDC(FHandle); FRC := CreateRenderingContextARB(FDC); {$IFDEF dglOpenGL} ActivateRenderingContext(FDC, FRC); {$ELSE} wglMakeCurrent(FDC, FRC); {$ENDIF} end; procedure TScene3D.FinalizeGL; begin {$IFDEF dglOpenGL} DeactivateRenderingContext; {$ELSE} wglMakeCurrent(0, 0); {$ENDIF} if FRC <> 0 then wglDeleteContext(FRC); if FDC <> 0 then ReleaseDC(FHandle, FDC); FDC := 0; FRC := 0; end; |
Отобразить 3D-модель
Первая часть из постановки задачи — научиться сглаживать, решена. Теперь надо убедиться, что старыми функция можно пользоваться, и что шейдеры для отрисовки заводить не обязательно.
Рисуем сетку земли
Сетка нам нужно, чтобы хоть как-то ориентироваться в пространстве. Рисуем её по старинке, через glBegin..glEnd. То есть метод рисования очень-очень старый. Не стал менять.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class procedure TScene3D.DoDrawGround(DrawAxes: Boolean); var i: Integer; begin glColor3f(0.8,0.8,0.8); glLineWidth(0.8); glBegin(GL_LINES); for i := -12 to 12 do begin if (i=0) and DrawAxes then continue; glVertex3f(i/10, 0, 1.2); glVertex3f(i/10, 0,-1.2); glVertex3f(-1.2, 0, i/10); glVertex3f( 1.2, 0, i/10); end; glEnd; end; |
Рисуем координатные оси
Можно нарисовать линиями, но хочется иметь масштабируемую толщину и симпатичную объёмную стрелку на конце. Поэтому используем квадрики и метод рисования снова очень старый.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
class procedure TScene3D.DrawArrow(const P1, P2: TPoint3D; D, H: Single); var P: TPoint3D; L: Single; Q: GLUquadricObj; begin P := P2-P1; L := P.Length; if (P.x<>0) or (P.y<>0) then begin glRotated(ArcTan2(P.y, P.x)*DegPerRad, 0.0, 0.0, 1.0); glRotated(ArcTan2(Sqrt(P.x*P.x + P.y*P.y), P.z)*DegPerRad, 0.0, 1.0, 0.0); end else if (P.z<0) then glRotated(180, 1.0, 0.0, 0.0); // Стрелка glTranslatef(0, 0, L/2-H); Q := gluNewQuadric(); try gluQuadricDrawStyle(Q, GLU_FILL); gluQuadricNormals(Q, GLU_SMOOTH); gluCylinder(Q, D, 0.0, H, 12, 1); finally gluDeleteQuadric(Q); end; // Ось (Труба) glTranslatef(0, 0, -L); Q := gluNewQuadric(); try gluQuadricDrawStyle(Q, GLU_FILL); gluQuadricNormals(Q, GLU_SMOOTH); gluCylinder(Q, D*0.25, D*0.25, L, 12, 1); finally gluDeleteQuadric(Q); end; end; class procedure TScene3D.DoDrawAxes; const L = 1.3; // Длина координатных осей begin // Для фронтовых и задних многоугольников выбираем полное заполнение glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glLineWidth(1.0); // Ось X (красный) glPushMatrix(); glColor3f(1.0, 0.0, 0.0); DrawArrow(Point3D(-L, 0, 0), Point3D(L, 0, 0), 0.016, 0.1); glPopMatrix(); // Ось Y (зелёный) glPushMatrix(); glColor3f(0.0, 0.64, 0.36); DrawArrow(Point3D(0, -L, 0), Point3D(0, L, 0), 0.016, 0.1); glPopMatrix(); // Ось Z (синий) glPushMatrix(); glColor3f(0.0, 0.0, 1.0); DrawArrow(Point3D(0, 0, -L), Point3D(0, 0, L), 0.016, 0.1); glPopMatrix(); end; |

Без сглаживания всё выглядит очень плохо.

Со сглаживанием тот же самый код выглядит очень хорошо.
Рисуем 3D-модель
Для отрисовки модели используем массивы координат вершин, векторов нормалей в вершинах и массив цвета в вершинах.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TScene3D.DoDrawTriangles; begin glEnableClientState(GL_NORMAL_ARRAY); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); glNormalPointer(GL_FLOAT, 0, Pointer(FNormalsArray)); glVertexPointer(3, GL_FLOAT, 0, Pointer(FVerticesArray)); glColorPointer(3, GL_FLOAT, 0, Pointer(FColorsArray)); glDrawElements(GL_TRIANGLES, Length(FIndicesArray), GL_UNSIGNED_INT, Pointer(FIndicesArray)); glDisableClientState(GL_COLOR_ARRAY); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY); end; |
Код очень тривиален. Подробнейшее описание функций можно найти в каждом первом мануале по OpenGL. Так что тут комментировать нечего. Самое главное тут — правильно сформировать массивы для отрисовки. Массивы, которые сформировались в модели при прочтении файла — непригодны.
Итак, у нас описаны такие массивы:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Массив индексов вершин FIndicesArray: TIntegerDynArray; // Массив геометрических вершин FVerticesArray: TPoint3DDynArray; // Массив нормалей в вершинах FNormalsArray: TPoint3DDynArray; // Массив текстурных координат в вершинах FTexturesArray: TPoint3DDynArray; // Массив цвета в вершинах FColorsArray: TPoint3DDynArray; // Массив индексов материала для фейса FMaterialsArray: TIntegerDynArray; |
После того, как мы прочитали модель из файла, запускаем процедуру формирования массивов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
procedure TScene3D.MakeArrays; var i, j, k, Idx: Integer; F: TWFFace; begin // Обнуляем массивы Clear; // Если в модели нет нормалей, if FModel.Normals = nil then // просим модель их посчитать FModel.CalcNormals; for i := 0 to High(FModel.Faces) do begin F := FModel.Faces[i]; k := 0; for j := 0 to High(F.FData) do begin // Добавляем значение в массив индексов SetLength(FIndicesArray, Length(FIndicesArray)+1); FIndicesArray[High(FIndicesArray)] := High(FIndicesArray); // Получаем индекс вершины Idx := F.FData[j].P-1; // Добавляем в массив вершин координаты вершины по индексу SetLength(FVerticesArray, Length(FVerticesArray)+1); FVerticesArray[High(FVerticesArray)] := FModel.Vertices[Idx]; // Получаем индекс нормали Idx := F.FData[j].N-1; // Добавляем в массив нормалей значение нормали по индексу SetLength(FNormalsArray, Length(FNormalsArray)+1); FNormalsArray[High(FNormalsArray)] := FModel.Normals[Idx]; // Если есть текстуры, формируем массив текстурных координат if FModel.Textures<>nil then begin Idx := F.FData[j].T-1; // Неприятные ситуации, когда в модели не полностью // прописаны текстурные вершины (бывает такое) if Idx<0 then Idx := 0; if Idx>High(FModel.Textures) then Idx := High(FModel.Textures); SetLength(FTexturesArray, Length(FTexturesArray)+1); FTexturesArray[High(FTexturesArray)] := FModel.Textures[Idx]; end; // Массив материалов для фейса, состоящего из 3 точек Inc(k); if k=3 then begin SetLength(FMaterialsArray, Length(FMaterialsArray)+1); FMaterialsArray[High(FMaterialsArray)] := F.FMatIndex; k := 0; end; end; end; // Просим модель освободить свои массивы, они больше не нужны FModel.ClearArrays; // Создаём массив цветов для каждой вершины RecreateColors; end; |
При таком методе отображения, когда один массив индексов работает одновременно на три (а позже и четыре) массива, мы не можем использовать массивы, которые сформировались в модели во время чтения файла. Индекс вершины и индекс нормали к этой вершине в модели не совпадают. Поэтому после прочтения происходит постобработка и очистка массивов модели.
Метод RecreateColors формирует массив цветов в вершинах аналогичным образом. Можно посмотреть в исходниках. Просто он пересоздаётся при изменении параметров выделения части модели, и если публиковать его листинг, то пришлось бы публиковать и всё остальное, а это уже много.
Скрины без сглаживания и со сглаживанием представлены в начале статьи, там где крик души про ступеньки и смысл жизни. Но скрины тут всё таки будут, но по другому поводу.
Казалось бы, линии сетки что со сглаживанием, что без сглаживания, визуально не отличаются. Не, отличаются.

Если посмотрим на модель снизу, или с любого другого ракурса, где линии идут поверх модели, то увидим, что они не сглажены ни разу.

Зато со сглаживанием всё очень хорошо с любого ракурса.
Итог
Конечно, это не самое лучшее сглаживание в мире. Есть продвинутые вещи, которые надо изучать. Но теперь вид модели вызывает положительные эмоции и стимул, как минимум, подключить текстуры и задействовать шейдеры. У меня был опыт подключать текстуры, но это было в мезозое и давно устарело. А шейдерами не занимался никогда, но всегда хотел изучить эту тему.
Поэтому, очень надеюсь, продолжение будет. Просто для этого нужен отрезок времени на несколько дней, когда можно будет безболезненно выключить телегу и телефон.
Скачать
Друзья, спасибо за внимание!
Исходник (zip) 233 Кб. Delphi XE 7
Исполняемый файл (zip) 1.34 Мб (Скомпилирован в XE 7) — дополнительно внутри небольшая коллекция маленьких моделей.
Модельки можно совершенно бесплатно и без регистрации скачать тут: rigmodels.com или creazilla.com.
Исполняемый файл лучше запускать непосредственно из архива. Или распаковывать всё в каталог. Так он по крайней мере будет видеть каталог с моделями.
Управление
Левая кнопка мыши — вращаем камеру
Правая кнопка мыши — отдаляем/приближаем камеру
Нажатое Колесо — перемещаем модель