Ver Fonte

Added Terrain support:
- New Terrain class for rendering heightmap based terrains. Terrains can be attached to Nodes, similar to Models.
- Terrain LOD support, multiple surface layers (texture splatting), flexible heightfield sizes, full transform (including scale) support, physics, and more.
- Header files are well documented and additional documentation and samples will be provided later.
- Removed the ability to create heightfield collision shapes from Images as this was error prone and not very accurate. Replaced this with a Terrain::HeightField class that can now be used to construct both heightfield collision shapes, as well as Terrain.
- Added a object-space normal map generation to gameplay-encoder, which is useful for generating terrain normal maps. This option can generate normal maps from PNG, RAW8 and RAW16 heightmap images.

Various other small changes and fixes:
- Added support for setting the depth compare function on render state.
- Added support for texture/sampler arrays being passed to materials.
- Fixed a couple minor memory leaks in the physics code, which were likely to be affecting anyone up to this point.
- Fixed typo in PhysicsRigidBody::Parameters constructor initializer that caused restitution to not get initialized properly, which could lead to erratic behavior for the rigid body.
- Fixed an issue where physics debug drawing for large scenes would cause the internal MeshBatch to grow to an enormous size, using large amounts of memory.
- Added support for preprocessor directive NO_LUA_BINDINGS in the gameplay project to omit inclusion of generated lua bindings in compilation. This does not disable the LUA runtime, but simply prevents all generated lua bindings from being compiled into gameplay. This is mainly a development feature to speed up compile time when making frequent changes to gameplay header files (which otherwise would result in a recompile of all lua bindings). In order for this to work you also need to remove all lua_ files from your project.

Steve Grenier há 13 anos atrás
pai
commit
138bb45f51
41 ficheiros alterados com 3421 adições e 530 exclusões
  1. 4 0
      gameplay-encoder/gameplay-encoder.vcxproj
  2. 12 0
      gameplay-encoder/gameplay-encoder.vcxproj.filters
  3. 193 48
      gameplay-encoder/src/EncoderArguments.cpp
  4. 42 1
      gameplay-encoder/src/EncoderArguments.h
  5. 1 1
      gameplay-encoder/src/GPBFile.cpp
  6. 41 29
      gameplay-encoder/src/Heightmap.cpp
  7. 3 2
      gameplay-encoder/src/Heightmap.h
  8. 262 0
      gameplay-encoder/src/Image.cpp
  9. 123 0
      gameplay-encoder/src/Image.h
  10. 268 0
      gameplay-encoder/src/NormalMapGenerator.cpp
  11. 35 0
      gameplay-encoder/src/NormalMapGenerator.h
  12. 18 0
      gameplay-encoder/src/main.cpp
  13. 4 0
      gameplay/gameplay.vcxproj
  14. 12 0
      gameplay/gameplay.vcxproj.filters
  15. 33 3
      gameplay/src/Effect.cpp
  16. 9 0
      gameplay/src/Effect.h
  17. 95 78
      gameplay/src/MaterialParameter.cpp
  18. 20 1
      gameplay/src/MaterialParameter.h
  19. 11 8
      gameplay/src/Model.h
  20. 36 8
      gameplay/src/Node.cpp
  21. 25 3
      gameplay/src/Node.h
  22. 3 4
      gameplay/src/PhysicsCharacter.cpp
  23. 10 3
      gameplay/src/PhysicsCollisionObject.cpp
  24. 10 3
      gameplay/src/PhysicsCollisionObject.h
  25. 124 104
      gameplay/src/PhysicsCollisionShape.cpp
  26. 39 11
      gameplay/src/PhysicsCollisionShape.h
  27. 80 190
      gameplay/src/PhysicsController.cpp
  28. 4 5
      gameplay/src/PhysicsController.h
  29. 4 5
      gameplay/src/PhysicsGhostObject.cpp
  30. 34 12
      gameplay/src/PhysicsRigidBody.cpp
  31. 5 6
      gameplay/src/PhysicsRigidBody.h
  32. 27 1
      gameplay/src/RenderState.cpp
  33. 34 0
      gameplay/src/RenderState.h
  34. 6 0
      gameplay/src/ScriptController.cpp
  35. 650 0
      gameplay/src/Terrain.cpp
  36. 405 0
      gameplay/src/Terrain.h
  37. 585 0
      gameplay/src/TerrainPatch.cpp
  38. 138 0
      gameplay/src/TerrainPatch.h
  39. 7 4
      gameplay/src/Texture.cpp
  40. 7 0
      gameplay/src/Texture.h
  41. 2 0
      gameplay/src/gameplay.h

+ 4 - 0
gameplay-encoder/gameplay-encoder.vcxproj

@@ -31,6 +31,7 @@
     <ClCompile Include="src\GPBDecoder.cpp" />
     <ClCompile Include="src\Animations.cpp" />
     <ClCompile Include="src\Heightmap.cpp" />
+    <ClCompile Include="src\Image.cpp" />
     <ClCompile Include="src\Light.cpp" />
     <ClCompile Include="src\main.cpp" />
     <ClCompile Include="src\Material.cpp" />
@@ -41,6 +42,7 @@
     <ClCompile Include="src\MeshPart.cpp" />
     <ClCompile Include="src\MeshSkin.cpp" />
     <ClCompile Include="src\Node.cpp" />
+    <ClCompile Include="src\NormalMapGenerator.cpp" />
     <ClCompile Include="src\Object.cpp" />
     <ClCompile Include="src\Quaternion.cpp" />
     <ClCompile Include="src\Reference.cpp" />
@@ -76,6 +78,7 @@
     <ClInclude Include="src\GPBDecoder.h" />
     <ClInclude Include="src\Animations.h" />
     <ClInclude Include="src\Heightmap.h" />
+    <ClInclude Include="src\Image.h" />
     <ClInclude Include="src\Light.h" />
     <ClInclude Include="src\Material.h" />
     <ClInclude Include="src\MaterialParameter.h" />
@@ -85,6 +88,7 @@
     <ClInclude Include="src\MeshPart.h" />
     <ClInclude Include="src\MeshSkin.h" />
     <ClInclude Include="src\Node.h" />
+    <ClInclude Include="src\NormalMapGenerator.h" />
     <ClInclude Include="src\Object.h" />
     <ClInclude Include="src\Quaternion.h" />
     <ClInclude Include="src\Reference.h" />

+ 12 - 0
gameplay-encoder/gameplay-encoder.vcxproj.filters

@@ -130,6 +130,12 @@
     <ClCompile Include="src\Heightmap.cpp">
       <Filter>src</Filter>
     </ClCompile>
+    <ClCompile Include="src\Image.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
+    <ClCompile Include="src\NormalMapGenerator.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="src\VertexElement.h">
@@ -261,6 +267,12 @@
     <ClInclude Include="src\Heightmap.h">
       <Filter>src</Filter>
     </ClInclude>
+    <ClInclude Include="src\Image.h">
+      <Filter>src</Filter>
+    </ClInclude>
+    <ClInclude Include="src\NormalMapGenerator.h">
+      <Filter>src</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <None Include="src\Vector2.inl">

+ 193 - 48
gameplay-encoder/src/EncoderArguments.cpp

@@ -8,6 +8,8 @@
     #define realpath(A,B)    _fullpath(B,A,PATH_MAX)
 #endif
 
+#define MAX_HEIGHTMAP_SIZE 2049
+
 namespace gameplay
 {
 
@@ -17,6 +19,7 @@ extern int __logVerbosity = 1;
 
 EncoderArguments::EncoderArguments(size_t argc, const char** argv) :
     _fontSize(0),
+    _normalMap(false),
     _parseError(false),
     _fontPreview(false),
     _textOutput(false),
@@ -25,6 +28,8 @@ EncoderArguments::EncoderArguments(size_t argc, const char** argv) :
 {
     __instance = this;
 
+    memset(_heightmapResolution, 0, sizeof(int) * 2);
+
     if (argc > 1)
     {
         // read the options
@@ -91,27 +96,41 @@ std::string EncoderArguments::getOutputDirPath() const
     }
 }
 
+std::string EncoderArguments::getOutputFileExtension() const
+{
+    switch (getFileFormat())
+    {
+    case FILEFORMAT_PNG:
+    case FILEFORMAT_RAW:
+        if (_normalMap)
+            return ".png";
+
+    default:
+        return ".gpb";
+    }
+}
+
 std::string EncoderArguments::getOutputFilePath() const
 {
     if (_fileOutputPath.size() > 0)
     {
+        // Output file explicitly set
         return _fileOutputPath;
     }
     else
     {
+        // Generate an output file path
         int pos = _filePath.find_last_of('.');
-        if (pos > 0)
-        {
-            std::string outputFilePath(_filePath.substr(0, pos));
-            outputFilePath.append(".gpb");
-            return outputFilePath;
-        }
-        else
+        std::string outputFilePath(pos > 0 ? _filePath.substr(0, pos) : _filePath);
+
+        // Modify the original file name if the output extension can be the same as the input
+        if (_normalMap)
         {
-            std::string outputFilePath(_filePath);
-            outputFilePath.append(".gpb");
-            return outputFilePath;
+            outputFilePath.append("_normalmap");
         }
+
+        outputFilePath.append(getOutputFileExtension());
+        return outputFilePath;
     }
 }
 
@@ -152,6 +171,22 @@ const std::vector<EncoderArguments::HeightmapOption>& EncoderArguments::getHeigh
     return _heightmaps;
 }
 
+bool EncoderArguments::normalMapGeneration() const
+{
+    return _normalMap;
+}
+
+void EncoderArguments::getHeightmapResolution(int* x, int* y) const
+{
+    *x = _heightmapResolution[0];
+    *y = _heightmapResolution[1];
+}
+
+const Vector3& EncoderArguments::getHeightmapWorldSize() const
+{
+    return _heightmapWorldSize;
+}
+
 bool EncoderArguments::parseErrorOccured() const
 {
     return _parseError;
@@ -170,6 +205,21 @@ bool EncoderArguments::fileExists() const
     return false;
 }
 
+void splitString(const char* str, std::vector<std::string>* tokens)
+{
+    // Split node id list into tokens
+    unsigned int length = strlen(str);
+    char* temp = new char[length + 1];
+    strcpy(temp, str);
+    char* tok = strtok(temp, ",");
+    while (tok)
+    {
+        tokens->push_back(tok);
+        tok = strtok(NULL, ",");
+    }
+    delete[] temp;
+}
+
 void EncoderArguments::printUsage() const
 {
     LOG(1, "Usage: gameplay-encoder [options] <input filepath> <output filepath>\n\n");
@@ -191,13 +241,28 @@ void EncoderArguments::printUsage() const
         "\t\tremoving any channels that contain default/identity values\n" \
         "\t\tand removing any duplicate contiguous keyframes, which are common\n" \
         "\t\twhen exporting baked animation data.\n");
-    LOG(1, "  -h \"<node ids>\" <filename>\n" \
+    LOG(1, "  -h <size> \"<node ids>\" <filename>\n" \
         "\t\tGenerates a single heightmap image using meshes from the specified\n" \
-        "\t\tnodes. Node id list should be in quotes with a space between each id.\n" \
+        "\t\tnodes. <size> should be two comma-separated numbers in the format\n" \
+        "\t\t\"X,Y\", indicating the dimensions of the produced heightmap image.\n" \
+        "\t\t<node ids> should be in quotes with a space between each id.\n" \
         "\t\tFilename is the name of the image (PNG) to be saved.\n" \
         "\t\tMultiple -h arguments can be supplied to generate more than one heightmap.\n" \
         "\t\tFor 24-bit packed height data use -hp instead of -h.\n");
     LOG(1, "\n");
+    LOG(1, "Normal map generation options:\n" \
+        "  -n\t\tGenerate normal map (requires input file of type PNG or RAW)\n" \
+        "  -s\t\tSize/resolution of the input heightmap image (requried for RAW files)\n" \
+        "  -w <size>\tSpecifies the size of an input terran heightmap file in world\n" \
+        "\t\tunits, along the X, Y and Z axes. <size> should be three comma-separated\n" \
+        "\t\tnumbers in the format \"X,Y,Z\". The Y value represents the maximum\n" \
+        "\t\tpossible height value of a full intensity heightmap pixel.\n" \
+        "\n" \
+        "  Normal map generation can be used to create object-space normal maps from heightmap\n" \
+        "  images. Heightmaps must be in either PNG format (where the intensity of each pixel\n" \
+        "  represents a height value), or in RAW format (8 or 16-bit), which is a common\n" \
+        "  headerless format supported by most terran generation tools.\n");
+    LOG(1, "\n");
     LOG(1, "TTF file options:\n");
     LOG(1, "  -s <size>\tSize of the font.\n");
     LOG(1, "  -p\t\tOutput font preview.\n");
@@ -252,24 +317,34 @@ EncoderArguments::FileFormat EncoderArguments::getFileFormat() const
     {
         ext = _filePath.substr(pos + 1);
     }
+    for (size_t i = 0; i < ext.size(); ++i)
+        ext[i] = (char)tolower(ext[i]);
     
     // Match every supported extension with its format constant
-    if (ext.compare("dae") == 0 || ext.compare("DAE") == 0)
+    if (ext.compare("dae") == 0)
     {
         return FILEFORMAT_DAE;
     }
-    if (ext.compare("fbx") == 0 || ext.compare("FBX") == 0)
+    if (ext.compare("fbx") == 0)
     {
         return FILEFORMAT_FBX;
     }
-    if (ext.compare("ttf") == 0 || ext.compare("TTF") == 0)
+    if (ext.compare("ttf") == 0)
     {
         return FILEFORMAT_TTF;
     }
-    if (ext.compare("gpb") == 0 || ext.compare("GPB") == 0)
+    if (ext.compare("gpb") == 0)
     {
         return FILEFORMAT_GPB;
     }
+    if (ext.compare("png") == 0)
+    {
+        return FILEFORMAT_PNG;
+    }
+    if (ext.compare("raw") == 0)
+    {
+        return FILEFORMAT_RAW;
+    }
 
     return FILEFORMAT_UNKNOWN;
 }
@@ -342,25 +417,36 @@ void EncoderArguments::readOption(const std::vector<std::string>& options, size_
             if (str.compare("-heightmap") == 0 || str.compare("-h") == 0 || isHighPrecision)
             {
                 (*index)++;
-                if (*index < (options.size() + 1))
+                if (*index < (options.size() + 2))
                 {
                     _heightmaps.resize(_heightmaps.size() + 1);
                     HeightmapOption& heightmap = _heightmaps.back();
                     
                     heightmap.isHighPrecision = isHighPrecision;
 
-                    // Split node id list into tokens
-                    unsigned int length = options[*index].size() + 1;
-                    char* nodeIds = new char[length];
-                    strcpy(nodeIds, options[*index].c_str());
-                    nodeIds[length-1] = 0;
-                    char* id = strtok(nodeIds, " ");
-                    while (id)
+                    // Read heightmap size
+                    std::vector<std::string> parts;
+                    splitString(options[*index].c_str(), &parts);
+                    if (parts.size() != 2)
                     {
-                        heightmap.nodeIds.push_back(id);
-                        id = strtok(NULL, " ");
+                        LOG(1, "Error: invalid size argument for -h|-heightmap.\n");
+                        _parseError = true;
+                        return;
+                    }
+                    heightmap.width = atoi(parts[0].c_str());
+                    heightmap.height = atoi(parts[1].c_str());
+
+                    // Put some artificial bounds on heightmap dimensions
+                    if (heightmap.width <= 0 || heightmap.height <= 0 || heightmap.width > MAX_HEIGHTMAP_SIZE || heightmap.height > MAX_HEIGHTMAP_SIZE)
+                    {
+                        LOG(1, "Error: size argument for -h|-heightmap must be between (1,1) and (%d,%d).\n", (int)MAX_HEIGHTMAP_SIZE, (int)MAX_HEIGHTMAP_SIZE);
+                        _parseError = true;
+                        return;
                     }
-                    delete[] nodeIds;
+
+                    // Split node id list into tokens
+                    (*index)++;
+                    splitString(options[*index].c_str(), &heightmap.nodeIds);
 
                     // Store output filename
                     (*index)++;
@@ -391,34 +477,90 @@ void EncoderArguments::readOption(const std::vector<std::string>& options, size_
             }
         }
         break;
+    case 'n':
+        _normalMap = true;
+        break;
+    case 'w':
+        {
+            // Read world size
+            (*index)++;
+            if (*index >= options.size())
+            {
+                LOG(1, "Error: missing world size argument for -w.\n");
+                _parseError = true;
+                return;
+            }
+            std::vector<std::string> parts;
+            splitString(options[*index].c_str(), &parts);
+            if (parts.size() != 3)
+            {
+                LOG(1, "Error: invalid world size argument for -w.\n");
+                _parseError = true;
+                return;
+            }
+            _heightmapWorldSize.x = (float)atof(parts[0].c_str());
+            _heightmapWorldSize.y = (float)atof(parts[1].c_str());
+            _heightmapWorldSize.z = (float)atof(parts[2].c_str());
+            if (_heightmapWorldSize.x == 0 || _heightmapWorldSize.y == 0 || _heightmapWorldSize.z == 0)
+            {
+                LOG(1, "Error: invalid world size argument for -w.\n");
+                _parseError = true;
+                return;
+            }
+        }
+        break;
     case 'p':
         _fontPreview = true;
         break;
     case 's':
-        // Font Size
-
-        // old format was -s##
-        if (str.length() > 2)
+        if (_normalMap)
         {
-            char n = str[2];
-            if (n > '0' && n <= '9')
+            (*index)++;
+            if (*index >= options.size())
             {
-                const char* number = str.c_str() + 2;
-                _fontSize = atoi(number);
-                break;
+                LOG(1, "Error: missing argument for -s.\n");
+                _parseError = true;
+                return;
+            }
+            // Heightmap size
+            std::vector<std::string> parts;
+            splitString(options[*index].c_str(), &parts);
+            if (parts.size() != 2 ||
+                (_heightmapResolution[0] = atoi(parts[0].c_str())) <= 0 ||
+                (_heightmapResolution[1] = atoi(parts[1].c_str())) <= 0)
+            {
+                LOG(1, "Error: invalid argument for -s.\n");
+                _parseError = true;
+                return;
             }
-        }
-
-        (*index)++;
-        if (*index < options.size())
-        {
-            _fontSize = atoi(options[*index].c_str());
         }
         else
         {
-            LOG(1, "Error: missing arguemnt for -%c.\n", str[1]);
-            _parseError = true;
-            return;
+            // Font Size
+
+            // old format was -s##
+            if (str.length() > 2)
+            {
+                char n = str[2];
+                if (n > '0' && n <= '9')
+                {
+                    const char* number = str.c_str() + 2;
+                    _fontSize = atoi(number);
+                    break;
+                }
+            }
+
+            (*index)++;
+            if (*index < options.size())
+            {
+                _fontSize = atoi(options[*index].c_str());
+            }
+            else
+            {
+                LOG(1, "Error: missing arguemnt for -%c.\n", str[1]);
+                _parseError = true;
+                return;
+            }
         }
         break;
     case 't':
@@ -434,6 +576,7 @@ void EncoderArguments::readOption(const std::vector<std::string>& options, size_
             else if (__logVerbosity > 4)
                 __logVerbosity = 4;
         }
+        break;
     default:
         break;
     }
@@ -446,10 +589,12 @@ void EncoderArguments::setInputfilePath(const std::string& inputPath)
 
 void EncoderArguments::setOutputfilePath(const std::string& outputPath)
 {
+    std::string ext = getOutputFileExtension();
+
     if (outputPath.size() > 0 && outputPath[0] != '\0')
     {
         std::string realPath = getRealPath(outputPath);
-        if (endsWith(realPath.c_str(), ".gpb"))
+        if (endsWith(realPath.c_str(), ext.c_str()))
         {
             _fileOutputPath.assign(realPath);
         }
@@ -459,7 +604,7 @@ void EncoderArguments::setOutputfilePath(const std::string& outputPath)
 
             _fileOutputPath.assign(outputPath);
             _fileOutputPath.append(filenameNoExt);
-            _fileOutputPath.append(".gpb");
+            _fileOutputPath.append(ext);
         }
         else
         {
@@ -470,7 +615,7 @@ void EncoderArguments::setOutputfilePath(const std::string& outputPath)
                 _fileOutputPath = realPath.substr(0, pos);
                 _fileOutputPath.append("/");
                 _fileOutputPath.append(filenameNoExt);
-                _fileOutputPath.append(".gpb");
+                _fileOutputPath.append(ext);
             }
         }
     }

+ 42 - 1
gameplay-encoder/src/EncoderArguments.h

@@ -1,6 +1,8 @@
 #ifndef ENCODERARGUMENTS_H_
 #define ENCODERARGUMENTS_H_
 
+#include "Vector3.h"
+
 namespace gameplay
 {
 
@@ -17,7 +19,9 @@ public:
         FILEFORMAT_DAE,
         FILEFORMAT_FBX,
         FILEFORMAT_TTF,
-        FILEFORMAT_GPB
+        FILEFORMAT_GPB,
+        FILEFORMAT_PNG,
+        FILEFORMAT_RAW
     };
 
     struct HeightmapOption
@@ -25,6 +29,15 @@ public:
         std::vector<std::string> nodeIds;
         std::string filename;
         bool isHighPrecision;
+        int width;
+        int height;
+    };
+
+    struct NormalMapOption
+    {
+        std::string inputFile;
+        std::string outputFile;
+        Vector3 worldSize;
     };
 
     /**
@@ -74,6 +87,11 @@ public:
      */
     std::string getOutputFilePath() const;
 
+    /**
+     * Returns the output file extension.
+     */
+    std::string getOutputFileExtension() const;
+
     const std::vector<std::string>& getGroupAnimationNodeId() const;
     const std::vector<std::string>& getGroupAnimationAnimationId() const;
 
@@ -82,6 +100,25 @@ public:
 
     const std::vector<HeightmapOption>& getHeightmapOptions() const;
 
+    /**
+     * Returns true if normal map generation is turned on.
+     */
+    bool normalMapGeneration() const;
+    
+    /**
+     * Returns the supplied intput heightmap resolution.
+     *
+     * This option is only applicable for normal map generation.
+     */
+    void getHeightmapResolution(int* x, int* y) const;
+
+    /**
+     * Returns world size option.
+     *
+     * This option is only applicable for normal map generation.
+     */
+    const Vector3& getHeightmapWorldSize() const;
+    
     /**
      * Returns true if an error occurred while parsing the command line arguments.
      */
@@ -143,6 +180,10 @@ private:
 
     unsigned int _fontSize;
 
+    bool _normalMap;
+    Vector3 _heightmapWorldSize;
+    int _heightmapResolution[2];
+
     bool _parseError;
     bool _fontPreview;
     bool _textOutput;

+ 1 - 1
gameplay-encoder/src/GPBFile.cpp

@@ -338,7 +338,7 @@ void GPBFile::adjust()
     const std::vector<EncoderArguments::HeightmapOption>& heightmaps = EncoderArguments::getInstance()->getHeightmapOptions();
     for (unsigned int i = 0, count = heightmaps.size(); i < count; ++i)
     {
-        Heightmap::generate(heightmaps[i].nodeIds, heightmaps[i].filename.c_str(), heightmaps[i].isHighPrecision);
+        Heightmap::generate(heightmaps[i].nodeIds, heightmaps[i].width, heightmaps[i].height, heightmaps[i].filename.c_str(), heightmaps[i].isHighPrecision);
     }
 }
 

+ 41 - 29
gameplay-encoder/src/Heightmap.cpp

@@ -15,13 +15,17 @@ struct HeightmapThreadData
     const Vector3* rayDirection;        // [in]
     const std::vector<Mesh*>* meshes;   // [in]
     const BoundingVolume* bounds;       // [in]
-    int minX;                           // [in]
-    int maxX;                           // [in]
-    int minZ;                           // [in]
-    int maxZ;                           // [in]
+    float minX;                         // [in]
+    float maxX;                         // [in]
+    float minZ;                         // [in]
+    float maxZ;                         // [in]
+    float stepX;                        // [in]
+    float stepZ;                        // [in]
     float minHeight;                    // [out]
     float maxHeight;                    // [out]
     float* heights;                     // [in][out]
+    int width;                          // [in]
+    int height;                         // [in]
     int heightIndex;                    // [in]
 };
 
@@ -36,7 +40,7 @@ bool intersect(const Vector3& rayOrigin, const Vector3& rayDirection, const Vect
 int intersect_triangle(const float orig[3], const float dir[3], const float vert0[3], const float vert1[3], const float vert2[3], float *t, float *u, float *v);
 bool intersect(const Vector3& rayOrigin, const Vector3& rayDirection, const std::vector<Vertex>& vertices, const std::vector<MeshPart*>& parts, Vector3* point);
 
-void Heightmap::generate(const std::vector<std::string>& nodeIds, const char* filename, bool highP)
+void Heightmap::generate(const std::vector<std::string>& nodeIds, int width, int height, const char* filename, bool highP)
 {
     LOG(1, "Generating heightmap: %s...\n", filename);
 
@@ -92,12 +96,10 @@ void Heightmap::generate(const std::vector<std::string>& nodeIds, const char* fi
     Vector3 rayOrigin(0, bounds.max.y + 10, 0);
     Vector3 rayDirection(0, -1, 0);
 
-    int minX = (int)ceil(bounds.min.x);
-    int maxX = (int)floor(bounds.max.x);
-    int minZ = (int)ceil(bounds.min.z);
-    int maxZ = (int)floor(bounds.max.z);
-    int width = maxX - minX + 1;
-    int height = maxZ - minZ + 1;
+    float minX = bounds.min.x;
+    float maxX = bounds.max.x;
+    float minZ = bounds.min.z;
+    float maxZ = bounds.max.z;
     int size = width * height;
     float* heights = new float[size];
     float minHeight = FLT_MAX;
@@ -105,11 +107,14 @@ void Heightmap::generate(const std::vector<std::string>& nodeIds, const char* fi
 
     __totalHeightmapScanlines = height;
 
+    // Determine # of threads to spawn
+    int threadCount = min(THREAD_COUNT, height);
+
     // Split the work into separate threads to make max use of available cpu cores and speed up computation.
-    HeightmapThreadData threadData[THREAD_COUNT];
-    THREAD_HANDLE threads[THREAD_COUNT];
-    int stepSize = height / THREAD_COUNT;
-    for (int i = 0; i < THREAD_COUNT; ++i)
+    HeightmapThreadData* threadData = new HeightmapThreadData[threadCount];
+    THREAD_HANDLE* threads = new THREAD_HANDLE[threadCount];
+    int stepSize = height / threadCount;
+    for (int i = 0, remaining = height; i < threadCount; ++i, remaining -= stepSize)
     {
         HeightmapThreadData& data = threadData[i];
         data.rayHeight = rayOrigin.y;
@@ -120,9 +125,13 @@ void Heightmap::generate(const std::vector<std::string>& nodeIds, const char* fi
         data.maxX = maxX;
         data.minZ = minZ + (stepSize * i);
         data.maxZ = data.minZ + stepSize - 1;
-        if (i == THREAD_COUNT - 1)
+        if (i == threadCount - 1)
             data.maxZ = maxZ;
+        data.stepX = (maxX - minX) / width;
+        data.stepZ = (maxZ - minZ) / height;
         data.heights = heights;
+        data.width = width;
+        data.height = remaining > stepSize ? stepSize : remaining;
         data.heightIndex = width * (stepSize * i);
 
         // Start the processing thread
@@ -134,14 +143,14 @@ void Heightmap::generate(const std::vector<std::string>& nodeIds, const char* fi
     }
 
     // Wait for all threads to terminate
-    waitForThreads(THREAD_COUNT, threads);
+    waitForThreads(threadCount, threads);
 
     // Close all thread handles and free memory allocations.
-    for (int i = 0; i < THREAD_COUNT; ++i)
+    for (int i = 0; i < threadCount; ++i)
         closeThread(threads[i]);
 
     // Update min/max height from all completed threads
-    for (int i = 0; i < THREAD_COUNT; ++i)
+    for (int i = 0; i < threadCount; ++i)
     {
         if (threadData[i].minHeight < minHeight)
             minHeight = threadData[i].minHeight;
@@ -153,7 +162,7 @@ void Heightmap::generate(const std::vector<std::string>& nodeIds, const char* fi
 
     if (__failedRayCasts)
     {
-        LOG(1, "Warning: %d triangle intersections failed for heightmap: %s\n", __failedRayCasts, filename);
+        LOG(2, "Warning: %d triangle intersections failed for heightmap: %s\n", __failedRayCasts, filename);
 
         // Go through and clamp any height values that are set to -FLT_MAX to the min recorded height value
         // (otherwise the range of height values will be far too large).
@@ -234,6 +243,10 @@ void Heightmap::generate(const std::vector<std::string>& nodeIds, const char* fi
     LOG(1, "Saved heightmap: %s\n", filename);
 
 error:
+    if (threadData)
+        delete[] threadData;
+    if (threads)
+        delete[] threads;
     if (heights)
         delete[] heights;
     if (fp)
@@ -252,10 +265,6 @@ int generateHeightmapChunk(void* threadData)
 
     Vector3 rayOrigin(0, data->rayHeight, 0);
     const Vector3& rayDirection = *data->rayDirection;
-    int minX = data->minX;
-    int maxX = data->maxX;
-    int minZ = data->minZ;
-    int maxZ = data->maxZ;
     const std::vector<Mesh*>& meshes = *data->meshes;
     float* heights = data->heights;
 
@@ -264,15 +273,18 @@ int generateHeightmapChunk(void* threadData)
     float maxHeight = -FLT_MAX;
     int index = data->heightIndex;
 
-    for (int z = minZ; z <= maxZ; ++z)
+    int zi = 0;
+    for (float z = data->minZ; zi < data->height; z += data->stepZ, ++zi)
     {
         LOG(1, "\r\t%d%%", (int)(((float)__processedHeightmapScanLines / __totalHeightmapScanlines) * 100.0f));
 
-        rayOrigin.z = (float)z;
-        for (int x = minX; x <= maxX; ++x)
+        rayOrigin.z = z;
+
+        int xi = 0;
+        for (float x = data->minX; xi < data->width; x += data->stepX, ++xi)
         {
             float h = -FLT_MAX;
-            rayOrigin.x = (float)x;
+            rayOrigin.x = x;
 
             for (unsigned int i = 0, count = meshes.size(); i < count; ++i)
             {
@@ -283,7 +295,7 @@ int generateHeightmapChunk(void* threadData)
                 if (!intersect(rayOrigin, rayDirection, mesh->bounds.min, mesh->bounds.max))
                     continue;
 
-                // Computer intersection point of ray with mesh
+                // Compute the intersection point of ray with mesh
                 if (intersect(rayOrigin, rayDirection, mesh->vertices, mesh->parts, &intersectionPoint))
                 {
                     if (intersectionPoint.y > h)

+ 3 - 2
gameplay-encoder/src/Heightmap.h

@@ -14,11 +14,12 @@ public:
      * Generates heightmap data and saves the result to the specified filename (PNG file).
      *
      * @param nodeIds List of node ids to include in the heightmap generation.
+     * @param width Width of the produced heightmap image.
+     * @param height Height of the produced  heightmap image.
      * @param filename Output PNG file to write the heightmap image to.
      * @param highP Use packed 24-bit (RGB) instead of standard 8-bit grayscale.
      */
-    static void generate(const std::vector<std::string>& nodeIds, const char* filename, bool highP = false);
-
+    static void generate(const std::vector<std::string>& nodeIds, int width, int height, const char* filename, bool highP = false);
 
 };
 

+ 262 - 0
gameplay-encoder/src/Image.cpp

@@ -0,0 +1,262 @@
+#include "Image.h"
+#include "Base.h"
+
+namespace gameplay
+{
+
+Image::Image() :
+    _data(NULL), _format(RGBA), _width(0), _height(0), _bpp(0)
+{
+}
+
+Image::~Image()
+{
+    delete[] _data;
+}
+
+Image* Image::create(const char* path)
+{
+    // Open the file.
+    FILE* fp = fopen(path, "rb");
+    if (fp == NULL)
+    {
+        LOG(1, "Failed to open image file '%s'.\n", path);
+        return NULL;
+    }
+
+    // Verify PNG signature.
+    unsigned char sig[8];
+    if (fread(sig, 1, 8, fp) != 8 || png_sig_cmp(sig, 0, 8) != 0)
+    {
+        LOG(1, "Failed to load file '%s'; not a valid PNG.\n", path);
+        if (fclose(fp) != 0)
+        {
+            LOG(1, "Failed to close image file '%s'.\n", path);
+        }
+        return NULL;
+    }
+
+    // Initialize png read struct (last three parameters use stderr+longjump if NULL).
+    png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+    if (png == NULL)
+    {
+        LOG(1, "Failed to create PNG structure for reading PNG file '%s'.\n", path);
+        if (fclose(fp) != 0)
+        {
+            LOG(1, "Failed to close image file '%s'.\n", path);
+        }
+        return NULL;
+    }
+
+    // Initialize info struct.
+    png_infop info = png_create_info_struct(png);
+    if (info == NULL)
+    {
+        LOG(1, "Failed to create PNG info structure for PNG file '%s'.\n", path);
+        if (fclose(fp) != 0)
+        {
+            LOG(1, "Failed to close image file '%s'.\n", path);
+        }
+        png_destroy_read_struct(&png, NULL, NULL);
+        return NULL;
+    }
+
+    // Initialize file io.
+    png_init_io(png, fp);
+
+    // Indicate that we already read the first 8 bytes (signature).
+    png_set_sig_bytes(png, 8);
+
+    // Read the entire image into memory.
+    png_read_png(png, info, PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND, NULL);
+
+    Image* image = new Image();
+    image->_width = png_get_image_width(png, info);
+    image->_height = png_get_image_height(png, info);
+
+    png_byte colorType = png_get_color_type(png, info);
+    switch (colorType)
+    {
+    case PNG_COLOR_TYPE_GRAY:
+        image->_bpp = 1;
+        image->_format = Image::LUMINANCE;
+        break;
+
+    case PNG_COLOR_TYPE_RGBA:
+        image->_bpp = 4;
+        image->_format = Image::RGBA;
+        break;
+
+    case PNG_COLOR_TYPE_RGB:
+        image->_bpp = 3;
+        image->_format = Image::RGB;
+        break;
+
+    default:
+        LOG(1, "Unsupported PNG color type (%d) for image file '%s'.\n", (int)colorType, path);
+        if (fclose(fp) != 0)
+        {
+            LOG(1, "Failed to close image file '%s'.\n", path);
+        }
+        png_destroy_read_struct(&png, &info, NULL);
+        return NULL;
+    }
+
+    size_t stride = png_get_rowbytes(png, info);
+
+    // Allocate image data.
+    image->_data = new unsigned char[stride * image->_height];
+
+    // Read rows into image data.
+    png_bytepp rows = png_get_rows(png, info);
+    for (unsigned int i = 0; i < image->_height; ++i)
+    {
+        memcpy(image->_data+(stride * i), rows[i], stride);
+    }
+
+    // Clean up.
+    png_destroy_read_struct(&png, &info, NULL);
+    if (fclose(fp) != 0)
+    {
+        LOG(1, "Failed to close image file '%s'.\n", path);
+    }
+
+    return image;
+}
+
+Image* Image::create(Format format, unsigned int width, unsigned int height)
+{
+    unsigned int bpp;
+    switch (format)
+    {
+    case LUMINANCE:
+        bpp = 1;
+        break;
+    case RGB:
+        bpp = 3;
+        break;
+    case RGBA:
+        bpp = 4;
+        break;
+    default:
+        LOG(1, "Invalid image format passed to create.\n");
+        return NULL;
+    }
+
+    Image* image = new Image();
+    image->_format = format;
+    image->_width = width;
+    image->_height = height;
+    image->_bpp = bpp;
+    image->_data = new unsigned char[width * height * bpp];
+    memset(image->_data, 0, width * height * bpp);
+    return image;
+}
+
+void* Image::getData() const
+{
+    return _data;
+}
+
+void Image::setData(void* data)
+{
+    memcpy(_data, data, _width * _height * _bpp);
+}
+
+Image::Format Image::getFormat() const
+{
+    return _format;
+}
+
+unsigned int Image::getHeight() const
+{
+    return _height;
+}
+        
+unsigned int Image::getWidth() const
+{
+    return _width;
+}
+
+unsigned int Image::getBpp() const
+{
+    return _bpp;
+}
+
+int getPNGColorType(Image::Format format)
+{
+    switch (format)
+    {
+    case Image::LUMINANCE:
+        return PNG_COLOR_TYPE_GRAY;
+    case Image::RGB:
+        return PNG_COLOR_TYPE_RGB;
+    case Image::RGBA:
+        return PNG_COLOR_TYPE_RGBA;
+    }
+
+    return PNG_COLOR_TYPE_RGBA;
+}
+
+void Image::save(const char* path)
+{
+    png_structp png_ptr = NULL;
+    png_infop info_ptr = NULL;
+    png_bytep row = NULL;
+
+    FILE* fp = fopen(path, "wb");
+    if (fp == NULL)
+    {
+        LOG(1, "Error: Failed to open image for writing: %s\n", path);
+        goto error;
+    }
+
+    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+    if (png_ptr == NULL)
+    {
+        LOG(1, "Error: Write struct creation failed: %s\n", path);
+        goto error;
+    }
+
+    info_ptr = png_create_info_struct(png_ptr);
+    if (info_ptr == NULL)
+    {
+        LOG(1, "Error: Info struct creation failed: %s\n", path);
+        goto error;
+    }
+
+    png_init_io(png_ptr, fp);
+
+    png_set_IHDR(png_ptr, info_ptr, _width, _height, 8, getPNGColorType(_format), PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
+
+    png_write_info(png_ptr, info_ptr);
+
+    // Allocate memory for a single row of image data
+    unsigned int stride = _bpp * _width * sizeof(png_byte);
+    row = (png_bytep)malloc(stride);
+
+    int index = 0;
+    for (unsigned int y = 0; y < _height; ++y)
+    {
+        for (unsigned int x = 0; x < stride; ++x)
+        {
+            // Write data
+            row[x] = (png_byte)_data[index++];
+        }
+        png_write_row(png_ptr, row);
+    }
+
+    png_write_end(png_ptr, NULL);
+
+error:
+    if (fp)
+        fclose(fp);
+    if (row)
+        free(row);
+    if (info_ptr)
+        png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1);
+    if (png_ptr)
+        png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
+}
+
+}

+ 123 - 0
gameplay-encoder/src/Image.h

@@ -0,0 +1,123 @@
+#ifndef IMAGE_H__
+#define IMAGE_H__
+
+namespace gameplay
+{
+
+/**
+ * Represents an image (currently only supports PNG files).
+ */
+class Image
+{
+public:
+
+    /**
+     * Defines the set of supported image formats.
+     */
+    enum Format
+    {
+        LUMINANCE,
+        RGB,
+        RGBA
+    };
+
+    /**
+     * Destructor.
+     */
+    ~Image();
+
+    /**
+     * Creates an image from the image file at the given path.
+     * 
+     * @param path The path to the image file.
+     * @return The newly created image.
+     */
+    static Image* create(const char* path);
+
+    /**
+     * Creates a new empty image of the given format and size.
+     *
+     * @param format Image format.
+     * @param width Image width.
+     * @param height Image height.
+     * @return The newly created image.
+     */
+    static Image* create(Format format, unsigned int width, unsigned int height);
+
+    /**
+     * Gets the image's raw pixel data.
+     * 
+     * @return The image's pixel data.
+     */
+    void* getData() const;
+
+    /** 
+     * Sets the image's raw pixel data.
+     *
+     * The passed in data MUST be at least width*height*bpp
+     * bytes of data.
+     */
+    void setData(void* data);
+
+    /**
+     * Gets the image's format.
+     * 
+     * @return The image's format.
+     */
+    Format getFormat() const;
+
+    /**
+     * Gets the height of the image.
+     * 
+     * @return The height of the image.
+     */
+    unsigned int getHeight() const;
+        
+    /**
+     * Gets the width of the image.
+     * 
+     * @return The width of the image.
+     */
+    unsigned int getWidth() const;
+
+    /**
+     * Returns the number of bytes per pixel for this image.
+     *
+     * @return The number of bytes per pixel.
+     */
+    unsigned int getBpp() const;
+
+    /**
+     * Saves the contents of the image as a PNG to the specified location.
+     *
+     * @param path Path to save to.
+     */
+    void save(const char* path);
+
+private:
+
+    /**
+     * Hidden constructor.
+     */
+    Image();
+
+    /**
+     * Hidden copy constructor.
+     */
+    Image(const Image&);
+
+    /**
+     * Hidden copy assignment operator.
+     */
+    Image& operator=(const Image&);
+
+    unsigned char* _data;
+    Format _format;
+    unsigned int _height;
+    unsigned int _width;
+    unsigned int _bpp;
+};
+
+}
+
+#endif

+ 268 - 0
gameplay-encoder/src/NormalMapGenerator.cpp

@@ -0,0 +1,268 @@
+#include "NormalMapGenerator.h"
+#include "Image.h"
+#include "Base.h"
+
+namespace gameplay
+{
+
+NormalMapGenerator::NormalMapGenerator(const char* inputFile, const char* outputFile, int resolutionX, int resolutionY, const Vector3& worldSize)
+    : _inputFile(inputFile), _outputFile(outputFile), _resolutionX(resolutionX), _resolutionY(resolutionY), _worldSize(worldSize)
+{
+}
+
+NormalMapGenerator::~NormalMapGenerator()
+{
+}
+
+bool equalsIgnoreCase(const std::string& s1, const std::string& s2)
+{
+    size_t l1 = s1.size();
+    size_t l2 = s2.size();
+    if (l1 != l2)
+        return false;
+
+    for (size_t i = 0; i < l1; ++i)
+    {
+        if (tolower(s1[i]) != tolower(s2[i]))
+            return false;
+    }
+
+    return true;
+}
+
+float getHeight(float* heights, int width, int height, int x, int y)
+{
+    if (x < 0)
+        x = 0;
+    else if (x >= width)
+        x = width-1;
+    if (y < 0)
+        y = 0;
+    else if (y >= height)
+        y = height-1;
+    return heights[y*width+x];
+}
+
+void calculateNormal(
+    float x1, float y1, float z1,
+    float x2, float y2, float z2,
+    float x3, float y3, float z3,
+    Vector3* normal)
+{
+    Vector3 E(x1, y1, z1);
+    Vector3 F(x2, y2, z2);
+    Vector3 G(x3, y3, z3);
+
+    Vector3 P, Q;
+    Vector3::subtract(F, E, &P);
+    Vector3::subtract(G, E, &Q);
+
+    Vector3::cross(Q, P, normal);
+}
+
+void NormalMapGenerator::generate()
+{
+    // Load the input heightmap
+    float* heights = NULL;
+    size_t pos = _inputFile.find_last_of('.');
+    std::string ext = pos == std::string::npos ? "" : _inputFile.substr(pos, _inputFile.size()-pos);
+    if (equalsIgnoreCase(ext, ".png"))
+    {
+        // Load heights from PNG image
+        Image* image = Image::create(_inputFile.c_str());
+        if (image == NULL)
+        {
+            LOG(1, "Failed to load input heightmap PNG: %s.\n", _inputFile.c_str());
+            return;
+        }
+
+        // TODO: Add command-line argument for high precision heightmaps
+
+        _resolutionX = image->getWidth();
+        _resolutionY = image->getHeight();
+        int size = _resolutionX * _resolutionY;
+        heights = new float[size];
+        unsigned char* data = (unsigned char*)image->getData();
+        for (int i = 0; i < size; ++i)
+        {
+            switch (image->getFormat())
+            {
+            case Image::LUMINANCE:
+                heights[i] = data[i];
+                break;
+            case Image::RGB:
+            case Image::RGBA:
+                {
+                    int pos = i * image->getBpp();
+                    heights[i] = (data[pos] + data[pos+1] + data[pos+2]) / 3.0f;
+                }
+                break;
+            default:
+                heights[i] = 0.0f;
+                break;
+            }
+            heights[i] = (heights[i] / 255.0f) * _worldSize.y;
+        }
+        SAFE_DELETE(image);
+    }
+    else if (equalsIgnoreCase(ext, ".raw"))
+    {
+        // Load heights from RAW 8 or 16-bit file
+        if (_resolutionX <= 0 || _resolutionY <= 0)
+        {
+            LOG(1, "Missing resolution argument - must be explicitly specified for RAW heightmap files: %s\n.", _inputFile.c_str());
+            return;
+        }
+
+        // TODO
+        heights = new float[_resolutionX * _resolutionY];
+        // TODO
+
+        LOG(1, "RAW files not yet implemented...");
+        return;
+    }
+    else
+    {
+        LOG(1, "Unsupported input heightmap file (must be a valid PNG or RAW file: %s.\n", _inputFile.c_str());
+        return;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    // NOTE: This method assumes the heightmap geometry is generated as follows.
+    //
+    //   -----------
+    //  | / | / | / |
+    //  |-----------|
+    //  | / | / | / |
+    //  |-----------|
+    //  | / | / | / |
+    //   -----------
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////////////
+
+    struct NormalPixel
+    {
+        unsigned char r, g, b;
+    };
+    NormalPixel* normalPixels = new NormalPixel[_resolutionX * _resolutionY];
+
+    struct Face
+    {
+        Vector3 normal1;
+        Vector3 normal2;
+    };
+
+    int progressMax = (_resolutionX-1) * (_resolutionY-1) + _resolutionX * _resolutionY;
+    int progress = 0;
+
+    Vector2 scale(_worldSize.x / (_resolutionX-1), _worldSize.z / (_resolutionY-1));
+
+    // First calculate all face normals for the heightmap
+    LOG(1, "Calculating normals... 0%%");
+    Face* faceNormals = new Face[(_resolutionX - 1) * (_resolutionY - 1)];
+    Vector3 v1, v2;
+    for (int z = 0; z < _resolutionY-1; z++)
+    {
+        for (int x = 0; x < _resolutionX-1; x++)
+        {
+            float topLeftHeight = getHeight(heights, _resolutionX, _resolutionY, x, z);
+            float bottomLeftHeight = getHeight(heights, _resolutionX, _resolutionY, x, z + 1);
+            float bottomRightHeight = getHeight(heights, _resolutionX, _resolutionY, x + 1, z + 1);
+            float topRightHeight = getHeight(heights, _resolutionX, _resolutionY, x + 1, z);
+
+            // Triangle 1
+            calculateNormal(
+                (float)x*scale.x, bottomLeftHeight, (float)(z + 1)*scale.y,
+                (float)x*scale.x, topLeftHeight, (float)z*scale.y,
+                (float)(x + 1)*scale.x, topRightHeight, (float)z*scale.y,
+                &faceNormals[z*(_resolutionX-1)+x].normal1);
+
+            // Triangle 2
+            calculateNormal(
+                (float)x*scale.x, bottomLeftHeight, (float)(z + 1)*scale.y,
+                (float)(x + 1)*scale.x, topRightHeight, (float)z*scale.y,
+                (float)(x + 1)*scale.x, bottomRightHeight, (float)(z + 1)*scale.y,
+                &faceNormals[z*(_resolutionX-1)+x].normal2);
+
+            ++progress;
+            LOG(1, "\rCalculating normals... %d%%", (int)(((float)progress / progressMax) * 100));
+        }
+    }
+
+    // Free height array
+    delete[] heights;
+    heights = NULL;
+
+    // Smooth normals by taking an average for each vertex
+    Vector3 normal;
+    for (int z = 0; z < _resolutionY; z++)
+    {
+        for (int x = 0; x < _resolutionX; x++)
+        {
+            // Reset normal sum
+            normal.set(0, 0, 0);
+
+            if (x > 0)
+            {
+                if (z > 0)
+                {
+                    // Top left
+                    normal.add(faceNormals[(z-1)*(_resolutionX-1) + (x-1)].normal2);
+                }
+
+                if (z < (_resolutionY - 1))
+                {
+                    // Bottom left
+                    normal.add(faceNormals[z*(_resolutionX-1) + (x - 1)].normal1);
+                    normal.add(faceNormals[z*(_resolutionX-1) + (x - 1)].normal2);
+                }
+            }
+
+            if (x < (_resolutionX - 1))
+            {
+                if (z > 0)
+                {
+                    // Top right
+                    normal.add(faceNormals[(z-1)*(_resolutionX-1) + x].normal1);
+                    normal.add(faceNormals[(z-1)*(_resolutionX-1) + x].normal2);
+                }
+
+                if (z < (_resolutionY - 1))
+                {
+                    // Bottom right
+                    normal.add(faceNormals[z*(_resolutionX-1) + x].normal1);
+                }
+            }
+
+            // We don't have to worry about weighting the normals by
+            // the surface area of the triangles since a heightmap 
+            // guarantees that all triangles have the same surface area.
+            normal.normalize();
+
+            // Store this vertex normal
+            NormalPixel& pixel = normalPixels[z*_resolutionX + x];
+            pixel.r = (unsigned char)((normal.x + 1.0f) * 0.5f * 255.0f);
+            pixel.g = (unsigned char)((normal.y + 1.0f) * 0.5f * 255.0f);
+            pixel.b = (unsigned char)((normal.z + 1.0f) * 0.5f * 255.0f);
+
+            ++progress;
+            LOG(1, "\rCalculating normals... %d%%", (int)(((float)progress / progressMax) * 100));
+        }
+    }
+
+    LOG(1, "\rCalculating normals... Done.\n");
+
+    // Create and save an image for the normal map
+    Image* normalMap = Image::create(Image::RGB, _resolutionX, _resolutionY);
+    normalMap->setData(normalPixels);
+    normalMap->save(_outputFile.c_str());
+
+    LOG(1, "Normal map saved to '%s'.\n", _outputFile.c_str());
+
+    // Free temp data
+    delete[] normalPixels;
+    normalPixels = NULL;
+}
+
+}

+ 35 - 0
gameplay-encoder/src/NormalMapGenerator.h

@@ -0,0 +1,35 @@
+#ifndef NORMALMAPGENERATOR_H_
+#define NORMALMAPGENERATOR_H_
+
+#include "Vector3.h"
+
+namespace gameplay
+{
+
+class NormalMapGenerator
+{
+
+public:
+
+    NormalMapGenerator(const char* inputFile, const char* outputFile, int resolutionX, int resolutionY, const Vector3& worldSize);
+    ~NormalMapGenerator();
+
+    void generate();
+
+private:
+
+    // Hidden copy/assignment
+    NormalMapGenerator(const NormalMapGenerator&);
+    NormalMapGenerator& operator=(const NormalMapGenerator&);
+
+    std::string _inputFile;
+    std::string _outputFile;
+    int _resolutionX;
+    int _resolutionY;
+    Vector3 _worldSize;
+
+};
+
+}
+
+#endif

+ 18 - 0
gameplay-encoder/src/main.cpp

@@ -4,6 +4,7 @@
 #include "TTFFontEncoder.h"
 #include "GPBDecoder.h"
 #include "EncoderArguments.h"
+#include "NormalMapGenerator.h"
 
 using namespace gameplay;
 
@@ -102,6 +103,23 @@ int main(int argc, const char** argv)
             decoder.readBinary(realpath);
             break;
         }
+    case EncoderArguments::FILEFORMAT_PNG:
+    case EncoderArguments::FILEFORMAT_RAW:
+        {
+            if (arguments.normalMapGeneration())
+            {
+                int x, y;
+                arguments.getHeightmapResolution(&x, &y);
+                NormalMapGenerator generator(arguments.getFilePath().c_str(), arguments.getOutputFilePath().c_str(), x, y, arguments.getHeightmapWorldSize());
+                generator.generate();
+            }
+            else
+            {
+                LOG(1, "Error: Nothing to do for specified file format. Did you forget an option?\n");
+                return -1;
+            }
+            break;
+        }
    default:
         {
             LOG(1, "Error: Unsupported file format: %s\n", arguments.getFilePathPointer());

+ 4 - 0
gameplay/gameplay.vcxproj

@@ -307,6 +307,8 @@
     <ClCompile Include="src\Slider.cpp" />
     <ClCompile Include="src\SpriteBatch.cpp" />
     <ClCompile Include="src\Technique.cpp" />
+    <ClCompile Include="src\Terrain.cpp" />
+    <ClCompile Include="src\TerrainPatch.cpp" />
     <ClCompile Include="src\TextBox.cpp" />
     <ClCompile Include="src\Texture.cpp" />
     <ClCompile Include="src\Theme.cpp" />
@@ -574,6 +576,8 @@
     <ClInclude Include="src\Slider.h" />
     <ClInclude Include="src\SpriteBatch.h" />
     <ClInclude Include="src\Technique.h" />
+    <ClInclude Include="src\Terrain.h" />
+    <ClInclude Include="src\TerrainPatch.h" />
     <ClInclude Include="src\TextBox.h" />
     <ClInclude Include="src\Texture.h" />
     <ClInclude Include="src\Theme.h" />

+ 12 - 0
gameplay/gameplay.vcxproj.filters

@@ -819,6 +819,12 @@
     <ClCompile Include="src\lua\lua_Logger.cpp">
       <Filter>lua</Filter>
     </ClCompile>
+    <ClCompile Include="src\Terrain.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
+    <ClCompile Include="src\TerrainPatch.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="src\Animation.h">
@@ -1622,6 +1628,12 @@
     <ClInclude Include="src\lua\lua_LoggerLevel.h">
       <Filter>lua</Filter>
     </ClInclude>
+    <ClInclude Include="src\Terrain.h">
+      <Filter>src</Filter>
+    </ClInclude>
+    <ClInclude Include="src\TerrainPatch.h">
+      <Filter>src</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <None Include="src\Game.inl">

+ 33 - 3
gameplay/src/Effect.cpp

@@ -402,9 +402,9 @@ Effect* Effect::createFromSource(const char* vshPath, const char* vshSource, con
                 // Query uniform info.
                 GL_ASSERT( glGetActiveUniform(program, i, length, NULL, &uniformSize, &uniformType, uniformName) );
                 uniformName[length] = '\0';  // null terminate
-                if (uniformSize > 1 && length > 3)
+                if (length > 3)
                 {
-                    // This is an array uniform. I'm stripping array indexers off it since GL does not
+                    // If this is an array uniform, strip array indexers off it since GL does not
                     // seem to be consistent across different drivers/implementations in how it returns
                     // array uniforms. On some systems it will return "u_matrixArray", while on others
                     // it will return "u_matrixArray[0]".
@@ -423,7 +423,15 @@ Effect* Effect::createFromSource(const char* vshPath, const char* vshSource, con
                 uniform->_name = uniformName;
                 uniform->_location = uniformLocation;
                 uniform->_type = uniformType;
-                uniform->_index = uniformType == GL_SAMPLER_2D ? (samplerIndex++) : 0;
+                if (uniformType == GL_SAMPLER_2D)
+                {
+                    uniform->_index = samplerIndex;
+                    samplerIndex += uniformSize;
+                }
+                else
+                {
+                    uniform->_index = 0;
+                }
 
                 effect->_uniforms[uniformName] = uniform;
             }
@@ -561,6 +569,28 @@ void Effect::setValue(Uniform* uniform, const Texture::Sampler* sampler)
     GL_ASSERT( glUniform1i(uniform->_location, uniform->_index) );
 }
 
+void Effect::setValue(Uniform* uniform, const Texture::Sampler** values, unsigned int count)
+{
+    GP_ASSERT(uniform);
+    GP_ASSERT(uniform->_type == GL_SAMPLER_2D);
+    GP_ASSERT(values);
+
+    // Set samplers as active and load texture unit array
+    GLint units[32];
+    for (unsigned int i = 0; i < count; ++i)
+    {
+        GL_ASSERT( glActiveTexture(GL_TEXTURE0 + uniform->_index + i) );
+
+        // Bind the sampler - this binds the texture and applies sampler state
+        const_cast<Texture::Sampler*>(values[i])->bind();
+
+        units[i] = uniform->_index + i;
+    }
+
+    // Pass texture unit array to GL
+    GL_ASSERT( glUniform1iv(uniform->_location, count, units) );
+}
+
 void Effect::bind()
 {
    GL_ASSERT( glUseProgram(_program) );

+ 9 - 0
gameplay/src/Effect.h

@@ -199,6 +199,15 @@ public:
      */
     void setValue(Uniform* uniform, const Texture::Sampler* sampler);
 
+    /**
+     * Sets a sampler array uniform value.
+     *
+     * @param uniform The uniform to set.
+     * @param values The sampler array to set.
+     * @param count The number of elements in the array.
+     */
+    void setValue(Uniform* uniform, const Texture::Sampler** values, unsigned int count = 1);
+
     /**
      * Binds this effect to make it the currently active effect for the rendering system.
      */

+ 95 - 78
gameplay/src/MaterialParameter.cpp

@@ -18,11 +18,34 @@ MaterialParameter::~MaterialParameter()
 
 void MaterialParameter::clearValue()
 {
+    // Release parameters
+    switch (_type)
+    {
+    case MaterialParameter::SAMPLER:
+        if (_value.samplerValue)
+            const_cast<Texture::Sampler*>(_value.samplerValue)->release();
+        break;
+    case MaterialParameter::SAMPLER_ARRAY:
+        if (_value.samplerArrayValue)
+        {
+            for (unsigned int i = 0; i < _count; ++i)
+            {
+                const_cast<Texture::Sampler*>(_value.samplerArrayValue[i])->release();
+            }
+        }
+        break;
+    default:
+        // Ignore all other cases.
+        break;
+    }
+
+    // Free dynamic data
     if (_dynamic)
     {
         switch (_type)
         {
         case MaterialParameter::FLOAT:
+        case MaterialParameter::FLOAT_ARRAY:
         case MaterialParameter::VECTOR2:
         case MaterialParameter::VECTOR3:
         case MaterialParameter::VECTOR4:
@@ -30,11 +53,15 @@ void MaterialParameter::clearValue()
             SAFE_DELETE_ARRAY(_value.floatPtrValue);
             break;
         case MaterialParameter::INT:
+        case MaterialParameter::INT_ARRAY:
             SAFE_DELETE_ARRAY(_value.intPtrValue);
             break;
         case MaterialParameter::METHOD:
             SAFE_RELEASE(_value.method);
             break;
+        case MaterialParameter::SAMPLER_ARRAY:
+            SAFE_DELETE_ARRAY(_value.samplerArrayValue);
+            break;
         default:
             // Ignore all other cases.
             break;
@@ -43,21 +70,6 @@ void MaterialParameter::clearValue()
         _dynamic = false;
         _count = 1;
     }
-    else
-    {
-        switch (_type)
-        {
-        case MaterialParameter::SAMPLER:
-            if (_value.samplerValue)
-            {
-                const_cast<Texture::Sampler*>(_value.samplerValue)->release();
-            }
-            break;
-        default:
-            // Ignore all other cases.
-            break;
-        }
-    }
 
     memset(&_value, 0, sizeof(_value));
     _type = MaterialParameter::NONE;
@@ -68,10 +80,12 @@ const char* MaterialParameter::getName() const
     return _name.c_str();
 }
 
-Texture::Sampler* MaterialParameter::getSampler() const
+Texture::Sampler* MaterialParameter::getSampler(unsigned int index) const
 {
     if (_type == MaterialParameter::SAMPLER)
         return const_cast<Texture::Sampler*>(_value.samplerValue);
+    if (_type == MaterialParameter::SAMPLER_ARRAY && index < _count)
+        return const_cast<Texture::Sampler*>(_value.samplerArrayValue[index]);
     return NULL;
 }
 
@@ -97,7 +111,7 @@ void MaterialParameter::setValue(const float* values, unsigned int count)
 
     _value.floatPtrValue = const_cast<float*> (values);
     _count = count;
-    _type = MaterialParameter::FLOAT;
+    _type = MaterialParameter::FLOAT_ARRAY;
 }
 
 void MaterialParameter::setValue(const int* values, unsigned int count)
@@ -106,7 +120,7 @@ void MaterialParameter::setValue(const int* values, unsigned int count)
 
     _value.intPtrValue = const_cast<int*> (values);
     _count = count;
-    _type = MaterialParameter::INT;
+    _type = MaterialParameter::INT_ARRAY;
 }
 
 void MaterialParameter::setValue(const Vector2& value)
@@ -221,6 +235,22 @@ void MaterialParameter::setValue(const Texture::Sampler* sampler)
     }
 }
 
+void MaterialParameter::setValue(const Texture::Sampler** samplers, unsigned int count)
+{
+    clearValue();
+
+    if (samplers)
+    {
+        for (unsigned int i = 0; i < count; ++i)
+        {
+            const_cast<Texture::Sampler*>(samplers[i])->addRef();
+        }
+        _value.samplerArrayValue = samplers;
+        _count = count;
+        _type = MaterialParameter::SAMPLER_ARRAY;
+    }
+}
+
 Texture::Sampler* MaterialParameter::setValue(const char* texturePath, bool generateMipmaps)
 {
     if (texturePath)
@@ -259,24 +289,16 @@ void MaterialParameter::bind(Effect* effect)
     switch (_type)
     {
     case MaterialParameter::FLOAT:
-        if (_count == 1)
-        {
-            effect->setValue(_uniform, _value.floatValue);
-        }
-        else
-        {
-            effect->setValue(_uniform, _value.floatPtrValue, _count);
-        }
+        effect->setValue(_uniform, _value.floatValue);
+        break;
+    case MaterialParameter::FLOAT_ARRAY:
+        effect->setValue(_uniform, _value.floatPtrValue, _count);
         break;
     case MaterialParameter::INT:
-        if (_count == 1)
-        {
-            effect->setValue(_uniform, _value.intValue);
-        }
-        else
-        {
-            effect->setValue(_uniform, _value.intPtrValue, _count);
-        }
+        effect->setValue(_uniform, _value.intValue);
+        break;
+    case MaterialParameter::INT_ARRAY:
+        effect->setValue(_uniform, _value.intPtrValue, _count);
         break;
     case MaterialParameter::VECTOR2:
         effect->setValue(_uniform, reinterpret_cast<Vector2*>(_value.floatPtrValue), _count);
@@ -293,6 +315,9 @@ void MaterialParameter::bind(Effect* effect)
     case MaterialParameter::SAMPLER:
         effect->setValue(_uniform, _value.samplerValue);
         break;
+    case MaterialParameter::SAMPLER_ARRAY:
+        effect->setValue(_uniform, _value.samplerArrayValue, _count);
+        break;
     case MaterialParameter::METHOD:
         GP_ASSERT(_value.method);
         _value.method->setValue(effect);
@@ -405,10 +430,13 @@ unsigned int MaterialParameter::getAnimationPropertyComponentCount(int propertyI
                 case NONE:
                 case MATRIX:
                 case SAMPLER:
+                case SAMPLER_ARRAY:
                 case METHOD:
                     return 0;
                 case FLOAT:
+                case FLOAT_ARRAY:
                 case INT:
+                case INT_ARRAY:
                     return _count;
                 case VECTOR2:
                     return 2 * _count;
@@ -437,31 +465,23 @@ void MaterialParameter::getAnimationPropertyValue(int propertyId, AnimationValue
             switch (_type)
             {
                 case FLOAT:
-                    if (_count == 1)
-                    {
-                        value->setFloat(0, _value.floatValue);
-                    }
-                    else
+                    value->setFloat(0, _value.floatValue);
+                    break;
+                case FLOAT_ARRAY:
+                    GP_ASSERT(_value.floatPtrValue);
+                    for (unsigned int i = 0; i < _count; i++)
                     {
-                        GP_ASSERT(_value.floatPtrValue);
-                        for (unsigned int i = 0; i < _count; i++)
-                        {
-                            value->setFloat(i, _value.floatPtrValue[i]);
-                        }
+                        value->setFloat(i, _value.floatPtrValue[i]);
                     }
                     break;
                 case INT:
-                    if (_count == 1)
-                    {
-                        value->setFloat(0, _value.intValue);
-                    }
-                    else
+                    value->setFloat(0, _value.intValue);
+                    break;
+                case INT_ARRAY:
+                    GP_ASSERT(_value.intPtrValue);
+                    for (unsigned int i = 0; i < _count; i++)
                     {
-                        GP_ASSERT(_value.intPtrValue);
-                        for (unsigned int i = 0; i < _count; i++)
-                        {
-                            value->setFloat(i, _value.intPtrValue[i]);
-                        }
+                        value->setFloat(i, _value.intPtrValue[i]);
                     }
                     break;
                 case VECTOR2:
@@ -477,6 +497,7 @@ void MaterialParameter::getAnimationPropertyValue(int propertyId, AnimationValue
                 case MATRIX:
                 case METHOD:
                 case SAMPLER:
+                case SAMPLER_ARRAY:
                     // Unsupported material parameter types for animation.
                     break;
                 default:
@@ -500,46 +521,33 @@ void MaterialParameter::setAnimationPropertyValue(int propertyId, AnimationValue
             switch (_type)
             {
                 case FLOAT:
-                {
-                    if (_count == 1)
-                        _value.floatValue = Curve::lerp(blendWeight, _value.floatValue, value->getFloat(0));
-                    else
-                        applyAnimationValue(value, blendWeight, 1);
+                    _value.floatValue = Curve::lerp(blendWeight, _value.floatValue, value->getFloat(0));
+                    break;
+                case FLOAT_ARRAY:
+                    applyAnimationValue(value, blendWeight, 1);
                     break;
-                }
                 case INT:
-                {
-                    if (_count == 1)
-                    {
-                        _value.intValue = Curve::lerp(blendWeight, _value.intValue, value->getFloat(0));
-                    }
-                    else
-                    {
-                        GP_ASSERT(_value.intPtrValue);
-                        for (unsigned int i = 0; i < _count; i++)
-                            _value.intPtrValue[i] = Curve::lerp(blendWeight, _value.intPtrValue[i], value->getFloat(i));
-                    }
+                    _value.intValue = Curve::lerp(blendWeight, _value.intValue, value->getFloat(0));
+                    break;
+                case INT_ARRAY:
+                    GP_ASSERT(_value.intPtrValue);
+                    for (unsigned int i = 0; i < _count; i++)
+                        _value.intPtrValue[i] = Curve::lerp(blendWeight, _value.intPtrValue[i], value->getFloat(i));
                     break;
-                }
                 case VECTOR2:
-                {
                     applyAnimationValue(value, blendWeight, 2);
                     break;
-                }
                 case VECTOR3:
-                {
                     applyAnimationValue(value, blendWeight, 3);
                     break;
-                }
                 case VECTOR4:
-                {
                     applyAnimationValue(value, blendWeight, 4);
                     break;
-                }
                 case NONE:
                 case MATRIX:
                 case METHOD:
                 case SAMPLER:
+                case SAMPLER_ARRAY:
                     // Unsupported material parameter types for animation.
                     break;
                 default:
@@ -575,9 +583,15 @@ void MaterialParameter::cloneInto(MaterialParameter* materialParameter) const
     case FLOAT:
         materialParameter->setValue(_value.floatValue);
         break;
+    case FLOAT_ARRAY:
+        materialParameter->setValue(_value.floatPtrValue, _count);
+        break;
     case INT:
         materialParameter->setValue(_value.intValue);
         break;
+    case INT_ARRAY:
+        materialParameter->setValue(_value.intPtrValue, _count);
+        break;
     case VECTOR2:
     {
         Vector2* value = reinterpret_cast<Vector2*>(_value.floatPtrValue);
@@ -637,6 +651,9 @@ void MaterialParameter::cloneInto(MaterialParameter* materialParameter) const
     case SAMPLER:
         materialParameter->setValue(_value.samplerValue);
         break;
+    case SAMPLER_ARRAY:
+        materialParameter->setValue(_value.samplerArrayValue, _count);
+        break;
     case METHOD:
         materialParameter->_value.method = _value.method;
         GP_ASSERT(materialParameter->_value.method);

+ 20 - 1
gameplay/src/MaterialParameter.h

@@ -27,6 +27,12 @@ namespace gameplay
  * setting the parameter value to a pointer to a Matrix, any changes
  * to the Matrix will automatically be reflected in the technique the
  * next time the parameter is applied to the render state.
+ *
+ * Note that for parameter values to arrays or pointers, the 
+ * MaterialParameter will keep a long-lived reference to the passed
+ * in array/pointer. Therefore, you must ensure that the pointers
+ * you pass in are valid for the lifetime of the MaterialParameter
+ * object.
  */
 class MaterialParameter : public AnimationTarget, public Ref
 {
@@ -47,9 +53,12 @@ public:
     /**
      * Returns the texture sampler or NULL if this MaterialParameter is not a sampler type.
      * 
+     * @param index Index of the sampler (if the parameter is a sampler array),
+     *      or zero if it is a single sampler value.
+     *
      * @return The texture sampler or NULL if this MaterialParameter is not a sampler type.
      */
-    Texture::Sampler* getSampler() const;
+    Texture::Sampler* getSampler(unsigned int index = 0) const;
 
     /**
      * Sets the value of this parameter to a float value.
@@ -116,6 +125,11 @@ public:
      */
     void setValue(const Texture::Sampler* sampler);
 
+    /**
+     * Sets the value of this parameter to the specified texture sampler array.
+     */
+    void setValue(const Texture::Sampler** samplers, unsigned int count = 1);
+
     /**
      * Loads a texture sampler from the specified path and sets it as the value of this parameter.
      *
@@ -298,6 +312,8 @@ private:
         /** @script{ignore} */
         const Texture::Sampler* samplerValue;
         /** @script{ignore} */
+        const Texture::Sampler** samplerArrayValue;
+        /** @script{ignore} */
         MethodBinding* method;
     } _value;
     
@@ -305,12 +321,15 @@ private:
     {
         NONE,
         FLOAT,
+        FLOAT_ARRAY,
         INT,
+        INT_ARRAY,
         VECTOR2,
         VECTOR3,
         VECTOR4,
         MATRIX,
         SAMPLER,
+        SAMPLER_ARRAY,
         METHOD
     } _type;
     

+ 11 - 8
gameplay/src/Model.h

@@ -134,6 +134,16 @@ public:
      */
     Node* getNode() const;
 
+    /**
+     * Sets the node that is associated with this model.
+     *
+     * This method is automatically called when a model is attached to a node
+     * and therefore should not normally be called explicitly.
+     * 
+     * @param node The node that is associated with this model.
+     */
+    void setNode(Node* node);
+
     /**
      * Draws this mesh instance.
      *
@@ -171,14 +181,7 @@ private:
     void setSkin(MeshSkin* skin);
 
     /**
-     * Sets the node that is associated with this model.
-     * 
-     * @param node The node that is associated with this model.
-     */
-    void setNode(Node* node);
-
-    /**
-     * Sets the specified materia's node binding to this model's node.
+     * Sets the specified material's node binding to this model's node.
      */
     void setMaterialNodeBinding(Material *m);
 

+ 36 - 8
gameplay/src/Node.cpp

@@ -1,6 +1,6 @@
 #include "Base.h"
-#include "AudioSource.h"
 #include "Node.h"
+#include "AudioSource.h"
 #include "Scene.h"
 #include "Joint.h"
 #include "PhysicsRigidBody.h"
@@ -9,6 +9,7 @@
 #include "PhysicsGhostObject.h"
 #include "PhysicsCharacter.h"
 #include "Game.h"
+#include "Terrain.h"
 
 // Node dirty flags
 #define NODE_DIRTY_WORLD 1
@@ -20,7 +21,7 @@ namespace gameplay
 
 Node::Node(const char* id)
     : _scene(NULL), _firstChild(NULL), _nextSibling(NULL), _prevSibling(NULL), _parent(NULL), _childCount(0),
-    _tags(NULL), _camera(NULL), _light(NULL), _model(NULL), _form(NULL), _audioSource(NULL), _particleEmitter(NULL),
+    _tags(NULL), _camera(NULL), _light(NULL), _model(NULL), _terrain(NULL), _form(NULL), _audioSource(NULL), _particleEmitter(NULL),
     _collisionObject(NULL), _agent(NULL), _dirtyBits(NODE_DIRTY_ALL), _notifyHierarchyChanged(true), _userData(NULL)
 {
     if (id)
@@ -45,6 +46,7 @@ Node::~Node()
     SAFE_RELEASE(_camera);
     SAFE_RELEASE(_light);
     SAFE_RELEASE(_model);
+    SAFE_RELEASE(_terrain);
     SAFE_RELEASE(_audioSource);
     SAFE_RELEASE(_particleEmitter);
     SAFE_RELEASE(_form);
@@ -755,6 +757,11 @@ void Node::setLight(Light* light)
     }
 }
 
+Model* Node::getModel() const
+{
+    return _model;
+}
+
 void Node::setModel(Model* model)
 {
     if (_model != model)
@@ -775,9 +782,34 @@ void Node::setModel(Model* model)
     }
 }
 
-Model* Node::getModel() const
+Terrain* Node::getTerrain() const
 {
-    return _model;
+    return _terrain;
+}
+
+void Node::setTerrain(Terrain* terrain)
+{
+    if (_terrain != terrain)
+    {
+        if (_terrain)
+        {
+            _terrain->setNode(NULL);
+            SAFE_RELEASE(_terrain);
+        }
+
+        _terrain = terrain;
+
+        if (_terrain)
+        {
+            _terrain->addRef();
+            _terrain->setNode(this);
+        }
+    }
+}
+
+Form* Node::getForm() const
+{
+    return _form;
 }
 
 void Node::setForm(Form* form)
@@ -800,10 +832,6 @@ void Node::setForm(Form* form)
     }
 }
 
-Form* Node::getForm() const
-{
-    return _form;
-}
 
 const BoundingSphere& Node::getBoundingSphere() const
 {

+ 25 - 3
gameplay/src/Node.h

@@ -9,7 +9,6 @@
 #include "ParticleEmitter.h"
 #include "PhysicsRigidBody.h"
 #include "PhysicsCollisionObject.h"
-#include "PhysicsCollisionShape.h"
 #include "BoundingBox.h"
 #include "AIAgent.h"
 
@@ -20,6 +19,7 @@ class AudioSource;
 class Bundle;
 class Scene;
 class Form;
+class Terrain;
 
 /**
  * Defines a basic hierarchical structure of transformation spaces.
@@ -38,7 +38,7 @@ public:
     enum Type
     {
         NODE = 1,
-        JOINT = 2
+        JOINT
     };
 
     /**
@@ -425,6 +425,23 @@ public:
      */
     void setModel(Model* model);
 
+    /**
+     * Returns the pointer to this node's terrain.
+     *
+     * @return The pointer to this node's terrain.
+     */
+    Terrain* getTerrain() const;
+
+    /**
+     * Assigns a terrain to this node.
+     *
+     * This will increase the reference count of the new terrain and decrease
+     * the reference count of the old terrain.
+     *
+     * @param terrain The new terrain. May be NULL.
+     */
+    void setTerrain(Terrain* terrain);
+
     /**
      * Returns the pointer to this node's form.
      * 
@@ -768,7 +785,12 @@ protected:
      * Pointer to the Model attached to the Node.
      */
     Model* _model;
-    
+
+    /**
+     * Pointer to the Terrain attached to the Node.
+     */
+    Terrain* _terrain;
+
     /**
      * Pointer to the Form attached to the Node.
      */

+ 3 - 4
gameplay/src/PhysicsCharacter.cpp

@@ -99,8 +99,8 @@ PhysicsCharacter* PhysicsCharacter::create(Node* node, Properties* properties)
     }
 
     // Load the physics collision shape definition.
-    PhysicsCollisionShape::Definition* shape = PhysicsCollisionShape::Definition::create(node, properties);
-    if (shape == NULL)
+    PhysicsCollisionShape::Definition shape = PhysicsCollisionShape::Definition::create(node, properties);
+    if (shape.isEmpty())
     {
         GP_ERROR("Failed to create collision shape during physics character creation.");
         return NULL;
@@ -133,10 +133,9 @@ PhysicsCharacter* PhysicsCharacter::create(Node* node, Properties* properties)
     }
 
     // Create the physics character.
-    PhysicsCharacter* character = new PhysicsCharacter(node, *shape, mass);
+    PhysicsCharacter* character = new PhysicsCharacter(node, shape, mass);
     character->setMaxStepHeight(maxStepHeight);
     character->setMaxSlopeAngle(maxSlopeAngle);
-    SAFE_DELETE(shape);
 
     return character;
 }

+ 10 - 3
gameplay/src/PhysicsCollisionObject.cpp

@@ -187,8 +187,8 @@ bool PhysicsCollisionObject::CollisionPair::operator < (const CollisionPair& col
     return false;
 }
 
-PhysicsCollisionObject::PhysicsMotionState::PhysicsMotionState(Node* node, const Vector3* centerOfMassOffset) : _node(node),
-    _centerOfMassOffset(btTransform::getIdentity())
+PhysicsCollisionObject::PhysicsMotionState::PhysicsMotionState(Node* node, PhysicsCollisionObject* collisionObject, const Vector3* centerOfMassOffset) :
+    _node(node), _collisionObject(collisionObject), _centerOfMassOffset(btTransform::getIdentity())
 {
     if (centerOfMassOffset)
     {
@@ -206,7 +206,9 @@ PhysicsCollisionObject::PhysicsMotionState::~PhysicsMotionState()
 void PhysicsCollisionObject::PhysicsMotionState::getWorldTransform(btTransform &transform) const
 {
     GP_ASSERT(_node);
-    if (_node->getCollisionObject() && _node->getCollisionObject()->isKinematic())
+    GP_ASSERT(_collisionObject);
+
+    if (_collisionObject->isKinematic())
         updateTransformFromNode();
 
     transform = _centerOfMassOffset.inverse() * _worldTransform;
@@ -251,6 +253,11 @@ void PhysicsCollisionObject::PhysicsMotionState::updateTransformFromNode() const
     }
 }
 
+void PhysicsCollisionObject::PhysicsMotionState::setCenterOfMassOffset(const Vector3& centerOfMassOffset)
+{
+    _centerOfMassOffset.setOrigin(BV(centerOfMassOffset));
+}
+
 PhysicsCollisionObject::ScriptListener::ScriptListener(const char* url)
 {
     this->url = url;

+ 10 - 3
gameplay/src/PhysicsCollisionObject.h

@@ -270,16 +270,17 @@ protected:
     class PhysicsMotionState : public btMotionState
     {
         friend class PhysicsConstraint;
-
+        
     public:
 
         /**
          * Creates a physics motion state for a rigid body.
          * 
-         * @param node The node that owns the rigid body that the motion state is being created for.
+         * @param node The node that contains the transformation to be associated with the motion state.
+         * @param collisionObject The collision object that owns the motion state.
          * @param centerOfMassOffset The translation offset to the center of mass of the rigid body.
          */
-        PhysicsMotionState(Node* node, const Vector3* centerOfMassOffset = NULL);
+        PhysicsMotionState(Node* node, PhysicsCollisionObject* collisionObject, const Vector3* centerOfMassOffset = NULL);
 
         /**
          * Destructor.
@@ -301,9 +302,15 @@ protected:
          */
         void updateTransformFromNode() const;
 
+        /**
+         * Sets the center of mass offset for the associated collision shape.
+         */
+        void setCenterOfMassOffset(const Vector3& centerOfMassOffset);
+
     private:
 
         Node* _node;
+        PhysicsCollisionObject* _collisionObject;
         btTransform _centerOfMassOffset;
         mutable btTransform _worldTransform;
     };

+ 124 - 104
gameplay/src/PhysicsCollisionShape.cpp

@@ -1,7 +1,9 @@
 #include "Base.h"
 #include "PhysicsCollisionShape.h"
 #include "Node.h"
+#include "Image.h"
 #include "Properties.h"
+#include "FileSystem.h"
 
 namespace gameplay
 {
@@ -32,13 +34,12 @@ PhysicsCollisionShape::~PhysicsCollisionShape()
 
             // Also need to delete the btTriangleIndexVertexArray, if it exists.
             SAFE_DELETE(_meshInterface);
-
             break;
+
         case SHAPE_HEIGHTFIELD:
             if (_shapeData.heightfieldData)
             {
-                SAFE_DELETE_ARRAY(_shapeData.heightfieldData->heightData);
-                SAFE_DELETE_ARRAY(_shapeData.heightfieldData->normalData);
+                SAFE_RELEASE(_shapeData.heightfieldData->heightfield);
                 SAFE_DELETE(_shapeData.heightfieldData);
             }
             break;
@@ -55,7 +56,7 @@ PhysicsCollisionShape::Type PhysicsCollisionShape::getType() const
 }
 
 PhysicsCollisionShape::Definition::Definition()
-    : isExplicit(false), centerAbsolute(false)
+    : type(SHAPE_NONE), isExplicit(false), centerAbsolute(false)
 {
     memset(&data, 0, sizeof(data));
 }
@@ -69,8 +70,8 @@ PhysicsCollisionShape::Definition::Definition(const Definition& definition)
     switch (type)
     {
     case PhysicsCollisionShape::SHAPE_HEIGHTFIELD:
-        GP_ASSERT(data.heightfield);
-        data.heightfield->addRef();
+        if (data.heightfield)
+            data.heightfield->addRef();
         break;
 
     case PhysicsCollisionShape::SHAPE_MESH:
@@ -105,8 +106,8 @@ PhysicsCollisionShape::Definition& PhysicsCollisionShape::Definition::operator=(
         switch (type)
         {
         case PhysicsCollisionShape::SHAPE_HEIGHTFIELD:
-            GP_ASSERT(data.heightfield);
-            data.heightfield->addRef();
+            if (data.heightfield)
+                data.heightfield->addRef();
             break;
 
         case PhysicsCollisionShape::SHAPE_MESH:
@@ -119,7 +120,12 @@ PhysicsCollisionShape::Definition& PhysicsCollisionShape::Definition::operator=(
     return *this;
 }
 
-PhysicsCollisionShape::Definition* PhysicsCollisionShape::Definition::create(Node* node, Properties* properties)
+bool PhysicsCollisionShape::Definition::isEmpty() const
+{
+    return type == SHAPE_NONE;
+}
+
+PhysicsCollisionShape::Definition PhysicsCollisionShape::Definition::create(Node* node, Properties* properties)
 {
     GP_ASSERT(node);
 
@@ -127,17 +133,21 @@ PhysicsCollisionShape::Definition* PhysicsCollisionShape::Definition::create(Nod
     if (!properties || !(strcmp(properties->getNamespace(), "collisionObject") == 0))
     {
         GP_ERROR("Failed to load physics collision shape from properties object: must be non-null object and have namespace equal to 'collisionObject'.");
-        return NULL;
+        return Definition();
     }
 
     // Set values to their defaults.
     PhysicsCollisionShape::Type type = PhysicsCollisionShape::SHAPE_BOX;
-    Vector3* extents = NULL;
-    Vector3* center = NULL;
+    Vector3 extents, center;
+    bool extentsSpecified = false;
+    bool centerSpecified = false;
     float radius = -1.0f;
+    float width = -1.0f;
     float height = -1.0f;
     bool centerIsAbsolute = false;
     const char* imagePath = NULL;
+    float maxHeight = 0;
+    float minHeight = 0;
     bool shapeSpecified = false;
 
     // Load the defined properties.
@@ -161,7 +171,7 @@ PhysicsCollisionShape::Definition* PhysicsCollisionShape::Definition::create(Nod
             else
             {
                 GP_ERROR("Could not create physics collision shape; unsupported value for collision shape type: '%s'.", shapeStr.c_str());
-                return NULL;
+                return Definition();
             }
 
             shapeSpecified = true;
@@ -170,23 +180,35 @@ PhysicsCollisionShape::Definition* PhysicsCollisionShape::Definition::create(Nod
         {
             imagePath = properties->getString();
         }
+        else if (strcmp(name, "maxHeight") == 0)
+        {
+            maxHeight = properties->getFloat();
+        }
+        else if (strcmp(name, "minHeight") == 0)
+        {
+            minHeight = properties->getFloat();
+        }
         else if (strcmp(name, "radius") == 0)
         {
             radius = properties->getFloat();
         }
+        else if (strcmp(name, "width") == 0)
+        {
+            width = properties->getFloat();
+        }
         else if (strcmp(name, "height") == 0)
         {
             height = properties->getFloat();
         }
         else if (strcmp(name, "extents") == 0)
         {
-            extents = new Vector3();
-            properties->getVector3("extents", extents);
+            properties->getVector3("extents", &extents);
+            extentsSpecified = true;
         }
         else if (strcmp(name, "center") == 0)
         {
-            center = new Vector3();
-            properties->getVector3("center", center);
+            properties->getVector3("center", &center);
+            centerSpecified = true;
         }
         else if (strcmp(name, "centerAbsolute") == 0)
         {
@@ -201,136 +223,124 @@ PhysicsCollisionShape::Definition* PhysicsCollisionShape::Definition::create(Nod
     if (!shapeSpecified)
     {
         GP_ERROR("Missing 'shape' specifier for collision shape definition.");
-        return NULL;
+        return Definition();
     }
 
     // Create the collision shape.
-    PhysicsCollisionShape::Definition* shape = new PhysicsCollisionShape::Definition();
+    Definition shape;
     switch (type)
     {
-        case SHAPE_BOX:
-            if (extents)
+    case SHAPE_BOX:
+        if (extentsSpecified)
+        {
+            if (centerSpecified)
             {
-                if (center)
-                {
-                    *shape = box(*extents, *center, centerIsAbsolute);
-                }
-                else
-                {
-                    *shape = box(*extents);
-                }
+                shape = box(extents, center, centerIsAbsolute);
             }
             else
             {
-                *shape = box();
+                shape = box(extents);
             }
-            break;
-        case SHAPE_SPHERE:
-            if (radius != -1.0f)
+        }
+        else
+        {
+            shape = box();
+        }
+        break;
+
+    case SHAPE_SPHERE:
+        if (radius != -1.0f)
+        {
+            if (centerSpecified)
             {
-                if (center)
-                {
-                    *shape = sphere(radius, *center, centerIsAbsolute);
-                }
-                else
-                {
-                    *shape = sphere(radius);
-                }
+                shape = sphere(radius, center, centerIsAbsolute);
             }
             else
             {
-                *shape = sphere();
+                shape = sphere(radius);
             }
-            break;
-        case SHAPE_CAPSULE:
-            if (radius != -1.0f && height != -1.0f)
+        }
+        else
+        {
+            shape = sphere();
+        }
+        break;
+
+    case SHAPE_CAPSULE:
+        if (radius != -1.0f && height != -1.0f)
+        {
+            if (centerSpecified)
             {
-                if (center)
-                {
-                    *shape = capsule(radius, height, *center, centerIsAbsolute);
-                }
-                else
-                {
-                    *shape = capsule(radius, height);
-                }
+                shape = capsule(radius, height, center, centerIsAbsolute);
             }
             else
             {
-                *shape = capsule();
+                shape = capsule(radius, height);
             }
-            break;
-        case SHAPE_MESH:
+        }
+        else
+        {
+            shape = capsule();
+        }
+        break;
+
+    case SHAPE_MESH:
         {
             // Mesh is required on node.
             Mesh* nodeMesh = node->getModel() ? node->getModel()->getMesh() : NULL;
             if (nodeMesh == NULL)
             {
                 GP_ERROR("Cannot create mesh collision object for node without model/mesh.");
-                return NULL;
             }
-
-            // Check that the node's mesh's primitive type is supported.
-            switch (nodeMesh->getPrimitiveType())
+            else
             {
-                case Mesh::TRIANGLES:
+                // Check that the node's mesh's primitive type is supported.
+                switch (nodeMesh->getPrimitiveType())
                 {
-                    *shape = mesh(nodeMesh);
+                case Mesh::TRIANGLES:
+                    shape = mesh(nodeMesh);
                     break;
-                }
                 case Mesh::LINES:
                 case Mesh::LINE_STRIP:
                 case Mesh::POINTS:
                 case Mesh::TRIANGLE_STRIP:
                     GP_ERROR("Mesh collision objects are currently only supported on meshes with primitive type equal to TRIANGLES.");
-                    SAFE_DELETE(shape);
                     break;
+                }
             }
-
-            break;
         }
-        case SHAPE_HEIGHTFIELD:
+        break;
+
+    case SHAPE_HEIGHTFIELD:
+        {
             if (imagePath == NULL)
             {
-                GP_ERROR("Heightfield collision objects require an image path.");
-                SAFE_DELETE(shape);
-                return NULL;
+                // Node requires a valid terrain
+                if (node->getTerrain() == NULL)
+                {
+                    GP_ERROR("Heightfield collision objects can only be specified on nodes that have a valid terrain, or that specify an image path.");
+                }
+                else
+                {
+                    shape = PhysicsCollisionShape::heightfield();
+                }
             }
             else
             {
-                // Load the image data from the given file path.
-                Image* image = Image::create(imagePath);
-                if (!image)
-                {
-                    GP_ERROR("Failed create image for heightfield collision object from file '%s'.", imagePath);
-                    SAFE_DELETE(shape);
-                    return NULL;
-                }
-
-                // Ensure that the image's pixel format is supported.
-                switch (image->getFormat())
+                Terrain::HeightField* heightfield = Terrain::HeightField::create(imagePath, (unsigned int)width, (unsigned int)height, minHeight, maxHeight);
+                if (heightfield)
                 {
-                    case Image::RGB:
-                    case Image::RGBA:
-                        break;
-                    default:
-                        GP_ERROR("Heightmap: pixel format is not supported: %d.", image->getFormat());
-                        SAFE_RELEASE(image);
-                        SAFE_DELETE(shape);
-                        return NULL;
+                    shape = PhysicsCollisionShape::heightfield(heightfield);
+                    SAFE_RELEASE(heightfield);
                 }
-
-                *shape = PhysicsCollisionShape::heightfield(image);
-                SAFE_RELEASE(image);
             }
-            break;
-        default:
-            GP_ERROR("Unsupported physics collision shape type (%d).", type);
-            SAFE_DELETE(shape);
-            return NULL;
-    }
+        }
+        break;
 
-    SAFE_DELETE(extents);
-    SAFE_DELETE(center);
+    default:
+        GP_ERROR("Unsupported physics collision shape type (%d).", type);
+        break;
+    }
 
     return shape;
 }
@@ -396,14 +406,24 @@ PhysicsCollisionShape::Definition PhysicsCollisionShape::capsule(float radius, f
     return d;
 }
 
-PhysicsCollisionShape::Definition PhysicsCollisionShape::heightfield(Image* image)
+PhysicsCollisionShape::Definition PhysicsCollisionShape::heightfield()
 {
-    GP_ASSERT(image);
-    image->addRef();
+    Definition d;
+    d.type = SHAPE_HEIGHTFIELD;
+    d.isExplicit = false;
+    d.centerAbsolute = false;
+    return d;
+}
+
+PhysicsCollisionShape::Definition PhysicsCollisionShape::heightfield(Terrain::HeightField* heightfield)
+{
+    GP_ASSERT(heightfield);
+
+    heightfield->addRef();
 
     Definition d;
     d.type = SHAPE_HEIGHTFIELD;
-    d.data.heightfield = image;
+    d.data.heightfield = heightfield;
     d.isExplicit = true;
     d.centerAbsolute = false;
     return d;

+ 39 - 11
gameplay/src/PhysicsCollisionShape.h

@@ -2,8 +2,8 @@
 #define PHYSICSCOLLISIONSHAPE_H_
 
 #include "Vector3.h"
-#include "Image.h"
 #include "Mesh.h"
+#include "Terrain.h"
 
 namespace gameplay
 {
@@ -25,6 +25,7 @@ public:
      */
     enum Type
     {
+        SHAPE_NONE,
         SHAPE_BOX,
         SHAPE_SPHERE,
         SHAPE_CAPSULE,
@@ -73,6 +74,11 @@ public:
          */
         ~Definition();
 
+        /**
+         * Determines if this is an empty/undefined collision shape definition.
+         */
+        bool isEmpty() const;
+
     private:
 
         /**
@@ -82,7 +88,7 @@ public:
          * @param properties The properties object to create the PhysicsCollisionShape::Definition object from.
          * @return A PhysicsCollisionShape::Definition object.
          */
-        static Definition* create(Node* node, Properties* properties);
+        static Definition create(Node* node, Properties* properties);
 
         // Shape type.
         PhysicsCollisionShape::Type type;
@@ -101,7 +107,7 @@ public:
             /** @script{ignore} */
             CapsuleData capsule;
             /** @script{ignore} */
-            Image* heightfield;
+            Terrain::HeightField* heightfield;
             /** @script{ignore} */
             Mesh* mesh;
         } data;
@@ -193,11 +199,34 @@ public:
     static PhysicsCollisionShape::Definition capsule(float radius, float height, const Vector3& center = Vector3::zero(), bool absolute = false);
 
     /**
-     * Defines a heightfield shape using the specified heightfield image.
+     * Defines a heightfield shape, using the height data of a terrain on the node that is attached to.
+     *
+     * This method only results in a valid heightfield collision object when the shape is used
+     * to create a collision object on a node that has a Terrain attached to it. If there is no
+     * Terrain attached to the node, the collision object creation will fail.
+     *
+     * @return Definition of a heightfield shape.
+     */
+    static PhysicsCollisionShape::Definition heightfield();
+
+    /**
+     * Defines a heightfield shape using the specified array of height values.
+     *
+     * The dimensions of the heightfield will be (width, maxHeight, height), where width and
+     * height are the dimensions of the passed in height array and maxHeight is the maximum
+     * height value in the height array.
+     *
+     * Heightfield rigid bodies are always assumed be Y-up (height value on the Y axis) and 
+     * be centered around the X and Z axes.
+     *
+     * The heightfield can be scaled once a PhysicsRigidBody has been created for it, using the
+     * PhysicsRigidBody::setLocalScaling method.
+     *
+     * @param heightfield HeightField object containing the array of height values representing the heightfield.
      *
      * @return Definition of a heightfield shape.
      */
-    static PhysicsCollisionShape::Definition heightfield(Image* image);
+    static PhysicsCollisionShape::Definition heightfield(Terrain::HeightField* heightfield);
 
     /**
      * Defines a mesh shape using the specified mesh.
@@ -216,12 +245,11 @@ private:
 
     struct HeightfieldData
     {
-        float* heightData;
-        Vector3* normalData;
-        unsigned int width;
-        unsigned int height;
-        mutable Matrix inverse;
-        mutable bool inverseIsDirty;
+        Terrain::HeightField* heightfield;
+        bool inverseIsDirty;
+        Matrix inverse;
+        float minHeight;
+        float maxHeight;
     };
 
     /**

+ 80 - 190
gameplay/src/PhysicsController.cpp

@@ -859,8 +859,19 @@ PhysicsCollisionShape* PhysicsController::createShape(Node* node, const PhysicsC
 
     case PhysicsCollisionShape::SHAPE_HEIGHTFIELD:
         {
-            // Build heightfield rigid body from the passed in shape.
-            collisionShape = createHeightfield(node, shape.data.heightfield, centerOfMassOffset);
+            if (shape.isExplicit)
+            {
+                // Build heightfield rigid body from the passed in shape.
+                collisionShape = createHeightfield(node, shape.data.heightfield, centerOfMassOffset);
+            }
+            else
+            {
+                // Build the heightfield from an attached terrain's height array
+                if (node->getTerrain() == NULL)
+                    GP_ERROR("Empty heightfield collision shapes can only be used on nodes that have an attached Terrain.");
+                else
+                    collisionShape = createHeightfield(node, node->getTerrain()->_heightfield, centerOfMassOffset);
+            }
         }
         break;
 
@@ -870,6 +881,7 @@ PhysicsCollisionShape* PhysicsController::createShape(Node* node, const PhysicsC
             collisionShape = createMesh(shape.data.mesh, scale);
         }
         break;
+
     default:
         GP_ERROR("Unsupported collision shape type (%d).", shape.type);
         break;
@@ -977,147 +989,53 @@ PhysicsCollisionShape* PhysicsController::createCapsule(float radius, float heig
     return shape;
 }
 
-PhysicsCollisionShape* PhysicsController::createHeightfield(Node* node, Image* image, Vector3* centerOfMassOffset)
+PhysicsCollisionShape* PhysicsController::createHeightfield(Node* node, Terrain::HeightField* heightfield, Vector3* centerOfMassOffset)
 {
     GP_ASSERT(node);
-    GP_ASSERT(image);
+    GP_ASSERT(heightfield);
     GP_ASSERT(centerOfMassOffset);
 
-    // Get the dimensions of the heightfield.
-    // If the node has a mesh defined, use the dimensions of the bounding box for the mesh.
-    // Otherwise simply use the image dimensions (with a max height of 255).
-    float width, length, minHeight, maxHeight;
-    if (node->getModel() && node->getModel()->getMesh())
+    // Inspect the height array for the min and max values
+    float* heights = heightfield->getArray();
+    float minHeight = FLT_MAX, maxHeight = -FLT_MAX;
+    for (unsigned int i = 0, count = heightfield->getColumnCount()*heightfield->getRowCount(); i < count; ++i)
     {
-        const BoundingBox& box = node->getModel()->getMesh()->getBoundingBox();
-        width = box.max.x - box.min.x;
-        length = box.max.z - box.min.z;
-        minHeight = box.min.y;
-        maxHeight = box.max.y;
-    }
-    else
-    {
-        width = image->getWidth();
-        length = image->getHeight();
-        minHeight = 0.0f;
-        maxHeight = 255.0f;
+        float h = heights[i];
+        if (h < minHeight)
+            minHeight = h;
+        if (h > maxHeight)
+            maxHeight = h;
     }
 
-    // Get the size in bytes of a pixel (we ensure that the image's
-    // pixel format is actually supported before calling this constructor).
-    unsigned int pixelSize = 0;
-    switch (image->getFormat())
-    {
-        case Image::RGB:
-            pixelSize = 3;
-            break;
-        case Image::RGBA:
-            pixelSize = 4;
-            break;
-        default:
-            GP_ERROR("Unsupported pixel format for heightmap image (%d).", image->getFormat());
-            return NULL;
-    }
+    // Compute initial heightfield scale by pulling the current world scale out of the node
+    Vector3 scale;
+    node->getWorldMatrix().getScale(&scale);
 
-    // Calculate the heights for each pixel.
-    float* heights = new float[image->getWidth() * image->getHeight()];
-    unsigned char* data = image->getData();
-    for (unsigned int x = 0, w = image->getWidth(); x < w; ++x)
+    // If the node has a terrain, apply the terrain's local scale to the world scale
+    if (node->getTerrain())
     {
-        for (unsigned int y = 0, h = image->getHeight(); y < h; ++y)
-        {
-            //
-            // Originally in GamePlay this was normalizedHeightGrayscale which generally yielded
-            // only 8-bit precision. This has been replaced by normalizedHeightPacked (with a
-            // corresponding change in gameplay-encoder).
-            //
-            // BACKWARD COMPATIBILITY
-            // In grayscale images where r=g=b this will maintain some degree of compatibility,
-            // to within 0.4%. This can be seen by setting r=g=b=x and comparing the grayscale
-            // height expression to the packed height expression: the error is 2^-8 + 2^-16
-            // which is just under 0.4%.
-            //
-            heights[x + y * w] = normalizedHeightPacked(
-                data[(x + y * w) * pixelSize + 0],
-                data[(x + y * w) * pixelSize + 1],
-                data[(x + y * w) * pixelSize + 2]) * (maxHeight - minHeight) + minHeight;
-        }
+        Vector3& tScale = node->getTerrain()->_localScale;
+        scale.set(scale.x * tScale.x, scale.y * tScale.y, scale.z * tScale.z);
     }
 
+    // Compute initial center of mass offset necessary to move the height from its position in bullet
+    // physics (always centered around origin) to its intended location.
+    centerOfMassOffset->set(0, -(minHeight + (maxHeight-minHeight)*0.5f) * scale.y, 0);
+
+    // Create our heightfield data to be stored in the collision shape
     PhysicsCollisionShape::HeightfieldData* heightfieldData = new PhysicsCollisionShape::HeightfieldData();
-    heightfieldData->heightData = NULL;
-    heightfieldData->normalData = NULL;
+    heightfieldData->heightfield = heightfield;
+    heightfieldData->heightfield->addRef();
     heightfieldData->inverseIsDirty = true;
+    heightfieldData->minHeight = minHeight;
+    heightfieldData->maxHeight = maxHeight;
 
-    unsigned int sizeWidth = width;
-    unsigned int sizeHeight = length;
-    GP_ASSERT(sizeWidth);
-    GP_ASSERT(sizeHeight);
-    
-    // Generate the heightmap data needed for physics (one height per world unit).
-    heightfieldData->width = sizeWidth + 1;
-    heightfieldData->height = sizeHeight + 1;
-    heightfieldData->heightData = new float[heightfieldData->width * heightfieldData->height];
-    heightfieldData->normalData = new Vector3[heightfieldData->width * heightfieldData->height];
-    unsigned int heightIndex = 0, prevRowIndex = 0, prevColIndex = 0;
-    float widthImageFactor = (float)(image->getWidth() - 1) / sizeWidth;
-    float heightImageFactor = (float)(image->getHeight() - 1) / sizeHeight;
-    float x = 0.0f;
-    float z = 0.0f;
-    const float horizStepsize = 1.0f;
-    for (unsigned int row = 0, z = 0.0f; row <= sizeHeight; row++, z += horizStepsize)
-    {
-        for (unsigned int col = 0, x = 0.0f; col <= sizeWidth; col++, x += horizStepsize)
-        {
-            heightIndex = row * heightfieldData->width + col;
-            prevRowIndex = heightIndex - heightfieldData->width; // ignored if row<1
-            prevColIndex = heightIndex - 1; // ignored if col<1
-
-            heightfieldData->heightData[heightIndex] = calculateHeight(heights, image->getWidth(), image->getHeight(), x * widthImageFactor, (sizeHeight - z) * heightImageFactor);
-
-            //
-            // Normal calculation based on height data using a backward difference.
-            //
-            if (row == 0 || col == 0)
-            {
-                // This is just a safe default value.
-                heightfieldData->normalData[heightIndex].set(Vector3::unitY());
-            }
-            else
-            {
-                heightfieldData->normalData[heightIndex].set(
-                    heightfieldData->heightData[prevColIndex] - heightfieldData->heightData[heightIndex],
-                    horizStepsize,
-                    heightfieldData->heightData[prevRowIndex] - heightfieldData->heightData[heightIndex]);
-                heightfieldData->normalData[heightIndex].normalize();
-            }
-
-            // For the starting row, just copy from the second row (i.e., a forward difference).
-            if (row == 1)
-            {
-                heightfieldData->normalData[prevRowIndex].set(heightfieldData->normalData[heightIndex]);
-            }
-
-            // For the starting column, just copy from the second column (i.e., a forward difference).
-            // (We don't care which of the 2 valid sources heightfieldData->normalData[0] ultimately comes from).
-            if (col == 1)
-            {
-                heightfieldData->normalData[prevColIndex].set(heightfieldData->normalData[heightIndex]);
-            }
-        }
-    }
-    SAFE_DELETE_ARRAY(heights);
-
-    // Offset the heightmap's center of mass according to the way that Bullet calculates the origin 
-    // of its heightfield collision shape; see documentation for the btHeightfieldTerrainShape for more info.
-    Vector3 s;
-    node->getWorldMatrix().getScale(&s);
-    GP_ASSERT(s.y);
-    centerOfMassOffset->set(0.0f, -(maxHeight - (0.5f * (maxHeight - minHeight))) / s.y, 0.0f);
-
-    // Create the bullet terrain shape.
+    // Create the bullet terrain shape
     btHeightfieldTerrainShape* terrainShape = bullet_new<btHeightfieldTerrainShape>(
-        heightfieldData->width, heightfieldData->height, heightfieldData->heightData, 1.0f, minHeight, maxHeight, 1, PHY_FLOAT, false);
+        heightfield->getColumnCount(), heightfield->getRowCount(), heightfield->getArray(), 1.0f, minHeight, maxHeight, 1, PHY_FLOAT, false);
+
+    // Set initial bullet local scaling for the heightfield
+    terrainShape->setLocalScaling(BV(scale));
 
     // Create our collision shape object and store heightfieldData in it.
     PhysicsCollisionShape* shape = new PhysicsCollisionShape(PhysicsCollisionShape::SHAPE_HEIGHTFIELD, terrainShape);
@@ -1302,8 +1220,7 @@ void PhysicsController::destroyShape(PhysicsCollisionShape* shape)
     }
 }
 
-float PhysicsController::calculateHeight(float* data, unsigned int width, unsigned int height, float x, float y,
-    const Matrix* worldMatrix, Vector3* normalData, Vector3* normalResult)
+float PhysicsController::calculateHeight(float* data, unsigned int width, unsigned int height, float x, float y)
 {
     GP_ASSERT(data);
 
@@ -1319,31 +1236,14 @@ float PhysicsController::calculateHeight(float* data, unsigned int width, unsign
 
     if (x2 >= width && y2 >= height)
     {
-        if (normalResult)
-        {
-            normalResult->set(normalData[x1 + y1 * width]);
-            worldMatrix->transformVector(normalResult);
-        }
         return data[x1 + y1 * width];
     }
     else if (x2 >= width)
     {
-        if (normalResult)
-        {
-            normalResult->set(normalData[x1 + y1 * width] * yFactorI + normalData[x1 + y2 * width] * yFactor);
-            normalResult->normalize();
-            worldMatrix->transformVector(normalResult);
-        }
         return data[x1 + y1 * width] * yFactorI + data[x1 + y2 * width] * yFactor;
     }
     else if (y2 >= height)
     {
-        if (normalResult)
-        {
-            normalResult->set(normalData[x1 + y1 * width] * xFactorI + normalData[x2 + y1 * width] * xFactor);
-            normalResult->normalize();
-            worldMatrix->transformVector(normalResult);
-        }
         return data[x1 + y1 * width] * xFactorI + data[x2 + y1 * width] * xFactor;
     }
     else
@@ -1352,28 +1252,11 @@ float PhysicsController::calculateHeight(float* data, unsigned int width, unsign
         float b = xFactorI * yFactor;
         float c = xFactor * yFactor;
         float d = xFactor * yFactorI;
-        if (normalResult)
-        {
-            normalResult->set(normalData[x1 + y1 * width] * a + normalData[x1 + y2 * width] * b +
-                normalData[x2 + y2 * width] * c + normalData[x2 + y1 * width] * d);
-            normalResult->normalize();
-            worldMatrix->transformVector(normalResult);
-        }
         return data[x1 + y1 * width] * a + data[x1 + y2 * width] * b +
             data[x2 + y2 * width] * c + data[x2 + y1 * width] * d;
     }
 }
 
-float PhysicsController::normalizedHeightGrayscale(float r, float g, float b)
-{
-    return (r + g + b) / 768.0f;
-}
-
-float PhysicsController::normalizedHeightPacked(float r, float g, float b)
-{
-    return (256.0f*r + g + 0.00390625f*b) / 65536.0f;
-}
-
 void PhysicsController::addConstraint(PhysicsRigidBody* a, PhysicsRigidBody* b, PhysicsConstraint* constraint)
 {
     GP_ASSERT(a);
@@ -1429,7 +1312,7 @@ void PhysicsController::removeConstraint(PhysicsConstraint* constraint)
 
 PhysicsController::DebugDrawer::DebugDrawer()
     : _mode(btIDebugDraw::DBG_DrawAabb | btIDebugDraw::DBG_DrawConstraintLimits | btIDebugDraw::DBG_DrawConstraints | 
-       btIDebugDraw::DBG_DrawContactPoints | btIDebugDraw::DBG_DrawWireframe), _viewProjection(NULL), _meshBatch(NULL)
+       btIDebugDraw::DBG_DrawContactPoints | btIDebugDraw::DBG_DrawWireframe), _meshBatch(NULL), _lineCount(0)
 {
     // Vertex shader for drawing colored lines.
     const char* vs_str = 
@@ -1460,14 +1343,14 @@ PhysicsController::DebugDrawer::DebugDrawer()
     Material* material = Material::create(effect);
     GP_ASSERT(material && material->getStateBlock());
     material->getStateBlock()->setDepthTest(true);
+    material->getStateBlock()->setDepthFunction(RenderState::DEPTH_LEQUAL);
 
     VertexFormat::Element elements[] =
     {
         VertexFormat::Element(VertexFormat::POSITION, 3),
         VertexFormat::Element(VertexFormat::COLOR, 4),
     };
-    _meshBatch = MeshBatch::create(VertexFormat(elements, 2), Mesh::LINES, material, false);
-
+    _meshBatch = MeshBatch::create(VertexFormat(elements, 2), Mesh::LINES, material, false, 4096, 4096);
     SAFE_RELEASE(material);
     SAFE_RELEASE(effect);
 }
@@ -1480,42 +1363,49 @@ PhysicsController::DebugDrawer::~DebugDrawer()
 void PhysicsController::DebugDrawer::begin(const Matrix& viewProjection)
 {
     GP_ASSERT(_meshBatch);
-    _viewProjection = &viewProjection;
     _meshBatch->start();
+    _meshBatch->getMaterial()->getParameter("u_viewProjectionMatrix")->setValue(viewProjection);
 }
 
 void PhysicsController::DebugDrawer::end()
 {
-    GP_ASSERT(_meshBatch && _meshBatch->getMaterial() && _meshBatch->getMaterial()->getParameter("u_viewProjectionMatrix"));
+    GP_ASSERT(_meshBatch && _meshBatch->getMaterial());
     _meshBatch->finish();
-    _meshBatch->getMaterial()->getParameter("u_viewProjectionMatrix")->setValue(_viewProjection);
     _meshBatch->draw();
+    _lineCount = 0;
 }
 
 void PhysicsController::DebugDrawer::drawLine(const btVector3& from, const btVector3& to, const btVector3& fromColor, const btVector3& toColor)
 {
     GP_ASSERT(_meshBatch);
 
-    static DebugDrawer::DebugVertex fromVertex, toVertex;
-
-    fromVertex.x = from.getX();
-    fromVertex.y = from.getY();
-    fromVertex.z = from.getZ();
-    fromVertex.r = fromColor.getX();
-    fromVertex.g = fromColor.getY();
-    fromVertex.b = fromColor.getZ();
-    fromVertex.a = 1.0f;
-
-    toVertex.x = to.getX();
-    toVertex.y = to.getY();
-    toVertex.z = to.getZ();
-    toVertex.r = toColor.getX();
-    toVertex.g = toColor.getY();
-    toVertex.b = toColor.getZ();
-    toVertex.a = 1.0f;
-
-    _meshBatch->add(&fromVertex, 1);
-    _meshBatch->add(&toVertex, 1);
+    static DebugDrawer::DebugVertex vertices[2];
+
+    vertices[0].x = from.getX();
+    vertices[0].y = from.getY();
+    vertices[0].z = from.getZ();
+    vertices[0].r = fromColor.getX();
+    vertices[0].g = fromColor.getY();
+    vertices[0].b = fromColor.getZ();
+    vertices[0].a = 1.0f;
+
+    vertices[1].x = to.getX();
+    vertices[1].y = to.getY();
+    vertices[1].z = to.getZ();
+    vertices[1].r = toColor.getX();
+    vertices[1].g = toColor.getY();
+    vertices[1].b = toColor.getZ();
+    vertices[1].a = 1.0f;
+
+    _meshBatch->add(vertices, 2);
+
+    ++_lineCount;
+    if (_lineCount >= 4096)
+    {
+        // Flush the batch when it gets full (don't want to to grow infinitely)
+        end();
+        _meshBatch->start();
+    }
 }
 
 void PhysicsController::DebugDrawer::drawLine(const btVector3& from, const btVector3& to, const btVector3& color)

+ 4 - 5
gameplay/src/PhysicsController.h

@@ -9,6 +9,7 @@
 #include "PhysicsSpringConstraint.h"
 #include "PhysicsCollisionObject.h"
 #include "MeshBatch.h"
+#include "Terrain.h"
 #include "ScriptTarget.h"
 
 namespace gameplay
@@ -430,7 +431,7 @@ private:
     PhysicsCollisionShape* createCapsule(float radius, float height, const Vector3& scale);
 
     // Creates a heightfield collision shape.
-    PhysicsCollisionShape* createHeightfield(Node* node, Image* image, Vector3* centerOfMassOffset);
+    PhysicsCollisionShape* createHeightfield(Node* node, Terrain::HeightField* heightfield, Vector3* centerOfMassOffset);
 
     // Creates a triangle mesh collision shape.
     PhysicsCollisionShape* createMesh(Mesh* mesh, const Vector3& scale);
@@ -439,9 +440,7 @@ private:
     void destroyShape(PhysicsCollisionShape* shape);
 
     // Helper function for calculating heights from heightmap (image) or heightfield data.
-    // The worldMatrix and normalData arguments are ignored if normalResult is NULL.
-    static float calculateHeight(float* data, unsigned int width, unsigned int height, float x, float y,
-        const Matrix* worldMatrix = NULL, Vector3* normalData = NULL, Vector3* normalResult = NULL);
+    static float calculateHeight(float* data, unsigned int width, unsigned int height, float x, float y);
 
     // Legacy method for grayscale heightmaps: r + g + b, normalized.
     static float normalizedHeightGrayscale(float r, float g, float b);
@@ -533,8 +532,8 @@ private:
     private:
         
         int _mode;
-        const Matrix* _viewProjection;
         MeshBatch* _meshBatch;
+        int _lineCount;
     };
 
     bool _isUpdating;

+ 4 - 5
gameplay/src/PhysicsGhostObject.cpp

@@ -23,7 +23,7 @@ PhysicsGhostObject::PhysicsGhostObject(Node* node, const PhysicsCollisionShape::
     _ghostObject->setCollisionFlags(_ghostObject->getCollisionFlags() | btCollisionObject::CF_NO_CONTACT_RESPONSE);
 
     // Initialize a physics motion state object for syncing the transform.
-    _motionState = new PhysicsMotionState(_node, &centerOfMassOffset);
+    _motionState = new PhysicsMotionState(_node, this, &centerOfMassOffset);
     _motionState->getWorldTransform(_ghostObject->getWorldTransform());
 
     // Add the ghost object to the physics world.
@@ -67,16 +67,15 @@ PhysicsGhostObject* PhysicsGhostObject::create(Node* node, Properties* propertie
     }
 
     // Load the physics collision shape definition.
-    PhysicsCollisionShape::Definition* shape = PhysicsCollisionShape::Definition::create(node, properties);
-    if (shape == NULL)
+    PhysicsCollisionShape::Definition shape = PhysicsCollisionShape::Definition::create(node, properties);
+    if (shape.isEmpty())
     {
         GP_ERROR("Failed to create collision shape during ghost object creation.");
         return NULL;
     }
 
     // Create the ghost object.
-    PhysicsGhostObject* ghost = new PhysicsGhostObject(node, *shape);
-    SAFE_DELETE(shape);
+    PhysicsGhostObject* ghost = new PhysicsGhostObject(node, shape);
 
     return ghost;
 }

+ 34 - 12
gameplay/src/PhysicsRigidBody.cpp

@@ -21,7 +21,7 @@ PhysicsRigidBody::PhysicsRigidBody(Node* node, const PhysicsCollisionShape::Defi
     GP_ASSERT(_collisionShape && _collisionShape->getShape());
 
     // Create motion state object.
-    _motionState = new PhysicsMotionState(node, (centerOfMassOffset.lengthSquared() > MATH_EPSILON) ? &centerOfMassOffset : NULL);
+    _motionState = new PhysicsMotionState(node, this, (centerOfMassOffset.lengthSquared() > MATH_EPSILON) ? &centerOfMassOffset : NULL);
 
     // If the mass is non-zero, then the object is dynamic so we calculate the local 
     // inertia. However, if the collision shape is a triangle mesh, we don't calculate 
@@ -31,7 +31,7 @@ PhysicsRigidBody::PhysicsRigidBody(Node* node, const PhysicsCollisionShape::Defi
         _collisionShape->getShape()->calculateLocalInertia(parameters.mass, localInertia);
 
     // Create the Bullet physics rigid body object.
-    btRigidBody::btRigidBodyConstructionInfo rbInfo(parameters.mass, _motionState, _collisionShape->getShape(), localInertia);
+    btRigidBody::btRigidBodyConstructionInfo rbInfo(parameters.mass, NULL, _collisionShape->getShape(), localInertia);
     rbInfo.m_friction = parameters.friction;
     rbInfo.m_restitution = parameters.restitution;
     rbInfo.m_linearDamping = parameters.linearDamping;
@@ -40,6 +40,10 @@ PhysicsRigidBody::PhysicsRigidBody(Node* node, const PhysicsCollisionShape::Defi
     // Create + assign the new bullet rigid body object.
     _body = bullet_new<btRigidBody>(rbInfo);
 
+    // Set motion state after rigid body assignment, since bullet will callback on the motion state interface to query
+    // the initial transform and it will need to access to rigid body (_body).
+    _body->setMotionState(_motionState);
+
     // Set other initially defined properties.
     setKinematic(parameters.kinematic);
     setAnisotropicFriction(parameters.anisotropicFriction);
@@ -176,8 +180,8 @@ PhysicsRigidBody* PhysicsRigidBody::create(Node* node, Properties* properties, c
     }
 
     // Load the physics collision shape definition.
-    PhysicsCollisionShape::Definition* shape = PhysicsCollisionShape::Definition::create(node, properties);
-    if (shape == NULL)
+    PhysicsCollisionShape::Definition shape = PhysicsCollisionShape::Definition::create(node, properties);
+    if (shape.isEmpty())
     {
         GP_ERROR("Failed to create collision shape during rigid body creation.");
         return NULL;
@@ -240,8 +244,7 @@ PhysicsRigidBody* PhysicsRigidBody::create(Node* node, Properties* properties, c
     }
 
     // Create the rigid body.
-    PhysicsRigidBody* body = new PhysicsRigidBody(node, *shape, parameters);
-    SAFE_DELETE(shape);
+    PhysicsRigidBody* body = new PhysicsRigidBody(node, shape, parameters);
 
     if (gravity)
     {
@@ -275,14 +278,14 @@ void PhysicsRigidBody::setEnabled(bool enable)
         _body->setMotionState(_motionState);
 }
 
-float PhysicsRigidBody::getHeight(float x, float y, Vector3* normal) const
+float PhysicsRigidBody::getHeight(float x, float y) const
 {
     GP_ASSERT(_collisionShape);
 
     // This function is only supported for heightfield rigid bodies.
     if (_collisionShape->getType() != PhysicsCollisionShape::SHAPE_HEIGHTFIELD)
     {
-        GP_ERROR("Attempting to get the height of a non-heightfield rigid body.");
+        GP_WARN("Attempting to get the height of a non-heightfield rigid body.");
         return 0.0f;
     }
 
@@ -296,8 +299,8 @@ float PhysicsRigidBody::getHeight(float x, float y, Vector3* normal) const
         _collisionShape->_shapeData.heightfieldData->inverseIsDirty = false;
     }
 
-    float w = _collisionShape->_shapeData.heightfieldData->width;
-    float h = _collisionShape->_shapeData.heightfieldData->height;
+    float w = _collisionShape->_shapeData.heightfieldData->heightfield->getColumnCount();
+    float h = _collisionShape->_shapeData.heightfieldData->heightfield->getRowCount();
 
     GP_ASSERT(w - 1);
     GP_ASSERT(h - 1);
@@ -313,8 +316,7 @@ float PhysicsRigidBody::getHeight(float x, float y, Vector3* normal) const
         return 0.0f;
     }
 
-    return PhysicsController::calculateHeight(_collisionShape->_shapeData.heightfieldData->heightData, w, h, x, y,
-        &_node->getWorldMatrix(), _collisionShape->_shapeData.heightfieldData->normalData, normal);
+    return PhysicsController::calculateHeight(_collisionShape->_shapeData.heightfieldData->heightfield->getArray(), w, h, x, y) * _collisionShape->getShape()->getLocalScaling().y();
 }
 
 void PhysicsRigidBody::addConstraint(PhysicsConstraint* constraint)
@@ -354,7 +356,27 @@ void PhysicsRigidBody::transformChanged(Transform* transform, long cookie)
     if (getShapeType() == PhysicsCollisionShape::SHAPE_HEIGHTFIELD)
     {
         GP_ASSERT(_collisionShape && _collisionShape->_shapeData.heightfieldData);
+
+        // Dirty the heightfield's inverse matrix (used to compute height values from world-space coordinates)
         _collisionShape->_shapeData.heightfieldData->inverseIsDirty = true;
+
+        // Update local scaling for the heightfield.
+        Vector3 scale;
+        _node->getWorldMatrix().getScale(&scale);
+
+        // If the node has a terrain attached, factor in the terrain local scaling as well for the collision shape
+        if (_node->getTerrain())
+        {
+            Vector3& tScale = _node->getTerrain()->_localScale;
+            scale.set(scale.x * tScale.x, scale.y * tScale.y, scale.z * tScale.z);
+        }
+
+        _collisionShape->_shape->setLocalScaling(BV(scale));
+
+        // Update center of mass offset
+        float minHeight = _collisionShape->_shapeData.heightfieldData->minHeight;
+        float maxHeight = _collisionShape->_shapeData.heightfieldData->maxHeight;
+        _motionState->setCenterOfMassOffset(Vector3(0, -(minHeight + (maxHeight-minHeight)*0.5f) * scale.y, 0));
     }
 }
 

+ 5 - 6
gameplay/src/PhysicsRigidBody.h

@@ -99,7 +99,7 @@ public:
         /**
          * Constructor.
          */
-        Parameters(float mass, float friction = 0.5f, float resititution = 0.0f,
+        Parameters(float mass, float friction = 0.5f, float restitution = 0.0f,
             float linearDamping = 0.0f, float angularDamping = 0.0f, bool kinematic = false,
             const Vector3& anisotropicFriction = Vector3::one(), const Vector3& linearFactor = Vector3::one(), 
             const Vector3& angularFactor = Vector3::one())
@@ -333,14 +333,13 @@ public:
     void setEnabled(bool enable);
 
     /**
-     * Gets the height and normal at the given point (only for rigid bodies of type HEIGHTFIELD).
+     * Gets the height at the given point (only for rigid bodies of type HEIGHTFIELD).
      * 
-     * @param x The x position.
-     * @param y The y position.
-     * @param normal If non-null, the surface normal at the given point.
+     * @param x The x position, in world space.
+     * @param y The y position, in world space.
      * @return The height at the given point, or zero if this is not a heightfield rigid body.
      */
-    float getHeight(float x, float y, Vector3* normal = NULL) const;
+    float getHeight(float x, float y) const;
 
     /**
      * Gets whether the rigid body is a static rigid body or not.

+ 27 - 1
gameplay/src/RenderState.cpp

@@ -11,6 +11,7 @@
 #define RS_CULL_FACE 4
 #define RS_DEPTH_TEST 8
 #define RS_DEPTH_WRITE 16
+#define RS_DEPTH_FUNC 32
 
 namespace gameplay
 {
@@ -354,7 +355,7 @@ void RenderState::cloneInto(RenderState* renderState, NodeCloneContext& context)
 }
 
 RenderState::StateBlock::StateBlock()
-    : _cullFaceEnabled(false), _depthTestEnabled(false), _depthWriteEnabled(false),
+    : _cullFaceEnabled(false), _depthTestEnabled(false), _depthWriteEnabled(false), _depthFunction(RenderState::DEPTH_LESS),
       _blendEnabled(false), _blendSrc(RenderState::BLEND_ONE), _blendDst(RenderState::BLEND_ZERO),
       _bits(0L)
 {
@@ -426,6 +427,11 @@ void RenderState::StateBlock::bindNoRestore()
         GL_ASSERT( glDepthMask(_depthWriteEnabled ? GL_TRUE : GL_FALSE) );
         _defaultState->_depthWriteEnabled = _depthWriteEnabled;
     }
+    if ((_bits & RS_DEPTH_FUNC) && (_depthFunction != _defaultState->_depthFunction))
+    {
+        GL_ASSERT( glDepthFunc((GLenum)_depthFunction) );
+        _defaultState->_depthFunction = _depthFunction;
+    }
 
     _defaultState->_bits |= _bits;
 }
@@ -472,6 +478,12 @@ void RenderState::StateBlock::restore(long stateOverrideBits)
         _defaultState->_bits &= ~RS_DEPTH_WRITE;
         _defaultState->_depthWriteEnabled = true;
     }
+    if (!(stateOverrideBits & RS_DEPTH_FUNC) && (_defaultState->_bits & RS_DEPTH_FUNC))
+    {
+        GL_ASSERT( glDepthFunc((GLenum)GL_LESS) );
+        _defaultState->_bits &= ~RS_DEPTH_FUNC;
+        _defaultState->_depthFunction = RenderState::DEPTH_LESS;
+    }
 }
 
 void RenderState::StateBlock::enableDepthWrite()
@@ -651,4 +663,18 @@ void RenderState::StateBlock::setDepthWrite(bool enabled)
     }
 }
 
+void RenderState::StateBlock::setDepthFunction(DepthFunction func)
+{
+    _depthFunction = func;
+    if (_depthFunction == DEPTH_LESS)
+    {
+        // Default depth function
+        _bits &= ~RS_DEPTH_FUNC;
+    }
+    else
+    {
+        _bits |= RS_DEPTH_FUNC;
+    }
+}
+
 }

+ 34 - 0
gameplay/src/RenderState.h

@@ -127,6 +127,27 @@ public:
         BLEND_SRC_ALPHA_SATURATE = GL_SRC_ALPHA_SATURATE
     };
 
+    /**
+     * Defines the supported depth compare functions.
+     *
+     * Depth compare functions specify the comparison that takes place between the
+     * incoming pixel's depth value and the depth value already in the depth buffer.
+     * If the compare function passes, the new pixel will be drawn.
+     *
+     * The intial depth compare function is DEPTH_LESS.
+     */
+    enum DepthFunction
+    {
+        DEPTH_NEVER = GL_NEVER,
+        DEPTH_LESS = GL_LESS,
+        DEPTH_EQUAL = GL_EQUAL,
+        DEPTH_LEQUAL = GL_LEQUAL,
+        DEPTH_GREATER = GL_GREATER,
+        DEPTH_NOTEQUAL = GL_NOTEQUAL,
+        DEPTH_GEQUAL = GL_GEQUAL,
+        DEPTH_ALWAYS = GL_ALWAYS
+    };
+
     /**
      * Defines a block of fixed-function render states that can be applied to a
      * RenderState object.
@@ -187,6 +208,8 @@ public:
         /**
          * Toggles depth testing.
          *
+         * By default, depth testing is disabled.
+         *
          * @param enabled true to enable, false to disable.
          */
         void setDepthTest(bool enabled);
@@ -198,6 +221,16 @@ public:
          */
         void setDepthWrite(bool enabled);
 
+        /**
+         * Sets the depth function to use when depth testing is enabled.
+         *
+         * When not explicitly set and when depth testing is enabled, the default
+         * depth function is DEPTH_LESS.
+         *
+         * @param func The depth function.
+         */
+        void setDepthFunction(DepthFunction func);
+
         /**
          * Sets a render state from the given name and value strings.
          *
@@ -237,6 +270,7 @@ public:
         bool _cullFaceEnabled;
         bool _depthTestEnabled;
         bool _depthWriteEnabled;
+        DepthFunction _depthFunction;
         bool _blendEnabled;
         Blend _blendSrc;
         Blend _blendDst;

+ 6 - 0
gameplay/src/ScriptController.cpp

@@ -1,7 +1,10 @@
 #include "Base.h"
 #include "FileSystem.h"
 #include "ScriptController.h"
+
+#ifndef NO_LUA_BINDINGS
 #include "lua/lua_all_bindings.h"
+#endif
 
 #define GENERATE_LUA_GET_POINTER(type, checkFunc) \
     ScriptController* sc = Game::getInstance()->getScriptController(); \
@@ -576,7 +579,10 @@ void ScriptController::initialize()
     if (!_lua)
         GP_ERROR("Failed to initialize Lua scripting engine.");
     luaL_openlibs(_lua);
+
+#ifndef NO_LUA_BINDINGS
     lua_RegisterAllBindings();
+#endif
 
     // Create our own print() function that uses gameplay::print.
     if (luaL_dostring(_lua, lua_print_function))

+ 650 - 0
gameplay/src/Terrain.cpp

@@ -0,0 +1,650 @@
+#include "Base.h"
+#include "Terrain.h"
+#include "Node.h"
+#include "Image.h"
+#include "FileSystem.h"
+
+namespace gameplay
+{
+
+// The default square size of terrain patches for a terrain that
+// does not have an explicitly specified patch size.
+//
+#define DEFAULT_TERRAIN_PATCH_SIZE 32
+
+// The default height of a terrain that does not have an explicitly
+// specified terrain size, expressed as a ratio of the average
+// of the dimensions of the terrain heightfield:
+//
+//   maxHeight = (image.width + image.height) / 2 * DEFAULT_TERRAIN_HEIGHT_RATIO
+//
+#define DEFAULT_TERRAIN_HEIGHT_RATIO 0.3f
+
+// Forward declaration of helper functions
+std::string getExtension(const char* filename);
+float getDefaultHeight(unsigned int width, unsigned int height);
+
+Terrain::Terrain() :
+    _heightfield(NULL), _node(NULL), _normalMap(NULL), _flags(ENABLE_FRUSTUM_CULLING | ENABLE_LEVEL_OF_DETAIL), _worldMatrixDirty(true)
+{
+}
+
+Terrain::~Terrain()
+{
+    if (_node)
+        _node->removeListener(this);
+
+    SAFE_RELEASE(_normalMap);
+    SAFE_RELEASE(_heightfield);
+}
+
+Terrain* Terrain::create(const char* terrainFile)
+{
+    // Terrain properties
+    HeightField* heightfield = NULL;
+    Vector3 terrainSize;
+    int patchSize = 0;
+    int detailLevels = 1;
+    float skirtScale = 0;
+    Properties* p = NULL;
+    Properties* pTerrain = NULL;
+    const char* normalMap = NULL;
+
+    std::string ext = getExtension(terrainFile);
+    if (ext == "PNG")
+    {
+        // Load terrain directly from a heightmap image
+        heightfield = HeightField::create(terrainFile, 0, 0, 0, 1);
+    }
+    else
+    {
+        // Read terrain from properties file
+        p = Properties::create(terrainFile);
+        if (p == NULL)
+            return NULL;
+
+        pTerrain = strlen(p->getNamespace()) > 0 ? p : p->getNextNamespace();
+        if (pTerrain == NULL)
+        {
+            GP_WARN("Invalid terrain definition file.");
+            SAFE_DELETE(p);
+            return NULL;
+        }
+
+        // Read heightmap info
+        Properties* pHeightmap = pTerrain->getNamespace("heightmap", true);
+        if (pHeightmap)
+        {
+            // Read heightmap path
+            const char* heightmap = pHeightmap->getString("path");
+            if (strlen(heightmap) == 0)
+            {
+                GP_WARN("No 'path' property supplied in heightmap section of terrain definition file: %s", terrainFile);
+                SAFE_DELETE(p);
+                return NULL;
+            }
+
+            ext = getExtension(heightmap);
+            if (ext == "PNG")
+            {
+                // Read normalized height values from heightmap image
+                heightfield = HeightField::create(heightmap, 0, 0, 0, 1);
+            }
+            else if (ext == "RAW")
+            {
+                // Require additional properties to be specified for RAW files
+                Vector2 imageSize;
+                if (!pHeightmap->getVector2("size", &imageSize))
+                {
+                    GP_WARN("Invalid or missing 'size' attribute in heightmap defintion of terrain file: %s", terrainFile);
+                    SAFE_DELETE(p);
+                    return NULL;
+                }
+
+                // Read normalized height values from RAW file
+                heightfield = HeightField::create(heightmap, (unsigned int)imageSize.x, (unsigned int)imageSize.y, 0, 1);
+            }
+            else
+            {
+                // Unsupported heightmap format
+                GP_WARN("Unsupported heightmap format ('%s') in terrain definition file: %s", heightmap, terrainFile);
+                SAFE_DELETE(p);
+                return NULL;
+            }
+        }
+        else
+        {
+            // Try to read 'heightmap' as a simple string property
+            const char* heightmap = pTerrain->getString("heightmap");
+            if (heightmap == NULL || strlen(heightmap) == 0)
+            {
+                GP_WARN("No 'heightmap' property supplied in terrain definition file: %s", terrainFile);
+                SAFE_DELETE(p);
+                return NULL;
+            }
+
+            ext = getExtension(heightmap);
+            if (ext == "PNG")
+            {
+                // Read normalized height values from heightmap image
+                heightfield = HeightField::create(heightmap, 0, 0, 0, 1);
+            }
+            else if (ext == "RAW")
+            {
+                GP_WARN("RAW heightmaps must be specified inside a heightmap block with width and height properties.");
+                SAFE_DELETE(p);
+                return NULL;
+            }
+            else
+            {
+                GP_WARN("Unsupported 'heightmap' format ('%s') in terrain definition file: %s.", heightmap, terrainFile);
+                SAFE_DELETE(p);
+                return NULL;
+            }
+        }
+
+        // Read terrain 'size'
+        if (pTerrain->exists("size"))
+        {
+            if (!pTerrain->getVector3("size", &terrainSize))
+            {
+                GP_WARN("Invalid 'size' value ('%s') in terrain definition file: %s", pTerrain->getString("size"), terrainFile);
+            }
+        }
+
+        // Read terrain 'patch size'
+        if (pTerrain->exists("patchSize"))
+        {
+            patchSize = pTerrain->getInt("patchSize");
+        }
+
+        // Read terrain 'detailLevels'
+        if (pTerrain->exists("detailLevels"))
+        {
+            detailLevels = pTerrain->getInt("detailLevels");
+        }
+
+        // Read 'skirtScale'
+        if (pTerrain->exists("skirtScale"))
+        {
+            skirtScale = pTerrain->getFloat("skirtScale");
+        }
+
+        // Read 'normalMap'
+        normalMap = pTerrain->getString("normalMap");
+    }
+
+    if (heightfield == NULL)
+    {
+        GP_WARN("Failed to read heightfield heights for terrain: %s", terrainFile);
+        SAFE_DELETE(p);
+        return NULL;
+    }
+
+    if (terrainSize.isZero())
+    {
+        terrainSize.set(heightfield->getColumnCount(), getDefaultHeight(heightfield->getColumnCount(), heightfield->getRowCount()), heightfield->getRowCount());
+    }
+
+    if (patchSize <= 0 || patchSize > (int)heightfield->getColumnCount() || patchSize > (int)heightfield->getRowCount())
+    {
+        patchSize = std::min(heightfield->getRowCount(), std::min(heightfield->getColumnCount(), (unsigned int)DEFAULT_TERRAIN_PATCH_SIZE));
+    }
+
+    if (detailLevels <= 0)
+        detailLevels = 1;
+
+    if (skirtScale < 0)
+        skirtScale = 0;
+
+    // Compute terrain scale
+    Vector3 scale(terrainSize.x / (heightfield->getColumnCount()-1), terrainSize.y, terrainSize.z / (heightfield->getRowCount()-1));
+
+    // Create terrain
+    Terrain* terrain = create(heightfield, scale, (unsigned int)patchSize, (unsigned int)detailLevels, skirtScale, normalMap, pTerrain);
+
+    SAFE_DELETE(p);
+
+    return terrain;
+}
+
+Terrain* Terrain::create(HeightField* heightfield, const Vector3& scale, unsigned int patchSize, unsigned int detailLevels, float skirtScale, const char* normalMapPath)
+{
+    return create(heightfield, scale, patchSize, detailLevels, skirtScale, normalMapPath, NULL);
+}
+
+Terrain* Terrain::create(HeightField* heightfield, const Vector3& scale, unsigned int patchSize, unsigned int detailLevels, float skirtScale, const char* normalMapPath, Properties* properties)
+{
+    GP_ASSERT(heightfield);
+
+    unsigned int width = heightfield->getColumnCount();
+    unsigned int height = heightfield->getRowCount();
+
+    // Create the terrain object
+    Terrain* terrain = new Terrain();
+    terrain->_heightfield = heightfield;
+    terrain->_localScale = scale;
+
+    if (normalMapPath)
+        terrain->_normalMap = Texture::Sampler::create(normalMapPath, true);
+
+    float halfWidth = (width - 1) * 0.5f;
+    float halfHeight = (height - 1) * 0.5f;
+    unsigned int maxStep = (unsigned int)std::pow(2.0, (double)(detailLevels-1));
+
+    // Create terrain patches
+    unsigned int x1, x2, z1, z2;
+    unsigned int row = 0, column = 0;
+    for (unsigned int z = 0; z < height-1; z = z2, ++row)
+    {
+        z1 = z;
+        z2 = std::min(z1 + patchSize, height-1);
+
+        for (unsigned int x = 0; x < width-1; x = x2, ++column)
+        {
+            x1 = x;
+            x2 = std::min(x1 + patchSize, width-1);
+
+            terrain->_patches.push_back(TerrainPatch::create(terrain, row, column, heightfield->getArray(), width, height, x1, z1, x2, z2, -halfWidth, -halfHeight, maxStep, skirtScale));
+        }
+    }
+
+    // Read additional layer information from properties (if specified)
+    if (properties)
+    {
+        // Parse terrain layers
+        Properties* lp;
+        int index = -1;
+        while (lp = properties->getNextNamespace())
+        {
+            if (strcmp(lp->getNamespace(), "layer") == 0)
+            {
+                // If there is no explicitly specified index for this layer, assume it's the 'next' layer
+                if (lp->exists("index"))
+                    index = lp->getInt("index");
+                else
+                    ++index;
+
+                const char* textureMap = NULL;
+                const char* blendMap = NULL;
+                Vector2 textureRepeat;
+                int blendChannel = 0;
+                int row = -1, column = -1;
+                Vector4 temp;
+
+                // Read layer textures
+                Properties* t = lp->getNamespace("texture", true);
+                if (t)
+                {
+                    textureMap = t->getString("path");
+                    if (!t->getVector2("repeat", &textureRepeat))
+                        textureRepeat.set(1,1);
+                }
+
+                Properties* b = lp->getNamespace("blend", true);
+                if (b)
+                {
+                    blendMap = b->getString("path");
+                    const char* channel = b->getString("channel");
+                    if (channel && strlen(channel) > 0)
+                    {
+                        char c = std::toupper(channel[0]);
+                        if (c == 'R' || c == '0')
+                            blendChannel = 0;
+                        else if (c == 'G' || c == '1')
+                            blendChannel = 1;
+                        else if (c == 'B' || c == '2')
+                            blendChannel = 2;
+                        else if (c == 'A' || c == '3')
+                            blendChannel = 3;
+                    }
+                }
+
+                // Get patch row/columns that this layer applies to.
+                if (lp->exists("row"))
+                    row = lp->getInt("row");
+                if (lp->exists("column"))
+                    column = lp->getInt("column");
+
+                if (!terrain->setLayer(index, textureMap, textureRepeat, blendMap, blendChannel, row, column))
+                {
+                    GP_WARN("Failed to load terrain layer: %s", textureMap);
+                }
+            }
+        }
+    }
+
+    // Load materials for all patches
+    for (size_t i = 0, count = terrain->_patches.size(); i < count; ++i)
+        terrain->_patches[i]->updateMaterial();
+
+    return terrain;
+}
+
+void Terrain::setNode(Node* node)
+{
+    if (_node != node)
+    {
+        if (_node)
+            _node->removeListener(this);
+
+        _node = node;
+
+        if (_node)
+            _node->addListener(this);
+
+        _worldMatrixDirty = true;
+    }
+}
+
+bool Terrain::setLayer(int index, const char* texturePath, const Vector2& textureRepeat, const char* blendPath, int blendChannel, int row, int column)
+{
+    if (!texturePath)
+        return false;
+
+    // Set layer on applicable patches
+    bool result = true;
+    for (size_t i = 0, count = _patches.size(); i < count; ++i)
+    {
+        TerrainPatch* patch = _patches[i];
+
+        if ((row == -1 || patch->_row == row) && (column == -1 || patch->_column == column))
+        {
+            if (!patch->setLayer(index, texturePath, textureRepeat, blendPath, blendChannel))
+                result = false;
+        }
+    }
+
+    return result;
+}
+
+bool Terrain::isFlagSet(Flags flag) const
+{
+    return (_flags & flag) == flag;
+}
+
+void Terrain::setFlag(Flags flag, bool on)
+{
+    bool changed = false;
+
+    if (on)
+    {
+        if ((_flags & flag) == 0)
+        {
+            _flags |= flag;
+            changed = true;
+        }
+    }
+    else
+    {
+        if ((_flags & flag) == flag)
+        {
+            _flags &= ~flag;
+            changed = true;
+        }
+    }
+
+    if (flag == DEBUG_PATCHES && changed)
+    {
+        // Dirty all materials since they need to be updated to support debug drawing
+        for (size_t i = 0, count = _patches.size(); i < count; ++i)
+            _patches[i]->_materialDirty = true;
+    }
+}
+
+unsigned int Terrain::getPatchCount() const
+{
+    return _patches.size();
+}
+
+unsigned int Terrain::getVisiblePatchCount() const
+{
+    unsigned int visibleCount = 0;
+    for (size_t i = 0, count = _patches.size(); i < count; ++i)
+    {
+        if (_patches[i]->isVisible())
+            ++visibleCount;
+    }
+    return visibleCount;
+}
+
+void Terrain::draw(bool wireframe)
+{
+    for (size_t i = 0, count = _patches.size(); i < count; ++i)
+    {
+        _patches[i]->draw(wireframe);
+    }
+}
+
+void Terrain::transformChanged(Transform* transform, long cookie)
+{
+    _worldMatrixDirty = true;
+}
+
+const Matrix& Terrain::getWorldMatrix() const
+{
+    if (_worldMatrixDirty)
+    {
+        _worldMatrixDirty = false;
+
+        // Apply our attached node's world matrix
+        if (_node)
+        {
+            _worldMatrix = _node->getWorldMatrix();
+        }
+        else
+        {
+            _worldMatrix.setIdentity();
+        }
+
+        // Factor in our local scaling
+        _worldMatrix.scale(_localScale);
+    }
+
+    return _worldMatrix;
+}
+
+const Matrix& Terrain::getWorldViewProjectionMatrix() const
+{
+    static Matrix worldViewProj;
+
+    if (_node)
+        Matrix::multiply(_node->getViewProjectionMatrix(), getWorldMatrix(), &worldViewProj);
+    else
+        worldViewProj = getWorldMatrix(); // no node, so nothing to get viewProjection from
+
+    return worldViewProj;
+}
+
+// Returns the uppercase extension of a file
+std::string getExtension(const char* filename)
+{
+    const char* str = strrchr(filename, '.');
+    if (str == NULL)
+        return NULL;
+
+    std::string ext;
+    size_t len = strlen(str);
+    for (size_t i = 1; i < len; ++i)
+        ext += std::toupper(str[i]);
+
+    return ext;
+}
+
+float getDefaultHeight(unsigned int width, unsigned int height)
+{
+    // When terrain height is not specified, we'll use a default height of ~ 0.3 of the image dimensions
+    return ((width + height) * 0.5f) * DEFAULT_TERRAIN_HEIGHT_RATIO;
+}
+
+Terrain::HeightField::HeightField(unsigned int columns, unsigned int rows)
+    : _array(NULL), _cols(columns), _rows(rows)
+{
+    _array = new float[columns * rows];
+}
+
+Terrain::HeightField::~HeightField()
+{
+    SAFE_DELETE_ARRAY(_array);
+}
+
+Terrain::HeightField* Terrain::HeightField::create(unsigned int columns, unsigned int rows)
+{
+    return new HeightField(columns, rows);
+}
+
+float normalizedHeightPacked(float r, float g, float b)
+{
+    return (256.0f*r + g + 0.00390625f*b) / 65536.0f;
+}
+
+Terrain::HeightField* Terrain::HeightField::create(const char* imagePath, unsigned int width, unsigned int height, float minHeight, float maxHeight)
+{
+    GP_ASSERT(imagePath);
+    GP_ASSERT(maxHeight >= minHeight);
+
+    // Validate input parameters
+    size_t pathLength = strlen(imagePath);
+    if (pathLength <= 4)
+    {
+        GP_WARN("Unrecognized file extension for heightfield image: %s.", imagePath);
+        return NULL;
+    }
+
+    float heightScale = maxHeight - minHeight;
+
+    HeightField* heightfield = NULL;
+
+    // Load height data from image
+    const char* ext = imagePath + (pathLength - 4);
+    if (ext[0] == '.' && toupper(ext[1]) == 'P' && toupper(ext[2]) == 'N' && toupper(ext[3]) == 'G')
+    {
+        // Normal image
+        Image* image = Image::create(imagePath);
+        if (!image)
+            return NULL;
+
+        unsigned int pixelSize = 0;
+        switch (image->getFormat())
+        {
+            case Image::RGB:
+                pixelSize = 3;
+                break;
+            case Image::RGBA:
+                pixelSize = 4;
+                break;
+            default:
+                SAFE_RELEASE(image);
+                GP_WARN("Unsupported pixel format for heightfield image: %s.", imagePath);
+                return NULL;
+        }
+
+        // Calculate the heights for each pixel.
+        heightfield = HeightField::create(image->getWidth(), image->getHeight());
+        float* heights = heightfield->getArray();
+        unsigned char* data = image->getData();
+        int idx;
+        for (int y = image->getHeight()-1, i = 0; y >= 0; --y)
+        {
+            for (unsigned int x = 0, w = image->getWidth(); x < w; ++x)
+            {
+                // Originally in GamePlay this was normalizedHeightGrayscale which yielded
+                // only 8-bit precision. This has been replaced by normalizedHeightPacked (with a
+                // corresponding change in gameplay-encoder).
+                //
+                // BACKWARD COMPATIBILITY
+                // In grayscale images where r=g=b this will maintain some degree of compatibility,
+                // to within 0.4%. This can be seen by setting r=g=b=x and comparing the grayscale
+                // height expression to the packed height expression: the error is 2^-8 + 2^-16
+                // which is just under 0.4%.
+                //
+                idx = (y*w + x) * pixelSize;
+                heights[i++] = minHeight + normalizedHeightPacked(data[idx], data[idx + 1], data[idx + 2]) * heightScale;
+            }
+        }
+
+        SAFE_RELEASE(image);
+    }
+    else if (ext[0] == '.' && toupper(ext[1]) == 'R' && toupper(ext[2]) == 'A' && toupper(ext[3]) == 'W')
+    {
+        // RAW image (headerless)
+        if (width < 2 || height < 2 || maxHeight < 0)
+        {
+            GP_WARN("Invalid 'width', 'height' or 'maxHeight' parameter for RAW heightfield image: %s.", imagePath);
+            return NULL;
+        }
+
+        // Load raw bytes
+        int fileSize = 0;
+        unsigned char* bytes = (unsigned char*)FileSystem::readAll(imagePath, &fileSize);
+        if (bytes == NULL)
+        {
+            GP_WARN("Falied to read bytes from RAW heightfield image: %s.", imagePath);
+            return NULL;
+        }
+
+        // Determine if the RAW file is 8-bit or 16-bit based on file size.
+        int bits = (fileSize / (width * height)) * 8;
+        if (bits != 8 && bits != 16)
+        {
+            GP_WARN("Invalid RAW file - must be 8-bit or 16-bit, but found neither: %s.", imagePath);
+            SAFE_DELETE_ARRAY(bytes);
+            return NULL;
+        }
+
+        heightfield = HeightField::create(width, height);
+        float* heights = heightfield->getArray();
+
+        // RAW files have an origin of bottom left, whereas our height array needs an origin of
+        // top left, so we need to flip the Y as we write height values out.
+        if (bits == 16)
+        {
+            // 16-bit (0-65535)
+            int idx;
+            for (int y = height-1, i = 0; y >= 0; --y)
+            {
+                for (unsigned int x = 0; x < width; ++x, ++i)
+                {
+                    idx = (y * width + x) << 1;
+                    heights[i] = minHeight + ((bytes[idx] | (int)bytes[idx+1] << 8) / 65535.0f) * heightScale;
+                }
+            }
+        }
+        else
+        {
+            // 8-bit (0-255)
+            for (int y = height-1, i = 0; y >= 0; --y)
+            {
+                for (unsigned int x = 0; x < width; ++x, ++i)
+                {
+                    heights[i] = minHeight + (bytes[y * width + x] / 255.0f) * heightScale;
+                }
+            }
+        }
+
+        SAFE_DELETE_ARRAY(bytes);
+    }
+    else
+    {
+        GP_WARN("Unsupported heightfield image format: %s.", imagePath);
+    }
+
+    return heightfield;
+}
+
+float* Terrain::HeightField::getArray() const
+{
+    return _array;
+}
+
+unsigned int Terrain::HeightField::getColumnCount() const
+{
+    return _cols;
+}
+
+unsigned int Terrain::HeightField::getRowCount() const
+{
+    return _rows;
+}
+
+}

+ 405 - 0
gameplay/src/Terrain.h

@@ -0,0 +1,405 @@
+#ifndef TERRAIN_H_
+#define TERRAIN_H_
+
+#include "TerrainPatch.h"
+#include "Transform.h"
+
+namespace gameplay
+{
+
+class Node;
+
+/**
+ * Defines a Terrain that is capable of rendering large landscapes from 2D heightmap images.
+ *
+ * Terrains can be constructed from several different heightmap sources:
+ *
+ * 1. Basic intensity image (PNG), where the intensity of pixel represents the height of the
+ *    terrain.
+ * 2. 24-bit high precision heightmap image (PNG), which can be generated from a mesh using
+ *    gameplay-encoder.
+ * 3. 8-bit or 16-bit RAW heightmap image using PC byte ordering (little endian), which is
+ *    compatible with many external tools such as World Machine, Unity and more.
+ *
+ * Physics/collision is supported by setting a rigid body collision object on the Node that
+ * the terrain is attached to. The collision shape should be specified using
+ * PhysicsCollisionShape::heightfield(), which will utilize the internal height array of the
+ * terrain to define the collision shape. Define a collision object in this way will allow
+ * the terrain to automatically interact with other rigid bodies, characters and vehicles in
+ * the scene.
+ *
+ * Surface detail is provided via texture splatting, where multiple texture layers can be added
+ * along with blend maps to define how different layers blend with each other. These layers
+ * can be defined in terrain properties files, as well as with the setLayer method. The number
+ * of supported layers depends on the target hardware, although typically 2-3 levels is
+ * sufficient. Multiple blend maps for different layers can be packed into different channels
+ * of a single texture for more efficient texture utilization. Levels can be applied across
+ * the entire terrain, or in more complex cases, for individual patches only.
+ *
+ * Surface lighting is achieved with either vertex normals or with a normal map. If a
+ * normal map is used, it should be an object-space normal map containing normal vectors for
+ * the entire terrain, encoded into the red, green and blue channels of the texture. This is
+ * useful as a replacement for vertex normals, especially when using level-of-detail, since
+ * it allows you to provide high quality normal vectors regardless of the tessellation or 
+ * LOD of the terrain. This also eliminates lighting artifacts/popping from LOD changes,
+ * which commonly occurs when vertex normals are used. A terrain normal map can only be
+ * specified at creation time, since it requires omission of vertex normal information in
+ * the generated terrain geometry data.
+ *
+ * Internally, Terrain is broken into smaller, more managable patches, which can be culled
+ * separately for more efficient rendering. The size of the terrain patches can be controlled
+ * via the patchSize property. Patches can be previewed by enabling the DEBUG_PATCHES flag
+ * via the setFlag method. Other terrain behavior can also be enabled and disabled using terrain
+ * flags.
+ * 
+ * Level of detail (LOD) is supported using a technique that is similar to texture mipmapping.
+ * A distance-to-camera based test, using a simple screen-space error metric is used to decide
+ * the appropriate LOD for a terrain patch. The number of LOD levels is 1 by default (which
+ * means only the base level is used), but can be specified via the detailLevels property.
+ * Using too large a number for detailLevels can result in excessive popping in the distance
+ * for very hilly terrains, so a smaller number (2-3) often works best in these cases.
+ *
+ * Finally, when LOD is enabled, cracks can begin to appear between terrain patches of
+ * different LOD levels. If the cracks are only minor (depends on your terrain topology
+ * and tetures used), an acceptable appraoch might be to simply use a background clear 
+ * color that closely matches your terrain to make the cracks much less visible. However,
+ * often that is not acceptable, so the Terrain class also supports a simple solution called
+ * "vertical skirts". When enabled (via the skirtScale parameter in the terrain file), a vertical
+ * edge will extend down along the sides of all terrain patches, which fills in the crack.
+ * This is a very fast approach as it adds only a small number of triangles per patch and requires
+ * zero extra CPU time or draw calls, which are often needed for more complex stitching 
+ * approaches. In practice, the skirts are often not noticable at all unless the LOD variation
+ * is very large and the terrain is excessively hilly on the edge of a LOD transition.
+ */
+class Terrain : public Ref, public Transform::Listener
+{
+    friend class Node;
+    friend class TerrainPatch;
+    friend class PhysicsController;
+    friend class PhysicsRigidBody;
+
+public:
+
+    /**
+     * Defines a reference counted class that holds heightfeild data.
+     *
+     * Heightfields can be used to construct both Terrain objects as well as PhysicsCollisionShape
+     * heightfield defintions, which are used in heightfield rigid body creation. Heightfields can
+     * be populated manually, or loaded from images and RAW files.
+     */
+    class HeightField : public Ref
+    {
+    public:
+
+        /**
+         * Creates a new HeightField of the given dimensions, with uninitialized height data.
+         *
+         * @param rows Number of rows in the height field.
+         * @param columns Number of columns in the height field.
+         *
+         * @return The new HeightField.
+         */
+        static HeightField* create(unsigned int rows, unsigned int columns);
+
+        /**
+         * Creates a HeightField from the specified heightfield image.
+         *
+         * The specified image path must refer to a valid heightfield image. Supported
+         * formats include PNG, RAW8 and RAW16. The latter two formats are simply raw,
+         * headerless height data in either 8-bit format or 16-bit format. RAW16 images
+         * must have little endian (PC) byte ordering. For images that have header structures
+         * (such as PNG), the width and hegiht parameters are ignored and are instead
+         * read from the image header.
+         *
+         * The minHeight and maxHeight parameters provides a mapping from heightfield pixel
+         * intensity to height values. The minHeight parameter is mapped to zero intensity
+         * pixel, while maxHeight maxHeight is mapped to full intensity pixels.
+         *
+         * @param imagePath Path to a heightfield image.
+         * @param width Width of the image (required for headerless/RAW files, can be zero for other image formats).
+         * @param height Height of the image (required for headerless/RAW files, can be zero for other image formats).
+         * @param minHeight Minimum height value for a zero intensity pixel.
+         * @param maxHeight Maximum height value for a full intensity heightfield pixel.
+         * 
+         * @return The new HeightField.
+         */
+        static HeightField* create(const char* imagePath, unsigned int width = 0, unsigned int height = 0, float minHeight = 0, float maxHeight = 1);
+
+        /**
+         * Returns a pointer to the underying height array.
+         *
+         * The array is packed in row major order, meaning that the data is aligned in rows,
+         * from top left to bottom right.
+         *
+         * @return The underlying height array.
+         */
+        float* getArray() const;
+
+        /**
+         * Returns the number of rows in the heightfield.
+         *
+         * @return The number of rows.
+         */
+        unsigned int getRowCount() const;
+
+        /**
+         * Returns the number of columns in the heightfield.
+         * 
+         * @return The column count.
+         */
+        unsigned int getColumnCount() const;
+
+    private:
+
+        /**
+         * Hidden constructor.
+         */
+        HeightField(unsigned int columns, unsigned int rows);
+
+        /**
+         * Hidden destructor (use Ref::release()).
+         */
+        ~HeightField();
+
+        float* _array;
+        unsigned int _rows;
+        unsigned int _cols;
+    };
+
+    /**
+     * Terrain flags.
+     */
+    enum Flags
+    {
+        /**
+         * Draw terrain patches different colors (off by default).
+         */
+        DEBUG_PATCHES = 1,
+
+        /**
+         * Enables view frustum culling (on by default).
+         *
+         * Frustum culling uses the scene's active camera. The terrain must be attached
+         * to a node that is within a scene for this to work.
+         */
+        ENABLE_FRUSTUM_CULLING = 2,
+
+         /**
+          * Enables level of detail (on by default).
+          *
+          * This flag enables or disables level of detail, however it does nothing if
+          * "detailLevels" was not set to a value greater than 1 in the terrain
+          * properties file at creation time.
+          */
+         ENABLE_LEVEL_OF_DETAIL = 8
+    };
+
+    /** 
+     * Loads a Terran from the given file.
+     *
+     * The specified file can be either a simple heightmap image, or it can be a terrain properties
+     * file containing the definition of a terrain.
+     *
+     * Only supported image types that have valid headers (i.e. not RAW) are supported if specifying
+     * an image directly. In this case, the dimensions of the unscaled terrain along each world axis
+     * will be as follows:
+     *
+     *  X = image.width
+     *  Y = (image.width + image.height) / 2 * 0.3f
+     *  Z = image.height
+     *
+     * When specifying an image directly, there terrain will be rendered using a default white material,
+     * until layer textures are specified via the setLayer method. Only a single level of detail will
+     * be generated and terrain patches will use a default size of 32x32.
+     *
+     * If specifying a terrain properties file, the properties file can contain a full terrain definition,
+     * including an image or RAW16/RAW32 heightmap, level of detail information, patch size, layer texture
+     * details and vertical skirt size.
+     *
+     * @param terrainFile Properties file describing the terrain.
+     *
+     * @return A new Terrain.
+     */
+    static Terrain* create(const char* terrainFile);
+
+    /**
+     * Creates a terrain from the given height array.
+     *
+     * Terrain geometry is loaded from the given height array, using the specified parameters for
+     * size, patch size, detail levels and skirt scale.
+     *
+     * @param heights Height array containing terrain height values.
+     * @param scale A scale to apply to the terrain along the X, Y and Z axes. The terrain and any associated
+     *      physics hegihtfield is scaled by this amount. Pass Vector3::one() to use the exact dimensions and heights
+     *      in the supplied height array.
+     * @param patchSize Size of terrain patches (number of quads).
+     * @param detailLevel Number of detail levels to generate for the terrain (a value of one generates only the base
+     *      level, resulting in no LOD at runtime.
+     * @param skirtScale A positive value indicates that vertical skirts should be generated at the specified
+     *      scale, which is relative to the height of the terrain. For example, a value of 0.5f indicates that
+     *      vertical skirts should extend down by half of the maximum height of the terrain. A value of zero
+     *      disables vertical skirts.
+     * @param normalMapPath Path to an object-space normal map to use for terrain lighting, instead of vertex normals.
+     *
+     * @return A new Terrain.
+     */
+    static Terrain* create(HeightField* heightfield,
+        const Vector3& scale = Vector3::one(), unsigned int patchSize = 32,
+        unsigned int detailLevels = 1, float skirtScale = 0.0f,
+        const char* normalMapPath = NULL);
+
+    /**
+     * Sets the detail textures information for a terrain layer.
+     *
+     * A detail layer includes a color texture, a repeat count across the terrain for the texture and
+     * a region of the texture to use.
+     *
+     * Optionally, a layer can also include a blend texture, which is used to instruct the terrain how
+     * to blend the new layer with the layer underneath it. Blend maps use only a single channel of a 
+     * texture and are best supplied by packing the blend map for a layer into the alpha channel of
+     * the color texture. Blend maps are always stretched over the entire terrain 
+     *
+     * The lowest/base layer of the terrain should not include a blend map, since there is no lower
+     * level to blend with. All other layers should normally include a blend map. However, since no
+     * blend map will result in the texture completely masking the layer underneath it.
+     *
+     * Detail layers can be applied globally (to the entire terrain), or to one or more specific
+     * patches in the terrain. Patches are specified by row and column number, which is dependent
+     * on the patch size configuration of your terrain. For layers that span the entire terrain, 
+     * the repeat count is relative to the entire terrain. For layers that span only specific
+     * patches, the repeat count is relative to those patches only.
+     *
+     * @param index Layer index number. Layer indexes do not neccessarily need to be sequential and
+     *      are used simply to uinquely identify layers, where higher numbers specificy higher-level
+     *      layers.
+     * @param texturePath Path to the color texture for this layer.
+     * @param textureRepeat Repeat count for the color texture across the terrain or patches.
+     * @param blendPath Path to the blend texture for this layer (optional).
+     * @param blendChannel Channel of the blend texture to sample for the blend map (0 == R, 1 == G, 2 == B, 3 == A).
+     * @param row Specifies the row index of patches to use this layer (optional, -1 means all rows).
+     * @param column Specifies the column index of patches to use this layer (optional, -1 means all columns).
+     *
+     * @return True if the layer was successfully set, false otherwise. The most common reason for failure is an
+     *      invalid texture path.
+     */
+    bool setLayer(int index,
+        const char* texturePath, const Vector2& textureRepeat = Vector2::one(),
+        const char* blendPath = NULL, int blendChannel = 0,
+        int row = -1, int column = -1);
+
+    /**
+     * Determines if the specified terrain flag is currently set.
+     */
+    bool isFlagSet(Flags flag) const;
+
+    /**
+     * Enables or disables the specified terrain flag.
+     *
+     * @param flag The terrain flag to set.
+     * @param on True to turn the flag on, false to turn it off.
+     */
+    void setFlag(Flags flag, bool on);
+
+    /**
+     * Returns the total number of terrain patches.
+     *
+     * @return The number of terrain patches.
+     */
+    unsigned int getPatchCount() const;
+
+    /**
+     * Returns the number of patches that are currently visible from the scene camera's point of view.
+     *
+     * If the terrain is not attached to a scene, or if there is no active scene camera, this method
+     * returns zero.
+     *
+     * This method is not exact - it may return false positives since it only determines if the
+     * bounding box of terrain patches intersect the view frustum. Should be used for debug 
+     * purposes only.
+     *
+     * @return The number of currently visible patches.
+     */
+    unsigned int getVisiblePatchCount() const;
+
+    /**
+     * Returns the total number of triangles for this terrain at the base LOD.
+     *
+     * @return The total triangle count for the terrain at the base LOD.
+     */
+    unsigned int getTriangleCount() const;
+
+    /**
+     * Returns the number of currently visible triangles, taking LOD and view frustum
+     * (if enabled) into consideration.
+     *
+     * @return The current visible triangle count.
+     */
+    unsigned int getVisibleTriangleCount() const;
+
+    /**
+     * Gets the local bounding box for this terrain.
+     *
+     * @return The local bounding box for the terrain.
+     */
+    const BoundingBox& getBoundingBox() const;
+
+    /**
+     * Draws the terrain.
+     *
+     * @param wireframe True to draw the terrain as wireframe, false to draw it solid (default).
+     */
+    void draw(bool wireframe = false);
+
+    /**
+     * @see Transform::Listener::transformChanged.
+     *
+     * Internal use only.
+     *
+     * @script{ignore}
+     */
+    void transformChanged(Transform* transform, long cookie);
+
+private:
+
+    /**
+     * Constructor.
+     */
+    Terrain();
+
+    /**
+     * Hidden copy constructor.
+     */
+    Terrain(const Terrain&);
+
+    /**
+     * Hidden copy assignment operator.
+     */
+    Terrain& operator=(const Terrain&);
+
+    /**
+     * Destructor.
+     */
+    ~Terrain();
+
+    static Terrain* create(HeightField* heightfield, const Vector3& scale, unsigned int patchSize, unsigned int detailLevels, float skirtScale, const char* normalMapPath, Properties* properties);
+
+    void setNode(Node* node);
+
+    const Matrix& getWorldMatrix() const;
+
+    const Matrix& getWorldViewProjectionMatrix() const;
+
+    HeightField* _heightfield;
+    Node* _node;
+    std::vector<TerrainPatch*> _patches;
+    Vector3 _localScale;
+    Texture::Sampler* _normalMap;
+    unsigned int _flags;
+    mutable Matrix _worldMatrix;
+    mutable bool _worldMatrixDirty;
+
+};
+
+}
+
+#endif

+ 585 - 0
gameplay/src/TerrainPatch.cpp

@@ -0,0 +1,585 @@
+#include "Base.h"
+#include "TerrainPatch.h"
+#include "Terrain.h"
+#include "MeshPart.h"
+#include "Scene.h"
+#include "Game.h"
+
+namespace gameplay
+{
+
+// Forward declarations
+float calculateHeight(float* heights, unsigned int width, unsigned int height, unsigned int x, unsigned int z);
+template <class T> T clamp(T value, T min, T max) { return value < min ? min : (value > max ? max : value); }
+
+TerrainPatch::TerrainPatch() :
+    _terrain(NULL), _row(0), _column(0), _materialDirty(true)
+{
+}
+
+TerrainPatch::~TerrainPatch()
+{
+    for (size_t i = 0, count = _levels.size(); i < count; ++i)
+    {
+        Level* level = _levels[i];
+
+        SAFE_RELEASE(level->model);
+        SAFE_DELETE(level);
+    }
+
+    while (_layers.size() > 0)
+    {
+        deleteLayer(*_layers.begin());
+    }
+}
+
+TerrainPatch* TerrainPatch::create(Terrain* terrain,
+    unsigned int row, unsigned int column,
+    float* heights, unsigned int width, unsigned int height,
+    unsigned int x1, unsigned int z1, unsigned int x2, unsigned int z2,
+    float xOffset, float zOffset,
+    unsigned int maxStep, float verticalSkirtSize)
+{
+    // Create patch
+    TerrainPatch* patch = new TerrainPatch();
+    patch->_terrain = terrain;
+    patch->_row = row;
+    patch->_column = column;
+
+    // Add patch lods
+    for (unsigned int step = 1; step <= maxStep; step *= 2)
+    {
+        patch->addLOD(heights, width, height, x1, z1, x2, z2, xOffset, zOffset, step, verticalSkirtSize);
+    }
+
+    return patch;
+}
+
+void TerrainPatch::addLOD(float* heights, unsigned int width, unsigned int height,
+    unsigned int x1, unsigned int z1, unsigned int x2, unsigned int z2,
+    float xOffset, float zOffset,
+    unsigned int step, float verticalSkirtSize)
+{
+    // Allocate vertex data for this patch
+    unsigned int patchWidth;
+    unsigned int patchHeight;
+    if (step == 1)
+    {
+        patchWidth = (x2 - x1) + 1;
+        patchHeight = (z2 - z1) + 1;
+    }
+    else
+    {
+        patchWidth = (x2 - x1) / step + ((x2 - x1) %step == 0 ? 0 : 1) + 1;
+        patchHeight = (z2 - z1) / step + ((z2 - z1) % step == 0 ? 0 : 1) + 1;
+    }
+
+    if (patchWidth < 2 || patchHeight < 2)
+        return; // ignore this level, not enough geometry
+
+    if (verticalSkirtSize > 0.0f)
+    {
+        patchWidth += 2;
+        patchHeight += 2;
+    }
+
+    unsigned int vertexCount = patchHeight * patchWidth;
+    unsigned int vertexElements = _terrain->_normalMap ? 5 : 8; //<x,y,z>[i,j,k]<u,v>
+    float* vertices = new float[vertexCount * vertexElements];
+    unsigned int index = 0;
+    Vector3 min(FLT_MAX, FLT_MAX, FLT_MAX);
+    Vector3 max(-FLT_MAX, -FLT_MAX, -FLT_MAX);
+    bool zskirt = verticalSkirtSize > 0 ? true : false;
+    for (unsigned int z = z1; ; )
+    {
+        bool xskirt = verticalSkirtSize > 0 ? true : false;
+        for (unsigned int x = x1; ; )
+        {
+            GP_ASSERT(index < vertexCount);
+
+            float* v = vertices + (index * vertexElements);
+            index++;
+
+            // Compute position
+            v[0] = x + xOffset;
+            v[1] = calculateHeight(heights, width, height, x, z);
+            if (xskirt || zskirt)
+                v[1] -= verticalSkirtSize;
+            v[2] = z + zOffset;
+
+            // Update bounding box min/max (don't include vertical skirt vertices in bounding box)
+            if (!(xskirt || zskirt))
+            {
+                if (v[0] < min.x)
+                    min.x = v[0];
+                if (v[1] < min.y)
+                    min.y = v[1];
+                if (v[2] < min.z)
+                    min.z = v[2];
+                if (v[0] > max.x)
+                    max.x = v[0];
+                if (v[1] > max.y)
+                    max.y = v[1];
+                if (v[2] > max.z)
+                    max.z = v[2];
+            }
+            v += 3;
+
+            // Compute normal
+            if (!_terrain->_normalMap)
+            {
+                float maxHeight = 100;
+                Vector3 p(x, calculateHeight(heights, width, height, x, z)*maxHeight, z);
+                Vector3 w(Vector3(x>=step ? x-step : x, calculateHeight(heights, width, height, x>=step ? x-step : x, z)*maxHeight, z), p);
+                Vector3 e(Vector3(x<width-step ? x+step : x, calculateHeight(heights, width, height, x<width-step ? x+step : x, z)*maxHeight, z), p);
+                Vector3 s(Vector3(x, calculateHeight(heights, width, height, x, z>=step ? z-step : z)*maxHeight, z>=step ? z-step : z), p);
+                Vector3 n(Vector3(x, calculateHeight(heights, width, height, x, z<height-step ? z+step : z)*maxHeight, z<height-step ? z+step : z), p);
+                Vector3 normals[4];
+                Vector3::cross(n, w, &normals[0]);
+                Vector3::cross(w, s, &normals[1]);
+                Vector3::cross(e, n, &normals[2]);
+                Vector3::cross(s, e, &normals[3]);
+                Vector3 normal = -(normals[0] + normals[1] + normals[2] + normals[3]);
+                normal.normalize();
+                v[0] = normal.x;
+                v[1] = normal.y;
+                v[2] = normal.z;
+                v += 3;
+            }
+
+            // Compute texture coord
+            v[0] = (float)x / width;
+            v[1] = 1.0f - (float)z / height;
+            if (xskirt)
+            {
+                float offset = verticalSkirtSize / width;
+                v[0] = x == x1 ? v[0]-offset : v[0]+offset;
+            }
+            else if (zskirt)
+            {
+                float offset = verticalSkirtSize / height;
+                v[1] = z == z1 ? v[1]-offset : v[1]+offset;
+            }
+
+            if (x == x2)
+            {
+                if ((verticalSkirtSize == 0) || xskirt)
+                    break;
+                else
+                    xskirt = true;
+            }
+            else if (xskirt)
+            {
+                xskirt = false;
+            }
+            else
+            {
+                x = std::min(x + step, x2);
+            }
+        }
+
+        if (z == z2)
+        {
+            if ((verticalSkirtSize == 0) || zskirt)
+                break;
+            else
+                zskirt = true;
+        }
+        else if (zskirt)
+        {
+            zskirt = false;
+        }
+        else
+        {
+            z = std::min(z + step, z2);
+        }
+    }
+    GP_ASSERT(index == vertexCount);
+
+    Vector3 center(min + ((max - min) * 0.5f));
+
+    // Create mesh
+    VertexFormat::Element elements[3];
+    elements[0] = VertexFormat::Element(VertexFormat::POSITION, 3);
+    if (_terrain->_normalMap)
+    {
+        elements[1] = VertexFormat::Element(VertexFormat::TEXCOORD0, 2);
+    }
+    else
+    {
+        elements[1] = VertexFormat::Element(VertexFormat::NORMAL, 3);
+        elements[2] = VertexFormat::Element(VertexFormat::TEXCOORD0, 2);
+    }
+    VertexFormat format(elements, _terrain->_normalMap ? 2 : 3);
+    Mesh* mesh = Mesh::createMesh(format, vertexCount);
+    mesh->setVertexData(vertices);
+    mesh->setBoundingBox(BoundingBox(min, max));
+    mesh->setBoundingSphere(BoundingSphere(center, center.distance(max)));
+
+    // Add mesh part for indices
+    unsigned int indexCount =
+        (patchWidth * 2) *      // # indices per row of tris
+        (patchHeight - 1) +     // # rows of tris
+        (patchHeight-2) * 2;    // # degenerate tris
+
+    // Support a maximum number of indices of USHRT_MAX. Any more indices we will require breaking up the
+    // terrain into smaller patches.
+    if (indexCount > USHRT_MAX)
+    {
+        GP_WARN("Index count of %d for terrain patch exceeds the limit of 65535. Please specifiy a smaller patch size.", indexCount);
+        GP_ASSERT(indexCount <= USHRT_MAX);
+    }
+
+    MeshPart* part = mesh->addPart(Mesh::TRIANGLE_STRIP, Mesh::INDEX16, indexCount);
+    unsigned short* indices = new unsigned short[indexCount];
+    index = 0;
+    for (unsigned int z = 0; z < patchHeight-1; ++z)
+    {
+        unsigned int i1 = z * patchWidth;
+        unsigned int i2 = (z+1) * patchWidth;
+
+        // Move left to right for even rows and right to left for odd rows.
+        // Note that this results in two degenerate triangles between rows
+        // for stitching purposes, but actually does not require any extra
+        // indices to achieve this.
+        if (z % 2 == 0)
+        {
+            if (z > 0)
+            {
+                // Add degenerate indices to connect strips
+                indices[index] = indices[index-1];
+                ++index;
+                indices[index++] = i1;
+            }
+
+            // Add row strip
+            for (unsigned int x = 0; x < patchWidth; ++x)
+            {
+                indices[index++] = i1 + x;
+                indices[index++] = i2 + x;
+            }
+        }
+        else
+        {
+            // Add degenerate indices to connect strips
+            if (z > 0)
+            {
+                indices[index] = indices[index-1];
+                ++index;
+                indices[index++] = i2 + ((int)patchWidth-1);
+            }
+
+            // Add row strip
+            for (int x = (int)patchWidth-1; x >= 0; --x)
+            {
+                indices[index++] = i2 + x;
+                indices[index++] = i1 + x;
+            }
+        }
+    }
+    GP_ASSERT(index == indexCount);
+    part->setIndexData(indices, 0, indexCount);
+
+    SAFE_DELETE_ARRAY(vertices);
+    SAFE_DELETE_ARRAY(indices);
+
+    // Create model
+    Model* model = Model::create(mesh);
+    mesh->release();
+
+    // Add this level
+    Level* level = new Level();
+    level->model = model;
+    _levels.push_back(level);
+}
+
+void TerrainPatch::deleteLayer(Layer* layer)
+{
+    // Release layer samplers
+    if (layer->textureIndex != -1)
+    {
+        if (_samplers[layer->textureIndex]->getRefCount() == 1)
+        {
+            SAFE_RELEASE(_samplers[layer->textureIndex]);
+        }
+        else
+        {
+            _samplers[layer->textureIndex]->release();
+        }
+    }
+
+    if (layer->blendIndex != -1)
+    {
+        if (_samplers[layer->blendIndex]->getRefCount() == 1)
+        {
+            SAFE_RELEASE(_samplers[layer->blendIndex]);
+        }
+        else
+        {
+            _samplers[layer->blendIndex]->release();
+        }
+    }
+
+    _layers.erase(layer);
+    SAFE_DELETE(layer);
+}
+
+bool TerrainPatch::isVisible() const
+{
+    Scene* scene = _terrain->_node ? _terrain->_node->getScene() : NULL;
+    Camera* camera = scene ? scene->getActiveCamera() : NULL;
+    if (!camera)
+        return false;
+
+    // Get the world-space bounding box for the base patch
+    BoundingBox box = _levels[0]->model->getMesh()->getBoundingBox();
+    box.transform(_terrain->getWorldMatrix());
+
+    // If the box does not intersect the view frustum, cull it
+    return camera->getFrustum().intersects(box);
+}
+
+int TerrainPatch::addSampler(const char* path)
+{
+    // TODO: Support shared samplers stored in Terrain class for layers that span all patches
+    // on the terrain (row == col == -1).
+
+    // Load the texture. If this texture is already loaded, it will return
+    // a pointer to the same one, with its ref count incremented.
+    Texture* texture = Texture::create(path, true);
+    if (!texture)
+        return -1;
+
+    size_t firstAvailableIndex = -1;
+    for (size_t i = 0, count = _samplers.size(); i < count; ++i)
+    {
+        Texture::Sampler* sampler = _samplers[i];
+
+        if (sampler == NULL && firstAvailableIndex == -1)
+        {
+            firstAvailableIndex = i;
+        }
+        else if (sampler->getTexture() == texture)
+        {
+            // A sampler was already added for this texture.
+            // Increase the ref count for the sampler to indicate that a new
+            // layer will be referencing it.
+            texture->release();
+            sampler->addRef();
+            return (int)i;
+        }
+    }
+
+    // Add a new sampler to the list
+    Texture::Sampler* sampler = Texture::Sampler::create(texture);
+    texture->release();
+    sampler->setWrapMode(Texture::REPEAT, Texture::REPEAT);
+    sampler->setFilterMode(Texture::LINEAR_MIPMAP_LINEAR, Texture::LINEAR);
+    if (firstAvailableIndex != -1)
+    {
+        _samplers[firstAvailableIndex] = sampler;
+        return firstAvailableIndex;
+    }
+
+    _samplers.push_back(sampler);
+    return (int)(_samplers.size()-1);
+}
+
+bool TerrainPatch::setLayer(int index, const char* texturePath, const Vector2& textureRepeat, const char* blendPath, int blendChannel)
+{
+    // If there is an existing layer at this index, delete it
+    for (std::set<Layer*, LayerCompare>::iterator itr = _layers.begin(); itr != _layers.end(); ++itr)
+    {
+        Layer* layer = *itr;
+        if (layer->index == index)
+        {
+            deleteLayer(layer);
+            break;
+        }
+    }
+
+    // Load texture sampler
+    int textureIndex = addSampler(texturePath);
+    if (textureIndex == -1)
+        return false;
+
+    // Load blend sampler
+    int blendIndex = -1;
+    if (blendPath)
+    {
+        blendIndex = addSampler(blendPath);
+    }
+
+    // Create the layer
+    Layer* layer = new Layer();
+    layer->index = index;
+    layer->textureIndex = textureIndex;
+    layer->textureRepeat = textureRepeat;
+    layer->blendIndex = blendIndex;
+    layer->blendChannel = blendChannel;
+
+    _layers.insert(layer);
+
+    _materialDirty = true;
+
+    return true;
+}
+
+bool TerrainPatch::updateMaterial()
+{
+    if (!_materialDirty)
+        return true;
+
+    _materialDirty = false;
+
+    for (size_t i = 0, count = _levels.size(); i < count; ++i)
+    {
+        // Load material/shaders
+        char defines[1024];
+        sprintf(defines, "LAYER_COUNT %d;SAMPLER_COUNT %d%s%s",
+            (int)_layers.size(),
+            (int)_samplers.size(),
+            _terrain->isFlagSet(Terrain::DEBUG_PATCHES) ? ";DEBUG_PATCHES" : "",
+            _terrain->_normalMap ? ";NORMAL_MAP" : "");
+        Material* material = Material::create("res/shaders/terrain.vert", "res/shaders/terrain.frag", defines);
+        if (!material)
+            return false;
+        material->getStateBlock()->setCullFace(true);
+        material->getStateBlock()->setDepthTest(true);
+
+        // Build layer lists
+        _textureIndex.clear();
+        _textureRepeat.clear();
+        _blendIndex.clear();
+        _blendChannel.clear();
+        for (std::set<Layer*, LayerCompare>::iterator itr = _layers.begin(); itr != _layers.end(); ++itr)
+        {
+            Layer* layer = *itr;
+
+            // Add texture parameters for every layer
+            _textureIndex.push_back(layer->textureIndex);
+            _textureRepeat.push_back(layer->textureRepeat);
+
+            // Add blend parameters for all but the first layer
+            if (itr != _layers.begin())
+            {
+                _blendIndex.push_back(layer->blendIndex);
+                _blendChannel.push_back(layer->blendChannel);
+            }
+        }
+
+        // Set material parameter bindings
+        material->getParameter("u_worldViewProjectionMatrix")->bindValue(_terrain, &Terrain::getWorldViewProjectionMatrix);
+        material->getParameter("u_lightDirection")->setValue(Vector3(-1,-1,-1));
+        material->getParameter("u_lightColor")->setValue(Vector3(1,1,1));
+        if (_layers.size() > 0)
+        {
+            material->getParameter("u_samplers")->setValue((const Texture::Sampler**)&_samplers[0], (unsigned int)_samplers.size());
+            material->getParameter("u_textureIndex")->setValue(&_textureIndex[0], (unsigned int)_textureIndex.size());
+            material->getParameter("u_textureRepeat")->setValue(&_textureRepeat[0], (unsigned int)_textureRepeat.size());
+            if (_terrain->_normalMap)
+                material->getParameter("u_normalMap")->setValue(_terrain->_normalMap);
+            if (_layers.size() > 1)
+            {
+                material->getParameter("u_blendIndex")->setValue(&_blendIndex[0], (unsigned int)_blendIndex.size());
+                material->getParameter("u_blendChannel")->setValue(&_blendChannel[0], (unsigned int)_blendChannel.size());
+            }
+        }
+
+        if (_terrain->isFlagSet(Terrain::DEBUG_PATCHES))
+        {
+            material->getParameter("u_row")->setValue((int)_row);
+            material->getParameter("u_column")->setValue((int)_column);
+        }
+
+        // Set material on this lod level
+        _levels[i]->model->setMaterial(material);
+
+        material->release();
+    }
+
+    return true;
+}
+
+void TerrainPatch::draw(bool wireframe)
+{
+    Scene* scene = _terrain->_node ? _terrain->_node->getScene() : NULL;
+    Camera* camera = scene ? scene->getActiveCamera() : NULL;
+    if (!camera)
+        return;
+
+    // Get the local bounding box for the patch (at the base LOD) and
+    // transform it by the terrain's world matrix.
+    BoundingBox box = _levels[0]->model->getMesh()->getBoundingBox();
+    box.transform(_terrain->getWorldMatrix());
+
+    // If the box does not intersect the view frustum, cull it
+    if (_terrain->isFlagSet(Terrain::ENABLE_FRUSTUM_CULLING) && !camera->getFrustum().intersects(box))
+        return;
+
+    if (!updateMaterial())
+        return;
+
+    size_t lod = 0;
+    if (_terrain->isFlagSet(Terrain::ENABLE_LEVEL_OF_DETAIL) && _levels.size() > 1)
+    {
+        // Compute LOD to use based on very simple distance metric
+        Game* game = Game::getInstance();
+        Rectangle vp(0, 0, game->getWidth(), game->getHeight());
+        Vector3 corners[8];
+        Vector2 min(FLT_MAX, FLT_MAX);
+        Vector2 max(-FLT_MAX, -FLT_MAX);
+        box.getCorners(corners);
+        for (unsigned int i = 0; i < 8; ++i)
+        {
+            const Vector3& corner = corners[i];
+            float x, y;
+            camera->project(vp, corners[i], &x, &y);
+            if (x < min.x)
+                min.x = x;
+            if (y < min.y)
+                min.y = y;
+            if (x > max.x)
+                max.x = x;
+            if (y > max.y)
+                max.y = y;
+        }
+        float area = (max.x - min.x) * (max.y - min.y);
+        float screenArea = game->getWidth() * game->getHeight() / 10.0f;
+        float error = screenArea / area;
+
+        // Level LOD based on distance from camera
+        size_t maxLod = _levels.size()-1;
+        lod = (size_t)error;
+        lod = std::max(lod, (size_t)0);
+        lod = std::min(lod, maxLod);
+    }
+
+    // Get the LOD to be drawn
+    Level* level = _levels[lod];
+    Model* model = level->model;
+
+    // Draw patch geometry
+    model->draw(wireframe);
+}
+
+float calculateHeight(float* heights, unsigned int width, unsigned int height, unsigned int x, unsigned int z)
+{
+    return heights[z * width + x];
+}
+
+TerrainPatch::Layer::Layer() :
+    index(0), row(-1), column(-1), textureIndex(-1), blendIndex(-1)
+{
+}
+
+TerrainPatch::Layer::~Layer()
+{
+}
+
+bool TerrainPatch::LayerCompare::operator() (const Layer* lhs, const Layer* rhs) const
+{
+    return (lhs->index < rhs->index);
+}
+
+}

+ 138 - 0
gameplay/src/TerrainPatch.h

@@ -0,0 +1,138 @@
+#ifndef TERRAINPATCH_H_
+#define TERRAINPATCH_H_
+
+#include "Model.h"
+
+namespace gameplay
+{
+
+class Terrain;
+
+/**
+ * Represents a single patch for a Terrain.
+ *
+ * This is an internal class used exclusively by Terrain.
+ */
+class TerrainPatch
+{
+    friend class Terrain;
+
+private:
+
+    struct Layer
+    {
+        Layer();
+        Layer(const Layer&);
+        ~Layer();
+        Layer& operator=(const Layer&);
+
+        int index;
+        int row;
+        int column;
+        int textureIndex;
+        Vector2 textureRepeat;
+        int blendIndex;
+        int blendChannel;
+    };
+
+    struct Level
+    {
+        Model* model;
+    };
+
+    struct LayerCompare
+    {
+        bool operator() (const Layer* lhs, const Layer* rhs) const;
+    };
+
+    /**
+     * Constructor.
+     */
+    TerrainPatch();
+
+    /**
+     * Hidden copy constructor.
+     */
+    TerrainPatch(const TerrainPatch&);
+
+    /**
+     * Hidden copy assignment operator.
+     */
+    TerrainPatch& operator=(const TerrainPatch&);
+
+    /**
+     * Destructor.
+     */
+    ~TerrainPatch();
+
+    /**
+     * Internal method to create new terrain patch.
+     */
+    static TerrainPatch* create(Terrain* terrain,
+        unsigned int row, unsigned int column,
+        float* heights, unsigned int width, unsigned int height,
+        unsigned int x1, unsigned int z1, unsigned int x2, unsigned int z2,
+        float xOffset, float zOffset,
+        unsigned int maxStep, float verticalSkirtSize);
+
+    /**
+     * Adds a single LOD level to the terrain patch.
+     */
+    void addLOD(float* heights, unsigned int width, unsigned int height,
+        unsigned int x1, unsigned int z1, unsigned int x2, unsigned int z2,
+        float xOffset, float zOffset,
+        unsigned int step, float verticalSkirtSize);
+
+    /**
+     * Sets details for a layer of this patch.
+     */
+    bool setLayer(int index, const char* texturePath, const Vector2& textureRepeat, const char* blendPath, int blendChannel);
+
+    /**
+     * Sets the level of detail for the terrain patch.
+     */
+    void setLod(unsigned int lod);
+
+    /**
+     * Adds a sampler to the patch.
+     */
+    int addSampler(const char* path);
+
+    /**
+     * Deletes the specified layer.
+     */
+    void deleteLayer(Layer* layer);
+
+    /**
+     * Determines whether this patch is current visible by the scene's active camera.
+     */
+    bool isVisible() const;
+
+    /**
+     * Draws the terrain patch.
+     */
+    void draw(bool wireframe);
+
+    /**
+     * Updates the material for the patch.
+     */
+    bool updateMaterial();
+
+    Terrain* _terrain;
+    std::vector<Level*> _levels;
+    unsigned int _lod;
+    unsigned int _row;
+    unsigned int _column;
+    std::set<Layer*, LayerCompare> _layers;
+    std::vector<Texture::Sampler*> _samplers;
+    std::vector<int> _textureIndex;
+    std::vector<Vector2> _textureRepeat;
+    std::vector<int> _blendIndex;
+    std::vector<int> _blendChannel;
+    bool _materialDirty;
+
+};
+
+}
+
+#endif

+ 7 - 4
gameplay/src/Texture.cpp

@@ -758,6 +758,11 @@ Texture::Format Texture::getFormat() const
     return _format;
 }
 
+const char* Texture::getPath() const
+{
+    return _path.c_str();
+}
+
 unsigned int Texture::getWidth() const
 {
     return _width;
@@ -837,12 +842,14 @@ void Texture::Sampler::setWrapMode(Wrap wrapS, Wrap wrapT)
 {
     _wrapS = wrapS;
     _wrapT = wrapT;
+    _texture->setWrapMode(wrapS, wrapT);
 }
 
 void Texture::Sampler::setFilterMode(Filter minificationFilter, Filter magnificationFilter)
 {
     _minFilter = minificationFilter;
     _magFilter = magnificationFilter;
+    _texture->setFilterMode(minificationFilter, magnificationFilter);
 }
 
 Texture* Texture::Sampler::getTexture() const
@@ -855,10 +862,6 @@ void Texture::Sampler::bind()
     GP_ASSERT(_texture);
 
     GL_ASSERT( glBindTexture(GL_TEXTURE_2D, _texture->_handle) );
-    GL_ASSERT( glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, (GLenum)_wrapS) );
-    GL_ASSERT( glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, (GLenum)_wrapT) );
-    GL_ASSERT( glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, (GLenum)_minFilter) );
-    GL_ASSERT( glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, (GLenum)_magFilter) );
 }
 
 }

+ 7 - 0
gameplay/src/Texture.h

@@ -198,6 +198,13 @@ public:
      */
     static Texture* create(TextureHandle handle, int width, int height, Format format = UNKNOWN);
 
+    /**
+     * Returns the path that the texture was originally loaded from (if applicable).
+     *
+     * @return The texture path, or an empty string if the texture was not loaded from file.
+     */
+    const char* getPath() const;
+
     /**
      * Gets the format of the texture.
      *

+ 2 - 0
gameplay/src/gameplay.h

@@ -29,6 +29,7 @@
 
 // Graphics
 #include "Texture.h"
+#include "Image.h"
 #include "Mesh.h"
 #include "MeshPart.h"
 #include "Effect.h"
@@ -37,6 +38,7 @@
 #include "VertexFormat.h"
 #include "VertexAttributeBinding.h"
 #include "Model.h"
+#include "Terrain.h"
 #include "Camera.h"
 #include "Light.h"
 #include "Scene.h"