Frameها در OpenGL، کلاسهای ماتریس و بردار و مکعب رنگی
در نسخههای قدیمی OpenGL با Fixed-Function Pipeline و Immediate-Mode Rendering، شش Frame در خط لوله مشخص بودند. با Shaderهای برنامهپذیر انعطاف زیادی داریم و میتوان Frameهای بیشتری افزود یا بعضی Frameهای سنتی را کنار گذاشت. هرچند در مثالهای نخست با تکیه بر شناخت عملکرد خط لوله توانستیم از برخی Frameها صرفنظر کنیم، این روش برای ساخت برنامههای واقعی مناسب نیست. هر یک از شش Frame زیر در توسعهٔ برنامه یا پیادهسازی خط لوله مفید است. بعضی در کد برنامه و بعضی در Shader اعمال میشوند و برخی ممکن است اصلاً برای برنامه قابل مشاهده نباشند. مختصات یک رأس در هر Frame متفاوت است. ترتیب معمول آنها در خط لوله چنین است:
- Object/Model Coordinates؛
- World Coordinates؛
- Eye/Camera Coordinates؛
- Clip Coordinates؛
- Normalized Device Coordinates؛
- Window/Screen Coordinates.
وقتی برنامه یک رأس را تعریف میکند، رأس ممکن است مستقیماً در کد یا غیرمستقیم از طریق نمونهای از یک شیء ساخته شود. در بیشتر برنامهها شیء را با اندازه، جهت و مکان مناسب در Frame خود، یعنی Model/Object Frame، تعریف میکنیم. مثلاً یک مکعب معمولاً سطوحی همراستا با محورهای Frame، مرکز در مبدأ و طول ضلع ۱ یا ۲ واحد دارد. مقادیر این مرحله Object/Model Coordinates هستند.
یک صحنه ممکن است صدها یا هزاران شیء داشته باشد. برنامه معمولاً برای هر شیء زنجیرهای از تبدیلها را اعمال میکند تا اندازه، جهت و موقعیت آن در Frame مناسب برنامه تعیین شود. مثلاً اگر مربعی را در برنامهٔ معماری بهعنوان پنجره استفاده کنیم، آن را به ابعاد درست و واحدهای مناسب ــ احتمالاً متر یا فوت ــ Scale میکنیم. مبدأ مختصات برنامه ممکن است مرکز کف طبقهٔ همکف ساختمان باشد. این Frame برنامه «World Frame» و مقادیر آن World Coordinates هستند. اگر مدلسازی با شیءهای ازپیشتعریفشده یا تبدیلهای اولیه نداشته باشیم، Object و World Coordinates یکسان خواهند بود.
Object و World Frame برای برنامه طبیعیاند، اما تصویر نهایی به چیزی بستگی دارد که دوربین یا ناظر میبیند. تقریباً همهٔ سامانههای گرافیکی Frameای دارند که مبدأ آن مرکز لنز دوربین است و محورهایش با اضلاع دوربین همراستا هستند. در Perspective View، مرکز لنز Center of Projection و در Orthographic View جهت Projection با اضلاع دوربین همراستا است.
این Frame «Camera Frame» یا «Eye Frame» نام دارد. چون هر تغییر Frame متناظر با تبدیل آفین است، ماتریسهای ۴×۴ برای تبدیل Model Coordinates به World و World به Eye وجود دارند. این تبدیلها معمولاً در یک «Model-View Transformation» ترکیب میشوند که با Model-View Matrix نمایش داده میشود. در بیشتر موارد استفاده از ماتریس ترکیبی مشکلی ایجاد نمیکند؛ در فصل ۵ و بحث Lighting/Shading مواردی میبینیم که باید دو تبدیل را جدا نگه داشت.
سه نمایش آخر عمدتاً در پیادهسازی خط لوله استفاده میشوند. وقتی اشیا در Eye Coordinates هستند، OpenGL بررسی میکند آیا داخل View Volume قرار دارند یا خیر. اشیای خارج از آن پیش از Rasterization Clip میشوند. این کار زمانی کارآمدتر است که ابتدا Projection Transformation همهٔ اشیای بالقوه قابل مشاهده را به مکعبی حول مبدأ در Clip Coordinates ببرد. این تبدیل در فصل ۴ بررسی میشود. پس از آن، رأسها هنوز در Homogeneous Coordinates هستند. تقسیم بر مؤلفهٔ w، که «Perspective Division» نام دارد، نمایش سهبعدی در Normalized Device Coordinates میدهد. تبدیل نهایی با توجه به Viewport، موقعیت NDC را به Window Coordinates میبرد. Window Coordinates بر حسب پیکسل اندازهگیری میشوند ولی اطلاعات Depth را حفظ میکنند؛ با حذف Depth به Screen Coordinates دوبعدی میرسیم.
برنامهنویس معمولاً با دو Frame اصلی Eye و Object کار میکند. با ترکیب تبدیلها در Model-View Matrix، تبدیل واحدی داریم که Object Frame را نسبت به Eye Frame قرار میدهد. این ماتریس نمایش همگن نقاط و بردارها را از فضای برنامه به Eye Frame تبدیل میکند.
استفاده از Model-View Matrix اجباری نیست، اما آنقدر در برنامهها مهم است که تقریباً در همهٔ مثالها از آن استفاده میکنیم. یکی از مسائل مهم این است که تبدیلها کجا تعریف و کجا اعمال شوند. میتوان تبدیل را در برنامه تعریف و همانجا روی داده اعمال کرد، یا فقط پارامترهای آن را به Shaderها فرستاد و اجرای تبدیل را به GPU سپرد. در بخشهای بعد هر دو رویکرد بررسی میشوند.
فرض کنیم Model-View Matrix در برنامه تخصیص داده و با Identity Matrix مقداردهی اولیه شود. در این حالت Object و Eye Frame یکساناند؛ پس تا زمانی که ماتریس را تغییر ندهیم در Eye Coordinates کار میکنیم. همانند فصل ۲، دوربین در مبدأ Frame خود قرار دارد. سه جهت پایهٔ Eye Space متناظرند با: جهت بالای دوربین یعنی y، جهت نگاه دوربین یعنی z منفی، و جهت متعامد سوم x به شکلی که x، y و z دستگاه راستگرد بسازند.
شکل ۳.۲۶ — Camera و Object Frame: (الف) موقعیتهای پیشفرض؛ (ب) پس از اعمال Model-View Matrix.
Frameهای دیگر برای قرار دادن اشیا با تبدیلهای مختصات همگن تعریف میشوند که Frame جدید را نسبت به Camera Frame مشخص میکنند. در بخش ۳.۵ نحوهٔ تعریف این تبدیلها را میآموزیم. تغییر Frameها با Model-View Matrix نمایش داده میشود و چون ماتریس قابل ذخیره است، میتوان Frameها را ذخیره و با تغییر ماتریس جاری بین آنها جابهجا شد. در فصل ۷ خواهیم دید ساخت ساختار دادهای مانند Stack برای ذخیرهٔ تبدیلها در مدلهای پیچیده بسیار سودمند است.
در کار با چند Frame ممکن است این پرسش پیش آید که کدام Frame ثابت و کدام متغیر است. چون Model-View Matrix موقعیت دوربین را نسبت به اشیا مشخص میکند، انتخاب Frame ثابت عمدتاً قراردادی است. معمولاً دوربین را ثابت و Frameهای دیگر را متحرک نسبت به آن فرض میکنیم، ولی دیدگاه معکوس نیز معتبر است.
پیش از بحث دقیق تبدیلها دو مثال ساده میآوریم. در حالت پیشفرض شکل ۳.۲۶(a)، Camera و Object Frame منطبقاند و دوربین در جهت z منفی نگاه میکند.
در بسیاری برنامهها طبیعی است اشیا را نزدیک مبدأ تعریف کنیم، مثلاً مربعی با مرکز مبدأ یا مجموعهای از اشیا که مرکز جرمشان در مبدأ است. همچنین طبیعی است شرایط مشاهده را طوری تنظیم کنیم که دوربین فقط اشیای جلوی خود را ببیند. بنابراین برای دیدهشدن اشیای اطراف مبدأ باید یا دوربین را از اشیا دور کنیم یا اشیا را از دوربین دور کنیم؛ یعنی Camera Frame و Object Frame را نسبت به هم جابهجا کنیم.
اگر Camera Frame را ثابت بدانیم، Model-View Matrix زیر Object Frame را نسبت به دوربین در جهت z به اندازهٔ −d منتقل میکند:
A = [1 0 0 0
0 1 0 0
0 0 1 -d
0 0 0 1]
این ماتریس نقطهٔ (x,y,z) در Object Frame را به (x,y,z−d) در Camera Frame میبرد. با انتخاب d مثبت و کافی بزرگ، اشیا جلوی دوربین قرار میگیرند، شکل ۳.۲۶(b). کاربر همچنان اشیا را در World Coordinates عادی تعریف میکند و Model-View Matrix موقعیت نسبی Object و Eye Frame را مدیریت میکند. این روش تقریباً همیشه بهتر از تغییر مستقیم Vertex Positionها برای قراردادن اشیا جلوی دوربین است.
مثال دوم: هنگام تعریف اشیا با رأسها، در Application/World Frame کار میکنیم و Vertex Positionها نمایش نقاط در همان Frame هستند. ما خود World Frame را مستقیماً دستکاری نمیکنیم؛ بلکه با نمایش نقطه و بردار در آن کار میکنیم. وضعیت شکل ۳.۲۷ را در نظر بگیرید.
شکل ۳.۲۷ — دوربین در (1,0,1) و رو به مبدأ.
در این مثال دوربین در Object/World Frame در نقطهٔ همگن p=(1,0,1,1)^T قرار دارد و به مبدأ World Frame نگاه میکند. بردار n=(−1,0,−1,0)^T بر پشت دوربین عمود و به سوی مبدأ است. جهت بالای دوربین با جهت بالای World یکسان است: v=(0,1,0,0)^T. با ضرب برداری، جهت متعامد سوم دوربین را مییابیم: u=(1,0,−1,0)^T.
اکنون مانند بخش ۳.۳.۶ ماتریسی میسازیم که نمایش نقاط و بردارها را از World Frame به Camera Frame تبدیل کند. ترانهادهٔ ماتریس تغییر Frame با معکوس ماتریس شامل مختصات دوربین به دست میآید. نتیجه نشان میدهد مبدأ Frame اولیه در Camera Frame یک واحد در جهت n قرار دارد، یعنی نمایش آن (0,0,1,1) است.
در OpenGL میتوان Model-View Matrix را به شکل آرایهای ۱۶ عضوی به Vertex Shader فرستاد. اگر نمایش یک Frame نسبت به Frame دیگر با بردارهای پایه و مبدأ مشخص باشد، یافتن ضرایب ماتریس تمرینی مستقیم است؛ اما در مسائل هندسی واقعی معمولاً از Frameای به Frame دیگر با زنجیرهای از تبدیلهای هندسی مانند Rotation، Translation و Scale میرویم. در بخشهای بعد همین مسیر را دنبال میکنیم. پیش از آن چند کلاس مفید C++ معرفی میشود.
۳.۵ کلاسهای ماتریس و بردار
در فصل ۲ دیدیم انواع دادهٔ جدید هم کد برنامه را روشنتر میکنند و هم برای GLSL ضروریاند. اکنون این مفاهیم را با کلاسهای C++ مورد استفاده در برنامهها رسمیتر میکنیم. کد در دو فایل mat.h و vec.h قرار دارد که هر دو با Include زیر قابل استفادهاند:
انواع پایه عبارتاند از mat2, mat3, mat4, vec2, vec3, vec4. کلاسهای ماتریس بهترتیب برای ماتریسهای ۲×۲، ۳×۳ و ۴×۴ و انواع برداری برای آرایههای ۲، ۳ و ۴ عضویاند. عملگرهای حسابی Overload شدهاند تا بتوان کدهای طبیعی ماتریسی و برداری نوشت؛ نمونهها در صفحهٔ بعد ادامه دارند.
برای نمونه میتوان چنین نوشت:
#include "mat.h"
vec4 x, y = vec4(1.0, 2.0, 3.0, 1.0); // use of constructor
x = 2.0*y;
x[2] = 5.0;
mat4 a, b = mat4(vec4(y), vec4(x), vec4(y), vec4(x)); //matrix constructor
float s = 2.5;
a[2][1] = 3.5;
b = s*a;
vec4 z = b*x;
y = x*b;
پس میتوان به عناصر منفرد هر نوع دسترسی داشت و عملیات استاندارد ماتریسی ضمیمهٔ B را انجام داد. توجه کنید حاصل b*x و x*b عموماً متفاوت است؛ یکی ضرب سطر ۱×۴ در ماتریس ۴×۴ و دیگری ضرب ماتریس ۴×۴ در ستون ۴×۱ است. هر دو چهار عنصر تولید میکنند و در vec4 قابل ذخیرهاند.
با توجه به تعریف قبلی نقطه و بردار، نامهای vec2, vec3, vec4 ممکن است کمی گمراهکننده باشند. GLSL این نوعها را برای ذخیرهٔ هر کمیت چندعضوی به کار میبرد: بردارهای جهت، نقاط در مختصات همگن، رنگهای RGB/RGBA و بعداً مختصات بافت. مزیت آن است که همهٔ این انواع میتوانند از عملیات مشابه استفاده کنند. استفاده از همین نوعهای GLSL در کلاسهای vec.h و mat.h باعث میشود کد دستکاری نقطه، بردار و تبدیل در برنامه شبیه کد GLSL باشد. چون الگوریتمی را ممکن است در CPU یا Shader اجرا کنیم، یکسانبودن نوعها انتقال الگوریتم میان برنامه و Shader را آسان میکند.
برای خوانایی بیشتر میتوان با typedef نامهای معنایی تعریف کرد:
typedef vec3 color3;
typedef vec4 color4;
typedef vec3 point3;
typedef vec4 point4;
کلاس جدیدی ایجاد نشده است، اما کدی مانند
color3 red = color3(1.0, 0.0, 0.0);
از
vec3 red = vec3(1.0, 0.0, 0.0);
معنای روشنتری دارد.
۳.۶ مدلسازی یک مکعب رنگی
اکنون بیشتر دانش مفهومی و عملی لازم برای ساخت برنامهٔ گرافیکی سهبعدی را داریم و از آن برای برنامهای استفاده میکنیم که مکعبی چرخان رسم میکند. یک Frame از انیمیشن میتواند شبیه شکل ۳.۲۸ باشد. پیش از چرخاندن مکعب باید روش مدلسازی کارآمد آن را بررسی کنیم. اشیای سهبعدی مانند دوبعدی با مجموعهای از رأسها قابل نمایشاند، اما ساختارهای داده کمک میکنند روابط میان رأسها، یالها و وجهها نیز حفظ شود. OpenGL این نوع سازماندهی را با Vertex Arrayها پشتیبانی میکند که در پایان بخش معرفی میشوند.
شکل ۳.۲۸ — یک Frame از انیمیشن مکعب.
پس از مدلسازی مکعب، آن را با تبدیلهای آفین متحرک میکنیم. این تبدیلها در بخش ۳.۷ معرفی و برای تغییر Model-View Matrix استفاده میشوند. در فصل ۴ همان تبدیلها بخشی از فرایند Viewing خواهند بود. رأسها در خط لوله از چند تبدیل عبور میکنند و همه از نمایش مختصات همگن استفاده میکنند؛ Rasterizer در انتهای خط لوله قرار دارد و در صورت درستبودن مراحل قبلی وظیفهٔ خود را انجام میدهد.
۳.۶.۱ مدلسازی وجهها
مکعب از سادهترین اشیای سهبعدی برای مدل و نمایش است، اما روشهای مختلفی برای مدلکردن آن وجود دارد. سامانهٔ CSG آن را یک Primitive واحد میداند؛ در پایینترین سطح، سختافزار آن را شیئی تعریفشده با هشت رأس پردازش میکند. چون مدل سطحمحور انتخاب کردهایم، مکعب را یا اشتراک شش صفحه یا شش چندضلعیِ Facet میدانیم که وجههای آن را تشکیل میدهند. ساختار دادهٔ خوب باید هم دید سطح بالای برنامه و هم دید سطح پایین پیادهسازی را پشتیبانی کند.
فرض میکنیم رأسهای مکعب در آرایهای موجودند و با مختصات همگن کار میکنیم:
point4 vertices[8] = {
point4(-1.0,-1.0,-1.0,1.0),point4(1.0,-1.0,-1.0,1.0),
point4(1.0,1.0,-1.0,1.0), point4(-1.0,1.0,-1.0,1.0),
point4(-1.0,-1.0,1.0,1.0), point4(1.0,-1.0,1.0,1.0)
point4(1.0,1.0,1.0,1.0), point4(-1.0,1.0,1.0,1.0)};
با این فهرست نقاط، وجهها مشخص میشوند؛ مثلاً یک وجه توالی رأسهای (0,3,2,1) است و پنج وجه دیگر نیز به همین روش تعریف میشوند.
۳.۶.۲ وجههای رو به داخل و بیرون
هنگام تعریف چندضلعی سهبعدی ترتیب رأسها مهم است. توالی 0,3,2,1 یک وجه را تعریف میکند؛ 1,0,3,2 همان جهت پیمایش را دارد چون رأس آخر همیشه به نخست متصل میشود. اما 0,1,2,3 متفاوت است و همان مرز را در جهت معکوس طی میکند.
شکل ۳.۲۹ — پیمایش یالهای یک چندضلعی.
ترتیب مهم است چون هر چندضلعی دو سمت دارد و سامانهٔ گرافیکی میتواند یکی یا هر دو را نمایش دهد. برای تشخیص سازگار دو سمت از دید دوربین، ترتیب رأسها اطلاعات لازم را فراهم میکند.
یک وجه را «رو به بیرون» مینامیم اگر هنگام مشاهده از بیرون، رأسهای آن خلاف جهت عقربههای ساعت پیمایش شوند. این روش «قاعدهٔ دست راست» نیز نام دارد: اگر انگشتان دست راست در جهت پیمایش رأسها خم شوند، شست به سمت بیرون اشاره میکند.
در مثال ما 0,3,2,1 وجه بیرونی و 0,1,2,3 وجه پشتی همان چندضلعی را مشخص میکند. هر وجه از جسم بستهای مانند مکعب، مستقل از محل ناظر، سمت داخلی و خارجی مشخصی دارد؛ به شرط آنکه از بیرون جسم دیده شود. با تعیین دقیق Front و Back میتوان وجههای نامرئی را Cull کرد یا برای دو سمت ویژگیهای متفاوت به کار برد. Culling در فصل ۶ بررسی میشود.
۳.۶.۳ ساختارهای داده برای نمایش شیء
میتوان مکعب را صرفاً با مجموعهای از رأسها توصیف کرد، مثلاً آرایهٔ دوبعدی
یا آرایهٔ ۲۴ رأسی
point3 cube_vertices[24];
که cube_vertices[i] مختصات رأس iام را نگه دارد. هر دو روش کار میکنند، اما Topology مکعب را از Geometry آن جدا نمیکنند. اگر مکعب را Polyhedron بدانیم، از شش وجه تشکیل شده است؛ هر وجه یک چهارضلعی و هر رأس میان سه وجه مشترک است. جفت رأسها یالها را میسازند و هر یال میان دو وجه مشترک است. این گزارهها Topology یک چندوجهی ششوجهی را توصیف میکنند و مستقل از محل رأسها، یعنی مستقل از Geometry، هستند. حالتهای تکین ویژه مانند همخطشدن چند رأس یا تقاطع وجهها را فعلاً نادیده میگیریم.
در ادامهٔ کتاب بارها مزیت ساخت ساختار دادهای که Topology را از Geometry جدا میکند دیده خواهد شد. اینجا از ساختار ساده و قابل گسترش «Vertex List» استفاده میکنیم.
شکل ۳.۳۰ — نمایش Vertex-List یک مکعب.
دادهٔ مکان رأسها Geometry را تشکیل میدهد و میتواند در List یا آرایهای مانند vertices[8] ذخیره شود. موجودیت سطح بالا مکعب است که شش وجه دارد. هر وجه چهار رأس مرتب دارد و هر رأس بهطور غیرمستقیم با Index خود مشخص میشود. یکی از مزیتها این است که هر مکان هندسی فقط یک بار ذخیره میشود، نه هر بار که در یک Facet به کار میرود. اگر در برنامهٔ تعاملی مکان رأسی تغییر کند فقط همان یک مقدار اصلاح میشود و نیازی به یافتن همهٔ تکرارهای رأس نیست.
۳.۶.۴ مکعب رنگی
از Vertex List برای تعریف «مکعب رنگ» استفاده میکنیم. تابع quad Index چهار رأس را در ترتیب رو به بیرون میگیرد و مانند فصل ۲ دادهها را در دو آرایه برای مکان رأس و رنگ متناظر ذخیره میکند:
vec4 quad_colors[36], vertices[36];
int i = 0; /* vertex and color index */
چون فقط میتوانیم مثلث نمایش دهیم، quad باید برای هر وجه دو مثلث و در نتیجه شش رأس تولید کند. اگر هر رأس رنگ خودش را داشته باشد، ۲۴ رأس و ۲۴ رنگ برای داده لازم است. مکعب با شش فراخوانی quad تعریف میشود.
void colorcube()
{
quad(0,3,2,1);
quad(2,3,7,6);
quad(3,0,4,7);
quad(1,2,6,5);
quad(4,5,6,7);
quad(5,4,0,1);
}
رنگ رأسها را از گوشههای جسم رنگی فصل ۲ انتخاب میکنیم: سیاه، سفید، قرمز، سبز، آبی، Cyan، Magenta و زرد. برای هر رأس رنگ را با Index رأس تعیین میکنیم؛ راه دیگر آن است که Index نخستین رأس ورودی quad رنگ کل وجه را تعیین کند. رنگهای RGBA چنیناند:
color4 colors[8] = {color4(0.0,0.0,0.0,1.0),
color4(1.0,0.0,0.0,1.0),
color4(1.0,1.0,0.0,1.0),
color4(0.0,1.0,0.0,1.0),
color4(0.0,0.0,1.0,1.0),
color4(1.0,0.0,1.0,1.0),
color4(0.0,1.0,1.0,1.0),
color4(1.0,1.0,1.0,1.0)};
رأسهای مکعب متناظر با ناحیهٔ Clipping در Clip Coordinates:
point4 vertices[8] = {point4(-1.0,-1.0,1.0,1.0), point4(-1.0,1.0,1.0,1.0),
point4(1.0,1.0,1.0,1.0), point4(1.0,-1.0,1.0,1.0),
point4(-1.0,-1.0,-1.0,1.0), point4(-1.0,1.0,-1.0,1.0),
point4(1.0,1.0,-1.0,1.0), point4(1.0,-1.0,-1.0,1.0)};
تابع quad از سه رأس اول مثلث نخست و از رأس اول، سوم و چهارم مثلث دوم را میسازد. ابتدای کد آن:
int i = 0;
void quad(int a, int b, int c, int d)
{
quad_color[i] = colors[a];
points[i] = vertices[a];
i++;
quad_color[i] = colors[b];
points[i] = vertices[b];
i++;
quad_color[i] = colors[c];
points[i] = vertices[c];
i++;
ادامهٔ تابع در صفحهٔ بعد آمده است.
ادامهٔ تابع quad:
quad_color[i] = colors[a];
points[i] = vertices[a];
i++;
quad_color[i] = colors[c];
points[i] = vertices[c];
i++;
quad_color[i] = colors[d];
points[i] = vertices[d];
i++;
}
توجه کنید i بیرون از تابع quad مقداردهی اولیه شده است. اگر در مثالهای بعد quad را چند بار، بهعلت تغییر رنگ یا مکان همان رأسها یا داشتن چند مکعب، اجرا کنیم باید دقت کنیم افزودن داده به آرایهٔ points از کجا آغاز شود.
برنامه تقریباً کامل است؛ پیش از پایان، نحوهٔ اختصاص رنگها و سایر Vertex Attributeها به Fragmentها توسط Rasterizer را بررسی میکنیم.
۳.۶.۵ درونیابی
با اینکه رنگ رأسهای مکعب را تعریف کردهایم، سامانهٔ گرافیکی باید تصمیم بگیرد چگونه از این داده برای رنگکردن نقاط داخل چندضلعی استفاده کند. روشهای مختلفی برای Fill یا Interpolate کردن رنگ در سطح چندضلعی وجود دارد. رایجترین روش در گرافیک رایانهای بر «مختصات باریسنترک» مثلثها تکیه دارد که در بخش ۳.۱ معرفی شدند؛ یکی از دلایل اصلی همین است که مثلث Primitive کلیدی رندر است.
چندضلعی شکل ۳.۳۱ را در نظر بگیرید. رنگهای C0, C1, C2 در برنامه به رأسها اختصاص یافتهاند. فرض کنید رنگ RGB است و Interpolation برای هر مؤلفهٔ اصلی جداگانه انجام میشود. ابتدا روی یال میان رأسهای ۰ و ۱ درونیابی خطی میکنیم:
C01(α) = (1−α)C0 + αC1.
با تغییر α از ۰ تا ۱ رنگهای روی یال تولید میشوند. برای α مشخص رنگ C3 به دست میآید. سپس روی خط میان C3 و رنگ رأس سوم C2 درونیابی میکنیم:
C32(β) = (1−β)C3 + βC2.
برای β مشخص رنگ C4 در نقطهای داخلی به دست میآید. با تغییر مختصات باریسنترک α و β از ۰ تا ۱، رنگ همهٔ نقاط داخلی و در نتیجه هر Fragment تولیدشده توسط Rasterizer تعیین میشود.
شکل ۳.۳۱ — درونیابی با مختصات باریسنترک.
همین روش Interpolation را میتوان برای هر Vertex Attribute دیگر نیز به کار برد. سختافزارهای گرافیکی نوین روشهای درونیابی صحیح تحت Perspective Viewing را پشتیبانی میکنند.
اکنون شیئی داریم که مانند واشر سیرپینسکی سهبعدی بخش ۲.۹ با Projection متعامد ساده قابل نمایش است. در بخش ۳.۷ تبدیلها را معرفی میکنیم تا مکعب را متحرک و اشیای پیچیدهتر را ایجاد کنیم. ابتدا قابلیتی از OpenGL را بررسی میکنیم که هم سربار تولید مکعب را کاهش میدهد و هم روش سطح بالاتری برای کار با مکعب و دیگر Polyhedronها فراهم میکند.
۳.۶.۶ نمایش مکعب
برنامهٔ کامل در ضمیمهٔ A آمده است. بخشهای برنامه برای نمایش مکعب و Shaderها تقریباً همان کدی هستند که در فصل ۲ برای واشر سهبعدی استفاده شد. تفاوت فقط در نحوهٔ قرار دادن داده در آرایههای Vertex Position و Vertex Color است؛ بخشهای OpenGL از جمله Shaderها یکساناند.
نمایش پیشفرض مکعب اطلاعات زیادی نمیدهد، زیرا اضلاع مکعب با Clipping Volume همراستا هستند و فقط وجه جلو دیده میشود؛ مکعب نیز کل پنجره را پر میکند. میتوان با تغییر دادهها مکعب را چرخاند یا کوچک کرد. مثلاً برای Scale نصف، رأسها را چنین تغییر میدهیم:
point4 vertices[8] = {point4(-0.5,-0.5,0.5,1.0),
point4(-0.5,0.5,0.5,1.0),
point4(0.5,0.5,0.5,1.0),
point4(0.5,-0.5,0.5,1.0),
point4(-0.5,-0.5,-0.5,1.0),
point4(-0.5,0.5,-0.5,1.0),
point4(0.5,0.5,-0.5,1.0),
point4(0.5,-0.5,-0.5,1.0)};
اما این راه انعطافپذیر نیست. میتوان Scale Factor را در quad قرار داد، یا Vertex Shader را چنین تغییر داد:
in vec4 vPosition;
in vec4 vColor;
out vec4 color;
void main()
{
gl_Position = 0.5*vPosition;
color = vColor;
}
در این نسخه Vertex Shader برای دادهٔ ورودی چهاربعدی Homogeneous Coordinate تنظیم شده است. Fragment Shader نیز ساده میشود:
in vec4 color;
out vec4 fragColor;
void main()
{
fragColor = color;
}
بهجای این راهحلهای موردی، قابلیتی عمومی برای Transformation توسعه میدهیم که بتواند داده را در برنامه یا Shader بچرخاند، Scale کند و Translation دهد. همچنین روش انتقال داده میان برنامه و Shaderها برای اجرای تبدیل روی GPU و تغییر پویاى آنها را دقیقتر بررسی خواهیم کرد.
۳.۷ تبدیلهای آفین
Transformation تابعی است که یک نقطه یا بردار را به نقطه یا برداری دیگر نگاشت میکند، شکل ۳.۳۲. برای نقاط میتوان نوشت:
Q = T(P)
و برای بردارها:
v = R(u).
اگر از نمایش مختصات همگن استفاده کنیم، هر دو نوع به صورت ماتریس ستونی چهاربعدیاند و میتوان تابع واحدی تعریف کرد:
q = f(p) و v = f(u)
که نمایش نقاط و بردارها را در یک Frame تبدیل میکند.
شکل ۳.۳۲ — Transformation.
این فرمولبندی عمومیتر از آن است که عملی باشد، زیرا همهٔ نگاشتهای تکمقداری را شامل میشود. حتی اگر توصیف راحتی از f داشته باشیم، برای تبدیل یک منحنی عمومی ممکن است مجبور شویم تابع را برای تکتک نقاط آن اجرا کنیم؛ مثلاً برای یک پارهخط شاید همهٔ نقاط میان دو انتها نیازمند تبدیل باشند.
بهجای آن کلاس محدودتری از تبدیلها را بررسی میکنیم. در مختصات همگن چهاربعدی، هم نقطه و هم بردار 4-تایی هستند.
کلاس مفیدی از تبدیلها با افزودن محدودیت به f به دست میآید. مهمترین محدودیت «خطیبودن» (Linearity) است. تابع f زمانی و فقط زمانی خطی است که برای هر اسکالر α و β و هر دو رأس یا بردار p و q:
f(αp + βq) = αf(p) + βf(q).
اهمیت این رابطه آن است که اگر تبدیل p و q را بدانیم، تبدیل هر ترکیب خطی آنها از ترکیب خطی همان تبدیلها به دست میآید و لازم نیست هر ترکیب را جداگانه محاسبه کنیم.
در مختصات همگن با نمایش نقطه و بردار کار میکنیم. تبدیل خطی، نمایش دادهشدهٔ یک نقطه یا بردار را به نمایش دیگری تبدیل میکند و همیشه میتوان آن را به ضرب ماتریسی نوشت:
v = Cu
که C ماتریسی مربعی است. با مقایسه با تغییر Frame بخش ۳.۳ میبینیم تا زمانی که C غیرتکین باشد، هر تبدیل خطی متناظر با تغییر Frame است. پس دو دیدگاه معادل داریم: ۱) تغییر نمایش یا Frame زیربنایی و به دست آوردن نمایش جدید رأسها؛ ۲) تبدیل خود رأسها در همان Frame.
در مختصات همگن، C ماتریس ۴×۴ است که مؤلفهٔ چهارم w را بدون تغییر نگه میدارد:
C = [α11 α12 α13 α14
α21 α22 α23 α24
α31 α32 α33 α34
0 0 0 1]
این همان ترانهادهٔ M بخش ۳.۳.۴ است. ۱۲ مقدار آن میتوانند مستقل تعیین شوند؛ بنابراین تبدیل ۱۲ «درجهٔ آزادی» (Degrees of Freedom) دارد.
اما نقطه و بردار نمایشهای متفاوتی دارند. بردار:
u=[α1,α2,α3,0]^T
است.
هر نقطه نیز:
p=[β1,β2,β3,1]^T.
اگر C دلخواه را بر بردار اعمال کنیم، v=Cu، فقط ۹ عنصر C روی u اثر میگذارند؛ بنابراین تبدیل بردار فقط ۹ درجهٔ آزادی دارد. تبدیل آفین نقاط از همهٔ ۱۲ درجهٔ آزادی استفاده میکند.
تبدیلهای آفین خطها را نیز حفظ میکنند. خطی را به فرم
P(α)=P0+αd
در نظر بگیرید؛ در یک Frame:
p(α)=p0+αd.
برای هر ماتریس تبدیل آفین C:
Cp(α)=Cp0+αCd.
پس برای ساخت خط تبدیلشده کافی است ابتدا p0 و d را تبدیل کنیم و سپس هنگام نمایش از هر الگوریتم تولید خط دلخواه استفاده کنیم. اگر خط را به شکل دونقطهای
p(α)=αp0+(1−α)p1
بنویسیم نیز همین نتیجه برقرار است: فقط دو نقطهٔ انتهایی تبدیل میشوند و خط جدید از آنها ساخته میشود. چون فقط ۱۲ عنصر C آزادند، تبدیل آفین خط یا پارهخط نیز ۱۲ درجهٔ آزادی دارد.
اهمیت عملی این نتیجه در گرافیک بسیار زیاد است. برای تعیین کامل پارهخط تبدیلشده فقط لازم است نمایش همگن دو انتهای آن را تبدیل کنیم. بنابراین خط لوله میتواند End Pointها را از واحدهای تبدیل آفین عبور دهد و نقاط داخلی را فقط در مرحلهٔ Rasterization تولید کند.
خوشبختانه بیشتر تبدیلهای موردنیاز گرافیک، از جمله Rotation، Translation و Scaling، آفین هستند. جزئیات آنها در ادامه میآید.
با تغییراتی جزئی، همین نتایج برای توصیف Projectionهای موازی و پرسپکتیو استاندارد فصل ۴ نیز به کار میروند.
۳.۸ Translation، Rotation و Scaling
تا اینجا میان نگاه به اشیای هندسی بهعنوان موجودیتهای انتزاعی و کار با نمایش آنها در Frame مشخص رفتوبرگشت داشتهایم. در برنامهٔ کاربردی ناگزیر با نمایشها کار میکنیم. در این بخش ابتدا مهمترین تبدیلهای آفین را مستقل از نمایش توصیف میکنیم و سپس ماتریسهایی مییابیم که با عمل بر نمایش نقاط و بردارها همان تبدیلها را اجرا کنند. بعداً روش پیادهسازی آنها در OpenGL را خواهیم دید.
تبدیلها را روشی برای انتقال نقاط توصیفکنندهٔ یک یا چند شیء هندسی به مکانهای جدید در نظر میگیریم. برای بردن یک نقطهٔ منفرد به مکان جدید ماتریسهای فراوانی ممکناند، اما برای انتقال مجموعهای از نقاط به مکانهای جدید در حالی که روابط فضایی میان آنها حفظ شود معمولاً فقط یک تبدیل مناسب وجود دارد. مثلاً ماتریسهای زیادی یک گوشهٔ مکعب رنگ را از P0 به Q0 میبرند، اما فقط یک تبدیل مناسب وقتی به همهٔ رأسها اعمال شود مکعبی با همان اندازه و جهتگیری و صرفاً با مکان جدید تولید میکند.
۳.۸.۱ Translation
Translation عملی است که همهٔ نقاط را به اندازهٔ فاصلهای ثابت در جهتی مشخص جابهجا میکند، شکل ۳.۳۳. برای تعریف آن فقط به بردار جابهجایی d نیاز داریم:
P′ = P + d
برای همهٔ نقاط P جسم. این تعریف هیچ ارجاعی به Frame یا نمایش خاص ندارد. Translation سه درجهٔ آزادی دارد، چون سه مؤلفهٔ بردار d را میتوان مستقل تعیین کرد.
شکل ۳.۳۳ — Translation: (الف) شیء در موقعیت اولیه؛ (ب) شیء پس از جابهجایی.
۳.۸.۲ Rotation
تعریف Rotation از Translation دشوارتر است، زیرا پارامترهای بیشتری لازم دارد. ابتدا چرخش نقطه حول مبدأ در صفحهٔ دوبعدی را بررسی میکنیم، شکل ۳.۳۴. با انتخاب مبدأ، عملاً Frame مشخصی داریم. نقطهٔ (x,y) با زاویهٔ θ حول مبدأ به (x′,y′) میرود. اگر دو نقطه را به فرم قطبی بنویسیم:
x = ρ cosφ
y = ρ sinφ
x′ = ρ cos(θ+φ)
y′ = ρ sin(θ+φ).
با بسط روابط مثلثاتی:
x′ = x cosθ − y sinθ
y′ = x sinθ + y cosθ.
در فرم ماتریسی:
[x′] [cosθ -sinθ] [x]
[y′] = [sinθ cosθ] [y]
شکل ۳.۳۴ — Rotation دوبعدی.
سه ویژگی این تبدیل به Rotationهای دیگر نیز تعمیم مییابد:
- یک نقطه ــ اینجا مبدأ ــ در اثر Rotation تغییر نمیکند و «نقطهٔ ثابت» (Fixed Point) تبدیل نام دارد. شکل ۳.۳۵ چرخش دوبعدی حول نقطهای ثابت در مرکز جسم را بهجای مبدأ Frame نشان میدهد.
- صفحهٔ دوبعدی را میتوان بخشی از فضای سهبعدی دانست. در دستگاه راستگرد و رسم استاندارد محورهای x و y، محور z مثبت از صفحه بیرون میآید. Rotation مثبت وقتی در امتداد z مثبت به سمت مبدأ نگاه کنیم خلاف جهت عقربهٔ ساعت است؛ همین قرارداد برای چرخش حول محورهای دیگر استفاده میشود.
- Rotation در صفحهٔ
z=0 معادل Rotation سهبعدی حول محور z است. نقاط در صفحههای z ثابت به همان شکل میچرخند و مقدار z آنها ثابت میماند.
شکل ۳.۳۵ — Rotation حول نقطهٔ ثابت.
از این مشاهدهها برای تعریف Rotation عمومی سهبعدی مستقل از Frame استفاده میکنیم. سه موجودیت شکل ۳.۳۶ باید مشخص شوند: نقطهٔ ثابت Pf، زاویهٔ Rotation یعنی θ و خط یا برداری که محور چرخش است. برای یک نقطهٔ ثابت مشخص، سه درجهٔ آزادی داریم: دو زاویه برای جهت محور و یک زاویه برای مقدار چرخش حول آن.
شکل ۳.۳۶ — Rotation سهبعدی.
Rotation و Translation «تبدیلهای جسم صلب» (Rigid-Body Transformations) هستند. هیچ ترکیبی از آنها شکل یا حجم جسم را تغییر نمیدهد؛ فقط مکان و جهتگیری را عوض میکنند. بنابراین برای ساخت همهٔ تبدیلهای آفین به تبدیل دیگری نیز نیاز داریم. تبدیلهای شکل ۳.۳۷ آفیناند اما جسم صلب نیستند.
شکل ۳.۳۷ — تبدیلهای غیرصلب.
۳.۸.۳ Scaling
Scaling تبدیل آفین غیرصلبی است که جسم را بزرگ یا کوچک میکند. شکل ۳.۳۸ هم Uniform Scaling در همهٔ جهتها و هم Scaling در یک جهت را نشان میدهد.
شکل ۳.۳۸ — Scaling یکنواخت و غیریکنواخت.
برای ایجاد مجموعهٔ کامل تبدیلهای آفین مورد استفاده در Modeling و Viewing، باید Scaling غیریکنواخت را همراه با Translation و Rotation مناسب ترکیب کنیم.
Scaling نیز نقطهٔ ثابت دارد، شکل ۳.۳۹. برای تعریف آن میتوان نقطهٔ ثابت، جهت Scaling و Scale Factor یعنی α را تعیین کرد. اگر α>1 باشد جسم در آن جهت کشیده میشود؛ اگر 0≤α<1 باشد کوتاه میشود. α منفی Reflection نسبت به نقطهٔ ثابت در جهت Scaling ایجاد میکند، شکل ۳.۴۰. Scaling شش درجهٔ آزادی دارد: نقطهٔ ثابت دلخواه و سه Scale Factor مستقل.
شکل ۳.۳۹ — اثر Scale Factor.
شکل ۳.۴۰ — Reflection.
۳.۹ تبدیلها در مختصات همگن
همهٔ APIهای گرافیکی ما را مجبور میکنند در یک دستگاه مرجع کار کنیم؛ بنابراین نمیتوانیم فقط با عبارت انتزاعی سطح بالایی مانند
Q = P + αv
کار کنیم. باید از نمایش مختصات همگن استفاده کنیم، مثلاً:
q = p + αv.
در یک Frame، هر تبدیل آفین با ماتریس ۴×۴ استانداردی نمایش داده میشود که جزئیات آن در ادامه آمده است.
فرم عمومی ماتریس تبدیل آفین همگن همان ساختار زیر است:
A = [α11 α12 α13 α14
α21 α22 α23 α24
α31 α32 α33 α34
0 0 0 1]
۳.۹.۱ Translation
Translation نقاط را با بردار جابهجایی مشخصی به مکان جدید میبرد. اگر p با فاصلهٔ d به p′ منتقل شود:
p′ = p + d.
در نمایش همگن:
p=[x,y,z,1]^T، p′=[x′,y′,z′,1]^T و d=[αx,αy,αz,0]^T.
در نتیجه مؤلفهها:
x′=x+αx
y′=y+αy
z′=z+αz.
جمع ماتریسهای ستونی برای Translation با نمایش سایر تبدیلهای آفین سازگار نیست؛ اما همان نتیجه را با ضرب ماتریسی میتوان گرفت:
p′ = Tp
که Translation Matrix برابر است با:
T = [1 0 0 αx
0 1 0 αy
0 0 1 αz
0 0 0 1]
گاهی آن را T(αx,αy,αz) مینویسیم تا سه پارامتر مستقل آشکار باشند.
ممکن است مؤلفهٔ چهارم ثابت در نمایش همگن غیرضروری به نظر برسد، اما در فرم سهبعدی q=[x,y,z]^T و q′=[x′,y′,z′]^T هیچ ماتریس ۳×۳ ثابتی D وجود ندارد که برای جابهجایی دلخواه d رابطهٔ q′=Dq را بسازد. از این دید، مختصات همگن ترفند هوشمندانهای است که جمع ماتریسهای ستونی در سه بعد را به ضرب ماتریسی در چهار بعد تبدیل میکند.
معکوس Translation Matrix را میتوان با الگوریتم معکوسگیری یا مشاهدهٔ سادهٔ زیر یافت: اگر نقطه با d جابهجا شده، برای بازگشت کافی است −d اعمال شود. بنابراین:
T^−1(αx,αy,αz) = T(−αx,−αy,−αz).
۳.۹.۲ Scaling
هم Scaling و هم Rotation نقطهٔ ثابتی دارند که تبدیل آن را تغییر نمیدهد. فعلاً نقطهٔ ثابت را مبدأ میگیریم و بعداً با ترکیب تبدیلها حالت نقطهٔ ثابت دلخواه را میسازیم.
Scaling حول مبدأ اجازه میدهد هر محور مستقل Scale شود:
x′=βx x
y′=βy y
z′=βz z.
فرم همگن:
p′ = Sp
با:
S(βx,βy,βz) = [βx 0 0 0
0 βy 0 0
0 0 βz 0
0 0 0 1]
مانند همهٔ تبدیلهای همگن، سطر آخر به نوع تبدیل وابسته نیست و مؤلفهٔ چهارم نقطه را روی ۱ حفظ میکند.
معکوس Scaling با معکوس کردن Scale Factorها به دست میآید:
S^−1(βx,βy,βz) = S(1/βx, 1/βy, 1/βz).
۳.۹.۳ Rotation
ابتدا Rotation با نقطهٔ ثابت در مبدأ را بررسی میکنیم. سه درجهٔ آزادی داریم، متناظر با امکان چرخش مستقل حول سه محور مختصات. باید به ترتیب عملیات دقت کنیم، زیرا ضرب ماتریسی جابجاییپذیر نیست؛ چرخش حول x با زاویهٔ θ و سپس حول y با φ لزوماً با ترتیب معکوس نتیجهٔ یکسان ندارد.
ماتریسهای Rotation حول محورهای منفرد مستقیماً از Rotation دوبعدی بخش ۳.۷.۲ به دست میآیند. Rotation دوبعدی همان Rotation سهبعدی حول محور z است و نقاط در صفحههای z ثابت باقی میمانند. برای زاویهٔ θ:
x′ = x cosθ − y sinθ
y′ = x sinθ + y cosθ
z′ = z.
پس:
Rz(θ) = [ cosθ -sinθ 0 0
sinθ cosθ 0 0
0 0 1 0
0 0 0 1]
برای محور x، x ثابت است و چرخش دوبعدی در صفحههای x ثابت رخ میدهد:
Rx(θ) = [1 0 0 0
0 cosθ -sinθ 0
0 sinθ cosθ 0
0 0 0 1]
برای محور y، y ثابت میماند:
Ry(θ) = [ cosθ 0 sinθ 0
0 1 0 0
-sinθ 0 cosθ 0
0 0 0 1]
علامتهای جملههای سینوسی با تعریف Rotation مثبت در دستگاه راستگرد سازگارند.
اگر R یکی از سه Rotation Matrix باشد، Rotation با θ با Rotation بعدی −θ خنثی میشود:
R^−1(θ)=R(−θ).
با هویتهای cos(−θ)=cosθ و sin(−θ)=−sinθ نیز داریم:
R^−1(θ)=R^T(θ).
در بخش ۳.۱۰.۱ نشان داده میشود هر Rotation دلخواه حول مبدأ را میتوان از حاصلضرب Rotationهای سه محور ساخت:
R = Rz Ry Rx.
از خاصیت ترانهادهٔ حاصلضرب نتیجه میشود برای هر Rotation Matrix:
R^−1 = R^T.
ماتریسی که معکوسش برابر ترانهادهاش باشد «ماتریس متعامد» (Orthogonal Matrix) نام دارد. ماتریسهای متعامد نرمالشده متناظر با Rotation حول مبدأ هستند.
۳.۹.۴ Shear
هر تبدیل آفین را میتوان از زنجیرهای از Rotation، Translation و Scaling ساخت؛ بااینحال «Shear» آنقدر مهم است که آن را نوع پایهٔ مستقلی در نظر میگیریم. مکعبی را فرض کنید که در مبدأ مرکز دارد، با محورهای مختصات همراستاست و از z مثبت دیده میشود، شکل ۳.۴۱. اگر بالای آن را به راست و پایین آن را به چپ بکشیم، جسم در جهت x Shear میشود. چون y و z تغییر نمیکنند، این عمل را x-Shear مینامیم تا از Shearهای جهتهای دیگر متمایز شود. ادامهٔ فرمولبندی Shear در مقالهٔ بعد میآید.