ترکیب تبدیلها، ماتریسها، چرخش مکعب و رابطهای سهبعدی
فرمول x-Shear از مثلثات سادهٔ شکل ۳.۴۲ به دست میآید. هر Shear با یک زاویهٔ θ مشخص میشود:
x′ = x + y cotθ
y′ = y
z′ = z.
پس ماتریس Shear در جهت x:
Hx(θ) = [1 cotθ 0 0
0 1 0 0
0 0 1 0
0 0 0 1]
است. معکوس آن فقط Shear در جهت مخالف است:
Hx^−1(θ)=Hx(−θ).
Shear در جهت x و سپس z، مقادیر y را ثابت نگه میدارد و میتوان آن را Shear در صفحهٔ x–z دانست.
شکل ۳.۴۱ — Shear.
شکل ۳.۴۲ — محاسبهٔ Shear Matrix.
۳.۱۰ ترکیب تبدیلها
در این بخش با ضرب یا Concatenate کردن زنجیرهای از تبدیلهای پایه، تبدیلهای آفین پیچیده میسازیم. این راهبرد از تعریف مستقیم یک تبدیل دلخواه مناسبتر و با معماری خط لوله سازگارتر است.
فرض کنید سه تبدیل متوالی روی نقطهٔ p انجام شوند و q حاصل شود. چون ضرب ماتریسی شرکتپذیر است، میتوان نوشت:
q = CBAp.
ماتریسهای A، B و C میتوانند ۴×۴ دلخواه باشند، هرچند در عمل معمولاً آفیناند. ترتیب ارزیابی روی کارایی اثر میگذارد. اگر فقط یک نقطه داشته باشیم، اجرای A سپس B و سپس C، یعنی
q = C(B(Ap))
کارآمد است، چون هر مرحله ضرب ماتریس مربعی در ستون است.
شکل ۳.۴۳ — اعمال تبدیلها یکییکی.
اگر نقاط زیادی داشته باشیم، بهتر است ابتدا یک بار ماتریس ترکیبی را حساب کنیم:
M = CBA
و سپس برای هر نقطه:
q = Mp.
این همان خط لولهٔ شکل ۳.۴۴ است: ابتدا M محاسبه و در واحد Transform Pipeline بارگذاری میشود. هزینهٔ اضافی محاسبهٔ M در برابر اعمال آن به دهها هزار نقطه ناچیز است و برای هر نقطه فقط یک ضرب ماتریسی لازم میشود.
شکل ۳.۴۴ — Pipeline Transformation.
۳.۱۰.۱ Rotation حول نقطهٔ ثابت
تبدیلهایی مانند Rotation، Scaling و Shear را که ابتدا با نقطهٔ ثابت در مبدأ تعریف کردیم میتوان برای نقطهٔ ثابت دلخواه تعمیم داد. مثال را با Rotation حول محور z انجام میدهیم.
مکعبی با مرکز pf و اضلاع همراستا با محورهای مختصات را در نظر بگیرید. میخواهیم مکعب حول محور z ولی این بار حول مرکز خودش بچرخد؛ pf نقطهٔ ثابت است، شکل ۳.۴۵. اگر pf مبدأ بود فقط Rz(θ) کافی بود. بنابراین ابتدا مکعب را به مبدأ میبریم، Rotation را اعمال میکنیم و سپس آن را به مکان اولیه بازمیگردانیم.
شکل ۳.۴۵ — Rotation مکعب حول مرکز خودش.
شکل ۳.۴۶ — زنجیرهٔ تبدیلها.
سه تبدیل بهترتیب T(−pf)، سپس Rz(θ) و در پایان T(pf) هستند. با Concatenation:
M = T(pf) Rz(θ) T(−pf).
حاصل ضرب:
M = [ cosθ -sinθ 0 xf - xf cosθ + yf sinθ
sinθ cosθ 0 yf - xf sinθ - yf cosθ
0 0 1 0
0 0 0 1 ]
است.
۳.۱۰.۲ Rotation عمومی
اکنون نشان میدهیم هر Rotation دلخواه حول مبدأ را میتوان از سه Rotation متوالی حول محورهای مختصات ساخت. ترتیب یکتا نیست، گرچه Rotation Matrix نهایی برای Orientation مشخص یکتا است.
ابتدا حول z، سپس حول y و در پایان حول x میچرخانیم. مکعب مرکز مبدأ و همراستا با محورها را در شکل ۳.۴۷(a) در نظر بگیرید. آن را با زاویهٔ α حول z میچرخانیم، شکل ۳.۴۷(b)، سپس با β حول y در نمای بالا، شکل ۳.۴۸، و در پایان با γ حول x در نمای جانبی، شکل ۳.۴۹. ماتریس نهایی:
R = Rx Ry Rz.
شکل ۳.۴۷ — Rotation مکعب حول محور z: (الف) پیش از چرخش؛ (ب) پس از چرخش.
شکل ۳.۴۸ — Rotation مکعب حول محور y.
شکل ۳.۴۹ — Rotation مکعب حول محور x.
با انتخاب درست α، β و γ میتوان هر Orientation دلخواه را ایجاد کرد، هرچند یافتن این زاویهها همیشه ساده نیست و در مثال بخش ۳.۱۰.۴ دشواری آن را خواهیم دید.
۳.۱۰.۳ Instance Transformation
مثال مکعب قابل چرخش، تعمیم مهمی برای Modeling پیشنهاد میکند. صحنهای شامل اشیای سادهٔ فراوان را در نظر بگیرید، شکل ۳.۵۰. یک روش آن است که رأسهای هر شیء را مستقیماً در اندازه، Orientation و Location نهایی تعریف کنیم. روش بهتر این است که هر نوع شیء فقط یک بار در اندازه، مکان و Orientation مناسب بهعنوان Prototype تعریف شود. هر حضور آن شیء در صحنه یک Instance از Prototype است و اندازه، جهت و محل موردنیاز با یک تبدیل آفین یعنی «Instance Transformation» به دست میآید.
شکل ۳.۵۰ — صحنهای از اشیای ساده.
میتوان پایگاه دادهٔ سادهای برای صحنه داشت که فهرست شناسهٔ اشیا ــ مثلاً ۱ برای Cube و ۲ برای Sphere ــ و Instance Transformation هرکدام را ذخیره کند.
ترتیب Instance Transformation در شکل ۳.۵۱ آمده است. اشیا معمولاً در Frame خود با مبدأ در مرکز جرم و اضلاع همراستا با محورهای Model Frame تعریف میشوند. نخست جسم به اندازهٔ دلخواه Scale میشود، سپس با Rotation جهتگیری آن تعیین و در پایان با Translation به مکان نهایی منتقل میشود. پس:
M = TRS.
این روش هم با معماری Pipeline و هم با روشهای نگهداری شیء مانند Scene Graph فصل ۸ سازگار است. شیء پیچیدهای که بارها استفاده میشود میتواند یک بار در Server بارگذاری شود و هر Instance فقط با ارسال Instance Transformation مناسب نمایش داده شود.
شکل ۳.۵۱ — Instance Transformation با ترتیب M=TRS.
۳.۱۰.۴ Rotation حول محور دلخواه
این مثال هم روش Rotation حول نقطه و خط دلخواه در فضا را نشان میدهد و هم استفاده از «زاویههای جهت» (Direction Angles) برای تعیین Orientation را. برای Rotation مکعب شکل ۳.۵۲ به سه موجودیت نیاز داریم: نقطهٔ ثابت p0 که مرکز مکعب فرض میشود، برداری بهعنوان محور Rotation، و زاویهٔ θ. هیچیک ذاتاً به Frame خاصی وابسته نیستند؛ بااینحال برای یافتن Affine Matrix ناچاریم نمایش آنها را در یک Frame انتخاب کنیم.
محور را میتوان با دو نقطهٔ p1 و p2 تعریف کرد:
u = p2 − p1.
ترتیب نقاط جهت مثبت Rotation را تعیین میکند. فقط Orientation بردار u مهم است، نه محل رسم آن. برای سادهشدن محاسبات u را نرمال میکنیم:
v = u/|u| = [αx,αy,αz]^T.
نقطهٔ ثابت را ابتدا به مبدأ میبریم؛ پس اولین تبدیل T(−p0) و آخرین T(p0) است. پس از Translation اولیه باید محور v را با یک محور مختصات، مثلاً z، همراستا کنیم.
شکل ۳.۵۲ — Rotation مکعب حول محور دلخواه.
شکل ۳.۵۳ — انتقال نقطهٔ ثابت به مبدأ.
راهبرد این است که با دو Rotation، v را با محور z همراستا کنیم، سپس Rotation دلخواه θ را حول z انجام دهیم و بعد دو Rotation همترازکننده را معکوس کنیم. در نتیجه:
R = Rx(−θx) Ry(−θy) Rz(θ) Ry(θy) Rx(θx).
توالی در شکل ۳.۵۴ دیده میشود. بخش دشوار یافتن θx و θy است.
شکل ۳.۵۴ — توالی Rotationها.
چون v بردار واحد است:
αx² + αy² + αz² = 1.
پارهخطی واحد از مبدأ تا (αx,αy,αz) در جهت v رسم میکنیم. عمودهای آن بر محورها، شکل ۳.۵۵، سه Direction Angle یعنی φx,φy,φz را مشخص میکنند. Direction Cosineها:
cosφx=αx
cosφy=αy
cosφz=αz.
فقط دو زاویه مستقلاند، زیرا مجموع مربع این سه کسینوس ۱ است.
شکل ۳.۵۵ — Direction Angles.
اکنون θx و θy را تعیین میکنیم. شکل ۳.۵۶ نشان میدهد Rotation نخست، پارهخط را به صفحهٔ y=0 میبرد. Projection پارهخط روی صفحهٔ x=0 طولی برابر
d = sqrt(αy² + αz²)
دارد. زاویهٔ موردنیاز همان زاویهٔ این Projection با محور z است. چون Rotation Matrix فقط به sine و cosine زاویه نیاز دارد، لازم نیست خود θx را صریح محاسبه کنیم:
Rx(θx) = [1 0 0 0
0 αz/d -αy/d 0
0 αy/d αz/d 0
0 0 0 1]
شکل ۳.۵۶ — محاسبهٔ Rotation حول x.
Ry نیز مشابه محاسبه میشود. شکل ۳.۵۷ Rotation ساعتگرد حول y را نشان میدهد، بنابراین علامت sine مهم است:
Ry(θy) = [ d 0 -αx 0
0 1 0 0
αx 0 d 0
0 0 0 1]
شکل ۳.۵۷ — محاسبهٔ Rotation حول y.
با اضافهکردن Translationهای نقطهٔ ثابت، ماتریس کامل:
M = T(p0) Rx(−θx) Ry(−θy) Rz(θ) Ry(θy) Rx(θx) T(−p0).
مثال عددی: جسم را ۴۵ درجه حول خطی از مبدأ تا نقطهٔ (1,2,3) بچرخانید. نقطهٔ ثابت مبدأ است. بردار محور را نرمال میکنیم:
(1/√14, 2/√14, 3/√14).
دو Rotation نخست این جهت را با z همراستا میکنند؛ سپس ۴۵ درجه حول z میچرخانیم و دو Rotation اول را معکوس میکنیم.
جزئیات محاسبه نشان میدهد Rotation نخست حول x با زاویهای متناظر با cos⁻¹(3/√13)، جهت را به صفحهٔ y=0 میبرد و Rotation بعدی حول y آن را با z همراستا میکند. سپس Rz(45°) اعمال میشود و دو تبدیل همترازکننده برگشت داده میشوند. حاصل، یک Rotation Matrix واحد R است که هر نقطهٔ روی خط گذرنده از مبدأ و (1,2,3) را ثابت نگه میدارد.
اگر نقطهٔ ثابت pf غیر از مبدأ باشد، ماتریس کلی:
M = T(pf) R T(−pf).
این مثال ساده نیست، اما قدرت روش «ترکیب چند تبدیل ساده برای ساخت تبدیل پیچیده» را نشان میدهد. Rotation حول نقطه یا محور دلخواه در برنامههای زیادی رخ میدهد. تفاوت اصلی معمولاً در نحوهٔ تعریف محور است؛ با Direction Angle یا Direction Cosine و تکنیکهای مشابه میتوان مسئله را حل کرد.
۳.۱۱ Transformation Matrixها در OpenGL
اکنون میتوانیم روی پیادهسازی بستهٔ تبدیل مختصات همگن و رابط آن تمرکز کنیم. Frameهایی مانند World و Camera Frame برای توسعهٔ برنامه مهماند، اما در OpenGL مبتنی بر Shader وجود یا عدم وجود آنها کاملاً تصمیم برنامهنویس است. در نسخههای Fixed-Function قدیمی این Frameها بخشی از مشخصات و State سیستم بودند.
در OpenGL نوین برنامهنویس نهفقط Frameهای مورد استفاده، بلکه محل اجرای تبدیل میان آنها را نیز تعیین میکند: برخی در Application و برخی در Shader مناسبترند.
هنگام طراحی روش تعریف و اجرای تبدیلها باید اهمیت State را در نظر بگیریم. هرچند متغیرهای State ازپیشتعریفشده در OpenGL نوین کماند، Attributeها و Matrixهایی که خودمان تعریف میکنیم عملاً State سامانه را میسازند. بنابراین نحوهٔ پردازش هر Vertex به مقادیر جاری این Stateها وابسته است.
دو تبدیل پرکاربرد «Model-View Transformation» و «Projection Transformation» هستند. Model-View نمایش اشیای هندسی را از Application/Model Frame به Camera Frame میبرد. Projection Matrix هم Projection موردنظر را اجرا میکند و هم نمایش را به Clip Coordinates میبرد. در این فصل فقط Model-View Matrix استفاده میشود. این ماتریس معمولاً Affine و دارای ۱۲ درجهٔ آزادی است. Projection Matrix نیز ۴×۴ است، اما همانطور که در فصل ۴ خواهیم دید آفین نیست.
۳.۱۱.۱ Current Transformation Matrix
تعمیم رایج در سامانههای گرافیکی «Current Transformation Matrix» یا CTM است. CTM بخشی از Pipeline است؛ اگر p رأسی در برنامه باشد، خروجی مرحلهٔ Transform برابر Cp است. محل دقیق اعمال CTM در Pipeline میتواند متفاوت باشد، اما اگر CTM داشته باشیم میتوان آن را بخشی از State سامانه دانست.
ابتدا مجموعهای ساده از توابع برای ساخت و دستکاری Affine Matrixهای ۴×۴ در نظر میگیریم. C را CTM فرض کنید. ابتدا آن را Identity Matrix میکنیم و هر زمان لازم باشد دوباره Reset میکنیم:
C ← I.
توابع تغییر C دو نوعاند: یا ماتریس جدیدی مستقیماً Load میکنند یا C را با Pre/Post-Multiplication تغییر میدهند. سه تبدیل معمول Translation، Scaling حول مبدأ و Rotation حول مبدأ هستند. در فرم Post-Multiplication:
C ← CT
C ← CS
و ادامهٔ فرم Rotation در صفحهٔ بعد آمده است.
ادامهٔ عملیات Post-Multiplication:
C ← CR.
در فرم Load نیز میتوان مستقیماً نوشت:
C ← T، C ← S، C ← R.
بیشتر سامانهها اجازه میدهند CTM با ماتریس دلخواه M بارگذاری شود:
C ← M
یا در M ضرب شود:
C ← CM.
اگرچه گاهی ماتریس را مستقیماً Set میکنیم، بیشتر اوقات ماتریس موجود را تغییر میدهیم؛ بنابراین عملی مانند C ← CR رایجتر از C ← R است.
۳.۱۱.۲ Rotation، Translation و Scaling
در برنامه و Shader، ماتریسی که معمولاً به همهٔ رأسها اعمال میشود حاصلضرب Model-View Matrix و Projection Matrix است. میتوان CTM را محصول این دو دانست، شکل ۳.۵۹، و هرکدام را جداگانه دستکاری کرد.
شکل ۳.۵۹ — Model-View و Projection Matrixها.
با کلاسهای Matrix و Vector پنج تابع برای ساخت Affine Matrixهای پایه تعریف میکنیم:
RotateX(float xangle);
RotateY(float yangle);
RotateZ(float zangle);
Translate(float dx, float dy, float dz);
Scale(float sx, float sy, float sz);
نمونهٔ تابع Rotatez:
mat4 Rotatez(const float theta)
{
float s = M_PI/180.0*theta; //convert degrees to radians
mat4 c; // an identity matrix
c[2][2] = c[3][3] = 1.0;
c[0][0] = c[1][1] = cos(s);
c[1][0] = sin(s);
c[0][1] = -c[1][0];
return c;
}
زاویهٔ Rotation بر حسب درجه است و Fixed Point مبدأ فرض میشود. در Translate پارامترها مؤلفههای Displacement Vector و در Scale پارامترها Scale Factorهای سه محورند؛ Fixed Point Scaling نیز مبدأ است.
۳.۱۱.۳ Rotation حول نقطهٔ ثابت
در بخش ۳.۱۰ دیدیم Rotation حول Fixed Point غیرمبدأ با سه مرحله ساخته میشود: انتقال Fixed Point به مبدأ، Rotation، و انتقال به مکان اصلی. برای Rotation ۴۵ درجه حول خطی در جهت (1,2,3) با Fixed Point (4,5,6):
mat4 R, ctm;
float thetax, thetay;
const float Radians_To_Degrees = 180.0/M_PI;
thetax = Radians_To_Degrees*acos(3.0/sqrt(14.0));
thetay = Radians_To_Degrees*acos(sqrt(13.0/14.0));
R = RotateX(-thetax)*RotateY(-thetay)*RotateZ(-45.0)
*RotateY(-thetax)*RotateX(thetax);
ctm = Translate(4.0, 5.0, 6.0)*R* Translate(-4.0, -5.0, -6.0);
چون Rotation دلخواه زیاد استفاده میشود، تمرین مناسبی است توابعی مانند
Rotate(float theta, vec3 d)
Rotate(float theta, float dx, float dy, float dz)
برای ساخت Rotation Matrix حول خطی در جهت d=(dx,dy,dz) بنویسیم. فایل کمکی angel.h نیز این توابع را فراهم میکند.
۳.۱۱.۴ ترتیب تبدیلها
قاعدهٔ مهم: در زنجیرهٔ ضرب ماتریسی، تبدیلی که آخر نوشته میشود نخست روی Vertex اعمال میشود. این نتیجه از Post-Multiplication ماتریس جاری به دست میآید. توالی:
C ← I
C ← CT(4,5,6)
C ← CR(45,1,2,3)
C ← CT(−4,−5,−6)
ماتریس نهایی را میسازد:
C = T(4,5,6) R(45,1,2,3) T(−4,−5,−6).
هر Vertex p سپس به q=Cp تبدیل میشود.
میتوان این ترتیب را با Stack نیز تصور کرد: ماتریسها روی Stack قرار میگیرند و هنگام اعمال نهایی با ترتیب معکوس خارج میشوند، هرچند این تشبیه دقیق نیست چون در واقع با هر تابع، CTM همان لحظه تغییر میکند.
۳.۱۲ چرخاندن مکعب
اکنون مکعب رنگی بخش ۳.۶ را تعاملی میچرخانیم. سه دکمهٔ ماوس سه محور Rotation را انتخاب میکنند. برنامه بر Callbackهای زیر استوار است:
glutDisplayFunc(display);
glutIdleFunc(spincube);
glutMouseFunc(mouse)
دو روش بنیادی برای Update نمایش بررسی میشود. در روش نخست، Model-View Matrix جدید در Display Callback ساخته و روی Vertex Data در CPU اعمال میشود و سپس دادهٔ جدید به GPU فرستاده میشود. در روش دوم فقط Matrix به Vertex Shader ارسال و تبدیل روی GPU انجام میشود. Mouse و Idle Callback در هر دو روش یکساناند.
Mouse Callback محور را با ۰، ۱ و ۲ برای x، y و z انتخاب میکند:
int axis = 0;
void mouse(int button, int state, int x, int y)
{
if(button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) axis = 0;
if(button == GLUT_MIDDLE_BUTTON && state == GLUT_DOWN) axis = 1;
if(button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN) axis = 2;
}
Idle Callback هر بار زاویهٔ محور انتخابشده را ۰٫۱ درجه افزایش میدهد:
void spinCube()
{
theta[axis] += 0.1;
if( theta[axis] > 360.0 ) theta[axis] -= 360.0;
glutPostRedisplay();
}
۳.۱۲.۱ Update در Display Callback
تابع display ابتدا Color و Depth Buffer را پاک میکند و سپس Model-View Matrix را از سه زاویه میسازد:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
mat4 ctm = RotateX(theta[0])*RotateY(theta[1])*RotateZ(theta[2]);
فرض کنید colorcube در Initialization اجرا شده و برای ۳۶ Vertex دادهٔ رنگ و موقعیت در quad_colors و points داریم. آرایهٔ دوم:
نقاط تبدیلشده را نگه میدارد:
for(i=0; i<36; i++)
{
new_points[i] = ctm*points[i];
}
اما این نقاط هنوز روی CPU هستند و باید به GPU منتقل شوند.
Vertex Attribute موقعیت را مانند قبل تنظیم میکنیم:
loc = glGetAttribLocation(program, "vPosition");
glEnableVertexAttribArray(loc);
glVertexAttribPointer(loc, 4, GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET(0));
و در Display Callback دادهٔ جدید را به GPU میفرستیم:
glBindVertexArray(abuffer);
glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(new_points), new_points,
GL_STATIC_DRAW);
glDrawArrays(GL_TRIANGLES, 0, N);
glutSwapBuffers();
ضعف اصلی این روش آن است که Current Transformation در Application اجرا و Vertex Positionها در هر Update دوباره از CPU به GPU منتقل میشوند. بنابراین از توان GPU استفاده نمیکنیم و کارایی به سرعت انتقال داده CPU→GPU محدود میشود؛ برای Geometry پیچیده این روش ضعیف است.
روش دوم Vertex Data را فقط یک بار به GPU میفرستد. هر بار که Transformation Matrix تغییر کرد، فقط Matrix جدید ارسال میشود و Vertex Positionها روی GPU Update میشوند. برای این کار باید بدانیم چگونه دادهای مانند Matrix را از CPU به Shader برسانیم.
۳.۱۲.۲ Uniform Variableها
متغیرها در برنامه میتوانند با نرخهای متفاوت تغییر کنند. Vertex Attributeها ممکن است برای هر Vertex متفاوت باشند. در مقابل گاهی پارامتری میخواهیم که برای همهٔ رأسهای یک Primitive، یا همهٔ رأسهای یک فراخوانی glDrawArrays، ثابت باشد. چنین متغیرهایی Uniform-Qualified Variable نام دارند. در مثال ما همان Rotation Matrix باید به همهٔ رأسهای آرایهٔ points اعمال شود.
Uniformها مشابه Vertex Attributeها متصل میشوند. مثلاً زمان سپریشده را میتوان با GLUT در Application گرفت:
float etime;
etime = 0.001*glutGet(GLUT_ELAPSED_TIME);
و به متغیر time در Vertex Shader فرستاد. مثال Shader که مؤلفهٔ x هر Vertex را سینوسی تغییر میدهد در صفحهٔ بعد ادامه دارد.
در Vertex Shader میتوان Uniform زمان را چنین به کار برد:
uniform float time;
attribute vec4 vPosition;
void main()
{
vPosition.x *= (1+sin(time));
gl_Position = vPosition;
}
برای مرتبطکردن متغیر time در Application و Shader، پس از Compile و Link Shaderها Location آن را میگیریم:
GLint timeParam;
timeParam = glGetUniformLocation(program, "time");
و هنگام ارسال زمان:
glUniform1f(timeParam, etime);
خانوادهٔ glUniform برای انواع GLSL شامل float, int, بردارهای ۲/۳/۴بعدی و ماتریسها فرمهای مختلف دارد. برای Rotation Matrix چهاردرچهار:
glUniformMatrix4fv(matrix_loc, 1, GL_TRUE, ctm);
matrix_loc با:
GLint matrix_loc;
matrix_loc = getUniformLocation(program, "rotation");
برای Shader زیر تعیین میشود:
in vec4 vPosition;
in vec4 vColor;
out vec4 color;
uniform mat4 rotation;
void main()
{
gl_Position = rotation*vPosition;
color = vColor;
}
پارامتر دوم glUniformMatrix4fv تعداد عنصرهای ctm ارسالی را تعیین میکند و پارامتر سوم مشخص میکند دادهها با ترتیب Row-Major فرستاده شوند. Display Callback اکنون چنین است:
mat4 ctm;
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ctm = RotateX(theta[0])*RotateY(theta[1])*RotateZ(theta[2]);
glUniformMatrix4fv(matrix_loc, 1, GL_TRUE, ctm);
glDrawArrays(GL_TRIANGLES, 0, N);
glutSwapBuffers();
}
راه دیگر آن است که فقط زاویهها به Vertex Shader فرستاده شوند و خود Rotation Matrix در Shader محاسبه شود؛ نمونهٔ این رویکرد در ضمیمهٔ A آمده است.
۳.۱۳ رابطهای برنامههای سهبعدی
در بخش ۳.۱۲ با ماوس سهدکمهای جهت Rotation مکعب را کنترل کردیم، اما این Interface محدود است. بهتر است برخی دکمهها برای اعمالی مانند Menu آزاد بمانند. از آنجا که Orientation دلخواه را با Rotation حول فقط دو محور نیز میتوان ساخت، میتوان با دو دکمه Orientation را کنترل کرد؛ اما هنوز Rotation فقط در یک جهت خواهد بود. Interface بهتر باید امکان Rotation جلو و عقب و توقف دقیق در Orientation دلخواه را بدهد.
GLUT اجازهٔ ترکیب Keyboard و Mouse را میدهد؛ مثلاً دکمهٔ چپ برای Rotation مثبت حول x و Control + Left برای Rotation منفی. بااینحال این روشها چندان شهودی نیستند. در ادامه گزینههای نرمتر و طبیعیتر بررسی میشوند.
۳.۱۳.۱ استفاده از ناحیههای صفحه
فرض کنید یک دکمه برای Orientation، یکی برای نزدیک/دورشدن و یکی برای Translation چپ/راست در نظر بگیریم. Motion Callback هم دکمهٔ فعال و هم محل Mouse را میدهد، بنابراین مکان Mouse میتواند سرعت و جهت Rotation یا Translation و حرکت جلو/عقب را کنترل کند.
برای Orientation فقط Rotation حول دو محور کافی است. دکمهٔ چپ و موقعیت Mouse را به آن اختصاص میدهیم. فاصله از مرکز صفحه سرعت Rotation x و y را تعیین میکند. در مرکز هیچ Rotation رخ نمیدهد؛ حرکت به بالا یا پایین Rotationهای مخالف حول y و حرکت به راست یا چپ Rotation حول x را ایجاد میکند. حرکت به گوشهها میتواند هر دو Rotation را همزمان اعمال کند.
دکمهٔ راست میتواند به همین شیوه Translation افقی و عمودی را کنترل کند و دکمهٔ میانی با موقعیت Mouse Translation در جهت z، یعنی نزدیک یا دورشدن، را تعیین کند. پیادهسازی این Interface در GLUT مستقیم است و بهعنوان تمرین ۳.۲۰ واگذار شده است.
۳.۱۳.۲ Trackball مجازی
کنترل Rotation دو محور با موقعیت Mouse بخش زیادی از عملکرد Trackball واقعی را شبیهسازی میکند. میتوان یک Trackball گرافیکی/مجازی کامل ساخت که حتی بدون اصطکاک باشد: پس از شروع Rotation تا زمانی که کاربر متوقفش کند ادامه یابد و در عین حال سرعت و محور Rotation قابل تغییر باشد. همین ایده برای Translation و سایر پارامترها نیز قابل استفاده است.
از نگاشت موقعیت Trackball به Mouse آغاز میکنیم. Trackball شکل ۳.۶۰ شعاع ۱ دارد.
شکل ۳.۶۰ — Frame مربوط به Trackball.
موقعیت (x,y,z) روی سطح کره را با Projection متعامد روی صفحهٔ y=0 به (x,0,z) نگاشت میکنیم، شکل ۳.۶۱. این نگاشت قابل معکوسکردن است، چون نقطهٔ سهبعدی باید معادلهٔ کرهٔ واحد را ارضا کند:
x² + y² + z² = 1.
پس از نقطهٔ صفحه (x,0,z)، مختصهٔ نیمکره:
y = sqrt(1 − x² − z²)
به دست میآید.
شکل ۳.۶۱ — Projection موقعیت Trackball روی صفحه.
با حرکت Mouse اطلاعات سهبعدی محاسبه و دنبال میشوند. اگر دو موقعیت روی نیمکره p1 و p2 داشته باشیم، بردارهای از مبدأ به آنها صفحهٔ Rotation را تعریف میکنند، شکل ۳.۶۲. نرمال صفحه:
n = p1 × p2.
حرکت Trackball از p1 به p2 با Rotation حول n اجرا میشود. زاویه Rotation زاویهٔ میان دو بردار است. چون هر دو طول واحد دارند:
|sinθ| = |n|.
اگر Mouse با نرخ بالا Tracking شود تغییرات کوچکاند و بهجای تابع معکوس مثلثاتی میتوان از تقریب sinθ ≈ θ استفاده کرد.
شکل ۳.۶۲ — محاسبهٔ صفحهٔ Rotation.
Trackball مجازی با Idle، Motion و Mouse Callback در GLUT پیاده میشود. سه Flag منطقی Tracking و Redraw را کنترل میکنند:
bool trackingMouse = false;
bool trackballMove = false;
bool redrawContinue = false;
اگر redrawContinue درست باشد، Idle Function درخواست Redisplay میدهد. اگر trackingMouse درست باشد، موقعیت Trackball در Motion Callback بهروزرسانی میشود. اگر trackballMove درست باشد، Rotation Matrix مورد استفاده در Display Routine Update میشود.
تغییر این متغیرها در Mouse Callback کنترل میشود. با فشردن دکمه، Tracking Trackball آغاز و مقدار اولیه تعیین میشود؛ سپس Motion Callback در پاسخ به تغییر موقعیت Mouse، Trackball را Update و Redisplay درخواست میکند. با رهاکردن دکمه Tracking Mouse متوقف میشود. دو موقعیت اخیر Mouse میتوانند Velocity Vector را تعیین کنند تا Rotation Matrix بهصورت پیوسته Update شود؛ در نتیجه پس از رهاکردن دکمه، جسم با سرعت ثابت به Rotation ادامه میدهد. اگر کامپایلر نوع Boolean نداشته باشد میتوان آن را با int شبیهسازی کرد.
این رفتار همان چیزی است که از یک Trackball ایدهآل و بدون اصطکاک انتظار داریم و با Mouse یا Trackball فیزیکی بهطور مستقیم حاصل نمیشود.
۳.۱۳.۳ Rotationهای نرم
رویکرد ما برای Orientation اشیا بر «زاویههای اویلر» (Euler Angles) نسبت به سه محور مختصات استوار بوده است. از این دید، Rotation Matrix دلخواه با ترکیب Rotationهای ساده حول x، y و z ساخته میشود. OpenGL هرچند Rotation حول محور دلخواه را میپذیرد، معمولاً محور و زاویه از همین راهبرد ترکیبی به دست میآیند.
اگر بخواهیم در انیمیشن میان دو Orientation حرکت کنیم، از نظر نظری میتوان:
R(θ)=Rx(θx)Ry(θy)Rz(θz)
را تشکیل داد و زاویهها را مرحلهبهمرحله تغییر داد. اما اگر Rotationهای سه محور جداگانه یا حتی همزمان Increment شوند، حرکت الزاماً نرم دیده نمیشود و ناظر ممکن است Rotationهای مجزای محورها را تشخیص دهد.
در Trackball دیدیم که مکعب میتواند نرم حول هر محور بچرخد. دو Orientation را میتوان با دو نقطه روی کرهٔ واحد مرتبط کرد؛ مسیر نرم میان آنها Great Circle روی سطح کره است. این مسیر متناظر با Rotation واحدی حول محوری است که بر صفحهٔ تعیینشده توسط دو نقطه و مرکز کره عمود است. با افزایش نرم زاویه، ناظر Rotation نرم میبیند.
فرمولبندی محورمحور ما تا حدی از وابستگی به محورهای مختصات رنج میبرد، اما ساختار عمیقتر در خود Matrix نهفته است. برای Rotation Matrix دلخواه R، همهٔ نقاط روی خطی در جهت d ثابت میمانند؛ پس برای نقطهٔ p روی آن خط:
Rp=p.
یعنی p یک Eigenvector ماتریس R با Eigenvalue برابر ۱ است. همچنین:
Rd=d.
پس d نیز Eigenvector متناظر با Eigenvalue ۱ است. p نقطهٔ ثابت Rotation و d نرمال صفحهای عمود بر محور Rotation است. در Trackball، محاسبهٔ محور Rotation عملاً یافتن Eigenvector خاصی از Rotation Matrix بود. برعکس، از یک Rotation Matrix دلخواه نیز میتوان با یافتن Eigenvalueها و Eigenvectorها، محور Rotation و Fixed Point را استخراج کرد.
۳.۱۳.۴ Rotation افزایشی
فرض کنید دو Orientation از شیئی مانند دوربین داریم و میخواهیم نرم از یکی به دیگری برویم. یک راه یافتن Great Circle مانند Trackball و افزایش تدریجی زاویه در امتداد آن است. در این حالت محور Rotation، زاویهٔ شروع، زاویهٔ پایان و Increment زاویه بر اساس تعداد Stepها مشخصاند. حلقهٔ اصلی میتواند چنین باشد:
mat4 ctm;
for(i=0, i<imax; i++)
{
thetax += dx;
thetay += dy;
thetaz += dz;
ctm = RotateXm(thetax)*RotateYm(thetay)*RotateZm(thetaz);
draw_object();
}
مشکل این روش محاسبهٔ مکرر sine و cosine سه زاویه است. بهتر است Rotation Matrix یک بار محاسبه و دوباره استفاده شود. برای زاویههای کوچک نیز تقریبهای
sinθ ≈ θ
cosθ ≈ 1
مفیدند.
اگر Rotation عمومی را با Euler Angleها بنویسیم:
R = Rz(ψ) Ry(φ) Rx(θ)
تقریب زاویهٔ کوچک شکل سادهای میدهد.
با جایگذاری تقریبها در حاصلضرب ماتریسها:
R ≈ [ 1 -ψ φ 0
ψ 1 -θ 0
-φ θ 1 0
0 0 0 1]
میشود. این تقریب محاسبهٔ Rotationهای کوچک افزایشی را بسیار سبک میکند.
۳.۱۴ کواترنیونها
Quaternionها تعمیمی از اعداد مختلطاند و روش دیگری برای توصیف و دستکاری Rotation فراهم میکنند. هرچند از رویکرد ماتریسی اولیه شهودیتر نیستند، در Animation و پیادهسازی سختافزاری Rotation مزایای مهمی دارند. جزئیات آنها در مقالهٔ بعد ادامه مییابد.
۳.۱۴.۱ اعداد مختلط و Quaternionها — آغاز بحث
در دو بعد، استفاده از اعداد مختلط برای عملیاتی مانند Rotation شناختهشده است. اگر i²=−1 و از هویت اویلر
e^(iθ)=cosθ+i sinθ
استفاده کنیم، عدد مختلط c را میتوان نوشت:
c=a+ib = r e^(iθ)
که r=sqrt(a²+b²) و θ=tan⁻¹(b/a) است.
اگر c حول مبدأ به اندازهٔ φ به c′ بچرخد:
c′ = r e^(i(θ+φ)) = r e^(iθ) e^(iφ).
پس e^(iφ) عملگر Rotation در صفحهٔ مختلط است و میتواند جایگزین کارآمدی برای Transformation Matrix دوبعدی باشد.