ادامهٔ ۲.۵ رنگ
سامانهٔ بینایی انسان بهجای دریافت کامل توزیع طیفی C(λ) برای یک رنگ، عملاً سه مقدار تحریک دریافت میکند که پاسخ سه نوع سلول مخروطی به آن رنگاند. این کاهش یک رنگ به سه مقدار، اصل بنیادین نظریهٔ سهرنگ را نتیجه میدهد: اگر دو رنگ مقادیر تحریک سهگانهٔ یکسانی ایجاد کنند، از نظر دیداری غیرقابل تمایزند.
پیامد این اصل آن است که از نظر نظری یک نمایشگر برای تولید سه مقدار تحریک لازم برای ناظر انسان فقط به سه رنگ اصلی نیاز دارد. شدت هر رنگ اصلی را تغییر میدهیم تا رنگ نهایی ساخته شود، همانگونه که برای CRT در فصل ۱ دیدیم. CRT نمونهای از «رنگ افزایشی» (Additive Color) است؛ رنگهای اصلی به یکدیگر افزوده میشوند تا رنگ ادراکشده شکل بگیرد. پروژکتورها و فیلم مثبت نیز از این روش استفاده میکنند. رنگهای اصلی در چنین سامانههایی معمولاً قرمز، سبز و آبیاند. در رنگ افزایشی، نورهای اصلی به زمینهای که در ابتدا سیاه است افزوده میشوند تا رنگ مطلوب ایجاد شود.
برای فرایندهایی مانند چاپ تجاری و نقاشی، مدل «رنگ کاهشی» (Subtractive Color) مناسبتر است. در اینجا از سطحی سفید، مانند کاغذ، آغاز میکنیم. رنگدانهها بعضی مؤلفههای نور تابیدهشده به سطح را جذب میکنند. اگر نور سفید به سطح بخورد، نقطهای زمانی قرمز دیده میشود که همهٔ مؤلفههای نور ورودی بهجز طول موجهای بخش قرمز طیف جذب و فقط قرمز بازتاب شود. رنگهای اصلی سامانههای کاهشی معمولاً رنگهای مکمل فیروزهای، ارغوانی و زرد هستند (CMY). فعلاً رنگ کاهشی را بیشتر بررسی نمیکنیم؛ کافی است بدانیم سامانهٔ افزایشی RGB دوگان سامانهٔ کاهشی CMY است؛ تمرین ۲.۸ را ببینید.
شکل ۲.۲۵ — تشکیل رنگ: (الف) رنگ افزایشی؛ (ب) رنگ کاهشی.
میتوان رنگ را نقطهای در یک جسم رنگی در نظر گرفت، مطابق شکل ۲.۲۶ و صفحهٔ رنگی ۲۱. این جسم در دستگاه مختصاتی متناظر با سه رنگ اصلی رسم میشود. فاصله در امتداد هر محور مقدار همان رنگ اصلی را نشان میدهد. اگر بیشینهٔ مقدار هر رنگ اصلی را 1 نرمال کنیم، هر رنگ قابل تولید با این سه رنگ اصلی بهصورت نقطهای در مکعب واحد نمایش داده میشود. رأسهای مکعب متناظرند با سیاه، قرمز، سبز، آبی، فیروزهای، ارغوانی، زرد و سفید. قطر اصلی مکعب مبدأ یا سیاه را به سفید متصل میکند.
شکل ۲.۲۶ — جسم رنگ RGB.
همهٔ رنگهای روی قطر اصلی مقادیر تحریک سهگانهٔ برابر دارند و بهصورت سایههای مختلف خاکستری دیده میشوند.
موضوعات بسیاری دربارهٔ رنگ باقی مانده که در فصل ۶ به آنها بازمیگردیم. بیشتر آنها به تفاوت میان مجموعههای رنگ اصلی یا محدودیتهای فیزیکی دستگاههای واقعی مربوطاند. بهویژه مجموعهٔ رنگهای قابل تولید توسط یک دستگاه، یعنی «گسترهٔ رنگ» (Color Gamut)، با دستگاه دیگر و نیز با گسترهٔ رنگ انسان یکسان نیست. همچنین مقادیر تحریک سهگانهٔ یکسان روی دو دستگاه مختلف الزاماً رنگ دیداری یکسانی تولید نمیکنند.
۲.۵.۱ رنگ RGB
اکنون میتوان نحوهٔ مدیریت رنگ در سامانهٔ گرافیکی را از دید برنامهنویس و API بررسی کرد. دو رویکرد وجود دارد. تأکید اصلی این کتاب بر مدل رنگ RGB است، زیرا فهم آن برای بحث سایهزنی ضروری خواهد بود. از نظر تاریخی، مدل رنگ شاخصیشده بهدلیل نیاز کمتر به حافظه و تعداد محدود رنگهای نمایشگرها در سختافزار آسانتر پشتیبانی میشد، اما در سامانههای جدید RGB به استاندارد رایج تبدیل شده است.
در یک سامانهٔ افزایشی RGB با سه رنگ اصلی، از نظر مفهومی بافرهای جداگانهای برای تصویر قرمز، سبز و آبی وجود دارد. هر پیکسل مؤلفههای مستقل R، G و B دارد که متناظر با مکانهایی در حافظهاند، شکل ۲.۲۷. یک سامانهٔ معمول ممکن است آرایهای 1280 × 1024 از پیکسلها داشته باشد و هر پیکسل ۲۴ بیت، یعنی سه بایت، باشد: یک بایت برای هر یک از قرمز، سبز و آبی. با کارتهای گرافیکی عمومی دارای چندین گیگابایت حافظه، ذخیره و نمایش بافر فریم با نرخ ویدئویی دیگر مشکل اساسی نیست.
بهعنوان برنامهنویس میخواهیم بتوانیم هر رنگ قابل ذخیره در بافر فریم را مشخص کنیم. در مثال ۲۴ بیتی، 2^24 رنگ ممکن وجود دارد که معمولاً «۱۶ میلیون رنگ» نامیده میشود. سامانههای دیگر ممکن است برای هر رنگ اصلی ۱۲ بیت یا بیشتر یا حتی ۴ بیت داشته باشند.
شکل ۲.۲۷ — سازمان رنگ RGB در بافر فریم.
چون API باید از جزئیات سختافزار مستقل باشد، بهتر است رنگ را مستقل از تعداد بیتهای بافر فریم مشخص کنیم و اجازه دهیم درایور و سختافزار نزدیکترین نمایش ممکن را تولید کنند. روش طبیعی استفاده از مکعب رنگ و تعیین هر مؤلفه بهصورت عددی میان 0.0 و 1.0 است؛ 1.0 بیشینه یا مقدار اشباعشدهٔ آن رنگ اصلی و 0.0 نبود آن مؤلفه را نشان میدهد.
در کاربردی که بخواهیم به هر رأس یک رنگ اختصاص دهیم، میتوان رنگها را در ساختار دادهای جداگانه قرار داد:
typedef vec3 color3;
color3 colors[3] = {color3(1.0, 0.0, 0.0), color3(0.0, 1.0, 0.0),
color3(0.0, 0.0. 1.0)};
این آرایه رنگهای قرمز، سبز و آبی را نگه میدارد. همچنین میتوان آرایهای واحد ساخت که هم مکان رأسها و هم رنگ آنها را داشته باشد. این دادهها به Shaderها فرستاده میشوند تا رنگها روی پیکسلهای بافر فریم اعمال شوند.
بعداً به سامانهٔ چهارمؤلفهای RGBA نیاز خواهیم داشت. مؤلفهٔ چهارم، A یا Alpha، همانند RGB در بافر فریم ذخیره میشود و با نسخههای چهاربعدی توابع رنگ تنظیم میگردد. در فصل ۷ کاربردهای Alpha، از جمله ترکیب تصاویر، بررسی میشود. در اینجا کافی است Alpha را هنگام مقداردهی اولیهٔ برنامهٔ OpenGL مشخص کنیم. اگر Blending فعال باشد، OpenGL مقدار Alpha را بهصورت مقدار کدری یا شفافیت تفسیر میکند. شفافیت و کدری مکمل یکدیگرند: جسم کاملاً کدر هیچ نوری را عبور نمیدهد و جسم کاملاً شفاف همهٔ نور را عبور میدهد. مقدار کدری از A=0.0 برای کاملاً شفاف تا A=1.0 برای کاملاً کدر تغییر میکند.
یکی از نخستین کارهای برنامه پاککردن ناحیهای از صفحه، یعنی پنجرهٔ رسم، برای نمایش خروجی است. همچنین هر بار که میخواهیم فریم تازهای رسم کنیم باید این پنجره پاک شود. با سامانهٔ RGBA، سامانهٔ گرافیکی و سیستمعامل میتوانند با تغییر کدری پنجره هنگام پاکشدن جلوههایی ایجاد کنند که پنجرهٔ رسم با پنجرههای زیر خود تعامل داشته باشد.
فراخوانی زیر رنگ پاکسازی را در RGB برابر سفید و با Alpha برابر کدر تعیین میکند:
glClearColor(1.0, 1.0, 1.0, 1.0);
سپس با glClear میتوان پنجره را سفید و یکپارچه کرد. توجه کنید Blending بهطور پیشفرض فعال نیست؛ بنابراین حتی اگر Alpha در glClearColor مقداری غیر از 1.0 داشته باشد، پنجرهٔ پیشفرض همچنان کدر خواهد بود.
۲.۵.۲ رنگ شاخصیشده
سامانههای گرافیکی اولیه بافر فریم کمعمقی داشتند. برای مثال ممکن بود وضوح مکانی 1280 × 1024 باشد اما هر پیکسل فقط ۸ بیت عمق داشته باشد. میشد این ۸ بیت را میان R، G و B تقسیم کرد، اما این روش در بیشتر کاربردها انعطاف کافی برای تخصیص رنگ نداشت.
«رنگ شاخصیشده» (Indexed Color) راهحلی بود که اجازه میداد برنامه دامنهٔ گستردهای از رنگها را نمایش دهد، به شرط آنکه تعداد رنگهای همزمان از تعداد شاخصهای قابل نمایش در یک پیکسل بیشتر نشود. این قابلیت دیگر بخشی از نسخههای جدید OpenGL نیست، اما همان روش را میتوان در برنامهٔ کاربردی پیادهسازی کرد.
میتوان آن را با نقاش رنگروغن مقایسه کرد. نقاش با ترکیب تعداد محدودی رنگدانه از تیوبها میتواند تقریباً بینهایت رنگ بسازد؛ یعنی یک Palette بالقوه بزرگ دارد. بااینحال در هر لحظه شاید بهدلیل تعداد محدود قلممو فقط چند رنگ را به کار ببرد. به همین شکل، اگر برای هر برنامه بتوان تعداد محدودی رنگ را از یک Palette بزرگ انتخاب کرد، در بیشتر مواقع میتوان تصاویر باکیفیتی ساخت.
در مدل رایانهای، پیکسلهای کمعمق را بهجای مقدار رنگ بهعنوان شاخص یک جدول رنگ تفسیر میکنیم. فرض کنید بافر فریم k بیت بهازای هر پیکسل دارد. مقدار یا شاخص هر پیکسل عدد صحیحی بین 0 و 2^k - 1 است. همچنین فرض کنید هر مؤلفهٔ رنگ با دقت m بیت نمایش داده میشود؛ پس 2^m مقدار قرمز، 2^m مقدار سبز و 2^m مقدار آبی داریم و در مجموع 2^(3m) رنگ بالقوه قابل تولید است، اما بافر فریم تنها میتواند 2^k تای آنها را در یک زمان مشخص کند.
این مسئله با جدول جستوجوی رنگ تعریفشده توسط کاربر با اندازهٔ 2^k × 3m حل میشود، شکل ۲.۲۸. برنامهٔ کاربر 2^k سطر جدول را با رنگهای مطلوب پُر میکند و برای هر مؤلفه R، G و B از m بیت استفاده میشود. سپس رنگ با شاخص خود مشخص میشود که به سطر مربوط در جدول اشاره میکند. اگر k=m=8 باشد، کاربر میتواند ۲۵۶ رنگ از میان حدود ۱۶ میلیون رنگ انتخاب کند و همین ۲۵۶ ورودی Palette او هستند.
در سامانههای پشتیبان حالت Color Index، رنگ جاری با تابعی انتخاب میشود که یکی از ورودیهای جدول را برمیگزیند. تنظیم یا تغییر جدول نیز مستلزم تعامل با سامانهٔ پنجرهای است. دشواری زمانی ایجاد میشود که سامانهٔ پنجرهای و سختافزار فقط تعداد محدودی رنگ را پشتیبانی کنند و یک جدول رنگ مشترک برای همهٔ پنجرهها داشته باشند یا ناچار باشند چند جدول را برای پنجرههای مختلف مدیریت کنند.
شکل ۲.۲۸ — جدول جستوجوی رنگ؛ هر ورودی به مؤلفههای قرمز، سبز و آبی با دقت m بیت نگاشت میشود.
شکل ۲.۲۹ — رنگ شاخصیشده: مقدار پیکسل در بافر فریم ورودی جدول جستوجوی R، G و B را تعیین میکند.
از نظر تاریخی حالت رنگ شاخصیشده مهم بود، زیرا حافظهٔ کمتر و سختافزار سادهتری برای بافر فریم لازم داشت. امروز هزینهٔ حافظه دیگر مسئلهٔ اصلی نیست و حالت شاخصیشده مشکلاتی دارد. در تصاویر پویا که باید سایهزنی شوند معمولاً به رنگهای بیشتری از ظرفیت این حالت نیاز است، و تعامل با سامانهٔ پنجرهای نیز از RGB پیچیدهتر میشود. بنابراین در بیشتر کتاب فرض میکنیم از RGB استفاده میکنیم.
استثنای اصلی روشی به نام «شبهرنگآمیزی» (Pseudocoloring) است که با تصویر تکرنگ آغاز میشود. مثلاً ممکن است مجموعهای از مقادیر اسکالر یک کمیت فیزیکی مانند دما داشته باشیم و بخواهیم آنها را رنگی نمایش دهیم. میتوان نگاشتی از این مقادیر به R، G و B ساخت که عملاً همان جدول جستوجوی رنگ Indexed Color است.
۲.۵.۳ تنظیم ویژگیهای رنگ
برای برنامهٔ نمونهٔ ساده از RGB استفاده میکنیم. سه ویژگی اصلی باید تنظیم شوند. نخست «رنگ پاکسازی» است که با فراخوانی مناسب روی سفید قرار میگیرد؛ ادامهٔ کد در صفحهٔ بعد آمده است.
رنگ پاکسازی سفید با فراخوانی زیر تنظیم میشود:
glClearColor(1.0, 1.0, 1.0, 1.0);
توجه کنید این تابع از رنگ RGBA استفاده میکند.
رنگی که برای رندر نقاط استفاده میشود در Shaderها تعیین میگردد. میتوان در برنامه یک رنگ RGB تعریف کرد:
typedef vec3 color3;
color3 point_color = color3(1.0, 0.0, 0.0);
یا همان رنگ را بهصورت RGBA نوشت:
typedef vec4 color4;
color4 point_color = color4(1.0, 0.0, 0.0, 1.0);
و سپس آن را به Vertex Shader فرستاد. میتوان رنگ را بهطور کامل در خود Shader نیز تعیین کرد. چند گزینه در ادامهٔ فصل بررسی میشود.
اندازهٔ نقاط رندرشده را میتوان با تابع زیر روی پهنای ۲ پیکسل قرار داد:
ویژگیهایی مانند اندازهٔ نقطه و پهنای خط برحسب اندازهٔ پیکسل تعریف میشوند. بنابراین اگر دو نمایشگر بهدلیل ابعاد فیزیکی و وضوح خود پیکسلهایی با اندازههای متفاوت داشته باشند، تصاویر رندرشده ممکن است کمی متفاوت به نظر برسند. بعضی APIهای گرافیکی برای تضمین خروجی یکسان در سامانههای مختلف ویژگیها را مستقل از دستگاه تعریف میکنند، اما در عمل تضمین نمایش کاملاً یکسان مسئلهٔ دشواری است. OpenGL تعادلی عملی میان رفتار مطلوب و محدودیتهای واقعی برگزیده است.
یادداشت ۶: اندازهٔ نقطه یکی از معدود متغیرهای حالت است که در نسخههای جدید OpenGL همچنان میتوان آن را مستقیماً با تابع OpenGL تنظیم کرد.
۲.۶ مشاهده
اکنون میتوانیم اطلاعات گرافیکی مختلفی را در جهان خود قرار دهیم و ظاهر مطلوب اشیا را توصیف کنیم، اما هنوز روش دقیقی برای تعیین اینکه کدام اشیا باید روی صفحه دیده شوند نداریم. همانگونه که محتوای یک عکس به جهت دوربین و عدسی آن وابسته است، در برنامهٔ گرافیکی نیز باید تصمیمهای مشابهی دربارهٔ مشاهده بگیریم.
مفهوم بنیادی برگرفته از مدل دوربین مصنوعی فصل ۱ این است که تعریف اشیای صحنه کاملاً مستقل از تعریف دوربین است. پس از تعریف صحنه و دوربین میتوان تصویر را تشکیل داد. دوربین با نوردهی فیلم تصویر میسازد؛ سامانهٔ رایانهای با اجرای زنجیرهای از عملیات در خط لوله. برنامهٔ کاربردی فقط لازم است پارامترهای اشیا و دوربین را مشخص کند، همانگونه که عکاس معمولی به نتیجهٔ عکس اهمیت میدهد نه جزئیات کار شاتر یا واکنش فتوشیمیایی فیلم با نور.
در تشکیل تصویر رایانهای شرایط مشاهدهٔ پیشفرضی وجود دارد که مشابه تنظیمات یک دوربین پایه با عدسی ثابت است. اما دوربین با عدسی و مکان ثابت ما را مجبور میکند برای گرفتن عکس، جهان را متناسب با دوربین تنظیم کنیم. برای مثال، فیل باید بهاندازهٔ کافی دور باشد و مورچه نسبتاً نزدیک به عدسی قرار گیرد. ترجیح میدهیم بتوانیم عدسی یا تنظیمات مشاهده را تغییر دهیم تا تشکیل تصویر از مجموعه اشیا آسانتر شود. همین موضوع در سامانهٔ گرافیکی نیز صادق است.
۲.۶.۱ نمای متعامد
سادهترین نما و نمای پیشفرض OpenGL «تصویرسازی متعامد» (Orthographic Projection) است. این و دیگر تصویرسازیها در فصل ۴ با جزئیات بررسی میشوند، اما برای آغاز برنامهنویسی سهبعدی در همین فصل معرفی کوتاهی لازم است.
از نظر ریاضی، تصویرسازی متعامد همان حالتی است که دوربین مصنوعی عدسی تلهفوتویی با فاصلهٔ کانونی نامتناهی داشته باشد و دوربین را بینهایت دور از اشیا قرار دهیم. این اثر را میتوان با ثابت نگهداشتن صفحهٔ تصویر و دورکردن دوربین از آن تقریب زد، مطابق شکل ۲.۳۰. در حد نهایی همهٔ Projectorها موازی میشوند و مرکز تصویرسازی جای خود را به «جهت تصویرسازی» میدهد.
بهجای تصور دوربینی در بینهایت، فرض کنید پرتوهای تصویرسازی همگی موازی محور مثبت z و صفحهٔ تصویرسازی در z = 0 باشند، شکل ۲.۳۱. Projectorها نهتنها بر صفحهٔ تصویرسازی عمودند، بلکه میتوان صفحه را در امتداد محور z جابهجا کرد بیآنکه محل تقاطع پرتوها با آن در مختصات x و y تغییر کند.
برای مشاهدهٔ متعامد میتوان دوربین ویژهای تصور کرد که روی خود صفحهٔ تصویرسازی قرار دارد؛ وضعیتی که برای نماهای دیگر ممکن نیست. بیان دقیقتر آن است که نقطهٔ مرجعی در صفحهٔ تصویرسازی داریم که اندازههای حجم مشاهده نسبت به آن سنجیده میشوند و یک جهت تصویرسازی نیز مشخص است. در OpenGL نقطهٔ مرجع ابتدا در مبدأ قرار دارد و دوربین در جهت منفی z نگاه میکند، شکل ۲.۳۲.
تصویرسازی متعامد نقطهٔ (x, y, z) را به (x, y, 0) میبرد، شکل ۲.۳۳. اگر در دو بعد کار کنیم و همهٔ رأسها روی صفحهٔ z = 0 باشند، نقطه و تصویر آن یکساناند؛ بااینحال میتوان از تمام سازوکار سامانهٔ گرافیکی سهبعدی برای تولید تصویر استفاده کرد.
در OpenGL حجم مشاهدهٔ متعامد پیشفرض یک متوازیالسطوح قائم است؛ مکعبی که صفحههای مرزی آن عبارتاند از:
شکل ۲.۳۰ — ایجاد نمای متعامد با دورکردن دوربین از صفحهٔ تصویرسازی.
تصویرسازی متعامد فقط اشیایی را «میبیند» که در حجم مشاهدهٔ تعیینشده قرار دارند. برخلاف دوربین واقعی، این تصویرسازی میتواند اشیای پشت دوربین را نیز در بر گیرد. چون صفحهٔ z = 0 میان -1 و 1 قرار دارد، صفحهٔ دوبعدی ما حجم مشاهده را قطع میکند.
در فصلهای ۳ و ۴ با استفاده از تبدیلها نماهای دیگری میسازیم. فعلاً اشیا را بهگونهای مقیاس و جایگذاری میکنیم که بخشهای موردنظر داخل حجم پیشفرض قرار گیرند.
شکل ۲.۳۱ — Projectorهای متعامد با صفحهٔ تصویرسازی z = 0.
شکل ۲.۳۲ — دوربین پیشفرض و حجم مشاهدهٔ متعامد؛ حجم با حدهای left, right, bottom, top, near, far توصیف میشود.
شکل ۲.۳۳ — تصویرسازی متعامد: (x, y, z) به (x, y, 0) نگاشت میشود.
شکل ۲.۳۴ — حجم مشاهده با مستطیل مشاهده روی صفحهٔ z = 0.
شکل ۲.۳۵ — مشاهدهٔ دوبعدی: (الف) اشیا پیش از برش؛ (ب) تصویر پس از برش.
۲.۶.۲ مشاهدهٔ دوبعدی
به یاد داشته باشید در دیدگاه ما گرافیک دوبعدی حالت خاصی از گرافیک سهبعدی است. ناحیهٔ مشاهده روی صفحهٔ z = 0 و داخل یک حجم مشاهدهٔ سهبعدی قرار دارد، مطابق شکل ۲.۳۴.
میتوان مشاهدهٔ دوبعدی را مستقیمتر نیز در نظر گرفت: ناحیهای مستطیلی از جهان دوبعدی را انتخاب و محتوای آن را به نمایشگر منتقل میکنیم، شکل ۲.۳۵. ناحیهای از جهان که تصویر میشود «مستطیل مشاهده» یا «مستطیل برش» نام دارد. اشیای داخل مستطیل در تصویر ظاهر میشوند؛ اشیای بیرون برش میخورند و نمایش داده نمیشوند؛ و اشیایی که از مرز مستطیل عبور میکنند فقط تا حدی دیده میشوند. اندازهٔ پنجره روی نمایشگر و محل قرارگیری آن تصمیمهایی مستقلاند که در بخش ۲.۷ بررسی میشوند.
۲.۷ توابع کنترل
تقریباً به پایان نخستین برنامه رسیدهایم، اما باید حداقل تعامل لازم با سامانهٔ پنجرهای و سیستمعامل را نیز بررسی کنیم. اگر جزئیات محیطی مشخص، مانند X Window System روی Linux یا Microsoft Windows روی PC، را مطالعه کنیم میبینیم رابط برنامهنویس میان سامانهٔ گرافیکی، سیستمعامل و سامانهٔ پنجرهای میتواند پیچیده باشد. استفادهٔ کامل از امکانات آنها به دانش اختصاصی همان محیط نیاز دارد و جزئیات میان دو محیط مختلف نیز فرق میکند؛ ورود به این تفاوتها کمک چندانی به فهم گرافیک رایانهای نمیکند.
بهجای آن، حداقل عملیات لازم از دید برنامهٔ کاربردی گرافیکی را بررسی میکنیم. پیشتر OpenGL Utility Toolkit یا GLUT را معرفی کردیم؛ کتابخانهای از توابع که رابط سادهای میان سامانهها فراهم میکند. جزئیات وابسته به سیستمعامل یا سامانهٔ پنجرهای درون پیادهسازی GLUT پنهاناند و بخشی از API آن نیستند. از نظر عملی فقط یک کتابخانهٔ دیگر به مسیر استاندارد جستوجوی کتابخانهها اضافه میشود. GLUT برای درک تعاملهای رایج در سامانههای گرافیکی تعاملی نوین مفید است و برنامههای نوشتهشده با آن باید در سامانههای پنجرهای مختلف اجرا شوند.
۲.۷.۱ تعامل با سامانهٔ پنجرهای
واژهٔ «پنجره» در منابع گرافیک و Workstation چند معنی دارد. در این کتاب Window یا Screen Window به ناحیهای مستطیلی از نمایشگر گفته میشود. فقط نمایشگرهای رستری را در نظر میگیریم. پنجره دارای ارتفاع و عرض است و چون محتوای بافر فریم را نمایش میدهد، مکانهای داخل آن با مختصات پنجره یا صفحه و با واحد پیکسل اندازهگیری میشوند.
در محیطهای جدید میتوان چندین پنجره را همزمان روی مانیتور داشت و هر کدام کاربردی متفاوت، از ویرایش فایل تا پایش سامانه، داشته باشند. «سامانهٔ پنجرهای» به چنین محیط چندپنجرهای، مانند X Window System یا Microsoft Windows، گفته میشود. پنجرهٔ خروجی گرافیکی یکی از پنجرههایی است که سامانهٔ پنجرهای مدیریت میکند. از دید سامانه، این فقط نوعی پنجره است که میتوان در آن گرافیک رندر کرد.
مختصات موقعیتهای درون پنجره نسبت به یکی از گوشهها سنجیده میشوند و باید دقت کنیم کدام گوشه مبدأ است. در علوم و مهندسی معمولاً گوشهٔ پایین-چپ مبدأ (0,0) است. بااینحال تقریباً همهٔ سامانههای رستری نمایش را مانند تلویزیون تجاری از بالا به پایین و چپ به راست پیمایش میکنند و از این دیدگاه گوشهٔ بالا-چپ مبدأ طبیعی است. توابع OpenGL مبدأ را پایین-چپ فرض میکنند، درحالیکه اطلاعات بازگشتی از سامانهٔ پنجرهای، مانند مکان ماوس، اغلب مبدأ بالا-چپ دارند و باید میان این دو دستگاه مختصات تبدیل انجام شود.
یادداشت ۷: مختصات پنجره در OpenGL سهبعدیاند، درحالیکه مختصات صفحه دوبعدی است. هر دو برای x و y از واحد پیکسل استفاده میکنند، اما مختصات پنجره اطلاعات عمق را نیز حفظ میکند.
اگرچه نمایشگر ممکن است مثلاً وضوح 1280 × 1024 داشته باشد، پنجرهٔ برنامه میتواند هر اندازهای داشته باشد. ازاینرو بافر فریم واقعی باید وضوحی دستکم برابر نمایشگر داشته باشد. از نظر مفهومی اگر پنجرهای 300 × 400 پیکسلی داشته باشیم، میتوان آن را معادل بافر فریمی 300 × 400 دانست، هرچند فقط بخشی از بافر فریم واقعی را اشغال میکند.
پیش از بازکردن پنجره باید تعامل میان سامانهٔ پنجرهای و OpenGL آغاز شود. در GLUT این کار با فراخوانی زیر انجام میشود:
glutInit(int *argc, char **argv);
دو آرگومان اجازه میدهند پارامترهای خط فرمان همانند تابع استاندارد main در C منتقل شوند و معمولاً همان آرگومانهای main هستند. سپس میتوان با تابع GLUT زیر پنجرهٔ OpenGL ساخت:
glutCreateWindow(char *title);
رشتهٔ title عنوان بالای پنجره را مشخص میکند.
پنجرهٔ ساختهشده اندازه، موقعیت و ویژگیهای پیشفرض مانند استفاده از RGB دارد. پیش از ایجاد پنجره میتوان با توابع GLUT این پارامترها را تغییر داد. برای نمونه:
glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);
glutInitWindowSize(640, 480);
glutInitWindowPosition(0, 0);
این کد پنجرهای با عرض ۶۴۰ و ارتفاع ۴۸۰ پیکسل در گوشهٔ بالا-چپ نمایشگر میسازد. حالت RGB بهجای Indexed (GLUT_INDEX)، بافر عمق برای حذف سطح پنهان، و بافر دوگانه بهجای بافر منفرد (GLUT_SINGLE) درخواست شدهاند. پیشفرضها، که فعلاً کافیاند، RGB، بدون حذف سطح پنهان و بافر منفرد هستند. نوشتن صریح گزینهها خوانایی کد را بیشتر میکند. پارامترهای glutInitDisplayMode با OR منطقی بیتی با هم ترکیب میشوند.
۲.۷.۲ نسبت ابعاد و Viewport
«نسبت ابعاد» (Aspect Ratio) یک مستطیل نسبت عرض به ارتفاع آن است. استقلال تعریف جسم، مشاهده و پنجرهٔ Workstation میتواند زمانی عارضه ایجاد کند که نسبت ابعاد مستطیل مشاهده، که با پارامترهای دوربین مشخص میشود، با نسبت ابعاد پنجرهٔ تعیینشده با glutInitWindowSize برابر نباشد.
اگر این دو نسبت متفاوت باشند، مانند شکل ۲.۳۶، اشیا روی صفحه اعوجاج پیدا میکنند. علت آن حالت پیشفرض عملکرد است که کل مستطیل برش را به کل پنجرهٔ نمایش نگاشت میکند. تنها راه قرار دادن کامل محتوای یک مستطیل با نسبت ابعاد متفاوت در پنجره، کشیدن یا فشردن آن است. با برابرکردن نسبت ابعاد مستطیل برش و پنجره میتوان از این اعوجاج جلوگیری کرد.
شکل ۲.۳۶ — ناهماهنگی نسبت ابعاد: (الف) مستطیل مشاهده؛ (ب) پنجرهٔ نمایش.
روش انعطافپذیرتر استفاده از «ناحیهٔ دید» (Viewport) است. Viewport ناحیهای مستطیلی درون پنجرهٔ نمایش است. بهطور پیشفرض کل پنجره را پوشش میدهد، اما با تابع زیر میتوان آن را به هر مستطیل کوچکتری برحسب پیکسل تنظیم کرد:
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h);
(x,y) گوشهٔ پایین-چپ Viewport نسبت به گوشهٔ پایین-چپ پنجره و w و h عرض و ارتفاع آن هستند. همهٔ این نوعها صحیحاند، زیرا موقعیت و فاصله برحسب پیکسل بیان میشود. بدویها در Viewport نمایش داده میشوند، شکل ۲.۳۷. برای یک پنجره میتوان ارتفاع و عرض Viewport را متناسب با نسبت ابعاد مستطیل برش تنظیم و از اعوجاج جلوگیری کرد.
Viewport بخشی از حالت سامانه است. اگر میان رندر اشیای مختلف Viewport را عوض کنیم یا همان اشیا را با Viewportهای متفاوت دوباره رندر کنیم، میتوان چند ناحیهٔ دید با تصاویر متفاوت در بخشهای مختلف پنجره ایجاد کرد. کاربردهای بیشتر Viewport در فصل ۳ و هنگام تغییر تعاملی اندازه و شکل پنجره بررسی میشوند.
۲.۷.۳ توابع main، display و init
از نظر نظری باید بتوان کد سادهٔ مقداردهی اولیه را با کد بخش ۲.۱ ترکیب کرد و برنامهٔ کامل OpenGL برای واشر سیرپینسکی ساخت؛ اما در سامانهٔ نوین دو مسئله وجود دارد: یکی عمومی و مربوط به همهٔ سامانههای گرافیکی، دیگری مربوط به تعامل با سامانهٔ پنجرهای زیرین.
شکل ۲.۳۷ — نگاشت پنجرهٔ برش به Viewport داخل پنجرهٔ گرافیکی.
سازوکار پایهٔ نمایش ما ساخت ساختار دادهای شامل هندسه و ویژگیهای لازم برای تعریف بدوی و نحوهٔ نمایش آن است. سپس این ساختار به Shaderها فرستاده میشود تا دادهها را پردازش و نتیجه را نمایش دهند. پس از ارسال داده به Shaderها، برنامهٔ کاربردی برای انجام کارهای دیگر آزاد است. در برنامهٔ تعاملی معمولاً تولید بدویهای بیشتری ادامه پیدا میکند.
اما در برنامهٔ نمونه فقط چند بدوی رسم میکنیم و کار تمام میشود. با پایان برنامه، پنجرهٔ کاربردی ممکن است پیش از آنکه فرصت دیدن خروجی داشته باشیم ناپدید شود. راهحل ساده میتواند افزودن تأخیری مانند sleep(enough_time) باشد، اما برای برنامههای غیرابتدایی سازوکار پیشرفتهتری لازم است.
بیشتر سامانههای گرافیکی و پنجرهای از «پردازش رویداد» (Event Processing) استفاده میکنند که کنترل تعاملی برنامه را ممکن میسازد. رویدادها تغییرهایی هستند که سیستمعامل تشخیص میدهد؛ مانند فشردن کلید، کلیک یا حرکت ماوس، یا Minimize/Iconify کردن پنجره. رویدادها متنوعاند و معمولاً فقط زیرمجموعهای از آنها برای یک برنامهٔ گرافیکی مهم است. رویداد ممکن است دادهای همراه خود داشته باشد؛ مثلاً با فشردن کلید، کد آن کلید ذخیره میشود.
هنگام وقوع، رویدادها در «صف رویداد» قرار میگیرند که برنامه یا سیستمعامل میتواند آن را بررسی کند. هر رویداد ممکن است نادیده گرفته شود یا عملی را آغاز کند. برنامهای که از صفحهکلید استفاده نمیکند رویدادهای کلید را نادیده میگیرد؛ برنامهای دیگر ممکن است همین رویدادها را برای کنترل جریان اجرا به کار برد.
در GLUT با تابع زیر حلقهٔ پردازش رویداد آغاز میشود:
اگر رویدادی برای پردازش وجود نداشته باشد، برنامه در حالت انتظار میماند و گرافیک روی صفحه باقی میماند تا اجرا با روشی خارجی، مثلاً کلید ویژه یا ترکیبی مانند Control-C، خاتمه یابد.
اگر رویدادهایی در صف باشند، برنامه از طریق توابعی به نام «Callback» به آنها پاسخ میدهد. هر تابع Callback با نوع خاصی از رویداد مرتبط است. یک برنامهٔ تعاملی معمولاً Callback ماوس و شاید صفحهکلید دارد. برای مثال سادهٔ ما فقط یک Callback لازم است: «Display Callback». این رویداد زمانی ایجاد میشود که برنامه یا سیستمعامل تشخیص دهد گرافیک پنجره باید دوباره رسم شود. یکی از این زمانها هنگام مقداردهی اولیه و ساخت پنجره است. بنابراین تقریباً هر برنامه باید تابع Display Callback داشته باشد.
تابع Display Callback با تابع GLUT زیر معرفی و در سامانهٔ پنجرهای ثبت میشود:
void glutDisplayFunc(void (*func)(void));
تابع func هر زمان که سامانهٔ پنجرهای تشخیص دهد پنجرهٔ OpenGL باید دوباره نمایش داده شود فراخوانی میشود. چون یکی از این زمانها نخستین بازشدن پنجره است، اگر در مثال غیرتعاملی همهٔ دستورات گرافیکی را در همین تابع قرار دهیم، func یک بار اجرا و واشر رسم میشود. استفاده از تابع Display فقط برای نظم کد نیست؛ GLUT آن را الزام میکند. Callback نمایش هنگام جابهجایی پنجره یا کناررفتن پنجرهای که روی آن قرار داشته نیز ممکن است اجرا شود.
تابع main زیر برای بیشتر برنامههای غیرتعاملی مناسب است:
#include <glew.h>
#include <GL/glut.h>
int main(int argc, char **argv)
{
glutInit(&argc, argv);
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB);
glutInitWindowSize(500, 500);
glutInitWindowPosition(0, 0);
glutCreateWindow("simple OpenGL example");
glewInit();
glutDisplayFunc(display);
init();
glutMainLoop();
}
تابع مقداردهی اولیهٔ init() را برای تنظیم متغیرهای حالت OpenGL مربوط به مشاهده و ویژگیها به کار میبریم؛ یعنی پارامترهایی که ترجیح میدهیم مستقل از تابع نمایش فقط یک بار تنظیم شوند. فایل Include استاندارد (.h) مربوط به GLUT پیش از تعریف توابع بارگذاری میشود. در بیشتر پیادهسازیها دستور کامپایلر زیر
فایلهای سرآیند کتابخانهٔ GLUT و کتابخانهٔ OpenGL (gl.h) را نیز اضافه میکند. تعریفهای Macro مربوط به مقادیر استاندارد ما، مانند GL_LINES و GL_RGB، در همین فایلها قرار دارند. اگر از کتابخانهٔ GLEW استفاده کنیم، معمولاً کافی است فایل Include آن را اضافه و glewInit را اجرا کنیم.
۲.۷.۴ ساختار برنامه
هر برنامهای که مینویسیم ساختاری مشابه برنامهٔ واشر خواهد داشت. همواره از Toolkit مربوط به GLUT استفاده میکنیم. در نتیجه تابع main شامل فراخوانی توابع GLUT برای برپا کردن پنجره یا پنجرهها و اطمینان از پشتیبانی محیط محلی از ویژگیهای نمایشی موردنیاز است. main همچنین Callbackهای موردنیاز و توابع Callback را مشخص میکند. هر برنامه باید یک Display Callback داشته باشد و بیشتر برنامهها Callbackهای دیگری نیز برای فراهمکردن تعامل خواهند داشت.
تابع init گزینههای کاربر را، معمولاً با توابع OpenGL در کتابخانهٔ GL، تنظیم میکند. هرچند این گزینهها را میتوان در main نیز تنظیم کرد، جدا نگه داشتن توابع GLUT از توابع OpenGL خواناتر است. در بیشتر برنامهها خروجی گرافیکی در Display Callback تولید میشود.
هر برنامهٔ کاربردی، هرقدر هم ساده باشد، باید هم «شیدر رأس» (Vertex Shader) و هم «شیدر قطعه» (Fragment Shader) ارائه کند. راهاندازی Shaderها چند مرحله دارد، از جمله خواندن کد Shader از فایلها، کامپایل کد و Link کردن Shaderها با برنامهٔ کاربردی. این مراحل در بیشتر برنامهها تقریباً یکساناند؛ بنابراین این کد را در تابعی به نام initShaders قرار میدهیم. این عملیات به چند تابع OpenGL نیاز دارد که ارتباط مستقیمی با خود گرافیک ندارند؛ جزئیات این توابع در ضمیمهٔ A آمده است.
۲.۸ برنامهٔ واشر
اکنون میتوانیم برنامهٔ واشر را کامل کنیم. نقاط را پیشتر ساخته و در یک آرایه قرار دادهایم. حال باید این دادهها را به GPU منتقل و رندر کنیم. ابتدا یک «شیء آرایهٔ رأس» (Vertex-Array Object) میسازیم تا بتوانیم دادههای وابسته به یک آرایهٔ رأس را بهصورت یک مجموعه نگه داریم. استفاده از چند Vertex-Array Object جابهجایی میان آرایههای رأس مختلف را آسان میکند.
از glGenVertexArrays برای بهدستآوردن یک نام استفادهنشده برای بافر استفاده میکنیم. نخستین بار که glBindVertexArray برای یک نام معین اجرا میشود، شیء ساخته میشود. فراخوانیهای بعدی همین تابع، شیء نامگذاریشده را فعال میکنند. در این مثال فقط به یک Vertex Array Buffer نیاز داریم که آن را به شکل زیر آماده میکنیم:
GLuint abuffer;
glGenVertexArrays(1, &abuffer);
glBindVertexArray(abuffer);
سپس یک Buffer Object روی GPU میسازیم و دادهها را در آن قرار میدهیم. پس از تولید نقاط، سه تابع زیر را فراخوانی میکنیم:
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(points),
points, GL_STATIC_DRAW);
ابتدا glGenBuffers یک شناسهٔ استفادهنشده برای Buffer Object در اختیار ما میگذارد و آن را در متغیر buffer قرار میدهد. تابع glBindBuffer با شناسهٔ تولیدشده توسط glGenBuffers بافر را میسازد. نوع GL_ARRAY_BUFFER مشخص میکند دادهٔ بافر از نوع ویژگیهای رأس (Vertex Attribute Data) است، نه یکی از انواع ذخیرهسازی دیگری که بعداً خواهیم دید.
در پایان با glBufferData حافظهٔ کافی روی GPU برای دادهها تخصیص میدهیم و اشارهگری به آرایهٔ نگهدارندهٔ داده ارائه میکنیم. وقتی داده در حافظهٔ GPU قرار گرفت، ممکن است مانند این مثال فقط یک بار آن را نمایش دهیم؛ اما در برنامههای واقعیتر میتوانیم داده را تغییر دهیم، بارها دوباره نمایش دهیم و حتی آن را از GPU به CPU بازگردانیم.
GPUهای نوین میتوانند شیوهٔ ذخیرهسازی داده را بسته به نوع برنامه تغییر دهند تا کارایی افزایش یابد. آخرین پارامتر glBufferData سرنخی دربارهٔ نحوهٔ استفادهٔ موردنظر برنامه از داده است. در اینجا داده را یک بار میفرستیم و نمایش میدهیم، بنابراین GL_STATIC_DRAW انتخاب مناسبی است. کد محاسبهٔ نقاط و ساخت Buffer Object میتواند بخشی از مقداردهی اولیه باشد.
۲.۸.۱ رندر کردن نقاط
هنگامی که بخواهیم نقاط خود را نمایش دهیم، میتوانیم از تابع زیر استفاده کنیم:
glDrawArrays(GL_POINTS, 0, N);
این تابع سبب میشود N داده، با آغاز از نخستین نقطه، رندر شود. مقدار پارامتر اول، یعنی GL_POINTS، به GPU میگوید داده باید برای نمایش نقاط مستقل به کار رود، نه بدویهای دیگری مانند خط یا چندضلعی که ممکن است با همان داده توصیف شوند. بنابراین یک Display Callback ساده چنین است:
void mydisplay(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_POINTS, 0, N);
glFlush();
}
ابتدا Framebuffer را پاک میکنیم و سپس دادهٔ نقاط موجود روی GPU را رندر میکنیم. glFlush تضمین میکند همهٔ دادهها در سریعترین زمان ممکن رندر شوند. اگر آن را حذف کنید، برنامه همچنان باید درست کار کند، اما در محیطی شلوغ یا شبکهای ممکن است تأخیر مشاهده کنید.
با این حال این فقط آغاز ماجراست. برای اینکه پیکسلهای مناسب در Framebuffer نمایش داده شوند، فرایند رندر باید از خط لولهٔ Vertex Shader، Rasterizer و Fragment Shader عبور کند. چون مثال ما فقط از نقطه استفاده میکند، کافی است Shaderهای بسیار سادهای بسازیم و کل برنامه را کنار هم قرار دهیم. حتی با وجود تقریباً بدیهیبودن Shaderها، برای یک برنامهٔ کامل باید هم Vertex Shader و هم Fragment Shader ارائه کنیم؛ Shader پیشفرضی وجود ندارد.
۲.۸.۲ Vertex Shader
تنها اطلاعاتی که در Buffer Object خود قرار دادهایم موقعیت هر نقطه است. هنگام اجرای glDrawArrays، هر یک از رأسهای NumPoints یک اجرای Vertex Shader ایجاد میکند که باید آن را فراهم کنیم. اگر تعیین رنگ را به Fragment Shader واگذار کنیم، تمام کاری که Vertex Shader باید انجام دهد انتقال موقعیت رأس به Rasterizer است. بعداً وظایف بسیار بیشتری برای Vertex Shader خواهیم دید، اما حداقلِ مطلق آن ارسال یک موقعیت رأس به Rasterizer است.
Shader را با «زبان سایهزنی OpenGL» یا GLSL (OpenGL Shading Language) مینویسیم؛ زبانی شبیه C که با آن هم Vertex Shader و هم Fragment Shader نوشته میشود. هنگامی که به Shaderهای پیچیدهتر برسیم GLSL را با جزئیات بیشتری بررسی خواهیم کرد. کد یک Vertex Shader سادهٔ عبوری (Pass-Through) چنین است:
in vec4 vPosition;
void main()
{
gl_Position = vPosition;
}
هر Shader یک برنامهٔ کامل است که main نقطهٔ ورود آن محسوب میشود. GLSL انواع دادهٔ C را با انواع ماتریسی و برداری گسترش میدهد. نوع vec4 معادل یک کلاس C++ برای آرایهای چهارعضوی از float است. در سمت برنامه، انواع مشابهی را در vec.h فراهم کردهایم و انواع بیشتری را در فصل ۳ معرفی خواهیم کرد.
موقعیت رأس ورودی با بردار چهاربعدی vPosition داده میشود که در تعریف آن کلیدواژهٔ in نشان میدهد مقدار آن هنگام آغاز Shader بهعنوان ورودی تحویل میشود. یک متغیر حالت ویژه در Shader داریم: gl_Position، یعنی موقعیتی که به Rasterizer ارسال میشود و هر Vertex Shader باید آن را خروجی دهد. چون gl_Position برای OpenGL شناختهشده است، لازم نیست آن را در Shader تعریف کنیم.
بهطور کلی یک Vertex Shader نمایش موقعیت رأس را از هر دستگاه مختصاتی که در آن تعریف شده، برای Rasterizer به نمایش در مختصات Clip تبدیل میکند. با این حال چون در برنامهٔ خود مقادیر را مستقیماً در مختصات Clip مشخص کردهایم، Shader نیازی به تغییر مقادیر ورودی ندارد و فقط آنها را از طریق gl_Position عبور میدهد.
هنوز باید میان آرایهٔ points در برنامه و آرایهٔ ورودی vPosition در Shader ارتباط برقرار کنیم. این کار را پس از کامپایل و Link کردن Shaderها انجام خواهیم داد. نخست Fragment Shader را بررسی میکنیم.
۲.۸.۳ Fragment Shader
هر اجرای Vertex Shader رأسی را خروجی میدهد که پیش از رسیدن به Rasterizer از مراحل مونتاژ بدوی (Primitive Assembly) و Clipping عبور میکند. Rasterizer برای هر بدوی داخل حجم برش Fragmentهایی تولید میکند. هر Fragment باعث یک اجرای Fragment Shader میشود. حداقل کاری که هر اجرای Fragment Shader باید انجام دهد، خروجیدادن رنگی برای Fragment است، مگر آنکه قرار باشد Fragment دور انداخته شود.
یک Fragment Shader حداقلی در GLSL چنین است:
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
تمام کاری که این Shader انجام میدهد اختصاص یک رنگ چهاربعدی RGBA به هر Fragment از طریق متغیر داخلی gl_FragColor است. مؤلفهٔ A رنگ میزان Opacity یا کدری آن است. میخواهیم نقاط مات باشند و نیمهشفاف نباشند، پس A = 1.0 قرار میدهیم. قرار دادن R برابر 1.0 و دو مؤلفهٔ رنگی دیگر برابر 0.0 هر Fragment را قرمز میکند.
۲.۸.۴ ترکیب اجزا
اکنون اجزای لازم را در اختیار داریم، اما باید آنها را به هم متصل کنیم. بهطور مشخص باید Shaderها را کامپایل کنیم، متغیرهای برنامهٔ کاربردی را به همتایان آنها در Shader متصل کنیم و همهچیز را Link کنیم. از حداقل موارد لازم شروع میکنیم. Shaderها باید کامپایل و Link شوند. در بیشتر موارد این عملیات را بخشی از مقداردهی اولیه قرار میدهیم تا کد لازم در تابعی به نام initShader قرار گیرد که از یک برنامه به برنامهٔ دیگر تقریباً بدون تغییر باقی میماند.
۲.۸.۵ تابع initShader
یک برنامهٔ کاربردی معمولی سه بخش مجزا دارد: برنامهٔ کاربردی شامل تابع main و توابع دیگری مانند init، یک Vertex Shader، و یک Fragment Shader. بخش نخست مجموعهای از توابع C یا C++ است، در حالی که Shaderها با GLSL نوشته میشوند. برای بهدستآوردن ماژولی که بتوان اجرا کرد، باید این اجزا را متصل کنیم؛ فرایندی که شامل خواندن کد مبدأ از فایلها، کامپایل اجزای جداگانه و Link کردن همهٔ آنهاست. این فرایند را میتوان از طریق برنامهٔ کاربردی و مجموعهای از توابع OpenGL کنترل کرد که در فصل ۳ با جزئیات بررسی خواهند شد. در اینجا توصیف کوتاه مراحل کافی است.
نخست یک ظرف به نام «Program Object» برای نگهداری Shaderها و دو Shader Object، یکی برای هر نوع Shader، میسازیم. Program Object شناسهای صحیح دارد که میتوانیم با آن در برنامه به شیء ارجاع دهیم. پس از ساخت این اشیا، Shaderها را به Program Object پیوست (Attach) میکنیم. کد مبدأ Shader معمولاً در فایلهای متنی استاندارد قرار دارد. آنها را به رشتههایی میخوانیم که میتوانند به Program متصل و کامپایل شوند. اگر کامپایل موفق باشد، برنامهٔ کاربردی و Shaderها میتوانند با هم Link شوند.
با فرض اینکه کد مبدأ Vertex Shader در فایل vshader.glsl و Fragment Shader در فایل fshader.glsl باشد، مراحل بالا را میتوان در تابع main برنامه با فراخوانیای به شکل زیر اجرا کرد:
GLuint program;
program = InitShader("vsource.glsl", "fsource.glsl");
هنگامی که Program Object و Shaderها را Link میکنیم، نام متغیرهای Shader به Indexهایی در جدولهایی متصل میشود که در فرایند Link ساخته شدهاند. تابع glGetAttribLocation Index یک متغیر Attribute، مانند ویژگی موقعیت رأس vPosition در Vertex Shader ما، را برمیگرداند.
از دید برنامهٔ کاربردی یا Client باید دو کار انجام دهیم: ویژگیهای رأسی را که در Shaderها وجود دارند با glEnableVertexAttribArray فعال کنیم، و قالب داده در Vertex Array را با glVertexAttribPointer توصیف کنیم؛ مانند کد زیر:
GLuint loc;
loc = glGetAttribLocation(program, "vPosition");
glEnableVertexAttribArray(loc);
glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET(0));
در glVertexAttribPointer پارامترهای دوم و سوم مشخص میکنند آرایهٔ points آرایهای دوبعدی از floatهاست. پارامتر چهارم میگوید نمیخواهیم داده به بازهٔ (0.0, 1.0) نرمال شود، در حالی که پارامتر پنجم بیان میکند مقادیر آرایه پشتسرهم (Contiguous) هستند. در مثالهای بعدی با دادههای غیرپیوسته نیز کار خواهیم کرد. پارامتر آخر نشانی محلی در Buffer است که داده از آن آغاز میشود. چون در این مثال فقط یک آرایهٔ داده، یعنی points، داریم مقدار صفر مناسب است.
راهبرد مقاومتر آن است که Buffer Offset را مشخص کنیم و آن را به شکل زیر به کار ببریم:
#define BUFFER_OFFSET(bytes) ((GLvoid*) (bytes))
glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET(0));
توجه کنید دادهٔ points در برنامه فقط شامل مقادیر x و y است، در حالی که آرایهٔ vPosition در Vertex Shader چهاربعدی است. این تفاوت مشکلی ایجاد نمیکند، زیرا داده را در پارامترهای تابع بهدرستی توصیف کردهایم. علت بنیادی این تفاوت به شیوهٔ کار سامانههای گرافیکی ما بازمیگردد: میخواهیم برنامههای کاربردی تا حد امکان به خود مسئله نزدیک باشند. برخی برنامهها دوبعدیاند، بیشتر آنها سهبعدی هستند و بعضی حتی ممکن است چهاربعدی باشند.