Standardowo biblioteka OpenGL korzysta z modelu barw RGB, który wykorzystuje trzy barwy podstawowe: czerwoną (R), zieloną (G) i niebieską (B). Jak wspomnieliśmy na początku kursu barwa może być określona bezpośrednio poprzez wartości składowych bądź w trybie indeksowym z użyciem mapy (tablicy) barw. Tryb indeksowy zostanie opisany, ale przykładowe programy nie będą go stosowały. Przedtem jednak poznamy budowę bufora koloru - najważniejszego elementu bufora ramki obrazu.
Bufor koloru
Bufor koloru służy do przechowywania obrazu renderowanej sceny 3D.
Jest on podzielony na kilka buforów. Typowo OpenGL stosuje dwa bufory koloru: przedni i tylny. Bieżąca scena znajduje się w przednim buforze i jest wyświetlana na ekranie monitora, a jednocześnie w tylnym buforze rysowana jest kolejna ramka sceny. Po zakończeniu rysowania, bufory są zamieniane i cały proces zaczyna się od początku. Podwójne buforowanie eliminuje migotanie obrazu występujące przy stosowaniu pojedynczego bufora koloru.
Niektóre implementacje biblioteki OpenGL udostępniają stereoskopowy tryb działania bufora koloru, tj. generowanie dwóch niezależnych obrazów dla prawego i lewego oka. Oba kanały stereoskopowe mają własne niezależne bufory przednie i tylne. Ponadto implementacja OpenGL może posiadać dodatkowe (pomocnicze) bufory koloru.
Wybór trybu działania bufora kolorów dokonywany jest na początku działania programu. W przypadku biblioteki GLUT realizuje to opisywana już funkcja glutInitDisplayMode, której parametrem jest maska bitowa.
Stała GLUT_SINGLE wybiera domyślny w bibliotece GLUT tryb pojedynczego bufora koloru. Podwójne buforowanie wymaga stałej GLUT_DOUBLE. Z kolei tryb stereoskopowy uruchamia stała GLUT_STEREO.
Inna grupa stałych odpowiada, za wybór sposobu zapisu kolorów w buforach koloru: GLUT_RGB lub GLUT_RGBA - bezpośredni tryb RGB wyświetlania kolorów (bez kanału alfa), GLUT_INDEX - tryb indeksowy z mapą kolorów, GLUT_ALPHA - obsługa kanału alfa, GLUT_LUMINANCE - alternatywny tryb wyświetlania obrazu w odcieniach szarości, w którym składowe R pikseli stanowią indeksy do mapy kolorów początkowo zawierającej liniowo uporządkowane odcienie szarości.
Kolor - tryb bezpośredni
W trybie bezpośrednim kolor ustalany jest przy użyciu funkcji z grupy glColor. Funkcje te można podzielić na trzy grupy. Pierwsza to funkcje trójargumentowe, przyjmujące składowe RGB w różnych formatach; druga grupa to funkcje czteroargumentowe umożliwiające określenie także wartości składowej alfa (RGBA); trzecia to funkcje przyjmujące wskaźnik na tablicę ze składowymi RGB lub RGBA. W przypadku, gdy kolor określają trzy składowe wartość składowej alfa wynosi 1.0. Początkową wartość bieżącego koloru określają składowe RGBA: (1, 1, 1, 1).
Funkcje przyjmujące argumenty zmiennoprzecinkowe wymagają wartości z przedziału [0, 1], gdzie 0 określa minimalną wartość składowej koloru, a 1 maksymalną wartość składowej. Wartości przekraczające ten przedział są odpowiednio przycinane. Funkcje przyjmujące argumenty całkowitoliczbowe przekształcają te wartości do liczb zmiennoprzecinkowych z przedziału [0, 1] wg schematu przedstawionego w tabeli 1, gdzie c oznacza składową koloru.
Tabela 1: Konwersja składowych koloru w OpenGL
Funkcje z grupy glColor3
void glColor3b( GLbyte red, GLbyte green, GLbyte blue )
void glColor3d( GLdouble red, GLdouble green, GLdouble blue )
void glColor3f( GLfloat red, GLfloat green, GLfloat blue )
void glColor3i( GLint red, GLint green, GLint blue )
void glColor3s( GLshort red, GLshort green, GLshort blue )
void glColor3ub( GLubyte red, GLubyte green, GLubyte blue )
void glColor3ui( GLuint red, GLuint green, GLuint blue )
void glColor3us( GLushort red, GLushort green, GLushort blue )
void glColor3bv( const GLbyte * v )
void glColor3dv( const GLdouble * v )
void glColor3fv( const GLfloat * v )
void glColor3iv( const GLint * v )
void glColor3sv( const GLshort * v )
void glColor3ubv( const GLubyte * v )
void glColor3uiv( const GLuint * v )
void glColor3usv( const GLushort * v )
Funkcje z grupy glColor4
void glColor4b( GLbyte red, GLbyte green, GLbyte blue,
GLbyte alpha )
void glColor4d( GLdouble red, GLdouble green, GLdouble blue, GLdouble alpha )
void glColor4f( GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha )
void glColor4i( GLint red, GLint green, GLint blue, GLint alpha )
void glColor4s( GLshort red, GLshort green, GLshort blue, GLshort alpha )
void glColor4ub( GLubyte red, GLubyte green, GLubyte blue, GLubyte alpha )
void glColor4ui( GLuint red, GLuint green, GLuint blue, GLuint alpha )
void glColor4us( GLushort red, GLushort green, GLushort blue, GLushort alpha )
void glColor4bv( const GLbyte * v )
void glColor4dv( const GLdouble * v )
void glColor4fv( const GLfloat * v )
void glColor4iv( const GLint * v )
void glColor4sv( const GLshort * v )
void glColor4ubv( const GLubyte * v )
void glColor4uiv( const GLuint * v )
void glColor4usv( const GLushort * v )
Kolor - tryb indeksowy
W trybie indeksowym kolor określany jest pojedynczą liczbą - indeksem do mapy (tablicy) kolorów, która zawiera informacje o składowych RGB.
Typowo wielkość mapy kolorów wynosi od 256 (28) do 4.096 (212). Zarządzanie mapą kolorów jest uzależnione od konkretnego systemu okienkowego, w którym pracuje program. Biblioteka OpenGL nie posiada żadnych mechanizmów pozwalających na zmianę zawartości mapy kolorów oraz zmiany jej rozmiarów.
Wybór indeksu bieżącego koloru realizują funkcje z grupy glIndex:
void glIndexd( GLdouble c )
void glIndexf( GLfloat c )
void glIndexi( GLint c )
void glIndexs( GLshort c )
void glIndexdv( const GLdouble * c )
void glIndexfv( const GLfloat * c )
void glIndexiv( const GLint * c )
void glIndexsv( const GLshort * c )
Początkowa wartość indeksu bieżącego koloru wynosi 1.
Czyszczenie bufora koloru w trybie indeksowym wykonuje funkcja:
void glClearIndex( GLfloat c )
gdzie parametr c określa indeks mapy kolorów. Domyślna wartość indeksu wynosi 0. Jest to odpowiednik funkcji glClearColor działającej w trybie bezpośrednim.
Cieniowanie
Cieniowaniem nazywamy metodę ustalania koloru poszczególnych pikseli wielokąta. Biblioteka OpenGL udostępnia standardowo dwa modele cieniowania: cieniowanie płaskie oraz cieniowanie gładkie. W cieniowaniu płaskim wielokąt otrzymuje jeden kolor - określony dla ostatniego wierzchołka (wyjątek stanowi prymityw GL_POLYGON, o kolorze którego decyduje kolor określony dla pierwszego wierzchołka). Cieniowanie gładkie wykorzystuje algorytm Gourauda, który w uproszczeniu polega na liniowej interpolacji wartości składowych koloru określonych dla każdego wierzchołka wielokąta.
Cieniowane gładkie daje zadowalające efekty przy stosunkowo niewielkich wymaganiach obliczeniowych.
Wybór rodzaju cieniowania umożliwia funkcja:
void glShadeModel( GLenum mode )
której parametr mode przyjmuje jedną z wartości:
Domyślnie stosowane jest cieniowanie gładkie.
Dithering
Wewnętrznie biblioteka OpenGL opisuje kolory za pomocą liczb zmiennoprzecinkowych. Natomiast końcowy efekt widoczny na ekranie monitora zależy od możliwości systemu okienkowego oraz karty gra?cznej. W sytuacji, gdy w systemie dostępna jest niewielka ilość barw (np. 256), biblioteka OpenGL może symulować kolory metodą ditheringu. W uproszczeniu algorytm ten umieszcza w pobliżu siebie różnokolorowe plamki (piksele) tak aby sprawiały wrażenie jednolitej barwy. Technika ta bywa określana także mianem rozpraszania, rozsiewania lub roztrząsania.
Włączenie i wyłączenie ditheringu wymaga odpowiedniego użycia funkcji glEnable/glDisable z parametrem GL_DITHER. Domyślnie dithering jest wyłączony.
Program przykładowy
Przykładowy program (plik trojkat.cpp) to nieco uproszczona wersja klasycznego programu wykorzystującego bibliotekę OpenGL. Początkowo barwa każdego z wierzchołków odpowiada maksymalnej wartości każdej z trzech składowych modelu RGB (patrz rysunek 1).
Program umożliwia zmianę kolorów wierzchołków trójkąta. Służą do tego przyciski r, g, b (zmniejszanie wartości składowych RGB) oraz R, G, B (zwiększanie wartości składowych RGB). Ponadto z poziomu menu podręcznego użytkownik może wybrać rodzaj cieniowania oraz włączyć i wyłączyć dithering. Na dole okna program wyświetla m.in. ilość bitów przypadającą na każdą składową RGBA. Wykorzystano do tego funkcję glGetIntegerv z pierwszym parametrem o wartości: GL_RED_BITS, GL_GREEN_BITS, GL_BLUE -BITS i GL_ALPHA_BITS.
Po uruchomieniu programu w trybie 256 kolorowym Czytelnik ma możliwość sprawdzenia zarówno efektów działania algorytmu ditheringu (rysunek 2) jak też i działania cieniowania gładkiego przy ograniczonej ilości barw (rysunek 3). Oba rysunki powstały w systemie Windows XP w trybie zgodności 256 kolorów. Ocenę jakości tak generowanej gra?ki pozostawiamy Czytelnikowi.
Plik trojkat.cpp
#include <GL/glut.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
enum
{
FLAT_SHADING,
SMOOTH_SHADING,
DITHERING,
EXIT
};
GLenum shading_model = GL_SMOOTH;
GLboolean dithering = GL_FALSE;
GLfloat red = 1.0, green = 1.0, blue = 1.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 );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
if( dithering == GL_TRUE )
glEnable( GL_DITHER );
glShadeModel( shading_model );
glBegin( GL_TRIANGLES );
glColor3f( red, 0.0, 0.0 );
glVertex2f( 0.1, 0.1 );
glColor3f( 0.0, green, 0.0 );
glVertex2f( 0.9, 0.3 );
glColor3f( 0.0, 0.0, blue );
glVertex2f( 0.3, 0.9 );
glEnd();
char string[ 100 ];
glColor3f( 0.0, 0.0, 0.0 );
GLint r, b, g, a;
glGetIntegerv( GL_RED_BITS, & r );
glGetIntegerv( GL_GREEN_BITS, & g );
glGetIntegerv( GL_BLUE_BITS, & b );
glGetIntegerv( GL_ALPHA_BITS, & a );
sprintf( string, "BITS: R=%i G=%i B=%i A=%i", r, g, b, a );
DrawString( 0, 0.01, string );
if( glIsEnabled( GL_DITHER ) == GL_TRUE )
strcpy( string, "GL_DITHER: true" );
else
strcpy( string, "GL_DITHER: false" );
DrawString( 0, 0.05, string );
glDisable( GL_DITHER );
glFlush();
glutSwapBuffers();
}
void Reshape( int width, int height )
{
glViewport( 0, 0, width, height );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluOrtho2D( 0, 1, 0, 1 );
Display();
}
void Keyboard( unsigned char key, int x, int y )
{
if( key == 'R' && red < 1.0 )
red += 0.05;
else
if( key == 'r' && red > 0.0 )
red -= 0.05;
if( key == 'G' && green < 1.0 )
green += 0.05;
else
if( key == 'g' && green > 0.0 )
green -= 0.05;
if( key == 'B' && blue < 1.0 )
blue += 0.05;
else
if( key == 'b' && blue > 0.0 )
blue -= 0.05;
Display();
}
void Menu( int value )
{
switch( value )
{
case FLAT_SHADING:
shading_model = GL_FLAT;
Display();
break;
case SMOOTH_SHADING:
shading_model = GL_SMOOTH;
Display();
break;
case DITHERING:
dithering = !dithering;
Display();
break;
case EXIT:
exit( 0 );
}
}
int main( int argc, char * argv[] )
{
glutInit( & argc, argv );
glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB );
glutInitWindowSize( 500, 500 );
#ifdef WIN32
glutCreateWindow( "Trójkąt" );
#else
glutCreateWindow( "Trojkat" );
#endif
glutDisplayFunc( Display );
glutReshapeFunc( Reshape );
glutKeyboardFunc( Keyboard );
int ShadingModel = glutCreateMenu( Menu );
#ifdef WIN32
glutAddMenuEntry( "Cieniowanie plaskie", FLAT_SHADING );
glutAddMenuEntry( "Cieniowanie gladkie (Gourauda)", SMOOTH_SHADING );
#else
#endif
glutCreateMenu( Menu );
glutAddSubMenu( "Model cieniowania", ShadingModel );
#ifdef WIN32
glutAddMenuEntry( "Dithering: włącz/wyłącz", DITHERING );
glutAddMenuEntry( "Wyjście", EXIT );
#else
glutAddMenuEntry( "Dithering: wlacz/wylacz", DITHERING );
glutAddMenuEntry( "Wyjscie", EXIT );
#endif
glutAttachMenu( GLUT_RIGHT_BUTTON );
glutMainLoop();
return 0;
}