Pārlūkot izejas kodu

Merge pull request #47 from OTHGMars/Assimp_Mats

Material Update.
Areloch 6 gadi atpakaļ
vecāks
revīzija
a1097fbd13

+ 2 - 0
Engine/lib/assimp/code/glTF2Asset.inl

@@ -405,8 +405,10 @@ inline void Buffer::Read(Value& obj, Asset& r)
 inline bool Buffer::LoadFromStream(IOStream& stream, size_t length, size_t baseOffset)
 {
     byteLength = length ? length : stream.FileSize();
+    //T3D_CHANGE_BEGIN
     if ((byteLength + baseOffset) > stream.FileSize())
        byteLength = stream.FileSize() - baseOffset;
+    //T3D_CHANGE_END
 
     if (baseOffset) {
         stream.Seek(baseOffset, aiOrigin_SET);

+ 107 - 35
Engine/lib/assimp/code/glTF2Importer.cpp

@@ -822,8 +822,6 @@ aiNode* ImportNode(aiScene* pScene, glTF2::Asset& r, std::vector<unsigned int>&
         if (node.skin) {
             for (int primitiveNo = 0; primitiveNo < count; ++primitiveNo) {
                 aiMesh* mesh = pScene->mMeshes[meshOffsets[mesh_idx]+primitiveNo];
-                mesh->mNumBones = static_cast<unsigned int>(node.skin->jointNames.size());
-                mesh->mBones = new aiBone*[mesh->mNumBones];
 
                 // GLTF and Assimp choose to store bone weights differently.
                 // GLTF has each vertex specify which bones influence the vertex.
@@ -834,39 +832,98 @@ aiNode* ImportNode(aiScene* pScene, glTF2::Asset& r, std::vector<unsigned int>&
                 // both because it's somewhat slow and because, for many applications,
                 // we then need to reconvert the data back into the vertex-to-bone
                 // mapping which makes things doubly-slow.
-                std::vector<std::vector<aiVertexWeight>> weighting(mesh->mNumBones);
+
+                //T3D_CHANGE_BEGIN
+                // The following commented block has been completely replaced.
+                // Portions of the replacement code block have been taken from:
+                // https://github.com/ConfettiFX/The-Forge/blob/master/Common_3/ThirdParty/OpenSource/assimp/4.1.0/code/glTF2Importer.cpp#L823-L860
+                //std::vector<std::vector<aiVertexWeight>> weighting(mesh->mNumBones);
+                //BuildVertexWeightMapping(node.meshes[0]->primitives[primitiveNo], weighting);
+
+                //for (uint32_t i = 0; i < mesh->mNumBones; ++i) {
+                //    aiBone* bone = new aiBone();
+
+                //    Ref<Node> joint = node.skin->jointNames[i];
+                //    if (!joint->name.empty()) {
+                //      bone->mName = joint->name;
+                //    } else {
+                //      // Assimp expects each bone to have a unique name.
+                //      static const std::string kDefaultName = "bone_";
+                //      char postfix[10] = {0};
+                //      ASSIMP_itoa10(postfix, i);
+                //      bone->mName = (kDefaultName + postfix);
+                //    }
+                //    GetNodeTransform(bone->mOffsetMatrix, *joint);
+
+                //    std::vector<aiVertexWeight>& weights = weighting[i];
+
+                //    bone->mNumWeights = static_cast<uint32_t>(weights.size());
+                //    if (bone->mNumWeights > 0) {
+                //      bone->mWeights = new aiVertexWeight[bone->mNumWeights];
+                //      memcpy(bone->mWeights, weights.data(), bone->mNumWeights * sizeof(aiVertexWeight));
+                //    } else {
+                //      // Assimp expects all bones to have at least 1 weight.
+                //      bone->mWeights = new aiVertexWeight[1];
+                //      bone->mNumWeights = 1;
+                //      bone->mWeights->mVertexId = 0;
+                //      bone->mWeights->mWeight = 0.f;
+                //    }
+                //    mesh->mBones[i] = bone;
+
+                std::vector<std::vector<aiVertexWeight>> weighting(node.skin->jointNames.size());
                 BuildVertexWeightMapping(node.meshes[0]->primitives[primitiveNo], weighting);
 
-                for (uint32_t i = 0; i < mesh->mNumBones; ++i) {
-                    aiBone* bone = new aiBone();
-
-                    Ref<Node> joint = node.skin->jointNames[i];
-                    if (!joint->name.empty()) {
-                      bone->mName = joint->name;
-                    } else {
-                      // Assimp expects each bone to have a unique name.
-                      static const std::string kDefaultName = "bone_";
-                      char postfix[10] = {0};
-                      ASSIMP_itoa10(postfix, i);
-                      bone->mName = (kDefaultName + postfix);
-                    }
-                    GetNodeTransform(bone->mOffsetMatrix, *joint);
-
-                    std::vector<aiVertexWeight>& weights = weighting[i];
-
-                    bone->mNumWeights = static_cast<uint32_t>(weights.size());
-                    if (bone->mNumWeights > 0) {
-                      bone->mWeights = new aiVertexWeight[bone->mNumWeights];
-                      memcpy(bone->mWeights, weights.data(), bone->mNumWeights * sizeof(aiVertexWeight));
-                    } else {
-                      // Assimp expects all bones to have at least 1 weight.
-                      bone->mWeights = new aiVertexWeight[1];
-                      bone->mNumWeights = 1;
-                      bone->mWeights->mVertexId = 0;
-                      bone->mWeights->mWeight = 0.f;
+                // CONFFX_BEGIN
+                // Assimp doesn't support bones with no weight. We have to count the
+                // number of bones that affect the mesh and limit it to just those bones.
+                int numBones = 0;
+                for (size_t i = 0; i < node.skin->jointNames.size(); ++i) {
+                    if (!weighting[i].empty())
+                        ++numBones;
+                }
+
+                mesh->mNumBones = numBones;
+                if (numBones > 0)
+                {
+                    mesh->mBones = new aiBone*[mesh->mNumBones];
+
+                    int j = 0;
+                    for (size_t i = 0; i < node.skin->jointNames.size(); ++i) {
+                        if (!weighting[i].empty())
+                        {
+                            aiBone* bone = new aiBone();
+
+                            Ref<Node> joint = node.skin->jointNames[i];
+                            bone->mName = joint->name.empty() ? joint->id : joint->name;
+
+                            // Get the inverseBindMatrix for the joint, grab the position out of row 4,
+                            // invert the matrix and put the position back as column 4.
+                            aiMatrix4x4 *tmpMat;
+                            uint8_t *matPtr = node.skin->inverseBindMatrices->GetPointer();
+                            tmpMat = (aiMatrix4x4*)matPtr;
+                            bone->mOffsetMatrix = tmpMat[i];
+                            aiVector3D tmpPos(bone->mOffsetMatrix.d1, bone->mOffsetMatrix.d2, bone->mOffsetMatrix.d3);
+                            bone->mOffsetMatrix.d1 = bone->mOffsetMatrix.d2 = bone->mOffsetMatrix.d3 = 0.0;
+                            bone->mOffsetMatrix.Inverse();
+                            bone->mOffsetMatrix.a4 = tmpPos.x;
+                            bone->mOffsetMatrix.b4 = tmpPos.y;
+                            bone->mOffsetMatrix.c4 = tmpPos.z;
+
+                            std::vector<aiVertexWeight>& weights = weighting[i];
+
+                            bone->mNumWeights = static_cast<uint32_t>(weights.size());
+                            if (bone->mNumWeights > 0) {
+                              bone->mWeights = new aiVertexWeight[bone->mNumWeights];
+                              memcpy(bone->mWeights, weights.data(), bone->mNumWeights * sizeof(aiVertexWeight));
+                            }
+                            mesh->mBones[j++] = bone;
+                        }
                     }
-                    mesh->mBones[i] = bone;
                 }
+                else
+                    mesh->mBones = nullptr;
+                // CONFFX_END
+                //T3D_CHANGE_END
             }
         }
 
@@ -921,7 +978,10 @@ struct AnimationSamplers {
 aiNodeAnim* CreateNodeAnim(glTF2::Asset& r, Node& node, AnimationSamplers& samplers)
 {
     aiNodeAnim* anim = new aiNodeAnim();
-    anim->mNodeName = node.name;
+    //T3D_CHANGE_BEGIN
+    //anim->mNodeName = node.name;
+    anim->mNodeName = node.name.empty() ? node.id : node.name;
+    //T3D_CHANGE_END
 
     static const float kMillisecondsFromSeconds = 1000.f;
 
@@ -1042,15 +1102,27 @@ void glTF2Importer::ImportAnimations(glTF2::Asset& r)
 
         std::unordered_map<unsigned int, AnimationSamplers> samplers = GatherSamplers(anim);
 
-        ai_anim->mNumChannels = static_cast<uint32_t>(samplers.size());
+        //T3D_CHANGE_BEGIN
+        //ai_anim->mNumChannels = static_cast<uint32_t>(samplers.size());
+        //if (ai_anim->mNumChannels > 0) {
+        //    ai_anim->mChannels = new aiNodeAnim*[ai_anim->mNumChannels];
+        //    int j = 0;
+        //    for (auto& iter : samplers) {
+        //        ai_anim->mChannels[j] = CreateNodeAnim(r, r.nodes[iter.first], iter.second);
+        //        ++j;
+        //    }
+        //}
+
+        ai_anim->mNumChannels = r.skins.Size() > 0 ? r.skins[0].jointNames.size() : 0;
         if (ai_anim->mNumChannels > 0) {
             ai_anim->mChannels = new aiNodeAnim*[ai_anim->mNumChannels];
             int j = 0;
-            for (auto& iter : samplers) {
-                ai_anim->mChannels[j] = CreateNodeAnim(r, r.nodes[iter.first], iter.second);
+            for (auto& iter : r.skins[0].jointNames) {
+                ai_anim->mChannels[j] = CreateNodeAnim(r, *iter, samplers[iter.GetIndex()]);
                 ++j;
             }
         }
+        //T3D_CHANGE_END
 
         // Use the latest keyframe for the duration of the animation
         double maxDuration = 0;

+ 2 - 0
Engine/lib/assimp/code/glTFAsset.inl

@@ -345,8 +345,10 @@ inline void Buffer::Read(Value& obj, Asset& r)
 inline bool Buffer::LoadFromStream(IOStream& stream, size_t length, size_t baseOffset)
 {
     byteLength = length ? length : stream.FileSize();
+    //T3D_CHANGE_BEGIN
     if ((byteLength + baseOffset) > stream.FileSize())
        byteLength = stream.FileSize() - baseOffset;
+    //T3D_CHANGE_END
 
     if (baseOffset) {
         stream.Seek(baseOffset, aiOrigin_SET);

+ 266 - 58
Engine/source/ts/assimp/assimpAppMaterial.cpp

@@ -19,6 +19,7 @@
 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 // IN THE SOFTWARE.
 //-----------------------------------------------------------------------------
+//#define TORQUE_PBR_MATERIALS
 
 #include "platform/platform.h"
 #include "ts/loader/appSequence.h"
@@ -38,7 +39,7 @@ String AppMaterial::cleanString(const String& str)
    String cleanStr(str);
 
    // Replace invalid characters with underscores
-   const String badChars(" -,.+=*/");
+   const String badChars(" -,.+=*/[]%$~;:");
    for (String::SizeType i = 0; i < badChars.length(); i++)
       cleanStr.replace(badChars[i], '_');
 
@@ -52,50 +53,27 @@ String AppMaterial::cleanString(const String& str)
 AssimpAppMaterial::AssimpAppMaterial(const char* matName)
 {
    name = matName;
-   diffuseColor = LinearColorF::ONE;
-   specularColor = LinearColorF::ONE;
-   specularPower = 0.8f;
-   doubleSided = false;
 
    // Set some defaults
    flags |= TSMaterialList::S_Wrap;
    flags |= TSMaterialList::T_Wrap;
 }
 
-AssimpAppMaterial::AssimpAppMaterial(const struct aiMaterial* mtl)
+AssimpAppMaterial::AssimpAppMaterial(aiMaterial* mtl) :
+   mAIMat(mtl)
 {
    aiString matName;
    mtl->Get(AI_MATKEY_NAME, matName);
    name = matName.C_Str();
-   if ( name.isEmpty() )
-      name = "defaultMaterial";
-   Con::printf("[ASSIMP] Loaded Material: %s", matName.C_Str());
-   
-   // Opacity
-   F32 opacity = 0.0f;
-   mtl->Get(AI_MATKEY_OPACITY, opacity);
-
-   // Diffuse color
-   aiColor3D diff_color (0.f, 0.f, 0.f);
-   mtl->Get(AI_MATKEY_COLOR_DIFFUSE, diff_color);
-   diffuseColor = LinearColorF(diff_color.r, diff_color.g, diff_color.b, opacity);
-
-   // Spec Color color
-   aiColor3D spec_color (0.f, 0.f, 0.f);
-   mtl->Get(AI_MATKEY_COLOR_DIFFUSE, spec_color );
-   specularColor = LinearColorF(spec_color.r, spec_color.g, spec_color.b, 1.0f);
-
-   // Specular Power
-   mtl->Get(AI_MATKEY_SHININESS_STRENGTH, specularPower);
-
-   // Double-Sided
-   S32 dbl_sided = 0;
-   mtl->Get(AI_MATKEY_TWOSIDED, dbl_sided);
-   doubleSided = (dbl_sided != 0);
-
-   // Set some defaults
-   flags |= TSMaterialList::S_Wrap;
-   flags |= TSMaterialList::T_Wrap;
+   if (name.isEmpty())
+   {
+      name = cleanString(TSShapeLoader::getShapePath().getFileName());;
+      name += "_defMat";
+   }
+   Con::printf("[ASSIMP] Loading Material: %s", name.c_str());
+#ifdef TORQUE_DEBUG
+   enumerateMaterialProperties(mtl);
+#endif
 }
 
 Material* AssimpAppMaterial::createMaterial(const Torque::Path& path) const
@@ -105,34 +83,264 @@ Material* AssimpAppMaterial::createMaterial(const Torque::Path& path) const
    String cleanFile = cleanString(TSShapeLoader::getShapePath().getFileName());
    String cleanName = cleanString(getName());
 
-   // Prefix the material name with the filename (if not done already by TSShapeConstructor prefix)
-   //if (!cleanName.startsWith(cleanFile))
-   //   cleanName = cleanFile + "_" + cleanName;
-
-   // Determine the blend operation for this material
-   Material::BlendOp blendOp = (flags & TSMaterialList::Translucent) ? Material::LerpAlpha : Material::None;
-   if (flags & TSMaterialList::Additive)
-      blendOp = Material::Add;
-   else if (flags & TSMaterialList::Subtractive)
-      blendOp = Material::Sub;
-
    // Create the Material definition
    const String oldScriptFile = Con::getVariable("$Con::File");
    Con::setVariable("$Con::File", path.getFullPath());   // modify current script path so texture lookups are correct
-   Material *newMat = MATMGR->allocateAndRegister( cleanName, getName() );
+   Material *newMat = MATMGR->allocateAndRegister(cleanName, getName());
    Con::setVariable("$Con::File", oldScriptFile);        // restore script path
 
-   newMat->mDiffuseMapFilename[0] = "";
-   newMat->mNormalMapFilename[0] = "";
-   newMat->mSpecularMapFilename[0] = "";
+   initMaterial(path, newMat);
 
-   newMat->mDiffuse[0] = diffuseColor;
-   //newMat->mSpecular[0] = specularColor;
-   //newMat->mSpecularPower[0] = specularPower;
+   return newMat;
+}
 
-   newMat->mDoubleSided = doubleSided;
-   newMat->mTranslucent = (bool)(flags & TSMaterialList::Translucent);
-   newMat->mTranslucentBlendOp = blendOp;
+void AssimpAppMaterial::initMaterial(const Torque::Path& path, Material* mat) const
+{
+   String cleanFile = cleanString(TSShapeLoader::getShapePath().getFileName());
+   String cleanName = cleanString(getName());
 
-   return newMat;
-}
+   // Determine the blend mode and transparency for this material
+   Material::BlendOp blendOp = Material::None;
+   bool translucent = false;
+   float opacity = 1.0f;
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_OPACITY, opacity))
+   {
+      if (opacity != 1.0f)
+      {
+         translucent = true;
+         int blendInt;
+         blendOp = Material::LerpAlpha;
+         if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_BLEND_FUNC, blendInt))
+         {
+            if (blendInt == aiBlendMode_Additive)
+               blendOp = Material::Add;
+         }
+      }
+   }
+   else
+   {  // No opacity key, see if it's defined as a gltf property
+      aiString opacityMode;
+      if (AI_SUCCESS == mAIMat->Get("$mat.gltf.alphaMode", 0, 0, opacityMode))
+      {
+         if (dStrcmp("MASK", opacityMode.C_Str()) == 0)
+         {
+            translucent = true;
+            blendOp = Material::LerpAlpha;
+
+            float cutoff;
+            if (AI_SUCCESS == mAIMat->Get("$mat.gltf.alphaCutoff", 0, 0, cutoff))
+            {
+               mat->mAlphaRef = (U32)(cutoff * 255);  // alpha ref 0-255
+               mat->mAlphaTest = true;
+            }
+         }
+         else if (dStrcmp("OPAQUE", opacityMode.C_Str()) != 0)
+         {
+            translucent = true;
+            blendOp = Material::LerpAlpha;
+         }
+      }
+   }
+   mat->mTranslucent = translucent;
+   mat->mTranslucentBlendOp = blendOp;
+
+   // Assign color values.
+   LinearColorF diffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
+   aiColor3D read_color(1.f, 1.f, 1.f);
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_COLOR_DIFFUSE, read_color))
+      diffuseColor.set(read_color.r, read_color.g, read_color.b, opacity);
+   mat->mDiffuse[0] = diffuseColor;
+
+   aiString texName;
+   String torquePath;
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0), texName))
+   {
+      torquePath = texName.C_Str();
+      if (!torquePath.isEmpty())
+         mat->mDiffuseMapFilename[0] = cleanTextureName(torquePath, cleanFile);
+   }
+
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_TEXTURE(aiTextureType_NORMALS, 0), texName))
+   {
+      torquePath = texName.C_Str();
+      if (!torquePath.isEmpty())
+         mat->mNormalMapFilename[0] = cleanTextureName(torquePath, cleanFile);
+   }
+
+#ifdef TORQUE_PBR_MATERIALS
+   float floatVal;
+   if (AI_SUCCESS == mAIMat->Get("$mat.gltf.pbrMetallicRoughness.roughnessFactor", 0, 0, floatVal))
+   {  // The shape has pbr material definitions
+      String aoName, rmName; // occlusion and roughness/metalness maps
+      if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_TEXTURE(aiTextureType_LIGHTMAP, 0), texName))
+         aoName = texName.C_Str();
+      if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_TEXTURE(aiTextureType_UNKNOWN, 0), texName))
+         rmName = texName.C_Str();
+
+      //if (aoName.isNotEmpty() && (aoName == rmName))
+      //   mat->mOrmMapFilename[0] = cleanTextureName(aoName, cleanFile); // It's an ORM map
+      //else if (aoName.isNotEmpty() || rmName.isNotEmpty())
+      if (aoName.isNotEmpty() || rmName.isNotEmpty())
+      {  // If we have either map, fill all three slots
+         if (rmName.isNotEmpty())
+         {
+            mat->mRoughMapFilename[0] = cleanTextureName(rmName, cleanFile); // Roughness
+            mat->mSmoothnessChan[0] = 1.0f;
+            mat->mInvertSmoothness = (floatVal == 1.0f);
+            mat->mMetalMapFilename[0] = cleanTextureName(rmName, cleanFile); // Metallic
+            mat->mMetalChan[0] = 2.0f;
+         }
+         if (aoName.isNotEmpty())
+         {
+            mat->mAOMapFilename[0] = cleanTextureName(aoName, cleanFile); // occlusion
+            mat->mAOChan[0] = 0.0f;
+         }
+         else
+         {
+            mat->mAOMapFilename[0] = cleanTextureName(rmName, cleanFile); // occlusion
+            mat->mAOChan[0] = 0.0f;
+         }
+      }
+   }
+#else
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_TEXTURE(aiTextureType_SPECULAR, 0), texName))
+   {
+      torquePath = texName.C_Str();
+      if (!torquePath.isEmpty())
+         mat->mSpecularMapFilename[0] = cleanTextureName(torquePath, cleanFile);
+   }
+
+   LinearColorF specularColor(1.0f, 1.0f, 1.0f, 1.0f);
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_COLOR_SPECULAR, read_color))
+      specularColor.set(read_color.r, read_color.g, read_color.b, opacity);
+   mat->mSpecular[0] = specularColor;
+
+   // Specular Power
+   F32 specularPower = 1.0f;
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_SHININESS_STRENGTH, specularPower))
+      mat->mSpecularPower[0] = specularPower;
+
+   // Specular
+   F32 specularStrength = 0.0f;
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_SHININESS, specularStrength))
+      mat->mSpecularStrength[0] = specularStrength;
+#endif
+
+   // Double-Sided
+   bool doubleSided = false;
+   S32 dbl_sided = 0;
+   if (AI_SUCCESS == mAIMat->Get(AI_MATKEY_TWOSIDED, dbl_sided))
+      doubleSided = (dbl_sided != 0);
+   mat->mDoubleSided = doubleSided;
+}
+
+String AssimpAppMaterial::cleanTextureName(String& texName, String& shapeName)
+{
+   String cleanStr;
+
+   if (texName[0] == '*')
+   {
+      cleanStr = shapeName;
+      cleanStr += "_cachedTex";
+      cleanStr += texName.substr(1);
+   }
+   else
+   {
+      cleanStr = texName;
+      cleanStr.replace('\\', '/');
+   }
+
+   return cleanStr;
+}
+
+#ifdef TORQUE_DEBUG
+void AssimpAppMaterial::enumerateMaterialProperties(aiMaterial* mtl)
+{
+   for (U32 i = 0; i < mtl->mNumProperties; ++i)
+   {
+      aiMaterialProperty* matProp = mtl->mProperties[i];
+      String outText;
+      if (matProp)
+      {
+         outText = String::ToString(" Key: %s, Index: %d, Semantic: ", matProp->mKey.C_Str(), matProp->mIndex);
+         switch (matProp->mSemantic)
+         {
+         case aiTextureType_NONE:
+            outText += "aiTextureType_NONE";
+            break;
+         case aiTextureType_DIFFUSE:
+            outText += "aiTextureType_DIFFUSE";
+            break;
+         case aiTextureType_SPECULAR:
+            outText += "aiTextureType_SPECULAR";
+            break;
+         case aiTextureType_AMBIENT:
+            outText += "aiTextureType_AMBIENT";
+            break;
+         case aiTextureType_EMISSIVE:
+            outText += "aiTextureType_EMISSIVE";
+            break;
+         case aiTextureType_HEIGHT:
+            outText += "aiTextureType_HEIGHT";
+            break;
+         case aiTextureType_NORMALS:
+            outText += "aiTextureType_NORMALS";
+            break;
+         case aiTextureType_SHININESS:
+            outText += "aiTextureType_SHININESS";
+            break;
+         case aiTextureType_OPACITY:
+            outText += "aiTextureType_OPACITY";
+            break;
+         case aiTextureType_DISPLACEMENT:
+            outText += "aiTextureType_DISPLACEMENT";
+            break;
+         case aiTextureType_LIGHTMAP:
+            outText += "aiTextureType_LIGHTMAP";
+            break;
+         case aiTextureType_REFLECTION:
+            outText += "aiTextureType_REFLECTION";
+            break;
+         default:
+            outText += "aiTextureType_UNKNOWN";
+            break;
+         }
+
+         aiString stringProp;
+         F32* floatProp;
+         double* doubleProp;
+         S32* intProp;
+
+         switch (matProp->mType)
+         {
+         case aiPTI_Float:
+            floatProp = (F32*)matProp->mData;
+            for (U32 j = 0; j < matProp->mDataLength / sizeof(F32); ++j)
+               outText += String::ToString(", %0.4f", floatProp[j]);
+            break;
+         case aiPTI_Double:
+            doubleProp = (double*)matProp->mData;
+            for (U32 j = 0; j < matProp->mDataLength / sizeof(double); ++j)
+               outText += String::ToString(", %0.4lf", doubleProp[j]);
+            break;
+         case aiPTI_String:
+            aiGetMaterialString(mtl, matProp->mKey.C_Str(), matProp->mSemantic, matProp->mIndex, &stringProp);
+            outText += String::ToString(", %s", stringProp.C_Str());
+            break;
+         case aiPTI_Integer:
+            intProp = (S32*)matProp->mData;
+            for (U32 j = 0; j < matProp->mDataLength / sizeof(S32); ++j)
+               outText += String::ToString(", %d", intProp[j]);
+            break;
+         case aiPTI_Buffer:
+            outText += ", aiPTI_Buffer format data";
+            break;
+         default:
+            outText += ", Unknown data type";
+         }
+
+         Con::printf("%s", outText.c_str());
+      }
+   }
+}
+#endif

+ 10 - 5
Engine/source/ts/assimp/assimpAppMaterial.h

@@ -26,6 +26,7 @@
 #ifndef _APPMATERIAL_H_
 #include "ts/loader/appMaterial.h"
 #endif
+#include <assimp/scene.h>
 
 class Material;
 
@@ -34,18 +35,22 @@ class AssimpAppMaterial : public AppMaterial
    typedef AppMaterial Parent;
 
    String   name; 
-   LinearColorF   diffuseColor;
-   LinearColorF   specularColor;
-   F32      specularPower;
-   bool     doubleSided;
+   aiMaterial* mAIMat;
+
+#ifdef TORQUE_DEBUG
+   void enumerateMaterialProperties(aiMaterial* mtl);
+#endif
+   static String cleanTextureName(String& texName, String& shapeName);
+
 public:
 
    AssimpAppMaterial(const char* matName);
-   AssimpAppMaterial(const struct aiMaterial* mtl);
+   AssimpAppMaterial(aiMaterial* mtl);
    ~AssimpAppMaterial() { }
 
    String getName() const { return name; }
    Material* createMaterial(const Torque::Path& path) const;
+   void initMaterial(const Torque::Path& path, Material* mat) const;
 };
 
 #endif // _ASSIMP_APPMATERIAL_H_

+ 24 - 3
Engine/source/ts/assimp/assimpAppMesh.cpp

@@ -202,6 +202,15 @@ void AssimpAppMesh::lockMesh(F32 t, const MatrixF& objOffset)
 
       MatrixF boneTransform;
       AssimpAppNode::assimpToTorqueMat(mMeshData->mBones[b]->mOffsetMatrix, boneTransform);
+      Point3F boneScale = boneTransform.getScale();
+      if (boneScale != Point3F::One)
+      {
+         Point3F scaleMult = Point3F::One / boneScale;
+         Point3F scalePos = boneTransform.getPosition();
+         boneTransform.scale(scaleMult);
+         scalePos /= scaleMult;
+         boneTransform.setPosition(scalePos);
+      }
       initialTransforms.push_back(boneTransform);
 
       //Weights
@@ -225,20 +234,32 @@ void AssimpAppMesh::lockMesh(F32 t, const MatrixF& objOffset)
    vertexIndex.setSize(nonZeroWeights);
    boneIndex.setSize(nonZeroWeights);
 
-   // Copy the weights to our vectors in vertex order
+   // Copy the weights to our vectors in vertex order and
+   // normalize vertex weights (force weights for each vert to sum to 1)
    U32 nextWeight = 0;
-   for (U32 i = 0; i < mMeshData->mNumVertices; i++)
+   for (U32 i = 0; i < mMeshData->mNumVertices; ++i)
    {
-      for (U32 ind = 0; ind < nonZeroWeights; ind++)
+      U32 vertStart = nextWeight;
+      F32 invTotalWeight = 0;
+      for (U32 ind = 0; ind < nonZeroWeights; ++ind)
       {
          if (tmpVertexIndex[ind] == i)
          {
             weight[nextWeight] = tmpWeight[ind];
+            invTotalWeight += tmpWeight[ind];
             vertexIndex[nextWeight] = tmpVertexIndex[ind];
             boneIndex[nextWeight] = tmpBoneIndex[ind];
             nextWeight++;
          }
       }
+
+      // Now normalize the vertex weights
+      if (invTotalWeight > 0.0)
+      {
+         invTotalWeight = 1.0f / invTotalWeight;
+         for (U32 ind = vertStart; ind < nextWeight; ++ind)
+            weight[ind] *= invTotalWeight;
+      }
    }
 
    if ( noUVFound )

+ 10 - 9
Engine/source/ts/assimp/assimpAppNode.cpp

@@ -32,6 +32,7 @@
 #include <assimp/types.h>
 
 aiAnimation* AssimpAppNode::sActiveSequence = NULL;
+F32 AssimpAppNode::sTimeMultiplier = 1.0f;
 
 AssimpAppNode::AssimpAppNode(const struct aiScene* scene, const struct aiNode* node, AssimpAppNode* parent)
 :  mInvertMeshes(false),
@@ -132,15 +133,15 @@ void AssimpAppNode::getAnimatedTransform(MatrixF& mat, F32 t, aiAnimation* animS
             F32 lastT = 0.0;
             for (U32 key = 0; key < nodeAnim->mNumPositionKeys; ++key)
             {
-               F32 curT = (F32)nodeAnim->mPositionKeys[key].mTime;
+               F32 curT = sTimeMultiplier * (F32)nodeAnim->mPositionKeys[key].mTime;
                curPos.set(nodeAnim->mPositionKeys[key].mValue.x, nodeAnim->mPositionKeys[key].mValue.y, nodeAnim->mPositionKeys[key].mValue.z);
-               if (curT > t)
+               if ((curT > t) && (key > 0))
                {
                   F32 factor = (t - lastT) / (curT - lastT);
                   trans.interpolate(lastPos, curPos, factor);
                   break;
                }
-               else if ((curT == t) || (key == nodeAnim->mNumPositionKeys - 1))
+               else if ((curT >= t) || (key == nodeAnim->mNumPositionKeys - 1))
                {
                   trans = curPos;
                   break;
@@ -161,16 +162,16 @@ void AssimpAppNode::getAnimatedTransform(MatrixF& mat, F32 t, aiAnimation* animS
             F32 lastT = 0.0;
             for (U32 key = 0; key < nodeAnim->mNumRotationKeys; ++key)
             {
-               F32 curT = (F32)nodeAnim->mRotationKeys[key].mTime;
+               F32 curT = sTimeMultiplier * (F32)nodeAnim->mRotationKeys[key].mTime;
                curRot.set(nodeAnim->mRotationKeys[key].mValue.x, nodeAnim->mRotationKeys[key].mValue.y,
                   nodeAnim->mRotationKeys[key].mValue.z, nodeAnim->mRotationKeys[key].mValue.w);
-               if (curT > t)
+               if ((curT > t) && (key > 0))
                {
                   F32 factor = (t - lastT) / (curT - lastT);
                   rot.interpolate(lastRot, curRot, factor);
                   break;
                }
-               else if ((curT == t) || (key == nodeAnim->mNumRotationKeys - 1))
+               else if ((curT >= t) || (key == nodeAnim->mNumRotationKeys - 1))
                {
                   rot = curRot;
                   break;
@@ -190,15 +191,15 @@ void AssimpAppNode::getAnimatedTransform(MatrixF& mat, F32 t, aiAnimation* animS
             F32 lastT = 0.0;
             for (U32 key = 0; key < nodeAnim->mNumScalingKeys; ++key)
             {
-               F32 curT = (F32)nodeAnim->mScalingKeys[key].mTime;
+               F32 curT = sTimeMultiplier * (F32)nodeAnim->mScalingKeys[key].mTime;
                curScale.set(nodeAnim->mScalingKeys[key].mValue.x, nodeAnim->mScalingKeys[key].mValue.y, nodeAnim->mScalingKeys[key].mValue.z);
-               if (curT > t)
+               if ((curT > t) && (key > 0))
                {
                   F32 factor = (t - lastT) / (curT - lastT);
                   scale.interpolate(lastScale, curScale, factor);
                   break;
                }
-               else if ((curT == t) || (key == nodeAnim->mNumScalingKeys - 1))
+               else if ((curT >= t) || (key == nodeAnim->mNumScalingKeys - 1))
                {
                   scale = curScale;
                   break;

+ 1 - 0
Engine/source/ts/assimp/assimpAppNode.h

@@ -70,6 +70,7 @@ public:
    }
 
    static aiAnimation* sActiveSequence;
+   static F32 sTimeMultiplier;
 
    //-----------------------------------------------------------------------
    const char *getName() { return mName; }

+ 39 - 9
Engine/source/ts/assimp/assimpAppSequence.cpp

@@ -16,19 +16,24 @@ AssimpAppSequence::AssimpAppSequence(aiAnimation *a) :
    seqStart(0.0f),
    mAnim(a)
 {
-   // From: http://sir-kimmi.de/assimp/lib_html/data.html#anims
-   // An aiAnimation has a duration. The duration as well as all time stamps are given in ticks.
-   // To get the correct timing, all time stamp thus have to be divided by aiAnimation::mTicksPerSecond.
-   // Beware, though, that certain combinations of file format and exporter don't always store this
-   // information in the exported file. In this case, mTicksPerSecond is set to 0 to indicate the lack of knowledge.
+   mSequenceName = mAnim->mName.C_Str();
+   if (mSequenceName.isEmpty())
+      mSequenceName = "ambient";
+   Con::printf("\n[Assimp] Adding %s animation", mSequenceName.c_str());
+
    fps = (mAnim->mTicksPerSecond > 0) ? mAnim->mTicksPerSecond : 30.0f;
 
+   U32 maxKeys = 0;
    F32 maxEndTime = 0;
-   F32 minFrameTime = 1000.0f;
+   F32 minFrameTime = 100000.0f;
    // Detect the frame rate (minimum time between keyframes) and max sequence time
    for (U32 i = 0; i < mAnim->mNumChannels; ++i)
    {
       aiNodeAnim *nodeAnim = mAnim->mChannels[i];
+      maxKeys = getMax(maxKeys, nodeAnim->mNumPositionKeys);
+      maxKeys = getMax(maxKeys, nodeAnim->mNumRotationKeys);
+      maxKeys = getMax(maxKeys, nodeAnim->mNumScalingKeys);
+
       if (nodeAnim->mNumPositionKeys)
          maxEndTime = getMax(maxEndTime, (F32) nodeAnim->mPositionKeys[nodeAnim->mNumPositionKeys-1].mTime);
       if (nodeAnim->mNumRotationKeys)
@@ -53,9 +58,31 @@ AssimpAppSequence::AssimpAppSequence(aiAnimation *a) :
       }
    }
 
-   fps = (minFrameTime > 0.0f) ? 1.0f / minFrameTime : fps;
-   fps = mClamp(fps, TSShapeLoader::MinFrameRate, TSShapeLoader::MaxFrameRate);
-   seqEnd = maxEndTime;
+   S32 timeFactor = Con::getIntVariable("$Assimp::AnimTiming", 1);
+   S32 fpsRequest = Con::getIntVariable("$Assimp::AnimFPS", 30);
+   if (timeFactor == 0)
+   {  // Timing specified in frames
+      fps = mClamp(fpsRequest, 5 /*TSShapeLoader::MinFrameRate*/, TSShapeLoader::MaxFrameRate);
+      maxKeys = getMax(maxKeys, (U32)maxEndTime);  // Keys won't be assigned for every frame.
+      seqEnd = maxKeys / fps;
+      mTimeMultiplier = 1.0f / fps;
+   }
+   else
+   {  // Timing specified in seconds or ms depending on format
+      if (maxEndTime > 1000.0f || mAnim->mDuration > 1000.0f)
+      {
+         timeFactor = 1000.0f;   // If it's more than 1000 seconds, assume it's ms.
+         Con::setIntVariable("$Assimp::AnimTiming", 1000);
+      }
+
+      timeFactor = mClamp(timeFactor, 1, 1000);
+      minFrameTime /= (F32)timeFactor;
+      maxEndTime /= (F32)timeFactor;
+      fps = (minFrameTime > 0.0f) ? 1.0f / minFrameTime : fps;
+      fps = mClamp(fpsRequest, 5 /*TSShapeLoader::MinFrameRate*/, TSShapeLoader::MaxFrameRate);
+      seqEnd = maxEndTime;
+      mTimeMultiplier = 1.0f / timeFactor;
+   }
 }
 
 AssimpAppSequence::~AssimpAppSequence()
@@ -65,7 +92,10 @@ AssimpAppSequence::~AssimpAppSequence()
 void AssimpAppSequence::setActive(bool active)
 {
    if (active)
+   {
       AssimpAppNode::sActiveSequence = mAnim;
+      AssimpAppNode::sTimeMultiplier = mTimeMultiplier;
+   }
    else
    {
       if (AssimpAppNode::sActiveSequence == mAnim)

+ 3 - 1
Engine/source/ts/assimp/assimpAppSequence.h

@@ -22,8 +22,10 @@
 
 class AssimpAppSequence : public AppSequence
 {
+   String   mSequenceName;
    F32      seqStart;
    F32      seqEnd;
+   F32      mTimeMultiplier; // The factor needed to convert the sequence data timestamp to seconds
 
 public:
 
@@ -37,7 +39,7 @@ public:
    virtual S32 getNumTriggers() const { return 0; }
    virtual void getTrigger(S32 index, TSShape::Trigger& trigger) const { trigger.state = 0; }
 
-   virtual const char* getName() const { return mAnim->mName.C_Str(); }
+   virtual const char* getName() const { return mSequenceName.c_str(); }
 
    F32 getStart() const { return seqStart; }
    F32 getEnd() const { return seqEnd; }

+ 62 - 7
Engine/source/ts/assimp/assimpShapeLoader.cpp

@@ -200,6 +200,10 @@ void AssimpShapeLoader::enumerateScene()
       Con::printf("[ASSIMP] Mesh Count: %d", mScene->mNumMeshes);
       Con::printf("[ASSIMP] Material Count: %d", mScene->mNumMaterials);
 
+      // Extract embedded textures
+      for (U32 i = 0; i < mScene->mNumTextures; ++i)
+         extractTexture(i, mScene->mTextures[i]);
+
       // Load all the materials.
       for ( U32 i = 0; i < mScene->mNumMaterials; i++ )
          AppMesh::appMaterials.push_back(new AssimpAppMaterial(mScene->mMaterials[i]));
@@ -302,8 +306,11 @@ void AssimpShapeLoader::updateMaterialsScript(const Torque::Path &path)
          if ( Sim::findObject( MATMGR->getMapEntry( mat->getName() ), mappedMat ) )
          {
             // Only update existing materials if forced to
-            if ( ColladaUtils::getOptions().forceUpdateMaterials )
-               persistMgr.setDirty( mappedMat );
+            if (Con::getBoolVariable("$Assimp::ForceUpdateMats", false))
+            {
+               mat->initMaterial(scriptPath, mappedMat);
+               persistMgr.setDirty(mappedMat);
+            }
          }
          else
          {
@@ -322,8 +329,6 @@ void AssimpShapeLoader::updateMaterialsScript(const Torque::Path &path)
 /// Check if an up-to-date cached DTS is available for this DAE file
 bool AssimpShapeLoader::canLoadCachedDTS(const Torque::Path& path)
 {
-   return false;
-
    // Generate the cached filename
    Torque::Path cachedPath(path);
    cachedPath.setExtension("cached.dts");
@@ -332,13 +337,13 @@ bool AssimpShapeLoader::canLoadCachedDTS(const Torque::Path& path)
    FileTime cachedModifyTime;
    if (Platform::getFileTimes(cachedPath.getFullPath(), NULL, &cachedModifyTime))
    {
-      bool forceLoadDAE = Con::getBoolVariable("$assimp::forceLoad", false);
+      bool forceLoad = Con::getBoolVariable("$assimp::forceLoad", false);
 
       FileTime daeModifyTime;
       if (!Platform::getFileTimes(path.getFullPath(), NULL, &daeModifyTime) ||
-         (!forceLoadDAE && (Platform::compareFileTimes(cachedModifyTime, daeModifyTime) >= 0) ))
+         (!forceLoad && (Platform::compareFileTimes(cachedModifyTime, daeModifyTime) >= 0) ))
       {
-         // DAE not found, or cached DTS is newer
+         // Original file not found, or cached DTS is newer
          return true;
       }
    }
@@ -393,6 +398,56 @@ void AssimpShapeLoader::detectDetails()
    AssimpAppMesh::fixDetailSize(singleDetail, Con::getIntVariable("$Assimp::singleDetailSize", 2));
 }
 
+void AssimpShapeLoader::extractTexture(U32 index, aiTexture* pTex)
+{  // Cache an embedded texture to disk
+   updateProgress(Load_EnumerateScene, "Extracting Textures...", mScene->mNumTextures, index);
+   Con::printf("[Assimp] Extracting Texture %s, W: %d, H: %d, %d of %d, format hint: (%s)", pTex->mFilename.C_Str(),
+      pTex->mWidth, pTex->mHeight, index, mScene->mNumTextures, pTex->achFormatHint);
+
+   // Create the texture filename
+   String cleanFile = AppMaterial::cleanString(TSShapeLoader::getShapePath().getFileName());
+   String texName = String::ToString("%s_cachedTex%d", cleanFile.c_str(), index);
+   Torque::Path texPath = shapePath;
+   texPath.setFileName(texName);
+
+   if (pTex->mHeight == 0)
+   {  // Compressed format, write the data directly to disc
+      texPath.setExtension(pTex->achFormatHint);
+      FileStream *outputStream;
+      if ((outputStream = FileStream::createAndOpen(texPath.getFullPath(), Torque::FS::File::Write)) != NULL)
+      {
+         outputStream->setPosition(0);
+         outputStream->write(pTex->mWidth, pTex->pcData);
+         outputStream->close();
+         delete outputStream;
+      }
+   }
+   else
+   {  // Embedded pixel data, fill a bitmap and save it.
+      GFXTexHandle shapeTex;
+      shapeTex.set(pTex->mWidth, pTex->mHeight, GFXFormatR8G8B8A8_SRGB, &GFXDynamicTextureSRGBProfile,
+         String::ToString("AssimpShapeLoader (%s:%i)", __FILE__, __LINE__), 1, 0);
+      GFXLockedRect *rect = shapeTex.lock();
+
+      for (U32 y = 0; y < pTex->mHeight; ++y)
+      {
+         for (U32 x = 0; x < pTex->mWidth; ++x)
+         {
+            U32 targetIndex = (y * rect->pitch) + (x * 4);
+            U32 sourceIndex = ((y * pTex->mWidth) + x) * 4;
+            rect->bits[targetIndex] = pTex->pcData[sourceIndex].r;
+            rect->bits[targetIndex + 1] = pTex->pcData[sourceIndex].g;
+            rect->bits[targetIndex + 2] = pTex->pcData[sourceIndex].b;
+            rect->bits[targetIndex + 3] = pTex->pcData[sourceIndex].a;
+         }
+      }
+      shapeTex.unlock();
+
+      texPath.setExtension("png");
+      shapeTex->dumpToDisk("PNG", texPath.getFullPath());
+   }
+}
+
 //-----------------------------------------------------------------------------
 /// This function is invoked by the resource manager based on file extension.
 TSShape* assimpLoadShape(const Torque::Path &path)

+ 2 - 0
Engine/source/ts/assimp/assimpShapeLoader.h

@@ -26,6 +26,7 @@
 #ifndef _TSSHAPELOADER_H_
 #include "ts/loader/tsShapeLoader.h"
 #endif
+#include <assimp/texture.h>
 
 //-----------------------------------------------------------------------------
 class AssimpShapeLoader : public TSShapeLoader
@@ -37,6 +38,7 @@ protected:
 
    virtual bool ignoreNode(const String& name);
    void detectDetails();
+   void extractTexture(U32 index, aiTexture* pTex);
 
 public:
    AssimpShapeLoader();

+ 137 - 1
Templates/BaseGame/game/tools/gui/assimpImport.ed.gui

@@ -393,6 +393,107 @@
          canSaveDynamicFields = "0";
       };
 
+      new GuiTextCtrl() {
+         text = "Animation Timing:";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiTextRightProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "10 311";
+         Extent = "85 16";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         hovertime = "1000";
+         canSaveDynamicFields = "0";
+      };
+      new GuiPopUpMenuCtrl() {
+         maxPopupHeight = "200";
+         sbUsesNAColor = "0";
+         reverseTextList = "0";
+         bitmapBounds = "16 16";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiPopUpMenuProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "100 310";
+         Extent = "86 18";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = "Select the timing units used in the animation data.";
+         hovertime = "1000";
+         internalName = "animTiming";
+         canSaveDynamicFields = "0";
+      };
+      new GuiTextCtrl() {
+         text = "FPS:";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiTextRightProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "200 311";
+         Extent = "20 16";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         hovertime = "1000";
+         canSaveDynamicFields = "0";
+      };
+      new GuiTextEditCtrl() {
+         historySize = "0";
+         password = "0";
+         tabComplete = "0";
+         sinkAllKeyEvents = "0";
+         passwordMask = "*";
+         text = "2";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiTextEditProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "225 310";
+         Extent = "26 18";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = "Frames per second for all animations when Animation Timing type is Frames (5 - 60)";
+         hovertime = "1000";
+         internalName = "animFPS";
+         canSaveDynamicFields = "0";
+      };
+
       new GuiTextCtrl() {
          text = "LOD";
          maxLength = "1024";
@@ -582,6 +683,28 @@
          canSaveDynamicFields = "0";
       };
 
+      new GuiCheckBoxCtrl() {
+         useInactiveState = "0";
+         text = " Force update materials.cs";
+         groupNum = "-1";
+         buttonType = "ToggleButton";
+         useMouseEvents = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiCheckBoxProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "210 150";
+         Extent = "200 13";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         variable = "$Assimp::ForceUpdateMats";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = "Forces update of materials.cs (even if Materials already exist)";
+         hovertime = "1000";
+         canSaveDynamicFields = "0";
+      };
+
       new GuiButtonCtrl() {
          text = "OK";
          groupNum = "-1";
@@ -643,6 +766,10 @@ function AssimpImportDlg::showDialog(%this, %shapePath, %cmd)
       $Assimp::FindDegenerates = true;
       $Assimp::FindInvalidData = true;
       $Assimp::JoinIdenticalVertices = true;
+      $Assimp::FlipNormals = false;
+      
+      $Assimp::AnimTiming = 1; // Seconds
+      $Assimp::AnimFPS = 30;   // Framerate when timing is frames.
    }
 
    %this-->upAxis.clear();
@@ -656,9 +783,15 @@ function AssimpImportDlg::showDialog(%this, %shapePath, %cmd)
    %this-->lodType.add("SingleSize", 1);
    %this-->lodType.add("TrailingNumber", 2);
    %this-->lodType.setSelected($Assimp::lodType);
-
    %this-->singleDetailSize.text = $Assimp::singleDetailSize;
 
+   %this-->animTiming.clear();
+   %this-->animTiming.add("Frames", 0);
+   %this-->animTiming.add("Seconds", 1);
+   %this-->animTiming.add("Milliseconds", 1000);
+   %this-->animTiming.setSelected($Assimp::AnimTiming);
+   %this-->animFPS.text = $Assimp::AnimFPS;
+
    //Triangulate is a default(currently mandatory) behavior
    $Assimp::Triangulate = true;
 
@@ -681,6 +814,9 @@ function AssimpImportDlg::onOK(%this)
    $Assimp::lodType = %this-->lodType.getSelected();
    $Assimp::singleDetailSize = %this-->singleDetailSize.getText();
 
+   $Assimp::AnimTiming = %this-->animTiming.getSelected();
+   $Assimp::AnimFPS = %this-->animFPS.getText();
+
    // Load the shape (always from the DAE)
    $assimp::forceLoad = true;
    eval(%this.cmd);

+ 2 - 2
Templates/BaseGame/game/tools/shapeEditor/scripts/shapeEditor.ed.cs

@@ -261,13 +261,13 @@ function ShapeEdSelectWindow::onSelect( %this, %path )
    // Prompt user to save the old shape if it is dirty
    if ( ShapeEditor.isDirty() )
    {
-      %cmd = "ColladaImportDlg.showDialog( \"" @ %path @ "\", \"ShapeEditor.selectShape( \\\"" @ %path @ "\\\", ";
+      %cmd = "showImportDialog( \"" @ %path @ "\", \"ShapeEditor.selectShape( \\\"" @ %path @ "\\\", ";
       MessageBoxYesNoCancel( "Shape Modified", "Would you like to save your changes?", %cmd @ "true );\" );", %cmd @ "false );\" );" );
    }
    else
    {
       %cmd = "ShapeEditor.selectShape( \"" @ %path @ "\", false );";
-      ColladaImportDlg.showDialog( %path, %cmd );
+      showImportDialog( %path, %cmd );
    }
 }
 

+ 137 - 1
Templates/Full/game/tools/gui/assimpImport.ed.gui

@@ -393,6 +393,107 @@
          canSaveDynamicFields = "0";
       };
 
+      new GuiTextCtrl() {
+         text = "Animation Timing:";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiTextRightProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "10 311";
+         Extent = "85 16";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         hovertime = "1000";
+         canSaveDynamicFields = "0";
+      };
+      new GuiPopUpMenuCtrl() {
+         maxPopupHeight = "200";
+         sbUsesNAColor = "0";
+         reverseTextList = "0";
+         bitmapBounds = "16 16";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiPopUpMenuProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "100 310";
+         Extent = "86 18";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = "Select the timing units used in the animation data.";
+         hovertime = "1000";
+         internalName = "animTiming";
+         canSaveDynamicFields = "0";
+      };
+      new GuiTextCtrl() {
+         text = "FPS:";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiTextRightProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "200 311";
+         Extent = "20 16";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         hovertime = "1000";
+         canSaveDynamicFields = "0";
+      };
+      new GuiTextEditCtrl() {
+         historySize = "0";
+         password = "0";
+         tabComplete = "0";
+         sinkAllKeyEvents = "0";
+         passwordMask = "*";
+         text = "2";
+         maxLength = "1024";
+         Margin = "0 0 0 0";
+         Padding = "0 0 0 0";
+         AnchorTop = "1";
+         AnchorBottom = "0";
+         AnchorLeft = "1";
+         AnchorRight = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiTextEditProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "225 310";
+         Extent = "26 18";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = "Frames per second for all animations when Animation Timing type is Frames (5 - 60)";
+         hovertime = "1000";
+         internalName = "animFPS";
+         canSaveDynamicFields = "0";
+      };
+
       new GuiTextCtrl() {
          text = "LOD";
          maxLength = "1024";
@@ -582,6 +683,28 @@
          canSaveDynamicFields = "0";
       };
 
+      new GuiCheckBoxCtrl() {
+         useInactiveState = "0";
+         text = " Force update materials.cs";
+         groupNum = "-1";
+         buttonType = "ToggleButton";
+         useMouseEvents = "0";
+         isContainer = "0";
+         Profile = "ToolsGuiCheckBoxProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         position = "210 150";
+         Extent = "200 13";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         variable = "$Assimp::ForceUpdateMats";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = "Forces update of materials.cs (even if Materials already exist)";
+         hovertime = "1000";
+         canSaveDynamicFields = "0";
+      };
+
       new GuiButtonCtrl() {
          text = "OK";
          groupNum = "-1";
@@ -643,6 +766,10 @@ function AssimpImportDlg::showDialog(%this, %shapePath, %cmd)
       $Assimp::FindDegenerates = true;
       $Assimp::FindInvalidData = true;
       $Assimp::JoinIdenticalVertices = true;
+      $Assimp::FlipNormals = false;
+      
+      $Assimp::AnimTiming = 1; // Seconds
+      $Assimp::AnimFPS = 30;   // Framerate when timing is frames.
    }
 
    %this-->upAxis.clear();
@@ -656,9 +783,15 @@ function AssimpImportDlg::showDialog(%this, %shapePath, %cmd)
    %this-->lodType.add("SingleSize", 1);
    %this-->lodType.add("TrailingNumber", 2);
    %this-->lodType.setSelected($Assimp::lodType);
-
    %this-->singleDetailSize.text = $Assimp::singleDetailSize;
 
+   %this-->animTiming.clear();
+   %this-->animTiming.add("Frames", 0);
+   %this-->animTiming.add("Seconds", 1);
+   %this-->animTiming.add("Milliseconds", 1000);
+   %this-->animTiming.setSelected($Assimp::AnimTiming);
+   %this-->animFPS.text = $Assimp::AnimFPS;
+
    //Triangulate is a default(currently mandatory) behavior
    $Assimp::Triangulate = true;
 
@@ -681,6 +814,9 @@ function AssimpImportDlg::onOK(%this)
    $Assimp::lodType = %this-->lodType.getSelected();
    $Assimp::singleDetailSize = %this-->singleDetailSize.getText();
 
+   $Assimp::AnimTiming = %this-->animTiming.getSelected();
+   $Assimp::AnimFPS = %this-->animFPS.getText();
+
    // Load the shape (always from the DAE)
    $assimp::forceLoad = true;
    eval(%this.cmd);

+ 1 - 1
Templates/Full/game/tools/shapeEditor/scripts/shapeEditor.ed.cs

@@ -261,7 +261,7 @@ function ShapeEdSelectWindow::onSelect( %this, %path )
    // Prompt user to save the old shape if it is dirty
    if ( ShapeEditor.isDirty() )
    {
-      %cmd = "ColladaImportDlg.showDialog( \"" @ %path @ "\", \"ShapeEditor.selectShape( \\\"" @ %path @ "\\\", ";
+      %cmd = "showImportDialog( \"" @ %path @ "\", \"ShapeEditor.selectShape( \\\"" @ %path @ "\\\", ";
       MessageBoxYesNoCancel( "Shape Modified", "Would you like to save your changes?", %cmd @ "true );\" );", %cmd @ "false );\" );" );
    }
    else