فصل ۲: برنامهنویسی گرافیکی
رویکرد این کتاب به گرافیک رایانهای، برنامهنویسیمحور است. بنابراین هدف این است که هرچه زودتر نوشتن برنامههای گرافیکی را آغاز کنید. برای این منظور ابتدا یک رابط برنامهنویسی کاربردی یا API حداقلی معرفی میشود. همین API برای برنامهنویسی بسیاری از مسائل جالب دوبعدی و سهبعدی و آشنایی با مفاهیم پایهٔ گرافیک کافی است.
گرافیک دوبعدی را حالت خاصی از گرافیک سهبعدی در نظر میگیریم. این دیدگاه اجازه میدهد کار را از همین فصل آغاز کنیم، هرچند مفاهیم سهبعدی را در اینجا فقط بهصورت مقدماتی لمس خواهیم کرد. کد دوبعدی ما بدون تغییر روی یک سامانهٔ سهبعدی اجرا میشود.
برای توسعهٔ مطالب از مسئلهای ساده اما آموزنده استفاده میکنیم: واشر سیرپینسکی (Sierpinski Gasket). این مثال نشان میدهد چگونه تنها با چند تابع گرافیکی میتوان تصویری جالب و، برای بسیاری از افراد، بهطور غیرمنتظره پیچیده تولید کرد. API مورد استفاده OpenGL است، اما بحث مفاهیم زیربنایی آنقدر عمومی است که بیشتر سامانههای نوین را پوشش میدهد. قابلیتهایی که در این فصل معرفی میشوند برای نوشتن برنامههای پایهٔ دوبعدی و سهبعدی که به تعامل کاربر نیاز ندارند کافیاند.
۲.۱ واشر سیرپینسکی
برای مسئلهٔ نمونه، رسم واشر سیرپینسکی را در نظر میگیریم؛ شکلی جالب با پیشینهای طولانی که در حوزههایی مانند هندسهٔ فراکتالی اهمیت دارد. واشر سیرپینسکی را میتوان هم بهصورت بازگشتی و هم بهصورت تصادفی تعریف کرد، اما در حد نهایی ویژگیهای آن اصلاً تصادفی نیست. کار را با نسخهٔ دوبعدی آغاز میکنیم؛ همانطور که در بخش ۲.۱۰ خواهیم دید، نسخهٔ سهبعدی تقریباً همان ساختار را دارد.
فرض کنید سه نقطه در فضا داریم. تا زمانی که این نقاط همخط نباشند، رأسهای یک مثلث یکتا را میسازند و یک صفحهٔ یکتا را نیز تعریف میکنند. فرض میکنیم این صفحه همان صفحهٔ z = 0 است و نقاط، در یک دستگاه مختصات مناسب، بهصورت (x1, y1, 0)، (x2, y2, 0) و (x3, y3, 0) مشخص شدهاند.
فرایند ساخت به این صورت است:
- یک نقطهٔ اولیه
p = (x, y, 0) را بهطور تصادفی درون مثلث انتخاب کنید.
- یکی از سه رأس را بهطور تصادفی انتخاب کنید.
- نقطهٔ
q را در نیمهٔ فاصلهٔ p تا رأس انتخابشده پیدا کنید.
q را با قراردادن یک نشانگر، مثلاً یک دایرهٔ کوچک، در مکان متناظر روی نمایشگر نشان دهید.
p را با q جایگزین کنید.
- به مرحلهٔ ۲ بازگردید.
بنابراین هر بار که نقطهٔ جدیدی میسازیم، آن را روی دستگاه خروجی نمایش میدهیم. این فرایند در شکل ۲.۱ نشان داده شده است؛ p0 نقطهٔ اولیه و p1 و p2 نخستین دو نقطهٔ تولیدشده توسط الگوریتماند.
شکل ۲.۱ — تولید واشر سیرپینسکی.
پیش از توسعهٔ برنامه، تلاش کنید حدس بزنید تصویر حاصل چگونه خواهد بود. آن را روی کاغذ بسازید؛ ممکن است از نتیجه شگفتزده شوید.
یک قالب ممکن برای برنامهٔ گرافیکی ما چنین است:
main( )
{
initialize_the_system();
p = find_initial_point();
for(some_number_of_points)
{
q = generate_a_point(p);
display_the_point(q);
p = q;
}
cleanup();
}
این قالب بهسادگی قابل تبدیل به یک برنامهٔ واقعی است. حتی در همین سطح انتزاع نیز دو گزینهٔ دیگر میتوان دید. شبهکد زیر را در نظر بگیرید:
main( )
{
initialize_the_system();
p = find_initial_point();
یادداشت ۱: در فصل ۳ مفهوم دستگاه مختصات به صورتبندی عمومیتر «چارچوب» (Frame) گسترش داده میشود.
for(some_number_of_points)
{
q = generate_a_point(p);
store_the_point(q);
p = q;
}
display_all_points();
cleanup();
}
در این الگوریتم ابتدا همهٔ نقاط را محاسبه و در یک آرایه یا ساختار دادهٔ دیگر قرار میدهیم. سپس همهٔ نقاط را با یک فراخوانی تابع نمایش میدهیم. این رویکرد سربار ارسال مقدارهای بسیار کوچک داده به پردازندهٔ گرافیکی برای هر نقطه را حذف میکند، اما به قیمت نیاز به ذخیرهٔ همهٔ دادهها تمام میشود.
راهبرد الگوریتم نخست «گرافیک حالت فوری» (Immediate Mode Graphics) نام دارد و تا سالهای اخیر روش استاندارد نمایش گرافیکی، بهویژه در کاربردهای نیازمند عملکرد تعاملی، بود. یکی از پیامدهای حالت فوری این است که دادهٔ هندسی در حافظه نگهداری نمیشود. در مثال نخست، اگر بخواهیم نقاط را دوباره نمایش دهیم باید کل فرایند تولید و نمایش را بار دیگر انجام دهیم.
در الگوریتم دوم، چون دادهها در یک ساختار داده ذخیره شدهاند، میتوان آنها را دوباره نمایش داد و حتی تغییراتی مانند عوضکردن رنگ یا اندازهٔ نقطهٔ نمایشدادهشده اعمال کرد؛ کافی است آرایه را بدون تولید دوبارهٔ نقاط ارسال کنیم. این شیوه «گرافیک حالت نگهداریشده» (Retained Mode Graphics) نام دارد و سابقهٔ آن به برخی از نخستین سختافزارهای نمایش گرافیکی ویژه بازمیگردد. معماری سامانههای گرافیکی جدید مبتنی بر GPU ما را به نسخهٔ سومی از برنامه میرساند.
رویکرد دوم یک ایراد اصلی دارد. فرض کنید مانند یک پویانمایی بخواهیم همان اشیا را بارها نمایش دهیم. هندسهٔ اشیا تغییر نکرده است، اما اشیا ممکن است در حال حرکت باشند. نمایش همهٔ نقاط به این معناست که هر بار برای نمایش جسم در مکان جدید، دادهها از CPU به GPU منتقل شوند. برای حجمهای بزرگ داده، همین انتقال اصلیترین گلوگاه فرایند نمایش میشود. طرح جایگزین زیر را در نظر بگیرید:
main( )
{
initialize_the_system();
p = find_initial_point();
for(some_number_of_points)
{
q = generate_a_point(p);
store_the_point(q);
p = q;
}
send_all_points_to_GPU();
display_data_on_GPU();
cleanup();
}
مانند قبل دادهها را در یک آرایه قرار میدهیم، اما اکنون فرایند نمایش به دو بخش شکسته شده است: ذخیرهٔ داده روی GPU و نمایش دادهٔ ذخیرهشده. اگر داده فقط یکبار نمایش داده شود مزیتی نسبت به روش قبلی نداریم؛ اما برای پویانمایی، داده از قبل روی GPU قرار دارد و نمایش مجدد به انتقال دوبارهٔ داده نیاز ندارد. تنها یک فراخوانی سادهٔ تابع لازم است تا مکان برخی دادههای فضاییِ توصیفکنندهٔ اشیای متحرک تغییر کند.
اگرچه برنامهٔ نهایی OpenGL سازمانی کمی متفاوت خواهد داشت، از همین راهبرد سوم پیروی میکند. برنامه را مرحلهبهمرحله توسعه میدهیم. ابتدا بر هستهٔ مسئله، یعنی تولید و نمایش نقاط، تمرکز میکنیم. دو پرسش باید پاسخ داده شوند:
- نقاط در فضا را چگونه نمایش دهیم؟
- نمایش دوبعدی، سهبعدی یا شکل دیگری را انتخاب کنیم؟
پس از پاسخ به این پرسشها میتوان هندسه را در قالبی مناسب برای رندر روی GPU قرار داد و سپس با استفاده از توان Shaderهای برنامهپذیر، نحوهٔ مشاهدهٔ اشیا را کنترل کرد.
۲.۲ برنامهنویسی کاربردهای دوبعدی
برای کاربردهای دوبعدی مانند واشر سیرپینسکی، هرچند میتوان از API مبتنی بر پلاتر قلمی استفاده کرد، چنین رویکردی محدودکننده است. بهجای آن از یک جهان سهبعدی آغاز میکنیم و سامانههای دوبعدی، مانند صفحهای که تصویر روی آن تولید میشود، را حالتهای خاص آن میدانیم. از دید ریاضی، یک صفحهٔ دوبعدی یا سطح خمیدهٔ دوبعدی ساده، زیرفضایی از فضای سهبعدی است. بنابراین گزارههای عملی و انتزاعی دربارهٔ جهان سهبعدی بزرگتر برای جهان دوبعدی سادهتر نیز برقرارند.
میتوان نقطهای روی صفحهٔ z = 0 را در جهان سهبعدی به صورت p = (x, y, 0) یا در صفحهٔ دوبعدی به صورت p = (x, y) نمایش داد. OpenGL، مانند بیشتر سامانههای گرافیکی سهبعدی، اجازه میدهد از هر یک از این دو نمایش استفاده شود و نمایش داخلی زیرین، مستقل از انتخاب کاربر، یکسان خواهد بود. روشهای مختلفی برای نمایش نقاط وجود دارد، اما سادهترین روش این است که یک نقطهٔ سهبعدی را سهتایی p = (x, y, z) یا ماتریس ستونی متناظر در نظر بگیریم که مؤلفههای آن مکان نقطه را مشخص میکنند. فعلاً میتوان پرسش مربوط به دستگاه مختصاتی که p در آن بیان شده است کنار گذاشت.
در OpenGL اصطلاحهای «رأس» (Vertex) و «نقطه» (Point) را با معنایی کمی متفاوت به کار میبریم. رأس یک موقعیت در فضاست؛ در گرافیک رایانهای از فضاهای دو، سه و چهاربعدی استفاده میکنیم. رأسها برای مشخصکردن بدویهای هندسی اتمی شناختهشده توسط سامانهٔ گرافیکی به کار میروند.
سادهترین بدوی هندسی یک نقطه در فضاست که معمولاً با یک رأس مشخص میشود. دو رأس میتوانند یک پارهخط را تعریف کنند؛ سه رأس میتوانند مثلث یا دایره را مشخص کنند؛ چهار رأس میتوانند یک چهارضلعی بسازند؛ و به همین ترتیب. دو رأس نیز ممکن است یک دایره یا مستطیل را توصیف کنند. سه رأس میتوانند سه نقطه یا دو پارهخط متصل را مشخص کنند و چهار رأس نیز میتوانند اشیای گوناگونی، از جمله دو مثلث، بسازند.
هستهٔ برنامهٔ واشر سیرپینسکی تولید نقاط است. برای تبدیل الگوریتم سوم به یک برنامهٔ عملی OpenGL باید کمی جزئیات بیشتر دربارهٔ OpenGL معرفی کنیم. هدف این است که با سادهترین برنامهٔ ممکن آغاز کنیم. یکی از سادهسازیها این است که بحث دستگاههای مختصات و تبدیل میان آنها را به تعویق بیندازیم و همهٔ دادههای قابل نمایش را درون مکعبی با مرکز مبدأ قرار دهیم که قطر آن از (-1, -1, -1) تا (1, 1, 1) امتداد دارد. این دستگاه که «مختصات برش» (Clip Coordinates) نامیده میشود همان دستگاهی است که Vertex Shader برای ارسال اطلاعات به رسترساز استفاده میکند. اشیای بیرون این مکعب حذف یا Clip میشوند و نمیتوانند روی نمایشگر ظاهر شوند. بعداً خواهیم آموخت هندسه را در برنامهٔ کاربردی با مختصات مناسبتر برای مسئله، یعنی «مختصات جسم» (Object Coordinates)، تعریف کنیم و با تبدیلها آن را به مختصات برش ببریم.
میتوانستیم برنامه را با یک آرایهٔ سادهٔ دو عضوی برای نگهداری مقادیر x و y هر نقطه بنویسیم، اما اگر ابتدا یک نوع دادهٔ نقطهٔ دوبعدی و عملیات مربوط به آن را تعریف کنیم کد بسیار روشنتر میشود. این کلاسها و عملگرها در فایل vec.h تعریف شدهاند. نوعهای موجود در vec.h و نوعهای دیگری که بعداً برای کلاسهای سه و چهاربعدی تعریف میشوند با نوعهای زبان سایهزنی OpenGL یا GLSL همخواناند و مثالهای کد را نسبت به استفاده از آرایههای معمولی خواناتر میکنند. افزون بر تعریف نوعهای جدید، vec.h و فایل همراه آن mat2.h، سازندهها و عملگرهای Overloadشدهای متناظر با GLSL نیز تعریف میکنند. بنابراین کدی مانند زیر میتواند هم در Shader و هم در برنامهٔ کاربردی ظاهر شود:
vec2 a = vec2(1.0, 2.0);
vec2 b = vec2(3.0, 4.0);
vec2 c = a + b;
نقاط را میتوان با عملگرهای معمول جریان cin و cout ورودی و خروجی کرد. به عضوهای منفرد نیز میتوان با عملگر عضویت، مانند p.x و p.y، یا با اندیسگذاری آرایهای، مانند p[0] و p[1]، دسترسی داشت.
یک افزودهٔ کوچک دیگر برنامهها را روشنتر میکند. بهجای استفادهٔ مستقیم از vec2 در GLSL، نوع point2 را تعریف میکنیم:
در vec.h، نوع vec2 بهصورت یک struct با دو عضو از نوع GLfloat تعریف شده است. در OpenGL اغلب از نوعهای پایهٔ خود OpenGL، مانند GLfloat و GLint، بهجای نوعهای متناظر C یعنی float و int استفاده میکنیم. این نوعها در فایلهای سرآیند OpenGL تعریف شدهاند و معمولاً تعریفشان بدیهی است؛ برای مثال:
بااینحال استفاده از نوعهای OpenGL انعطاف بیشتری برای پیادهسازی فراهم میکند؛ مثلاً اگر در یک پیادهسازی بخواهیم float را بدون تغییر برنامههای کاربردی موجود به double تبدیل کنیم.
کد زیر با آغاز از رأسهای مثلثی در صفحهٔ z = 0، تعداد ۵۰۰۰ نقطه تولید میکند:
#include "vec.h" // include point types and operations
#include <stdlib.h> //includes random number generator
typedef vec2 point2; //defines a point2 type identical to a vec2
void init()
{
const int NumPoints = 5000;
point2 points[NumPoints];
// A triangle in the plane z= 0
point2 vertices[3]={point2(-1.0,-1.0), point2(0.0,1.0),
point2(1.0,-1.0)};
// An arbitrary initial point inside the triangle
points[0] = point2(0.25, 0.50);
// compute and store NumPoints-1 new points
for(int k = 1; k < NumPoints; k++)
{
int j = rand() % 3; // pick a vertex at random
// Compute the point halfway between selected
// vertex and previous point
points[k] = (points[k-1]+vertices[j])/2.0;
}
}
چون هر نقطهای که تولید میکنیم باید داخل مثلث تعیینشده توسط این رأسها قرار گیرد، میدانیم هیچیک از نقاط تولیدشده در مرحلهٔ برش حذف نخواهد شد.
تابع rand() مولد استاندارد عدد تصادفی است که در هر فراخوانی یک عدد صحیح تصادفی تازه تولید میکند. با عملگر باقیمانده، این اعداد را به سه مقدار صحیح 0، 1 و 2 کاهش میدهیم. برای تعداد کمی تکرار، ویژگیهای دقیق مولد عدد تصادفی اهمیت حیاتی ندارند و مولدهای دیگر نیز باید دستکم بهخوبی rand عمل کنند.
قصد داریم نقاط را فقط یکبار تولید و سپس روی GPU قرار دهیم. بنابراین ایجاد آنها را بخشی از تابع مقداردهی اولیهٔ init در نظر میگیریم.
نقاط در مثال بهصورت دوبعدی تعریف شدند. میتوانستیم با افزودن مختصات z که همیشه صفر است، از نوعهای سهبعدی موجود در mat.h و vec.h نیز استفاده کنیم. تغییرهای کد بسیار اندک بود و شامل خطوطی مانند زیر میشد:
#include "vec.h" // three-dimensional type
typedef vec3 point3;
و در بخش مقداردهی اولیه:
point3 points [NumPoints];
point3 vertices[3] = {point3(-1.0,-1.0, 0.0), point3(0.0,1.0, 0.0),
point3(1.0,-1.0, 0.0)};
هرچند هنوز برنامهٔ کامل نداریم، شکل ۲.۲ خروجی مورد انتظار را نشان میدهد.
توجه کنید چون هر سه نقطهٔ ناهمخط یک صفحهٔ یکتا را مشخص میکنند، اگر بهجای آن از سه نقطهٔ (x1, y1, z1)، (x2, y2, z2) و (x3, y3, z3) همراه با یک نقطهٔ اولیه روی همان صفحه آغاز میکردیم، واشر در همان صفحهٔ تعیینشده توسط سه رأس اصلی تولید میشد.
اکنون هستهٔ برنامه نوشته شده است. داده داریم، اما هنوز آنها را روی GPU قرار ندادهایم و از GPU نخواستهایم چیزی نمایش دهد. حتی هنوز هیچ تابع OpenGL را معرفی نکردهایم. پیش از نمایش خروجی باید به پرسشهایی مانند موارد زیر پاسخ دهیم:
- با چه رنگهایی رسم میکنیم؟
- تصویر در کجای نمایشگر ظاهر میشود؟
- اندازهٔ تصویر چقدر خواهد بود؟
- چگونه ناحیهای از نمایشگر، یعنی یک پنجره، برای تصویر ایجاد کنیم؟
- چه مقدار از سطح رسم نامتناهی روی نمایشگر دیده خواهد شد؟
- تصویر چه مدت روی نمایشگر باقی میماند؟
پاسخ همهٔ این پرسشها مهم است، هرچند در ابتدا ممکن است حاشیهای به نظر برسند. کد پایهای که برای پاسخ به آنها و کنترل مکان و ظاهر رندرها توسعه میدهیم در برنامههای مختلف تغییر زیادی نخواهد کرد، بنابراین این تلاش در ادامه بارها سود خواهد داد.
شکل ۲.۲ — واشر سیرپینسکی تولیدشده با ۵۰۰۰ نقطهٔ تصادفی.
۲.۳ رابط برنامهنویسی کاربردی OpenGL
هستهٔ یک برنامهٔ گرافیکی ساده را داریم و اکنون میخواهیم کنترل کنیم اشیا چگونه روی نمایشگر ظاهر شوند. همچنین باید جریان اجرای برنامه و تعامل با سامانهٔ پنجرهای را مدیریت کنیم. پیش از تکمیل برنامه، رابط برنامهنویسی کاربردی OpenGL را با جزئیات بیشتری معرفی میکنیم.
از آنجا که رأسها در داخل سامانه صرفنظر از اینکه بهصورت دوبعدی یا سهبعدی تعریف شوند به شیوهای یکسان نمایش داده میشوند، هر آنچه در اینجا انجام میدهیم در سه بعد نیز معتبر است. طبیعی است که در سه بعد کارهای بسیار بیشتری میتوان انجام داد، اما این فقط آغاز راه است. تمرکز این فصل بر روش تعریف بدویهایی است که باید نمایش داده شوند.
ساختار OpenGL مشابه بیشتر APIهای نوین، مانند DirectX، است؛ بنابراین آموختن OpenGL برای کار با سامانههای نرمافزاری دیگر نیز مفید خواهد بود. OpenGL در مقایسه با برخی APIها یادگیری آسانی دارد، اما در عین حال قدرتمند است. هم برنامههای سادهٔ دوبعدی و سهبعدی فصلهای ۲ تا ۵ و هم روشهای رندر پیشرفتهٔ فصلهای ۷ تا ۱۱ را پشتیبانی میکند.
هدف اصلی ما مطالعهٔ گرافیک رایانهای است و API ابزاری برای رسیدن به این هدف است. بنابراین همهٔ توابع OpenGL را ارائه نمیکنیم و بسیاری از جزئیات کنار گذاشته میشوند، اما برنامههای نمونه کامل خواهند بود. منابع انتهای فصل اطلاعات دقیقتری دربارهٔ OpenGL و APIهای دیگر ارائه میکنند.
شکل ۲.۳ — سامانهٔ گرافیکی بهصورت جعبهٔ سیاه: برنامهٔ کاربردی فراخوانی تابع و داده را به سامانهٔ گرافیکی میفرستد؛ سامانه نیز با دستگاههای ورودی/خروجی تبادل داده دارد و خروجی تولید میکند.
۲.۳.۱ توابع گرافیکی
مدل پایهٔ ما از یک بستهٔ گرافیکی «جعبهٔ سیاه» (Black Box) است؛ اصطلاحی مهندسی برای سامانهای که ویژگیهایش فقط بر اساس ورودی و خروجی توصیف میشوند و ممکن است از نحوهٔ کار داخلی آن هیچ اطلاعی نداشته باشیم. میتوان سامانهٔ گرافیکی را جعبهای تصور کرد که ورودیهای آن فراخوانیهای تابع از برنامهٔ کاربردی، اندازهگیریهای ابزارهای ورودی مانند ماوس و صفحهکلید و شاید ورودیهای دیگری مانند پیامهای سیستمعاملاند. خروجی آن عمدتاً گرافیکی است که به دستگاههای خروجی فرستاده میشود. فعلاً میتوان دید سادهشدهای داشت که ورودیها را فراخوانی تابع و خروجیها را بدویهای نمایشدادهشده روی مانیتور میداند.
سامانهٔ گرافیکی برای تولید خروجی و رسیدگی به ورودی کاربر وظایف متعددی انجام میدهد. API رابط با چنین سامانهای ممکن است صدها تابع منفرد داشته باشد. تقسیم این توابع به هفت گروه اصلی مفید است:
- توابع بدوی؛
- توابع ویژگی؛
- توابع مشاهده؛
- توابع تبدیل؛
- توابع ورودی؛
- توابع کنترل؛
- توابع پرسوجو.
هرچند تمرکز ما بر OpenGL است، همهٔ APIهای گرافیکی قابلیتهای مشابهی فراهم میکنند؛ تفاوت در این است که این قابلیتها در کجای معماری پشتیبانی میشوند. OpenGL حول معماری خط لوله طراحی شده و نسخههای جدید آن بر Shaderهای برنامهپذیر تکیه دارند. بنابراین OpenGL و APIهایی مانند DirectX که معماری مشابهی دارند اشتراک زیادی خواهند داشت، درحالیکه همپوشانی OpenGL با API یک رهگیر پرتو کمتر است. با وجود این، مستقل از معماری و API زیرین، هر هفت وظیفه باید انجام شوند.
توابع بدوی اشیای سطح پایین یا موجودیتهای اتمی قابل نمایش توسط سامانه را تعریف میکنند. بسته به API، این بدویها میتوانند نقطه، پارهخط، چندضلعی، پیکسل، متن و انواع منحنی و سطح را شامل شوند. OpenGL بهطور مستقیم مجموعهٔ بسیار محدودی از بدویها را پشتیبانی میکند: فقط نقطهها، پارهخطها و مثلثها.
پشتیبانی از بدویهای دیگر با تقریبزدن آنها به کمک بدویهای پشتیبانیشده در خود برنامهٔ کاربردی انجام میشود. برای اشیای مهمی مانند چندوجهیهای منتظم، سطوح درجهدوم و منحنیها و سطوح Bézier که OpenGL مستقیماً از آنها پشتیبانی نمیکند، کتابخانههایی وجود دارند که کد لازم را فراهم میکنند. مجموعههای گستردهتر بدویها معمولاً بهکمک Shaderهای برنامهپذیر با کارایی بسیار بالا پشتیبانی میشوند.
اگر بدویها «چه چیزی» یک API باشند ــ یعنی اشیایی که میتوان نمایش داد ــ ویژگیها (Attributes) «چگونه» را تعیین میکنند؛ یعنی نحوهٔ ظاهرشدن بدوی روی نمایشگر. توابع ویژگی امکان کارهایی از انتخاب رنگ یک پارهخط، تا انتخاب الگوی پرکردن داخل چندضلعی و انتخاب قلم عنوانهای یک نمودار را فراهم میکنند. در OpenGL میتوان رنگ را با ارسال اطلاعات از برنامهٔ کاربردی به Shader تعیین کرد یا اجازه داد Shader رنگ را محاسبه کند؛ مثلاً با مدل نورپردازیای که دادههای منابع نور و ویژگیهای سطوح مدل را به کار میگیرد.
برای ساخت تصویر باید دوربین مصنوعی توصیف شود. همانطور که در فصل ۱ دیدیم، باید مکان و جهت دوربین را در جهان مشخص و معادل یک عدسی را انتخاب کنیم. این فرایند علاوه بر تعیین نما، اجازه میدهد اشیای بیش از حد نزدیک یا دور را برش دهیم. توابع مشاهده امکان تعریف نماهای مختلف را فراهم میکنند، هرچند APIها از نظر میزان انعطاف متفاوتاند. OpenGL تابع مشاهدهٔ مستقیمی ارائه نمیکند و برای ایجاد نمای مطلوب به تبدیلهای انجامشده در Shaderها متکی است.
یکی از ویژگیهای API مناسب این است که مجموعهای از توابع تبدیل برای عملیاتهایی مانند چرخش، انتقال و تغییر مقیاس اشیا فراهم کند. بحث مشاهده در فصل ۴ و مدلسازی در فصل ۸ بهشدت از تبدیلهای ماتریسی استفاده میکنند. در OpenGL تبدیلها را در برنامهٔ کاربردی تشکیل میدهیم و سپس آنها را یا در خود برنامه یا در Shaderها اعمال میکنیم.
برای برنامههای تعاملی، API باید مجموعهای از توابع ورودی داشته باشد تا با شکلهای متنوع ورودی در سامانههای گرافیکی جدید کار کنیم. به توابعی برای ابزارهایی مانند صفحهکلید، ماوس و لوح داده نیاز داریم. در ادامهٔ همین فصل توابع مربوط به حالتهای مختلف ورودی و انواع دستگاههای ورودی معرفی خواهند شد.
در هر برنامهٔ واقعی باید پیچیدگیهای کار در محیطی چندپردازشی و چندپنجرهای را نیز مدیریت کنیم؛ محیطی که معمولاً به شبکه متصل است و کاربران دیگری نیز دارد. توابع کنترل امکان ارتباط با سامانهٔ پنجرهای، مقداردهی اولیهٔ برنامه و رسیدگی به خطاهای زمان اجرا را فراهم میکنند.
اگر هدف نوشتن برنامههای مستقل از دستگاه باشد، انتظار داریم پیادهسازی API تفاوت میان دستگاهها، مانند تعداد رنگهای پشتیبانیشده یا اندازهٔ نمایشگر، را مدیریت کند. بااینحال در بعضی کاربردها باید ویژگیهای خاص پیادهسازی را بدانیم. برای مثال اگر از قبل بدانیم نمایشگر فقط دو رنگ پشتیبانی میکند، احتمالاً طراحی برنامه را با حالتی که میلیونها رنگ در دسترس است متفاوت انجام میدهیم. بهطور کلی نیز میتوان درون برنامه از اطلاعات بیشتری دربارهٔ پیادهسازی استفاده کرد.
در برنامه میتوان از اطلاعات دیگری دربارهٔ API، از جمله پارامترهای دوربین یا مقادیر موجود در بافر فریم، استفاده کرد. یک API خوب این اطلاعات را از طریق مجموعهای از توابع پرسوجو (Query Functions) در اختیار برنامه قرار میدهد.
۲.۳.۲ خط لولهٔ گرافیکی و ماشینهای حالت
اگر دیدگاههای مختلف دربارهٔ APIهای گرافیکی را کنار هم بگذاریم، به تصویری میرسیم که به سازمان و پیادهسازی واقعی OpenGL نزدیکتر است. میتوان کل سامانهٔ گرافیکی را یک «ماشین حالت» (State Machine) دانست؛ جعبهٔ سیاهی که درون آن یک ماشین حالت متناهی وجود دارد. ورودیهای این ماشین از برنامهٔ کاربردی میآیند. این ورودیها ممکن است حالت ماشین را تغییر دهند یا موجب تولید خروجی دیداری شوند.
از دید API، توابع گرافیکی دو نوعاند: توابعی که بدویهایی را مشخص میکنند که از خط لولهٔ داخل ماشین حالت عبور میکنند، و توابعی که یا حالت داخلی ماشین را تغییر میدهند یا اطلاعات حالت را بازمیگردانند. در OpenGL توابع بسیار کمی مستقیماً موجب تولید خروجی میشوند. بیشتر توابع حالت را تنظیم میکنند؛ یا قابلیتهایی مانند حذف سطح پنهان و بافت را فعال میکنند، یا پارامترهای مورد استفاده در رندرینگ را تعیین میکنند.
تا مدتها OpenGL تعداد زیادی متغیر حالت تعریف میکرد و برای تنظیم مقدار هر متغیر توابع جداگانهای داشت. نسخههای جدیدتر بیشتر این متغیرها و توابع را حذف کردند. بهجای آن، برنامهٔ کاربردی میتواند متغیرهای حالت خودش را تعریف و استفاده کند یا مقدار آنها را به Shaderها بفرستد.
یکی از پیامدهای مهم دیدگاه ماشین حالت این است که بیشتر پارامترها پایدارند: مقدارشان تا زمانی که با تابعی تغییرشان ندهیم ثابت میماند. برای مثال، پس از تعیین یک رنگ، آن رنگ تا فراخوانی تابعی که رنگ را تغییر دهد «رنگ جاری» باقی میماند. پیامد دیگر این است که ویژگیهایی که ممکن است از نظر مفهومی وابسته به جسم بدانیم ــ مثلاً خط قرمز یا دایرهٔ آبی ــ در واقع بخشی از حالتاند؛ خط فقط زمانی قرمز رسم میشود که حالت رنگ جاری، قرمز باشد. هرچند در برنامهٔ کاربردی معمولاً بیضرر و حتی مفید است ویژگیها را وابسته به بدویها تصور کنیم، فراموشکردن تغییر حالت در زمان لازم یا گمکردن رد حالت جاری میتواند پیامدهای آزاردهندهای ایجاد کند.
۲.۳.۳ رابط OpenGL
توابع OpenGL در یک کتابخانهٔ واحد به نام GL، یا در Windows با نام OpenGL، قرار دارند. نام توابع با حروف gl آغاز میشود. Shaderها با «زبان سایهزنی OpenGL» یا GLSL نوشته میشوند که مشخصات مستقلی از OpenGL دارد؛ هرچند توابع لازم برای ارتباط Shader با برنامهٔ کاربردی بخشی از API در OpenGL هستند.
برای ارتباط با سامانهٔ پنجرهای و دریافت ورودی دستگاههای بیرونی، دستکم به یک کتابخانهٔ دیگر نیاز داریم. برای هر سامانهٔ پنجرهای مهم، کتابخانهای اختصاصی وجود دارد که نقش «چسب» میان سامانهٔ پنجرهای و OpenGL را بازی میکند. در X Window System این کتابخانه GLX، در Windows برابر wgl و در Macintosh برابر agl است. بهجای استفاده از کتابخانهای متفاوت برای هر سامانه، از دو کتابخانهٔ در دسترس OpenGL Extension Wrangler یا GLEW و OpenGL Utility Toolkit یا GLUT استفاده میکنیم. GLEW وابستگیهای سیستمعامل را حذف میکند و GLUT حداقل قابلیتهایی را فراهم میکند که از هر سامانهٔ پنجرهای جدید انتظار میرود.
شکل ۲.۴ — سازمان کتابخانهها در محیط X Window: برنامهٔ OpenGL از GLEW، GL و GLUT استفاده میکند؛ GL از GLX و کتابخانههای X برای ارتباط با درایور گرافیکی بهره میگیرد.
در این فصل چند تابع GLUT معرفی و در فصل ۳ تعداد بیشتری بررسی میشوند. شکل ۲.۴ سازمان کتابخانهها در محیط X Window System را نشان میدهد. در این محیط GLUT از GLX و کتابخانههای X استفاده میکند، اما برنامهٔ کاربردی میتواند فقط توابع GLUT را فراخوانی کند و سپس با کتابخانهٔ GLUT متناظر روی سامانههای پنجرهای دیگر دوباره کامپایل شود.
یادداشت ۲: نسخهای جدیدتر از GLUT با نام freeglut نیز در دسترس است.
OpenGL برای خوانایی بیشتر کد و پرهیز از «اعداد جادویی» بهشدت از ثابتهای تعریفشده استفاده میکند. بنابراین رشتههایی مانند GL_FILL و GL_POINTS در فایلهای سرآیند .h تعریف شدهاند. در بیشتر پیادهسازیها یکی از خطوط زیر برای خواندن glut.h و gl.h کافی است:
یا:
OpenGL شیگرا نیست، اما برای بسیاری از توابع از شکلهای مختلفی استفاده میکند تا انواع دادهٔ متنوع پشتیبانی شوند. برای نمونه، در ادامه از شکلهای مختلف تابع glUniform برای انتقال داده به Shaderها استفاده خواهیم کرد. اگر یک مقدار ممیز شناور، مثلاً زمان، را منتقل کنیم از glUniform1f استفاده میکنیم. برای انتقال موقعیت صحیح سهبعدی از طریق اشارهگر به آرایهٔ سهبعدی از intها میتوان از glUniform3iv بهره برد. بعداً از glUniformMatrix4fv برای انتقال یک ماتریس 4 × 4 از floatها استفاده خواهیم کرد.
برای اشارهٔ کلی به چنین خانوادههایی از توابع، از نماد زیر استفاده میشود:
در اینجا * را میتوان دو یا سه نویسه با قالب nt یا ntv در نظر گرفت. n تعداد ابعاد را مشخص میکند (2، 3، 4 یا matrix)، t نوع داده را نشان میدهد، مانند عدد صحیح i، ممیز شناور f یا double با d، و v، اگر وجود داشته باشد، نشان میدهد متغیرها از طریق اشارهگر به آرایه مشخص شدهاند نه فهرست آرگومانها.
در بحثها از شکلی استفاده میکنیم که مناسبتر باشد و جزئیات شکلهای دیگر را به OpenGL Programming Guide [Shr10] واگذار میکنیم. مستقل از شکلی که برنامهنویس انتخاب میکند، نمایش زیرین یکسان است؛ همانگونه که صفحهٔ واشر را میتوان هم فضای دوبعدی و هم زیرفضایی از فضای سهبعدی متناظر با z = 0 در نظر گرفت. در فصل ۳ خواهیم دید نمایش زیرین در واقع چهاربعدی است، اما فعلاً لازم نیست نگران این موضوع باشیم. بهطور کلی برنامهنویس شکلی را انتخاب میکند که برای کاربردش مناسبتر باشد.
۲.۳.۴ دستگاههای مختصات
اگر به کد واشر سیرپینسکی بازگردیم، ممکن است بپرسید مقادیر x، y و z در تعریف رأسها دقیقاً چه معنایی دارند. واحدشان چیست؟ فوت، متر یا میکرون؟ مبدأ کجاست؟ پاسخ ساده در همهٔ این موارد این است: انتخاب با شماست.
در نخستین سامانههای گرافیکی، کاربر مجبور بود همهٔ اطلاعات مانند مکان رأسها را مستقیماً برحسب واحدهای دستگاه نمایش تعیین کند. اگر برنامههای کاربردی سطحبالا نیز چنین محدودیتی داشتند، باید نقطهها را مثلاً با مکان پیکسلی روی صفحه یا فاصله برحسب سانتیمتر از گوشهٔ نمایشگر توصیف میکردیم. مشکل این روش آشکار است؛ استفاده از فاصله روی نمایشگر برای توصیف پدیدههایی که واحد طبیعیشان سال نوری، مانند دادههای نجومی، یا میکرون، مانند طراحی مدار مجتمع، است بیمعنا خواهد بود.
یکی از پیشرفتهای مهم نرمافزارهای گرافیکی زمانی رخ داد که سامانهها به کاربر اجازه دادند در هر دستگاه مختصات دلخواه کار کند. ظهور گرافیک مستقل از دستگاه، برنامهنویسان را از نگرانی دربارهٔ جزئیات دستگاههای ورودی و خروجی آزاد کرد. دستگاه مختصات کاربر «دستگاه مختصات جهان» (World Coordinates) یا دستگاه مختصات کاربرد/جسم نام گرفت. در محدودهٔ محدودیتهای حساب ممیز شناور رایانه، میتوان از هر عددی که برای مسئله مناسب است استفاده کرد.
واحدهایی را که برنامهٔ کاربردی برای تعیین مکان رأسها به کار میگیرد «مختصات رأس» (Vertex Coordinates) مینامیم. در بیشتر برنامهها مختصات رأس همان مختصات جسم یا جهان است، اما بسته به کاری که در Shader انجام میدهیم یا نمیدهیم، مختصات رأس ممکن است یکی از دستگاههای مختصات داخلی دیگر خط لوله باشد. این دستگاهها در فصلهای ۳ و ۴ بررسی میشوند.
واحدهای روی نمایشگر در ابتدا «مختصات فیزیکی دستگاه» یا سادهتر «مختصات دستگاه» نامیده میشدند. برای دستگاههای رستری مانند بیشتر CRTها و نمایشگرهای تخت، اصطلاح «مختصات پنجره» یا «مختصات صفحه» را به کار میبریم. مختصات پنجره همواره با نوعی عدد صحیح بیان میشوند، زیرا مرکز هر پیکسل در بافر فریم باید روی شبکهای ثابت قرار گیرد؛ به بیان دیگر پیکسلها ذاتاً گسستهاند و مکان آنها با اعداد صحیح مشخص میشود.
در نقطهای از فرایند، مقادیر مختصات رأس باید به مختصات پنجره نگاشت شوند، همانگونه که در شکل ۲.۵ آمده است. این وظیفه بر عهدهٔ سامانهٔ گرافیکی، نه کاربر، است و نگاشت بهطور خودکار بخشی از فرایند رندرینگ است.
شکل ۲.۵ — نگاشت از مختصات رأس/جهان (x, y) در بازهٔ تعریفشده به مختصات صفحه (r, s).
همانطور که در چند بخش بعد خواهیم دید، برای تعریف این نگاشت کاربر فقط چند پارامتر، مانند ناحیهای از جهان که میخواهد ببیند و اندازهٔ نمایشگر، را مشخص میکند. بااینحال میان برنامهٔ کاربردی و بافر فریم دو Shader و رسترساز قرار دارند و هنگام بحث مشاهده خواهیم دید سه دستگاه مختصات میانی مهم دیگر نیز وجود دارد.
۲.۴ بدویها و ویژگیها
در جامعهٔ گرافیک همواره بحثی دربارهٔ این وجود داشته که یک API باید از چه بدویهایی پشتیبانی کند. این بحث قدیمی است و هرگز بهطور کامل حل نشده است. دیدگاه مینیمالیستی میگوید API باید مجموعهای کوچک از بدویها داشته باشد که بتوان انتظار داشت همهٔ سختافزارها آن را پشتیبانی کنند. افزون بر آن، بدویها باید متعامد باشند، یعنی هر کدام قابلیتی بدهند که از دیگران به دست نمیآید. سامانههای حداقلی معمولاً خط، چندضلعی و نوعی متن، یعنی رشتهٔ نویسهها، را پشتیبانی میکنند؛ همگی نیز بهطور کارآمد در سختافزار تولید میشوند.
در سوی دیگر، سامانههایی قرار دارند که مجموعهٔ گستردهتری از بدویها مانند دایره، منحنی، سطح و جسم جامد را پشتیبانی میکنند. استدلال این دیدگاه آن است که کاربران برای ساخت آسان برنامههای پیچیده به بدویهای سطحبالا نیاز دارند. اما چون نمیتوان انتظار داشت سختافزارهای اندکی مجموعهٔ اتحاد همهٔ خواستههای کاربران را پشتیبانی کنند، برنامهای که بر چنین مجموعهٔ بزرگی متکی باشد احتمالاً قابلیت حمل کمتری خواهد داشت.
با پیشرفت سختافزار گرافیکی و رسیدن عملکرد بلادرنگ به دهها میلیون چندضلعی در ثانیه، کفه بهسوی پشتیبانی از مجموعهٔ حداقلی بدویها سنگینتر شده است. یکی از دلایل این است که GPUها بخش بزرگی از سرعت خود را از بهینهشدن برای نقطه، خط و مثلث به دست میآورند. بعداً کدی توسعه میدهیم که منحنیها و سطوح مختلف را با بدویهای پشتیبانیشده روی GPU تقریب بزند.
بدویها را میتوان به دو دسته تقسیم کرد: بدویهای هندسی و بدویهای تصویری یا رستری. بدویهای هندسی در دامنهٔ مسئله تعریف میشوند و نقطه، پارهخط، چندضلعی، منحنی و سطح را شامل میشوند. این بدویها از خط لولهٔ هندسی، مطابق شکل ۲.۶، عبور میکنند و در آن مجموعهای از عملیات هندسی تعیین میکند بدوی دیده میشود یا نه، در صورت دیدهشدن کجای نمایشگر ظاهر میشود و چگونه به پیکسلهای بافر فریم رسترسازی میشود.
شکل ۲.۶ — خط لولهٔ سادهشدهٔ OpenGL: دادهٔ برنامهٔ کاربردی در خط لولهٔ هندسی از مراحل تبدیل، تصویرسازی و برش عبور میکند؛ عملیات پیکسلی نیز مسیر موازی خود را تا بافر فریم طی میکنند.
از آنجا که بدویهای هندسی در فضای دو یا سهبعدی وجود دارند، میتوان آنها را با عملیاتهایی مانند چرخش و انتقال در فضا دستکاری کرد. همین بدویها با همان عملیاتها میتوانند بلوکهای سازندهٔ اشیای هندسی پیچیدهتر باشند. در مقابل، بدویهای رستری مانند آرایههای پیکسل ویژگی هندسی ندارند و نمیتوان آنها را مانند بدویهای هندسی در فضا دستکاری کرد. آنها در مسیر رسیدن به بافر فریم از خط لولهای موازی عبور میکنند. بحث بدویهای رستری تا فصل ۷ به تعویق انداخته میشود.
بدویهای هندسی پایه در OpenGL با مجموعهای از رأسها مشخص میشوند. برنامه با محاسبهٔ دادههای رأس ــ مکان و ویژگیهای دیگر ــ و قراردادن نتیجه در آرایههایی که برای نمایش به GPU فرستاده میشوند آغاز میشود. وقتی میخواهیم هندسهای را نمایش دهیم، تابعهایی را اجرا میکنیم که پارامترهایشان نحوهٔ تفسیر رأسها را مشخص میکند. برای نمونه، پس از قرارگرفتن رأسهای واشر سیرپینسکی روی GPU، میتوان آنها را از رأس نخست بهصورت نقطه با فراخوانی زیر نمایش داد:
glDrawArrays(GL_POINTS, 0, NumPoints);
همهٔ بدویهای هندسی OpenGL گونههایی از نقطه، پارهخط و چندضلعی مثلثی هستند. یک نقطه میتواند یک پیکسل یا گروه کوچکی از پیکسلها نمایش داده شود. بخش محدود یک خط میان دو رأس «پارهخط» نام دارد ــ در مقابل خط که امتداد نامتناهی دارد ــ و در هندسه و گرافیک رایانهای اهمیت زیادی دارد. میتوان از پارهخطها برای تقریب منحنیها، اتصال مقادیر داده در یک نمودار، یا نمایش لبههای اشیای بسته مانند چندضلعیها استفاده کرد. بنابراین اغلب مفید است هم برحسب رأس و هم برحسب پارهخط فکر کنیم.
برای نمایش نقطهها یا پارهخطها در OpenGL چند گزینه داریم، مطابق شکل ۲.۷. نخستین نوع چنین است:
- نقطهها (
GL_POINTS): هر رأس با اندازهای دستکم برابر یک پیکسل نمایش داده میشود.
شکل ۲.۷ — انواع نمایش نقطه و پارهخط.
- پارهخطها (
GL_LINES): هر جفت پیاپی از رأسها بهعنوان دو انتهای یک پارهخط مستقل تفسیر میشود. چون رأسها جفتجفت پردازش میشوند، پارهخطهای متوالی معمولاً به یکدیگر متصل نیستند.
- چندخطیها (
GL_LINE_STRIP, GL_LINE_LOOP): اگر رأسها و پارهخطهای متوالی باید به هم متصل باشند، میتوان از Line Strip یا Polyline استفاده کرد. بسیاری از منحنیها را میتوان با چندخطی مناسب تقریب زد. برای بستن چندخطی میتوان رأس نهایی را در مکان رأس نخست قرار داد یا از GL_LINE_LOOP استفاده کرد؛ در حالت دوم سامانه بهطور خودکار از رأس نهایی به رأس نخست پارهخطی رسم میکند و مسیر بسته میشود.
۲.۴.۱ مبانی چندضلعی
پارهخطها و چندخطیها میتوانند لبههای اشیا را مدل کنند، اما اشیای بسته دارای داخل هستند؛ شکل ۲.۸ را ببینید. معمولاً واژهٔ «چندضلعی» را برای جسمی به کار میبریم که مرزش با یک حلقهٔ خطی توصیف میشود و علاوه بر آن ناحیهٔ داخلی مشخصی دارد.
شکل ۲.۸ — اشیای پُرشده.
چندضلعیها در گرافیک رایانهای نقش ویژهای دارند، زیرا با سرعت بالا نمایش داده میشوند و میتوانند برای تقریب سطوح دلخواه به کار روند. عملکرد سامانههای گرافیکی نیز اغلب بر حسب تعداد چندضلعیهایی که در هر ثانیه رندر میشوند سنجیده میشود. یک چندضلعی را میتوان به روشهای مختلف رندر کرد: فقط لبهها، داخل با رنگ یکنواخت یا الگو، و با یا بدون نمایش لبهها، مطابق شکل ۲.۹.
شکل ۲.۹ — روشهای مختلف نمایش یک چندضلعی.
هرچند لبههای بیرونی چندضلعی بهسادگی با فهرست مرتب رأسها تعریف میشوند، اگر ناحیهٔ داخلی بهدرستی تعریف نشده باشد ممکن است فهرست رأسها اصلاً رندر نشود یا به شکل نامطلوب نمایش داده شود. سه ویژگی تضمین میکنند چندضلعی درست نمایش داده شود: باید ساده، محدب و تخت باشد.
در دو بعد، تا زمانی که هیچ دو لبهٔ چندضلعی یکدیگر را قطع نکنند، چندضلعی «ساده» است. همانگونه که در شکل ۲.۱۰ دیده میشود، چندضلعی سادهٔ دوبعدی داخل مشخصی دارد. هرچند مکان رأسها تعیین میکند چندضلعی ساده است یا نه، هزینهٔ آزمون این ویژگی بهاندازهای زیاد است ــ تمرین ۲.۱۲ را ببینید ــ که بیشتر سامانههای گرافیکی انتظار دارند برنامهٔ کاربردی آزمون لازم را انجام دهد.
شکل ۲.۱۰ — چندضلعیها: (الف) ساده؛ (ب) غیرساده.
یادداشت ۳: گاهی بهجای Polygon از اصطلاح Fill Area استفاده میشود.
یادداشت ۴: اندازهگیری سرعت رندر چندضلعی هم به تعداد رأسها و هم به تعداد پیکسلهای داخل آن وابسته است.
میتوان پرسید اگر سامانهٔ گرافیکی یک چندضلعی غیرساده برای نمایش دریافت کند چه میکند و آیا راهی برای تعریف داخل یک چندضلعی غیرساده وجود دارد یا نه. این پرسشها در فصل ۶ بیشتر بررسی میشوند.
از دید پیادهسازی الگوریتمی عملی برای پُرکردن داخل چندضلعی، سادهبودن بهتنهایی اغلب کافی نیست. برخی APIها فقط زمانی نتیجهٔ پُرکردن یکسانی در پیادهسازیهای مختلف تضمین میکنند که چندضلعی محدب باشد. یک جسم زمانی «محدب» (Convex) است که همهٔ نقاط پارهخط میان هر دو نقطهٔ داخل جسم یا روی مرز آن، درون جسم باقی بمانند. در شکل ۲.۱۱، p1 و p2 دو نقطهٔ دلخواه داخل چندضلعیاند و تمام پارهخط وصلکنندهٔ آنها نیز در داخل چندضلعی قرار دارد.
شکل ۲.۱۱ — مفهوم تحدب.
این تعریف نه به نوع جسم و نه به تعداد ابعاد وابسته است. اشیای محدب شامل مثلث، چهاروجهی، مستطیل، دایره، کره و متوازیالسطوح هستند، مطابق شکل ۲.۱۲. آزمونهای مختلفی برای تحدب وجود دارد؛ تمرین ۲.۱۹ را ببینید. بااینحال، مانند آزمون سادگی، آزمون تحدب نیز پرهزینه است و معمولاً به برنامهٔ کاربردی واگذار میشود.
در سه بعد، چندضلعیها دشواریهای بیشتری ایجاد میکنند، زیرا برخلاف اشیای دوبعدی، همهٔ رأسهای مشخصکنندهٔ چندضلعی الزاماً در یک صفحه قرار ندارند. یکی از ویژگیهایی که بیشتر سامانههای گرافیکی از آن استفاده میکنند و پایهٔ چندضلعیهای OpenGL است، این است که هر سه رأس ناهمخط هم یک مثلث و هم صفحهٔ آن مثلث را بهطور یکتا تعیین میکنند. بنابراین اگر همیشه از مثلث استفاده کنیم، مطمئن هستیم جسم بهدرستی رندر میشود. در عمل نیز اغلب ناچاریم از مثلثها استفاده کنیم، زیرا الگوریتمهای رندر معمولاً فقط زمانی درستبودن را تضمین میکنند که رأسها چندضلعی محدب و تخت تشکیل دهند. افزون بر این، سختافزار و نرمافزار معمولاً نوع مثلثی را پشتیبانی میکنند که از یک چندضلعی سهرأسی عمومی سریعتر رندر میشود.
۲.۴.۲ چندضلعیها در OpenGL
از میان انواع چندضلعی، OpenGL فقط مثلث را مستقیماً پشتیبانی میکند؛ شکل ۲.۱۳. مثلثها را میتوان به سه روش نمایش داد: بهصورت نقطههای متناظر با رأسها، فقط بهصورت لبهها، یا با داخل پُرشده. با تابع glPolygonMode میتوان از رندرکننده خواست بهجای حالت پیشفرض پُرشده، فقط لبهها یا فقط نقطههای رأس را تولید کند. اگر بخواهیم یک چندضلعی هم پُرشده باشد و هم لبههایش دیده شوند، باید آن را دو بار رندر کنیم ــ یک بار در هر حالت ــ یا یک چندضلعی پُرشده و یک حلقهٔ خطی با همان رأسها رسم کنیم.
شکل ۲.۱۲ — نمونههایی از اشیای محدب.
شکل ۲.۱۳ — انواع نمایش مثلث.
انواع اصلی عبارتاند از:
- مثلثها (
GL_TRIANGLES): لبهها همان حالتی را دارند که اگر حلقههای خطی استفاده میکردیم. هر گروه متوالی از سه رأس یک مثلث جدید تعریف میکند.
- نوارها و بادبزنها (
GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN): این اشیا بر گروهی از مثلثها بنا شدهاند که رأسها و لبههای مشترک دارند. در نوار مثلثی، هر رأس تازه با دو رأس قبلی ترکیب و مثلث جدیدی تعریف میکند. در بادبزن مثلثی یک نقطه ثابت است؛ دو نقطهٔ بعدی نخستین مثلث را میسازند و هر مثلث بعدی از یک نقطهٔ تازه، نقطهٔ قبلی و همان نقطهٔ ثابت نخست تشکیل میشود.
شکل ۲.۱۴ — نوار مثلثی و بادبزن مثلثی.
۲.۴.۳ تقریب یک کره
بادبزنها و نوارها امکان تقریب سادهٔ بسیاری از سطوح خمیده را فراهم میکنند. برای مثال، یکی از روشهای تقریب کره استفاده از مجموعهای از چندضلعیهاست که خطوط طول و عرض جغرافیایی آنها را تعریف میکنند، مطابق شکل ۲.۱۵. این کار را میتوان با نوارهای مثلثی بسیار کارآمد انجام داد.
شکل ۲.۱۵ — تقریب کره با چهارضلعیها.
یک کرهٔ واحد را در نظر بگیرید. میتوان آن را با سه رابطهٔ زیر توصیف کرد:
x(\theta,\phi)=\sin\theta\cos\phi,
y(\theta,\phi)=\cos\theta\cos\phi,
z(\theta,\phi)=\sin\phi.
اگر θ را ثابت و φ را تغییر دهیم، دایرههای طول جغرافیایی ثابت به دست میآیند. به همین ترتیب، اگر φ ثابت و θ متغیر باشد، دایرههای عرض جغرافیایی ثابت حاصل میشوند. با تولید نقاط در گامهای ثابت θ و φ میتوان چهارضلعیهای شکل ۲.۱۵ را مشخص کرد. اما چون OpenGL مثلث را پشتیبانی میکند نه چهارضلعی، برای هر چهارضلعی دادهٔ دو مثلث را تولید میکنیم.
با در نظر گرفتن اینکه توابع مثلثاتی استاندارد زاویه را برحسب رادیان میخواهند، کد چهارضلعیها برای گامهای ۲۰ درجهای θ و φ چنین است:
const float DegreesToRadians = M_PI / 180.0; // M_PI = 3.14159...
point3 quad_data[342]; // 8 rows of 18 quads
int k = 0;
for(float phi = -80.0; phi <= 80.0; phi += 20.0)
{
float phir = phi*DegreesToRadians;
float phir20 = (phi + 20.0)*DegreesToRadians;
for(float theta = -180.0; theta <= 180.0; theta += 20.0)
{
float thetar = theta*DegreesToRadians;
quad_data[k] = point3(sin(thetar)*cos(phir),
cos(thetar)*cos(phir), sin(phir));
k++;
quad_data[k] = point3(sin(thetar)*cos(phir20),
cos(thetar)*cos(phir20), sin(phir20));
k++;
}
}
بعداً این دادهها را میتوان با glDrawArrays(GL_LINE_LOOP,...) یا تابع رسم دیگری رندر کرد. بااینحال در قطبها مشکل ایجاد میشود، زیرا همهٔ خطوط طول جغرافیایی در آنجا همگرا هستند و دیگر نمیتوان از نوار استفاده کرد. راهحل استفاده از دو بادبزن مثلثی، یکی در هر قطب، است:
const float DegreesToRadians = M_PI / 180.0; // M_PI = 3.14159...
int k = 0;
point3 strip_data[40];
strip_data[k] = point3(0.0, 0.0, 1.0);
k++;
float sin80 = sin(80.0*DegreesToRadians);
float cos80 = cos(80.0*DegreesToRadians);
for(float theta = -180.0; theta <= 180.0; theta += 20.0)
{
float thetar = theta*DegreesToRadians;
strip_data[k] = point3(sin(thetar)*cos80,
cos(thetar)*cos80, sin80);
k++;
}
strip_data[k] = point3(0.0, 0.0, -1.0);
k++;
for(float theta = -180.0; theta <= 180.0; theta += 20.0)
{
float thetar = theta;
strip_data[k] = point3(sin(thetar)*cos80,
cos(thetar)*cos80, sin80);
k++;
}
این دادهها را میتوان با glDrawArrays(GL_TRIANGLE_FAN, ....) یا تابع رسم دیگری رندر کرد. توجه کنید چون بادبزنهای مثلثی چندضلعیاند، برای دستیابی به نمایش پارهخطی شکل ۲.۱۵ باید ابتدا حالت چندضلعی را از پُرشدن به خط تغییر دهیم.
۲.۴.۴ مثلثبندی
تا اینجا واژههای «چندضلعی» و «مثلث» را گاهی تقریباً مترادف به کار بردهایم. اگر به اشیای دارای داخل علاقهمند باشیم، چندضلعیهای عمومی مشکلسازند. ممکن است مجموعهٔ رأسها همگی در یک صفحه نباشند یا چندضلعیای بسازند که ساده یا محدب نیست. این مشکلات برای مثلث رخ نمیدهند. تا زمانی که سه رأس مثلث همخط نباشند، داخل آن بهدرستی تعریف میشود و مثلث همزمان ساده، تخت و محدب است. به همین دلیل مثلثها آسان رندر میشوند و تنها موجودیت هندسی قابل پُرشدن هستند که OpenGL مستقیماً میشناسد.
در عمل باید با چندضلعیهای عمومیتر نیز کار کنیم. راهبرد رایج این است که از فهرست رأسها آغاز کنیم و مجموعهای از مثلثها تولید کنیم که با چندضلعی تعریفشده توسط آن فهرست سازگار باشند. این فرایند «مثلثبندی» (Triangulation) نام دارد.
شکل ۲.۱۶ یک چندضلعی محدب و دو مثلثبندی متفاوت از آن را نشان میدهد. هرچند هر مجموعه رأس را میتوان مثلثبندی کرد، همهٔ مثلثبندیها همارز نیستند. چهارضلعی شکل ۲.۱۷ را در نظر بگیرید. اگر مانند شکل ۲.۱۷(ب) آن را مثلثبندی کنیم، دو مثلث بلند و باریک میسازیم؛ درحالیکه در شکل ۲.۱۷(ج) دو مثلث به حالت متساویالاضلاع نزدیکترند. در فصل ۵ و بحث نورپردازی خواهیم دید مثلثهای بلند و باریک میتوانند در رندر مصنوعات دیداری ایجاد کنند.
برای چندضلعیهای محدب تخت الگوریتمهای سادهای وجود دارد. میتوان با سه رأس نخست یک مثلث ساخت، سپس رأس دوم را از فهرست حذف کرد و فرایند را آنقدر تکرار کرد تا فقط سه رأس باقی بماند که مثلث نهایی را میسازند. این فرایند در شکل ۲.۱۸ نشان داده شده است، اما نه مجموعهٔ خوبی از مثلثها را تضمین میکند و نه میتواند چندضلعیهای مقعر را مدیریت کند. در فصل ۶ مثلثبندی چندضلعیهای عمومیتر بررسی خواهد شد.
شکل ۲.۱۶ — (الف) چندضلعی دوبعدی؛ (ب) یک مثلثبندی؛ (ج) مثلثبندی دیگر.
شکل ۲.۱۷ — (الف) چهارضلعی؛ (ب) یک مثلثبندی؛ (ج) مثلثبندی دیگر.
شکل ۲.۱۸ — مثلثبندی بازگشتی یک چندضلعی محدب.
در فصل ۶، بهعنوان بخشی از بحث رسترسازی، مثلثبندی چندضلعیهای ساده اما نامحدب را نیز بررسی خواهیم کرد. این روش اجازه میدهد چندضلعیهایی عمومیتر از مثلث را رندر کنیم.
بحث الگوریتمهای عمومیتر مثلثبندی را تا فصل ۱۰ و بررسی منحنیها و سطوح به تعویق میاندازیم. یکی از دلایل این تأخیر وجود چند فرایند مرتبط در مدلسازی سطح است. برای نمونه، فناوری اسکن لیزری میتواند میلیونها رأس سهبعدیِ بدون ساختار تولید کند. سپس باید از این رأسها سطحی، معمولاً بهصورت مشی از مثلثها، ساخته شود.
الگوریتم «مثلثبندی دلونی» (Delaunay Triangulation) از این نظر مثلثبندی مناسبی مییابد که اگر دایرهٔ تعیینشده توسط هر مثلث را در نظر بگیریم، هیچ رأس دیگری داخل آن دایره قرار نمیگیرد. مثلثبندی حالت خاصی از مسئلهٔ عمومیتر «شبکهبندی» (Tessellation) است که چندضلعی را به یک مش چندضلعی تقسیم میکند و لازم نیست همهٔ سلولهای آن مثلث باشند. الگوریتمهای عمومی شبکهبندی، بهویژه وقتی چندضلعی اولیه ممکن است حفره داشته باشد، پیچیدهاند.
۲.۴.۵ متن
خروجی گرافیکی در کاربردهایی مانند تحلیل و نمایش داده به حاشیهنویسی و برچسبگذاری نمودارها نیاز دارد. در برنامههای غیرگرافیکی خروجی متنی امری عادی است، اما متن در گرافیک رایانهای مشکلات ویژهای دارد. در کاربردهای غیرگرافیکی معمولاً مجموعهای ساده از نویسهها که همیشه به یک شکل نمایش داده میشوند کافی است. در گرافیک رایانهای، در مقابل، اغلب میخواهیم متن را به شکلهای متعدد و با کنترل سبک قلم، اندازه، رنگ و پارامترهای دیگر نمایش دهیم و به مجموعهای از Fontها دسترسی داشته باشیم. Font خانوادهای از Typefaceها با سبک مشخص، مانند Times، Computer Modern یا Helvetica، است.
دو شکل اصلی متن وجود دارد: Stroke و Raster. «متن خطی» (Stroke Text)، شکل ۲.۱۹، همانند دیگر اشیای هندسی ساخته میشود. از رأسها برای تعریف پارهخطها یا منحنیهایی استفاده میکنیم که پیرامون هر نویسه را شکل میدهند. اگر نویسه با مرز بسته تعریف شده باشد، میتوان داخل آن را پُر کرد.
شکل ۲.۱۹ — متن خطی با قلم PostScript.
مزیت متن خطی این است که میتواند بهاندازهٔ هر جسم دیگر جزئیات داشته باشد و چون همانند اشیای گرافیکی دیگر تعریف میشود، میتوان آن را با تبدیلهای استاندارد دستکاری و مانند هر بدوی هندسی مشاهده کرد. با تبدیلها میتوان نویسهٔ خطی را بزرگتر یا چرخان کرد و جزئیات و ظاهرش حفظ شود. بنابراین کافی است هر نویسه فقط یکبار تعریف شود و سپس با تبدیلها در اندازه و جهت دلخواه تولید گردد.
بااینحال تعریف یک قلم خطی کامل با ۱۲۸ یا ۲۵۶ نویسه میتواند پیچیده باشد و حافظه و زمان پردازش زیادی مصرف کند. قلمهای استاندارد PostScript با منحنیهای چندجملهای تعریف میشوند و همهٔ مزایا و معایب متن خطی را نشان میدهند. این قلمها هم در کاربردهای وضوح بالا و هم وضوح پایین قابل استفادهاند. برای کاهش مشکل رندر کند چنین نویسههایی، اغلب بخشی از پردازش به چاپگر واگذار میشود.
«متن رستری» (Raster Text)، شکل ۲.۲۰، ساده و سریع است. نویسهها بهصورت مستطیلهایی از بیتها به نام Bit Block تعریف میشوند. هر بلوک با الگوی صفرها و یکها یک نویسه را مشخص میکند. یک نویسهٔ رستری را میتوان با عملیات «انتقال بلوک بیت» یا bitblt بهسرعت در بافر فریم قرار داد؛ عملیاتی که کل بلوک بیت را با یک فراخوانی تابع جابهجا میکند. bitblt در فصل ۷ بررسی میشود.
شکل ۲.۲۰ — متن رستری.
اندازهٔ نویسههای رستری را میتوان با تکرار یا تکثیر پیکسلها افزایش داد؛ فرایندی که نویسههای بزرگ را بلوکی و دندانهدار میکند، مطابق شکل ۲.۲۱. تبدیلهای دیگر مانند چرخش ممکن است برای نویسهٔ رستری معنای مناسبی نداشته باشند، زیرا تبدیل ممکن است بیتهای تعریفکنندهٔ نویسه را به مکانهایی ببرد که با جای پیکسلهای بافر فریم منطبق نیستند.
شکل ۲.۲۱ — تکثیر پیکسل در نویسهٔ رستری.
از آنجا که نویسههای Stroke و Bitmap را میتوان با بدویهای دیگر ساخت، OpenGL بدوی مستقلی برای متن ندارد. بااینحال کتابخانهٔ GLUT چند مجموعهٔ از پیش تعریفشدهٔ نویسههای Bitmap و Stroke فراهم میکند که نرمافزاری و قابل حملاند.
۲.۴.۶ اشیای خمیده
بدویهای مجموعهٔ پایهٔ ما همگی با رأسها تعریف شدهاند. بهجز نوع نقطه، همه از پارهخطها تشکیل میشوند یا از پارهخط برای تعیین مرز ناحیهای استفاده میکنند که میتواند با رنگ یکنواخت یا الگو پُر شود. برای ساخت مجموعهای غنیتر از اشیا دو رویکرد وجود دارد.
رویکرد نخست استفاده از همین بدویها برای تقریب منحنیها و سطوح است. برای مثال اگر دایره بخواهیم، میتوان از چندضلعی منتظم n ضلعی استفاده کرد. به همین ترتیب، پیشتر کره را با مثلثها و چهارضلعیها تقریب زدیم. بهطور عمومیتر، سطح خمیده را با مشی از چندضلعیهای محدب، یعنی یک Tessellation، تقریب میزنیم. این شبکهبندی میتواند در مرحلهٔ رندر یا در خود برنامهٔ کاربردی انجام شود.
رویکرد دوم که در فصل ۱۰ بررسی میشود، آغاز از تعریف ریاضی اشیای خمیده و ساخت توابع گرافیکی برای پیادهسازی آنهاست. اشیایی مانند سطوح درجهدوم و منحنیها و سطوح چندجملهای پارامتری از نظر ریاضی بهخوبی شناخته شدهاند و میتوان آنها را با مجموعهای از رأسها مشخص کرد. برای نمونه میتوان کره را با مرکز و نقطهای روی سطح آن یا یک منحنی چندجملهای درجهٔ سه را با دادههای چهار نقطه تعریف کرد.
۲.۴.۷ ویژگیها
گرچه جسم هندسی را میتوان با مجموعهای از رأسها توصیف کرد، همان جسم میتواند به روشهای گوناگونی نمایش داده شود. ویژگیهایی که نحوهٔ رندر جسم را توصیف میکنند «Attributes» نام دارند. ویژگیهای قابل استفاده به نوع جسم وابستهاند. مثلاً یک خط میتواند سیاه یا سبز، پیوسته یا خطچین باشد. یک چندضلعی میتواند با رنگ یکنواخت یا الگو پُر شود و میتوان آن را پُرشده یا فقط با لبهها نمایش داد. شکل ۲.۲۲ تعدادی از این ویژگیها را برای خطها و چندضلعیها نشان میدهد.
شکل ۲.۲۲ — نمونهٔ ویژگیها برای (الف) خطها و (ب) چندضلعیها.
ویژگیها ممکن است به اشیای هندسی نسبت داده یا اصطلاحاً به آنها Bind شوند، مانند رنگ یک مکعب. اغلب بهتر است جسمی مانند مکعب را برحسب وجههای جداگانه مدل و ویژگیها را برای هر وجه تعیین کنیم. در این صورت مکعب سبز است چون هر شش وجه آن سبزند. هر وجه نیز میتواند با دو مثلث توصیف شود؛ بنابراین در نهایت یک مکعب سبز بهصورت ۱۲ مثلث سبز رندر خواهد شد.
اگر یک گام جلوتر برویم، هر مثلث با سه رأس مشخص میشود. در معماری خط لوله، هر رأس بهطور مستقل در Vertex Shader پردازش میشود. بنابراین میتوان ویژگیهایی را به هر رأس نسبت داد. مثلاً اگر به هر رأس یک چندضلعی رنگ متفاوتی اختصاص دهیم، رسترساز میتواند میان رنگهای رأسها درونیابی کند و برای هر قطعه رنگ متفاوتی به دست آورد. این ویژگیهای رأس میتوانند به برنامهٔ کاربردی نیز وابسته باشند. برای نمونه، در شبیهسازی توزیع گرما روی یک جسم، برنامه ممکن است برای هر رأس تعریفکنندهٔ جسم یک دما محاسبه کند. در فصل ۳ دادهٔ ویژگیهای رأس را همراه مکان رأسها در آرایهای قرار میدهیم که به GPU فرستاده میشود.
در سامانههایی که از گرافیک حالت فوری و معماری خط لوله استفاده میکنند، بعضی ویژگیها بخشی از حالت سامانهٔ گرافیکی هستند. مثلاً «رنگ جاری» وجود دارد که همهٔ بدویها با آن رندر میشوند تا زمانی که تابع تغییر حالت، مانند نمونهٔ زیر، آن را عوض کند:
set_current_color(color);
به همین ترتیب برای ویژگیهای مختلف توابع تنظیم ویژگی وجود دارد.
یادداشت ۵: نسخههای قدیمیتر OpenGL توابع تنظیم حالت مانند glColor، glLineWidth و glStipple داشتند. این ویژگیهای منسوخشده را میتوان در Shaderها پیادهسازی کرد.
هر نوع هندسی مجموعهٔ ویژگیهای خاص خود را دارد. یک نقطه مثلاً ویژگی رنگ و اندازه دارد. پارهخط میتواند رنگ، ضخامت و الگو ــ پیوسته، خطچین یا نقطهچین ــ داشته باشد. بدویهای پُرشده مانند چندضلعی ویژگیهای بیشتری دارند، زیرا باید نحوهٔ پُرکردن را با چند پارامتر مشخص کرد. میتوان داخل را با رنگ یا الگو پُر کرد، اصلاً پُر نکرد و فقط لبهها را نمایش داد، یا داخل و لبهها را با رنگهای متفاوت نشان داد.
شکل ۲.۲۳ — نمونههایی از ویژگیهای متن خطی.
در سامانههایی که متن خطی را بهعنوان بدوی پشتیبانی میکنند، ویژگیهای متنوعی وجود دارد. شکل ۲.۲۳ نمونههایی را نشان میدهد؛ از جمله جهت رشتهٔ متن، مسیری که نویسههای پیاپی دنبال میکنند، ارتفاع و عرض نویسهها، Font و سبک نوشتار مانند Bold، Italic و Underline.
اگرچه مفهوم حالت جاری در برنامههای تعاملی خوب کار میکند، با شهود فیزیکی ما کاملاً سازگار نیست. یک جعبه سبز یا قرمز است؛ سطح آن یا الگویی دارد یا ندارد. گرافیک شیگرا رویکردی اساساً متفاوت اتخاذ میکند که در آن ویژگیها جزئی از جسم هندسیاند. در فصل ۸ گراف صحنه را بررسی میکنیم؛ ساختاری بنیادی در سامانههایی مانند Open Scene Graph که رویکرد شیگرای سطحبالای دیگری به گرافیک رایانهای ارائه میکند.
۲.۵ رنگ
رنگ یکی از جذابترین جنبههای ادراک انسان و گرافیک رایانهای است. میتوان از مدل سامانهٔ بینایی انسان در فصل ۱ یک مدل رنگی ساده اما سودمند به دست آورد. بهرهبرداری کامل از توان سامانهٔ بینایی انسان در گرافیک رایانهای به درکی بسیار عمیقتر از کالبدشناسی، فیزیولوژی و روانفیزیک انسان نیاز دارد. توسعهٔ پیشرفتهتر این موضوع در فصل ۶ ارائه میشود.
یک رنگ مرئی را میتوان با تابعی C(λ) مشخص کرد که طول موجهای تقریباً ۳۵۰ تا ۷۸۰ نانومتر را پوشش میدهد، مطابق شکل ۲.۲۴. مقدار تابع برای یک طول موج λ در طیف مرئی، شدت همان طول موج در رنگ را نشان میدهد.
شکل ۲.۲۴ — توزیع طیفی یک رنگ.
این تعریف از نظر توصیف فیزیکی رنگی که خواصش قابل اندازهگیری است دقیق است، اما نحوهٔ ادراک رنگ توسط انسان را در نظر نمیگیرد. همانطور که در فصل ۱ اشاره شد، سامانهٔ بینایی انسان سه نوع سلول مخروطی مسئول دید رنگ دارد و ادامهٔ بحث بر همین ویژگی استوار خواهد بود.