diff --git a/bin/test3d.zip b/bin/test3d.zip index 3ae734e..019917a 100644 Binary files a/bin/test3d.zip and b/bin/test3d.zip differ diff --git a/configure b/configure old mode 100644 new mode 100755 diff --git a/notes.txt b/notes.txt index d3a55e3..d6ebd9a 100644 --- a/notes.txt +++ b/notes.txt @@ -1,4 +1,4 @@ -Game templates version 1.1.1 +Game templates version 1.2.0 [test3d] diff --git a/src/client/gui.cpp b/src/client/gui.cpp index c841b5a..07bf013 100644 --- a/src/client/gui.cpp +++ b/src/client/gui.cpp @@ -60,21 +60,21 @@ void MenuObject::SetFocus (const bool b) focussed = b; } -Menu::Menu(Client *p) : pClient (p) +Menu::Menu (Client *p) : pClient (p) { focussed = NULL; inputEnabled = true; } -Menu::~Menu() +Menu::~Menu () { - for(std::list::iterator it = objects.begin(); it != objects.end(); it++) + for(std::list ::iterator it = objects.begin(); it != objects.end(); it++) { MenuObject *obj = *it; delete obj; } - objects.clear(); + objects.clear (); } -void Menu::DisableInput() +void Menu::DisableInput () { /* if(focussed) @@ -85,12 +85,14 @@ void Menu::DisableInput() */ inputEnabled = false; } -void Menu::EnableInput() +void Menu::EnableInput () { inputEnabled = true; } -void Menu::DisableObjectFocus(MenuObject* obj) +void Menu::DisableObjectFocus (MenuObject* obj) { + // disables the underlying menu object + for(std::list::iterator it = objects.begin(); it != objects.end(); it++) { MenuObject *o = *it; @@ -102,8 +104,10 @@ void Menu::DisableObjectFocus(MenuObject* obj) } } } -void Menu::EnableObjectFocus(MenuObject* obj) +void Menu::EnableObjectFocus (MenuObject* obj) { + // enables the underlying menu object and cancels focus + for(std::list::iterator it = objects.begin(); it != objects.end(); it++) { MenuObject *o = *it; @@ -120,16 +124,16 @@ void Menu::EnableObjectFocus(MenuObject* obj) } } } -void Menu::AddMenuObject(MenuObject* o) +void Menu::AddMenuObject (MenuObject* o) { o->menu = this; objects.push_back (o); } -void Menu::FocusMenuObject(MenuObject* obj) +void Menu::FocusMenuObject (MenuObject* obj) { if (obj != focussed) { - // uset the old + // unset the old if (focussed) { focussed->SetFocus (false); @@ -160,6 +164,8 @@ void Menu::OnKeyPress (const SDL_KeyboardEvent *event) case SDLK_TAB: if (focussed) { + // tab key is pressed, switch to next menu object + MenuObject* next = focussed->NextInLine(); if (next) FocusMenuObject (next); @@ -173,7 +179,11 @@ void Menu::OnMouseClick (const SDL_MouseButtonEvent *event) if(!inputEnabled) return; - // The last object Rendered, is the focussed object + /* + When multiple objects are hit by the mouse cursor, + the last one in the list gets the focus. Because that + object is rendered on top. + */ MenuObject* mouseOverObj=NULL; for (std::list::iterator it = objects.begin(); it != objects.end(); it++) { @@ -198,7 +208,10 @@ void Menu::OnMouseWheel (const SDL_MouseWheelEvent *event) int mX, mY; SDL_GetMouseState (&mX, &mY); - // Mouse wheel events are sent through to anything under the mouse + /* + Mouse wheel events are sent through to anything under the mouse. + (also if not focussed) They might be scroll events. + */ for (std::list::iterator it = objects.begin(); it != objects.end(); it++) { MenuObject *pObj = *it; @@ -211,6 +224,8 @@ void Menu::OnMouseWheel (const SDL_MouseWheelEvent *event) } void Menu::Update(const float dt) { + // All menu objects get updated, not just the focussed one + for (std::list::iterator it = objects.begin(); it != objects.end(); it++) { MenuObject *pObj = *it; @@ -223,7 +238,10 @@ void Menu::Render() int mX, mY; SDL_GetMouseState (&mX, &mY); - // Determine over which object the mouse rolls: + /* + Determine over which object the mouse rolls, focussed has priority. + The chosen object gets to render the cursor. + */ MenuObject* mouseOverObj = NULL; if (focussed && focussed->MouseOver ((GLfloat) mX, (GLfloat) mY)) { @@ -244,7 +262,7 @@ void Menu::Render() int w, h; SDL_GL_GetDrawableSize (pClient->GetMainWindow (), &w, &h); - // Rendering the mouse cursor + // Render the mouse cursor, if in the window. if (mX > 0 && mX < w && mY > 0 && mY < h) { if (mouseOverObj && inputEnabled && mouseOverObj->enabled) @@ -272,11 +290,12 @@ TextInputBox::TextInputBox(const GLfloat x, const GLfloat y, this->x = x; this->y = y; - textMask=mask; - if(textMask) + textMask = mask; + if (textMask) { - showText=new char[maxTextLength+1]; - showText[0]=NULL; + // only allocate this string when a text mask is used + showText = new char [maxTextLength+1]; + showText [0] = NULL; } else showText=NULL; @@ -286,7 +305,7 @@ TextInputBox::TextInputBox(const GLfloat x, const GLfloat y, } void TextInputBox::UpdateShowText() { - if(textMask) + if (textMask) // only used when masking { int i; for(i=0; text[i]; i++) @@ -296,8 +315,8 @@ void TextInputBox::UpdateShowText() } TextInputBox::~TextInputBox() { - delete[] showText; - delete[] text; + delete [] showText; + delete [] text; } int TextInputBox::GetCursorPos() const { @@ -305,9 +324,9 @@ int TextInputBox::GetCursorPos() const } void TextInputBox::GetCursorCoords(GLfloat& cx, GLfloat& cy) const { - if(pFont) + if (pFont) { - char* t = textMask ? showText : text; + char *t = textMask ? showText : text; CoordsOfGlyph (pFont, t, cursorPos, cx, cy, textAlign); cx += x; cy += y; @@ -329,48 +348,63 @@ const char* TextInputBox::GetText() const } void TextInputBox::CopySelectedText() const { - int start = (cursorPosfixedCursorPos) ? cursorPos : fixedCursorPos; + // Determine where selection starts and where it ends: + int start = (cursorPos < fixedCursorPos) ? cursorPos : fixedCursorPos, + end = (cursorPos > fixedCursorPos) ? cursorPos : fixedCursorPos; + + // There might be no selection: if (start >= end) return; + // If a text mask was used, the masked text is copied: std::string ctxt; if (textMask) ctxt = std::string (showText + start, end - start); else ctxt = std::string (text + start, end - start); + // SDL must fill the clipboard for us SDL_SetClipboardText (ctxt.c_str()); } void TextInputBox::PasteText() { - int start = (cursorPos < fixedCursorPos)? cursorPos : fixedCursorPos, - end = (cursorPos > fixedCursorPos)? cursorPos : fixedCursorPos, + // Determine where selection starts and where it ends: + + int start = (cursorPos < fixedCursorPos) ? cursorPos : fixedCursorPos, + end = (cursorPos > fixedCursorPos) ? cursorPos : fixedCursorPos, n, i, space, charsPasted; - if(end > start) // Some text was selected + if(end > start) // Some text was selected, clear it first { ClearText (start, end); } - n = strlen(text); + /* + Inserted text might exceed the max length. + We therefore only insert the text that still fits in. + */ + n = strlen (text); space = maxTextLength - n; // space remaining for fill - // move everything maximally to the right: + // move the right side of the cursor maximally to the right: // (including null) for(i=n+1; i>=start; i--) { - text[i + space] = text[i]; + text [i + space] = text[i]; } + // Let sdl fetch the clipboard contents for us, then insert it: strncpy (text + start, SDL_GetClipboardText (), space); charsPasted = std::min ((int)strlen (SDL_GetClipboardText ()), space); + // move the cursor after the inserted part: cursorPos = fixedCursorPos = start + charsPasted; + // Move the right half of the original text back to the left, + // to follow after the inserted text. if (charsPasted < space) { int holeSize = space - charsPasted; @@ -384,28 +418,35 @@ void TextInputBox::PasteText() } } + // update the masked text with the new state UpdateShowText(); } void TextInputBox::RenderText() const { char* t=text; - if(textMask) + if (textMask) { - t=showText; + t = showText; } + // glRenderText renders at the origin, so translate to x, y. + glTranslatef (x, y, 0.0f); glRenderText (pFont, t, textAlign); glTranslatef (-x, -y, 0.0f); } void TextInputBox::RenderTextCursor() const { + /* + The text cursor has a fluctuating alpha value. + This alpha value is determined from the cursor_time field in the object. + */ + glPushAttrib (GL_TEXTURE_BIT | GL_CURRENT_BIT); + glDisable (GL_TEXTURE_2D); GLfloat alpha = 0.6f + 0.3f * sin (10 * cursor_time); - glDisable (GL_TEXTURE_2D); - GLfloat clr [4]; glGetFloatv (GL_CURRENT_COLOR, clr); @@ -414,8 +455,8 @@ void TextInputBox::RenderTextCursor() const int selectionStart = std::min (cursorPos, fixedCursorPos), selectionEnd = std::max (cursorPos, fixedCursorPos); - if (selectionStart == selectionEnd) - // no text selected, just render a single square at the cursor position + if (selectionStart >= selectionEnd) + // no text selected, just render a narrow rectangle at the cursor position { GLfloat h1,h2,cx,cy,cx2; @@ -425,13 +466,13 @@ void TextInputBox::RenderTextCursor() const h2 = h1 + GetLineSpacing (pFont); glBegin(GL_QUADS); - glVertex2f(cx - 2.0f, cy + h1); - glVertex2f(cx + 2.0f, cy + h1); - glVertex2f(cx + 2.0f, cy + h2); - glVertex2f(cx - 2.0f, cy + h2); + glVertex2f (cx - 2.0f, cy + h1); + glVertex2f (cx + 2.0f, cy + h1); + glVertex2f (cx + 2.0f, cy + h2); + glVertex2f (cx - 2.0f, cy + h2); glEnd(); } - else + else // render a rectangle over the selected text { char* t=text; if(textMask) @@ -462,9 +503,11 @@ void TextInputBox::OnFocusLose() fixedCursorPos = cursorPos = strlen (t); } #define TEXTSELECT_XMARGE 15.0f -bool TextInputBox::MouseOver(GLfloat mX, GLfloat mY) const +bool TextInputBox::MouseOver (GLfloat mX, GLfloat mY) const { - char* t=textMask?showText:text; + // See if the cursor hits the text: + + char* t = textMask ? showText : text; int cpos = WhichGlyphAt (pFont, t, (GLfloat)(mX - x),(GLfloat)(mY - y), textAlign), n = strlen(t); @@ -479,6 +522,12 @@ bool TextInputBox::MouseOver(GLfloat mX, GLfloat mY) const CoordsOfGlyph (pFont, t, n, cx2, cy2, textAlign); cx2 += x; cy2 += y; + /* + When mX, mY is located left or right from the text, + WhichGlyphAt returns the index of the leftmost or rightmost character, respectively. + If it's very close to the text, we will consider it a hit, else not. + */ + if (mX > (cx1 - TEXTSELECT_XMARGE) && mX < (cx2 + TEXTSELECT_XMARGE)) { return true; @@ -486,140 +535,174 @@ bool TextInputBox::MouseOver(GLfloat mX, GLfloat mY) const } return false; } -void TextInputBox::OnDelChar(char){} -void TextInputBox::OnAddChar(char){} -void TextInputBox::OnBlock(){} -void TextInputBox::OnMoveCursor(int direction){} -void TextInputBox::ClearText(int start, int end) +void TextInputBox::OnDelChar (char) {} +void TextInputBox::OnAddChar (char) {} +void TextInputBox::OnBlock () {} +void TextInputBox::OnMoveCursor (int direction) {} +void TextInputBox::ClearText (int start, int end) { - int n = strlen(text),i,d; - if(end>n) end=n; + // clamp start to the string's fist character. + // clamp end to the string's last character. + int n = strlen (text), i, d; + + if (start < 0) + start = 0; + + if (end > n) + end = n; - d=end-start; - if(d<0) return; + d = end - start; + if (d < 0) + return; // invalid function arguments - for(i=start; i<(n-d+1); i++) + // Move characters to the left: + for(i = start; i < (n - d + 1); i++) { - text[i]=text[i+d]; + text [i] = text [i + d]; } + + // If the text changes, then the masked text should also change: UpdateShowText(); } void TextInputBox::BackspaceProc() { int n = strlen(text); - if(n>0) + if (n > 0) { - if(fixedCursorPos>cursorPos) + /* + Either remove the selected text if any, + or the first character before the cursor. + */ + if (fixedCursorPos > cursorPos) { - ClearText(cursorPos,fixedCursorPos); - fixedCursorPos=cursorPos; + ClearText (cursorPos, fixedCursorPos); + fixedCursorPos = cursorPos; } - else if(fixedCursorPos0) + else if (cursorPos > 0) { - OnDelChar(text[cursorPos-1]); + // Remove a single character: + + OnDelChar (text [cursorPos - 1]); - ClearText(cursorPos-1,cursorPos); + ClearText (cursorPos - 1, cursorPos); cursorPos--; - fixedCursorPos=cursorPos; + fixedCursorPos = cursorPos; } - else OnBlock(); + else OnBlock (); // cannot go further } - else OnBlock(); + else OnBlock (); // cannot go further - UpdateShowText(); + // If the text changes, then the masked text should also change: + UpdateShowText (); } void TextInputBox::DelProc() { - int n = strlen(text); - if(n>0) + int n = strlen (text); + if (n > 0) { - if(fixedCursorPos>cursorPos) + /* + Either remove the selected text if any, + or the first character after the cursor. + */ + + if (fixedCursorPos > cursorPos) { - ClearText(cursorPos,fixedCursorPos); - fixedCursorPos=cursorPos; + ClearText (cursorPos, fixedCursorPos); + fixedCursorPos = cursorPos; } - else if(fixedCursorPosn) + + if (cursorPos > n) { - cursorPos=n; - OnBlock(); + // cannot go further + + cursorPos = n; + OnBlock (); } else - OnMoveCursor(1); + OnMoveCursor (1); + // if shift is down, keep fixedCursorPos the same, else move it along. if(!shift) - fixedCursorPos=cursorPos; + fixedCursorPos = cursorPos; - UpdateShowText(); + // If the text changes, then the masked text should also change: + UpdateShowText (); } void TextInputBox::AddCharProc(char c) { - // clear the selection first - if(fixedCursorPos > cursorPos) + // clear the selection first, if any + if (fixedCursorPos > cursorPos) { ClearText (cursorPos,fixedCursorPos); fixedCursorPos = cursorPos; } - else if(fixedCursorPos < cursorPos) + else if (fixedCursorPos < cursorPos) { ClearText (fixedCursorPos,cursorPos); cursorPos = fixedCursorPos; } - int n = strlen(text), i; + int n = strlen (text), i; if (n < maxTextLength) // room for more? { @@ -630,6 +713,7 @@ void TextInputBox::AddCharProc(char c) } text[cursorPos] = c; + // move the cursor to the right, also cursorPos++; fixedCursorPos = cursorPos; @@ -637,51 +721,59 @@ void TextInputBox::AddCharProc(char c) } else { + // Cannot add more characters + OnBlock(); } + // If the text changes, then the masked text should also change: UpdateShowText(); } #define TEXT_BUTTON_INTERVAL 0.03f #define FIRST_TEXT_BUTTON_INTERVAL 0.3f void TextInputBox::Update(const float dt) { - if(IsFocussed()) + if (IsFocussed ()) { - cursor_time += dt; + cursor_time += dt; // for cursor blinking - if (button_time<0) + if (button_time < 0) button_time += dt; - const Uint8 *keystate = SDL_GetKeyboardState(NULL); + const Uint8 *keystate = SDL_GetKeyboardState (NULL); if (button_time >= 0) { - if ( keystate[SDL_SCANCODE_BACKSPACE] ) + /* + If a button is held down for a certain amount of time + it must be repeated. + */ + + if (keystate [SDL_SCANCODE_BACKSPACE] ) { - BackspaceProc(); - button_time=-TEXT_BUTTON_INTERVAL; + BackspaceProc (); + button_time = -TEXT_BUTTON_INTERVAL; } - else if( keystate[SDL_SCANCODE_DELETE] ) + else if (keystate [SDL_SCANCODE_DELETE] ) { - DelProc(); - button_time=-TEXT_BUTTON_INTERVAL; + DelProc (); + button_time = -TEXT_BUTTON_INTERVAL; } - else if( keystate[SDL_SCANCODE_LEFT] ) + else if (keystate[SDL_SCANCODE_LEFT] ) { - LeftProc(); - button_time=-TEXT_BUTTON_INTERVAL; + LeftProc (); + button_time = -TEXT_BUTTON_INTERVAL; } - else if( keystate[SDL_SCANCODE_RIGHT] ) + else if (keystate [SDL_SCANCODE_RIGHT] ) { - RightProc(); - button_time=-TEXT_BUTTON_INTERVAL; + RightProc (); + button_time = -TEXT_BUTTON_INTERVAL; } if (keystate [lastCharKey]) { - AddCharProc(lastCharKeyChar); - button_time=-TEXT_BUTTON_INTERVAL; + AddCharProc (lastCharKeyChar); + button_time = -TEXT_BUTTON_INTERVAL; } else { @@ -690,20 +782,26 @@ void TextInputBox::Update(const float dt) } } - UpdateShowText(); + // If the text changes, then the masked text should also change: + UpdateShowText (); } } -void TextInputBox::OnMouseMove(const SDL_MouseMotionEvent *event) +void TextInputBox::OnMouseMove (const SDL_MouseMotionEvent *event) { if (event->state & SDL_BUTTON_LMASK) { + /* + Mouse key is pressed down while moving the cursor. + This means text can be selected. Determine which + glyph is under the cursor: + */ + char* t = text; if (textMask) { t=showText; } int cpos = WhichGlyphAt (pFont, t, (GLfloat)(event->x - x), (GLfloat)(event->y - y), textAlign); -// int cpos = font->IndexCursorPos (x, y, t, textAlign, (GLfloat)event->x, (GLfloat)event->y); if (cpos >= 0) { @@ -719,6 +817,7 @@ void TextInputBox::OnMouseMove(const SDL_MouseMotionEvent *event) CoordsOfGlyph (pFont, t, cpos, leftBound, textY, textAlign); CoordsOfGlyph (pFont, t, cpos + 1, rightBound, textY, textAlign); + // make leftBound and rightBound absolute: leftBound += x; rightBound += x; @@ -737,25 +836,33 @@ void TextInputBox::OnMouseMove(const SDL_MouseMotionEvent *event) } void TextInputBox::OnMouseClick (const SDL_MouseButtonEvent *event) { - char* t = text; + /* + See if a glyph was hit by the click. If so, + move the cursor and perhaps the selection. + */ + + char *t = text; if (textMask) { t = showText; } - int cpos, prev_fixedCursorPos = fixedCursorPos; + int cpos, + prev_fixedCursorPos = fixedCursorPos; - const Uint8 *keystate = SDL_GetKeyboardState(NULL); + const Uint8 *keystate = SDL_GetKeyboardState (NULL); cpos = WhichGlyphAt (pFont, t, event->x - x, event->y - y, textAlign); if (cpos >= 0) { - if (event->clicks > 1) + if (event->clicks > 1) // double click, select entire word + WordAt (t, cpos, fixedCursorPos, cursorPos); else // place the cursor at the left side of the selected character fixedCursorPos = cursorPos = cpos; - if(keystate[SDL_SCANCODE_LSHIFT] || keystate[SDL_SCANCODE_RSHIFT]) + // If shift is down, the selection start should stay where it was. + if (keystate [SDL_SCANCODE_LSHIFT] || keystate [SDL_SCANCODE_RSHIFT]) fixedCursorPos = prev_fixedCursorPos; } } @@ -767,17 +874,27 @@ void TextInputBox::OnKeyPress (const SDL_KeyboardEvent *event) bool bCTRL = mod & KMOD_CTRL; + /* + Take the approriate action, according to + key pressed. + */ + if (sym == SDLK_TAB || sym == SDLK_RETURN) { - // ignore '\t' and '\n' + // This input box only works for single lined text, + // ignore '\t' and '\n'. return; } else if (bCTRL && sym == SDLK_c) { + // CTRL + C + CopySelectedText(); } else if (bCTRL && sym == SDLK_v) { + // CTRL + V + PasteText(); } else if (sym == SDLK_BACKSPACE) @@ -815,6 +932,7 @@ void TextInputBox::OnKeyPress (const SDL_KeyboardEvent *event) return; } + // When the text changes, the masked text should also change: UpdateShowText(); } void TextInputBox::Render () @@ -837,18 +955,21 @@ void TextInputBox::Render () RenderText(); - if (IsFocussed()) + if (IsFocussed ()) RenderTextCursor(); glPopAttrib(); } void TextInputBox::SetText (const char* text) { + // copy text strncpy (this->text, text, maxTextLength - 1); this->text [maxTextLength] = NULL; - int n = strlen(this->text); + // set cursor position to the rightmost end: + int n = strlen (this->text); cursorPos = fixedCursorPos = n; + // When the text changes, the masked text should also change: UpdateShowText (); } diff --git a/src/client/gui.h b/src/client/gui.h index 225e6c1..d88813a 100644 --- a/src/client/gui.h +++ b/src/client/gui.h @@ -29,36 +29,48 @@ class Menu; -/* +/** * Base class for a GUI menu item. */ class MenuObject : public EventListener { private: - // automatically set by 'Menu': + + /* + The menu variable is automatically set by the menu itself when + this object is added to it. However, it's null when the menu object + is not attached to any menu. + */ Menu* menu; - bool focussed, enabled; + + bool focussed, // means this is the current menu object that recieves input from the menu. + enabled; // when disabled, it doesn't recieve mouse input. + protected: + // Override this if the cursor should look different when hovering over this object. virtual void RenderCursor (const int mX, const int mY); Menu *GetMenu () const { return menu; } public: + // These functions are usefull when using a menu object without a menu. void SetEnabled (const bool b) { enabled = b; } void SetFocus (const bool); bool IsFocussed () const {return focussed;} bool IsInputEnabled () const; + // Must implement this, decides whether mouse cursor hovers over or not. virtual bool MouseOver (const GLfloat mouse_x, const GLfloat mouse_y) const = 0; + // Override these to take special actions when the object gains or loses menu focus. virtual void OnFocusGain () {} virtual void OnFocusLose () {} virtual void Render () {} virtual void Update (const float dt) {} - // used when tab is pressed + // used when tab is pressed, returns NULL by default virtual MenuObject* NextInLine() const; MenuObject(); @@ -67,16 +79,33 @@ friend class Menu; }; -/* - * A Graphical menu, that handles events and rendering for the underlying menu objects. +/** + * This class represents a graphical menu, that handles events and rendering for the + * underlying menu objects. + * + * It's not necessary for menu objects. A menu is only usefull when there are multiple + * menu objects, that shouldn't all recieve input at the same time. When used, this object + * decides which menu object has input focus. + * + * When the menu objects should all recieve input at the same time, don't use this class. + * + * A menu object: + * - recieves focus from mouse click or tab key press + * - knows itself when it's focussed or not + * - has the ability to decide what the mouse cursor looks like. */ class Menu : public EventListener { private: Client *pClient; + // All objects in this menu: std::list objects; + + // currently focussed object: MenuObject* focussed; + + bool inputEnabled; public: @@ -97,7 +126,7 @@ class Menu : public EventListener void EnableObjectFocus (MenuObject* obj); MenuObject* FocussedObject () { return focussed; } - void FocusMenuObject (MenuObject *); // may be NULL + void FocusMenuObject (MenuObject *); // may be NULL to focus nothing virtual void OnEvent (const SDL_Event *); @@ -105,71 +134,101 @@ class Menu : public EventListener virtual void OnKeyPress (const SDL_KeyboardEvent *event); virtual void OnMouseWheel (const SDL_MouseWheelEvent *event); - void AddMenuObject (MenuObject *); // is autodeleted when the menu is deleted + /* + Any object added with AddMenuObject is automatically deleted when + the menu gets deleted. + */ + void AddMenuObject (MenuObject *); virtual void Render(); virtual void Update(const float dt); }; -/* +/** * This is where users can insert text. The text can be hidden (passwords) by setting a mask char. * Text in the input box is selectable by mouse and SHIFT + arrow keys and can be copied to the clipboard with Ctrl + C. * It depends on a font to know its dimensions. + * + * This has not been tested with multiline input text. + * The up and down keys haven't been implemented for this class. */ class TextInputBox : public MenuObject { private: - char*text, - *showText, - textMask; + + char *text, // actual inputted text + *showText, // only used when text is masked + + textMask; // mask char, NULL means no mask + int maxTextLength, + + /* + cursorPos is the current cursor position, + fixedCursorPos is where selection started + when either shift or the left mouse key was down. + */ cursorPos, fixedCursorPos; + + /* + The font is necessary, + it determines the coordinates of the characters and is needed for rendering. + + x, y and textAlign are just as important for this! + */ const Font *pFont; - float button_time, cursor_time; + GLfloat x, y; // render position of the text + int textAlign; // one of the defined TEXTALIGN constants + + // Variables used to measure time passed since certain events: + float button_time, + cursor_time; + + // Variables that remember which char key was last pressed: int lastCharKey; char lastCharKeyChar; - GLfloat x,y; - int textAlign; - + // This changes the masked text, must be called when input text changes. void UpdateShowText(); + + // Removes the characters at given positions from the text string. void ClearText(const int start, const int end); + /* + The following routines are called whenever a key press + changes something to the state of the input box. + */ void AddCharProc(char c); void BackspaceProc(); void DelProc(); - void LeftProc(); - void RightProc(); + void LeftProc(); // when left key is pressed + void RightProc(); // when right key is pressed + // Render subroutines: void RenderText() const; - void RenderTextCursor() const; + // Used for copy and paste actions: void CopySelectedText() const; void PasteText(); protected: - void SetX (GLfloat _x) { x = _x; } - void SetY (GLfloat _y) { y = _y; } - - GLfloat GetX() const { return x; } - GLfloat GetY() const { return y; } - const Font *GetFont() const { return pFont; } - - int GetCursorPos() const; - void GetCursorCoords(GLfloat& cx, GLfloat& cy) const; - void GetFixedCursorCoords(GLfloat& cx, GLfloat& cy) const; + /* + OnDelChar, OnAddChar, OnMoveCursor and OnBlock + do nothing by default, but they are called when the corresponding change occurs. + They can be overridden. + */ + virtual void OnDelChar (char); + virtual void OnAddChar (char); + virtual void OnMoveCursor (int direction); // -1: left, 1: right + virtual void OnBlock (); // user tries to move the cursor, but can't go any further - // extra effects when something happens: - virtual void OnDelChar(char); - virtual void OnAddChar(char); - virtual void OnMoveCursor(int direction); // -1: left, 1: right - virtual void OnBlock(); - virtual void OnFocusGain(); - virtual void OnFocusLose(); + virtual void OnFocusGain (); + virtual void OnFocusLose (); + // The input box needs to respond to the following input events: void OnMouseClick (const SDL_MouseButtonEvent *event); void OnMouseMove (const SDL_MouseMotionEvent *event); void OnKeyPress (const SDL_KeyboardEvent *event); @@ -181,15 +240,37 @@ class TextInputBox : public MenuObject const int textAlign, const char mask=NULL); - virtual ~TextInputBox(); + virtual ~TextInputBox (); + // menu object methods, that the tex box implements: virtual bool MouseOver (GLfloat mX, GLfloat mY) const; + + /* + Update and Render can be overridden, but MUST be called + in their derived classes for them to work properly. + */ virtual void Render (); virtual void Update (const float dt); + + // Getters and setters: void SetText (const char* text); const char* GetText () const; + void SetX (GLfloat _x) { x = _x; } + void SetY (GLfloat _y) { y = _y; } + + GLfloat GetX() const { return x; } + GLfloat GetY() const { return y; } + + const Font *GetFont () const { return pFont; } + + int GetCursorPos () const; + + // These two functions get the absolute coordinates of the cursor in the text: + void GetCursorCoords (GLfloat& cx, GLfloat& cy) const; + void GetFixedCursorCoords (GLfloat& cx, GLfloat& cy) const; + friend class Menu; }; #endif //GUI_H diff --git a/src/client/textscroll.cpp b/src/client/textscroll.cpp index 5f616ce..2046b0a 100644 --- a/src/client/textscroll.cpp +++ b/src/client/textscroll.cpp @@ -54,6 +54,8 @@ TextScroll::TextScroll (const float _frame_left_x, const float _top_y, } void TextScroll::ClampBar () { + // bar must stay withing min and max + if (bar_top_y < min_bar_y) bar_top_y = min_bar_y; if (bar_top_y > max_bar_y) @@ -67,6 +69,8 @@ void TextScroll::SetText (const char *_text) { if (pFont->glyphs.size () <= 0) { + // A font with no glyphs has probably not been loaded yet. + fprintf (stderr, "TextScroll::SetText: error, font isn\'t ready yet\n"); return; } @@ -78,13 +82,22 @@ void TextScroll::SetText (const char *_text) } void TextScroll::DeriveDimensions () { - float x1, x2, y1, y2, bar_frame_ratio, bar_domain, + float x1, x2, y1, y2, + + bar_frame_ratio, + bar_domain, text_width = frame_width - 2 * spacing_text_frame, text_window_height = height - 2 * spacing_text_frame; + /* + Now that we know the width of the text area, + find out how much height the text will need + */ + DimensionsOfText (pFont, text.c_str(), x1, y1, x2, y2, textAlign, text_width); + // Convert to absolute coords: x1 += frame_left_x; x2 += frame_left_x; @@ -93,7 +106,7 @@ void TextScroll::DeriveDimensions () scrolled_area_height = y2 - y1; - // bar_domain: the entire area that the bar can possibly occupy + // bar_domain: the entire area that the bar can maximally occupy bar_domain = height - 2 * spacing_strip_bar; // bar_frame_ratio: 1.0 is max, determines how big the bar will be @@ -103,9 +116,13 @@ void TextScroll::DeriveDimensions () bar_height = bar_domain * bar_frame_ratio; - // determine which y positions the bar can have: + /* + Determine which y positions the bar can have. + It can move down until its bottom hits the strip's bottom. + */ max_bar_y = min_bar_y + height - 2 * spacing_strip_bar - bar_height; + // Place bar within boundaries: ClampBar (); } TextScroll::~TextScroll() @@ -127,8 +144,11 @@ void TextScroll::OnMouseClick (const SDL_MouseButtonEvent *event) float offY = GetTextYOffset(), x1, y1, x2, y2; - int cpos, prev_selectionStart = selectionStart; + int cpos, + prev_selectionStart = selectionStart; + + // Remember, WhichGlyphAt takes relative coords !! GetTextRect (x1, y1, x2, y2); cpos = WhichGlyphAt (pFont, text.c_str(), event->x - x1, event->y - (y1 + offY), textAlign, x2 - x1); @@ -171,6 +191,7 @@ void TextScroll::OnMouseClick (const SDL_MouseButtonEvent *event) OnScroll (DY); } + // When the bar changes position, make sure it stays within boundaries: ClampBar (); } void TextScroll::OnMouseMove (const SDL_MouseMotionEvent *event) @@ -195,7 +216,7 @@ void TextScroll::OnMouseMove (const SDL_MouseMotionEvent *event) { // text is being selected, include the new mouse position in the selection. - // if the mouse cursor approaches the bottom or to of the frame while dragging, then scroll a bit: + // if the mouse cursor approaches the bottom or top of the frame while dragging, then scroll a bit: float charH = GetLineSpacing (pFont), DY = 0.05f * (max_bar_y - min_bar_y), @@ -205,23 +226,34 @@ void TextScroll::OnMouseMove (const SDL_MouseMotionEvent *event) GetTextRect (x1, y1, x2, y2); - if (event->y < (y1 + charH)) + if (event->y < (y1 + charH)) // mouse over top of frame { bar_top_y -= DY; OnScroll (-DY); } - else if (event->y > (y2 - charH)) + else if (event->y > (y2 - charH)) // mouse over bottom { bar_top_y += DY; OnScroll (DY); } + + // If the bar moved, it may not go outside boundaries ClampBar(); offY = GetTextYOffset(); - int cpos = WhichGlyphAt (pFont, text.c_str(), event->x - x1, event->y - (y1 + offY), textAlign, x2 - x1); + // Remember, WhichGlyphAt takes relative coords! + + int cpos = WhichGlyphAt (pFont, text.c_str(), + event->x - x1, event->y - (y1 + offY), + textAlign, x2 - x1); if (cpos >= 0) { + /* + The movement direction of the mouse decides + whether to include the glyph under the cursor. + */ + if (cpos < text.size() && event->xrel > 0) selectionEnd = cpos + 1; else @@ -246,13 +278,15 @@ void TextScroll::OnMouseWheel (const SDL_MouseWheelEvent *event) wheel_delta = -0.2f * GetLineSpacing (pFont); bar_top_y += wheel_delta * event->y; + + // If the bar moved, it may not go outside boundaries ClampBar (); OnScroll (bar_top_y - prev_bar_y); if (dragging_text && MouseOverText (mX, mY)) { - // user is still selecting text, move selecton to new cursor position + // user is still selecting text, move selection to new cursor position float y1, x1, y2, x2, @@ -260,9 +294,13 @@ void TextScroll::OnMouseWheel (const SDL_MouseWheelEvent *event) GetTextRect (x1, y1, x2, y2); + // Remember, WhichGlyphAt takes relative coords! + int cpos = WhichGlyphAt (pFont, text.c_str(), float (mX) - x1, float (mY) - (y1 + offY), textAlign, x2 - x1); if (cpos >= 0) { + // Move the selection to current cursor position + selectionEnd = cpos; } } @@ -271,18 +309,27 @@ void TextScroll::SetScroll (const float _bar_top_y) { // Scroll was set by something other than the mouse + int mX, mY; + const bool lbutton = SDL_GetMouseState(&mX, &mY) & SDL_BUTTON(SDL_BUTTON_LEFT); + + // Releasing the mouse stops drag + if (!lbutton) + { + dragging_bar = false; + dragging_text = false; + } + float prev_bar_y = bar_top_y; bar_top_y = _bar_top_y; + + // If the bar moved, it may not go outside boundaries ClampBar (); OnScroll (bar_top_y - prev_bar_y); - int mX, mY; - SDL_GetMouseState(&mX, &mY); - if (dragging_text && MouseOverText (mX, mY)) { - // user is still selecting text, move selecton to new cursor position + // user is still selecting text, move selection to new cursor position float y1, x1, y2, x2, @@ -290,9 +337,13 @@ void TextScroll::SetScroll (const float _bar_top_y) GetTextRect (x1, y1, x2, y2); + // Remember, WhichGlyphAt takes relative coords! + int cpos = WhichGlyphAt (pFont, text.c_str(), float (mX) - x1, float (mY) - (y1 + offY), textAlign, x2 - x1); if (cpos >= 0) { + // Move the selection to current cursor position + selectionEnd = cpos; } } @@ -303,7 +354,7 @@ void TextScroll::OnKeyPress (const SDL_KeyboardEvent *event) const SDL_Scancode scn = event->keysym.scancode; const Uint16 mod = event->keysym.mod; - if (mod & KMOD_CTRL && sym == SDLK_c) + if (mod & KMOD_CTRL && sym == SDLK_c && text_selectable) { // Ctrl + C CopySelectedText(); @@ -320,16 +371,27 @@ GLfloat TextScroll::GetTextYOffset() const const float window_height = height - 2 * spacing_text_frame; + /* + When the bar is at max_bar_y, the bottom of + the text is visible. When the bar is at min_bar_y, + the top of the text is visible. + */ + return -(scrolled_area_height - window_height) * (bar_top_y - min_bar_y) / (max_bar_y - min_bar_y); } void TextScroll::GetTextRect (float &x1, float &y1, float &x2, float &y2) const { + /* + Return the area where text is visible. + It's simply the frame, shrunk by the spacing. + */ + x1 = frame_left_x + spacing_text_frame; x2 = frame_left_x + frame_width - spacing_text_frame; y1 = top_y + spacing_text_frame; y2 = top_y + height - spacing_text_frame; } -void TextScroll::GetFrameRect(float& x1, float& y1, float& x2, float& y2) const +void TextScroll::GetFrameRect (float& x1, float& y1, float& x2, float& y2) const { x1 = frame_left_x; x2 = x1 + frame_width; @@ -338,16 +400,16 @@ void TextScroll::GetFrameRect(float& x1, float& y1, float& x2, float& y2) const } void TextScroll::GetStripRect (float &x1, float &y1, float &x2, float &y2) const { - if (strip_width == 0) + if (strip_width == 0) // there can be no strip this way { x1 = 0; x2 = 0; } - else if (strip_width < 0.0f) + else if (strip_width < 0.0f) // strip is on left side { x2 = frame_left_x - spacing_frame_strip; x1 = x2 + strip_width; } - else if (strip_width > 0.0f) + else if (strip_width > 0.0f) // strip is on right side { x1 = frame_left_x + frame_width + spacing_frame_strip; x2 = x1 + strip_width; @@ -358,6 +420,8 @@ void TextScroll::GetStripRect (float &x1, float &y1, float &x2, float &y2) const } void TextScroll::GetBarRect(float& x1, float& y1, float& x2, float& y2) const { + // Bar is on strip. Strip determines x position: + GetStripRect (x1, y1, x2, y2); x1 += spacing_strip_bar; @@ -371,19 +435,25 @@ void TextScroll::CopySelectedText() const int start = std::min (selectionStart, selectionEnd), end = std::max (selectionStart, selectionEnd); - if (start >= end) + if (start >= end) // no selection return; + // Get selected substring: std::string ctxt = text.substr (start, end - start); + // let SDL fill up the clipboard for us SDL_SetClipboardText (ctxt.c_str()); } bool TextScroll::MouseOver(GLfloat mX, GLfloat mY) const { - return (MouseOverBar(mX, mY) || MouseOverFrame(mX, mY) || MouseOverStrip(mX, mY) != 0); + // Mouse must be over either of the three: + + return (MouseOverBar (mX, mY) || MouseOverFrame (mX, mY) || MouseOverStrip (mX, mY) != 0); } bool TextScroll::MouseOverText (float mX, float mY) const { + // Mouse must be over the area where text is visible: + float x1, x2, y1, y2; GetTextRect (x1, y1, x2, y2); @@ -391,6 +461,8 @@ bool TextScroll::MouseOverText (float mX, float mY) const } bool TextScroll::MouseOverBar(GLfloat mX, GLfloat mY) const { + // Mouse must be over bar rectangular area: + float barX1, barX2, barY1, barY2; GetBarRect (barX1, barY1, barX2, barY2); @@ -398,6 +470,11 @@ bool TextScroll::MouseOverBar(GLfloat mX, GLfloat mY) const } int TextScroll::MouseOverStrip(float mX, float mY) const { + /* + Mouse must be over strip rectangular area, + below, or above bar rectangular area. + */ + float stripX1, stripX2, stripY1, stripY2; GetStripRect (stripX1, stripY1, stripX2, stripY2); @@ -408,7 +485,7 @@ int TextScroll::MouseOverStrip(float mX, float mY) const { if (mY > stripY1 && mY < barY1) return -1; - else if(mY < stripY2 && mY > barY2) + else if (mY < stripY2 && mY > barY2) return 1; } @@ -416,6 +493,8 @@ int TextScroll::MouseOverStrip(float mX, float mY) const } bool TextScroll::MouseOverFrame (float mX, float mY) const { + // Mouse must be over frame rectangular area: + float x1, x2, y1, y2; GetFrameRect (x1, y1, x2, y2); @@ -426,7 +505,9 @@ void TextScroll::RenderText () float x, y, x2, y2; GetTextRect (x, y, x2, y2); - y += GetTextYOffset(); + y += GetTextYOffset(); // offset from scrolling + + // glRenderText starts at the origin, so we transform: glTranslatef (x, y, 0); glRenderText (pFont, text.c_str(), textAlign, x2 - x); @@ -437,11 +518,14 @@ void TextScroll::RenderTextSelection () float x, y, x2, y2; GetTextRect (x, y, x2, y2); - y += GetTextYOffset(); + y += GetTextYOffset(); // offset from scrolling + // Determine selection substring: int start = std::min (selectionStart, selectionEnd), end = std::max (selectionStart, selectionEnd); + // glRenderTextAsRects starts at the origin, so we transform: + glTranslatef (x, y, 0); glRenderTextAsRects (pFont, text.c_str(), start, end, textAlign, x2 - x); glTranslatef (-x, -y, 0); diff --git a/src/client/textscroll.h b/src/client/textscroll.h index 8fa173e..4161509 100644 --- a/src/client/textscroll.h +++ b/src/client/textscroll.h @@ -35,11 +35,18 @@ class TextScroll : public MenuObject { private: - std::string text; + + std::string text; // text to be shown + + /* + Font and alignment need to be set + for the scroll to know the text's + dimensions. + */ const Font *pFont; int textAlign; - bool text_selectable; + bool text_selectable; // if false, text is not selectable by mouse float frame_left_x, frame_width, @@ -49,33 +56,43 @@ class TextScroll : public MenuObject strip_width, // negative if on the left side of the frame, positive if on the right, zero if no barstrip - spacing_text_frame, - spacing_frame_strip, + spacing_text_frame, // spacing between text and frame bounding boxes + spacing_frame_strip, // 0 for the strip to be right next to the frame spacing_strip_bar, // can be negative if the bar is wider - last_mouseclick_x, last_mouseclick_y, last_mouseclick_bar_y, + // Variables to remember the state of the scroll at teh moment the mouse was clicked: + last_mouseclick_x, + last_mouseclick_y, + last_mouseclick_bar_y, bar_top_y, // changes when scrolling, determines text offset - // these are determined when text is set: + // These parameters are determined when text is set: + // (using DeriveDimensions function) min_bar_y, max_bar_y, bar_height, scrolled_area_height; + // These booleans describe what's happening at the moment: bool dragging_bar, dragging_text; + /* + These variables remember what part of the text + is selected. If selecting is enabled of coarse. + */ int selectionStart, selectionEnd; - void DeriveDimensions (); - void ClampBar (); + void DeriveDimensions (); // derives dimensions from newly set text + void ClampBar (); // keeps bar within frame - void CopySelectedText () const; + void CopySelectedText () const; // copies to clipboard protected: + // Some MenuObject functions that need to be overridden: void OnFocusLose (); void OnMouseClick (const SDL_MouseButtonEvent *event); void OnMouseMove (const SDL_MouseMotionEvent *event); @@ -83,15 +100,16 @@ class TextScroll : public MenuObject void OnMouseWheel (const SDL_MouseWheelEvent *event); /* - * These two subroutines can be used to render the text at the correct position. - * position of text is determined by scrolling and the frame's set position, - * when function 'Render' calls this, it's responsible for clipping, stencil buffering or alpha masking the text + These two subroutines can be used to render the text at the correct position. + The position of text is determined by scrolling and the frame's set position, + when a render function calls these subroutines, it'll be responsible for + clipping or alpha masking the text. */ void RenderText (); void RenderTextSelection (); // only needed when text is selectable - - virtual void OnScroll (float y) {}; + // Overridable, called when scroll state changes. + virtual void OnScroll (float bar_delta_y) {}; /* * false by default @@ -100,19 +118,19 @@ class TextScroll : public MenuObject public: - // These can be useful for rendering: + // These can be useful when rendering: void GetFrameRect (float &x1, float &y1, float &x2, float &y2) const; void GetStripRect (float &x1, float &y1, float &x2, float &y2) const; void GetBarRect (float &x1, float &y1, float &x2, float &y2) const; - void GetTextRect (float &x1, float &y1, float &x2, float &y2) const; + void GetTextRect (float &x1, float &y1, float &x2, float &y2) const; // area where text is visible float GetBarY () const { return bar_top_y; } float GetMinBarY () const { return min_bar_y; } float GetMaxBarY () const { return max_bar_y; } - float GetBarHeight() const; + float GetBarHeight () const; - float GetTextYOffset() const; - void SetScroll (const float bar_top_y); + float GetTextYOffset () const; // 0 when scrolled to the top + void SetScroll (const float bar_top_y); // in bar-strip space int MouseOverStrip (float mX, float mY) const; // -1 (yes, under bar), 0 (not on strip, or over bar), 1 (yes, above bar) bool MouseOverBar (float mX, float mY) const; diff --git a/src/err.cpp b/src/err.cpp index 0cc06d5..3b90b85 100644 --- a/src/err.cpp +++ b/src/err.cpp @@ -26,6 +26,7 @@ #define ERRSTR_LEN 512 +// String to store the error in: char error [ERRSTR_LEN] = ""; const char *GetError () @@ -37,11 +38,13 @@ void SetError (const char* format, ...) // use a temporary buffer, because 'error' might be one of the args char tmp [ERRSTR_LEN]; + // Insert args: va_list args; va_start (args, format); vsprintf (tmp, format, args); va_end (args); + // copy from temporary buffer: strcpy (error, tmp); } diff --git a/src/err.h b/src/err.h index 434a5c3..b9eb2c9 100644 --- a/src/err.h +++ b/src/err.h @@ -21,6 +21,12 @@ #ifndef ERR_H #define ERR_H +/* + Set the error string when something goes wrong, and make the + parser/converter/loader return false. The application can then + read out this error string and show in in a window/console. + */ + const char *GetError (); void SetError (const char* format, ...); diff --git a/src/font.cpp b/src/font.cpp index 8db326b..040d8d3 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -29,18 +29,22 @@ #include #include -const char *utf8_next(const char *pString, unicode_char *out) +/** + * Picks one utf8 character from the byte array and returns + * a pointer to the data after it. + */ +const char *utf8_next(const char *pBytes, unicode_char *out) { int take; - if (pString[0] >> 5 == 0x6) + if (pBytes [0] >> 5 == 0x6) { take = 2; } - else if (pString[0] >> 4 == 0xe) + else if (pBytes [0] >> 4 == 0xe) { take = 3; } - else if (pString[0] >> 3 == 0x1e) + else if (pBytes [0] >> 3 == 0x1e) { take = 4; } @@ -49,25 +53,32 @@ const char *utf8_next(const char *pString, unicode_char *out) take = 1; } + // store the bytes that make up the utf8 character *out = 0; for (int i = 0; i < take; i++) { - *out = (*out << 8) | pString[i]; + *out = (*out << 8) | pBytes [i]; } - return pString + take; -} -bool ParseHtmlUnicode(const char *repr, unicode_char *out) + return pBytes + take; // move to the next utf8 character +} +/** + * Parses a XML character reference from a string. + * (https://en.wikipedia.org/wiki/Numeric_character_reference) + * + * Only handles strings with one unicode character in it :( + */ +bool ParseXMLUnicode (const char *repr, unicode_char *out) { - size_t len = strlen(repr); + size_t len = strlen (repr); if (len == 1) // ascii { - *out = repr[0]; + *out = repr [0]; return true; } - else if (repr[0] == '&' || repr[1] == '#' || repr[len - 1] == ';' ) // html code + else if (repr [0] == '&' || repr [1] == '#' || repr [len - 1] == ';' ) // html code { - if (repr[2] == 'x') // hexadecimal + if (repr [2] == 'x') // hexadecimal sscanf (repr + 3, "%x;", out); @@ -80,6 +91,10 @@ bool ParseHtmlUnicode(const char *repr, unicode_char *out) return false; } +/** + * Parses n comma/space separated floats from the given string and + * returns a pointer to the text after it. + */ const char *SVGParsePathFloats (const int n, const char *text, float outs []) { const char *tmp = text; @@ -97,16 +112,16 @@ const char *SVGParsePathFloats (const int n, const char *text, float outs []) return NULL; } - text = ParseFloat(text, &outs[i]); + text = ParseFloat (text, &outs[i]); if (!text) return NULL; } return text; } -/* - * The following chunk of code, used to draw quadratic curves in cairo, was - * borrowed from cairosvg (http://cairosvg.org/) +/** + * This function, used to draw quadratic curves in cairo, is + * based on code from cairosvg (http://cairosvg.org/) */ void Quadratic2Bezier (float *px1, float *py1, float *px2, float *py2, const float x, const float y) { @@ -120,6 +135,13 @@ void Quadratic2Bezier (float *px1, float *py1, float *px2, float *py2, const flo *px2 = xq2; *py2 = yq2; } + +/** + * Turns a cairo surface into a GL texture of equal dimensions. + * Only works for colored surfaces with alpha channel. + * Should only be used for generating glyph textures, was not + * tested for other things. + */ bool Cairo2GLTex (cairo_surface_t *surface, GLuint *pTex) { char errorString[128]; @@ -158,16 +180,15 @@ bool Cairo2GLTex (cairo_surface_t *surface, GLuint *pTex) return false; } - glBindTexture(GL_TEXTURE_2D, *pTex); - - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glBindTexture (GL_TEXTURE_2D, *pTex); - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + // These settings make it look smooth: + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + // This automatically clamps texture coordinates to [0.0 -- 1.0] + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); /* * Turn transparent black into transparent white. @@ -190,16 +211,17 @@ bool Cairo2GLTex (cairo_surface_t *surface, GLuint *pTex) // fill the texture: glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (GLsizei)w, (GLsizei)h, - 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pData); + 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, // (cairo has ARGB, but GL wants BGRA) + pData); glBindTexture(GL_TEXTURE_2D, 0); return true; } -/* - * The following chunk of code, used for drawing an svg arc in cairo, was - * borrowed from cairosvg (http://cairosvg.org/) +/** + * This function, used for drawing an svg arc in cairo, is + * based on code from cairosvg (http://cairosvg.org/) */ void cairo_svg_arc (cairo_t *cr, float current_x, float current_y, float rx, float ry, @@ -284,8 +306,19 @@ void cairo_svg_arc (cairo_t *cr, float current_x, float current_y, sweep?'+':'-', large_arc? '>':'<', xc, yc, xe, ye, rx, angle1 * (180/M_PI), angle2 * (180/M_PI)); */ } +// This is used only when clearing the background in cairo: #define GLYPH_BBOX_MARGE 10.0f +/** + * Parses the glyph path and creates its texture. + * (http://www.w3.org/TR/SVG/paths.html) + * + * :param pGlyph: glyph object, whose texture must be created. + * :param pBox: font's bounding box + * :param d: path data as in svg + * :param multiply: multiplies the size of the glyph + * :returns: true on success, falso on error + */ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyph *pGlyph) { const char *nd, *sd = d; @@ -335,20 +368,24 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp { prev_symbol = symbol; + // Get the next path symbol: + while (isspace (*d)) d++; - upper = isupper(*d); + upper = isupper(*d); // upper is absolute, lower is relative symbol = tolower(*d); d++; + // Take the approriate action for the symbol: switch (symbol) { - case 'z': + case 'z': // closepath + cairo_close_path(cr); break; - case 'm': + case 'm': // moveto (x y)+ while ((nd = SVGParsePathFloats (2, d, f))) { @@ -369,7 +406,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 'l': + case 'l': // lineto (x y)+ while ((nd = SVGParsePathFloats (2, d, f))) { @@ -390,7 +427,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 'h': + case 'h': // horizontal lineto x+ while ((nd = SVGParsePathFloats (1, d, f))) { @@ -409,7 +446,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 'v': + case 'v': // vertical lineto y+ while ((nd = SVGParsePathFloats (1, d, f))) { @@ -428,7 +465,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 'c': + case 'c': // curveto (x1 y1 x2 y2 x y)+ while ((nd = SVGParsePathFloats (6, d, f))) { @@ -451,7 +488,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 's': + case 's': // shorthand/smooth curveto (x2 y2 x y)+ while ((nd = SVGParsePathFloats (4, d, f))) { @@ -482,7 +519,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 'q': + case 'q': // quadratic Bezier curveto (x1 y1 x y)+ while ((nd = SVGParsePathFloats (4, d, f))) { @@ -498,6 +535,8 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp x += f[2]; y += f[3]; } + // Cairo doesn't do quadratic, so fake it: + qx1 = x1; qy1 = y1; qx2 = x2; qy2 = y2; Quadratic2Bezier (&qx1, &qy1, &qx2, &qy2, x, y); cairo_curve_to (cr, qx1, qy1, qx2, qy2, x, y); @@ -506,7 +545,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 't': + case 't': // Shorthand/smooth quadratic Bézier curveto (x y)+ while ((nd = SVGParsePathFloats (2, d, f))) { @@ -529,6 +568,8 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp x += f[0]; y += f[1]; } + // Cairo doesn't do quadratic, so fake it: + qx1 = x1; qy1 = y1; qx2 = x2; qy2 = y2; Quadratic2Bezier (&qx1, &qy1, &qx2, &qy2, x, y); cairo_curve_to(cr, qx1, qy1, qx2, qy2, x, y); @@ -537,7 +578,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp } break; - case 'a': + case 'a': // elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ while (isspace(*d)) d++; @@ -629,7 +670,7 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp // Convert the cairo surface to a GL texture: - bool success = Cairo2GLTex(surface, &pGlyph->tex); + bool success = Cairo2GLTex (surface, &pGlyph->tex); // Don't need this anymore: cairo_surface_destroy (surface); @@ -641,6 +682,14 @@ bool ParseGlyphPath (const char *d, const BBox *pBox, const float multiply, Glyp return success; } +/** + * The header contains important settings + * :param pFnt: xml font tag + * :param size: desired font size + * :param pFont: font object to set data to + * :param pMultiply: output value to multiply all the font's dimensions with + * :returns: true on success, false when data is missing + */ bool ParseSvgFontHeader(const xmlNodePtr pFnt, const int size, Font *pFont, float *pMultiply) { xmlChar *pAttrib; @@ -663,6 +712,8 @@ bool ParseSvgFontHeader(const xmlNodePtr pFnt, const int size, Font *pFont, floa pAttrib = xmlGetProp(pFace, (const xmlChar *)"units-per-em"); if (!pAttrib) { + // other size specifications are not implemented here ! + SetError ("units-per-em is not set on font-face tag, it\'s required"); return false; } @@ -674,6 +725,8 @@ bool ParseSvgFontHeader(const xmlNodePtr pFnt, const int size, Font *pFont, floa pAttrib = xmlGetProp(pFace, (const xmlChar *)"bbox"); if (!pAttrib) { + // other size specifications are not implemented here ! + SetError ("bbox is not set on font-face tag, it\'s required"); return false; } @@ -691,6 +744,11 @@ bool ParseSvgFontHeader(const xmlNodePtr pFnt, const int size, Font *pFont, floa pFont->bbox.right *= *pMultiply; pFont->bbox.top *= *pMultiply; + /* + horiz-adv-x, horiz-origin-x & horiz-origin-y are optional. + If not set, they are presumed 0. + */ + pAttrib = xmlGetProp(pFnt, (const xmlChar *)"horiz-adv-x"); if (pAttrib) { @@ -723,9 +781,10 @@ bool ParseSvgFontHeader(const xmlNodePtr pFnt, const int size, Font *pFont, floa bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) { + // The root tag of the xml document is svg: bool success; xmlNodePtr pRoot = xmlDocGetRootElement(pDoc), pList, pTag; - if(!pRoot) { + if (!pRoot) { SetError ("no root element in svg doc"); return false; @@ -737,7 +796,8 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) return false; } - // Find the defs, containing the font + // In svg, fonts are found in the defs section. + xmlNode *pDefs = NULL, *pFnt = NULL, *pChild = pRoot->children, @@ -758,6 +818,8 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) return false; } + // Pick the first font tag we see: + pChild = pDefs->children; while (pChild) { if (StrCaseCompare((const char *) pChild->name, "font") == 0) { @@ -774,14 +836,16 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) return false; } + // Get data from the font header: float multiply; if (!ParseSvgFontHeader(pFnt, size, pFont, &multiply)) return false; - pFont->size = size; + pFont->size = size; // size determines multiply, thus do not multiply size. std::map glyph_name_lookup; + // iterate over glyph tags for (pChild = pFnt->children; pChild; pChild = pChild->next) { if (StrCaseCompare((const char *) pChild->name, "glyph") == 0) @@ -798,7 +862,7 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) continue; } - if (!ParseHtmlUnicode((const char *)pAttrib, &ch)) + if (!ParseXMLUnicode((const char *)pAttrib, &ch)) { SetError ("failed to interpret unicode id: %s", (const char *)pAttrib); xmlFree (pAttrib); @@ -809,34 +873,39 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) pAttrib = xmlGetProp(pChild, (const xmlChar *)"glyph-name"); if (pAttrib) { + /* + In some fonts, glyphs are given names to identify them with. + These names need to be remembered during the parsing process. + */ + std::string name = std::string((const char *)pAttrib); xmlFree (pAttrib); - glyph_name_lookup[name] = ch; + glyph_name_lookup [name] = ch; } - pFont->glyphs.end(); - - pFont->glyphs.find(ch); - if (pFont->glyphs.find(ch) != pFont->glyphs.end()) { + // means the table already has an entry for this unicode character. + SetError ("error: duplicate unicode id 0x%X", ch); return false; } - pFont->glyphs[ch] = Glyph(); - pFont->glyphs[ch].ch = ch; + // Add new entry to glyphs table + pFont->glyphs [ch] = Glyph(); + pFont->glyphs [ch].ch = ch; - pAttrib = xmlGetProp(pChild, (const xmlChar *)"d"); + // Get path data and generate a glyph texture from it: + pAttrib = xmlGetProp (pChild, (const xmlChar *)"d"); if (pAttrib) { success = ParseGlyphPath ((const char *)pAttrib, &pFont->bbox, multiply, &pFont->glyphs[ch]); } - else// path may be an empty string, whitespace for example + else // path may be an empty string, whitespace for example { - pFont->glyphs[ch].tex = NULL; + pFont->glyphs [ch].tex = NULL; success = true; } xmlFree (pAttrib); @@ -847,12 +916,17 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) return false; } + /* + horiz-adv-x, horiz-origin-x & horiz-origin-y are optional, + if not present, use the font's default setting. + */ + pAttrib = xmlGetProp(pChild, (const xmlChar *)"horiz-adv-x"); if (pAttrib) - pFont->glyphs[ch].horiz_adv_x = atoi ((const char *)pAttrib) * multiply; + pFont->glyphs [ch].horiz_adv_x = atoi ((const char *)pAttrib) * multiply; else - pFont->glyphs[ch].horiz_adv_x = -1.0f; + pFont->glyphs [ch].horiz_adv_x = -1.0f; xmlFree (pAttrib); @@ -876,7 +950,7 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) } } - // Now look for kerning data + // Now look for horizontal kerning data, iterate over hkern tags: for (pChild = pFnt->children; pChild; pChild = pChild->next) { if (StrCaseCompare((const char *) pChild->name, "hkern") == 0) @@ -885,6 +959,7 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) std::list g1, g2, su1, su2; std::list u1, u2; + // Get kern value first: pAttrib = xmlGetProp(pChild, (const xmlChar *)"k"); if (!pAttrib) { @@ -894,6 +969,15 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) float k = atoi ((const char *)pAttrib) * multiply; xmlFree (pAttrib); + /* + Now determine the kern pair. In svg a kern pair can + be a many-to-many, but we store them as + one-to-one glyph kern pairs. + + u1 & u2 specify unicode references, that need to be parsed. + g1 & g2 specify glyph names, that need to be looked up. + */ + pAttrib = xmlGetProp(pChild, (const xmlChar *)"g1"); if (pAttrib) split ((const char *)pAttrib, ',', g1); @@ -926,6 +1010,9 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) return false; } + // Combine g1 & su1 to make u1 + // Combine g2 & su2 to make u2 + for (std::list::const_iterator it = g1.begin(); it != g1.end(); it++) { std::string name = *it; @@ -951,7 +1038,7 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) std::string s = *it; unicode_char c; - if (!ParseHtmlUnicode(s.c_str(), &c)) + if (!ParseXMLUnicode(s.c_str(), &c)) { SetError ("unparsable unicode char: %s", s.c_str()); return false; @@ -963,7 +1050,7 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) std::string s = *it; unicode_char c; - if (!ParseHtmlUnicode(s.c_str(), &c)) + if (!ParseXMLUnicode(s.c_str(), &c)) { SetError ("unparsable unicode char: %s", s.c_str()); return false; @@ -971,6 +1058,8 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) u2.push_back(c); } + // Add the combinations from u1 and u2 to the kern table + for (std::list::const_iterator it = u1.begin(); it != u1.end(); it++) { unicode_char c1 = *it; @@ -990,9 +1079,13 @@ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont) } } + // At this point, all went well. return true; } +/** + * Renders one glyph at x,y + */ void glRenderGlyph (const Font *pFont, const Glyph *pGlyph, const GLfloat x, const GLfloat y) { const BBox *pBox = &pFont->bbox; @@ -1003,6 +1096,7 @@ void glRenderGlyph (const Font *pFont, const Glyph *pGlyph, const GLfloat x, con texcoord_fw = GLfloat (pGlyph->tex_w) / w, texcoord_fh = GLfloat (pGlyph->tex_h) / h; + // Render the glyph texture to a bbox-sized quad at x,y: glBindTexture(GL_TEXTURE_2D, pGlyph->tex); glBegin(GL_QUADS); @@ -1021,6 +1115,9 @@ void glRenderGlyph (const Font *pFont, const Glyph *pGlyph, const GLfloat x, con glEnd(); } +/** + * looks up the kern value for two characters: prev_c and c + */ float GetHKern (const Font *pFont, const unicode_char prev_c, const unicode_char c) { if (prev_c && pFont->hKernTable.find(prev_c) != pFont->hKernTable.end()) @@ -1033,6 +1130,9 @@ float GetHKern (const Font *pFont, const unicode_char prev_c, const unicode_char } return 0.0f; } +/** + * Looks up the horizontal advance value for a glyph. + */ float GetHAdv (const Font *pFont, const Glyph *pGlyph) { if (pGlyph && pGlyph->horiz_adv_x > 0) @@ -1041,6 +1141,9 @@ float GetHAdv (const Font *pFont, const Glyph *pGlyph) else return pFont->horiz_adv_x; // default value } +/** + * Gets the width of the first word in pUTF8. + */ float NextWordWidth (const Font *pFont, const char *pUTF8, unicode_char prev_c) { float w = 0.0f; @@ -1103,6 +1206,10 @@ float GetLineSpacing (const Font *pFont) { return pFont->bbox.top - pFont->bbox.bottom; } + +/** + * returns: true if the next word exceeds maxWidth when started at current_x, false otherwise. + */ bool NeedNewLine (const Font *pFont, const unicode_char prev_c, const char *pUTF8, const float current_x, const float maxWidth) { unicode_char c; @@ -1135,10 +1242,13 @@ bool NeedNewLine (const Font *pFont, const unicode_char prev_c, const char *pUTF return false; } +/** + * consumes characters from pUTF8 that shouldn't be taken to the next line + * :returns: pointer to the next line + * :param n_removed: optional, tells how many characters were removed. + */ const char *CleanLineEnd (const char *pUTF8, int *n_removed = NULL) { - // consumes characters that shouldn't be taken to the next line - unicode_char c; const char *nUTF8; @@ -1170,6 +1280,9 @@ const char *CleanLineEnd (const char *pUTF8, int *n_removed = NULL) } } } +/* + * Looks up the origin for a glyph. + */ void GetGlyphOrigin (const Font *pFont, const Glyph *pGlyph, float &ori_x, float &ori_y) { ori_x = pFont->horiz_origin_x; @@ -1180,6 +1293,11 @@ void GetGlyphOrigin (const Font *pFont, const Glyph *pGlyph, float &ori_x, float if (pGlyph && pGlyph->horiz_origin_y > 0) ori_y = pGlyph->horiz_origin_y; } +/** + * Computes the width of the next line at pUTF8. + * (how many glyphs fit in) + * It can never be longer than maxLineWidth. + */ float NextLineWidth (const Font *pFont, const char *pUTF8, const float maxLineWidth) { float x = 0.0f, w = 0.0f; @@ -1215,12 +1333,25 @@ float NextLineWidth (const Font *pFont, const char *pUTF8, const float maxLineWi return w; } -/* +/** + * ThroughTextGlyphFuncs are callbacks that will be called for each glyph + * that ThroughText encounters on its pass through the text. + * * A ThroughTextGlyphFunc should return true if it wants the next glyph. * Glyph pointer is NULL for the terminating null character! + * + * x,y are the position where the glyph's origin should be. + * string_pos is the index of the character, starting from 0 + * the last argument is the object inserted as pObject in ThroughText. */ typedef bool (*ThroughTextGlyphFunc)(const Font *, const Glyph*, const float x, const float y, const int string_pos, void*); +/* + * ThroughText passes through the given utf8 string with given text formatting. + * It calls the given ThroughTextGlyphFunc for every glyph it encounters. + * pObject can be any object that must be passed on to the ThroughTextGlyphFunc + * in each call. + */ void ThroughText (const Font *pFont, const char *pUTF8, ThroughTextGlyphFunc GlyphFunc, void *pObject, const int align, float maxWidth) @@ -1230,12 +1361,17 @@ void ThroughText (const Font *pFont, const char *pUTF8, float x = 0.0f, y = 0.0f, lineWidth = NextLineWidth (pFont, pUTF8, maxWidth), glyph_x; - unicode_char c = NULL, prev_c = NULL; + + unicode_char c = NULL, + prev_c = NULL; + const Glyph *pGlyph; + while (*pUTF8) { prev_c = c; + // Before moving on to the next glyph, see if we need to start a new line if (NeedNewLine (pFont, prev_c, pUTF8, x, maxWidth)) { // start new line @@ -1250,6 +1386,7 @@ void ThroughText (const Font *pFont, const char *pUTF8, continue; } + // Get next utf8 char as c pUTF8 = utf8_next (pUTF8, &c); if (pFont->glyphs.find(c) == pFont->glyphs.end()) @@ -1258,29 +1395,40 @@ void ThroughText (const Font *pFont, const char *pUTF8, } pGlyph = &pFont->glyphs.at (c); + // Look up horizontal kern value: if (x > 0.0f) x -= GetHKern (pFont, prev_c, c); + // move the glyph to the left if alignment is mid or right glyph_x = x; if (halign == TEXTALIGN_MID) + glyph_x = x - lineWidth / 2; + else if (halign == TEXTALIGN_RIGHT) + glyph_x = x - lineWidth; + // Call the given ThroughTextGlyphFunc and break out if it returns false if (!GlyphFunc (pFont, pGlyph, glyph_x, y, i, pObject)) return; + // move on to the x after the glyph and update the index: x += GetHAdv (pFont, pGlyph); i ++; } - // Call GlyphFunc for the terminating null + // move the glyph to the left if alignment is mid or right glyph_x = x; if (halign == TEXTALIGN_MID) + glyph_x = x - lineWidth / 2; + else if (halign == TEXTALIGN_RIGHT) + glyph_x = x - lineWidth; + // Call GlyphFunc for the terminating null: GlyphFunc (pFont, NULL, glyph_x, y, i, pObject); } bool glRenderTextCallBack (const Font *pFont, const Glyph *pGlyph, const float x, const float y, const int string_pos, void *p) @@ -1288,6 +1436,8 @@ bool glRenderTextCallBack (const Font *pFont, const Glyph *pGlyph, const float x if (!pGlyph) return true; + // Render the glyph with its origin at the given x,y + float ori_x, ori_y; GetGlyphOrigin (pFont, pGlyph, ori_x, ori_y); @@ -1315,7 +1465,7 @@ bool glRenderTextAsRectsCallBack (const Font *pFont, const Glyph *pGlyph, const TextRectObject *pO = (TextRectObject *)p; if (string_pos < pO->from) { - return true; // no rects yet + return true; // no rects yet, wait till after 'from' } float ori_x, ori_y; @@ -1326,14 +1476,19 @@ bool glRenderTextAsRectsCallBack (const Font *pFont, const Glyph *pGlyph, const bool end_render = string_pos >= pO->to, first_glyph = string_pos <= pO->from, + + // If the y value has changed, means a new line started new_line = abs ((y - ori_y) - pO->current_y) >= h, + // A rect ends when we've passed 'to' or a new line is started end_prev_rect = end_render || new_line, + + // A rect starts when we're at the first glyph or a new line is started start_new_rect = new_line || first_glyph; if (end_prev_rect) { - //printf ("end at %d\n", string_pos); + // Render a quad from start to previous glyph GLfloat x1, y1, x2, y2; @@ -1351,7 +1506,7 @@ bool glRenderTextAsRectsCallBack (const Font *pFont, const Glyph *pGlyph, const } if (start_new_rect) { - //printf ("start at %d\n", string_pos); + // Remember the start x,y pO->start_x = x - ori_x; pO->current_y = y - ori_y; @@ -1360,6 +1515,7 @@ bool glRenderTextAsRectsCallBack (const Font *pFont, const Glyph *pGlyph, const if (end_render) return false; + // Remember the ending x of the last rect-included glyph pO->end_x = x - ori_x + w; return true; @@ -1394,18 +1550,30 @@ bool WhichGlyphAtCallBack (const Font *pFont, const Glyph *pGlyph, const float x { GlyphAtObject *pO = (GlyphAtObject *)p; - float ori_x, ori_y, h_adv; + // Calculate glyph bounding box: + float ori_x, + ori_y, + h_adv; + GetGlyphOrigin (pFont, pGlyph, ori_x, ori_y); h_adv = GetHAdv (pFont, pGlyph); - float x1 = x - ori_x, x2 = x1 + h_adv, - y1 = y - ori_y, y2 = y1 + GetLineSpacing (pFont); + float x1 = x - ori_x, + x2 = x1 + h_adv, + y1 = y - ori_y, + y2 = y1 + GetLineSpacing (pFont); - if (pO->py > y1 && pO->py < y2) + // See if px,py hits the bounding box: + + if (pO->py < y1) { - //printf ("%d (%f) on line between %f and %f\n", string_pos, pO->py, y1, y2); + // point is above this line, we will not encounter it anymore in the upcoming lines + return false; + } + else if (pO->py > y1 && pO->py < y2) + { // point is on this line if (pO->px >= x1 && pO->px < x2) // point is inside the glyph's box @@ -1428,12 +1596,16 @@ bool WhichGlyphAtCallBack (const Font *pFont, const Glyph *pGlyph, const float x } } - return (pO->pos < 0); // as long as pos id -1, keep checking the next glyph + return (pO->pos < 0); // as long as index is -1, keep checking the next glyph } int WhichGlyphAt (const Font *pFont, const char *pUTF8, const float px, const float py, const int align, float maxWidth) { + /* + Important! Index positions must be set to a negative number at start. + Negative is interpreted as 'unset'. + */ GlyphAtObject o; o.pos = -1; o.leftmost_pos = -1; @@ -1445,7 +1617,7 @@ int WhichGlyphAt (const Font *pFont, const char *pUTF8, WhichGlyphAtCallBack, &o, align, maxWidth); - if (o.pos < 0) // no exact match, but maybe a very close one + if (o.pos < 0) // no exact match, but maybe the point is on the same line { if (o.leftmost_pos >= 0) @@ -1462,12 +1634,13 @@ bool CoordsOfGlyphCallBack (const Font *pFont, const Glyph *pGlyph, const float { GlyphAtObject *pO = (GlyphAtObject *)p; - if (pO->pos == string_pos) + if (pO->pos == string_pos) // is this the glyph we were looking for? { float ori_x, ori_y; GetGlyphOrigin (pFont, pGlyph, ori_x, ori_y); + // Return its position pO->px = x - ori_x; pO->py = y - ori_y; @@ -1481,6 +1654,8 @@ void CoordsOfGlyph (const Font *pFont, const char *pUTF8, const int pos, { GlyphAtObject o; o.pos = pos; + + // Default returned coords: o.px = 0; o.py = 0; @@ -1499,6 +1674,7 @@ bool DimensionsOfTextCallBack (const Font *pFont, const Glyph *pGlyph, const flo { DimensionsTracker *pT = (DimensionsTracker *)p; + // Calculate glyph bounding box: float ori_x, ori_y, h_adv; GetGlyphOrigin (pFont, pGlyph, ori_x, ori_y); @@ -1507,6 +1683,7 @@ bool DimensionsOfTextCallBack (const Font *pFont, const Glyph *pGlyph, const flo float x1 = x - ori_x, x2 = x1 + h_adv, y1 = y - ori_y, y2 = y1 + GetLineSpacing (pFont); + // Expand text bounding box with glyph's bounding box: pT->minX = std::min (x1, pT->minX); pT->minY = std::min (y1, pT->minY); pT->maxX = std::max (x2, pT->maxX); @@ -1518,12 +1695,18 @@ void DimensionsOfText (const Font *pFont, const char *pUTF8, float &outX1, float &outY1, float &outX2, float &outY2, const int align, float maxWidth) { + // If the font has no glyphs, no text can exist: if (pFont->glyphs.size() <= 0) { outX1 = outY1 = outX2 = outY2 = 0.0; return; } + /* + At start, set the bounding box minima to unrealistically high numbers + and the maxima to unrealistically negative numbers. That way, we + know for sure that they'll be overruled by the glyphs in the text. + */ DimensionsTracker t; t.minX = t.minY = 1.0e+15f; t.maxX = t.maxY = -1.0e+15f; diff --git a/src/font.h b/src/font.h index bb443e8..fd6fe91 100644 --- a/src/font.h +++ b/src/font.h @@ -31,50 +31,90 @@ #define TEXTALIGN_MID 0x03 #define TEXTALIGN_RIGHT 0x06 -typedef int unicode_char; +typedef int unicode_char; // these codes are backbards compatible with ascii chars -struct Glyph { +struct Glyph { /* these objects are mapped one-to-one with character codes */ + // (stored for convenience) unicode_char ch; + + // Texture that contains the glyph's image GLuint tex; GLsizei tex_h, tex_w; + + /* + horiz_origin_x, horiz_origin_y & horiz_adv_x may be negative, + in which case they are ignored and the font's default is used: + */ float horiz_origin_x, horiz_origin_y, horiz_adv_x; }; -struct BBox { - float left, bottom, right, top; +struct BBox { /* all the font's glyphs should fit inside this */ + + float left, bottom, right, top; // parse order }; struct Font { - float size, + float size, // ems per unit horiz_origin_x, horiz_origin_y, horiz_adv_x; BBox bbox; - std::map glyphs; - std::map > hKernTable; -}; + // One glyph per character code: + std::map glyphs; -float GetLineSpacing (const Font *pFont); + // One kern value per character pair: + std::map > hKernTable; +}; +/** + * This takes the values from an svg document and creates a font with them: + * + * :param size: the desired ems per unit + * :param pDoc: input svg document + * :param pFont: font object to store data in + * + * :returns: true is successful, falso on error + */ bool ParseSVGFont (const xmlDocPtr pDoc, const int size, Font *pFont); +// This gives the distance between the bottoms of two successive lines of text: +float GetLineSpacing (const Font *pFont); + /* + In the text rendering and sizing functions I chose to use relative coordinates. + Thus, all X and Y values are based on the assumption that the first glyph starts at (0,0). + This reduces the number of function arguments needed. + + To place text at different locations than (0,0), + one must transform the coordinates. + + align and maxWidth may be set, but have default values also. + If maxWidth is negative, there is assumed to be no maximum text width. + */ + +/** * Renders with origin point (0,0,0). Use transforms to render text at a different position. * If maxWidth is set, then line breaks are added between some of the words so that the lines don't get wider. */ void glRenderText (const Font *pFont, const char *pUTF8, const int align = TEXTALIGN_LEFT, float maxWidth = -1.0f); -/* - * This is handy for text selections: +/** + * glRenderTextAsRects is handy for text selections. + * + * :param from: character index to start rendering from + * :param to: character index where rendering stops */ void glRenderTextAsRects (const Font *pFont, const char *pUTF8, const int from, const int to, const int align = TEXTALIGN_LEFT, float maxWidth = -1.0f); -/* - * px and py must be relative to origin point of the text. +/** + * Tells which glyph is at the requested point. + * + * :param px, py: Requested point's coordinates, relative to origin point of the text. + * * if (px,py) lies within a glyph's box, returns its index position. * if (px,py) lies on the same line, but hits no glyph, returns either the rightmost or leftmost index position. * If (px,py) is not in line, returns -1 @@ -83,13 +123,21 @@ int WhichGlyphAt (const Font *pFont, const char *pUTF8, const float px, const float py, const int align = TEXTALIGN_LEFT, float maxWidth = -1.0f); -/* - * Returns coordinates relative to origin point of text. +/** + * Tells coordinates of a glyph relative to origin point of text. + * + * :param pos: charater index, whose glyph we want the coordinates from. + * :param outX, outY: output coordinates of the requested glyph */ void CoordsOfGlyph (const Font *pFont, const char *pUTF8, const int pos, float &outX, float &outY, const int align = TEXTALIGN_LEFT, float maxWidth = -1.0f); - +/** + * Tells how big the text is, by giving back a bounding box. + * Returned bounding box area is relative to the origin point of the text. + * + * :param outX1, outY1, outX2, outY2: output relative bounding box. + */ void DimensionsOfText (const Font *pFont, const char *pUTF8, float &outX1, float &outY1, float &outX2, float &outY2, const int align = TEXTALIGN_LEFT, float maxWidth = -1.0f); diff --git a/src/geo2d.h b/src/geo2d.h index 5a87f3b..f46721e 100644 --- a/src/geo2d.h +++ b/src/geo2d.h @@ -33,8 +33,11 @@ struct vec2 void operator-= (const vec2& v2); bool operator== (const vec2& other) const; vec2 operator- () const; + + // Length2 is computationally less expensive than Length float length2() const; float length() const; + float angle() const; // with x-axis vec2 unit() const; vec2 rotate(float angle) const; diff --git a/src/ini.cpp b/src/ini.cpp index 757d1ae..73cb8fb 100644 --- a/src/ini.cpp +++ b/src/ini.cpp @@ -89,10 +89,10 @@ void SaveSettingString(const char* filename, const char* setting, const char* va { c=fgetc(f); while( !feof(f) && isspace(c) ) c=fgetc(f); - i=0; while( !feof(f) && c!='=' && !isspace(c) ) { var[i]=c; i++; c=fgetc(f); } var[i]=NULL; + i=0; while( !feof(f) && c != '=' && !isspace(c) ) { var[i]=c; i++; c=fgetc(f); } var[i]=NULL; while( !feof(f) && isspace(c)) c=fgetc(f); - if( c=='=' && fscanf(f,"%s",val)==1 ) + if (c=='=' && fscanf (f, "%s", val) == 1) { if(strcmp(var,setting)==0) { @@ -123,7 +123,7 @@ int LoadSetting(const char* filename, const char* setting) } void SaveSetting(const char* filename, const char* setting, const int value) { - char val[100]; - sprintf(val,"%d",value); - SaveSettingString(filename,setting,val); + char val [100]; + sprintf (val, "%d", value); + SaveSettingString (filename, setting, val); } diff --git a/src/ini.h b/src/ini.h index bc06fbb..86ddd2d 100644 --- a/src/ini.h +++ b/src/ini.h @@ -21,9 +21,20 @@ #ifndef INI_H #define INI_H -int LoadSetting(const char* filename, const char* setting); -bool LoadSettingString(const char* filename, const char* setting, char* value); -void SaveSetting(const char* filename, const char* setting, const int value); -void SaveSettingString(const char* filename, const char* setting, const char* value); +/* + Syntax of settings files: + = + = + ... + + Whitespaces are ignored. + Settings can be either loaded / saved as string or integer value. + Use 0/1 integers for booleans. + */ + +int LoadSetting (const char *filename, const char *setting); +bool LoadSettingString (const char *filename, const char *setting, char *value); +void SaveSetting (const char *filename, const char *setting, const int value); +void SaveSettingString (const char *filename, const char *setting, const char *value); #endif // INI_H diff --git a/src/io.cpp b/src/io.cpp index b6cdf3b..627dfe6 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -24,19 +24,23 @@ #include -size_t NoWriteCallBack(SDL_RWops *context, const void *ptr, size_t size, size_t num) +size_t NoWriteCallBack (SDL_RWops *context, const void *ptr, size_t size, size_t num) { + // Tell that we cannot write any bytes: return 0; } -Sint64 NoSeekCallBack(SDL_RWops *context, Sint64 offset, int whence) +Sint64 NoSeekCallBack (SDL_RWops *context, Sint64 offset, int whence) { + // Tell that we cannot seek: return -1; } -Sint64 ZipArchiveSizeCallBack(SDL_RWops *context) +Sint64 ZipArchiveSizeCallBack (SDL_RWops *context) { + // Get the unzip pointer from the object: unzFile zip = (unzFile)context->hidden.unknown.data1; + // Get the file info from unzip: unz_file_info info; int res = unzGetCurrentFileInfo (zip, &info, NULL, 0, NULL, 0, NULL, 0); @@ -46,33 +50,43 @@ Sint64 ZipArchiveSizeCallBack(SDL_RWops *context) return -1; } + // Get the size part from info: return info.uncompressed_size; } -size_t ZipArchiveReadCallBack(SDL_RWops *context, void *data, size_t size, size_t maxnum) +size_t ZipArchiveReadCallBack (SDL_RWops *context, void *data, size_t size, size_t maxnum) { + // Get the unzip pointer from the object: unzFile zip = (unzFile)context->hidden.unknown.data1; + // Read from the currently pointed file in the archive: int res = unzReadCurrentFile (zip, data, size*maxnum); + // Return the number of objects read: return res / size; } int ZipArchiveCloseCallBack (SDL_RWops *context) { + // Get the unzip pointer from the object: unzFile zip = (unzFile)context->hidden.unknown.data1; + // Call unzip's close procedures: unzCloseCurrentFile (zip); unzClose (zip); + // Delete the SDL_RWops: + SDL_FreeRW (context); + return 0; } SDL_RWops *SDL_RWFromZipArchive (const char *_archive, const char *_entry) { + // Create an SDL_RWops: SDL_RWops *c=SDL_AllocRW(); if (!c) return NULL; - + // Let unzip open the archive: unzFile zip = unzOpen(_archive); if (!zip) { @@ -80,6 +94,7 @@ SDL_RWops *SDL_RWFromZipArchive (const char *_archive, const char *_entry) return NULL; } + // Let unzip find the desired file in the archive: int result = unzLocateFile (zip, _entry, 1); if (result != UNZ_OK) @@ -90,15 +105,18 @@ SDL_RWops *SDL_RWFromZipArchive (const char *_archive, const char *_entry) return NULL; } + // Open the desired file in the archive: unzOpenCurrentFile (zip); + // Set the callbacks, for size, read and close. + // unzip cannot seek nor write. c->size = ZipArchiveSizeCallBack; c->seek = NoSeekCallBack; c->read = ZipArchiveReadCallBack; c->write = NoWriteCallBack; c->close = ZipArchiveCloseCallBack; c->type = SDL_RWOPS_UNKNOWN; - c->hidden.unknown.data1 = (voidp)zip; + c->hidden.unknown.data1 = (voidp) zip; return c; } diff --git a/src/io.h b/src/io.h index ce96bcc..f947c05 100644 --- a/src/io.h +++ b/src/io.h @@ -26,6 +26,10 @@ #include +/** + * Gets the contents of a file from an archive. + * It's read only. + */ SDL_RWops *SDL_RWFromZipArchive (const char *archive_path, const char *entry_path); #endif // IO_H diff --git a/src/ip.h b/src/ip.h index bef95f6..525f493 100644 --- a/src/ip.h +++ b/src/ip.h @@ -25,20 +25,26 @@ #include -inline void ip2String(const IPaddress& address,char* out) +/* + IP addres includes a port number. + Syntax: + xxx.xxx.xxx.xxx:xxxxx + */ + +inline void ip2String (const IPaddress& address, char* out) { - Uint8*bytes=(Uint8*)&address.host; - sprintf(out,"%u.%u.%u.%u:%u", + Uint8 *bytes = (Uint8 *)&address.host; + sprintf (out, "%u.%u.%u.%u:%u", (unsigned int)bytes[0], (unsigned int)bytes[1], (unsigned int)bytes[2], (unsigned int)bytes[3], (unsigned int)address.port); } -inline void string2IP(const char* str, IPaddress& out) +inline void string2IP (const char* str, IPaddress& out) { - Uint8*bytes=(Uint8*)&out.host; - sscanf(str,"%u.%u.%u.%u:%u", + Uint8 *bytes = (Uint8 *)&out.host; + sscanf (str, "%u.%u.%u.%u:%u", (unsigned int*)&bytes[0], (unsigned int*)&bytes[1], (unsigned int*)&bytes[2], diff --git a/src/quat.h b/src/quat.h index f45c7f4..9a65c59 100644 --- a/src/quat.h +++ b/src/quat.h @@ -48,6 +48,7 @@ struct Quaternion { q[i] = other.q[i]; } + // Length2 is computationally less expensive than Length float Length2 () const { float length2 = 0.0f; diff --git a/src/shader.cpp b/src/shader.cpp index e4c4486..940546d 100644 --- a/src/shader.cpp +++ b/src/shader.cpp @@ -24,7 +24,7 @@ #include "err.h" #include "shader.h" -GLuint createShader(const std::string& source, int type) +GLuint CreateShader(const std::string& source, int type) { if(type != GL_VERTEX_SHADER && type != GL_FRAGMENT_SHADER) { @@ -57,14 +57,14 @@ GLuint createShader(const std::string& source, int type) return shader; } -GLuint createShaderProgram(const std::string& sourceVertex, const std::string& sourceFragment) +GLuint CreateShaderProgram(const std::string& sourceVertex, const std::string& sourceFragment) { GLint result = GL_FALSE; int logLength; GLuint program=0, vertexShader, fragmentShader; - vertexShader = createShader(sourceVertex, GL_VERTEX_SHADER); - fragmentShader = createShader(sourceFragment, GL_FRAGMENT_SHADER); + vertexShader = CreateShader(sourceVertex, GL_VERTEX_SHADER); + fragmentShader = CreateShader(sourceFragment, GL_FRAGMENT_SHADER); if(vertexShader && fragmentShader) { diff --git a/src/shader.h b/src/shader.h index 747137e..13fdf7f 100644 --- a/src/shader.h +++ b/src/shader.h @@ -26,6 +26,6 @@ #include -GLuint createShaderProgram(const std::string& sourceVertex, const std::string& sourceFragment); +GLuint CreateShaderProgram (const std::string& sourceVertex, const std::string& sourceFragment); #endif // SHADER_H diff --git a/src/test3d/app.cpp b/src/test3d/app.cpp index d0b2521..3b339e8 100644 --- a/src/test3d/app.cpp +++ b/src/test3d/app.cpp @@ -247,7 +247,7 @@ void App::MainLoop (void) while ((statusGL = glGetError()) != GL_NO_ERROR) { GLErrorString (errorString, statusGL); - std::string msg = std::string ("GL Error during main loop: %s") + errorString; + std::string msg = std::string ("GL Error during main loop: ") + errorString; ShowError (msg.c_str()); } #endif // DEBUG diff --git a/src/test3d/app.h b/src/test3d/app.h index 0498955..fcce60c 100644 --- a/src/test3d/app.h +++ b/src/test3d/app.h @@ -70,7 +70,7 @@ class App bool SetResolution (const int w, const int h); #ifdef _WIN32 - HICON icon; + HICON icon; // icon to use for the main window #endif private: @@ -82,6 +82,7 @@ class App SDL_GLContext mainGLContext; bool done; + // Current scene to show Scene *pScene; // Initialization functions diff --git a/src/test3d/collision.h b/src/test3d/collision.h index e538298..e3c3d36 100644 --- a/src/test3d/collision.h +++ b/src/test3d/collision.h @@ -76,7 +76,7 @@ vec3 CollisionMove (const vec3& p1, const vec3& p2, vec3 CollisionTraceBeam (const vec3& p1, const vec3 &p2, const Triangle *triangles, const int n_triangles); -/* +/** * Slightly different from CollisionMove, keeps colliders stuck to the ground. (if possible) */ vec3 CollisionWalk (const vec3& p1, const vec3& p2, diff --git a/src/test3d/mesh.cpp b/src/test3d/mesh.cpp index 7a80027..02f99cd 100644 --- a/src/test3d/mesh.cpp +++ b/src/test3d/mesh.cpp @@ -29,7 +29,8 @@ MeshObject::MeshObject(const MeshData *data) : pMeshData(data) } void MeshObject::InitStates() { - // Make state representatives + // Copy the mesh data's rest states to this object's vertex and bone states: + for (std::map::const_iterator it = pMeshData->vertices.begin(); it != pMeshData->vertices.end(); it++) { const std::string id = it->first; @@ -54,6 +55,8 @@ void MeshObject::RenderBones () { for (std::map::const_iterator it = boneStates.begin(); it != boneStates.end(); it++) { + // Draw a line from head to tail pos in the bone's current state + const std::string id = it->first; const MeshBoneState *pState = &boneStates.at (id); @@ -70,6 +73,8 @@ void MeshObject::RenderNormals () { for (std::map::const_iterator it = vertexStates.begin(); it != vertexStates.end(); it++) { + // position and normal might be transformed by a bone, so get the current vertex state: + const std::string id = it->first; const MeshVertex *pState = &vertexStates.at (id); @@ -77,6 +82,9 @@ void MeshObject::RenderNormals () const vec3 *p = &pState->p, pn = pState->p + size * pState->n; + // pn is p, moved in the direction of the normal + + // Draw a line from p to pn: glBegin (GL_LINES); glVertex3f (p->x, p->y, p->z); glVertex3f (pn.x, pn.y, pn.z); @@ -91,6 +99,8 @@ void ToTriangles (const MeshData *pMeshData, Triangle **pT, size_t *pN) size_t i = 0; for (std::map::const_iterator it = pMeshData->quads.begin(); it != pMeshData->quads.end(); it++) { + // Convert a quad to two triangles: + const PMeshQuad pQuad = &(it -> second); triangles [i + 1].p[0] = triangles [i].p[0] = pMeshData->vertices.at (pQuad->GetVertexID (0)).p; @@ -126,10 +136,10 @@ void RenderUnAnimatedSubset (const MeshData *pMeshData, const std::string &id) const MeshTexel *t; const vec3 *n, *p; + // Give the subset's quads and triangles to OpenGL to render: + glBegin(GL_QUADS); for (std::list::const_iterator it = pSubset->quads.begin(); it != pSubset->quads.end(); it++) { - glBegin(GL_QUADS); - PMeshQuad pQuad = *it; for (size_t j = 0; j < 4; j++) { @@ -141,9 +151,8 @@ void RenderUnAnimatedSubset (const MeshData *pMeshData, const std::string &id) glNormal3f(n->x, n->y, n->z); glVertex3f(p->x, p->y, p->z); } - - glEnd(); } + glEnd(); glBegin(GL_TRIANGLES); for (std::list::const_iterator it = pSubset->triangles.begin(); it != pSubset->triangles.end(); it++) @@ -171,29 +180,27 @@ void MeshObject::RenderSubset (const std::string &id) } const MeshSubset *pSubset = &pMeshData->subsets.at(id); - // TODO: apply bone transformations here - const MeshTexel *t; const vec3 *n, *p; + // Give the subset's quads and triangles to OpenGL to render: + // Use the vertex states here, which might be transformed. + glBegin(GL_QUADS); for(std::list::const_iterator it = pSubset->quads.begin(); it != pSubset->quads.end(); it++) { - glBegin(GL_QUADS); - PMeshQuad pQuad = *it; for(size_t j = 0; j < 4; j++) { - t = &pQuad->texels[j]; - n = &vertexStates[pQuad->GetVertexID(j)].n; - p = &vertexStates[pQuad->GetVertexID(j)].p; + t = &pQuad->texels [j]; + n = &vertexStates [pQuad->GetVertexID(j)].n; + p = &vertexStates [pQuad->GetVertexID(j)].p; glTexCoord2f(t->u, t->v); glNormal3f(n->x, n->y, n->z); glVertex3f(p->x, p->y, p->z); } - - glEnd(); } + glEnd(); glBegin(GL_TRIANGLES); for(std::list::const_iterator it = pSubset->triangles.begin(); it != pSubset->triangles.end(); it++) @@ -201,18 +208,23 @@ void MeshObject::RenderSubset (const std::string &id) PMeshTriangle pTri = *it; for(size_t j = 0; j < 3; j++) { - t = &pTri->texels[j]; - n = &vertexStates[pTri->GetVertexID(j)].n; - p = &vertexStates[pTri->GetVertexID(j)].p; + t = &pTri->texels [j]; + n = &vertexStates [pTri->GetVertexID(j)].n; + p = &vertexStates [pTri->GetVertexID(j)].p; - glTexCoord2f(t->u, t->v); - glNormal3f(n->x, n->y, n->z); - glVertex3f(p->x, p->y, p->z); + glTexCoord2f (t->u, t->v); + glNormal3f (n->x, n->y, n->z); + glVertex3f (p->x, p->y, p->z); } } glEnd(); } -bool ParseRGBA(const xmlNodePtr pTag, GLfloat *c) + +/** + * Gets the r,g,b and a attributes from the given tag. + * :returns: true if all float values were there, false otherwise. + */ +bool ParseRGBA (const xmlNodePtr pTag, GLfloat *c) { xmlChar *pR = xmlGetProp(pTag, (const xmlChar *)"r"), *pG = xmlGetProp(pTag, (const xmlChar *)"g"), @@ -240,7 +252,11 @@ bool ParseRGBA(const xmlNodePtr pTag, GLfloat *c) return bres; } -bool ParseXYZ(const xmlNodePtr pTag, vec3 *pVec, const char *prefix = NULL) +/** + * Gets the x, y and z attributes from the given tag. Optionally with a previx in their identifiers. + * :returns: true if all float values were there, false otherwise. + */ +bool ParseXYZ (const xmlNodePtr pTag, vec3 *pVec, const char *prefix = NULL) { std::string id_x = "x", id_y = "y", id_z = "z"; if(prefix) @@ -274,6 +290,10 @@ bool ParseXYZ(const xmlNodePtr pTag, vec3 *pVec, const char *prefix = NULL) return bres; } +/** + * Gets one float attribute from the given tag with given id (key) + * :returns: true if float was successfully parsed, false otherwise. + */ bool ParseFloatAttrib(const xmlNodePtr pTag, const xmlChar *key, float *f) { xmlChar *pF = xmlGetProp(pTag, key); @@ -291,6 +311,10 @@ bool ParseFloatAttrib(const xmlNodePtr pTag, const xmlChar *key, float *f) return true; } +/** + * Gets one int attribute from the given tag with given id (key) + * :returns: true if int was successfully parsed, false otherwise. + */ bool ParseIntAttrib(const xmlNodePtr pTag, const xmlChar *key, int *i) { xmlChar *pI = xmlGetProp(pTag, key); @@ -305,6 +329,10 @@ bool ParseIntAttrib(const xmlNodePtr pTag, const xmlChar *key, int *i) return true; } +/** + * Gets one string attribute from the given tag with given id (key) + * :returns: true if the attribute was found, false otherwise. + */ bool ParseStringAttrib(const xmlNodePtr pTag, const xmlChar *key, std::string &s) { xmlChar *pS = xmlGetProp(pTag, key); @@ -318,6 +346,10 @@ bool ParseStringAttrib(const xmlNodePtr pTag, const xmlChar *key, std::string &s return true; } + +/** + * Converts an xml vertex tag to a vertex and adds it to given map with proper id. + */ bool ParseVertex(const xmlNodePtr pTag, std::map &vertices) { std::string id; @@ -357,10 +389,14 @@ bool ParseVertex(const xmlNodePtr pTag, std::map &verti return true; } + +/** + * Converts given tag to face object and adds is to given map with proper id. + */ template bool ParseFace (const xmlNodePtr pTag, - const std::map &vertices, - std::map > &faces) + const std::map &vertices, + std::map > &faces) { std::string id; if (!ParseStringAttrib(pTag, (const xmlChar *)"id", id)) @@ -417,37 +453,46 @@ bool ParseFace (const xmlNodePtr pTag, return true; } +/** + * Converts given xml tag to bone object and adds it to given map with proper id. + * Also checks the ids of the vertices are in the given vertex map. + */ bool ParseBone(const xmlNodePtr pTag, const std::map &vertices, std::map &bones) { - std::string id, vert_id; - if(!ParseStringAttrib(pTag, (const xmlChar *)"id", id)) + std::string id, + vert_id; + + if(!ParseStringAttrib (pTag, (const xmlChar *)"id", id)) { SetError ("No id found on %s", pTag->name); return false; } - bones[id] = MeshBone(); + bones [id] = MeshBone(); - if(!ParseStringAttrib(pTag, (const xmlChar *)"parent_id", bones[id].parent_id)) + if (!ParseStringAttrib (pTag, (const xmlChar *)"parent_id", bones [id].parent_id)) { - bones[id].parent_id = ""; + bones [id].parent_id = ""; } - if (!ParseXYZ(pTag, &bones[id].posHead)) + // Get head pos and tail pos. + if (!ParseXYZ (pTag, &bones[id].posHead)) { SetError ("No x, y, z on %s %s", pTag->name, id.c_str ()); return false; } - if (!ParseXYZ(pTag, &bones[id].posTail, "tail_")) - bones[id].posTail = bones[id].posHead; + if (!ParseXYZ (pTag, &bones[id].posTail, "tail_")) + bones [id].posTail = bones[id].posHead; - if (!ParseFloatAttrib(pTag, (const xmlChar *)"weight", &bones[id].weight)) - bones[id].weight = 1.0f; + if (!ParseFloatAttrib (pTag, (const xmlChar *)"weight", &bones[id].weight)) + bones [id].weight = 1.0f; - xmlNodePtr pChild = pTag->children, pVert; + // Find vertex tags with ids inside the bone tag: + xmlNodePtr pChild = pTag->children, + pVert; while (pChild) { if (StrCaseCompare((const char *) pChild->name, "vertices") == 0) @@ -457,13 +502,15 @@ bool ParseBone(const xmlNodePtr pTag, { if (StrCaseCompare((const char *) pVert->name, "vertex") == 0) { - if (!ParseStringAttrib(pVert, (const xmlChar *)"id", vert_id)) + // Check that the id is present and that it's in the vertices map also: + + if (!ParseStringAttrib (pVert, (const xmlChar *)"id", vert_id)) { SetError ("no id found on vertex in %s %s\n", pTag->name, id.c_str()); return false; } - if (vertices.find(vert_id) == vertices.end()) + if (vertices.find (vert_id) == vertices.end()) { SetError ("no such vertex: %s, referred to by %s %s\n", vert_id.c_str(), pTag->name, id.c_str()); return false; @@ -477,9 +524,15 @@ bool ParseBone(const xmlNodePtr pTag, } pChild = pChild->next; } - //printf("parsed bone %s with %d vertices\n", id.c_str(), bones[id].vertex_ids.size()); + + // at this point all went well return true; } + +/** + * Converts given xml tag to subset and adds it to the given subset map with proper id. + * Also check that the referenced triangles and quads already exist. + */ bool ParseSubset(const xmlNodePtr pTag, const std::map &quads, const std::map &triangles, @@ -495,10 +548,9 @@ bool ParseSubset(const xmlNodePtr pTag, return false; } + // Add a new subset to the map with defaults: MeshSubset subset; subsets[id] = subset; - //std::pair pair (id, subset); - //subsets.insert (pair); for(int i=0; i<4; i++) { subsets[id].diffuse[i] = 1.0; @@ -509,9 +561,10 @@ bool ParseSubset(const xmlNodePtr pTag, pChild = pTag->children; while (pChild) { + // Look for diffuse, specular, emission colors and a list of faces inside the subset tag: if (StrCaseCompare((const char *) pChild->name, "diffuse") == 0) { - if(!ParseRGBA(pChild, subsets[id].diffuse)) + if(!ParseRGBA (pChild, subsets[id].diffuse)) { SetError ("error getting %s RGBA for %s %s", pChild->name, id.c_str(), pTag->name); return false; @@ -537,6 +590,8 @@ bool ParseSubset(const xmlNodePtr pTag, pFace = pChild->children; while (pFace) { + // Add face as either quad or triangle. + if (StrCaseCompare((const char *) pFace->name, "quad") == 0) { if (!ParseStringAttrib(pFace, (const xmlChar *)"id", face_id)) @@ -576,22 +631,31 @@ bool ParseSubset(const xmlNodePtr pTag, } return true; } + +/** + * Converts given xml tag to animation object and adds it to the given + * animations map with proper id. + * Also verifies that the bones in the animation exist. + */ bool ParseAnimation(const xmlNodePtr pTag, const std::map &bones, std::map &animations) { xmlNodePtr pChild, pLayer, pList, pKey; + // the id is actually called 'name' in animation tags. std::string id, bone_id; - if(!ParseStringAttrib(pTag, (const xmlChar *)"name", id)) + if (!ParseStringAttrib(pTag, (const xmlChar *)"name", id)) { SetError ("%s without a name", pTag->name); return false; } + // Create a new animation object: animations[id] = MeshAnimation(); - if(!ParseIntAttrib(pTag, (const xmlChar *)"length", &animations[id].length)) + // An animation must have a length: + if (!ParseIntAttrib(pTag, (const xmlChar *)"length", &animations[id].length)) { SetError ("no length given for %s %s", pTag->name, id.c_str()); return false; @@ -602,6 +666,8 @@ bool ParseAnimation(const xmlNodePtr pTag, { if(StrCaseCompare((const char *)pLayer->name, "layer") == 0) { + // Check that the layer is associated with an existing bone: + if (!ParseStringAttrib(pLayer, (const xmlChar *)"bone_id", bone_id)) { SetError ("no id on %s in %s %s", pLayer->name, pTag->name, id.c_str()); @@ -614,41 +680,48 @@ bool ParseAnimation(const xmlNodePtr pTag, return false; } - animations[id].layers.push_back(MeshBoneLayer()); - animations[id].layers.back().pBone = &bones.at(bone_id); - animations[id].layers.back().bone_id = bone_id; + // Add the layer to the animation and set its references to its bone. + animations[id].layers.push_back (MeshBoneLayer()); + animations[id].layers.back ().pBone = &bones.at(bone_id); + animations[id].layers.back ().bone_id = bone_id; pKey = pLayer->children; while (pKey) { - if (StrCaseCompare((const char *)pKey->name, "key") == 0) + if (StrCaseCompare ((const char *)pKey->name, "key") == 0) { + // Get key's frame number: float frame; - if(!ParseFloatAttrib(pKey, (const xmlChar *)"frame", &frame)) + if (!ParseFloatAttrib(pKey, (const xmlChar *)"frame", &frame)) { SetError ("no frame number given for %s", pKey->name); return false; } - animations[id].layers.back().keys [frame] = MeshBoneKey(); - if (ParseFloatAttrib(pKey, (const xmlChar *)"rot_w", &animations[id].layers.back().keys[frame].rot.w)) + // Add key to layer at the correct frame number: + animations [id].layers.back().keys [frame] = MeshBoneKey(); + + // Parse the key's rotation and location state for the bone: + if (ParseFloatAttrib(pKey, (const xmlChar *)"rot_w", &animations [id].layers.back().keys[frame].rot.w)) { - if (!ParseFloatAttrib (pKey, (const xmlChar *)"rot_x", &animations[id].layers.back().keys[frame].rot.x) || - !ParseFloatAttrib (pKey, (const xmlChar *)"rot_y", &animations[id].layers.back().keys[frame].rot.y) || - !ParseFloatAttrib (pKey, (const xmlChar *)"rot_z", &animations[id].layers.back().keys[frame].rot.z)) + if (!ParseFloatAttrib (pKey, (const xmlChar *)"rot_x", &animations [id].layers.back().keys [frame].rot.x) || + !ParseFloatAttrib (pKey, (const xmlChar *)"rot_y", &animations [id].layers.back().keys [frame].rot.y) || + !ParseFloatAttrib (pKey, (const xmlChar *)"rot_z", &animations [id].layers.back().keys [frame].rot.z)) { SetError ("%s bone %s rotation incomplete for key at %f", id.c_str(), bone_id.c_str(), frame); return false; } } - else animations[id].layers.back().keys[frame].rot = QUAT_ID; + else + animations[id].layers.back ().keys [frame].rot = QUAT_ID; - if(!ParseXYZ(pKey, &animations[id].layers.back().keys[frame].loc)) - animations[id].layers.back().keys[frame].loc = vec3(0,0,0); + if (!ParseXYZ (pKey, &animations [id].layers.back ().keys [frame].loc)) + animations [id].layers.back ().keys [frame].loc = vec3 (0, 0, 0); } pKey = pKey->next; } + // Check that the layer actually got any keys from this: if (animations[id].layers.back().keys.size () <= 0) { SetError ("layer %s with no keys in %s", bone_id.c_str(), id.c_str()); @@ -661,7 +734,7 @@ bool ParseAnimation(const xmlNodePtr pTag, } bool ParseMesh(const xmlDocPtr pDoc, MeshData *pData) { - xmlNodePtr pChild, pRoot = xmlDocGetRootElement(pDoc), pList, pTag; + xmlNodePtr pChild, pRoot = xmlDocGetRootElement (pDoc), pList, pTag; if(!pRoot) { SetError ("no root element in mesh xml doc"); @@ -830,12 +903,21 @@ bool ParseMesh(const xmlDocPtr pDoc, MeshData *pData) return true; } -std::map GetBoneTransformations(MeshObject *pMesh, const MeshAnimation *pAnimation, const float frame, const bool loop) +/** + * Calculates the transformations for every bone in the mesh for a given animation and frame. + * + * :returns: the matrices per bone id + */ +std::map GetBoneTransformations (MeshObject *pMesh, const MeshAnimation *pAnimation, const float frame, const bool loop) { std::map transforms; + // Iterate over layers (bones) for (std::list::const_iterator it = pAnimation->layers.begin(); it != pAnimation->layers.end(); it++) { + + // Get the closest key on the left and right of the current frame. + // Also get the leftmost and rightmost keys, in case we have a looping animation. const MeshBoneLayer *pLayer = &(*it); float keyLeft = -1.0f, @@ -844,7 +926,7 @@ std::map GetBoneTransformations(MeshObject *pMesh, const M keyLast = -1.0f, key; - for (std::map::const_iterator jt = pLayer->keys.begin(); jt != pLayer->keys.end(); ++jt) + for (std::map ::const_iterator jt = pLayer->keys.begin (); jt != pLayer->keys.end (); ++jt) { key = jt->first; @@ -890,8 +972,9 @@ std::map GetBoneTransformations(MeshObject *pMesh, const M } } - const MeshBoneKey *pLeft = &pLayer->keys.at(keyLeft), - *pRight = &pLayer->keys.at(keyRight); + // Get the actual keys: + const MeshBoneKey *pLeft = &pLayer->keys.at (keyLeft), + *pRight = &pLayer->keys.at (keyRight); // Determine the location and rotation of the bone in this frame: @@ -905,7 +988,10 @@ std::map GetBoneTransformations(MeshObject *pMesh, const M } else { - // determine t and u factors, these tell how much of the left and rightkey must be taken + /* + Need to interpolate between the two keys. + Determine t and u factors, these tell how much of the left and rightkey must be taken + */ float range_length = keyRight - keyLeft, range_pos = frame - keyLeft, @@ -916,55 +1002,65 @@ std::map GetBoneTransformations(MeshObject *pMesh, const M rot = slerp (pLeft->rot, pRight->rot, u); } - // make a matrix of it: - transforms[pLayer->bone_id] = matQuat(rot) * matTranslation(loc); // apply rotation && position + /* + Make a matrix of it. Apply translation after rotation. + */ + transforms [pLayer->bone_id] = matTranslation (loc) * matQuat (rot); } return transforms; } void MeshObject::ApplyBoneTransformations(const std::map transforms) { - std::map bonesPerVertex; + // This counts how many bones modify a given vertex. + std::map bonesPerVertex; - // First set vertices to 0,0,0: + /* + First set vertices to 0,0,0. + On these, we will add up all bone transformations. + */ for(std::map::iterator it = vertexStates.begin(); it != vertexStates.end(); it++) { std::string id = it->first; MeshVertex *pVertex = &it->second; if (pMeshData->vertices.find(id) == pMeshData->vertices.end()) { - fprintf(stderr, "no such vertex: %s\n", id.c_str()); + // This is not expected to happen, since the parser checks for such errors. + + fprintf (stderr, "no such vertex: %s\n", id.c_str()); continue; } - pVertex->n = vec3(0,0,0); - pVertex->p = vec3(0,0,0); + pVertex->n = vec3 (0,0,0); + pVertex->p = vec3 (0,0,0); - bonesPerVertex[id] = 0; + bonesPerVertex [id] = 0; } - for(std::map::const_iterator it = pMeshData->bones.begin(); it != pMeshData->bones.end(); it++) + for(std::map ::const_iterator it = pMeshData->bones.begin (); it != pMeshData->bones.end (); it++) { const std::string bone_id = it->first; - if (pMeshData->bones.find(bone_id) == pMeshData->bones.end()) + if (pMeshData->bones.find (bone_id) == pMeshData->bones.end ()) { - fprintf(stderr, "no such bone: %s\n", bone_id.c_str()); + // This is not expected to happen, since the parser checks for such errors. + + fprintf (stderr, "no such bone: %s\n", bone_id.c_str ()); continue; } - const MeshBone *pBone = &pMeshData->bones.at(bone_id); + const MeshBone *pBone = &pMeshData->bones.at (bone_id); // Transform the bone state: - MeshBoneState* pBoneState = &boneStates[bone_id]; + MeshBoneState* pBoneState = &boneStates [bone_id]; std::string chainpoint_id = bone_id; vec3 newHead = pBone->posHead, newTail = pBone->posTail; - while (chainpoint_id.size() > 0) + while (chainpoint_id.size () > 0) { - const MeshBone *pChainPoint = &pMeshData->bones.at(chainpoint_id); - if (transforms.find(chainpoint_id) != transforms.end()) + const MeshBone *pChainPoint = &pMeshData->bones.at (chainpoint_id); + if (transforms.find (chainpoint_id) != transforms.end ()) { - newHead = transforms.at(chainpoint_id) * (newHead - pChainPoint->posHead) + pChainPoint->posHead; - newTail = transforms.at(chainpoint_id) * (newTail - pChainPoint->posHead) + pChainPoint->posHead; + newHead = transforms.at (chainpoint_id) * (newHead - pChainPoint->posHead) + pChainPoint->posHead; + newTail = transforms.at (chainpoint_id) * (newTail - pChainPoint->posHead) + pChainPoint->posHead; } chainpoint_id = pChainPoint->parent_id; @@ -987,7 +1083,9 @@ void MeshObject::ApplyBoneTransformations(const std::map t { if (pMeshData->bones.find(chainpoint_id) == pMeshData->bones.end()) { - fprintf(stderr, "no such bone: %s\n", chainpoint_id.c_str()); + // This is not expected to happen, since the parser checks for such errors. + + fprintf (stderr, "no such bone: %s\n", chainpoint_id.c_str()); break; } @@ -1005,26 +1103,28 @@ void MeshObject::ApplyBoneTransformations(const std::map t chainpoint_id = pChainPoint->parent_id; } - bonesPerVertex[vertex_id] ++; + // Add up the results of all transformations per vertex: + // (could maybe apply bone weights here) + bonesPerVertex [vertex_id] ++; pVertex->p += newp; pVertex->n += newn; } } - // Convert vector sums to averages + // Convert vector sums to averages, divide by transform count: for(std::map::iterator it = vertexStates.begin(); it != vertexStates.end(); it++) { std::string id = it->first; - MeshVertex* pVertex = &vertexStates[id]; + MeshVertex* pVertex = &vertexStates [id]; - if (bonesPerVertex[id] > 0) + if (bonesPerVertex [id] > 0) { - pVertex->p /= bonesPerVertex[id]; - pVertex->n /= bonesPerVertex[id]; + pVertex->p /= bonesPerVertex [id]; + pVertex->n /= bonesPerVertex [id]; } else // not transformed, set to rest state { - const MeshVertex *pDataVertex = &pMeshData->vertices.at(id); + const MeshVertex *pDataVertex = &pMeshData->vertices.at (id); pVertex->p = pDataVertex->p; pVertex->n = pDataVertex->n; } @@ -1044,8 +1144,10 @@ void MeshObject::SetAnimationState(const char *animation_id, float frame, bool l else pAnimation = &pMeshData->animations.at(animation_id); } - else + else // NULL is given as argument { + // set vertices and bone to initial states + InitStates(); return; } @@ -1055,12 +1157,14 @@ void MeshObject::SetAnimationState(const char *animation_id, float frame, bool l if (loop && frame > pAnimation->length) { + // if animation has length 10 and we request loop frame 11, we will get frame 1 + int iframe = int (frame), mframe = iframe % pAnimation->length; frame = mframe + (frame - iframe); } - std::map tranforms = GetBoneTransformations(this, pAnimation, frame, loop); + std::map tranforms = GetBoneTransformations (this, pAnimation, frame, loop); ApplyBoneTransformations(tranforms); } diff --git a/src/test3d/mesh.h b/src/test3d/mesh.h index 7466310..4aee52b 100644 --- a/src/test3d/mesh.h +++ b/src/test3d/mesh.h @@ -36,44 +36,72 @@ struct MeshVertex { }; typedef const MeshVertex *PMeshVertex; +/* + Describe's a bone in a mesh's armature. (if any) + */ struct MeshBone { - std::string parent_id; - MeshBone *parent; + std::string parent_id; // can be empty, if no parent + MeshBone *parent; // can be NULL if no parent + + // Two vectors to represent the rest position of a bone: vec3 posHead, posTail; + + // Haven't used this much: float weight; + // Vertices that this bone pulls at. std::list vertex_ids; }; -// MeshBoneState is mainly for debugging. +/* + MeshBoneState was added mainly for debugging. + One can register the current position and rotation + of a bone in it. + */ struct MeshBoneState { vec3 posHead, posTail; }; +/* + MeshBoneKey is a keyframe for one bone in an animation. + */ struct MeshBoneKey { Quaternion rot; vec3 loc; }; +/* + MeshBoneLayer describes the motion of a bone over time + in an animation. + */ struct MeshBoneLayer { + // Only one bone is modified by a layer object: std::string bone_id; const MeshBone *pBone; + + // the map keys are frame numbers, values are keyframes std::map keys; }; +/* + Only one animation can be active at the time per mesh. + The animation hass all the information needed to calculate + the positions of all bones in a mesh at a given time. + */ struct MeshAnimation { - int length; + int length; // max length ov animation in time. + std::list layers; }; -// MeshTexel represents a position of a texture +// MeshTexel represents a position on a texture struct MeshTexel { GLfloat u, v; @@ -81,11 +109,18 @@ struct MeshTexel { typedef char *charp; +/* + A MeshFace can be a triangle or quad, depending + on the number of vertices + */ template class MeshFace { private: + + // For some reason, std::string here wouldn't compile charp vertex_ids [N]; + public: MeshTexel texels [N]; @@ -122,7 +157,7 @@ class MeshFace { } void SetVertexID (const int i, const std::string &id) { - SetVertexID(i, id.c_str()); + SetVertexID (i, id.c_str()); } const char *GetVertexID (const int i) const { @@ -136,6 +171,11 @@ typedef MeshFace<4> MeshQuad; typedef const MeshQuad *PMeshQuad; typedef const MeshTriangle *PMeshTriangle; +/* + A subset is a set of faces with color settings and an id attached to it. + A Mesh can consist of more than one subset. + Texture is not included. + */ struct MeshSubset { GLfloat diffuse[4], specular[4], emission[4]; @@ -149,6 +189,7 @@ struct MeshSubset { // MeshData represents whats in the mesh file, can be rendered directly using RenderUnAnimatedSubset. struct MeshData { + // These maps have the xml tags' id values as keys std::map bones; std::map vertices; std::map triangles; @@ -162,25 +203,44 @@ void RenderUnAnimatedSubset (const MeshData *, const std::string &subset_id); // ToTriangles is useful for collision detection void ToTriangles (const MeshData *, Triangle **triangles, size_t *n_triangles); +/** + * Converts a given xml document to a mesh. + * :returns: true if all data was found and meshdata is valid, false otherwise. + */ bool ParseMesh (const xmlDocPtr, MeshData *pData); // MeshObject uses MeshData, but has an animation state that can be changed. class MeshObject { + private: const MeshData *pMeshData; - std::map vertexStates; + // current animation transformations of vertices and bones: + std::map vertexStates; + std::map boneStates; + + void InitStates (); // Resets all vertices and bones to default state + + // One matrix per bone id. Matrices are applied to bone states and vertex states. + void ApplyBoneTransformations (const std::map transforms); - void InitStates(); - void ApplyBoneTransformations(const std::map transforms); public: - std::map boneStates; + + // Render subset in OpenGL, could precede this with desired GL settings void RenderSubset (const std::string &subset_id); + + // These render functions can be usefull for debugging void RenderBones (); void RenderNormals (); - void SetAnimationState(const char *animation_id, float frame, bool loop = true); + /** + * Puts mesh in desired animation frame. + * :param animation_id: name of animation in xml file, or NULL for initial state. + */ + void SetAnimationState (const char *animation_id, float frame, bool loop = true); + + // Functions to manipulate bones directly: void SetBoneAngle(const char *bone_id, const vec3 *axis, const float angle); void SetBoneLoc(const char *bone_id, const vec3 *loc); diff --git a/src/test3d/shadow.cpp b/src/test3d/shadow.cpp index a427444..efd4065 100644 --- a/src/test3d/shadow.cpp +++ b/src/test3d/shadow.cpp @@ -65,11 +65,16 @@ ShadowScene::ShadowScene(App *pApp) : Scene(pApp), { texDummy.tex = texBox.tex = texSky.tex = texPar.tex = 0; - //colliders [0] = new FeetCollider (vec3 (0, -2.0f, 0)); + + // colliders [0] = new FeetCollider (vec3 (0, -2.0f, 0)); + + /* + Decided not to use the feet collider here. it's too buggy for onGround tests. + */ colliders [0] = new SphereCollider (vec3 (0, -1.999f, 0), 0.001f); colliders [1] = new SphereCollider (VEC_O, PLAYER_RADIUS); - // Place particles at random positions with random speeds + // Place particles at random positions with random speeds and sizes for (int i = 0; i < N_PARTICLES; i++) { float a = RandomFloat (0, M_PI * 2), @@ -101,12 +106,19 @@ ShadowScene::~ShadowScene() bool ShadowScene::Init(void) { + /* + Load and parse all resources. + On failure, set the error message and return false. + */ + std::string resPath = std::string(SDL_GetBasePath()) + "test3d.zip"; SDL_RWops *f; bool success; xmlDocPtr pDoc; + // Load dummy texture: + f = SDL_RWFromZipArchive (resPath.c_str(), "dummy.png"); if (!f) // file or archive missing return false; @@ -120,6 +132,8 @@ bool ShadowScene::Init(void) return false; } + // Load dummy mesh as xml document: + f = SDL_RWFromZipArchive (resPath.c_str(), "dummy.xml"); if (!f) return false; @@ -132,6 +146,8 @@ bool ShadowScene::Init(void) return false; } + // Convert xml document to mesh object: + success = ParseMesh(pDoc, &meshDataDummy); xmlFreeDoc (pDoc); @@ -141,6 +157,8 @@ bool ShadowScene::Init(void) return false; } + // Load environment texture: + f = SDL_RWFromZipArchive (resPath.c_str(), "box.png"); if (!f) return false; @@ -153,6 +171,8 @@ bool ShadowScene::Init(void) return false; } + // Load environment mesh as xml: + f = SDL_RWFromZipArchive (resPath.c_str(), "box.xml"); if (!f) return false; @@ -165,6 +185,8 @@ bool ShadowScene::Init(void) return false; } + // Convert xml to mesh: + success = ParseMesh(pDoc, &meshDataBox); xmlFreeDoc (pDoc); @@ -174,6 +196,8 @@ bool ShadowScene::Init(void) return false; } + // Load skybox texture: + f = SDL_RWFromZipArchive (resPath.c_str(), "sky.png"); if (!f) return false; @@ -186,6 +210,8 @@ bool ShadowScene::Init(void) return false; } + // Load skybox mesh as xml: + f = SDL_RWFromZipArchive (resPath.c_str(), "sky.xml"); if (!f) return false; @@ -198,6 +224,8 @@ bool ShadowScene::Init(void) return false; } + // convert xml to mesh: + success = ParseMesh(pDoc, &meshDataSky); xmlFreeDoc (pDoc); @@ -207,6 +235,8 @@ bool ShadowScene::Init(void) return false; } + // Load texture for the particles: + f = SDL_RWFromZipArchive (resPath.c_str(), "particle.png"); if (!f) return false; @@ -219,6 +249,8 @@ bool ShadowScene::Init(void) return false; } + // Derive extra objects from the data we just loaded: + shadowTriangles = new STriangle[meshDataDummy.triangles.size() + 2 * meshDataDummy.quads.size()]; pMeshDummy = new MeshObject (&meshDataDummy); @@ -231,6 +263,7 @@ bool ShadowScene::Init(void) return true; } +/* float toAngle(const float x, const float z) { if (x == 0) @@ -249,6 +282,8 @@ float toAngle(const float x, const float z) return M_PI - atan (z / x); } } +*/ + void getCameraPosition( const vec3 ¢er, const GLfloat angleX, const GLfloat angleY, const GLfloat dist, vec3 &posCamera) @@ -310,25 +345,30 @@ void ShadowScene::Update(const float dt) newPosPlayer.y += vy * dt; + /* + If on the ground, the collision mechanics are a little different. + We try to keep the feet on the ground. Unless the fall is too long + */ if (onGround) + posPlayer = CollisionWalk (posPlayer, newPosPlayer, colliders, N_PLAYER_COLLIDERS, collision_triangles, n_collision_triangles); else posPlayer = CollisionMove (posPlayer, newPosPlayer, colliders, N_PLAYER_COLLIDERS, collision_triangles, n_collision_triangles); // animate the mesh: - if (onGround && forward) + if (onGround && forward) // walking / running animation { frame += 15 * dt; pMeshDummy->SetAnimationState ("run", frame); touchDown = false; } - else if (vy > 0) // going up + else if (vy > 0) // going up animation { frame = 1.0f; pMeshDummy->SetAnimationState ("jump", frame); } - else if (!onGround) // falling + else if (!onGround) // falling animation { frame += 30 * dt; if (frame > 10.0f) @@ -342,15 +382,17 @@ void ShadowScene::Update(const float dt) if (frame > 20.0f) touchDown = false; - if (touchDown) + if (touchDown) // touchdown animation pMeshDummy->SetAnimationState ("jump", frame); - else + + else // stationary animation + pMeshDummy->SetAnimationState (NULL, 0); } - // Move the particles + // Move the particles, according to time passed. for (int i = 0; i < N_PARTICLES; i++) { particles [i].vel += -0.03f * particles [i].pos * dt; @@ -375,6 +417,11 @@ int GetTriangles(const MeshObject *pMesh, STriangle *triangles) { const MeshQuad *pQuad = &it->second; + /* + Converting quads to triangles might not be exactly right, + unless the quads are completely flat of coarse. + */ + triangles[i].p[0] = &vertices.at (pQuad->GetVertexID(0)).p; triangles[i].p[1] = &vertices.at (pQuad->GetVertexID(1)).p; triangles[i].p[2] = &vertices.at (pQuad->GetVertexID(2)).p; @@ -410,85 +457,117 @@ void SetVisibilities(const vec3 &posEye, const int n_triangles, STriangle *trian { STriangle* t = &triangles[i]; - // assume clockwise is front - vec3 n = Cross((*t->p[1] - *t->p[0]), (*t->p[2] - *t->p[0])).Unit(); - float d = -Dot(*t->p[0], n), + /* + Calculate normal from triangle, + assume clockwise is front. + */ + + vec3 n = Cross ((*t->p[1] - *t->p[0]), (*t->p[2] - *t->p[0])).Unit(); + + float d = -Dot (*t->p[0], n), // plane's distance from origin + side = Dot(n, posEye) + d; + // If the normal points towards the eye, then the triangle is visible. + t->visible = (side > 0); } } struct Edge { const vec3 *p[2]; }; + /** + * Determines which triangle Edges should make up the shadow. * Precondition is that the 'visible' fields have been set on the triangles beforehand !! */ -void GetShadowEdges(const int n_triangles, const STriangle *triangles, std::list &edges) +void GetShadowEdges(const int n_triangles, const STriangle *triangles, std::list &result) { // We're looking for edges that are not between two visible triangles, // but ARE part of a visible triangle - bool bUnshared[3]; // 0 between 0 and 1, 1 between 1 and 2, 2 between 2 and 0 - int shared[2]; + /* + one bool per edge of the triangle with points (0, 1, 2), + 0 between triangle point 0 and 1, + 1 between 1 and 2 + and 2 between 2 and 0 + */ + bool use_edge [3]; + int i, j, x, y, n, x2, y2; for (i = 0; i < n_triangles; i++) { + // no need to check the edges of invisible triangles: if (!triangles[i].visible) continue; + /* + Include all edges by default. + Iteration must rule out edges that are shared by two visible triangles. + */ for (x = 0; x < 3; x++) - bUnshared[x] = true; + use_edge [x] = true; + /* + Iterate to find visible triangles (j) + that share edges with visible triangles. (i) + */ for (j = 0; j < n_triangles; j++) { - // make sure not to compare triangles with themselves + /* + Make sure not to compare triangles with themselves + and be sure to skip other invisible triangles. + */ - if (i == j || !triangles[j].visible) + if (i == j || !triangles [j].visible) continue; // compare the three edges of both triangles with each other: for (x = 0; x < 3; x++) // iterate over the edges of triangle i { - x2 = (x + 1) % 3; + x2 = (x + 1) % 3; // second point of edge x for (y = 0; y < 3; y++) // iterate over the edges of triangle j { - y2 = (y + 1) % 3; + y2 = (y + 1) % 3; // second point of edge y if (triangles[i].p[x] == triangles[j].p[y] && triangles[i].p[x2] == triangles[j].p[y2] || triangles[i].p[x2] == triangles[j].p[y] && triangles[i].p[x] == triangles[j].p[y2]) { // edge x on triangle i is equal to edge y on triangle j - bUnshared[x] = false; + use_edge [x] = false; break; } } } - // We found shared visible edges on triangle j for all three edges on triangle i - if (!bUnshared[0] && !bUnshared[1] && !bUnshared[2]) + /* + If all three edges (0, 1, 2) of triangle (i) were found shared with other triangles, + then search no more other triangles (j) for this particular triangle (i). + */ + if (!use_edge [0] && !use_edge [1] && !use_edge [2]) break; } + // Add the edges (x) of triangle (i) that were found needed: for (x = 0; x < 3; x++) { - if (bUnshared[x]) + if (use_edge [x]) { - x2 = (x + 1) % 3; + x2 = (x + 1) % 3; // second point of edge x Edge edge; - edge.p[0] = triangles[i].p[x]; - edge.p[1] = triangles[i].p[x2]; - edges.push_back(edge); + edge.p [0] = triangles [i].p [x]; + edge.p [1] = triangles [i].p [x2]; + result.push_back (edge); } } } } -/* +/** * Renders squares from every edge to infinity, using the light position as origin. */ #define SHADOW_INFINITY 1000.0f @@ -497,6 +576,8 @@ void ShadowPass(const std::list &edges, const vec3 &posLight) for (std::list::const_iterator it = edges.begin(); it != edges.end(); it++) { const Edge *pEdge = &(*it); + + // These are the four vertices of the shadow quad: vec3 d1 = (*pEdge->p[0] - posLight).Unit(), d2 = (*pEdge->p[1] - posLight).Unit(), @@ -512,14 +593,18 @@ void ShadowPass(const std::list &edges, const vec3 &posLight) } } void RenderSprite (const vec3 &pos, const Texture *pTex, - const float tx1, const float ty1, const float tx2, const float ty2, + const float tx1, const float ty1, const float tx2, const float ty2, // texture coordinates (in pixels) const float size = 1.0f) { matrix4 modelView; glGetFloatv (GL_MODELVIEW_MATRIX, modelView.m); - /* extract the axes system from the current modelview matrix and normalize it, - then render the quad on it, so that it's always pointed towards the camera */ + /* + Extract the axes system from the current modelview matrix and normalize them, + then render the quad on the normalized axes system, so that it's always pointed towards the camera, + but it's zoom is still variable. + */ + const float sz = size / 2; vec3 modelViewRight = vec3 (modelView.m11, modelView.m12, modelView.m13).Unit(), modelViewUp = vec3 (modelView.m21, modelView.m22, modelView.m23).Unit(), @@ -629,7 +714,6 @@ void ShadowScene::Render () glDisable (GL_BLEND); glDisable (GL_TEXTURE_2D); - // For testing collision triangle generation if (show_triangles) { glDisable(GL_DEPTH_TEST); @@ -673,6 +757,8 @@ void ShadowScene::Render () pMeshDummy->RenderNormals (); } + // We need to have the light position in player space, because that's where the triangles are. + const vec3 invPosLight = matInverse (transformPlayer) * posLight; const int n_triangles = GetTriangles (pMeshDummy, shadowTriangles); SetVisibilities (invPosLight, n_triangles, shadowTriangles); @@ -696,14 +782,14 @@ void ShadowScene::Render () glStencilOp (GL_KEEP, GL_KEEP, GL_DECR); ShadowPass (edges, invPosLight); - // Set the stencil pixels to zero where the dummy is drawn and where nothing is drawn + // Set the stencil pixels to zero where the dummy is drawn and where nothing is drawn (background) glFrontFace (GL_CW); glStencilOp (GL_KEEP, GL_KEEP, GL_ZERO); pMeshDummy->RenderSubset ("0"); glLoadIdentity (); - // Set every pixel behind the walls/floor to zero + // Set every stencil pixel behind the walls/floor to zero glBegin (GL_QUADS); glVertex3f (-1.0e+15f, 1.0e+15f, 0.9f * SHADOW_INFINITY); glVertex3f ( 1.0e+15f, 1.0e+15f, 0.9f * SHADOW_INFINITY); @@ -729,7 +815,10 @@ void ShadowScene::Render () glVertex3f (-1.0f,-1.0f, NEAR_VIEW + 0.1f); glEnd (); - // Render the particles over the shadows + /* + Render the particles after the shadows, because they're transparent + and that doesn't go well with the depth buffer. + */ glColor4f (1.0f, 1.0f, 1.0f, 1.0f); glLoadMatrixf (matCameraView.m); @@ -745,14 +834,14 @@ void ShadowScene::Render () } void ShadowScene::OnMouseWheel (const SDL_MouseWheelEvent *event) { - // zoom in or out + // zoom in or out, but not closer than 0.5 distCamera -= 0.3f * event->y; if (distCamera < 0.5f) distCamera = 0.5f; } -void ShadowScene::OnKeyPress(const SDL_KeyboardEvent *event) +void ShadowScene::OnKeyPress (const SDL_KeyboardEvent *event) { if (event->type == SDL_KEYDOWN) { @@ -761,6 +850,10 @@ void ShadowScene::OnKeyPress(const SDL_KeyboardEvent *event) // jump vy = 15.0f; } + + /* + The following keys toggle settings: + */ else if (event->keysym.sym == SDLK_b) { show_bones = !show_bones; @@ -779,7 +872,7 @@ void ShadowScene::OnMouseMove(const SDL_MouseMotionEvent *event) { if(event -> state & SDL_BUTTON_LMASK) { - // Change camera angles + // Change camera angles, if mouse key is pressed angleY += 0.01f * event -> xrel; angleX += 0.01f * event -> yrel; diff --git a/src/test3d/shadow.h b/src/test3d/shadow.h index beb48f7..d041496 100644 --- a/src/test3d/shadow.h +++ b/src/test3d/shadow.h @@ -36,6 +36,7 @@ struct Particle }; struct STriangle +/* Triangle format specially designed for calculating shadows */ { const vec3 *p[3]; bool visible; @@ -61,25 +62,38 @@ class ShadowScene : public App::Scene vec3 posPlayer, directionPlayer; - Texture texDummy, - texBox, - texSky, - texPar; + Texture texDummy, // texture of the character mesh + texBox, // texture of the walls and floor + texSky, // texture for the sky + texPar; // semi-transparent particle texture - float frame; - bool show_bones, show_normals, show_triangles; + float frame; // current animation frame - STriangle *shadowTriangles; + // flags that represent the current settings for the scene: + bool show_bones, + show_normals, + show_triangles; + STriangle *shadowTriangles; // number of triangles depends on mesh used + + // Objects to keep track of the state of each particle in the scene Particle particles [N_PARTICLES]; + // Objects to represent the collision boundaries around the player ColliderP colliders [N_PLAYER_COLLIDERS]; + /* + These triangles represent the environment. + They're used for collision detection. + */ size_t n_collision_triangles; Triangle *collision_triangles; - MeshData meshDataDummy, meshDataBox, meshDataSky; - MeshObject *pMeshDummy; + MeshData meshDataDummy, // the movable character, with bones and animation data + meshDataBox, // environment: ground and walls + meshDataSky; // skybox, half a sphere + + MeshObject *pMeshDummy; // animatable object, representing the player public: ShadowScene (App*); diff --git a/src/test3d/toon.cpp b/src/test3d/toon.cpp index fe7dd24..cb205fb 100644 --- a/src/test3d/toon.cpp +++ b/src/test3d/toon.cpp @@ -107,7 +107,7 @@ bool ToonScene::Init () return false; } - shaderProgram = createShaderProgram (sourceV, sourceF); + shaderProgram = CreateShaderProgram (sourceV, sourceF); if (!shaderProgram) { SetError ("error creating shader program from toon.vsh and toon.fsh: %s", GetError ()); diff --git a/src/test3d/toon.h b/src/test3d/toon.h index 15bb600..1202e3c 100644 --- a/src/test3d/toon.h +++ b/src/test3d/toon.h @@ -24,6 +24,12 @@ #include "mesh.h" #include "../texture.h" +/* + Demonstrates: + - toon shader + - black lines around meshes + + */ class ToonScene : public App::Scene { private: diff --git a/src/test3d/water.cpp b/src/test3d/water.cpp index 0268045..d405410 100644 --- a/src/test3d/water.cpp +++ b/src/test3d/water.cpp @@ -36,7 +36,7 @@ #define NEAR_VIEW 0.1f #define FAR_VIEW 1000.0f -WaterScene::WaterScene(App *pApp) : Scene(pApp), +WaterScene::WaterScene (App *pApp) : Scene(pApp), fbReflection(0), cbReflection(0), dbReflection(0), texReflection(0), fbRefraction(0), cbRefraction(0), dbRefraction(0), texRefraction(0), @@ -76,28 +76,34 @@ bool WaterScene::Init() pathFSH = shaderDir + "water.fsh", sourceV = "", sourceF = ""; - glGenFramebuffers(1, &fbReflection); - glGenFramebuffers(1, &fbRefraction); + /* + Build two framebuffers for reflection and refraction. + Each framebuffer gets a color and depth buffer. - glGenRenderbuffers(1, &cbReflection); - glGenRenderbuffers(1, &cbRefraction); - glGenRenderbuffers(1, &dbReflection); - glGenRenderbuffers(1, &dbRefraction); + Also buid two textures, to which the frame buffer's pixel data + will be stored. + */ - glGenTextures(1, &texReflection); - glGenTextures(1, &texRefraction); + glGenFramebuffers (1, &fbReflection); + glGenFramebuffers (1, &fbRefraction); - glBindFramebuffer(GL_FRAMEBUFFER, fbReflection); + glGenRenderbuffers (1, &cbReflection); + glGenRenderbuffers (1, &cbRefraction); + glGenRenderbuffers (1, &dbReflection); + glGenRenderbuffers (1, &dbRefraction); - glBindRenderbuffer(GL_RENDERBUFFER, cbReflection); + glGenTextures (1, &texReflection); + glGenTextures (1, &texRefraction); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, w, h); + glBindFramebuffer (GL_FRAMEBUFFER, fbReflection); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, cbReflection); + glBindRenderbuffer (GL_RENDERBUFFER, cbReflection); + glRenderbufferStorage (GL_RENDERBUFFER, GL_RGBA, w, h); + glFramebufferRenderbuffer (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, cbReflection); glBindRenderbuffer(GL_RENDERBUFFER, dbReflection); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, dbReflection); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, dbReflection); glBindTexture(GL_TEXTURE_2D, texReflection); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA,GL_UNSIGNED_BYTE, NULL); @@ -118,22 +124,22 @@ bool WaterScene::Init() return false; } - glBindFramebuffer(GL_FRAMEBUFFER, fbRefraction); + glBindFramebuffer (GL_FRAMEBUFFER, fbRefraction); - glBindRenderbuffer(GL_RENDERBUFFER, cbRefraction); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, w, h); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, cbRefraction); + glBindRenderbuffer (GL_RENDERBUFFER, cbRefraction); + glRenderbufferStorage (GL_RENDERBUFFER, GL_RGBA, w, h); + glFramebufferRenderbuffer (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, cbRefraction); - glBindRenderbuffer(GL_RENDERBUFFER, dbRefraction); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, dbRefraction); + glBindRenderbuffer (GL_RENDERBUFFER, dbRefraction); + glRenderbufferStorage (GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h); + glFramebufferRenderbuffer (GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, dbRefraction); - glBindTexture(GL_TEXTURE_2D, texRefraction); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glBindTexture (GL_TEXTURE_2D, texRefraction); + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texRefraction, 0); @@ -150,6 +156,7 @@ bool WaterScene::Init() SDL_RWops *f; bool success; + // Read vertex shader source from archive: f = SDL_RWFromZipArchive (resourceZip.c_str(), pathVSH.c_str()); if (!f) return false; // file or archive missing @@ -163,9 +170,11 @@ bool WaterScene::Init() return false; } + // Read fragment shader source from archive: f = SDL_RWFromZipArchive (resourceZip.c_str(), pathFSH.c_str()); if (!f) return false; + success = ReadAll (f, sourceF); f->close(f); @@ -175,7 +184,8 @@ bool WaterScene::Init() return false; } - shaderProgramWater = createShaderProgram (sourceV, sourceF); + // Create shader program: + shaderProgramWater = CreateShaderProgram (sourceV, sourceF); if (!shaderProgramWater) { SetError ("error creating shader program from %s and %s: %s", pathVSH.c_str(), pathFSH.c_str(), GetError ()); @@ -203,33 +213,41 @@ void WaterScene::UpdateWaterNormals() { // Recalculate normals, depending on the edges around the vertices. + int x,z,x1,z1,x2,z2; vec3 p0,p1,p2; - for(int x=0; x= GRIDSIZE) + { + x1--; + x2--; + } + + if (z2 >= GRIDSIZE) + { + z1--; + z2--; + } + + p0 = gridpoints [x1][z1]; + p1 = gridpoints [x2][z1]; + p2 = gridpoints [x1][z2]; + + // Normal must point up, perpendicular to the two edges + + gridnormals [x][z] = Cross (p2 - p0, p1 - p0).Unit (); } } - for(int x=0; x DROP_INTERVAL) @@ -358,14 +387,16 @@ void WaterScene::Update(float dt) MakeWave (vec3(x, y, z), l); } - UpdateWater (dt < 0.02f ? dt : 0.02f); + // Make the waves move: + UpdateWater (dt < 0.02f ? dt : 0.02f); const Uint8 *state = SDL_GetKeyboardState (NULL); if (state [SDL_SCANCODE_UP] || state [SDL_SCANCODE_DOWN] || state [SDL_SCANCODE_LEFT] || state [SDL_SCANCODE_RIGHT] || state [SDL_SCANCODE_PAGEDOWN] || state [SDL_SCANCODE_PAGEUP]) { + // Get camera position for chosen angles and distance: vec3 posCamera; getCameraPosition (angleX, angleY, distCamera, posCamera); @@ -394,7 +425,7 @@ void WaterScene::Update(float dt) my = 0.0f; vec3 camZ = (-posCamera); camZ.y = 0; camZ = camZ.Unit(); - vec3 camX = Cross(VEC_UP, camZ); + vec3 camX = Cross (VEC_UP, camZ); posCube += mx * camX - mz * camZ; posCube.y += my; } @@ -435,9 +466,13 @@ void WaterScene::RenderWater() glEnd(); } + +/** + * Renders a square 40x40 plane at the origin, with normal pointing up. + */ void RenderPlane() { - GLfloat size=20.0f; + GLfloat size = 20.0f; glBegin(GL_QUADS); @@ -449,9 +484,12 @@ void RenderPlane() glEnd(); } +/** + * Renders a 2x2 cube at the origin. + */ void RenderCube() { - GLfloat size=1.0f; + GLfloat size = 1.0f; glBegin(GL_QUADS); @@ -510,6 +548,7 @@ const GLfloat colorCube[] = {0.0f, 1.0f, 0.0f, 1.0f}, void WaterScene::Render() { + GLint texLoc; int w, h; SDL_GL_GetDrawableSize (pApp->GetMainWindow (), &w, &h); @@ -544,12 +583,12 @@ void WaterScene::Render() glBindFramebuffer(GL_FRAMEBUFFER, fbRefraction); glViewport(0, 0, w, h); - // Clear the buffer with water color and minimum depth - glClearDepth(0.0f); + // Clear the refraction buffer with water color and minimum depth + glClearDepth (0.0f); glClearColor(bgcolorRefract[0], bgcolorRefract[1], bgcolorRefract[2], bgcolorRefract[3]); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glDisable(GL_CULL_FACE); + glEnable(GL_CULL_FACE); // Position the light in normal space glLightfv(GL_LIGHT0, GL_POSITION, posLight); @@ -557,12 +596,14 @@ void WaterScene::Render() // Make sure anything above the water isn't drawn glDepthFunc(GL_GREATER); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + + // Render an invisible depth plane at the water level RenderPlane(); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDisable(GL_COLOR_MATERIAL); glTranslatef(posCube.x, posCube.y, posCube.z); - glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, colorCube); + glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, colorCubeReflect); RenderCube(); glTranslatef(-posCube.x, -posCube.y, -posCube.z); @@ -573,7 +614,7 @@ void WaterScene::Render() glBindFramebuffer(GL_FRAMEBUFFER, fbReflection); glViewport(0, 0, w, h); - // Clear the buffer with water color and minimum depth + // Clear the reflection buffer with water color and minimum depth glClearDepth(0.0f); glClearColor(bgcolorReflect[0], bgcolorReflect[1], bgcolorReflect[2], bgcolorReflect[3]); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -584,6 +625,8 @@ void WaterScene::Render() // Make sure anything below the water isn't drawn glDepthFunc(GL_GEQUAL); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + + // Render an invisible depth plane at the water level RenderPlane(); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); @@ -604,16 +647,17 @@ void WaterScene::Render() glDepthFunc(GL_LEQUAL); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, texReflection); glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, texRefraction); - glEnable(GL_TEXTURE_2D); + // Bind reflection to texture 0 and refraction to texture 1 + glActiveTexture (GL_TEXTURE0); + glBindTexture (GL_TEXTURE_2D, texReflection); - glEnable(GL_CULL_FACE); - glCullFace(GL_FRONT); + glActiveTexture (GL_TEXTURE1); + glBindTexture (GL_TEXTURE_2D, texRefraction); + + glEnable (GL_CULL_FACE); + glCullFace (GL_FRONT); // clear the screen with black color and max depth glClearDepth (1.0f); @@ -622,6 +666,14 @@ void WaterScene::Render() // Render reflection and refraction to the water glUseProgram(shaderProgramWater); + + // Tell the shader to use textures GL_TEXTURE0 and GL_TEXTURE1 + texLoc = glGetUniformLocation (shaderProgramWater, "tex_reflect"); + glUniform1i (texLoc, 0); + + texLoc = glGetUniformLocation (shaderProgramWater, "tex_refract"); + glUniform1i (texLoc, 1); + RenderWater(); glUseProgram(0); diff --git a/src/test3d/water.h b/src/test3d/water.h index 522f29d..43c31f0 100644 --- a/src/test3d/water.h +++ b/src/test3d/water.h @@ -39,24 +39,35 @@ class WaterScene : public App::Scene // camera angles GLfloat angleY, angleX, distCamera; + // position of the cube vec3 posCube; + // time-dependent variables for vertical movement of the water grid: float gridforces [GRIDSIZE][GRIDSIZE], gridspeeds [GRIDSIZE][GRIDSIZE]; + // positions and normals of the water grid vertices: vec3 gridpoints [GRIDSIZE][GRIDSIZE], gridnormals [GRIDSIZE][GRIDSIZE]; + // Frame color/depth buffers to render to and textures to derive from them: GLuint fbReflection, cbReflection, dbReflection, texReflection, fbRefraction, cbRefraction, dbRefraction, texRefraction, + + // handle to shader program shaderProgramWater; - void UpdateWater(const float dt); - void UpdateWaterNormals(void); - void MakeWave(const vec3 p, const float l); + // Moves water grid points in time: + void UpdateWater (const float dt); + + // Re-calculates normals for water grid points: + void UpdateWaterNormals (void); + + // Create a wave at point p with length l + void MakeWave (const vec3 p, const float l); - void RenderWater(); + void RenderWater (); public: WaterScene (App*); diff --git a/src/texture.cpp b/src/texture.cpp index 4c7a175..78ae8db 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -26,11 +26,16 @@ void PNGReadCallback (png_structp png_ptr, png_bytep data, png_size_t length) { - png_voidp pio = png_get_io_ptr(png_ptr); - SDL_RWops *io = (SDL_RWops *) pio; + /* + Image data comes from an SDL_RWops input stream. + This callback function gets the requested bytes from it. + */ + + png_voidp pio = png_get_io_ptr (png_ptr); + SDL_RWops *io = (SDL_RWops *)pio; size_t n; - if((n = io->read(io, data, 1, length)) < length) + if ((n = io->read (io, data, 1, length)) < length) { SetError ("%d bytes requested, only %d read", length, n); @@ -39,15 +44,28 @@ void PNGReadCallback (png_structp png_ptr, png_bytep data, png_size_t length) } void PNGErrorCallback(png_structp png_ptr, png_const_charp msg) { + /* + PNG errors must be copied to the error string, + not just stderr. + */ + SetError ("Error from png: %s", (const char *)msg); longjmp (png_jmpbuf (png_ptr), PNG_ERROR); } void PNGWarningCallback(png_structp png_ptr, png_const_charp msg) { + // Not decided what to do with warnings yet. } -bool LoadPNG(SDL_RWops *io, Texture *pTex) +bool LoadPNG (SDL_RWops *io, Texture *pTex) { + /* + We use libpng to read in a png file. + + For further information: + http://www.libpng.org/pub/png/libpng-1.2.5-manual.html + */ + png_info* info_ptr = 0, *end_info = 0; png_struct* png_ptr = 0; png_bytep *row_pointers = 0; @@ -57,29 +75,29 @@ bool LoadPNG(SDL_RWops *io, Texture *pTex) png_byte num_channels; // make a read structure: - png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)NULL, NULL, NULL); - if(!png_ptr) + png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, (png_voidp)NULL, NULL, NULL); + if (!png_ptr) { SetError ("png: error creating read struct"); goto pngload_failure; } // info and end_info structures are also neccesary: - info_ptr=png_create_info_struct(png_ptr); - if(!info_ptr) + info_ptr=png_create_info_struct (png_ptr); + if (!info_ptr) { SetError ("png: error creating info struct"); goto pngload_failure; } - - end_info = png_create_info_struct(png_ptr); - if(!end_info) + end_info = png_create_info_struct (png_ptr); + if (!end_info) { SetError ("png: error creating end info"); goto pngload_failure; } - png_set_error_fn(png_ptr, png_get_error_ptr (png_ptr), PNGErrorCallback, PNGWarningCallback); + // Catch libPNG's errors and warnings: + png_set_error_fn (png_ptr, png_get_error_ptr (png_ptr), PNGErrorCallback, PNGWarningCallback); // if an error occurs, jump to the failure section: if (setjmp (png_jmpbuf (png_ptr))) @@ -87,55 +105,58 @@ bool LoadPNG(SDL_RWops *io, Texture *pTex) goto pngload_failure; } - // read info from the header: - png_set_read_fn(png_ptr,(png_voidp*)io, PNGReadCallback); + // Tell libpng that it must take its data from a callback function: + png_set_read_fn (png_ptr, (png_voidp *)io, PNGReadCallback); - png_read_info(png_ptr, info_ptr); + // read info from the header: + png_read_info (png_ptr, info_ptr); - // get neccesary information: + // Get info about the data in the image: png_uint_32 png_width, png_height; int bit_depth, color_type, interlace_type; - png_get_IHDR(png_ptr, info_ptr, &png_width, &png_height, &bit_depth, &color_type, &interlace_type, NULL, NULL); - - png_read_update_info(png_ptr, info_ptr); + png_get_IHDR (png_ptr, info_ptr, &png_width, &png_height, &bit_depth, &color_type, &interlace_type, NULL, NULL); // how many channels does the image have? - num_channels=png_get_channels(png_ptr, info_ptr); + num_channels = png_get_channels(png_ptr, info_ptr); - // We want RGB colors: - if(color_type==PNG_COLOR_TYPE_PALETTE) + // We want RGB colors, no palette: + if (color_type == PNG_COLOR_TYPE_PALETTE) { - png_set_palette_to_rgb(png_ptr); - color_type=PNG_COLOR_TYPE_RGB; + png_set_palette_to_rgb (png_ptr); + color_type = PNG_COLOR_TYPE_RGB; } // make sure the pixel depth is 8: - if(bit_depth<8) + if (bit_depth < 8) { - png_set_packing(png_ptr); - bit_depth=8; + png_set_packing (png_ptr); + bit_depth = 8; } - else if(bit_depth==16) + else if (bit_depth == 16) { - png_set_strip_16(png_ptr); - bit_depth=8; + png_set_strip_16 (png_ptr); + bit_depth = 8; } - // handle tRNS - if( png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) - png_set_tRNS_to_alpha(png_ptr); + // Make sure the transparency is set to an alpha channel, not tRNS: + if (png_get_valid (png_ptr, info_ptr, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha (png_ptr); + + // Handle expansion of interlaced image: + number_of_passes = png_set_interlace_handling (png_ptr); - number_of_passes = png_set_interlace_handling(png_ptr); + // Update png_info structure after setting transformations: + png_read_update_info (png_ptr, info_ptr); // get bytes per row: - rowbytes = png_get_rowbytes(png_ptr, info_ptr); + rowbytes = png_get_rowbytes (png_ptr, info_ptr); // allocate memory to store pixel data in: - png_data = (png_bytep) png_malloc(png_ptr, png_height * rowbytes); - if(!png_data) + png_data = (png_bytep) png_malloc (png_ptr, png_height * rowbytes); + if (!png_data) { SetError ("png_malloc failed"); goto pngload_failure; @@ -144,48 +165,61 @@ bool LoadPNG(SDL_RWops *io, Texture *pTex) // make row pointers for reading: row_pointers = new png_bytep[png_height]; - // place the pointers correctly: - for(png_uint_32 i=0; itex = tex; @@ -196,12 +230,12 @@ bool LoadPNG(SDL_RWops *io, Texture *pTex) png_data = 0; // clean up (also frees whatever was allocated with 'png_malloc'): - png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); - png_ptr=0; - info_ptr=0; - end_info=0; + png_destroy_read_struct (&png_ptr, &info_ptr, &end_info); + png_ptr = 0; + info_ptr = 0; + end_info = 0; - glBindTexture(GL_TEXTURE_2D, 0); + glBindTexture (GL_TEXTURE_2D, 0); } else goto pngload_failure; @@ -209,13 +243,11 @@ bool LoadPNG(SDL_RWops *io, Texture *pTex) pngload_failure: // things to do when an error occurs during reading: - glBindTexture(GL_TEXTURE_2D, 0); - - if(png_data) - png_free(png_ptr, png_data); + if (png_data) + png_free (png_ptr, png_data); - delete[] row_pointers; // free allocated row-pointers, if any - png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); // clean up the read structure, info and end_info + delete [] row_pointers; // free allocated row-pointers, if any + png_destroy_read_struct (&png_ptr, &info_ptr, &end_info); // clean up the read structure, info and end_info return false; } diff --git a/src/texture.h b/src/texture.h index b760f85..03d0d6c 100644 --- a/src/texture.h +++ b/src/texture.h @@ -34,6 +34,9 @@ struct Texture { GLsizei w, h; }; -bool LoadPNG(SDL_RWops *, Texture *); +/** + * Loads a png image from SDL_RWops as an OpenGL texture. + */ +bool LoadPNG (SDL_RWops *, Texture *); #endif // TEXTURE_H diff --git a/src/util.cpp b/src/util.cpp index 5c1dba0..4ae9416 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -89,16 +89,15 @@ char GetKeyChar (const SDL_KeyboardEvent *event) else if (sym == SDLK_RETURN) return '\n'; - const char *name = SDL_GetKeyName(sym); + const char *name = SDL_GetKeyName (sym); if (strlen (name) != 1) return NULL; - char c = name[0]; + char c = name [0]; if (mod & KMOD_CAPS) { - // caps lock is on - + // caps lock is on, it converts letters to uppercase: if (isalpha (c)) c = toupper (c); } @@ -106,9 +105,11 @@ char GetKeyChar (const SDL_KeyboardEvent *event) { // shift is down + // shift converts letters to uppercase: if (isalpha (c)) c = toupper (c); + // shift also makes the following conversions: else if (c == '0') return ')'; else if (c == '1') @@ -159,11 +160,11 @@ char GetKeyChar (const SDL_KeyboardEvent *event) return c; } -void Zero(void *p, size_t size) +void Zero (void *p, size_t size) { - char *s = (char*)p; - for(size_t i = 0; i < size; i++) - s[i] = 0; + char *s = (char *)p; + for (size_t i = 0; i < size; i++) + s [i] = 0; } // Make these inline so tha console applications can also include util.cpp void glColorHSV( float h, float s, float v ) @@ -189,10 +190,10 @@ void glColorHSV( float h, float s, float v ) case 5: glColor3f(v, p, q); } } -bool CheckGLOK(const char *doing) +bool CheckGLOK (const char *doing) { GLenum status = glGetError(); - if(status == GL_NO_ERROR) + if (status == GL_NO_ERROR) return true; char errorString [260]; diff --git a/src/util.h b/src/util.h index 2d291f1..80cdec3 100644 --- a/src/util.h +++ b/src/util.h @@ -35,27 +35,62 @@ #include "io.h" +/** + * square function, x must be a number + */ template -T sqr(T x) +T sqr (T x) { return x * x; } +/** + * Swaps the values of n1 and n2 + */ template -void swap(Value& n1, Value& n2) +void swap (Value& n1, Value& n2) { Value r=n1; n1=n2; n2=r ; } +/** + * Converts the SDL key code to an ascii character + * + * Only works for american key board setting. + */ char GetKeyChar (const SDL_KeyboardEvent *event); -bool ReadAll(SDL_RWops *, std::string &out); + +/** + * Reads the entire content of an input source to a string + */ +bool ReadAll (SDL_RWops *, std::string &out); + +/** + * Converts a GL error status to a string. + */ void GLErrorString (char *out, GLenum status); + +/** + * Gives a h,s,v color to OpenGL + */ void glColorHSV (float h, float s, float v); -bool CheckGLOK(const char *doing); + +/** + * Check GL status and sets error string if not OK. + * See err.h + */ +bool CheckGLOK (const char *doing); + +/** + * Sets memory to zeros. + */ void Zero(void *p, size_t size); +/* + Shortcuts to give vec and matrix objects to OpenGL: + */ inline void glVertex2f(const vec2& v) { glVertex2f((GLfloat)v.x,(GLfloat)v.y); diff --git a/src/vec.h b/src/vec.h index 10866fa..22466da 100644 --- a/src/vec.h +++ b/src/vec.h @@ -53,8 +53,10 @@ struct vec3 z = v.z; } + // Length2 is computationally less expensive than Length float Length() const {return sqrt(x*x+y*y+z*z);} float Length2() const {return (x*x+y*y+z*z);} + vec3 Unit() const { float l=Length(); diff --git a/src/xml.cpp b/src/xml.cpp index d6359ab..152759c 100644 --- a/src/xml.cpp +++ b/src/xml.cpp @@ -27,7 +27,7 @@ xmlDocPtr ParseXML(SDL_RWops *io) { const size_t bufsize = 1024; size_t res; - char buf[bufsize]; + char buf [bufsize]; xmlParserCtxtPtr ctxt; xmlDocPtr doc; @@ -40,31 +40,31 @@ xmlDocPtr ParseXML(SDL_RWops *io) } // Create a progressive parsing context - ctxt = xmlCreatePushParserCtxt(NULL, NULL, buf, res, NULL); + ctxt = xmlCreatePushParserCtxt (NULL, NULL, buf, res, NULL); if (! ctxt) { SetError ("Failed to create parser context!"); return NULL; } // loop on the input getting the document data - while ((res = io->read(io, buf, 1, bufsize)) > 0) { + while ((res = io->read (io, buf, 1, bufsize)) > 0) { - xmlParseChunk(ctxt, buf, res, 0); + xmlParseChunk (ctxt, buf, res, 0); } // there is no more input, indicate the parsing is finished. - xmlParseChunk(ctxt, buf, 0, 1); + xmlParseChunk (ctxt, buf, 0, 1); // check if it was well formed doc = ctxt->myDoc; res = ctxt->wellFormed; - xmlFreeParserCtxt(ctxt); + xmlFreeParserCtxt (ctxt); if (!res) { SetError ("xml document is not well formed"); - xmlFreeDoc(doc); + xmlFreeDoc (doc); doc = NULL; } diff --git a/src/xml.h b/src/xml.h index 2633de5..0aeb36d 100644 --- a/src/xml.h +++ b/src/xml.h @@ -24,6 +24,10 @@ #include #include +/** + * Gets data from SDL_RWops input stream and makes libxml + * parse it as a tree. + */ xmlDocPtr ParseXML (SDL_RWops *); #endif // XML_H diff --git a/xml_exporter.py b/xml_exporter.py index dc3e501..50412d2 100644 --- a/xml_exporter.py +++ b/xml_exporter.py @@ -13,15 +13,6 @@ format. """ -# This script exports the currently selected blender mesh to xml format. -# Supports: -# * - texture coordinates -# * - vertex normals -# * - material subsets -# * - bones and skeletal animation -# -# The c++ code to parse the outgoing xml file can be found in "mesh.cpp" - import Blender from Blender import Types, Object, Material,Armature,Mesh,Modifier from Blender.Mathutils import * @@ -31,65 +22,86 @@ import math import xml.etree.ElementTree as ET -EXTENSION = '.xml' # not very specific +EXTENSION='.xml' + +def callback_sel (filename): + + # A file path has been selected to export to -def callback_sel(filename): if not filename.endswith (EXTENSION): filename += EXTENSION xexport = Exporter() - xexport.exportSelected(filename) + xexport.exportSelected (filename) def draw (): - glClearColor(0.55,0.6,0.6,1) - glClear(GL_COLOR_BUFFER_BIT) - glRasterPos2d(20,75) - Draw.Button("Export Selected",1,20,155,100,30,"export the selected object") -def event (evt,val): + # Make the menu in Blender: a single button to press. + glClearColor (0.55,0.6,0.6,1) + glClear (GL_COLOR_BUFFER_BIT) + glRasterPos2d (20,75) + Draw.Button ("Export Selected",1,20,155,100,30,"export the selected object") + +def event (evt, val): if evt == Draw.ESCKEY: Draw.Exit() def button_event (evt): + if evt == 1: - fname = Blender.sys.makename(ext = EXTENSION) + fname = Blender.sys.makename (ext = EXTENSION) Blender.Window.FileSelector (callback_sel, "Export xml mesh", fname) - Draw.Exit() + Draw.Exit () +# Regeister the exported menu: Draw.Register (draw, event, button_event) +# Makes a path a basename without extension. def stripFilename (filename): - return os.path.basename(os.path.splitext(filename)[0]) + return os.path.basename (os.path.splitext (filename)[0]) +# Removes whitespaces. def makeAllowedNameString (name): - return ' %s ' % name.replace(' ', '_') + return ' %s ' % name.replace (' ', '_') +# Find an armature for a given mesh object: def findMeshArmature (mesh_obj): for mod in mesh_obj.modifiers: if mod.type == Modifier.Types.ARMATURE: - return mod[Modifier.Settings.OBJECT] + + # Return as an object: + return mod [Modifier.Settings.OBJECT] return None +# Find a mesh, to which the armature belongs def findArmatureMesh (arm_obj): arm_name = arm_obj.getData(True, False) + for obj in Object.Get(): - if type(obj.getData(False, True)) != Types.MeshType: + + # Skip objects that are not meshes + if type (obj.getData(False, True)) != Types.MeshType: continue for mod in obj.modifiers: + + # Skip modifiers that are not armatures if mod.type != Modifier.Types.ARMATURE: continue - objmodobj_name = mod[Modifier.Settings.OBJECT].getData(True, False) + objmodobj_name = mod [Modifier.Settings.OBJECT].getData(True, False) if objmodobj_name == arm_name: + # This is my armature ! + return obj return None -def getFaceFactor (matrix): # tells if we need to flip face direction when applying this matrix +# Tells if we need to flip face direction when applying this matrix. +def getFaceFactor (matrix): f = 1 for i in range (3): @@ -101,6 +113,7 @@ def getFaceFactor (matrix): # tells if we need to flip face direction when apply else: return 1 +# Makes a string representation of a blender interpolation type def interptypes2tring (i): if i == InterpTypes.CONST: @@ -112,6 +125,7 @@ def interptypes2tring (i): else: raise Exception ("not an interp type: %i" % i) +# Makes a string representation of a blender extension type def extend2string (e): if e == ExtendTypes.CONST: @@ -125,11 +139,14 @@ def extend2string (e): else: raise Exception ("not an extend type: %i" % e) -class Exporter(object): +class Exporter (object): + + # Code used in this exported is based on the following documentation: + # http://www.blender.org/api/248PythonDoc/ def __init__(self): - # Swap y and z coordinates: + # Switch y and z: self.transformVertex = Matrix ([1,0,0,0], [0,0,1,0], [0,1,0,0], @@ -150,25 +167,26 @@ def __init__(self): self.color_channels = ['r', 'g', 'b', 'a'] - def exportSelected (self, filename): + def exportSelected(self, filename): - # Find the selected mesh and maybe an associated armature: + # Find the selected mesh (and maybe its armature) mesh_obj = None arm_obj = None + # The selected object might actually be an armature, with a mesh attached to it. selected = Object.GetSelected() - if len (selected) == 0: + if len(selected) == 0: raise Exception ("No selection") sel_data = selected[0].getData(False,True) - if type (sel_data) == Types.MeshType: + if type(sel_data) == Types.MeshType: mesh_obj = selected [0] arm_obj = findMeshArmature (mesh_obj) - elif type (sel_data) == Types.ArmatureType: + elif type(sel_data) == Types.ArmatureType: arm_obj = selected [0] mesh_obj = findArmatureMesh (arm_obj) @@ -178,20 +196,18 @@ def exportSelected (self, filename): else: raise Exception ("The selected object %s is not a mesh or armature." % str(type(selected [0]))) - # Convert the blender objects to an xml tree root = self.toXML (mesh_obj, arm_obj) - # Write the xml tree ro a file: f = open(filename, "w") f.write(ET.tostring(root)) f.close() - def toXML(self, mesh_obj, arm_obj=None): + def toXML (self, mesh_obj, arm_obj=None): mesh = self.meshToXML (mesh_obj) if arm_obj: tag_armature = self.armatureToXML(arm_obj, mesh_obj) - mesh.append(tag_armature) + mesh.append (tag_armature) return mesh @@ -201,9 +217,10 @@ def meshToXML (self, mesh_obj): face_factor = -getFaceFactor (self.transformRot) mesh = mesh_obj.getData(False, True) - root = ET.Element('mesh') + root = ET.Element ('mesh') - tag_vertices = ET.Element('vertices') + # Convert the mesh's vertices to xml tags: + tag_vertices = ET.Element ('vertices') root.append (tag_vertices) for vertex in mesh.verts: @@ -211,7 +228,8 @@ def meshToXML (self, mesh_obj): tag_vertex = self.vertexToXML (vertex) tag_vertices.append(tag_vertex) - tag_faces = ET.Element('faces') + # Convert the mesh's faces to xml tags: + tag_faces = ET.Element ('faces') root.append (tag_faces) # Divide the mesh in subsets, based on their materials. @@ -228,20 +246,23 @@ def meshToXML (self, mesh_obj): subset_faces [face.mat].append (face) - if len(subset_faces) > 0: + # Did we even find materials? + if len (subset_faces) > 0: - tag_subsets = ET.Element('subsets') + tag_subsets = ET.Element ('subsets') root.append (tag_subsets) for material_i in subset_faces: - if material_i < len(mesh.materials): - material = mesh.materials[material_i] + if material_i < len (mesh.materials): + + material = mesh.materials [material_i] else: + # material index is not found in mesh material = None tag_subset = self.subsetToXML (material, subset_faces [material_i]) - tag_subset.attrib ['id'] = str(material_i) + tag_subset.attrib ['id'] = str (material_i) tag_subsets.append (tag_subset) return root @@ -249,37 +270,48 @@ def meshToXML (self, mesh_obj): def subsetToXML (self, material, faces): - # Add the material name and properties to the subset. - # The user of the exported file is free to ignore these, of coarse. + # Take the material color settings. + # We might need to add more in the future.. if material: name = material.name diffuse = material.rgbCol + [1.0] specular = material.specCol + [1.0] - emission = [material.emit * diffuse[i] for i in range(4)] + emission = [material.emit * diffuse [i] for i in range (4)] + else: + + # No material associated with subset, use default values: + name = None diffuse = [1.0, 1.0, 1.0, 1.0] specular = [0.0, 0.0, 0.0, 0.0] emission = [0.0, 0.0, 0.0, 0.0] - tag_subset = ET.Element('subset') + tag_subset = ET.Element ('subset') + # Add name to tag, makes it easier to find back in the file. if name: tag_subset.attrib ['name'] = str(name) - c = self.color_channels + # Convert material colors to xml attributes + + c = self.color_channels # strings, r,g,b,a + tag_diffuse = ET.Element('diffuse') tag_subset.append(tag_diffuse) tag_specular = ET.Element('specular') tag_subset.append(tag_specular) tag_emission = ET.Element('emission') tag_subset.append(tag_emission) - for i in range(4): + + for i in range (4): tag_diffuse.attrib[c[i]] = '%.4f' % diffuse [i] tag_specular.attrib[c[i]] = '%.4f' % specular [i] tag_emission.attrib[c[i]] = '%.4f' % emission [i] + # Register all the subset's faces in xml tags: + tag_faces = ET.Element('faces') tag_subset.append(tag_faces) for face in faces: @@ -296,10 +328,7 @@ def subsetToXML (self, material, faces): def faceToXML (self, face, face_factor): - # Only registers texture coordinates and vertex references. - # It doesn't store the actual vertex coordinates however. - # Those are in a different table and can occur in multiple faces. - + # Only faces with 3 or 4 faces are supported. if len(face.verts) == 3: tag_face = ET.Element('triangle') elif len(face.verts) == 4: @@ -309,6 +338,7 @@ def faceToXML (self, face, face_factor): tag_face.attrib['id'] = str(face.index) + # Order determines which side is front. order = range(len(face.verts)) if face_factor < 0: order.reverse() @@ -317,8 +347,10 @@ def faceToXML (self, face, face_factor): tag_corner = ET.Element('corner') tag_face.append (tag_corner) + # Reference to vertex: tag_corner.attrib['vertex_id'] = str(face.verts[i].index) + # Texture coordinates: try: tag_corner.attrib['tex_u'] = '%.4f' % face.uv[i][0] tag_corner.attrib['tex_v'] = '%.4f' % face.uv[i][1] @@ -331,41 +363,40 @@ def faceToXML (self, face, face_factor): def vertexToXML (self, vertex): - # In the xml, each vertex has its of id (reference), - # position (xyz) and normal. (xyz) - - tag_vertex = ET.Element('vertex') + # Register vertex properties as attributes. + # The entire vertex will be one xml tag. + tag_vertex = ET.Element ('vertex') - tag_vertex.attrib['id'] = str(vertex.index) + tag_vertex.attrib ['id'] = str (vertex.index) co = self.transformVertex * vertex.co no = self.transformVertex * vertex.no - tag_pos = ET.Element('pos') + # Vertex position in space: + tag_pos = ET.Element ('pos') tag_pos.attrib['x'] = '%.4f' % co [0] tag_pos.attrib['y'] = '%.4f' % co [1] tag_pos.attrib['z'] = '%.4f' % co [2] - tag_vertex.append(tag_pos) + tag_vertex.append (tag_pos) - tag_pos = ET.Element('norm') + # Vertex normal, used for lighting: + tag_pos = ET.Element ('norm') tag_pos.attrib['x'] = '%.4f' % no [0] tag_pos.attrib['y'] = '%.4f' % no [1] tag_pos.attrib['z'] = '%.4f' % no [2] - tag_vertex.append(tag_pos) + tag_vertex.append (tag_pos) return tag_vertex def armatureToXML (self, arm_obj, mesh_obj): - # An armature consists of bones. - # Bones have a head (xyz), tail (xzy), weight, and - # a set of references of the vertices which the bone modifies. - - armature = arm_obj.getData() - mesh = mesh_obj.getData(False, True) - vertex_groups = mesh.getVertGroupNames() + armature = arm_obj.getData () + mesh = mesh_obj.getData (False, True) + vertex_groups = mesh.getVertGroupNames () arm_matrix = arm_obj.getMatrix() + + # Any normal mesh transformation is expected to be invertible: try: mesh_inv_matrix = mesh_obj.getMatrix().invert() except: @@ -377,16 +408,19 @@ def armatureToXML (self, arm_obj, mesh_obj): bones_tag = ET.Element('bones') root.append (bones_tag) + # Iterate over the armature's bones: bone_list = [] for bone_name in armature.bones.keys(): bone_tag = ET.Element('bone') bones_tag.append (bone_tag) + # A tag for every bone: bone = armature.bones [bone_name] bone_tag.attrib['id'] = bone_name bone_list.append (bone) + # Export position of bone head and tail in mesh space: headpos = self.transformVertex * arm2mesh * (bone.head['ARMATURESPACE']) tailpos = self.transformVertex * arm2mesh * (bone.tail['ARMATURESPACE']) bone_tag.attrib['x'] = '%.4f' % headpos[0] @@ -395,21 +429,26 @@ def armatureToXML (self, arm_obj, mesh_obj): bone_tag.attrib['tail_x'] = '%.4f' % tailpos[0] bone_tag.attrib['tail_y'] = '%.4f' % tailpos[1] bone_tag.attrib['tail_z'] = '%.4f' % tailpos[2] + bone_tag.attrib['weight'] = '%.4f' % bone.weight + # Reference to parent bone: if bone.hasParent(): bone_tag.attrib['parent_id'] = bone.parent.name if bone_name in mesh.getVertGroupNames(): + # Register references to the verices that this bone pulls at: + vertices_tag = ET.Element('vertices') bone_tag.append(vertices_tag) for vert_i in mesh.getVertsFromGroup(bone_name, False): vertex_tag = ET.Element('vertex') - vertices_tag.append(vertex_tag) + vertices_tag.append (vertex_tag) vertex_tag.attrib['id'] = str(vert_i) + # The armature might have animations associated with it. animations_tag = self.armatureAnimationsToXML (arm_obj, arm2mesh, bone_list) root.append (animations_tag) @@ -418,43 +457,53 @@ def armatureToXML (self, arm_obj, mesh_obj): def armatureAnimationsToXML (self, arm_obj, arm2mesh, bone_list): - # Each blender action, attached to the armature is stored as animation. - # Each layer in the animation relates to a bone. - # Keys in the layer represent the bone's position at set time points. - - root = ET.Element('animations') + root = ET.Element ('animations') - for action in Armature.NLA.GetActions().values(): - # iterate over actions + # Check every action to see if it's an animation: + for action in Armature.NLA.GetActions ().values (): - animation_tag = ET.Element('animation') + animation_tag = ET.Element ('animation') nLayers = 0 + + # Check every known bone: for bone in bone_list: - # iterate over bones action.setActive (arm_obj) - # See if this bone is moved in this action.. if bone.name not in action.getChannelNames (): continue - ipo = action.getChannelIpo(bone.name) + ipo = action.getChannelIpo (bone.name) if len (ipo.curves) <= 0: + # Bone doesn't move in this action continue parent_ipo = None if bone.hasParent(): parent_ipo = action.getChannelIpo (bone.parent.name) + # A layer for every moving bone: + layer_tag = ET.Element('layer') layer_tag.attrib ['bone_id'] = bone.name animation_tag.append (layer_tag) nLayers += 1 - for frame_num in action.getFrameNumbers(): + # Find all key frames for this bone: + frame_numbers = [] + for curve in ipo.curves: + for bez_triple in curve.bezierPoints: + + frame_num = int (bez_triple.pt [0]) + + if frame_num not in frame_numbers: + frame_numbers.append (frame_num) + + # Iterate over key frames + for frame_num in frame_numbers: # Get pose at specified frame number: Blender.Set ("curframe", frame_num) @@ -463,11 +512,14 @@ def armatureAnimationsToXML (self, arm_obj, arm2mesh, bone_list): m = pose_bone.localMatrix if pose_bone.parent: - # We want the bone rotation relative to its parent: + # We want the bone rotation relative to its parent. + # This was the only known way to do it: n = pose_bone.parent.localMatrix.rotationPart().resize4x4() n.invert() m = m * n + # Register frame number, position and rotation of the bone: + q = m.toQuat() rot = self.transformRot * arm2mesh * Vector (q.x, q.y, q.z, q.w)