OpenGL - 3D Трансформации

Най-общо казано в настоящия материал ще научите как да раздвижите вашите обекти в 3D пространството и как да определите подходяща 3D перспектива на изгледа. Не се притеснявайте от това, че още не можете да създавате обекти в OpenGL, просто ще използваме готова функция от GLUT ;)
В OpenGL се използват матрици 4x4. ОpenGL предоставя готови функции с които ще можете да работите с матриците, без дори да сте чували досега за тях. В OpenGL съществуват три вида матрици: PROJECTION, MODELVIEW и TEXTURE. Те се използват съответно за определяне на изгледа в 3D пространството, задаване на траекторията на обектите/камерата и модификация на дадена текстура. В даден момент може да правите промени само по една от матриците, или по тази която се включили преди това с функцията glMatrixMode( ), приемаща един от аргументите: GL_MODELVIEW, GL_PROJECTION или GL_TEXTURE за съответната матрица. Сега започвам да обяснявам за всяка:


Projection матрица :

Използва се за дефиниране на изгледа т.е. ако изпуснете тази стъпка във вашата програма ще виждате единствено черен екран, независимо от това, дали има нещо за изрисуване или не. След като включите PROJECTION матрицата ( напимер: glMatrixMode( GL_PROJECTION ); ), трябва да дефинирате перспективата на виждане. Има два вида перспективи, които се избират съответно с функциите :

void glOrtho( GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar) - ортографична перспектива

void gluPerspective( GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar) - реалистична перспектива

void glFrustum( GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble znear, GLdouble zfar) - реалистична перспектива( еквивалентна на горната функция ).

Функцията glOrtho( ) изгражда т.н ортографична перспектива. Тя се отличава с това, че представя обектите по един и същи начин независимо от разстоянието им до камерата. Все едно имате триизмерен чертеж на къща, който представя съвсем точно къщата при каквато и да е гледна точка на камерата, а в действителност задната част от къщата би трябвало да ни се вижда по-малка от предната, защото е на по-голямо разстояние от нас. Използвайте ортографична перспектива само в случаите когато искате да изрисувате обекти в двуизмерен режим. Функцията приема 6 аргумента. Първите четири определят координатите на лява, дясна, долна и горна изрязващи равнини, а последните два аргумента определят съответно координатите на най-близкия и най-далечния обект, който ще бъде показан. Ако извикате glOrtho с координати glOrtho( - 20 , 20 , - 20 , 20 , 1 , 100 ) ще се построи един правоъгълник на перспективата на виждане със съответните координати. Всички обекти, които попадат в неговите параметри ще могат да бъдат виждани, а останалите - не. Всичко това може да видите ясно на следващата картинка.

http://cpp-examples.com/images/glOrtho.gif

Другите две функции - gluPerspective( ) и glFrustum( ) създават реалистична перспектива, като генерират пирамида за перспективата, а не правоъгълник, т.е ако обекта е по-далеч, то той се вижда съответно по-малък и обратно. Двете функции са идентични и коя ще изберете е въпрос на вкус ( аз използвам gluPerspective( ) ).

Функцията gluPerspective( ) приема четири аргумента. Първия определя ъгъла на виждане, който може да бъде от 0 - 180 ( стандартно избирайте 45 градуса ), втория аргумент се генерира като се раздели ширината на площа на виждане на височина й ( x/y ). Най-често тази площ представлява квадрат и подадения аргумент е 1. Последните два аргумента са съответно най-близкото и най-далечното разстояние на което ще бъдат видяни различните обекти.

http://cpp-examples.com/images/gluPerspective.gif

Функцията glFrustum( ) приема 6 аргумента. Първите четири аргумента определят площа на виждане при близко разстояние, а последните два аргумента - съответно най-близкото и най-далечното разстояние. Не използвам много обяснения за тази функция, защото смятам че би трябвало да ви стане ясна, след като сте прочели за предишните две, а и картинката е достатъчна.

http://cpp-examples.com/images/glFrustum.gif

Дефинирането на изгледа трябва да се усъществи във функция, която се извиква веднъж след създаването на прозореца и преди започването на цикъла на съобщенията и на рендване на изображенията т.е във функцията main( ) ако ползвате GLUT или при получаване на съответното съобщение от операционната система след създаването на прозореца, ако използвате Win32 API. Една от възможно най-добрите перспективи е : gluPerspective( 45 , 1 , 1 , 200 );

В OpenGL има функция, която определя часта от прозореца върху която ще се изрисува изображението :

void glViewport( GLint x, GLint y, GLsizei width, GLsizei height ) - първите два аргумена определят координатите на долния ляв ъгъл, а останалите два аргумента - ширината и височината на пространството в което ще се осъществи рендерирането. Ако не извикате тази функция няма да се случи нищо фатално, понеже по default графиката се рендерира на цял екран. Тази функция е изключително важна при правилното оразмеряване на графиката в прозореца, когато променим неговите размери след като вече е създаден.


MODELVIEW матрица:

Тази матрицата ви позволява да премествате, въртите, мащабирате различни обекти и да определите позиция и посока на камерата. След като предварително сте определили изгледа в main( ), трябва да превключите на MODELVIEW матрица преди да започне непрекъснатото извикване на рендериращата ви фунцкия. Много е важно как си представяте цялата сцена и нейните деформации. Например ако пред вас ( ще използвам думата "камера" ) т.е. пред камерата има обект, който е на две единици разстояние и вие преместите камерата с една единица към него, то крайния резултат би бил същия, ако бяхте преместили обекта с една единица към камерата. Всичко това се осъществява с едни и същи функции и как си обяснявате промените по сцената ( дали променяме положението на камерата върху сцената или на цялата сцена с всички обекти при статична камера ) зависи само от вас. Разбира се има някои особености за които ще стане ясно след малко. Ето някои функции от високо ниво, с които лесно ще можете да трансформирате MODELVIEW матрицата:

glTranslate{fd}( x , y , z ) - премества сцената/камерата в зависимост от подадените координати.

glRotate{fd}( angle , x , y , z ) - завърта сцената/камерата като умножава x , y и z координатите по аргумента angle. Например ако искате да завъртите обект на 30 градуса по x координата трябва да извикате съответно: glRotatef ( 30 , 1 , 0 , 0 );

glScale{fd} ( x , y , z ) - мащабира сцената. Подадените аргументи се използват като коефициент при мащабирането по x , y или z координата. Стойност по-малка от 1 води до смаляване на обекта, по-голяма от 1 - увеличаване на обекта. Ако някой аргумент е равен на 1, обектът запазва размерите си по съответната координата.

Важно е да се отбележи, че всяка промяна на матрицата се отразява само на следващите изрисувания. Функцията glLoadIdentity( void ) зарежда първоначалната матрица, по която не са правени никакви преобразувания. Например ако завъртите някой обект на 20 градуса и след това го завъртите на 30 градуса, ще получите обект завъртян на 50 градуса, ако не извикате glLoadIdentity( ) в началото на вашата рендерираща функция. Всичко се дължи на това, че втората трансформация се прави върху вече трансформирана матрица. Затова всеки път при извикване на рендериращата ви функция трябва да се зарежда и първоначалната матрица с glLoadIdentity( ).

За да определите позиция или посока на камерата ( ако си обяснявате всичко чрез движещата се камера ) трябва да извикате съответно glTranslatef( ) или glRotatef( ) в началото на вашата рендерираща функция, веднага след като сте изчистили буферите и заредили първоначалната матрица с glLoadIdentity( ). Когато позиционирате най-напред камерата, за да я придвижите напред трябва да уголемите z координатата и обратно - ако искате да я придвижите назад трябва да намалите z координатата. Стартовата ви позиция е ( 0 , 0 , 0 ). Със glRotatef( ) можете да определите посоката на гледане и за да погледнете назад трябва да завъртите камерата на 180 градуса по x или y координата. За сметка на това в началото ако искате да отдалечите един обект от камерата ( това е варианта с движението на цялата сцена при статична камера ) трябва да намаляте неговата z координата ( аналогично все едно премествате камерата назад ) и обратно ако искате да го приближите е нужно да увеличите координатата. Но ако пък завъртите сцената на 180 градуса всичко се обръща т.е. като намаляте z координатата обектите идват към статичната камерата, а след това и зад нея.

Всичко това изглежда доста сложно и надавям се, не ви обърквам още повече... Ако не ви става ясно защо е така, не се притеснявайте. Постепенно ще го научите, а и няма да ви трябва особенно засега. Най-лесно се научава с проби и грешки :) За вашата програма най-добре преместете камерата 10 единици назад ( glTranslatef ( 0 , 0 , -10 ); ), което може да се приеме и като преместване на сцената с обектите 10 единици напред, ако го разбирате по този начин. Така ако направите гаф с изрисуването на обект поне ще сте сигурни, че камерата е правилно разположена и гледа накъдето трябва :) А сега да ви представя още наколко неща за Modelview матрицата.

Вместо да определяте камерата/сцената с glTranslatef( ) и glRotatef( ) можете да използвате изключително полезната функция:

void gluLookAt ( eyex , eyey, eyez, centerx, centery, centerz, upx, upy, upz ) - функцията приема 9 аргумента. Първите 3 определят местоположението на камерата (или преместването на цялата сцена около статичната точка на виждане ), следващите 3 аргумента - координатите не гледната точка към която е насочена камерата(или завъртването на сцената), а последните 3 определят вектора, който показва кое е "нагоре". За нормална камера трябва да определите стойности за upx , upy и upz съответно 0 , 1 и 0.

Както може би вече сте забелязали схващането на 3D света в OpenGL с помоща на движеща се камера е по-лесно за разбиране, може би защото сме свикнали да се движим докато цялата карта ( сцена ) е статична, но понякога е по-лесно да се приеме варианта с движението на сцената с обектите около камерата.

Нещото с което ще завърша за Modelview матрицата, е че произволен брой отделни матрици могат да бъдат запазени в стек. Това се отнася съшо за PROJECTION и TEXTURE матрицата, но рядко се използва при тях. Важно е да знете че когато слагате матрици в стека, вие ги поставяте една върху друга и след това може да бъде извадена първо извадена само най-горната. Всякъкви промени на матрица в стека се отразяват само на нея и на останалите матрици в стека след нея, ако има такива. Поставянето на матрица в стека става с функцията glPushMatrix( ), а изваждането с glPopMatrix( ). Когато поставите матрица в стека, после трябва да я извадите за да се отразят съответните промени. Стекът е изключително важно нещо. Сега ще ви докажа с два примера :

Представете си че искате да направите два куба, първият от които се върти по x и y координата, а втория - просто да стои до първия. Най-вероятно това ще представлява рендериращата ви функция :

  1. void RenderFunction( )
  2. {
  3.  
  4.   glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
  5.  
  6.   glLoadIdentity( );
  7.  
  8.   angle++;
  9.   if ( angle>=360 ) angle=0;
  10.  
  11.   glTranslatef ( 0 , 0 , -50 ); // определя преместване на камерата 50 единици назад
  12.  
  13.   glRotate ( angle, 1 , 1 , 0 ); // завърта сцената по x и y координата
  14.  
  15.   glutSolidCube( 2 ); // тази функция създава куб със страна 2 единици
  16.  
  17.   glTranslatef ( 2 , 0 , 0 ); // премества сцената две единици надясно
  18.  
  19.   glutSolidCube( 2 );
  20.  
  21.   glutSwapBuffers( );
  22.  
  23.   glutPostRedisplay( );
  24. }

Ако изпробвате това ще бъдете разочаровани, защото докато виждате първия куб да се върти на растояние 50 единици пред вас, втория ще е на две едници разстояние до първия , въртейки се около него. Това се дължи на функцията glRotatef( ), която завърта цялата сцена ( или камерата ако си го обяснявате така, макар че в този случай и аз не мога да си го представя по този начин :) и както вече казах, че всяка промяна на матрицата се отразява на бъдещите изрисувания, то тази промяна засяга и втория куб. Функцията glTranslatef( 2, 0, 0 ) обаче въздейства само на втория куб, понеже единствено той може да се приеме за бъдещо изрисуване. Ако обаче сложите завъртането и изрисуването на първия куб в стек, то то ще се отрази само на него, без да обърква траекториите на останалите обекти, които ще се изрисуват след него. Например :

  1. void RenderFunction( )
  2. {
  3.  
  4.   glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
  5.  
  6.   glLoadIdentity( );
  7.  
  8.   angle++;
  9.   if ( angle>=360 ) angle=0;
  10.  
  11.   glTranslatef ( 0 , 0 , -50 );
  12.  
  13.   glPushMatrix( ); // поставяме матрица в стека
  14.  
  15.   glRotate ( angle , 1 , 1 , 0 ); glutSolidCube( 2 );
  16.  
  17.   glPopMatrix( ); // изваждаме матрицата от стека
  18.  
  19.   glTranslatef ( 2 , 0 , 0 ); glutSolidCube( 2 );
  20.  
  21.   glutSwapBuffers( );
  22.  
  23.   glutPostRedisplay( );
  24. }


Представете си че искате да направите кола която се състои от различни обекти ( гуми, стъкла , врати и т.н. ) и искате да я раздвижите, като едновременно с нея гумите се въртят и всичките части на колата се движат заедно с нея. Това се прави със матрици, които се слагат една след друга в стека и после се вадят, като се започне от най-горната. Ще видите това във един от примерите в сайта.

За TEXTURE матрицата ще можете да прочетете в друга тема, защото определено мястото й не е тук. Tук можете да видите един интересен сорс код използващ трансформации и матрици в стекове.

Автор: Иван Георгиев Иванов