Wprowadzenie do OpenGL: samouczek renderowania tekstu 3D

Opublikowany: 2022-03-11

Dzięki dostępności narzędzi takich jak DirectX i OpenGL, napisanie aplikacji desktopowej, która renderuje elementy 3D, nie jest w dzisiejszych czasach bardzo trudne. Jednak, podobnie jak w przypadku wielu technologii, czasami pojawiają się przeszkody utrudniające programistom wejście w tę niszę. Z biegiem czasu wyścig między DirectX i OpenGL sprawił, że te technologie stały się bardziej dostępne dla programistów, wraz z lepszą dokumentacją i łatwiejszym procesem stawania się wykwalifikowanym programistą DirectX lub OpenGL.

DirectX, wprowadzony i utrzymywany przez Microsoft, to technologia specyficzna dla platformy Windows. Z drugiej strony OpenGL to wieloplatformowy interfejs API dla obszaru grafiki 3D, którego specyfikację opiekuje się Khronos Group.

wprowadzenie do opengl

W tym wprowadzeniu do OpenGL wyjaśnię, jak napisać bardzo prostą aplikację do renderowania modeli tekstowych 3D. Będziemy używać Qt/Qt Creator do implementacji interfejsu użytkownika, ułatwiając kompilację i uruchamianie tej aplikacji na wielu platformach. Kod źródłowy prototypu zbudowanego na potrzeby tego artykułu jest dostępny w serwisie GitHub.

Celem tej prostej aplikacji jest generowanie modeli 3D, zapisywanie ich do pliku o prostym formacie oraz otwieranie i renderowanie na ekranie. Model 3D w renderowanej scenie będzie można obracać i powiększać, aby zapewnić lepsze wyczucie głębi i wymiarów.

Warunki wstępne

Zanim zaczniemy, będziemy musieli przygotować nasze środowisko programistyczne z kilkoma przydatnymi narzędziami do tego projektu. Pierwszą rzeczą, jakiej potrzebujemy, jest framework Qt i odpowiednie narzędzia, które można pobrać ze strony www.qt.io. Może być również dostępny za pośrednictwem standardowego menedżera pakietów systemu operacyjnego; jeśli tak jest, możesz najpierw spróbować z tym. Ten artykuł wymaga znajomości frameworka Qt. Jeśli jednak nie jesteś zaznajomiony z frameworkiem, nie zniechęcaj się do kontynuowania, ponieważ prototyp opiera się na kilku dość trywialnych cechach frameworka.

Możesz także użyć Microsoft Visual Studio 2013 w systemie Windows. W takim przypadku upewnij się, że używasz odpowiedniego dodatku Qt dla programu Visual Studio.

W tym momencie możesz sklonować repozytorium z GitHub i podążać za nim podczas czytania tego artykułu.

Przegląd OpenGL

Zaczniemy od stworzenia prostego projektu aplikacji Qt z pojedynczym widżetem dokumentu. Ponieważ jest to widżet typu bare-bones, kompilacja i uruchomienie nie przyniesie niczego użytecznego. W Qt Designer dodamy menu „Plik” z czterema pozycjami: „Nowy…”, „Otwórz…”, „Zamknij” i „Wyjdź”. Możesz znaleźć kod, który wiąże te elementy menu z odpowiadającymi im akcjami w repozytorium.

Kliknięcie „Nowy…” powinno wyświetlić okno dialogowe, które będzie wyglądać mniej więcej tak:

otwórz wyskakujące okienko

Tutaj użytkownik może wprowadzić tekst, wybrać czcionkę, dostosować wynikową wysokość modelu i wygenerować model 3D. Kliknięcie „Utwórz” powinno zapisać model, a także otworzyć go, jeśli użytkownik wybierze odpowiednią opcję z lewego dolnego rogu. Jak widać, celem jest tutaj przekształcenie tekstu wprowadzonego przez użytkownika w model 3D i wyrenderowanie go na wyświetlaczu.

Projekt będzie miał prostą strukturę, a komponenty zostaną podzielone na kilka plików C++ i nagłówkowych:

c++ i pliki nagłówkowe

createcharmodeldlg.h/cpp

Pliki zawierają obiekt pochodny QDialog. To implementuje widżet okna dialogowego, który pozwala użytkownikowi wpisać tekst, wybrać czcionkę i wybrać, czy zapisać wynik do pliku i/lub wyświetlić go w 3D.

gl_widget.h/cpp

Zawiera implementację obiektu pochodnego QOpenGLWidget. Ten widżet służy do renderowania sceny 3D.

mainwindow.h/cpp

Zawiera implementację głównego widżetu aplikacji. Te pliki pozostały niezmienione, ponieważ zostały utworzone przez kreatora Qt Creator.

main.cpp

Zawiera funkcję main(…), która tworzy główny widżet aplikacji i pokazuje go na ekranie.

model2d_processing.h/cpp

Zawiera funkcjonalność tworzenia sceny 2D.

model3d.h/cpp

Zawiera struktury, które przechowują obiekty modelu 3D i umożliwiają wykonywanie na nich operacji (zapisywanie, wczytywanie itp.).

model_creator.h/cpp

Zawiera implementację klasy, która umożliwia tworzenie obiektu modelu sceny 3D.

Implementacja OpenGL

Dla zwięzłości pominiemy oczywiste szczegóły implementacji interfejsu użytkownika w Qt Designer oraz kod definiujący zachowanie elementów interaktywnych. Z pewnością jest kilka bardziej interesujących aspektów tej prototypowej aplikacji, które są nie tylko ważne, ale także istotne dla kodowania i renderowania modeli 3D, które chcemy omówić. Na przykład pierwszy etap konwersji tekstu na model 3D w tym prototypie polega na przekształceniu tekstu w monochromatyczny obraz 2D. Po wygenerowaniu tego obrazu można dowiedzieć się, który piksel obrazu tworzy tekst, a które są po prostu „pustą” przestrzenią. Istnieje kilka prostszych sposobów renderowania podstawowego tekstu przy użyciu OpenGL, ale stosujemy to podejście, aby omówić niektóre najdrobniejsze szczegóły renderowania 3D za pomocą OpenGL.

Aby wygenerować ten obraz, tworzymy instancję obiektu QImage z flagą QImage::Format_Mono. Ponieważ wszystko, co musimy wiedzieć, to które piksele są częścią tekstu, a które nie, obraz monochromatyczny powinien działać dobrze. Gdy użytkownik wprowadzi jakiś tekst, synchronicznie aktualizujemy ten obiekt QImage. W oparciu o rozmiar czcionki i szerokość obrazu staramy się jak najlepiej dopasować tekst do wysokości zdefiniowanej przez użytkownika.

Następnie wyliczamy wszystkie piksele, które są częścią tekstu - w tym przypadku czarne piksele. Każdy piksel jest tutaj traktowany jako oddzielne jednostki o kształcie kwadratu. Na tej podstawie możemy wygenerować listę trójkątów, obliczając współrzędne ich wierzchołków i przechowywać je w naszym pliku modelu 3D.

Teraz, gdy mamy własny, prosty format pliku modelu 3D, możemy zacząć skupiać się na jego renderowaniu. Do renderowania 3D opartego na OpenGL, Qt udostępnia widżet o nazwie QOpenGLWidget. Aby korzystać z tego widżetu, można nadpisać trzy funkcje:

  • InitializeGl() - to jest miejsce, do którego trafia kod inicjujący
  • paintGl() - ta metoda jest wywoływana za każdym razem, gdy widżet jest przerysowywany
  • resizeGl(int w, int h) - ta metoda jest wywoływana z szerokością i wysokością widżetu za każdym razem, gdy zmieniany jest rozmiar

Format pliku modelu 3d

Zainicjujemy widżet, ustawiając odpowiednią konfigurację shadera w metodzie initializeGl.

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

Pierwsza linia powoduje, że program pokazuje tylko te wyrenderowane piksele, które są bliżej nas, a nie te, które znajdują się za innymi pikselami i są poza zasięgiem wzroku. Druga linia określa technikę cieniowania płaskiego. Trzecia linia powoduje, że program renderuje trójkąty bez względu na kierunek, na który wskazują ich normalne.

Po zainicjowaniu renderujemy model na wyświetlaczu za każdym razem, gdy wywoływany jest paintGl. Zanim zastąpimy metodę paintGl, musimy przygotować bufor. Aby to zrobić, najpierw tworzymy uchwyt bufora. Następnie wiążemy uchwyt z jednym z punktów wiązania, kopiujemy dane źródłowe do bufora, a na koniec mówimy programowi, aby odwiązał bufor:

 // 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);

Wewnątrz nadrzędnej metody paintGl używamy tablicy wierzchołków i tablicy normalnych danych, aby narysować trójkąty dla każdej klatki:

 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);

Aby zwiększyć wydajność, w naszej aplikacji prototypowej użyliśmy obiektu Vertex Buffer Object (VBO). Pozwala nam to przechowywać dane w pamięci wideo i wykorzystywać je bezpośrednio do renderowania. Alternatywna metoda obejmuje dostarczenie danych (współrzędnych wierzchołków, normalnych i kolorów) z kodu renderowania:

 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();

To może wydawać się prostszym rozwiązaniem; jednak ma to poważne konsekwencje dla wydajności, ponieważ wymaga to przesyłania danych przez magistralę pamięci wideo - jest to stosunkowo wolniejszy proces. Po wdrożeniu metody paintGl musimy zwrócić uwagę na shadery:

 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"));

W OpenGL moduły cieniujące są implementowane przy użyciu języka znanego jako GLSL. Język został zaprojektowany tak, aby ułatwić manipulowanie danymi 3D przed ich wyrenderowaniem. Tutaj będziemy potrzebować dwóch shaderów: Vertex Shader i Fragment Shader. W Vertex Shader przekształcimy współrzędne za pomocą macierzy transformacji, aby zastosować obrót i powiększenie oraz obliczyć kolor. W Fragment Shader przypiszemy kolor do fragmentu. Te programy cieniujące muszą być następnie skompilowane i połączone z kontekstem. OpenGL zapewnia proste sposoby łączenia dwóch środowisk, dzięki czemu parametry wewnątrz programu mogą być dostępne lub przypisywane z zewnątrz:

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

W kodzie Vertex Shader obliczamy nową pozycję wierzchołka, stosując macierz transformacji do oryginalnych wierzchołków:

 gl_Position = matrixVertex * vec4(coordVertexes, 1.0);

Aby obliczyć tę macierz transformacji, obliczamy kilka oddzielnych macierzy: skalę ekranu, translację sceny, skalę, obrót i środek. Następnie znajdujemy iloczyn tych macierzy, aby obliczyć ostateczną macierz transformacji. Zacznij od przetłumaczenia środka modelu na początek (0, 0, 0), który jest również środkiem ekranu. Obrót jest określany przez interakcję użytkownika ze sceną za pomocą jakiegoś urządzenia wskazującego. Użytkownik może kliknąć scenę i przeciągnąć, aby obrócić. Gdy użytkownik kliknie, zapamiętujemy pozycję kursora, a po ruchu mamy drugą pozycję kursora. Używając tych dwóch współrzędnych, wraz ze środkiem sceny, tworzymy trójkąt. Po wykonaniu kilku prostych obliczeń możemy określić kąt obrotu i zaktualizować naszą macierz obrotu, aby odzwierciedlić tę zmianę. W przypadku skalowania po prostu polegamy na kółku myszy, aby zmodyfikować współczynnik skalowania osi X i Y widżetu OpenGL. Model jest cofany o 0,5, aby utrzymać go za płaszczyzną, z której renderowana jest scena. Wreszcie, aby zachować naturalne proporcje, musimy dostosować zmniejszenie rozszerzenia modelu wzdłuż dłuższego boku (w przeciwieństwie do sceny OpenGL, widżet, w którym jest renderowany, może mieć różne wymiary fizyczne wzdłuż obu osi). Łącząc to wszystko, obliczamy ostateczną macierz transformacji w następujący sposób:

 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; }

Wniosek

W tym wprowadzeniu do renderowania 3D OpenGL zbadaliśmy jedną z technologii, które pozwalają firmie ud wykorzystać naszą kartę graficzną do renderowania modelu 3D. Jest to znacznie bardziej wydajne niż używanie cykli procesora w tym samym celu. Zastosowaliśmy bardzo prostą technikę cieniowania i uczyniliśmy scenę interaktywną poprzez obsługę danych wejściowych użytkownika z myszy. Uniknęliśmy używania magistrali pamięci wideo do przesyłania danych tam iz powrotem między pamięcią wideo a programem. Mimo że wyrenderowaliśmy tylko jedną linię tekstu w 3D, bardziej skomplikowane sceny można renderować w bardzo podobny sposób.

Aby być uczciwym, ten samouczek ledwo zarysował powierzchnię modelowania i renderowania 3D. To obszerny temat, a ten samouczek OpenGL nie może twierdzić, że to wszystko, co musisz wiedzieć, aby móc tworzyć gry 3D lub oprogramowanie do modelowania. Jednak celem tego artykułu jest zerknięcie w tę dziedzinę i pokazanie, jak łatwo można zacząć korzystać z OpenGL do tworzenia aplikacji 3D.