บทนำสู่ OpenGL: บทช่วยสอนการแสดงผลข้อความ 3 มิติ
เผยแพร่แล้ว: 2022-03-11ด้วยเครื่องมือที่มีอยู่อย่าง DirectX และ OpenGL การเขียนแอปพลิเคชันเดสก์ท็อปที่แสดงองค์ประกอบ 3 มิติจึงไม่ใช่เรื่องยากในปัจจุบัน อย่างไรก็ตาม เช่นเดียวกับเทคโนโลยีอื่นๆ บางครั้งก็มีอุปสรรคที่ทำให้นักพัฒนาพยายามเข้าสู่ช่องนี้ได้ยาก เมื่อเวลาผ่านไป การแข่งขันระหว่าง DirectX และ OpenGL ทำให้เทคโนโลยีเหล่านี้เข้าถึงได้ง่ายขึ้นสำหรับนักพัฒนา พร้อมกับเอกสารประกอบที่ดีขึ้นและกระบวนการที่ง่ายขึ้นในการเป็นนักพัฒนา DirectX หรือ OpenGL ที่มีทักษะ
DirectX ซึ่งเปิดตัวและดูแลโดย Microsoft เป็นเทคโนโลยีเฉพาะสำหรับแพลตฟอร์ม Windows ในทางกลับกัน OpenGL เป็น API แบบข้ามแพลตฟอร์มสำหรับเวทีกราฟิก 3 มิติที่ Khronos Group รักษาข้อกำหนดไว้
ในการแนะนำ OpenGL นี้ ฉันจะอธิบายวิธีเขียนแอปพลิเคชันง่ายๆ เพื่อแสดงโมเดลข้อความ 3 มิติ เราจะใช้ Qt/Qt Creator เพื่อใช้ UI ทำให้ง่ายต่อการคอมไพล์และเรียกใช้แอปพลิเคชันนี้บนหลายแพลตฟอร์ม ซอร์สโค้ดของต้นแบบที่สร้างขึ้นสำหรับบทความนี้มีอยู่ใน GitHub
เป้าหมายของแอปพลิเคชันง่ายๆ นี้คือการสร้างโมเดล 3 มิติ บันทึกลงในไฟล์ที่มีรูปแบบเรียบง่าย และเปิดและแสดงผลบนหน้าจอ โมเดล 3 มิติในฉากที่เรนเดอร์จะหมุนและซูมได้ เพื่อให้รู้สึกถึงความลึกและมิติที่ดีขึ้น
ข้อกำหนดเบื้องต้น
ก่อนเริ่มต้น เราจะต้องเตรียมสภาพแวดล้อมการพัฒนาของเราด้วยเครื่องมือที่มีประโยชน์สำหรับโครงการนี้ สิ่งแรกที่เราต้องการคือเฟรมเวิร์ก Qt และยูทิลิตี้ที่เกี่ยวข้อง ซึ่งสามารถดาวน์โหลดได้จาก www.qt.io นอกจากนี้ยังอาจใช้งานได้ผ่านตัวจัดการแพ็คเกจมาตรฐานของระบบปฏิบัติการของคุณ หากเป็นกรณีนี้ คุณอาจต้องการลองกับมันก่อน บทความนี้ต้องการความคุ้นเคยกับกรอบงาน Qt อย่างไรก็ตาม ถ้าคุณไม่คุ้นเคยกับเฟรมเวิร์ก โปรดอย่ารู้สึกท้อที่จะปฏิบัติตาม เนื่องจากต้นแบบนั้นอาศัยคุณสมบัติที่ค่อนข้างเล็กน้อยของเฟรมเวิร์ก
คุณยังสามารถใช้ Microsoft Visual Studio 2013 บน Windows ได้อีกด้วย ในกรณีนั้น โปรดตรวจสอบให้แน่ใจว่าคุณกำลังใช้ Qt Addin ที่เหมาะสมสำหรับ Visual Studio
ณ จุดนี้ คุณอาจต้องการโคลนที่เก็บจาก GitHub และทำตามเมื่อคุณอ่านบทความนี้
ภาพรวม OpenGL
เราจะเริ่มต้นด้วยการสร้างโครงการแอปพลิเคชัน Qt อย่างง่ายด้วยวิดเจ็ตเอกสารเดียว เนื่องจากเป็นวิดเจ็ตแบบไร้กระดูก การคอมไพล์และรันจะไม่ก่อให้เกิดประโยชน์ใดๆ ด้วยตัวออกแบบ Qt เราจะเพิ่มเมนู "ไฟล์" ที่มีสี่รายการ: "ใหม่ ... ", "เปิด ... ", "ปิด" และ "ออก" คุณสามารถค้นหาโค้ดที่เชื่อมโยงรายการเมนูเหล่านี้กับการดำเนินการที่เกี่ยวข้องในที่เก็บ
การคลิกที่ "ใหม่ ... " ควรป๊อปอัปกล่องโต้ตอบที่จะมีลักษณะดังนี้:
ที่นี่ ผู้ใช้สามารถป้อนข้อความ เลือกแบบอักษร ปรับแต่งความสูงของโมเดลที่ได้ และสร้างแบบจำลอง 3 มิติ การคลิกที่ "สร้าง" จะเป็นการบันทึกโมเดล และควรเปิดขึ้นหากผู้ใช้เลือกตัวเลือกที่เหมาะสมจากมุมล่างซ้าย อย่างที่คุณบอกได้ เป้าหมายที่นี่คือการแปลงข้อความที่ผู้ใช้ป้อนเป็นโมเดล 3 มิติและแสดงผลบนจอแสดงผล
โปรเจ็กต์จะมีโครงสร้างที่เรียบง่าย และส่วนประกอบจะถูกแบ่งออกเป็นไฟล์ C++ และส่วนหัวจำนวนหนึ่ง:
createcharmodeldlg.h/cpp
ไฟล์มีวัตถุที่ได้รับ QDialog การดำเนินการนี้ใช้วิดเจ็ตไดอะล็อกที่อนุญาตให้ผู้ใช้พิมพ์ข้อความ เลือกแบบอักษร และเลือกว่าจะบันทึกผลลัพธ์ลงในไฟล์และ/หรือแสดงในรูปแบบ 3 มิติ
gl_widget.h/cpp
มีการใช้งานวัตถุที่ได้รับ QOpenGLWidget วิดเจ็ตนี้ใช้เพื่อแสดงฉาก 3 มิติ
mainwindow.h/cpp
มีการนำวิดเจ็ตแอปพลิเคชันหลักไปใช้งาน ไฟล์เหล่านี้ไม่เปลี่ยนแปลงเนื่องจากสร้างขึ้นโดยวิซาร์ด Qt Creator
main.cpp
ประกอบด้วยฟังก์ชัน main(…) ซึ่งสร้างวิดเจ็ตแอปพลิเคชันหลักและแสดงบนหน้าจอ
model2d_processing.h/cpp
มีฟังก์ชันการสร้างฉาก 2 มิติ
model3d.h/cpp
ประกอบด้วยโครงสร้างที่เก็บอ็อบเจ็กต์โมเดล 3 มิติและอนุญาตให้ดำเนินการกับอ็อบเจ็กต์ (บันทึก โหลด ฯลฯ)
model_creator.h/cpp
มีการใช้งานคลาสที่อนุญาตให้สร้างวัตถุโมเดลฉาก 3 มิติ
การใช้งาน OpenGL
เพื่อความกระชับ เราจะข้ามรายละเอียดที่ชัดเจนของการใช้อินเทอร์เฟซผู้ใช้กับ Qt Designer และโค้ดที่กำหนดพฤติกรรมขององค์ประกอบแบบโต้ตอบ มีบางแง่มุมที่น่าสนใจมากขึ้นของแอปพลิเคชันต้นแบบนี้ ซึ่งไม่เพียงแต่มีความสำคัญ แต่ยังเกี่ยวข้องกับการเข้ารหัสและการเรนเดอร์โมเดล 3 มิติที่เราต้องการจะกล่าวถึง ตัวอย่างเช่น ขั้นตอนแรกของการแปลงข้อความเป็นโมเดล 3 มิติในต้นแบบนี้เกี่ยวข้องกับการแปลงข้อความเป็นภาพขาวดำ 2 มิติ เมื่อสร้างภาพนี้แล้ว คุณจะทราบได้ว่าพิกเซลใดของรูปภาพประกอบเป็นข้อความ และพิกเซลใดเป็นเพียงพื้นที่ "ว่างเปล่า" มีบางวิธีที่ง่ายกว่าในการแสดงข้อความพื้นฐานโดยใช้ OpenGL แต่เรากำลังใช้แนวทางนี้เพื่อให้ครอบคลุมรายละเอียดที่สำคัญบางประการของการเรนเดอร์ 3D ด้วย OpenGL
ในการสร้างภาพนี้ เราสร้างอินสแตนซ์อ็อบเจ็กต์ QImage ด้วยแฟล็ก QImage::Format_Mono เนื่องจากสิ่งที่เราต้องรู้คือพิกเซลใดเป็นส่วนหนึ่งของข้อความและพิกเซลใดไม่ใช่ภาพขาวดำจึงควรใช้ได้ดี เมื่อผู้ใช้ป้อนข้อความ เราจะอัปเดตวัตถุ QImage นี้พร้อมกัน ตามขนาดแบบอักษรและความกว้างของรูปภาพ เราพยายามอย่างเต็มที่เพื่อให้ข้อความอยู่ในความสูงที่ผู้ใช้กำหนด
ต่อไป เราจะแจกแจงพิกเซลทั้งหมดซึ่งเป็นส่วนหนึ่งของข้อความ - ในกรณีนี้คือพิกเซลสีดำ แต่ละพิกเซลที่นี่ถือเป็นหน่วยสี่เหลี่ยมจัตุรัสที่แยกจากกัน จากสิ่งนี้ เราสามารถสร้างรายการสามเหลี่ยม คำนวณพิกัดของจุดยอด และจัดเก็บไว้ในไฟล์แบบจำลอง 3 มิติของเรา
ตอนนี้เรามีรูปแบบไฟล์โมเดล 3 มิติอย่างง่ายแล้ว เราสามารถเริ่มเน้นที่การแสดงผลได้ สำหรับการเรนเดอร์ 3D บน OpenGL Qt จัดเตรียมวิดเจ็ตที่เรียกว่า QOpenGLWidget ในการใช้วิดเจ็ตนี้ ฟังก์ชันสามอย่างอาจถูกแทนที่:
- initializeGl() - นี่คือที่ที่รหัสเริ่มต้นจะไป
- paintGl() - วิธีการนี้ถูกเรียกทุกครั้งที่วิดเจ็ตถูกวาดใหม่
- resizeGl(int w, int h) - วิธีการนี้ถูกเรียกด้วยความกว้างและความสูงของวิดเจ็ตทุกครั้งที่ปรับขนาด
เราจะเริ่มต้นวิดเจ็ตด้วยการตั้งค่าการกำหนดค่า shader ที่เหมาะสมในวิธี 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 มีการใช้ shaders โดยใช้ภาษาที่เรียกว่า GLSL ภาษาได้รับการออกแบบมาเพื่อให้ง่ายต่อการจัดการข้อมูล 3 มิติก่อนที่จะแสดงผล ที่นี่เราต้องการตัวแรเงาสองตัว: จุดสุดยอดและตัวแยกส่วน ใน vertex shader เราจะแปลงพิกัดด้วยเมทริกซ์การแปลงเพื่อใช้การหมุนและการซูม และเพื่อคำนวณสี ใน Fragment Shader เราจะกำหนดสีให้กับ Fragment โปรแกรม shader เหล่านี้จะต้องรวบรวมและเชื่อมโยงกับบริบท OpenGL มีวิธีง่ายๆ ในการเชื่อมโยงทั้งสองสภาพแวดล้อม เพื่อให้สามารถเข้าถึงหรือกำหนดพารามิเตอร์ภายในโปรแกรมจากภายนอกได้:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
ในโค้ด vertex shader เราคำนวณตำแหน่งจุดยอดใหม่โดยใช้เมทริกซ์การแปลงบนจุดยอดเดิม:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
ในการคำนวณเมทริกซ์การแปลงนี้ เราคำนวณเมทริกซ์แยกกันสองสามตัว: มาตราส่วนหน้าจอ แปลฉาก มาตราส่วน หมุน และกึ่งกลาง จากนั้นเราจะหาผลคูณของเมทริกซ์เหล่านี้เพื่อคำนวณเมทริกซ์การแปลงสุดท้าย เริ่มด้วยการแปล model center เป็นจุดเริ่มต้น (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; }
บทสรุป
ในบทนำสู่การเรนเดอร์ OpenGL 3D เราได้สำรวจหนึ่งในเทคโนโลยีที่ช่วยให้ ud ใช้การ์ดวิดีโอของเราในการเรนเดอร์โมเดล 3 มิติ สิ่งนี้มีประสิทธิภาพมากกว่าการใช้รอบ CPU เพื่อจุดประสงค์เดียวกัน เราใช้เทคนิคการแรเงาที่ง่ายมาก และทำให้ฉากโต้ตอบผ่านการจัดการอินพุตของผู้ใช้จากเมาส์ เราหลีกเลี่ยงการใช้บัสหน่วยความจำวิดีโอเพื่อส่งข้อมูลไปมาระหว่างหน่วยความจำวิดีโอและโปรแกรม แม้ว่าเราจะสร้างข้อความเพียงบรรทัดเดียวในแบบ 3 มิติ แต่ฉากที่ซับซ้อนกว่าก็สามารถแสดงผลได้ในลักษณะที่คล้ายคลึงกันมาก
เพื่อความเป็นธรรม บทช่วยสอนนี้แทบไม่ได้ขีดข่วนพื้นผิวของการสร้างแบบจำลองและการเรนเดอร์ 3 มิติ นี่เป็นหัวข้อที่กว้างใหญ่ และบทช่วยสอน OpenGL นี้ไม่สามารถอ้างได้ว่านี่คือทั้งหมดที่คุณต้องรู้จึงจะสามารถสร้างเกม 3 มิติหรือซอฟต์แวร์สร้างแบบจำลองได้ อย่างไรก็ตาม บทความนี้มีจุดประสงค์เพื่อให้คุณได้มองเห็นขอบเขตนี้ และแสดงให้เห็นว่าคุณสามารถเริ่มต้นใช้งาน OpenGL เพื่อสร้างแอปพลิเคชัน 3D ได้ง่ายเพียงใด