OpenGL 소개: 3D 텍스트 렌더링 튜토리얼
게시 됨: 2022-03-11DirectX 및 OpenGL과 같은 도구를 사용할 수 있으므로 3D 요소를 렌더링하는 데스크톱 응용 프로그램을 작성하는 것이 오늘날에는 그리 어렵지 않습니다. 그러나 많은 기술과 마찬가지로 때때로 개발자가 이 틈새 시장에 진입하는 것을 어렵게 만드는 장애물이 있습니다. 시간이 지남에 따라 DirectX와 OpenGL 간의 경쟁으로 인해 개발자가 이러한 기술에 더 쉽게 액세스할 수 있게 되었으며, 더 나은 문서와 숙련된 DirectX 또는 OpenGL 개발자가 되기 위한 더 쉬운 프로세스가 가능해졌습니다.
Microsoft에서 도입 및 유지 관리하는 DirectX는 Windows 플랫폼 전용 기술입니다. 반면에 OpenGL은 크로노스 그룹에서 사양을 유지 관리하는 3D 그래픽 분야용 크로스 플랫폼 API입니다.
이 OpenGL 소개에서는 3D 텍스트 모델을 렌더링하는 매우 간단한 응용 프로그램을 작성하는 방법을 설명합니다. Qt/Qt Creator를 사용하여 UI를 구현하여 여러 플랫폼에서 이 애플리케이션을 쉽게 컴파일하고 실행할 수 있습니다. 이 기사를 위해 빌드된 프로토타입의 소스 코드는 GitHub에서 사용할 수 있습니다.
이 간단한 응용 프로그램의 목표는 3D 모델을 생성하고 간단한 형식의 파일로 저장하고 화면에서 열어 렌더링하는 것입니다. 렌더링된 장면의 3D 모델은 더 나은 깊이와 차원을 제공하기 위해 회전 및 확대/축소가 가능합니다.
전제 조건
시작하기 전에 이 프로젝트에 유용한 도구로 개발 환경을 준비해야 합니다. 가장 먼저 필요한 것은 www.qt.io에서 다운로드할 수 있는 Qt 프레임워크 및 관련 유틸리티입니다. 운영 체제의 표준 패키지 관리자를 통해서도 사용할 수 있습니다. 이 경우 먼저 시도해 볼 수 있습니다. 이 기사는 Qt 프레임워크에 대해 어느 정도 익숙해야 합니다. 그러나 프레임워크에 익숙하지 않은 경우 프로토타입이 프레임워크의 몇 가지 사소한 기능에 의존하므로 따라하는 것을 낙담하지 마십시오.
Windows에서 Microsoft Visual Studio 2013을 사용할 수도 있습니다. 이 경우 Visual Studio용으로 적절한 Qt Addin을 사용하고 있는지 확인하십시오.
이 시점에서 GitHub에서 리포지토리를 복제하고 이 문서를 읽으면서 따라할 수 있습니다.
OpenGL 개요
단일 문서 위젯으로 간단한 Qt 응용 프로그램 프로젝트를 만드는 것으로 시작하겠습니다. 기본 위젯이기 때문에 컴파일하고 실행해도 유용한 것이 생성되지 않습니다. Qt 디자이너를 사용하여 “새로 만들기…”, “열기…”, “닫기” 및 “종료”의 네 가지 항목이 있는 “파일” 메뉴를 추가합니다. 이러한 메뉴 항목을 저장소에서 해당 작업에 바인딩하는 코드를 찾을 수 있습니다.
"새로 만들기..."를 클릭하면 다음과 같은 대화 상자가 나타납니다.
여기에서 사용자는 일부 텍스트를 입력하고 글꼴을 선택하고 결과 모델 높이를 조정하고 3D 모델을 생성할 수 있습니다. "만들기"를 클릭하면 모델이 저장되고 사용자가 왼쪽 하단 모서리에서 적절한 옵션을 선택한 경우에도 열어야 합니다. 알 수 있듯이 여기의 목표는 일부 사용자 입력 텍스트를 3D 모델로 변환하고 디스플레이에 렌더링하는 것입니다.
프로젝트는 간단한 구조를 가지며 구성 요소는 소수의 C++ 및 헤더 파일로 나뉩니다.
createcharmodeldlg.h/cpp
파일에는 QDialog 파생 객체가 포함되어 있습니다. 이것은 사용자가 텍스트를 입력하고 글꼴을 선택하고 결과를 파일에 저장할지 및/또는 3D로 표시할지 여부를 선택할 수 있는 대화 상자 위젯을 구현합니다.
gl_widget.h/cpp
QOpenGLWidget 파생 객체의 구현을 포함합니다. 이 위젯은 3D 장면을 렌더링하는 데 사용됩니다.
메인윈도우.h/cpp
기본 애플리케이션 위젯의 구현을 포함합니다. 이 파일은 Qt Creator 마법사에 의해 생성된 이후로 변경되지 않은 상태로 남아 있습니다.
메인.cpp
메인 애플리케이션 위젯을 생성하고 화면에 보여주는 main(…) 함수를 포함합니다.
model2d_processing.h/cpp
2D 장면 생성 기능이 포함되어 있습니다.
model3d.h/cpp
3D 모델 개체를 저장하고 작업(저장, 로드 등)을 수행할 수 있는 구조를 포함합니다.
model_creator.h/cpp
3D 장면 모델 객체를 생성할 수 있는 클래스 구현을 포함합니다.
OpenGL 구현
간결함을 위해 우리는 Qt Designer로 사용자 인터페이스를 구현하는 명백한 세부 사항과 대화식 요소의 동작을 정의하는 코드를 건너뛸 것입니다. 이 프로토타입 응용 프로그램에는 중요할 뿐만 아니라 우리가 다루고자 하는 3D 모델 인코딩 및 렌더링과 관련된 몇 가지 더 흥미로운 측면이 분명히 있습니다. 예를 들어, 이 프로토타입에서 텍스트를 3D 모델로 변환하는 첫 번째 단계는 텍스트를 2D 흑백 이미지로 변환하는 것입니다. 이 이미지가 생성되면 이미지의 어떤 픽셀이 텍스트를 형성하고 어떤 픽셀이 "빈" 공간인지 알 수 있습니다. OpenGL을 사용하여 기본 텍스트를 렌더링하는 더 간단한 방법이 있지만 OpenGL을 사용한 3D 렌더링의 핵심 세부 정보를 다루기 위해 이 접근 방식을 사용합니다.
이 이미지를 생성하기 위해 QImage::Format_Mono 플래그로 QImage 객체를 인스턴스화합니다. 우리가 알아야 할 것은 텍스트의 일부인 픽셀과 그렇지 않은 픽셀뿐이므로 흑백 이미지는 잘 작동해야 합니다. 사용자가 텍스트를 입력하면 이 QImage 객체를 동기적으로 업데이트합니다. 글꼴 크기와 이미지 너비를 기준으로 사용자가 정의한 높이 내에서 텍스트가 맞도록 최선을 다합니다.
다음으로 텍스트의 일부인 모든 픽셀(이 경우 검은색 픽셀)을 열거합니다. 여기에서 각 픽셀은 별도의 정사각형 단위로 처리됩니다. 이를 기반으로 삼각형 목록을 생성하고 정점 좌표를 계산하고 3D 모델 파일에 저장할 수 있습니다.
이제 간단한 3D 모델 파일 형식이 있으므로 렌더링에 집중할 수 있습니다. OpenGL 기반 3D 렌더링을 위해 Qt는 QOpenGLWidget이라는 위젯을 제공합니다. 이 위젯을 사용하려면 세 가지 기능을 재정의할 수 있습니다.
- initializeGl() - 초기화 코드가 들어가는 곳입니다.
- paintGl() - 이 메소드는 위젯이 다시 그려질 때마다 호출됩니다.
- resizeGl(int w, int h) - 이 메소드는 크기가 조정될 때마다 위젯의 너비와 높이로 호출됩니다.
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);
성능 향상을 위해 프로토타입 애플리케이션에서 VBO(Vertex Buffer Object)를 사용했습니다. 이를 통해 비디오 메모리에 데이터를 저장하고 렌더링에 직접 사용할 수 있습니다. 이에 대한 다른 방법은 렌더링 코드에서 데이터(정점 좌표, 법선 및 색상)를 제공하는 것입니다.

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)으로 변환하여 시작합니다. 회전은 일부 포인팅 장치를 사용하여 장면과 사용자의 상호 작용에 의해 결정됩니다. 사용자는 장면을 클릭하고 주위를 드래그하여 회전할 수 있습니다. 사용자가 클릭하면 커서 위치가 저장되고 이동 후에는 두 번째 커서 위치가 저장됩니다. 장면 중심과 함께 이 두 좌표를 사용하여 삼각형을 형성합니다. 몇 가지 간단한 계산에 따라 회전 각도를 결정할 수 있고 이 변경 사항을 반영하도록 회전 행렬을 업데이트할 수 있습니다. 크기 조정을 위해 우리는 단순히 마우스 휠에 의존하여 OpenGL 위젯의 X 및 Y 축 크기 조정 요소를 수정합니다. 모델은 장면이 렌더링되는 평면 뒤에 유지하기 위해 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; }
결론
OpenGL 3D 렌더링에 대한 이 소개에서 우리는 ud가 3D 모델을 렌더링하기 위해 비디오 카드를 활용할 수 있게 해주는 기술 중 하나를 탐구했습니다. 이것은 같은 목적으로 CPU 사이클을 사용하는 것보다 훨씬 더 효율적입니다. 우리는 매우 간단한 셰이딩 기술을 사용했으며 마우스의 사용자 입력 처리를 통해 장면을 인터랙티브하게 만들었습니다. 비디오 메모리 버스를 사용하여 비디오 메모리와 프로그램 간에 데이터를 주고받는 것을 피했습니다. 3D에서 한 줄의 텍스트를 렌더링했지만 더 복잡한 장면도 매우 유사한 방식으로 렌더링할 수 있습니다.
공정하게 말하면 이 튜토리얼은 3D 모델링 및 렌더링의 표면을 거의 긁지 않았습니다. 이것은 방대한 주제이며 이 OpenGL 튜토리얼에서는 이것이 3D 게임이나 모델링 소프트웨어를 구축하기 위해 알아야 할 전부라고 주장할 수 없습니다. 그러나 이 기사의 목적은 이 영역을 살짝 들여다보고 OpenGL을 시작하여 3D 응용 프로그램을 빌드하는 방법을 보여주는 것입니다.