Wstępna implementacja GUI 3D z wykorzystaniem Stencil bufora
W tym wpisie chcę się podzielić sposobem implementacji podwalin GUI 3D w swoim frameworku “Sense” przy wykorzystaniu bufora Stencil. Wpis jest prawie artykułem i być może po rozwinięciu tematu taki kiedyś powstanie. Adresatami są osoby zainteresowane tzw. “silnikologią”, czyli pisaniem własnych silników do gier, jednocześnie mające doświadczenie w programowaniu i zaznajomione przynajmniej w minimalnym stopniu z jakimś API graficznym. Pierwsza część wpisu omawia sposób opakowania zdarzeń w system menadżera i jego słuchaczy. Ci co mają taką warstwę abstrakcji już zaimplementowaną (lub po prostu mają ją “gdzieś” :) ) powinni przejść do części opisującej sam system rozpoznawania obiektów pod myszą i przesyłania im zdarzeń.
Od razu zaznaczam, że będzie dużo kodu opakowującego wszystko w obiekty, ale też wspomnę, że w tym wpisie nie opiszę jak zainicjować stencil, jest to w dużej mierze zależne od używanego API graficznego. W związku z tym polecam szukać tych informacji w dokumentacji danego API / biblioteki, jednak jeżeli ktoś będzie miał z tym problem to postaram się odpowiedzieć :). Dodam tylko, że potrzebny jest nam bufor przynajmniej o ośmio-bitowej głębi – przy której będziemy w stanie obsłużyć 255 elementów GUI.
Wynik
Zanim jeszcze przejdę do rzeczy pokażę co zaimplementowałem na kodzie który w około 90% pokrywa się z tym opisanym tutaj. Otóż napisałem sobie kontrolkę która w założeniu ma służyć do przemieszczania, obracania i skalowania obiektów w edytorze, który sobię koduję na boku.
Oto rezultat:

Do pobrania:
http://www.mi-ku.net/projekty/sense/demos/Sense3DGUISample.zip
Do implementacji GUI 3D jest nam potrzebne połączenie wejścia (klawiatura + mysz) z elementami graficznymi które znajdują się w przestrzeni trójwymiarowej. Aby kierować zdarzenia z wejść do obiektów z reprezentacją graficzną potrzebny jest nam jakiś nadrzędny byt, który stwierdzi, że akurat ten obiekt powinien otrzymywać zdarzenia. Już na początku mamy pewien problem związany z tym, że pozycje zdarzeń myszy (kliknięcia i ruch) otrzymujemy w przestrzeni dwuwymiarowej a reprezentacja obiektów trzymamy w przestrzeni trójwymiarowej. W związku z tym potrzebna jest nam technika rozpoznania jaki obiekt znajduje się na danej pozycji bufora 2d obrazu. Często stosowanymi technikami są:
- użycie pomocniczego bufora obrazu
- kolizje promienia utworzonego na podstawie pozycji 2D myszy z obiektami na scenie
Ja wybrałem użycie bufora pomocniczego, w nadziei, że będzie szybsze niż sprawdzanie kolizji siatek 3D z promieniem. No i ten sposób wydaje mi się łatwiejszy w implementacji.
Jak stencil nam pomoże w stwierdzeniu jaki obiekt znajduje się na danej pozycji w 2D? Otóż zastosujemy klasyczną technikę, renderowania każdego obiektu z innym indeksem (wartością którą może przechowywać stencil), a w obsłudze zdarzeń myszy stwierdzimy który element jest aktywny przez odczytywanie wartości bufora na danej pozycji myszy. W skrócie pętla renderingu wygląda tak:
- Czyszczenie bufora obrazu, głębi oraz stencila
- Renderowanie wszystkich elementów GUI do stencila
- Renderowanie wszystkich elementów GUI do właściwego bufora obrazu
W większości przypadków chyba oba kroki można połączyć i rysować od razu do stencila i własciwego bufora obrazu.
A w obsłudze zdarzeń myszy:
- Ustal pozycje myszy.
- Pobierz wartość bufora stencil na danej pozycji myszy.
- Jeżeli pobrana wartość równa się wartości początkowej (np. 0) odrzuć zdarzenie.
- Znajdź element GUI który odpowiada danej wartości bufora.
- Wyślij zdarzenie myszy do znalezionego elementu.
Menedżer zdarzeń
Na wstępie implementacji jest nam potrzebny jakiś menedżer zdarzeń klawiatury i myszy. W przypadku kiedy w używanym przez nas API mamy dostęp do zdarzeń “niskopoziomowo” (np. procedura obsługi okna) to opakujmy wszystko w klasy takie jak InputManager wraz ze słuchaczami dziedziczącymi po KeyboardEventListener i MouseEventListener. Przykład implementacji:
{
protected:
typedef std::vector< T* > ListenerList;
ListenerList Listeners;
public:
void AddEventListener( T* _Listener )
{
Listeners.push_back( _Listener );
ListenerAdded( _Listener );
}
void RemoveEventListener( T* _Listener )
{
ListenerList::iterator FindResultIterator = std::find( Listeners.begin(), Listeners.end(), _Listener );
if ( FindResultIterator == Listeners.end() ) return;
Listeners.erase( FindResultIterator );
ListenerRemoved( _Listener );
}
void RemoveAllListeners()
{
Listeners.clear();
}
virtual void ListenerAdded( T* _Listener ) {}
virtual void ListenerRemoved( T* _Listener ) {}
};
class InputManager :
public EventListenerCollector< KeyboardEventListener >,
public EventListenerCollector< MouseEventListener >,
{
public:
void KeyDown( KeyboardEventKeyDown &E );
void KeyUp( KeyboardEventKeyUp &E );
void MouseMoved( MouseEventMove &E );
void MouseButtonUp( MouseEventButtonUp &E );
void MouseButtonDown( MouseEventButtonDown &E );
void MouseButtonClick( MouseEventButtonClick &E );
void AddKeyboardEventListener( KeyboardEventListener* );
void RemoveKeyboardEventListener( KeyboardEventListener* );
void AddMouseEventListener( MouseEventListener * );
void RemoveMouseEventListener( MouseEventListener * );
};
Żeby nie utrudniać czytania implementacje metod klasy InputManager pominąłem, ale w zasadzie są dosyć oczywiste. Pierwsze sześć metod zajmuje się przekazywaniem konkretnych struktur zdarzeń do słuchaczy. Kolejne metody Add/Remove nie robią nic innego jak dodają/usuwają kolejnych słuchaczy z kolekcji EventListenerCollector< KeyboardEventListener >::Listeners oraz EventListenerCollector< MouseEventListener >::Listeners.
Podpięcie takiego managera do obsługi zdarzeń z okna może wyglądać następująco:
InputManager *IM = …
switch( Message )
{
case WM_KEYDOWN:
{
KeyboardEventKeyDown E( WW, static_cast< KeyboardEventKeyDown::VirtualKeyCodeType >( WParam ) );
IM->KeyDown( E );
}
break;
case WM_KEYUP:
{
KeyboardEventKeyUp E( WW, static_cast< KeyboardEventKeyUp::VirtualKeyCodeType >( WParam ) );
IM->KeyUp( E );
}
break;
case WM_MOUSEMOVE:
{
MouseEventMove E( WW, static_cast< MouseEventMove::CoordinateType >( LOWORD( LParam ) ), static_cast< MouseEventMove::CoordinateType >( HIWORD( LParam ) ) );
IM->MouseMoved( E );
}
break;
case WM_LBUTTONDBLCLK:
{
MouseEventButtonClick E( WW, Sense::BUTTON_LEFT );
IM->MouseButtonClick( E );
}
break;
case WM_LBUTTONDOWN:
{
MouseEventButtonDown E( WW, Sense::BUTTON_LEFT, LOWORD( LParam ), HIWORD( LParam ) );
IM->MouseButtonDown( E );
}
break;
case WM_LBUTTONUP:
{
MouseEventButtonUp E( WW, Sense::BUTTON_LEFT, LOWORD( LParam ), HIWORD( LParam ) );
WW->MouseButtonUp( E );
}
break;
// etc. dla kolejnych eventów
…
}
Mamy już klasę tworzącą warstwę pomiędzy zdarzeniami z obsługi okna oraz obiekt do dalszego ich rozsyłania. Stworzenie tej warstwy umożliwia również implementacje obsługi zdarzeń z innych źródeł na innych platformach niż Windows, czy też chociażby na zaimplementowanie sprawdzania asynchronicznego stanu urządzeń wejściowych.
Rozdzielanie zdarzeń do elementów GUI 3D
Teraz czas na implementacje obiektu, który będzie zarządzał zdarzeniami w ramach obiektów z reprezentacją w przestrzeni trójwymiarowej. Zastanówmy się co jest nam potrzebne. Tak jak wspomniałem obiekty powinny się renderować oprócz do bufora obrazu to i do stencila. Musimy posiadać jakiś mechanizm który powie tym obiektom z jaką wartością stencila mają się renderować. Stwórzmy klasę którą nazwiemy np. StencilInputEventDispatcher która będzie mogła przechowywać wskaźniki do swoich słuchaczy (prawie identycznie jak w przypadku InputManager ) jednak dodatkowo do klasy słuchacza dodamy metody ustawiające wartość stencila oraz odczytujące ustawioną wartość (Set/GetIndex). Przydadzą się jeszcze metody MouseOver i MouseOut, dla oświadczenia danemu słuchaczowi (czyli obiektowi 3D), że najechaliśmy na niego myszą lub też z niego “zjechaliśmy”. Od klasy StencilInputEventDispatcher oczekujemy również, że będzie przesyłać zdarzenia wtedy gdy “zjedziemy” myszą z obiektu z wciśniętym przyciskiem myszy.
Przykład imlpementacji:
{
public:
virtual void MouseMoved ( MouseEventMove &E ) {};
virtual void MouseButtonUp ( MouseEventButtonUp &E ) {};
virtual void MouseButtonDown ( MouseEventButtonDown &E ) {};
virtual void MouseButtonClick ( MouseEventButtonClick &E ) {};
virtual void KeyDown ( KeyboardEventKeyDown &E ) {};
virtual void KeyUp ( KeyboardEventKeyUp &E ) {};
virtual void MouseOver () {}
virtual void MouseOut () {}
virtual int GetIndex () = 0;
virtual void SetIndex ( int ) = 0;
};
template < typename T >
class ValuePool
{
public:
bool Release( T Value )
{
ValueList::iterator It = std::find( Values.begin(), Values.end(), Value );
if( It != Values.end() )
{
Values.erase( It );
return true;
}
return false;
}
int Reserve()
{
ValueList::iterator It;
int Value = 0; // 0 is reserved
// TODO: search optimization
do
{
It = std::find( Values.begin(), Values.end(), Value + 1 );
++Value;
} while ( It != Values.end() );
Values.push_back( Value );
return Value;
}
private:
typedef std::vector< T > ValueList;
ValueList Values;
};
class StencilInputEventDispatcher : public MouseEventListener, public KeyboardEventListener, public EventListenerCollector< StencilInputEventListener >
{
public:
StencilInputEventDispatcher();
virtual void ListenerAdded ( StencilInputEventListener* _Listnerer );
virtual void ListenerRemoved ( StencilInputEventListener* _Listnerer );
virtual void MouseMoved ( MouseEventMove &E );
virtual void MouseButtonUp ( MouseEventButtonUp &E );
virtual void MouseButtonDown ( MouseEventButtonDown &E );
virtual void MouseButtonClick ( MouseEventButtonClick &E );
virtual void KeyDown ( KeyboardEventKeyDown &E );
virtual void KeyUp ( KeyboardEventKeyUp &E );
private:
ValuePool< unsigned short > IndexPool;
void DetectListeningIndex( MouseEventMove &E );
int MouseMovedListeningIndex;
bool LeftDown, RightDown, MiddleDown;
StencilInputEventListener *MouseOverListener;
};
StencilInputEventDispatcher::StencilInputEventDispatcher()
: MouseMovedListeningIndex( -1 ), LeftDown( false ), RightDown( false ), MiddleDown( false ), MouseOverListener( NULL )
{
}
void StencilInputEventDispatcher::ListenerAdded( StencilInputEventListener* _Listener )
{
_Listener->SetIndex( IndexPool.Reserve() );
}
void StencilInputEventDispatcher::ListenerRemoved( StencilInputEventListener* _Listener )
{
IndexPool.Release( _Listener->GetIndex() );
}
void StencilInputEventDispatcher::DetectListeningIndex( MouseEventMove &E )
{
unsigned int Pixel = 0;
Renderer = … ;
Pixel = Renderer->ReadStencilPoint( E.X, E.Y );
MouseMovedListeningIndex = Pixel;
}
void StencilInputEventDispatcher::MouseMoved ( MouseEventMove &E )
{
if ( !LeftDown && !RightDown && !MiddleDown )
{
int iLastIndex = MouseMovedListeningIndex;
DetectListeningIndex( E );
if ( iLastIndex != MouseMovedListeningIndex )
{
if ( MouseOverListener != NULL )
{
MouseOverListener->MouseOut();
}
MouseOverListener = NULL;
}
}
EventListenerCollector< StencilInputEventListener >::ListenerList::iterator It = Listeners.begin();
while( It != Listeners.end() )
{
StencilInputEventListener *pL = *It;
if ( pL->GetIndex() == MouseMovedListeningIndex )
{
( *It )->MouseMoved( E );
if ( MouseOverListener == NULL )
{
MouseOverListener = pL;
MouseOverListener->MouseOver();
}
}
++It;
}
};
void StencilInputEventDispatcher::MouseButtonUp ( MouseEventButtonUp &E )
{
switch( E.Button )
{
case BUTTON_LEFT:
LeftDown = false;
break;
case BUTTON_RIGHT:
RightDown = false;
break;
case BUTTON_MIDDLE:
MiddleDown = false;
break;
}
EventListenerCollector< StencilInputEventListener >::ListenerList::iterator It = Listeners.begin();
while( It != Listeners.end() )
{
StencilInputEventListener *pL = *It;
if ( pL->GetIndex() == MouseMovedListeningIndex )
{
( *It )->MouseButtonUp( E );
}
++It;
}
};
void StencilInputEventDispatcher::MouseButtonDown ( MouseEventButtonDown &E )
{
switch( E.Button )
{
case BUTTON_LEFT:
LeftDown = true;
break;
case BUTTON_RIGHT:
RightDown = true;
break;
case BUTTON_MIDDLE:
MiddleDown = true;
break;
}
EventListenerCollector< StencilInputEventListener >::ListenerList::iterator It = Listeners.begin();
while( It != Listeners.end() )
{
StencilInputEventListener *pL = *It;
if ( pL->GetIndex() == MouseMovedListeningIndex )
{
( *It )->MouseButtonDown( E );
}
++It;
}
};
void StencilInputEventDispatcher::MouseButtonClick ( MouseEventButtonClick &E )
{
EventListenerCollector< StencilInputEventListener >::ListenerList::iterator It = Listeners.begin();
while( It != Listeners.end() )
{
StencilInputEventListener *pL = *It;
if ( pL->GetIndex() == MouseMovedListeningIndex )
{
( *It )->MouseButtonClick( E );
}
++It;
}
};
void StencilInputEventDispatcher::KeyDown( KeyboardEventKeyDown &E )
{
EventListenerCollector< StencilInputEventListener >::ListenerList::iterator It = Listeners.begin();
while( It != Listeners.end() )
{
StencilInputEventListener *pL = *It;
if ( pL->GetIndex() == MouseMovedListeningIndex )
{
( *It )->KeyDown( E );
}
++It;
}
}
void StencilInputEventDispatcher::KeyUp( KeyboardEventKeyUp &E )
{
EventListenerCollector< StencilInputEventListener >::ListenerList::iterator It = Listeners.begin();
while( It != Listeners.end() )
{
StencilInputEventListener *pL = *It;
if ( pL->GetIndex() == MouseMovedListeningIndex )
{
( *It )->KeyUp( E );
}
++It;
}
}
Mając tak napisany obiekt, wystarczy go podpiąć do instancji InputManager (a tą polecam przechowywać jako np. singleton).
Wspomnę jeszcze o tym, że jak chcemy zmieniać aktywny obiekt za pomocą klawiatury np. poprzez TAB/Shift+TAB to trzeba odrobinę zmodyfikować implementację StencilInputEventDispatcher, ale to już zostawiam wytrwałym którzy tu dobrneli i jeszcze mają ochotę na wykorzystanie fragmentów mojej implementacji :D.
Przejdźmy do sedna, czyli jak zaimplementować kontrolkę reagującą na zdarzenia? Nic prostrzego, jedyne wymagania od takiej klasy to dziedziczenie po StencilInputEventListener aby móc ją zarejestrować w dispatcherze oraz to żeby się go posłuchała i renderowała się do stencila z podanym przez niego indeksem
prawie pseudo kod:
{
public:
…
int GetIndex()
{
return Index;
}
void SetIndex( int iIndex )
{
Index = iIndex;
}
…
virtual void Render( IRenderer *Renderer )
{
Renderer->SetStencilOp( IRenderer::STENCIL_MODE_ALWAYSPASS, Index );
Renderer->RenderMeshData( MeshData );
}
Mesh *MeshData;
};
IRenderer to jak sama nazwa wskazuje interfejs poprzez ktory dostajemy sie do renderera, tu zamiast niego wystarczy wykorzystać swój własny, lub bezpośrednio się odwołać do funkcji danego API. SceneNode to używana przeze mnie klasa pomocnicza implementująca większośc funkcji węzła w grafie sceny. Dla prostych obiektów wystarczy przeciążyć metodę Render.
Dalej to już zabawa z obsługą zdarzeń. Przycisk jak to przyciski powinny mieć w swoim zwyczaju powinien przeciążyć zdarzenie MouseButtonDown i odpowiednio na nie zareagować. Natomiast bardziej złożone kontrolki mogą zawierać kontrolki podrzędne implementujące interfejs słuchacza dispatchera. Pozostawiam to już Wam.
Podsumowanie
Opisałem przykład implementacji przekazywania zdarzeń do kontrolek 3D, który mi osobiście narazie wystarcza, jeżeli ktoś ma jakieś pytania i/lub pomysły na dalszy rozwój to czekam na komentarze.
