Введение в OpenGL: учебник по рендерингу 3D-текста

Опубликовано: 2022-03-11

Благодаря таким инструментам, как DirectX и OpenGL, написать настольное приложение, отображающее трехмерные элементы, в настоящее время не так уж сложно. Однако, как и во многих технологиях, иногда возникают препятствия, мешающие разработчикам войти в эту нишу. Со временем гонка между DirectX и OpenGL привела к тому, что эти технологии стали более доступными для разработчиков, наряду с лучшей документацией и более простым процессом становления опытным разработчиком DirectX или OpenGL.

DirectX, представленный и поддерживаемый Microsoft, представляет собой технологию, специфичную для платформы Windows. С другой стороны, OpenGL — это кроссплатформенный API для области 3D-графики, спецификация которого поддерживается Khronos Group.

введение в опенгл

В этом введении в OpenGL я объясню, как написать очень простое приложение для визуализации трехмерных текстовых моделей. Мы будем использовать Qt/Qt Creator для реализации пользовательского интерфейса, что упростит компиляцию и запуск этого приложения на нескольких платформах. Исходный код прототипа, созданного для этой статьи, доступен на GitHub.

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

Предпосылки

Прежде чем начать, нам нужно подготовить нашу среду разработки с некоторыми полезными инструментами для этого проекта. Самое первое, что нам нужно, это фреймворк Qt и соответствующие утилиты, которые можно скачать с www.qt.io. Он также может быть доступен через стандартный менеджер пакетов вашей операционной системы; если это так, вы можете сначала попробовать с ним. Эта статья требует некоторого знакомства с фреймворком Qt. Тем не менее, если вы не знакомы с фреймворком, не расстраивайтесь и продолжайте, так как прототип основан на некоторых довольно тривиальных функциях фреймворка.

Вы также можете использовать Microsoft Visual Studio 2013 в Windows. В этом случае убедитесь, что вы используете соответствующую надстройку Qt для Visual Studio.

На этом этапе вы можете клонировать репозиторий с GitHub и следовать ему, пока читаете эту статью.

Обзор OpenGL

Мы начнем с создания простого проекта приложения Qt с одним виджетом документа. Поскольку это простой виджет, его компиляция и запуск не дадут ничего полезного. С дизайнером Qt мы добавим меню «Файл» с четырьмя пунктами: «Создать…», «Открыть…», «Закрыть» и «Выход». Вы можете найти код, который связывает эти пункты меню с соответствующими им действиями в репозитории.

При нажатии на «Создать…» должно появиться диалоговое окно, которое будет выглядеть примерно так:

всплывающее окно opengl

Здесь пользователь может ввести некоторый текст, выбрать шрифт, настроить результирующую высоту модели и создать 3D-модель. Нажатие «Создать» должно сохранить модель, а также открыть ее, если пользователь выберет соответствующую опцию в левом нижнем углу. Как вы понимаете, цель здесь — преобразовать введенный пользователем текст в 3D-модель и отобразить его на дисплее.

Проект будет иметь простую структуру, а компоненты будут разбиты на несколько C++ и заголовочных файлов:

c++ и заголовочные файлы

создатьcharmodeldlg.h/cpp

Файлы содержат объект, производный от QDialog. Это реализует виджет диалога, который позволяет пользователю вводить текст, выбирать шрифт и выбирать, сохранять ли результат в файл и/или отображать его в 3D.

gl_widget.h/cpp

Содержит реализацию производного объекта QOpenGLWidget. Этот виджет используется для рендеринга 3D-сцены.

mainwindow.h/cpp

Содержит реализацию основного виджета приложения. Эти файлы остались без изменений, так как они были созданы мастером Qt Creator.

main.cpp

Содержит функцию main(…), которая создает основной виджет приложения и отображает его на экране.

model2d_processing.h/cpp

Содержит функционал создания 2D сцены.

model3d.h/cpp

Содержит структуры, которые хранят объекты 3D-моделей и позволяют выполнять над ними операции (сохранение, загрузка и т. д.).

model_creator.h/cpp

Содержит реализацию класса, который позволяет создавать объект модели 3D-сцены.

Реализация OpenGL

Для краткости мы пропустим очевидные детали реализации пользовательского интерфейса с помощью Qt Designer и код, определяющий поведение интерактивных элементов. Конечно, есть еще несколько интересных аспектов этого прототипа приложения, которые не только важны, но и имеют отношение к кодированию и рендерингу 3D-моделей, которые мы хотим осветить. Например, первый шаг преобразования текста в 3D-модель в этом прототипе включает преобразование текста в монохромное 2D-изображение. Как только это изображение сгенерировано, можно узнать, какие пиксели изображения образуют текст, а какие — просто «пустое» пространство. Есть несколько более простых способов рендеринга основного текста с помощью OpenGL, но мы используем этот подход, чтобы охватить некоторые мельчайшие детали 3D-рендеринга с помощью OpenGL.

Чтобы сгенерировать это изображение, мы создаем экземпляр объекта QImage с флагом QImage::Format_Mono. Поскольку все, что нам нужно знать, это какие пиксели являются частью текста, а какие нет, монохромное изображение должно работать нормально. Когда пользователь вводит текст, мы синхронно обновляем этот объект QImage. Основываясь на размере шрифта и ширине изображения, мы делаем все возможное, чтобы текст соответствовал высоте, заданной пользователем.

Далее мы перечисляем все пиксели, которые являются частью текста — в данном случае это черные пиксели. Каждый пиксель здесь рассматривается как отдельная квадратная единица. На основе этого мы можем сгенерировать список треугольников, вычислив координаты их вершин, и сохранить их в файле нашей 3D-модели.

Теперь, когда у нас есть собственный простой формат файла 3D-модели, мы можем сосредоточиться на его рендеринге. Для 3D-рендеринга на основе OpenGL Qt предоставляет виджет под названием QOpenGLWidget. Для использования этого виджета можно переопределить три функции:

  • initializeGl() - здесь находится код инициализации
  • paintGl() - этот метод вызывается каждый раз, когда виджет перерисовывается.
  • resizeGl(int w, int h) — этот метод вызывается с шириной и высотой виджета каждый раз, когда он изменяется.

формат файла 3dmodel

Мы инициализируем виджет, установив соответствующую конфигурацию шейдера в методе initializeGl.

 glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);

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

После инициализации мы визуализируем модель на дисплее каждый раз, когда вызывается paintGl. Прежде чем мы переопределим метод paintGl, мы должны подготовить буфер. Для этого мы сначала создадим дескриптор буфера. Затем мы привязываем дескриптор к одной из точек привязки, копируем исходные данные в буфер и, наконец, говорим программе отвязать буфер:

 // Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we'll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we've finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);

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

 QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);

Для повышения производительности мы использовали Vertex Buffer Object (VBO) в нашем прототипе приложения. Это позволяет нам хранить данные в видеопамяти и использовать их непосредственно для рендеринга. Альтернативный метод включает предоставление данных (координаты вершин, нормали и цвета) из кода рендеринга:

 glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();

Это может показаться более простым решением; однако это имеет серьезные последствия для производительности, поскольку для этого требуется, чтобы данные проходили через шину видеопамяти, что является относительно более медленным процессом. После реализации метода paintGl мы должны обратить внимание на шейдеры:

 m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( "#version 400\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8("coordVertexes")); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8("coordNormals")); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixVertex")); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixNormal")); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8("fragmentColor"));

В OpenGL шейдеры реализованы с использованием языка, известного как GLSL. Язык разработан, чтобы упростить манипулирование 3D-данными до их рендеринга. Здесь нам понадобятся два шейдера: вершинный шейдер и фрагментный шейдер. В вершинном шейдере мы преобразуем координаты с помощью матрицы преобразования, чтобы применить вращение и масштабирование, а также вычислить цвет. Во фрагментном шейдере мы назначим фрагменту цвет. Затем эти шейдерные программы должны быть скомпилированы и связаны с контекстом. OpenGL предоставляет простые способы соединения двух сред, так что параметры внутри программы могут быть доступны или назначены извне:

 // Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);

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

 gl_Position = matrixVertex * vec4(coordVertexes, 1.0);

Чтобы вычислить эту матрицу преобразования, мы вычисляем несколько отдельных матриц: масштаб экрана, перемещение сцены, масштабирование, поворот и центрирование. Затем мы находим произведение этих матриц, чтобы вычислить окончательную матрицу преобразования. Начните с перемещения центра модели в начало координат (0, 0, 0), которое также является центром экрана. Вращение определяется взаимодействием пользователя со сценой с помощью некоторого указывающего устройства. Пользователь может нажать на сцену и перетащить ее, чтобы повернуть. Когда пользователь нажимает, мы сохраняем позицию курсора, а после движения у нас есть вторая позиция курсора. Используя эти две координаты вместе с центром сцены, мы формируем треугольник. Выполнив несколько простых вычислений, мы можем определить угол поворота и обновить нашу матрицу поворота, чтобы отразить это изменение. Для масштабирования мы просто полагаемся на колесико мыши, чтобы изменить коэффициент масштабирования осей X и Y виджета OpenGL. Модель смещается назад на 0,5, чтобы оставаться за плоскостью, из которой рендерится сцена. Наконец, чтобы сохранить естественное соотношение сторон, нам нужно отрегулировать уменьшение расширения модели по длинной стороне (в отличие от сцены OpenGL, виджет, в котором он отображается, может иметь разные физические размеры по обеим осям). Объединив все это, мы вычисляем окончательную матрицу преобразования следующим образом:

 void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast<float>(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }

Заключение

В этом введении в 3D-рендеринг OpenGL мы рассмотрели одну из технологий, которая позволяет ud использовать нашу видеокарту для рендеринга 3D-модели. Это намного эффективнее, чем использование циклов ЦП для той же цели. Мы использовали очень простую технику затенения и сделали сцену интерактивной за счет обработки действий пользователя с помощью мыши. Мы избегали использования шины видеопамяти для передачи данных туда и обратно между видеопамятью и программой. Несмотря на то, что мы только что визуализировали одну строку текста в 3D, более сложные сцены могут быть визуализированы очень похожими способами.

Справедливости ради следует отметить, что в этом руководстве мы едва коснулись 3D-моделирования и рендеринга. Это обширная тема, и это руководство по OpenGL не может претендовать на то, что это все, что вам нужно знать, чтобы создавать 3D-игры или программы для моделирования. Однако цель этой статьи — дать вам возможность заглянуть в эту область и показать, как легко вы можете начать работу с OpenGL для создания 3D-приложений.