Pārlūkot izejas kodu

Merge branch 'next' of https://github.com/blackberry/GamePlay into next

Conflicts:
	gameplay/src/PhysicsController.cpp
seanpaultaylor 12 gadi atpakaļ
vecāks
revīzija
c6f45c7fa8
45 mainītis faili ar 811 papildinājumiem un 466 dzēšanām
  1. 3 3
      README.md
  2. 2 2
      gameplay/src/Button.cpp
  3. 1 0
      gameplay/src/Camera.cpp
  4. 1 0
      gameplay/src/CheckBox.cpp
  5. 29 9
      gameplay/src/Font.cpp
  6. 23 0
      gameplay/src/Font.h
  7. 5 0
      gameplay/src/ImageControl.cpp
  8. 7 11
      gameplay/src/Matrix.cpp
  9. 11 12
      gameplay/src/Node.cpp
  10. 126 97
      gameplay/src/PhysicsController.cpp
  11. 2 2
      gameplay/src/PhysicsController.h
  12. 1 1
      gameplay/src/PhysicsGhostObject.cpp
  13. 2 2
      gameplay/src/PhysicsRigidBody.cpp
  14. 5 1
      gameplay/src/PlatformAndroid.cpp
  15. 5 0
      gameplay/src/RadioButton.cpp
  16. 51 2
      gameplay/src/RenderState.cpp
  17. 22 0
      gameplay/src/RenderState.h
  18. 1 1
      gameplay/src/Scene.cpp
  19. 335 288
      gameplay/src/SceneLoader.cpp
  20. 25 12
      gameplay/src/SceneLoader.h
  21. 1 0
      gameplay/src/ScriptController.cpp
  22. 1 0
      gameplay/src/ScriptController.inl
  23. 25 0
      gameplay/src/Slider.cpp
  24. 8 4
      gameplay/src/SpriteBatch.cpp
  25. 2 1
      gameplay/src/TextBox.cpp
  26. 9 0
      gameplay/src/Texture.cpp
  27. 73 0
      gameplay/src/lua/lua_Font.cpp
  28. 2 0
      gameplay/src/lua/lua_Font.h
  29. 1 1
      samples/browser/android/jni/Android.mk
  30. 2 1
      samples/browser/android/jni/Application.mk
  31. 2 2
      samples/browser/res/common/physics.physics
  32. 1 1
      samples/character/android/jni/Android.mk
  33. 3 1
      samples/character/android/jni/Application.mk
  34. 1 1
      samples/longboard/android/jni/Android.mk
  35. 3 1
      samples/longboard/android/jni/Application.mk
  36. 1 1
      samples/lua/android/jni/Android.mk
  37. 3 1
      samples/lua/android/jni/Application.mk
  38. 1 1
      samples/mesh/android/jni/Android.mk
  39. 3 1
      samples/mesh/android/jni/Application.mk
  40. 1 1
      samples/particles/android/jni/Android.mk
  41. 3 1
      samples/particles/android/jni/Application.mk
  42. 1 1
      samples/racer/android/jni/Android.mk
  43. 3 1
      samples/racer/android/jni/Application.mk
  44. 1 1
      samples/spaceship/android/jni/Android.mk
  45. 3 1
      samples/spaceship/android/jni/Application.mk

+ 3 - 3
README.md

@@ -1,4 +1,4 @@
-## gameplay v1.7.0
+## gameplay v1.8.0
 
 GamePlay3D is an open-source, cross-platform 3D native C++ game framework making it easy to learn and write mobile and desktop games. 
 
@@ -16,12 +16,12 @@ GamePlay3D is an open-source, cross-platform 3D native C++ game framework making
 - [Google Android 2.3+](https://github.com/blackberry/GamePlay/wiki/Android-NDK-Setup) (using Google Android NDK)
 
 ## Supported Desktop Platforms
-- [Microsoft Windows 7](https://github.com/blackberry/GamePlay/wiki/Visual-Studio-Setup) (using Microsoft Visual Studio 2010)
+- [Microsoft Windows 7](https://github.com/blackberry/GamePlay/wiki/Visual-Studio-Setup) (using Microsoft Visual Studio 2012)
 - [Apple MacOS X](https://github.com/blackberry/GamePlay/wiki/Apple-Xcode-Setup) (using Apple XCode 4)
 - [Linux](https://github.com/blackberry/GamePlay/wiki/Linux-Setup) (using CMake)
 
 ## Roadmap for 'next' branch
-- [Version 2.0.0 Milestone](https://github.com/blackberry/GamePlay/issues?milestone=6)
+- [backlog](https://github.com/blackberry/GamePlay/issues?milestone=7)
 
 ## License
 The project is open sourced under the [Apache 2.0 license](http://www.tldrlegal.com/license/apache-license-2.0-%28apache-2.0%29).

+ 2 - 2
gameplay/src/Button.cpp

@@ -50,8 +50,8 @@ bool Button::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contac
                 y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
             {
                 _contactIndex = (int) contactIndex;
-                notifyListeners(Control::Listener::PRESS);
                 setState(Control::ACTIVE);
+                notifyListeners(Control::Listener::PRESS);
                 return _consumeInputEvents;
             }
             else
@@ -70,8 +70,8 @@ bool Button::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contac
                 x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
                 y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
             {
-                notifyListeners(Control::Listener::CLICK);
                 setState(Control::FOCUS);
+                notifyListeners(Control::Listener::CLICK);
             }
             else
             {

+ 1 - 0
gameplay/src/Camera.cpp

@@ -268,6 +268,7 @@ const Matrix& Camera::getProjectionMatrix() const
         }
         else
         {
+            // Create an ortho projection with the origin at the bottom left of the viewport, +X to the right and +Y up.
             Matrix::createOrthographic(_zoom[0], _zoom[1], _nearPlane, _farPlane, &_projection);
         }
 

+ 1 - 0
gameplay/src/CheckBox.cpp

@@ -56,6 +56,7 @@ void CheckBox::setChecked(bool checked)
 void CheckBox::setImageSize(float width, float height)
 {
     _imageSize.set(width, height);
+    _dirty = true;
 }
 
 const Vector2& CheckBox::getImageSize() const

+ 29 - 9
gameplay/src/Font.cpp

@@ -16,7 +16,7 @@ static std::vector<Font*> __fontCache;
 static Effect* __fontEffect = NULL;
 
 Font::Font() :
-    _style(PLAIN), _size(0), _glyphs(NULL), _glyphCount(0), _texture(NULL), _batch(NULL)
+    _style(PLAIN), _size(0), _spacing(0.125f), _glyphs(NULL), _glyphCount(0), _texture(NULL), _batch(NULL)
 {
 }
 
@@ -168,6 +168,7 @@ Font::Text* Font::createText(const char* text, const Rectangle& area, const Vect
         size = _size;
     GP_ASSERT(_size);
     float scale = (float)size / _size;
+    int spacing = (int)(size * _spacing);
     int yPos = area.y;
     const float areaHeight = area.height - size;
     std::vector<int> xPositions;
@@ -317,7 +318,7 @@ Font::Text* Font::createText(const char* text, const Rectangle& area, const Vect
 
                     }
                 }
-                xPos += (int)(g.width)*scale + (size >> 3);
+                xPos += (int)(g.width)*scale + spacing;
             }
         }
 
@@ -402,6 +403,7 @@ void Font::drawText(const char* text, int x, int y, const Vector4& color, unsign
     GP_ASSERT(_size);
     GP_ASSERT(text);
     float scale = (float)size / _size;
+    int spacing = (int)(size * _spacing);
     const char* cursor = NULL;
 
     if (rightToLeft)
@@ -497,7 +499,7 @@ void Font::drawText(const char* text, int x, int y, const Vector4& color, unsign
                 {
                     Glyph& g = _glyphs[index];
                     _batch->draw(xPos, yPos, g.width * scale, size, g.uvs[0], g.uvs[1], g.uvs[2], g.uvs[3], color);
-                    xPos += floor(g.width * scale + (float)(size >> 3));
+                    xPos += floor(g.width * scale + spacing);
                     break;
                 }
                 break;
@@ -528,6 +530,7 @@ void Font::drawText(const char* text, const Rectangle& area, const Vector4& colo
         size = _size;
     GP_ASSERT(_size);
     float scale = (float)size / _size;
+    int spacing = (int)(size * _spacing);
     int yPos = area.y;
     const float areaHeight = area.height - size;
     std::vector<int> xPositions;
@@ -647,7 +650,7 @@ void Font::drawText(const char* text, const Rectangle& area, const Vector4& colo
                         }
                     }
                 }
-                xPos += (int)(g.width)*scale + (size >> 3);
+                xPos += (int)(g.width)*scale + spacing;
             }
         }
 
@@ -1277,6 +1280,16 @@ void Font::getMeasurementInfo(const char* text, const Rectangle& area, unsigned
     }
 }
 
+float Font::getCharacterSpacing() const
+{
+    return _spacing;
+}
+
+void Font::setCharacterSpacing(float spacing)
+{
+    _spacing = spacing;
+}
+
 int Font::getIndexAtLocation(const char* text, const Rectangle& area, unsigned int size, const Vector2& inLocation, Vector2* outLocation,
                                       Justify justify, bool wrap, bool rightToLeft)
 {
@@ -1299,7 +1312,10 @@ int Font::getIndexOrLocation(const char* text, const Rectangle& area, unsigned i
     unsigned int charIndex = 0;
 
     // Essentially need to measure text until we reach inLocation.
+    if (size == 0)
+        size = _size;
     float scale = (float)size / _size;
+    int spacing = (int)(size * _spacing);
     int yPos = area.y;
     const float areaHeight = area.height - size;
     std::vector<int> xPositions;
@@ -1354,7 +1370,7 @@ int Font::getIndexOrLocation(const char* text, const Rectangle& area, unsigned i
 
         if (destIndex == (int)charIndex ||
             (destIndex == -1 &&
-             inLocation.x >= xPos && inLocation.x < floor(xPos + (float)(size >> 3)) &&
+             inLocation.x >= xPos && inLocation.x < xPos + spacing &&
              inLocation.y >= yPos && inLocation.y < yPos + size))
         {
             outLocation->x = xPos;
@@ -1426,7 +1442,7 @@ int Font::getIndexOrLocation(const char* text, const Rectangle& area, unsigned i
                 // Check against inLocation.
                 if (destIndex == (int)charIndex ||
                     (destIndex == -1 &&
-                    inLocation.x >= xPos && inLocation.x < floor(xPos + g.width*scale + (float)(size >> 3)) &&
+                    inLocation.x >= xPos && inLocation.x < floor(xPos + g.width*scale + spacing) &&
                     inLocation.y >= yPos && inLocation.y < yPos + size))
                 {
                     outLocation->x = xPos;
@@ -1434,7 +1450,7 @@ int Font::getIndexOrLocation(const char* text, const Rectangle& area, unsigned i
                     return charIndex;
                 }
 
-                xPos += floor(g.width*scale + (float)(size >> 3));
+                xPos += floor(g.width*scale + spacing);
                 charIndex++;
             }
         }
@@ -1508,7 +1524,7 @@ int Font::getIndexOrLocation(const char* text, const Rectangle& area, unsigned i
 
     if (destIndex == (int)charIndex ||
         (destIndex == -1 &&
-         inLocation.x >= xPos && inLocation.x < floor(xPos + (float)(size >> 3)) &&
+         inLocation.x >= xPos && inLocation.x < xPos + spacing &&
          inLocation.y >= yPos && inLocation.y < yPos + size))
     {
         outLocation->x = xPos;
@@ -1524,6 +1540,10 @@ unsigned int Font::getTokenWidth(const char* token, unsigned int length, unsigne
     GP_ASSERT(token);
     GP_ASSERT(_glyphs);
 
+    if (size == 0)
+        size = _size;
+    int spacing = (int)(size * _spacing);
+
     // Calculate width of word or line.
     unsigned int tokenWidth = 0;
     for (unsigned int i = 0; i < length; ++i)
@@ -1542,7 +1562,7 @@ unsigned int Font::getTokenWidth(const char* token, unsigned int length, unsigne
             if (glyphIndex >= 0 && glyphIndex < (int)_glyphCount)
             {
                 Glyph& g = _glyphs[glyphIndex];
-                tokenWidth += floor(g.width * scale + (float)(size >> 3));
+                tokenWidth += floor(g.width * scale + spacing);
             }
             break;
         }

+ 23 - 0
gameplay/src/Font.h

@@ -220,6 +220,28 @@ public:
     void measureText(const char* text, const Rectangle& clip, unsigned int size, Rectangle* out,
                      Justify justify = ALIGN_TOP_LEFT, bool wrap = true, bool ignoreClip = false);
 
+    /**
+     * Returns current character spacing for this font in percentage of fonts size.
+     *
+     * @see setCharacterSpacing(float)
+     */
+    float getCharacterSpacing() const;
+
+    /**
+     * Sets the fixed character spacing for this font.
+     *
+     * Character spacing is the fixed amount of space that is inserted between characters. This is a simplified
+     * type of kerning and does not take adjacent characters into consideration. Character spacing is defined
+     * as a floating point value that is interpreted as a percentage of size used to draw the font. For example,
+     * a value of 0.1 would cause a spacing of 10% of the font size to be inserted between adjacent characters.
+     * For a font size of 20, this would equate to 2 pixels of extra space between characters.
+     *
+     * The default character spacing for fonts is 0.125.
+     *
+     * @param spacing New fixed character spacing, expressed as a percentage of font size.
+     */
+    void setCharacterSpacing(float spacing);
+
     /**
      * Get an character index into a string corresponding to the character nearest the given location within the clip region.
      */
@@ -332,6 +354,7 @@ private:
     std::string _family;
     Style _style;
     unsigned int _size;
+    float _spacing;
     Glyph* _glyphs;
     unsigned int _glyphCount;
     Texture* _texture;

+ 5 - 0
gameplay/src/ImageControl.cpp

@@ -76,6 +76,7 @@ void ImageControl::setImage(const char* path)
     _tw = 1.0f / texture->getWidth();
     _th = 1.0f / texture->getHeight();
     texture->release();
+    _dirty = true;
 }
 
 void ImageControl::setRegionSrc(float x, float y, float width, float height)
@@ -86,11 +87,13 @@ void ImageControl::setRegionSrc(float x, float y, float width, float height)
     _uvs.u2 = (x + width) * _tw;
     _uvs.v1 = 1.0f - (y * _th);
     _uvs.v2 = 1.0f - ((y + height) * _th);
+    _dirty = true;
 }
 
 void ImageControl::setRegionSrc(const Rectangle& region)
 {
     setRegionSrc(region.x, region.y, region.width, region.height);
+    _dirty = true;
 }
 
 const Rectangle& ImageControl::getRegionSrc() const
@@ -101,11 +104,13 @@ const Rectangle& ImageControl::getRegionSrc() const
 void ImageControl::setRegionDst(float x, float y, float width, float height)
 {
     _dstRegion.set(x, y, width, height);
+    _dirty = true;
 }
 
 void ImageControl::setRegionDst(const Rectangle& region)
 {
     setRegionDst(region.x, region.y, region.width, region.height);
+    _dirty = true;
 }
 
 const Rectangle& ImageControl::getRegionDst() const

+ 7 - 11
gameplay/src/Matrix.cpp

@@ -152,18 +152,14 @@ void Matrix::createOrthographicOffCenter(float left, float right, float bottom,
     GP_ASSERT(top != bottom);
     GP_ASSERT(zFarPlane != zNearPlane);
 
-    float r_l = 1.0f / (right - left);
-    float t_b = 1.0f / (top - bottom);
-    float f_n = 1.0f / (zFarPlane - zNearPlane);
-
     memset(dst, 0, MATRIX_SIZE);
-    dst->m[0] = 2.0f * r_l;
-    dst->m[5] = 2.0f * t_b;
-    dst->m[10] = -2.0f * f_n;
-    dst->m[12] = (-(right + left)) * r_l;
-    dst->m[13] = (-(top + bottom)) * t_b;
-    dst->m[14] = (-(zFarPlane + zNearPlane)) * f_n;
-    dst->m[15] = 1.0f;
+    dst->m[0] = 2 / (right - left);
+    dst->m[5] = 2 / (top - bottom);
+    dst->m[12] = (left + right) / (left - right);
+    dst->m[10] = 1 / (zNearPlane - zFarPlane);
+    dst->m[13] = (top + bottom) / (bottom - top);
+    dst->m[14] = zNearPlane / (zNearPlane - zFarPlane);
+    dst->m[15] = 1;
 }
     
 void Matrix::createBillboard(const Vector3& objectPosition, const Vector3& cameraPosition,

+ 11 - 12
gameplay/src/Node.cpp

@@ -110,12 +110,18 @@ void Node::addChild(Node* child)
         child->_scene->removeNode(child);
     }
 
-    // Order is irrelevant, so add to the beginning of the list.
+    // Add child to the end of the list.
+    // NOTE: This is different than the original behavior which inserted nodes
+    // into the beginning of the list. Although slightly slower to add to the
+    // end of the list, it makes scene traversal and drawing order more
+    // predictable, so I've changed it.
     if (_firstChild)
     {
-        _firstChild->_prevSibling = child;
-        child->_nextSibling = _firstChild;
-        _firstChild = child;
+        Node* n = _firstChild;
+        while (n->_nextSibling)
+            n = n->_nextSibling;
+        n->_nextSibling = child;
+        child->_prevSibling = n;
     }
     else
     {
@@ -988,15 +994,8 @@ Node* Node::cloneRecursive(NodeCloneContext &context) const
     Node* copy = cloneSingleNode(context);
     GP_ASSERT(copy);
 
-    // Find our current last child
-    Node* lastChild = NULL;
+    // Add child nodes
     for (Node* child = getFirstChild(); child != NULL; child = child->getNextSibling())
-    {
-        lastChild = child;
-    }
-
-    // Loop through the nodes backwards because addChild adds the node to the front.
-    for (Node* child = lastChild; child != NULL; child = child->getPreviousSibling())
     {
         Node* childCopy = child->cloneRecursive(context);
         GP_ASSERT(childCopy);

+ 126 - 97
gameplay/src/PhysicsController.cpp

@@ -11,6 +11,7 @@
 #undef new
 #endif
 #include "BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h"
+#include "BulletCollision/CollisionShapes/btShapeHull.h"
 #ifdef GP_USE_MEM_LEAK_DETECTION
 #define new DEBUG_NEW
 #endif
@@ -762,7 +763,7 @@ static void computeCenterOfMass(const Vector3& center, const Vector3& scale, Vec
     centerOfMassOffset->negate();
 }
 
-PhysicsCollisionShape* PhysicsController::createShape(Node* node, const PhysicsCollisionShape::Definition& shape, Vector3* centerOfMassOffset)
+PhysicsCollisionShape* PhysicsController::createShape(Node* node, const PhysicsCollisionShape::Definition& shape, Vector3* centerOfMassOffset, bool dynamic)
 {
     GP_ASSERT(node);
 
@@ -887,7 +888,7 @@ PhysicsCollisionShape* PhysicsController::createShape(Node* node, const PhysicsC
     case PhysicsCollisionShape::SHAPE_MESH:
         {
             // Build mesh from passed in shape.
-            collisionShape = createMesh(shape.data.mesh, scale);
+            collisionShape = createMesh(shape.data.mesh, scale, dynamic);
         }
         break;
 
@@ -1055,34 +1056,10 @@ PhysicsCollisionShape* PhysicsController::createHeightfield(Node* node, HeightFi
     return shape;
 }
 
-PhysicsCollisionShape* PhysicsController::createMesh(Mesh* mesh, const Vector3& scale)
+PhysicsCollisionShape* PhysicsController::createMesh(Mesh* mesh, const Vector3& scale, bool dynamic)
 {
     GP_ASSERT(mesh);
 
-    // Only support meshes with triangle list primitive types.
-    bool triMesh = true;
-    if (mesh->getPartCount() > 0)
-    {
-        for (unsigned int i = 0; i < mesh->getPartCount(); ++i)
-        {
-            if (mesh->getPart(i)->getPrimitiveType() != Mesh::TRIANGLES)
-            {
-                triMesh = false;
-                break;
-            }
-        }
-    }
-    else
-    {
-        triMesh = mesh->getPrimitiveType() == Mesh::TRIANGLES;
-    }
-
-    if (!triMesh)
-    {
-        GP_ERROR("Mesh rigid bodies are currently only supported on meshes with TRIANGLES primitive type.");
-        return NULL;
-    }
-
     // The mesh must have a valid URL (i.e. it must have been loaded from a Bundle)
     // in order to fetch mesh data for computing mesh rigid body.
     if (strlen(mesh->getUrl()) == 0)
@@ -1091,6 +1068,36 @@ PhysicsCollisionShape* PhysicsController::createMesh(Mesh* mesh, const Vector3&
         return NULL;
     }
 
+    if (!dynamic)
+    {
+        // Static meshes use btBvhTriangleMeshShape and therefore only support triangle mesh shapes.
+        // Dynamic meshes are approximated with a btConvexHullShape (convex wrapper on cloud of vertices)
+        // and therefore can support any primitive type.
+        bool triMesh = true;
+        if (mesh->getPartCount() > 0)
+        {
+            for (unsigned int i = 0; i < mesh->getPartCount(); ++i)
+            {
+                if (mesh->getPart(i)->getPrimitiveType() != Mesh::TRIANGLES)
+                {
+                    triMesh = false;
+                    break;
+                }
+            }
+        }
+        else
+        {
+            triMesh = mesh->getPrimitiveType() == Mesh::TRIANGLES;
+        }
+
+        if (!triMesh)
+        {
+            GP_ERROR("Mesh rigid bodies are currently only supported on meshes with TRIANGLES primitive type.");
+            return NULL;
+        }
+    }
+
+    // Read mesh data from URL
     Bundle::MeshData* data = Bundle::readMeshData(mesh->getUrl());
     if (data == NULL)
     {
@@ -1112,96 +1119,118 @@ PhysicsCollisionShape* PhysicsController::createMesh(Mesh* mesh, const Vector3&
     for (unsigned int i = 0; i < data->vertexCount; i++)
     {
         v.set(*((float*)&data->vertexData[i * vertexStride + 0 * sizeof(float)]),
-              *((float*)&data->vertexData[i * vertexStride + 1 * sizeof(float)]),
-              *((float*)&data->vertexData[i * vertexStride + 2 * sizeof(float)]));
+                *((float*)&data->vertexData[i * vertexStride + 1 * sizeof(float)]),
+                *((float*)&data->vertexData[i * vertexStride + 2 * sizeof(float)]));
         v *= m;
         memcpy(&(shapeMeshData->vertexData[i * 3]), &v, sizeof(float) * 3);
     }
 
-    btTriangleIndexVertexArray* meshInterface = bullet_new<btTriangleIndexVertexArray>();
+    btCollisionShape* collisionShape = NULL;
+    btTriangleIndexVertexArray* meshInterface = NULL;
 
-    size_t partCount = data->parts.size();
-    if (partCount > 0)
+    if (dynamic)
+    {
+        // For dynamic meshes, use a btConvexHullShape approximation
+        btConvexHullShape* originalConvexShape = bullet_new<btConvexHullShape>(shapeMeshData->vertexData, data->vertexCount, sizeof(float)*3);
+
+        // Create a hull approximation for better performance
+	    btShapeHull* hull = bullet_new<btShapeHull>(originalConvexShape);
+	    hull->buildHull(originalConvexShape->getMargin());
+	    collisionShape = bullet_new<btConvexHullShape>((btScalar*)hull->getVertexPointer(), hull->numVertices());
+
+        SAFE_DELETE(hull);
+        SAFE_DELETE(originalConvexShape);
+    }
+    else
     {
-        PHY_ScalarType indexType = PHY_UCHAR;
-        int indexStride = 0;
-        Bundle::MeshPartData* meshPart = NULL;
-        for (size_t i = 0; i < partCount; i++)
+        // For static meshes, use btBvhTriangleMeshShape
+        meshInterface = bullet_new<btTriangleIndexVertexArray>();
+
+        size_t partCount = data->parts.size();
+        if (partCount > 0)
         {
-            meshPart = data->parts[i];
-            GP_ASSERT(meshPart);
+            PHY_ScalarType indexType = PHY_UCHAR;
+            int indexStride = 0;
+            Bundle::MeshPartData* meshPart = NULL;
+            for (size_t i = 0; i < partCount; i++)
+            {
+                meshPart = data->parts[i];
+                GP_ASSERT(meshPart);
 
-            switch (meshPart->indexFormat)
+                switch (meshPart->indexFormat)
+                {
+                case Mesh::INDEX8:
+                    indexType = PHY_UCHAR;
+                    indexStride = 1;
+                    break;
+                case Mesh::INDEX16:
+                    indexType = PHY_SHORT;
+                    indexStride = 2;
+                    break;
+                case Mesh::INDEX32:
+                    indexType = PHY_INTEGER;
+                    indexStride = 4;
+                    break;
+                default:
+                    GP_ERROR("Unsupported index format (%d).", meshPart->indexFormat);
+                    SAFE_DELETE(meshInterface);
+                    SAFE_DELETE_ARRAY(shapeMeshData->vertexData);
+                    SAFE_DELETE(shapeMeshData);
+                    SAFE_DELETE(data);
+                    return NULL;
+                }
+
+                // Move the index data into the rigid body's local buffer.
+                // Set it to NULL in the MeshPartData so it is not released when the data is freed.
+                shapeMeshData->indexData.push_back(meshPart->indexData);
+                meshPart->indexData = NULL;
+
+                // Create a btIndexedMesh object for the current mesh part.
+                btIndexedMesh indexedMesh;
+                indexedMesh.m_indexType = indexType;
+                indexedMesh.m_numTriangles = meshPart->indexCount / 3; // assume TRIANGLES primitive type
+                indexedMesh.m_numVertices = meshPart->indexCount;
+                indexedMesh.m_triangleIndexBase = (const unsigned char*)shapeMeshData->indexData[i];
+                indexedMesh.m_triangleIndexStride = indexStride*3;
+                indexedMesh.m_vertexBase = (const unsigned char*)shapeMeshData->vertexData;
+                indexedMesh.m_vertexStride = sizeof(float)*3;
+                indexedMesh.m_vertexType = PHY_FLOAT;
+
+                // Add the indexed mesh data to the mesh interface.
+                meshInterface->addIndexedMesh(indexedMesh, indexType);
+            }
+        }
+        else
+        {
+            // Generate index data for the mesh locally in the rigid body.
+            unsigned int* indexData = new unsigned int[data->vertexCount];
+            for (unsigned int i = 0; i < data->vertexCount; i++)
             {
-            case Mesh::INDEX8:
-                indexType = PHY_UCHAR;
-                indexStride = 1;
-                break;
-            case Mesh::INDEX16:
-                indexType = PHY_SHORT;
-                indexStride = 2;
-                break;
-            case Mesh::INDEX32:
-                indexType = PHY_INTEGER;
-                indexStride = 4;
-                break;
-            default:
-                GP_ERROR("Unsupported index format (%d).", meshPart->indexFormat);
-                SAFE_DELETE(meshInterface);
-                SAFE_DELETE_ARRAY(shapeMeshData->vertexData);
-                SAFE_DELETE(shapeMeshData);
-                SAFE_DELETE(data);
-                return NULL;
+                indexData[i] = i;
             }
+            shapeMeshData->indexData.push_back((unsigned char*)indexData);
 
-            // Move the index data into the rigid body's local buffer.
-            // Set it to NULL in the MeshPartData so it is not released when the data is freed.
-            shapeMeshData->indexData.push_back(meshPart->indexData);
-            meshPart->indexData = NULL;
-
-            // Create a btIndexedMesh object for the current mesh part.
+            // Create a single btIndexedMesh object for the mesh interface.
             btIndexedMesh indexedMesh;
-            indexedMesh.m_indexType = indexType;
-            indexedMesh.m_numTriangles = meshPart->indexCount / 3; // assume TRIANGLES primitive type
-            indexedMesh.m_numVertices = meshPart->indexCount;
-            indexedMesh.m_triangleIndexBase = (const unsigned char*)shapeMeshData->indexData[i];
-            indexedMesh.m_triangleIndexStride = indexStride*3;
+            indexedMesh.m_indexType = PHY_INTEGER;
+            indexedMesh.m_numTriangles = data->vertexCount / 3; // assume TRIANGLES primitive type
+            indexedMesh.m_numVertices = data->vertexCount;
+            indexedMesh.m_triangleIndexBase = shapeMeshData->indexData[0];
+            indexedMesh.m_triangleIndexStride = sizeof(unsigned int);
             indexedMesh.m_vertexBase = (const unsigned char*)shapeMeshData->vertexData;
             indexedMesh.m_vertexStride = sizeof(float)*3;
             indexedMesh.m_vertexType = PHY_FLOAT;
 
-            // Add the indexed mesh data to the mesh interface.
-            meshInterface->addIndexedMesh(indexedMesh, indexType);
+            // Set the data in the mesh interface.
+            meshInterface->addIndexedMesh(indexedMesh, indexedMesh.m_indexType);
         }
-    }
-    else
-    {
-        // Generate index data for the mesh locally in the rigid body.
-        unsigned int* indexData = new unsigned int[data->vertexCount];
-        for (unsigned int i = 0; i < data->vertexCount; i++)
-        {
-            indexData[i] = i;
-        }
-        shapeMeshData->indexData.push_back((unsigned char*)indexData);
-
-        // Create a single btIndexedMesh object for the mesh interface.
-        btIndexedMesh indexedMesh;
-        indexedMesh.m_indexType = PHY_INTEGER;
-        indexedMesh.m_numTriangles = data->vertexCount / 3; // assume TRIANGLES primitive type
-        indexedMesh.m_numVertices = data->vertexCount;
-        indexedMesh.m_triangleIndexBase = shapeMeshData->indexData[0];
-        indexedMesh.m_triangleIndexStride = sizeof(unsigned int);
-        indexedMesh.m_vertexBase = (const unsigned char*)shapeMeshData->vertexData;
-        indexedMesh.m_vertexStride = sizeof(float)*3;
-        indexedMesh.m_vertexType = PHY_FLOAT;
-
-        // Set the data in the mesh interface.
-        meshInterface->addIndexedMesh(indexedMesh, indexedMesh.m_indexType);
+
+        // Create our collision shape object and store shapeMeshData in it.
+        collisionShape = bullet_new<btBvhTriangleMeshShape>(meshInterface, true);
     }
 
     // Create our collision shape object and store shapeMeshData in it.
-    PhysicsCollisionShape* shape =
-        new PhysicsCollisionShape(PhysicsCollisionShape::SHAPE_MESH, bullet_new<btBvhTriangleMeshShape>(meshInterface, true), meshInterface);
+    PhysicsCollisionShape* shape = new PhysicsCollisionShape(PhysicsCollisionShape::SHAPE_MESH, collisionShape, meshInterface);
     shape->_shapeData.meshData = shapeMeshData;
 
     _shapes.push_back(shape);

+ 2 - 2
gameplay/src/PhysicsController.h

@@ -419,7 +419,7 @@ private:
 
     // Creates a collision shape for the given node and gameplay shape definition.
     // Populates 'centerOfMassOffset' with the correct calculated center of mass offset.
-    PhysicsCollisionShape* createShape(Node* node, const PhysicsCollisionShape::Definition& shape, Vector3* centerOfMassOffset);
+    PhysicsCollisionShape* createShape(Node* node, const PhysicsCollisionShape::Definition& shape, Vector3* centerOfMassOffset, bool dynamic);
     
     // Creates a box collision shape.
     PhysicsCollisionShape* createBox(const Vector3& extents, const Vector3& scale);
@@ -434,7 +434,7 @@ private:
     PhysicsCollisionShape* createHeightfield(Node* node, HeightField* heightfield, Vector3* centerOfMassOffset);
 
     // Creates a triangle mesh collision shape.
-    PhysicsCollisionShape* createMesh(Mesh* mesh, const Vector3& scale);
+    PhysicsCollisionShape* createMesh(Mesh* mesh, const Vector3& scale, bool dynamic);
 
     // Destroys a collision shape created through PhysicsController
     void destroyShape(PhysicsCollisionShape* shape);

+ 1 - 1
gameplay/src/PhysicsGhostObject.cpp

@@ -14,7 +14,7 @@ PhysicsGhostObject::PhysicsGhostObject(Node* node, const PhysicsCollisionShape::
     GP_ASSERT(physicsController);
 
     // Create and set the collision shape for the ghost object.
-    _collisionShape = physicsController->createShape(node, shape, &centerOfMassOffset);
+    _collisionShape = physicsController->createShape(node, shape, &centerOfMassOffset, false);
     GP_ASSERT(_collisionShape);
 
     // Create the ghost object.

+ 2 - 2
gameplay/src/PhysicsRigidBody.cpp

@@ -18,7 +18,7 @@ PhysicsRigidBody::PhysicsRigidBody(Node* node, const PhysicsCollisionShape::Defi
 
     // Create our collision shape.
     Vector3 centerOfMassOffset;
-    _collisionShape = Game::getInstance()->getPhysicsController()->createShape(node, shape, &centerOfMassOffset);
+    _collisionShape = Game::getInstance()->getPhysicsController()->createShape(node, shape, &centerOfMassOffset, parameters.mass != 0.0f);
     GP_ASSERT(_collisionShape && _collisionShape->getShape());
 
     // Create motion state object.
@@ -28,7 +28,7 @@ PhysicsRigidBody::PhysicsRigidBody(Node* node, const PhysicsCollisionShape::Defi
     // inertia. However, if the collision shape is a triangle mesh, we don't calculate 
     // inertia since Bullet doesn't currently support this.
     btVector3 localInertia(0.0, 0.0, 0.0);
-    if (parameters.mass != 0.0 && _collisionShape->getType() != PhysicsCollisionShape::SHAPE_MESH)
+    if (parameters.mass != 0.0)
         _collisionShape->getShape()->calculateLocalInertia(parameters.mass, localInertia);
 
     // Create the Bullet physics rigid body object.

+ 5 - 1
gameplay/src/PlatformAndroid.cpp

@@ -555,6 +555,8 @@ static Keyboard::Key getKey(int keycode, int metastate)
             return Keyboard::KEY_MENU;
         case AKEYCODE_SEARCH:
             return Keyboard::KEY_SEARCH;
+        case AKEYCODE_BACK:
+            return Keyboard::KEY_ESCAPE;
         default:
             return Keyboard::KEY_NONE;
     }
@@ -872,7 +874,7 @@ static int32_t engine_handle_input(struct android_app* app, AInputEvent* event)
         int32_t action = AKeyEvent_getAction(event);
         int32_t keycode = AKeyEvent_getKeyCode(event);
         int32_t metastate = AKeyEvent_getMetaState(event); 
-        
+
         switch(action)
         {
             case AKEY_EVENT_ACTION_DOWN:
@@ -885,6 +887,8 @@ static int32_t engine_handle_input(struct android_app* app, AInputEvent* event)
                 gameplay::Platform::keyEventInternal(Keyboard::KEY_RELEASE, getKey(keycode, metastate));
                 break;
         }
+
+        return 1;
     }
     return 0;
 }

+ 5 - 0
gameplay/src/RadioButton.cpp

@@ -66,12 +66,17 @@ bool RadioButton::isSelected() const
 
 void RadioButton::setSelected(bool selected)
 {
+    if (selected != _selected)
+    {
+        _dirty = true;
+    }
     _selected = selected;
 }
 
 void RadioButton::setImageSize(float width, float height)
 {
     _imageSize.set(width, height);
+    _dirty = true;
 }
 
 const Vector2& RadioButton::getImageSize() const

+ 51 - 2
gameplay/src/RenderState.cpp

@@ -18,6 +18,7 @@
 #define RS_STENCIL_WRITE 256
 #define RS_STENCIL_FUNC 512
 #define RS_STENCIL_OP 1024
+#define RS_FRONT_FACE 2048
 
 #define RS_ALL_ONES 0xFFFFFFFF
 
@@ -513,7 +514,7 @@ void RenderState::cloneInto(RenderState* renderState, NodeCloneContext& context)
 RenderState::StateBlock::StateBlock()
     : _cullFaceEnabled(false), _depthTestEnabled(false), _depthWriteEnabled(true), _depthFunction(RenderState::DEPTH_LESS),
       _blendEnabled(false), _blendSrc(RenderState::BLEND_ONE), _blendDst(RenderState::BLEND_ZERO),
-	  _stencilTestEnabled(false), _stencilWrite(RS_ALL_ONES), 
+      _cullFaceSide(CULL_FACE_SIDE_BACK), _frontFace(FRONT_FACE_CCW), _stencilTestEnabled(false), _stencilWrite(RS_ALL_ONES), 
 	  _stencilFunction(RenderState::STENCIL_ALWAYS), _stencilFunctionRef(0), _stencilFunctionMask(RS_ALL_ONES), 
 	  _stencilOpSfail(RenderState::STENCIL_OP_KEEP), _stencilOpDpfail(RenderState::STENCIL_OP_KEEP), _stencilOpDppass(RenderState::STENCIL_OP_KEEP),
       _bits(0L)
@@ -578,6 +579,11 @@ void RenderState::StateBlock::bindNoRestore()
         GL_ASSERT( glCullFace((GLenum)_cullFaceSide) );
         _defaultState->_cullFaceSide = _cullFaceSide;
     }
+    if ((_bits & RS_FRONT_FACE) && (_frontFace != _defaultState->_frontFace))
+    {
+        GL_ASSERT( glFrontFace((GLenum)_frontFace) );
+        _defaultState->_frontFace = _frontFace;
+    }
     if ((_bits & RS_DEPTH_TEST) && (_depthTestEnabled != _defaultState->_depthTestEnabled))
     {
         if (_depthTestEnabled) 
@@ -667,6 +673,12 @@ void RenderState::StateBlock::restore(long stateOverrideBits)
         _defaultState->_bits &= ~RS_CULL_FACE_SIDE;
         _defaultState->_cullFaceSide = RenderState::CULL_FACE_SIDE_BACK;
     }
+    if (!(stateOverrideBits & RS_FRONT_FACE) && (_defaultState->_bits & RS_FRONT_FACE))
+    {
+        GL_ASSERT( glFrontFace((GLenum)GL_CCW) );
+        _defaultState->_bits &= ~RS_FRONT_FACE;
+        _defaultState->_frontFace = RenderState::FRONT_FACE_CCW;
+    }
     if (!(stateOverrideBits & RS_DEPTH_TEST) && (_defaultState->_bits & RS_DEPTH_TEST))
     {
         GL_ASSERT( glDisable(GL_DEPTH_TEST) );
@@ -742,6 +754,7 @@ void RenderState::StateBlock::cloneInto(StateBlock* state)
     state->_blendSrc = _blendSrc;
     state->_blendDst = _blendDst;
     state->_cullFaceSide = _cullFaceSide;
+    state->_frontFace = _frontFace;
 	state->_stencilTestEnabled = _stencilTestEnabled;
 	state->_stencilWrite = _stencilWrite;
 	state->_stencilFunction = _stencilFunction;
@@ -882,11 +895,29 @@ static RenderState::CullFaceSide parseCullFaceSide(const char* value)
         return RenderState::CULL_FACE_SIDE_FRONT_AND_BACK;
     else
     {
-        GP_ERROR("Unsupported cull face side value (%s). Will default to BACK if errors are treated as warnings)", value);
+        GP_ERROR("Unsupported cull face side value (%s). Will default to BACK if errors are treated as warnings.", value);
         return RenderState::CULL_FACE_SIDE_BACK;
     }
 }
 
+static RenderState::FrontFace parseFrontFace(const char* value)
+{
+    GP_ASSERT(value);
+
+    // Convert string to uppercase for comparison
+    std::string upper(value);
+    std::transform(upper.begin(), upper.end(), upper.begin(), (int(*)(int))toupper);
+    if (upper == "CCW")
+        return RenderState::FRONT_FACE_CCW;
+    else if (upper == "CW")
+        return RenderState::FRONT_FACE_CW;
+    else
+    {
+        GP_ERROR("Unsupported front face side value (%s). Will default to CCW if errors are treated as warnings.", value);
+        return RenderState::FRONT_FACE_CCW;
+    }
+}
+
 static RenderState::StencilFunction parseStencilFunc(const char* value)
 {
     GP_ASSERT(value);
@@ -971,6 +1002,10 @@ void RenderState::StateBlock::setState(const char* name, const char* value)
     {
         setCullFaceSide(parseCullFaceSide(value));
     }
+    else if (strcmp(name, "frontFace") == 0)
+    {
+        setFrontFace(parseFrontFace(value));
+    }
     else if (strcmp(name, "depthTest") == 0)
     {
         setDepthTest(parseBoolean(value));
@@ -1089,6 +1124,20 @@ void RenderState::StateBlock::setCullFaceSide(CullFaceSide side)
     }
 }
 
+void RenderState::StateBlock::setFrontFace(FrontFace winding)
+{
+    _frontFace = winding;
+    if (_frontFace == FRONT_FACE_CCW)
+    {
+        // Default front face
+        _bits &= ~RS_FRONT_FACE;
+    }
+    else
+    {
+        _bits |= RS_FRONT_FACE;
+    }
+}
+
 void RenderState::StateBlock::setDepthTest(bool enabled)
 {
     _depthTestEnabled = enabled;

+ 22 - 0
gameplay/src/RenderState.h

@@ -180,6 +180,17 @@ public:
         CULL_FACE_SIDE_FRONT_AND_BACK = GL_FRONT_AND_BACK
     };
 
+    /**
+     * Defines the winding of vertices in faces that are considered front facing.
+     *
+     * The initial front face mode is set to FRONT_FACE_CCW.
+     */
+    enum FrontFace
+    {
+        FRONT_FACE_CW,
+        FRONT_FACE_CCW
+    };
+
 	/**
      * Defines the supported stencil compare functions.
 	 * 
@@ -280,10 +291,20 @@ public:
          * Sets the side of the facets to cull.
          *
          * When not explicitly set, the default is to cull back-facing facets.
+         *
          * @param side The side to cull.
          */
         void setCullFaceSide(CullFaceSide side);
 
+        /**
+         * Sets the winding for front facing polygons.
+         *
+         * By default, counter-clockwise wound polygons are considered front facing.
+         *
+         * @param winding The winding for front facing polygons.
+         */
+        void setFrontFace(FrontFace winding);
+
         /**
          * Toggles depth testing.
          *
@@ -396,6 +417,7 @@ public:
         Blend _blendSrc;
         Blend _blendDst;
         CullFaceSide _cullFaceSide;
+        FrontFace _frontFace;
 		bool _stencilTestEnabled;
 		unsigned int _stencilWrite;
 		StencilFunction _stencilFunction;

+ 1 - 1
gameplay/src/Scene.cpp

@@ -254,7 +254,7 @@ void Scene::addNode(Node* node)
         node->getParent()->removeChild(node);
     }
 
-    // Link the new node into our list.
+    // Link the new node into the end of our list.
     if (_lastNode)
     {
         _lastNode->_nextSibling = node;

+ 335 - 288
gameplay/src/SceneLoader.cpp

@@ -13,6 +13,10 @@ namespace gameplay
 extern void calculateNamespacePath(const std::string& urlString, std::string& fileString, std::vector<std::string>& namespacePath);
 extern Properties* getPropertiesFromNamespacePath(Properties* properties, const std::vector<std::string>& namespacePath);
 
+SceneLoader::SceneLoader() : _scene(NULL)
+{
+}
+
 Scene* SceneLoader::load(const char* url)
 {
     SceneLoader loader;
@@ -55,12 +59,11 @@ Scene* SceneLoader::loadInternal(const char* url)
     loadReferencedFiles();
 
     // Load the main scene data from GPB and apply the global scene properties.
-    Scene* scene = NULL;
     if (!_gpbPath.empty())
     {
         // Load scene from bundle
-        scene = loadMainSceneData(sceneProperties);
-        if (!scene)
+        _scene = loadMainSceneData(sceneProperties);
+        if (!_scene)
         {
             GP_ERROR("Failed to load main scene from bundle.");
             SAFE_DELETE(properties);
@@ -70,7 +73,7 @@ Scene* SceneLoader::loadInternal(const char* url)
     else
     {
         // Create a new empty scene
-        scene = Scene::create(sceneProperties->getId());
+        _scene = Scene::create(sceneProperties->getId());
     }
 
     // First apply the node url properties. Following that,
@@ -78,8 +81,8 @@ Scene* SceneLoader::loadInternal(const char* url)
     // We apply physics properties after all other node properties
     // so that the transform (SRT) properties get applied before
     // processing physics collision objects.
-    applyNodeUrls(scene);
-    applyNodeProperties(scene, sceneProperties, 
+    applyNodeUrls();
+    applyNodeProperties(sceneProperties, 
         SceneNodeProperty::AUDIO | 
         SceneNodeProperty::MATERIAL | 
         SceneNodeProperty::PARTICLE |
@@ -89,39 +92,34 @@ Scene* SceneLoader::loadInternal(const char* url)
         SceneNodeProperty::ROTATE |
         SceneNodeProperty::SCALE |
         SceneNodeProperty::TRANSLATE);
-    applyNodeProperties(scene, sceneProperties, SceneNodeProperty::COLLISION_OBJECT);
+    applyNodeProperties(sceneProperties, SceneNodeProperty::COLLISION_OBJECT);
 
     // Apply node tags
     for (size_t i = 0, sncount = _sceneNodes.size(); i < sncount; ++i)
     {
-        SceneNode& sceneNode = _sceneNodes[i];
-        for (std::map<std::string, std::string>::const_iterator itr = sceneNode._tags.begin(); itr != sceneNode._tags.end(); ++itr)
-        {
-            for (size_t n = 0, ncount = sceneNode._nodes.size(); n < ncount; ++n)
-                sceneNode._nodes[n]->setTag(itr->first.c_str(), itr->second.c_str());
-        }
+        applyTags(_sceneNodes[i]);
     }
 
     // Set active camera
     const char* activeCamera = sceneProperties->getString("activeCamera");
     if (activeCamera)
     {
-        Node* camera = scene->findNode(activeCamera);
+        Node* camera = _scene->findNode(activeCamera);
         if (camera && camera->getCamera())
-            scene->setActiveCamera(camera->getCamera());
+            _scene->setActiveCamera(camera->getCamera());
     }
 
     // Set ambient and light properties
     Vector3 vec3;
     if (sceneProperties->getVector3("ambientColor", &vec3))
-        scene->setAmbientColor(vec3.x, vec3.y, vec3.z);
+        _scene->setAmbientColor(vec3.x, vec3.y, vec3.z);
     if (sceneProperties->getVector3("lightColor", &vec3))
-        scene->setLightColor(vec3.x, vec3.y, vec3.z);
+        _scene->setLightColor(vec3.x, vec3.y, vec3.z);
     if (sceneProperties->getVector3("lightDirection", &vec3))
-        scene->setLightDirection(vec3);
+        _scene->setLightDirection(vec3);
 
     // Create animations for scene
-    createAnimations(scene);
+    createAnimations();
 
     // Find the physics properties object.
     Properties* physics = NULL;
@@ -138,7 +136,7 @@ Scene* SceneLoader::loadInternal(const char* url)
 
     // Load physics properties and constraints.
     if (physics)
-        loadPhysics(physics, scene);
+        loadPhysics(physics);
 
     // Clean up all loaded properties objects.
     std::map<std::string, Properties*>::iterator iter = _propertiesFromFile.begin();
@@ -150,7 +148,23 @@ Scene* SceneLoader::loadInternal(const char* url)
     // Clean up the .scene file's properties object.
     SAFE_DELETE(properties);
 
-    return scene;
+    return _scene;
+}
+
+void SceneLoader::applyTags(SceneNode& sceneNode)
+{
+    // Apply tags for this scene node
+    for (std::map<std::string, std::string>::const_iterator itr = sceneNode._tags.begin(); itr != sceneNode._tags.end(); ++itr)
+    {
+        for (size_t n = 0, ncount = sceneNode._nodes.size(); n < ncount; ++n)
+            sceneNode._nodes[n]->setTag(itr->first.c_str(), itr->second.c_str());
+    }
+
+    // Process children
+    for (size_t i = 0, count = sceneNode._children.size(); i < count; ++i)
+    {
+        applyTags(sceneNode._children[i]);
+    }
 }
 
 void SceneLoader::addSceneAnimation(const char* animationID, const char* targetID, const char* url)
@@ -191,25 +205,35 @@ void SceneLoader::addSceneNodeProperty(SceneNode& sceneNode, SceneNodeProperty::
     sceneNode._properties.push_back(prop);
 }
 
-void SceneLoader::applyNodeProperties(const Scene* scene, const Properties* sceneProperties, unsigned int typeFlags)
+void SceneLoader::applyNodeProperties(const Properties* sceneProperties, unsigned int typeFlags)
 {
-    for (size_t i = 0, sncount = _sceneNodes.size(); i < sncount; ++i)
+    for (size_t i = 0, count = _sceneNodes.size(); i < count; ++i)
     {
-        SceneNode& sceneNode = _sceneNodes[i];
+        applyNodeProperties(_sceneNodes[i], sceneProperties, typeFlags);
+    }
+}
 
-        for (size_t p = 0, pcount = sceneNode._properties.size(); p < pcount; ++p)
+void SceneLoader::applyNodeProperties(SceneNode& sceneNode, const Properties* sceneProperties, unsigned int typeFlags)
+{
+    // Apply properties for this node
+    for (size_t i = 0, pcount = sceneNode._properties.size(); i < pcount; ++i)
+    {
+        SceneNodeProperty& snp = sceneNode._properties[i];
+        if (typeFlags & snp._type)
         {
-            SceneNodeProperty& snp = sceneNode._properties[p];
-            if (typeFlags & snp._type)
-            {
-                for (size_t n = 0, ncount = sceneNode._nodes.size(); n < ncount; ++n)
-                    applyNodeProperty(sceneNode, sceneNode._nodes[n], sceneProperties, snp, scene);
-            }
+            for (size_t k = 0, ncount = sceneNode._nodes.size(); k < ncount; ++k)
+                applyNodeProperty(sceneNode, sceneNode._nodes[k], sceneProperties, snp);
         }
     }
+
+    // Apply properties to child nodes
+    for (size_t i = 0, ccount = sceneNode._children.size(); i < ccount; ++i)
+    {
+        applyNodeProperties(sceneNode._children[i], sceneProperties, typeFlags);
+    }
 }
 
-void SceneLoader::applyNodeProperty(SceneNode& sceneNode, Node* node, const Properties* sceneProperties, const SceneNodeProperty& snp, const Scene* scene)
+void SceneLoader::applyNodeProperty(SceneNode& sceneNode, Node* node, const Properties* sceneProperties, const SceneNodeProperty& snp)
 {
     if (snp._type == SceneNodeProperty::AUDIO ||
         snp._type == SceneNodeProperty::MATERIAL ||
@@ -303,8 +327,7 @@ void SceneLoader::applyNodeProperty(SceneNode& sceneNode, Node* node, const Prop
 
                 if (name)
                 {
-                    GP_ASSERT(scene);
-                    Node* modelNode = scene->findNode(name);
+                    Node* modelNode = _scene->findNode(name);
                     if (!modelNode)
                     {
                         GP_ERROR("Node '%s' does not exist; attempting to use its model for collision object creation.", name);
@@ -383,158 +406,172 @@ void SceneLoader::applyNodeProperty(SceneNode& sceneNode, Node* node, const Prop
     }
 }
 
-void SceneLoader::applyNodeUrls(Scene* scene)
+void SceneLoader::applyNodeUrls()
 {
-    GP_ASSERT(scene);
-
     // Apply all URL node properties so that when we go to apply
     // the other node properties, the node is in the scene.
-    for (size_t i = 0, ncount = _sceneNodes.size(); i < ncount; ++i)
+    for (size_t i = 0, count = _sceneNodes.size(); i < count; ++i)
     {
-        SceneNode& sceneNode = _sceneNodes[i];
+        applyNodeUrls(_sceneNodes[i], NULL);
+    }
+}
 
-        // Iterate backwards over the properties list so we can remove properties as we go
-        // without danger of indexing out of bounds.
-        bool hasURL = false;
-        for (int j = (int)sceneNode._properties.size() - 1; j >= 0; --j)
-        {
-            SceneNodeProperty& snp = sceneNode._properties[j];
-            if (snp._type != SceneNodeProperty::URL)
-                continue;
+void SceneLoader::applyNodeUrls(SceneNode& sceneNode, Node* parent)
+{
+    // Iterate backwards over the properties list so we can remove properties as we go
+    // without danger of indexing out of bounds.
+    bool hasURL = false;
+    for (int j = (int)sceneNode._properties.size() - 1; j >= 0; --j)
+    {
+        SceneNodeProperty& snp = sceneNode._properties[j];
+        if (snp._type != SceneNodeProperty::URL)
+            continue; // skip nodes without urls
 
-            hasURL = true;
+        hasURL = true;
 
-            std::string file;
-            std::string id;
-            splitURL(snp._url, &file, &id);
+        std::string file;
+        std::string id;
+        splitURL(snp._url, &file, &id);
 
-            if (file.empty())
+        if (file.empty())
+        {
+            // The node is from the main GPB and should just be renamed.
+
+            // TODO: Should we do all nodes with this case first to allow users to stitch in nodes with
+            // IDs equal to IDs that were in the original GPB file but were changed in the scene file?
+            if (sceneNode._exactMatch)
+            {
+                Node* node = parent ? parent->findNode(id.c_str()) : _scene->findNode(id.c_str());
+                if (node)
+                {
+                    node->setId(sceneNode._nodeID);
+                }
+                else
+                {
+                    GP_ERROR("Could not find node '%s' in main scene GPB file.", id.c_str());
+                }
+                sceneNode._nodes.push_back(node);
+            }
+            else
             {
-                // The node is from the main GPB and should just be renamed.
+                // Search for nodes using a partial match
+                std::vector<Node*> nodes;
+                unsigned int nodeCount = parent ? parent->findNodes(id.c_str(), nodes, true, false) : _scene->findNodes(id.c_str(), nodes, true, false);
+                if (nodeCount > 0)
+                {
+                    for (unsigned int k = 0; k < nodeCount; ++k)
+                    {
+                        // Construct a new node ID using _nodeID plus the remainder of the partial match.
+                        Node* node = nodes[k];
+                        std::string newID(sceneNode._nodeID);
+                        newID += (node->getId() + id.length());
+                        node->setId(newID.c_str());
+                        sceneNode._nodes.push_back(node);
+                    }
+                }
+                else
+                {
+                    GP_ERROR("Could not find any nodes matching '%s' in main scene GPB file.", id.c_str());
+                }
+            }
+        }
+        else
+        {
+            // An external file was referenced, so load the node(s) from file and then insert it into the scene with the new ID.
 
-                // TODO: Should we do all nodes with this case first to allow users to stitch in nodes with
-                // IDs equal to IDs that were in the original GPB file but were changed in the scene file?
+            // TODO: Revisit this to determine if we should cache Bundle objects for the duration of the scene
+            // load to prevent constantly creating/destroying the same externally referenced bundles each time
+            // a url with a file is encountered.
+            Bundle* tmpBundle = Bundle::create(file.c_str());
+            if (tmpBundle)
+            {
                 if (sceneNode._exactMatch)
                 {
-                    Node* node = scene->findNode(id.c_str());
+                    Node* node = tmpBundle->loadNode(id.c_str(), _scene);
                     if (node)
                     {
                         node->setId(sceneNode._nodeID);
+                        parent ? parent->addChild(node) : _scene->addNode(node);
+                        sceneNode._nodes.push_back(node);
+                        SAFE_RELEASE(node);
                     }
                     else
                     {
-                        GP_ERROR("Could not find node '%s' in main scene GPB file.", id.c_str());
+                        GP_ERROR("Could not load node '%s' from GPB file '%s'.", id.c_str(), file.c_str());
                     }
-                    sceneNode._nodes.push_back(node);
                 }
                 else
                 {
-                    // Search for nodes using a partial match
-                    std::vector<Node*> nodes;
-                    unsigned int nodeCount = scene->findNodes(id.c_str(), nodes, true, false);
-                    if (nodeCount > 0)
+                    // Search for nodes in the package using a partial match
+                    unsigned int objectCount = tmpBundle->getObjectCount();
+                    unsigned int matchCount = 0;
+                    for (unsigned int k = 0; k < objectCount; ++k)
                     {
-                        for (unsigned int k = 0; k < nodeCount; ++k)
+                        const char* objid = tmpBundle->getObjectId(k);
+                        if (strstr(objid, id.c_str()) == objid)
                         {
-                            // Construct a new node ID using _nodeID plus the remainder of the partial match.
-                            Node* node = nodes[k];
-                            std::string newID(sceneNode._nodeID);
-                            newID += (node->getId() + id.length());
-                            node->setId(newID.c_str());
-                            sceneNode._nodes.push_back(node);
+                            // This object ID matches (starts with).
+                            // Try to load this object as a Node.
+                            Node* node = tmpBundle->loadNode(objid);
+                            if (node)
+                            {
+                                // Construct a new node ID using _nodeID plus the remainder of the partial match.
+                                std::string newID(sceneNode._nodeID);
+                                newID += (node->getId() + id.length());
+                                node->setId(newID.c_str());
+                                parent ? parent->addChild(node) : _scene->addNode(node);
+                                sceneNode._nodes.push_back(node);
+                                SAFE_RELEASE(node);
+                                matchCount++;
+                            }
                         }
                     }
-                    else
+                    if (matchCount == 0)
                     {
-                        GP_ERROR("Could not find any nodes matching '%s' in main scene GPB file.", id.c_str());
+                        GP_ERROR("Could not find any nodes matching '%s' in GPB file '%s'.", id.c_str(), file.c_str());
                     }
                 }
+
+                SAFE_RELEASE(tmpBundle);
             }
             else
             {
-                // An external file was referenced, so load the node(s) from file and then insert it into the scene with the new ID.
-
-                // TODO: Revisit this to determine if we should cache Bundle objects for the duration of the scene
-                // load to prevent constantly creating/destroying the same externally referenced bundles each time
-                // a url with a file is encountered.
-                Bundle* tmpBundle = Bundle::create(file.c_str());
-                if (tmpBundle)
-                {
-                    if (sceneNode._exactMatch)
-                    {
-                        Node* node = tmpBundle->loadNode(id.c_str(), scene);
-                        if (node)
-                        {
-                            node->setId(sceneNode._nodeID);
-                            scene->addNode(node);
-                            sceneNode._nodes.push_back(node);
-                            SAFE_RELEASE(node);
-                        }
-                        else
-                        {
-                            GP_ERROR("Could not load node '%s' from GPB file '%s'.", id.c_str(), file.c_str());
-                        }
-                    }
-                    else
-                    {
-                        // Search for nodes in the package using a partial match
-                        unsigned int objectCount = tmpBundle->getObjectCount();
-                        unsigned int matchCount = 0;
-                        for (unsigned int k = 0; k < objectCount; ++k)
-                        {
-                            const char* objid = tmpBundle->getObjectId(k);
-                            if (strstr(objid, id.c_str()) == objid)
-                            {
-                                // This object ID matches (starts with).
-                                // Try to load this object as a Node.
-                                Node* node = tmpBundle->loadNode(objid);
-                                if (node)
-                                {
-                                    // Construct a new node ID using _nodeID plus the remainder of the partial match.
-                                    std::string newID(sceneNode._nodeID);
-                                    newID += (node->getId() + id.length());
-                                    node->setId(newID.c_str());
-                                    scene->addNode(node);
-                                    sceneNode._nodes.push_back(node);
-                                    SAFE_RELEASE(node);
-                                    matchCount++;
-                                }
-                            }
-                        }
-                        if (matchCount == 0)
-                        {
-                            GP_ERROR("Could not find any nodes matching '%s' in GPB file '%s'.", id.c_str(), file.c_str());
-                        }
-                    }
-
-                    SAFE_RELEASE(tmpBundle);
-                }
-                else
-                {
-                    GP_ERROR("Failed to load GPB file '%s' for node stitching.", file.c_str());
-                }
+                GP_ERROR("Failed to load GPB file '%s' for node stitching.", file.c_str());
             }
+        }
 
-            // Remove the 'url' node property since we are done applying it.
-            sceneNode._properties.erase(sceneNode._properties.begin() + j);
+        // Remove the 'url' node property since we are done applying it.
+        sceneNode._properties.erase(sceneNode._properties.begin() + j);
 
-            // Processed URL property, no need to inspect remaining properties
-            break;
+        // Processed URL property, no need to inspect remaining properties
+        break;
+    }
+
+    if (!hasURL)
+    {
+        // No explicit URL, find the node in the main scene with the existing ID
+        Node* node = parent ? parent->findNode(sceneNode._nodeID) : _scene->findNode(sceneNode._nodeID);
+        if (node)
+        {
+            sceneNode._nodes.push_back(node);
         }
+        else
+        {
+            // There is no node in the scene with this ID, so create a new empty node
+            node = Node::create(sceneNode._nodeID);
+            parent ? parent->addChild(node) : _scene->addNode(node);
+            node->release();
+            sceneNode._nodes.push_back(node);
+        }
+    }
 
-        if (!hasURL)
+    // Apply to child nodes
+    for (size_t i = 0, count = sceneNode._nodes.size(); i < count; ++i)
+    {
+        Node* parent = sceneNode._nodes[i];
+        for (size_t j = 0, childCount = sceneNode._children.size(); j < childCount; ++j)
         {
-            // No explicit URL, find the node in the main scene with the existing ID
-            Node* node = scene->findNode(sceneNode._nodeID);
-            if (node)
-            {
-                sceneNode._nodes.push_back(node);
-            }
-            else
-            {
-                // There is no node in the scene with this ID, so create a new empty node
-                sceneNode._nodes.push_back(scene->addNode(sceneNode._nodeID));
-            }
+            applyNodeUrls(sceneNode._children[j], parent);
         }
     }
 }
@@ -543,7 +580,6 @@ void SceneLoader::buildReferenceTables(Properties* sceneProperties)
 {
     // Go through the child namespaces of the scene.
     Properties* ns;
-    const char* name = NULL;
     while ((ns = sceneProperties->getNextNamespace()) != NULL)
     {
         if (strcmp(ns->getNamespace(), "node") == 0)
@@ -554,139 +590,7 @@ void SceneLoader::buildReferenceTables(Properties* sceneProperties)
                 continue;
             }
 
-            // Add a SceneNode to the end of the list.
-            _sceneNodes.resize(_sceneNodes.size() + 1);
-            SceneNode& sceneNode = _sceneNodes[_sceneNodes.size()-1];
-            sceneNode._nodeID = ns->getId();
-
-            // Parse the node's sub-namespaces.
-            Properties* subns;
-            std::string propertyUrl = _path + "#" + ns->getId() + "/";
-            while ((subns = ns->getNextNamespace()) != NULL)
-            {
-                if (strcmp(subns->getNamespace(), "audio") == 0)
-                {
-                    propertyUrl += "audio/" + std::string(subns->getId());
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::AUDIO, propertyUrl.c_str());
-                    _properties[propertyUrl] = subns;
-                }
-                else if (strcmp(subns->getNamespace(), "material") == 0)
-                {
-                    propertyUrl += "material/" + std::string(subns->getId());
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::MATERIAL, propertyUrl.c_str());
-                    _properties[propertyUrl] = subns;
-                }
-                else if (strcmp(subns->getNamespace(), "particle") == 0)
-                {
-                    propertyUrl += "particle/" + std::string(subns->getId());
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::PARTICLE, propertyUrl.c_str());
-                    _properties[propertyUrl] = subns;
-                }
-                else if (strcmp(subns->getNamespace(), "terrain") == 0)
-                {
-                    propertyUrl += "terrain/" + std::string(subns->getId());
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::TERRAIN, propertyUrl.c_str());
-                    _properties[propertyUrl] = subns;
-                }
-                else if (strcmp(subns->getNamespace(), "light") == 0)
-                {
-                    propertyUrl += "light/" + std::string(subns->getId());
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::LIGHT, propertyUrl.c_str());
-                    _properties[propertyUrl] = subns;
-                }
-                else if (strcmp(subns->getNamespace(), "camera") == 0)
-                {
-                    propertyUrl += "camera/" + std::string(subns->getId());
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::CAMERA, propertyUrl.c_str());
-                    _properties[propertyUrl] = subns;
-                }
-                else if (strcmp(subns->getNamespace(), "collisionObject") == 0)
-                {
-                    propertyUrl += "collisionObject/" + std::string(subns->getId());
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::COLLISION_OBJECT, propertyUrl.c_str());
-                    _properties[propertyUrl] = subns;
-                }
-                else if (strcmp(subns->getNamespace(), "tags") == 0)
-                {
-                    while ((name = subns->getNextProperty()) != NULL)
-                    {
-                        sceneNode._tags[name] = subns->getString();
-                    }
-                }
-                else
-                {
-                    GP_ERROR("Unsupported child namespace '%s' of 'node' namespace.", subns->getNamespace());
-                }
-            }
-
-            // Parse the node's attributes.
-            while ((name = ns->getNextProperty()) != NULL)
-            {
-                if (strcmp(name, "url") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::URL, ns->getString());
-                }
-                else if (strcmp(name, "audio") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::AUDIO, ns->getString());
-                }
-                else if (strncmp(name, "material", 8) == 0)
-                {
-                    int materialIndex = -1;
-                    name = strchr(name, '[');
-                    if (name && strlen(name) >= 3)
-                    {
-                        std::string indexString(name);
-                        indexString = indexString.substr(1, indexString.size()-2);
-                        materialIndex = (unsigned int)atoi(indexString.c_str());
-                    }
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::MATERIAL, ns->getString(), materialIndex);
-                }
-                else if (strcmp(name, "particle") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::PARTICLE, ns->getString());
-                }
-                else if (strcmp(name, "terrain") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::TERRAIN, ns->getString());
-                }
-                else if (strcmp(name, "light") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::LIGHT, ns->getString());
-                }
-                else if (strcmp(name, "camera") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::CAMERA, ns->getString());
-                }
-                else if (strcmp(name, "collisionObject") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::COLLISION_OBJECT, ns->getString());
-                }
-                else if (strcmp(name, "rigidBodyModel") == 0)
-                {
-                    // Ignore this for now. We process this when we do rigid body creation.
-                }
-                else if (strcmp(name, "collisionMesh") == 0)
-                {
-                    // Ignore this for now (new alias for rigidBodyModel). We process this when we do rigid body creation.
-                }
-                else if (strcmp(name, "translate") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::TRANSLATE);
-                }
-                else if (strcmp(name, "rotate") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::ROTATE);
-                }
-                else if (strcmp(name, "scale") == 0)
-                {
-                    addSceneNodeProperty(sceneNode, SceneNodeProperty::SCALE);
-                }
-                else
-                {
-                    GP_ERROR("Unsupported node property: %s = %s", name, ns->getString());
-                }
-            }
+            parseNode(ns, NULL, _path + "#" + ns->getId() + "/");
         }
         else if (strcmp(ns->getNamespace(), "animations") == 0)
         {
@@ -737,14 +641,158 @@ void SceneLoader::buildReferenceTables(Properties* sceneProperties)
     }
 }
 
-void SceneLoader::createAnimations(const Scene* scene)
+void SceneLoader::parseNode(Properties* ns, SceneNode* parent, const std::string& path)
+{
+    std::string propertyUrl;
+    const char* name = NULL;
+
+    // Add a SceneNode to the end of the list.
+    std::vector<SceneNode>& list = parent ? parent->_children : _sceneNodes;
+    list.push_back(SceneNode());
+    SceneNode& sceneNode = list[list.size()-1];
+    sceneNode._nodeID = ns->getId();
+
+    // Parse the node's sub-namespaces.
+    Properties* subns;
+    while ((subns = ns->getNextNamespace()) != NULL)
+    {
+        if (strcmp(subns->getNamespace(), "node") == 0)
+        {
+            parseNode(subns, &sceneNode, path + subns->getId() + "/");
+        }
+        else if (strcmp(subns->getNamespace(), "audio") == 0)
+        {
+            propertyUrl = path + "audio/" + std::string(subns->getId());
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::AUDIO, propertyUrl.c_str());
+            _properties[propertyUrl] = subns;
+        }
+        else if (strcmp(subns->getNamespace(), "material") == 0)
+        {
+            propertyUrl = path + "material/" + std::string(subns->getId());
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::MATERIAL, propertyUrl.c_str());
+            _properties[propertyUrl] = subns;
+        }
+        else if (strcmp(subns->getNamespace(), "particle") == 0)
+        {
+            propertyUrl = path + "particle/" + std::string(subns->getId());
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::PARTICLE, propertyUrl.c_str());
+            _properties[propertyUrl] = subns;
+        }
+        else if (strcmp(subns->getNamespace(), "terrain") == 0)
+        {
+            propertyUrl = path + "terrain/" + std::string(subns->getId());
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::TERRAIN, propertyUrl.c_str());
+            _properties[propertyUrl] = subns;
+        }
+        else if (strcmp(subns->getNamespace(), "light") == 0)
+        {
+            propertyUrl = path + "light/" + std::string(subns->getId());
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::LIGHT, propertyUrl.c_str());
+            _properties[propertyUrl] = subns;
+        }
+        else if (strcmp(subns->getNamespace(), "camera") == 0)
+        {
+            propertyUrl = path + "camera/" + std::string(subns->getId());
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::CAMERA, propertyUrl.c_str());
+            _properties[propertyUrl] = subns;
+        }
+        else if (strcmp(subns->getNamespace(), "collisionObject") == 0)
+        {
+            propertyUrl = path + "collisionObject/" + std::string(subns->getId());
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::COLLISION_OBJECT, propertyUrl.c_str());
+            _properties[propertyUrl] = subns;
+        }
+        else if (strcmp(subns->getNamespace(), "tags") == 0)
+        {
+            while ((name = subns->getNextProperty()) != NULL)
+            {
+                sceneNode._tags[name] = subns->getString();
+            }
+        }
+        else
+        {
+            GP_ERROR("Unsupported child namespace '%s' of 'node' namespace.", subns->getNamespace());
+        }
+    }
+
+    // Parse the node's attributes.
+    while ((name = ns->getNextProperty()) != NULL)
+    {
+        if (strcmp(name, "url") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::URL, ns->getString());
+        }
+        else if (strcmp(name, "audio") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::AUDIO, ns->getString());
+        }
+        else if (strncmp(name, "material", 8) == 0)
+        {
+            int materialIndex = -1;
+            name = strchr(name, '[');
+            if (name && strlen(name) >= 3)
+            {
+                std::string indexString(name);
+                indexString = indexString.substr(1, indexString.size()-2);
+                materialIndex = (unsigned int)atoi(indexString.c_str());
+            }
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::MATERIAL, ns->getString(), materialIndex);
+        }
+        else if (strcmp(name, "particle") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::PARTICLE, ns->getString());
+        }
+        else if (strcmp(name, "terrain") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::TERRAIN, ns->getString());
+        }
+        else if (strcmp(name, "light") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::LIGHT, ns->getString());
+        }
+        else if (strcmp(name, "camera") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::CAMERA, ns->getString());
+        }
+        else if (strcmp(name, "collisionObject") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::COLLISION_OBJECT, ns->getString());
+        }
+        else if (strcmp(name, "rigidBodyModel") == 0)
+        {
+            // Ignore this for now. We process this when we do rigid body creation.
+        }
+        else if (strcmp(name, "collisionMesh") == 0)
+        {
+            // Ignore this for now (new alias for rigidBodyModel). We process this when we do rigid body creation.
+        }
+        else if (strcmp(name, "translate") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::TRANSLATE);
+        }
+        else if (strcmp(name, "rotate") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::ROTATE);
+        }
+        else if (strcmp(name, "scale") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::SCALE);
+        }
+        else
+        {
+            GP_ERROR("Unsupported node property: %s = %s", name, ns->getString());
+        }
+    }
+}
+
+void SceneLoader::createAnimations()
 {
     // Create the scene animations.
     for (size_t i = 0, count = _animations.size(); i < count; i++)
     {
         // If the target node doesn't exist in the scene, then we
         // can't do anything so we skip to the next animation.
-        Node* node = scene->findNode(_animations[i]._targetID);
+        Node* node = _scene->findNode(_animations[i]._targetID);
         if (!node)
         {
             GP_ERROR("Attempting to create an animation targeting node '%s', which does not exist in the scene.", _animations[i]._targetID);
@@ -892,10 +940,9 @@ Scene* SceneLoader::loadMainSceneData(const Properties* sceneProperties)
     return scene;
 }
 
-void SceneLoader::loadPhysics(Properties* physics, Scene* scene)
+void SceneLoader::loadPhysics(Properties* physics)
 {
     GP_ASSERT(physics);
-    GP_ASSERT(scene);
     GP_ASSERT(Game::getInstance()->getPhysicsController());
 
     // Go through the supported global physics properties and apply them.
@@ -920,7 +967,7 @@ void SceneLoader::loadPhysics(Properties* physics, Scene* scene)
                 GP_ERROR("Missing property 'rigidBodyA' for constraint '%s'.", constraint->getId());
                 continue;
             }
-            Node* rbANode = scene->findNode(name);
+            Node* rbANode = _scene->findNode(name);
             if (!rbANode)
             {
                 GP_ERROR("Node '%s' to be used as 'rigidBodyA' for constraint '%s' cannot be found.", name, constraint->getId());
@@ -941,7 +988,7 @@ void SceneLoader::loadPhysics(Properties* physics, Scene* scene)
             PhysicsRigidBody* rbB = NULL;
             if (name)
             {
-                Node* rbBNode = scene->findNode(name);
+                Node* rbBNode = _scene->findNode(name);
                 if (!rbBNode)
                 {
                     GP_ERROR("Node '%s' to be used as 'rigidBodyB' for constraint '%s' cannot be found.", name, constraint->getId());

+ 25 - 12
gameplay/src/SceneLoader.h

@@ -72,28 +72,40 @@ private:
 
         const char* _nodeID;
         bool _exactMatch;
-        std::vector<Node*> _nodes;
+        Properties* _namespace;
+        std::vector<Node*> _nodes; // list of nodes sharing properties defined in this SceneNode
+        std::vector<SceneNode> _children; // list of unique child nodes
         std::vector<SceneNodeProperty> _properties;
         std::map<std::string, std::string> _tags;
     };
 
+    SceneLoader();
+
     Scene* loadInternal(const char* url);
 
+    void applyTags(SceneNode& sceneNode);
+
     void addSceneAnimation(const char* animationID, const char* targetID, const char* url);
 
     void addSceneNodeProperty(SceneNode& sceneNode, SceneNodeProperty::Type type, const char* url = NULL, int index = 0);
 
-    void applyNodeProperties(const Scene* scene, const Properties* sceneProperties, unsigned int typeFlags);
+    void applyNodeProperties(const Properties* sceneProperties, unsigned int typeFlags);
+
+    void applyNodeProperties(SceneNode& sceneNode, const Properties* sceneProperties, unsigned int typeFlags);
 
-    void applyNodeProperty(SceneNode& sceneNode, Node* node, const Properties* sceneProperties, const SceneNodeProperty& snp, const Scene* scene);
+    void applyNodeProperty(SceneNode& sceneNode, Node* node, const Properties* sceneProperties, const SceneNodeProperty& snp);
 
-    void applyNodeUrls(Scene* scene);
+    void applyNodeUrls();
+
+    void applyNodeUrls(SceneNode& sceneNode, Node* parent);
 
     void buildReferenceTables(Properties* sceneProperties);
 
+    void parseNode(Properties* ns, SceneNode* parent, const std::string& path);
+
     void calculateNodesWithMeshRigidBodies(const Properties* sceneProperties);
 
-    void createAnimations(const Scene* scene);
+    void createAnimations();
 
     PhysicsConstraint* loadGenericConstraint(const Properties* constraint, PhysicsRigidBody* rbA, PhysicsRigidBody* rbB);
 
@@ -101,7 +113,7 @@ private:
 
     Scene* loadMainSceneData(const Properties* sceneProperties);
 
-    void loadPhysics(Properties* physics, Scene* scene);
+    void loadPhysics(Properties* physics);
 
     void loadReferencedFiles();
 
@@ -109,12 +121,13 @@ private:
 
     PhysicsConstraint* loadSpringConstraint(const Properties* constraint, PhysicsRigidBody* rbA, PhysicsRigidBody* rbB);
 
-    std::map<std::string, Properties*> _propertiesFromFile;      // Holds the properties object for a given file.
-    std::map<std::string, Properties*> _properties;              // Holds the properties object for a given URL.
-    std::vector<SceneAnimation> _animations;                     // Holds the animations declared in the .scene file.
-    std::vector<SceneNode> _sceneNodes;                          // Holds all the nodes+properties declared in the .scene file.
-    std::string _gpbPath;                                        // The path of the main GPB for the scene being loaded.
-    std::string _path;                                           // The path of the scene file being loaded.
+    std::map<std::string, Properties*> _propertiesFromFile; // Holds the properties object for a given file.
+    std::map<std::string, Properties*> _properties;         // Holds the properties object for a given URL.
+    std::vector<SceneAnimation> _animations;                // Holds the animations declared in the .scene file.
+    std::vector<SceneNode> _sceneNodes;                     // Holds all the nodes+properties declared in the .scene file.
+    std::string _gpbPath;                                   // The path of the main GPB for the scene being loaded.
+    std::string _path;                                      // The path of the scene file being loaded.
+    Scene* _scene;                                          // The scene being loaded
 };
 
 /**

+ 1 - 0
gameplay/src/ScriptController.cpp

@@ -22,6 +22,7 @@
     /* Get the size of the array. */ \
     lua_len(sc->_lua, index); \
     int size = luaL_checkint(sc->_lua, -1); \
+    lua_pop(sc->_lua, 1); \
     if (size <= 0) \
         return LuaArray<type>((type*)NULL); \
     \

+ 1 - 0
gameplay/src/ScriptController.inl

@@ -121,6 +121,7 @@ ScriptUtil::LuaArray<T> ScriptUtil::getObjectPointer(int index, const char* type
         // Get the size of the array.
         lua_len(sc->_lua, index);
         int size = luaL_checkint(sc->_lua, -1);
+        lua_pop(sc->_lua, 1);
 
         if (size <= 0)
         {

+ 25 - 0
gameplay/src/Slider.cpp

@@ -59,6 +59,10 @@ Slider* Slider::create(Theme::Style* style, Properties* properties)
 
 void Slider::setMin(float min)
 {
+    if (_min != _min)
+    {
+        _dirty = true;
+    }
     _min = min;
 }
 
@@ -69,6 +73,10 @@ float Slider::getMin() const
 
 void Slider::setMax(float max)
 {
+    if (max != _max)
+    {
+        _dirty = true;
+    }
     _max = max;
 }
 
@@ -94,11 +102,20 @@ float Slider::getValue() const
 
 void Slider::setValue(float value)
 {
+    float oldValue = _value;
     _value = MATH_CLAMP(value, _min, _max);
+    if (_value != value)
+    {
+        _dirty = true;
+    }
 }
 
 void Slider::setValueTextVisible(bool valueTextVisible)
 {
+    if (valueTextVisible != _valueTextVisible)
+    {
+        _dirty = true;
+    }
     _valueTextVisible = valueTextVisible;
 }
 
@@ -109,6 +126,10 @@ bool Slider::isValueTextVisible() const
 
 void Slider::setValueTextAlignment(Font::Justify alignment)
 {
+    if (alignment != _alignment)
+    {
+        _dirty = true;
+    }
     _valueTextAlignment = alignment;
 }
 
@@ -119,6 +140,10 @@ Font::Justify Slider::getValueTextAlignment() const
 
 void Slider::setValueTextPrecision(unsigned int precision)
 {
+    if (precision != _valueTextPrecision)
+    {
+        _dirty = true;
+    }
     _valueTextPrecision = precision;
 }
 

+ 8 - 4
gameplay/src/SpriteBatch.cpp

@@ -406,17 +406,19 @@ bool SpriteBatch::clipSprite(const Rectangle& clip, float& x, float& y, float& w
         return false;
     }
 
-    const float uvWidth = u2 - u1;
-    const float uvHeight = v2 - v1;
+    float uvWidth = u2 - u1;
+    float uvHeight = v2 - v1;
 
     // Moving x to the right.
     if (x < clip.x)
     {
         const float percent = (clip.x - x) / width;
         const float dx = clip.x - x;
+        const float du = uvWidth * percent;
         x = clip.x;
         width -= dx;
-        u1 += uvWidth * percent;
+        u1 += du;
+        uvWidth -= du;
     }
 
     // Moving y down.
@@ -424,9 +426,11 @@ bool SpriteBatch::clipSprite(const Rectangle& clip, float& x, float& y, float& w
     {
         const float percent = (clip.y - y) / height;
         const float dy = clip.y - y;
+        const float dv = uvHeight * percent;
         y = clip.y;
         height -= dy;
-        v1 += uvHeight * percent;
+        v1 += dv;
+        uvHeight -= dv;
     }
 
     // Moving width to the left.

+ 2 - 1
gameplay/src/TextBox.cpp

@@ -397,7 +397,8 @@ void TextBox::drawImages(SpriteBatch* spriteBatch, const Rectangle& clip)
             Vector4 color = _caretImage->getColor();
             color.w *= _opacity;
 
-            spriteBatch->draw(_caretLocation.x - (region.width / 2.0f), _caretLocation.y, region.width, _fontSize, uvs.u1, uvs.v1, uvs.u2, uvs.v2, color, _viewportClipBounds);
+            float caretWidth = region.width * _fontSize / region.height;
+            spriteBatch->draw(_caretLocation.x - caretWidth * 0.5f, _caretLocation.y, caretWidth, _fontSize, uvs.u1, uvs.v1, uvs.u2, uvs.v2, color, _viewportClipBounds);
         }
     }
 

+ 9 - 0
gameplay/src/Texture.cpp

@@ -39,6 +39,11 @@
 #define ATC_RGBA_INTERPOLATED_ALPHA_AMD 0x87EE
 #endif
 
+// ETC1 (OES_compressed_ETC1_RGB8_texture) : All OpenGL ES chipsets
+#ifndef ETC1_RGB8
+#define ETC1_RGB8 0x8D64
+#endif
+
 namespace gameplay
 {
 
@@ -624,6 +629,10 @@ Texture* Texture::createCompressedDDS(const char* path)
             format = internalFormat = ATC_RGBA_INTERPOLATED_ALPHA_AMD;
             bytesPerBlock = 16;
             break;
+        case ('E'|('T'<<8)|('C'<<16)|('1'<<24)):
+            format = internalFormat = ETC1_RGB8;
+            bytesPerBlock = 8;
+            break;
         default:
             GP_ERROR("Unsupported compressed texture format (%d) for DDS file '%s'.", header.ddspf.dwFourCC, path);
             SAFE_DELETE_ARRAY(mipLevels);

+ 73 - 0
gameplay/src/lua/lua_Font.cpp

@@ -20,6 +20,7 @@ void luaRegister_Font()
         {"createText", lua_Font_createText},
         {"drawText", lua_Font_drawText},
         {"finish", lua_Font_finish},
+        {"getCharacterSpacing", lua_Font_getCharacterSpacing},
         {"getIndexAtLocation", lua_Font_getIndexAtLocation},
         {"getLocationAtIndex", lua_Font_getLocationAtIndex},
         {"getRefCount", lua_Font_getRefCount},
@@ -27,6 +28,7 @@ void luaRegister_Font()
         {"getSpriteBatch", lua_Font_getSpriteBatch},
         {"measureText", lua_Font_measureText},
         {"release", lua_Font_release},
+        {"setCharacterSpacing", lua_Font_setCharacterSpacing},
         {"start", lua_Font_start},
         {NULL, NULL}
     };
@@ -1086,6 +1088,41 @@ int lua_Font_finish(lua_State* state)
     return 0;
 }
 
+int lua_Font_getCharacterSpacing(lua_State* state)
+{
+    // Get the number of parameters.
+    int paramCount = lua_gettop(state);
+
+    // Attempt to match the parameters to a valid binding.
+    switch (paramCount)
+    {
+        case 1:
+        {
+            if ((lua_type(state, 1) == LUA_TUSERDATA))
+            {
+                Font* instance = getInstance(state);
+                float result = instance->getCharacterSpacing();
+
+                // Push the return value onto the stack.
+                lua_pushnumber(state, result);
+
+                return 1;
+            }
+
+            lua_pushstring(state, "lua_Font_getCharacterSpacing - Failed to match the given parameters to a valid function signature.");
+            lua_error(state);
+            break;
+        }
+        default:
+        {
+            lua_pushstring(state, "Invalid number of parameters (expected 1).");
+            lua_error(state);
+            break;
+        }
+    }
+    return 0;
+}
+
 int lua_Font_getIndexAtLocation(lua_State* state)
 {
     // Get the number of parameters.
@@ -1945,6 +1982,42 @@ int lua_Font_release(lua_State* state)
     return 0;
 }
 
+int lua_Font_setCharacterSpacing(lua_State* state)
+{
+    // Get the number of parameters.
+    int paramCount = lua_gettop(state);
+
+    // Attempt to match the parameters to a valid binding.
+    switch (paramCount)
+    {
+        case 2:
+        {
+            if ((lua_type(state, 1) == LUA_TUSERDATA) &&
+                lua_type(state, 2) == LUA_TNUMBER)
+            {
+                // Get parameter 1 off the stack.
+                float param1 = (float)luaL_checknumber(state, 2);
+
+                Font* instance = getInstance(state);
+                instance->setCharacterSpacing(param1);
+                
+                return 0;
+            }
+
+            lua_pushstring(state, "lua_Font_setCharacterSpacing - Failed to match the given parameters to a valid function signature.");
+            lua_error(state);
+            break;
+        }
+        default:
+        {
+            lua_pushstring(state, "Invalid number of parameters (expected 2).");
+            lua_error(state);
+            break;
+        }
+    }
+    return 0;
+}
+
 int lua_Font_start(lua_State* state)
 {
     // Get the number of parameters.

+ 2 - 0
gameplay/src/lua/lua_Font.h

@@ -10,6 +10,7 @@ int lua_Font_addRef(lua_State* state);
 int lua_Font_createText(lua_State* state);
 int lua_Font_drawText(lua_State* state);
 int lua_Font_finish(lua_State* state);
+int lua_Font_getCharacterSpacing(lua_State* state);
 int lua_Font_getIndexAtLocation(lua_State* state);
 int lua_Font_getLocationAtIndex(lua_State* state);
 int lua_Font_getRefCount(lua_State* state);
@@ -17,6 +18,7 @@ int lua_Font_getSize(lua_State* state);
 int lua_Font_getSpriteBatch(lua_State* state);
 int lua_Font_measureText(lua_State* state);
 int lua_Font_release(lua_State* state);
+int lua_Font_setCharacterSpacing(lua_State* state);
 int lua_Font_start(lua_State* state);
 int lua_Font_static_create(lua_State* state);
 int lua_Font_static_getJustify(lua_State* state);

+ 1 - 1
samples/browser/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 2 - 1
samples/browser/android/jni/Application.mk

@@ -1 +1,2 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a

+ 2 - 2
samples/browser/res/common/physics.physics

@@ -34,8 +34,8 @@ collisionObject capsule
 collisionObject duck
 {
     type = RIGID_BODY
-    shape = BOX
-    mass = 1.0
+    shape = MESH
+    mass = 5.0
     friction = 1.0
     restitution = 0.0
     linearDamping = 0.5

+ 1 - 1
samples/character/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 3 - 1
samples/character/android/jni/Application.mk

@@ -1 +1,3 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a
+

+ 1 - 1
samples/longboard/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 3 - 1
samples/longboard/android/jni/Application.mk

@@ -1 +1,3 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a
+

+ 1 - 1
samples/lua/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 3 - 1
samples/lua/android/jni/Application.mk

@@ -1 +1,3 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a
+

+ 1 - 1
samples/mesh/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 3 - 1
samples/mesh/android/jni/Application.mk

@@ -1 +1,3 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a
+

+ 1 - 1
samples/particles/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 3 - 1
samples/particles/android/jni/Application.mk

@@ -1 +1,3 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a
+

+ 1 - 1
samples/racer/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 3 - 1
samples/racer/android/jni/Application.mk

@@ -1 +1,3 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a
+

+ 1 - 1
samples/spaceship/android/jni/Android.mk

@@ -9,7 +9,7 @@ VORBIS_PATH := $(call my-dir)/../../../../external-deps/oggvorbis/lib/android/ar
 OPENAL_PATH := $(call my-dir)/../../../../external-deps/openal/lib/android/arm
 
 # gameplay
-LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi
+LOCAL_PATH := $(call my-dir)/../../../../gameplay/android/obj/local/armeabi-v7a
 include $(CLEAR_VARS)
 LOCAL_MODULE    := libgameplay
 LOCAL_SRC_FILES := libgameplay.a

+ 3 - 1
samples/spaceship/android/jni/Application.mk

@@ -1 +1,3 @@
-APP_STL     := stlport_static
+APP_STL     := stlport_static
+APP_ABI     := armeabi-v7a
+