Tryb selekcji jest jednym z trzech trybów renderingu dostępnym w bibliotece OpenGL. W trybie tym piksele nie są kopiowane do bufora ramki obrazu. W zamian za to w buforze selekcji tworzona jest lista prymitywów, których wierzchołki przecinają bryłę widzenia. Swoją budową bufor selekcji nie przypomina żadnego innego opisywanego bufora OpenGL - jest to po prostu jednowymiarowa tablica liczb całkowitych.
Zmiana trybu renderowania
Aby korzystać z selekcji obiektów w bibliotece OpenGL trzeba zmienić tryb renderingu. Służy do tego funkcja:
GLint glRenderMode( GLenum mode )
której parametr mode przyjmuje następujące wartości:
Wartość zwracana przez funkcję glRenderMode zależy od trybu renderingu. W przypadku trybu renderowania funkcja zwraca wartość 0. W trybie selekcji funkcja zwraca ilość rekordów trafień znajdujących się w buforze selekcji. Jeżeli zabraknie miejsca w buforze selekcji funkcja zwróci wartość -1. W ostatnim trybie sprzężenia zwrotnego funkcja zwraca ilość wartości zapisanych w buforze sprzężenia zwrotnego.
Tryb sprzężenia zwrotnego zostanie omówiony oddzielnie. W tym miejscu wspomnijmy jedynie, że w tym trybie piksele także nie są kopiowane do bufora ramki obrazu.
Stos nazw obiektów
Nazwy obiektów, które mogą zostać wybrane w trakcie selekcji, biblioteka OpenGL przechowuje na stosie. Jedną nazwą można opisać zarówno pojedynczy prymityw graficzny jak i dowolnie wybrana grupa obiektów.
Inicjalizację pustego stosu nazw obiektów wykonuje funkcja:
Nazwy poszczególnym obiektom nadaje się wywołując funkcję:
void glLoadName( GLuint name )
gdzie parametr name jest unikatowym identyfikatorem obiektu. Funkcja ta jednocześnie zastępuje nazwę znajdującą się aktualnie na szczycie stosu nazw.
Jeżeli chcemy wykrywać selekcję obiektów o hierarchicznej strukturze (np. elementy składowe złożonego obiektu) trzeba umieszczać kolejne nazwy obiektów na bieżącym stosie. Realizuje to funkcja:
void glPushName( GLuint name )
której parametr name jest unikatowym identyfikatorem obiektu. Po zdefiniowaniu obiektu podrzędnego trzeba zdjąć nazwę obiektu ze stosu, co wymaga wywołania funkcji:
Wielkość stosu nazw jest określana przez implementację biblioteki, ale nie może być mniejsza niż 64. Niedomiar i przepełnienie stosu powoduje zgłoszenie błędów:
GL_STACK_UNDERFLOW i
GL_STACK_OVERFLOW. Aktualną głębokość stosu zwraca wywołanie funkcji z grupy glGet z parametrem
GL_NAME_STACK_DEPTH, a ustalenie maksymalnej głębokości stosu wymaga użycie funkcji z tej samej grupy z parametrm
GL_MAX_NAME_STACK_DEPTH.
Wszystkie powyższe operacje na stosie nazw są ignorowane, gdy biblioteka OpenGL znajduje się w innym trybie renderingu niż tryb selekcji.
Przetwarzanie rekordu trafień
Ostatnią operacją, którą trzeba wykonać przy korzystaniu z selekcji obiektów jest ustawienie bufora selekcji przechowującego dane obiektów wybranych w trakcie selekcji. Wymaga to wywołania funkcji:
void glSelectBuffer( GLsizei size, GLuint * buffer )
gdzie parametr size określa wielkość bufora, do którego wskaźnik zawiera parametr buffer. Bufor powinien być na tyle duży, aby zmieścić informacje o wszystkich obiektach wybranych w wyniku selekcji, przy czym samo ustawienie bufora selekcji musi zostać wykonane przed przełączeniem biblioteki OpenGL w tryb selekcji. Przypomnijmy, że ilość wybranych obiektów zwraca funkcja glRenderMode w momencie powrotu do domyślnego trybu renderowania, czyli już po zakończeniu pracy w trybie selekcji.
Informacje o obiektach wybranych w trakcie selekcji zawarte są w tzw. rekordach trafień. Każdy rekord zawiera co najmniej cztery elementy (liczby całkowite ułożone kolejno w buforze selekcji) i ma następującą budowę:
Program przykładowy
W programie przykładowym (plik selekcja_obiektow.cpp) tworzymy cztery obiekty. Trzy z nich to sześciany, które należą do grupy CUBE. Każdy z sześcianów posiada swój własny identyfikator o nazwie odpowiadającej kolorowi (RED CUBE, GREEN CUBE i BLUE CUBE). Czwartym obiektem jest kula identyfikowana stałą o nazwie SPHERE.
Typowym przypadku, który został także zaimplementowany w prezentowanym programie, selekcję obiektów wykonuje się na podstawie położenia wskaźnika myszki, w chwili, gdy przycisk myszki został przyciśnięty. Aby uzyskać pożądany efekt trzeba ograniczyć bryłę odcinania tak aby obejmowała „najbliższą okolicę” punktu, w którym został przyciśnięty przycisk myszki. Najłatwiej jest to uzyskać korzystając z funkcji biblioteki GLU:
void gluPickMatrix( GLdouble x, GLdouble y, GLdouble deltax, GLdouble deltay, const GLint viewport[ 4 ] )
Parametry x i y określają współrzędne środka nowej bryły odcinania (oczywiście we współrzędnych okienkowych). Kolejne parametry deltax i deltay określają szerokość i wysokość bryły odcinania w pikselach. Ostatni parametr viewport zawiera dane o rozmiarach bieżącego obszaru renderingu. Dane te najłatwiej jest pobrać wywołując funkcję glGetIntegerv z parametrem
GL_VIEWPORT.
Jak ciekawostkę można podać w jaki sposób zaimplementowana została funkcja gluPickMatrix w bibliotece GLU autorstwa firmy SGI:
glTranslatef(( viewport[ 2 ] - 2 *( x - viewport[ 0 ] ) ) / deltax,( viewport[ 3 ] - 2 *( y - viewport[ 1 ] ) ) / deltay, 0 );
glScalef( viewport[ 2 ] / deltax, viewport[ 3 ] / deltay, 1.0 );
Zanim jednak, korzystając z funkcji gluPickMatrix, zmodyfikujemy bryłę odcinania trzeba odłożyć oryginalną macierz modelowania na stos. Po zmodyfikowaniu bryły odcinania wykonujemy wszystkie etapy normalnego renderingu sceny, włączając w to przekształcenia macierzy modelowania, przy czym samo renderowanie sceny wykonywane jest już po włączeniu trybu selekcji. Po zakończeniu renderingu w trybie selekcji i ustaleniu ilości rekordów trafień trzeba dokonać analizy zawartości bufora selekcji.
Analiza bufora selekcji jest ściśle związana z ilością i układem hierarchii obiektów rysowanych na scenie. W przykładowym programie obiekty są tak zdefiniowane i ułożone, że w buforze selekcji mogą znaleźć się maksymalnie dwa rekordy trafień, stąd ich analiza jest stosunkowo prosta. Przyśpieszenie analizy ułatwia hierarchia obiektów sceny. Przykładowo wiedząc, że w pierwszym rekordzie trafień znajduje się kula, mamy pewność, że drugi rekord może zawierać wyłącznie sześcian.
Efekt działania przykładowego programu przedstawiają rysunki 1 i 2. W obu przypadkach komunikaty na dole okna pokazują ilość rekordów trafień oraz obiekt, który w miejscu trafienia znajdował się najbliżej położenia obserwatora.
Plik selekcja_obiektow.cpp
#include <GL/glut.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "colors.h"
enum
{
FULL_WINDOW,
ASPECT_1_1,
EXIT
};
int aspect = FULL_WINDOW;
const GLdouble left = - 2.0;
const GLdouble right = 2.0;
const GLdouble bottom = - 2.0;
const GLdouble top = 2.0;
const GLdouble near = 3.0;
const GLdouble far = 7.0;
GLfloat rotatex = 0.0;
GLfloat rotatey = 0.0;
int button_state = GLUT_UP;
int button_x, button_y;
GLfloat scale = 1.0;
enum
{
NONE,
CUBE,
RED_CUBE,
GREEN_CUBE,
BLUE_CUBE,
SPHERE
};
char select_object[ 30 ] = "Trafienia: 0";
void DrawString( GLfloat x, GLfloat y, char * string )
{
glRasterPos2f( x, y );
int len = strlen( string );
for( int i = 0; i < len; i++ )
glutBitmapCharacter( GLUT_BITMAP_9_BY_15, string[ i ] );
}
void Display()
{
glClearColor( 1.0, 1.0, 1.0, 1.0 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glTranslatef( 0, 0, -( near + far ) / 2 );
glRotatef( rotatex, 1.0, 0.0, 0.0 );
glRotatef( rotatey, 0.0, 1.0, 0.0 );
glScalef( scale, scale, scale );
glEnable( GL_DEPTH_TEST );
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
glEnable( GL_NORMALIZE );
glEnable( GL_COLOR_MATERIAL );
glInitNames();
glPushName( NONE );
glLoadName( CUBE );
glPushName( RED_CUBE );
glColor4fv( Red );
glTranslatef( 0.0, 1.0, 0.0 );
glutSolidCube( 0.5 );
glPopName();
glPushName( GREEN_CUBE );
glColor4fv( Green );
glTranslatef( 1.0, - 1.0, 0.0 );
glutSolidCube( 0.5 );
glPopName();
glPushName( BLUE_CUBE );
glColor4fv( Blue );
glTranslatef( - 1.0, - 1.0, 0.0 );
glutSolidCube( 0.5 );
glPopName();
glLoadName( SPHERE );
glColor4fv( Orange );
glTranslatef( - 1.0, 1.0, 0.0 );
glutSolidSphere( 0.4, 20, 20 );
glDisable( GL_LIGHTING );
glDisable( GL_DEPTH_TEST );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glTranslatef( 0, 0, - near );
glColor4fv( Black );
DrawString( left + 0.02, bottom + 0.03, select_object );
glFlush();
glutSwapBuffers();
}
void Reshape( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
if( aspect == ASPECT_1_1 )
{
if( width < height && width > 0 )
glFrustum( left, right, bottom * height / width, top * height / width, near, far );
else
if( width >= height && height > 0 )
glFrustum( left * width / height, right * width / height, bottom, top, near, far );
}
else
glFrustum( left, right, bottom, top, near, far );
Display();
}
void Selection( int x, int y )
{
const int BUFFER_LENGTH = 64;
GLuint select_buffer[ BUFFER_LENGTH ];
glSelectBuffer( BUFFER_LENGTH, select_buffer );
int viewport[ 4 ];
glGetIntegerv( GL_VIEWPORT, viewport );
int width = viewport[ 2 ];
int height = viewport[ 3 ];
glMatrixMode( GL_PROJECTION );
glPushMatrix();
glLoadIdentity();
gluPickMatrix( x, height - y, 2, 2, viewport );
if( aspect == ASPECT_1_1 )
{
if( width < height && width > 0 )
glFrustum( left, right, bottom * height / width, top * height / width, near, far );
else
if( width >= height && height > 0 )
glFrustum( left * width / height, right * width / height, bottom, top, near, far );
}
else
glFrustum( left, right, bottom, top, near, far );
glRenderMode( GL_SELECT );
Display();
GLint hits = glRenderMode( GL_RENDER );
glMatrixMode( GL_PROJECTION );
glPopMatrix();
if( hits == 0 )
strcpy( select_object, "Trafienia: 0" );
if( hits == 1 )
{
if( select_buffer[ 0 ] == 1 && select_buffer[ 3 ] == SPHERE )
{
strcpy( select_object, "Trafienia: 1, obiekt: SPHERE" );
}
else
if( select_buffer[ 0 ] == 2 && select_buffer[ 3 ] == CUBE )
switch( select_buffer[ 4 ] )
{
case RED_CUBE:
strcpy( select_object, "Trafienia: 1, obiekt: RED_CUBE" );
break;
case GREEN_CUBE:
strcpy( select_object, "Trafienia: 1, obiekt: GREEN_CUBE" );
break;
case BLUE_CUBE:
strcpy( select_object, "Trafienia: 1, obiekt: BLUE_CUBE" );
break;
}
}
if( hits == 2 )
{
if( select_buffer[ 0 ] == 2 && select_buffer[ 8 ] == SPHERE )
{
if( select_buffer[ 2 ] > select_buffer[ 7 ] )
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: SPHERE" );
else
switch( select_buffer[ 4 ] )
{
case RED_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: RED_CUBE" );
break;
case GREEN_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: GREEN_CUBE" );
break;
case BLUE_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: BLUE_CUBE" );
break;
}
}
else
if( select_buffer[ 0 ] == 2 && select_buffer[ 8 ] == CUBE )
{
if( select_buffer[ 2 ] > select_buffer[ 7 ] )
switch( select_buffer[ 9 ] )
{
case RED_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: RED_CUBE" );
break;
case GREEN_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: GREEN_CUBE" );
break;
case BLUE_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: BLUE_CUBE" );
break;
}
else
switch( select_buffer[ 4 ] )
{
case RED_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: RED_CUBE" );
break;
case GREEN_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: GREEN_CUBE" );
break;
case BLUE_CUBE:
strcpy( select_object, "Trafienia: 2, pierwszy obiekt: BLUE_CUBE" );
break;
}
}
}
}
void Keyboard( unsigned char key, int x, int y )
{
if( key == '+' )
scale += 0.05;
else
if( key == '-' && scale > 0.05 )
scale -= 0.05;
Display();
}
void SpecialKeys( int key, int x, int y )
{
switch( key )
{
case GLUT_KEY_LEFT:
rotatey -= 1;
break;
case GLUT_KEY_UP:
rotatex -= 1;
break;
case GLUT_KEY_RIGHT:
rotatey += 1;
break;
case GLUT_KEY_DOWN:
rotatex += 1;
break;
}
Reshape( glutGet( GLUT_WINDOW_WIDTH ), glutGet( GLUT_WINDOW_HEIGHT ) );
}
void MouseButton( int button, int state, int x, int y )
{
if( button == GLUT_LEFT_BUTTON )
{
Selection( x, y );
glutPostRedisplay();
button_state = state;
if( state == GLUT_DOWN )
{
button_x = x;
button_y = y;
}
}
}
void MouseMotion( int x, int y )
{
if( button_state == GLUT_DOWN )
{
rotatey += 30 *( right - left ) / glutGet( GLUT_WINDOW_WIDTH ) *( x - button_x );
button_x = x;
rotatex -= 30 *( top - bottom ) / glutGet( GLUT_WINDOW_HEIGHT ) *( button_y - y );
button_y = y;
glutPostRedisplay();
Selection( x, y );
}
}
void Menu( int value )
{
switch( value )
{
case FULL_WINDOW:
aspect = FULL_WINDOW;
Reshape( glutGet( GLUT_WINDOW_WIDTH ), glutGet( GLUT_WINDOW_HEIGHT ) );
break;
case ASPECT_1_1:
aspect = ASPECT_1_1;
Reshape( glutGet( GLUT_WINDOW_WIDTH ), glutGet( GLUT_WINDOW_HEIGHT ) );
break;
case EXIT:
exit( 0 );
}
}
int main( int argc, char * argv[] )
{
glutInit( & argc, argv );
glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH );
glutInitWindowSize( 500, 500 );
#ifdef WIN32
glutCreateWindow( "Selekcja obiektów" );
#else
glutCreateWindow( "Selekcja obiektow" );
#endif
glutDisplayFunc( Display );
glutReshapeFunc( Reshape );
glutKeyboardFunc( Keyboard );
glutSpecialFunc( SpecialKeys );
glutMouseFunc( MouseButton );
glutMotionFunc( MouseMotion );
glutCreateMenu( Menu );
int MenuAspect = glutCreateMenu( Menu );
#ifdef WIN32
glutAddMenuEntry( "Aspekt obrazu - całe okno", FULL_WINDOW );
#else
glutAddMenuEntry( "Aspekt obrazu - cale okno", FULL_WINDOW );
#endif
glutAddMenuEntry( "Aspekt obrazu 1:1", ASPECT_1_1 );
glutCreateMenu( Menu );
glutAddSubMenu( "Aspekt obrazu", MenuAspect );
#ifdef WIN32
glutAddMenuEntry( "Wyjście", EXIT );
#else
glutAddMenuEntry( "Wyjscie", EXIT );
#endif
glutAttachMenu( GLUT_RIGHT_BUTTON );
glutMainLoop();
return 0;
}