Dev Log: رندر بسیار ابتدایی OpenGL
برخی از پس زمینه
از ابتدای سفر برنامه نویسی من، همیشه به سمت ایده ایجاد پروژه ها از ابتدا با حداقل کد شخص ثالث کشیده شده ام.
یکی از آن پروژه ها بازی با گرافیک بود و این بار در واقع به آن گیر دادم.
شروع پروژه
تصمیم گرفتم آموزش OpenGL را دنبال کنم. اگرچه آن را برای کد مفید یافتم، اما برخی از توضیحات آن با من کلیک نکردند.
من در مورد اینکه چگونه اشیاء آرایه رئوس (VAO) و اشیاء بافر رأس (VBOs) با هم تطبیق میشوند، گیج شده بودم، و با توجه به اینکه آنها بلوکهای اصلی OpenGL هستند، در وضعیت بدی قرار گرفتم. در نهایت، پس از هزاران جستجو در گوگل و یوتیوب، در نهایت کلیک کرد.
فکر میکنم دلیل این همه مشکل این بود که نمیدانستم VAO و VBO اساساً به هم مرتبط هستند. ببینید، VAO ها نه تنها طرح مشخص شده VBO ها را نگه می دارند، بلکه خود داده های VBO را نیز نگه می دارند. این یک جور عجیب است (به لطف دستگاه دولتی!)، و نسخههای جدیدتر OpenGL قالببندی دادهها و اتصال واقعی دادهها را جدا میکنند (به دسترسی مستقیم به حالت (DSA) مراجعه کنید).
ساختن انتزاعات
وقتی فهمیدم که VAO و VBO را نمی توان از هم جدا کرد (در نسخه های OpenGL زیر 4.5)، این باعث شد که برنامه نویسی یک انتزاع در اطراف آنها نسبتاً بی اهمیت باشد.
من با ابتدایی ترین داده ها، راس شروع کردم. من برای اولین بار در OpenGL به بافت ها یا حتی نورپردازی اهمیتی نمی دهم، بنابراین در حال حاضر رئوس من فقط حاوی اطلاعات موقعیت و اطلاعات رنگ است.
struct Vertex
{
std::array<GLfloat, 3> position;
std::array<GLfloat, 3> color;
// constructor and std::ostream& operator<< defined here
};
عالی! من راهی برای نشان دادن یک راس داشتم، اما شما نمی توانید کار زیادی با آن انجام دهید.
بنابراین، من یک کلاس برای نمایش گروهی از رئوس ایجاد کردم.
struct Mesh
{
std::vector<Vertex> vertices;
// constructors
};
چیزی کم است ببینید، در OpenGL، چون همه چیز به عنوان مجموعهای از مثلثها تعریف میشود، گاهی اوقات همپوشانی دارند. صورت یک مکعب را تصور کنید:
1-----2
| |
| |
3-----4
دو مثلث تشکیل دهنده صورت عبارتند از: 123، 324.
این نوعی هدر دادن است، درست است؟ ما نباید راس های تکراری را تعریف کنیم.
خوشبختانه، OpenGL با معرفی اشیاء بافر عنصر (EBOs) این مشکل را حل می کند. EBO ها شاخص ها را نگه می دارند، به این معنی که ما نیازی به کپی کردن رئوس خود در حافظه نداریم.
بنابراین، جدید Mesh
ساختار به شکل زیر است:
struct Mesh
{
std::vector<Vertex> vertices;
std::vector<GLushort> indices;
// constructors
};
عالی! اکنون می توانم رئوس و شاخص های یک مش را نشان دهم.
اما اکنون من به راهی برای ارسال این رئوس و شاخص ها به OpenGL نیاز دارم.
بنابراین، من یک را ایجاد کردم Model
کلاس این کلاس نشان دهنده a است Mesh
و همچنین داده های OpenGL آن، از جمله VBOs، VAO، و EBO.
class Model
{
Mesh m_mesh;
GLuint m_vao;
GLuint m_vbos[2];
GLuint m_ebo;
// constructors, convenience functions, destructor
};
در نهایت، من به راهی برای ترسیم واقعی نیاز داشتم Model
به صفحه نمایش و پیگیری مدل ها.
تصمیم گرفتم یک را ایجاد کنم Renderer
کلاس برای پیگیری ماتریس های دوربین و رسیدگی به تمام نقاشی ها. به این ترتیب، من می توانستم خود را حفظ کنم Model
جدا از کد ترسیمی
class Renderer
{
public:
std::list<std::unique_ptr<Model>> models;
int mode; // GL_POINTS, GL_TRIANGLES, or GL_LINES
// other members excluded for brevity, but shader is included
// constructor
// function to add model to models
void draw_models()
{
// clear the screen, color buffer, and depth buffer
// use the shader
// setup view matrices
// setup shader uniforms
for (const auto& model : models)
{
glBindVertexArray(model->m_vao);
glDrawElements(mode, model-m_mesh.indices.size(), GL_UNSIGNED_SHORT, 0);
glBindVertexArray(0);
}
}
};
بارگیری مدل ها از فایل ها
من پایه یک رندر را داشتم، اما اگر تمام مدت به یک صفحه خالی خیره شده باشم، بسیار خسته کننده است.
بنابراین، تصمیم گرفتم یک تابع برای بارگذاری a ایجاد کنم .obj
فایل به a Model
.
خوشبختانه، فرمت فایل نسبتاً ساده است، چیزی شبیه به این است:
v 0.5. 0.5 0.5
...
vt 1.0 1.0
...
vn 1.0 1.0 1.0
...
f 1 2 3
v
موقعیت های رأس (x، y، z) را نشان می دهد.vt
مختصات بافت را نشان می دهد، که برای ساده نگه داشتن چیزها نادیده گرفتم.vn
نشان دهنده نرمال های سطحی است که من هم نادیده گرفته ام.f
نشان دهنده شاخص های مثلث است.
بنابراین، من یک تابع ایجاد کردم تا در تمام خطوط حلقه بزند و یا رئوس و شاخص ها را به بردارها اضافه کنم.
با این حال، من به دو اشکال ظریف برخورد کردم که به راحتی قابل رفع بودند:
- شاخص ها با 0 اندیس گذاری شده اند نه 1، بنابراین هنگام اضافه کردن شاخص ها به بردار باید 1 را کم می کردم.
- برخی از خطوط صورت در واقع به جای مثلث، چهارگوش را مشخص می کنند، بنابراین خط به نظر می رسد:
f 1 2 3 4
پس از رفع دو باگ، من یک قابل عبور داشتم .obj
لودری که میتونستم استفاده کنم
پایان کار
پس از انجام این کار، تصمیم گرفتم برخی از آرگومان های خط فرمان را اضافه کنم، عمدتاً برای مشخص کردن آن .obj
فایل برای بارگیری، حالت نمایش با (GL_POINTS، GL_LINES، یا GL_TRIANGLES) و امکان تغییر فاصله دوربین از مدل را مشخص کنید.
در اینجا قوری کلاسیک یوتا ارائه شده به عنوان یک قاب سیمی است:
در اینجا پیوند به مخزن GitHub است.
افکار نهایی و مراحل بعدی
به طور کلی، این یک پروژه سرگرم کننده بود. انجام کاری بصری خوب بود، و خوشحالم که بالاخره فهمیدم OpenGL چگونه کار می کند.
من به خصوص از کد، عمدتا تجزیه آرگومان و خام بودن راضی نیستم glfw
تماس هایی که من برقرار می کنم main()
، اما خوشحالم که کار می کند.
بیرون main()
من فکر میکنم انتزاعیهایی که روی راسها و خود OpenGL انجام دادم خیلی بد نیست، و در رندر آینده احتمالاً از این پایه استفاده خواهم کرد.
دو ویژگی وجود دارد که می خواهم به این رندر اضافه کنم:
- امکان تعیین رنگ مدل در خط فرمان args
- نورپردازی اولیه
وقتی (یا اگر) این کارها را انجام دادم، فکر میکنم پروژه را همانطور که هست ترک میکنم. این یک تجربه یادگیری عالی بود، و جالب خواهد بود که ببینیم رندرهای آینده من چگونه با این یکی مقایسه می شوند.
در آینده، احتمالاً سعی خواهم کرد Vulkan را بررسی کنم، من به این واقعیت علاقه مند هستم که API – اگرچه بسیار پرمخاطب تر – بسیار تمیزتر از OpenGL به نظر می رسد.
نکاتی برای سایر مبتدیان OpenGL
-
اگر مدیریت حافظه و نحوه عملکرد اشاره گرها را نمی دانید، سعی نکنید OpenGL را یاد بگیرید، زمان بدی خواهید داشت.
-
OpenGL 3.3 به دلیل دستگاه حالت آن یک API بد است. در صورت امکان، سعی کنید OpenGL 4.5+ را یاد بگیرید (بله، می دانم که آموزش ها کم هستند، اما یادگیری آن آسان تر به نظر می رسد)
-
VAO و VBO به دلیل معماری ماشین حالت به هم مرتبط هستند، هیچ راهی واقعی برای جدا کردن آنها وجود ندارد.