From 0859b29fa1a392cbfde58206c37f9de953927aed Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Sun, 21 Apr 2019 23:59:40 -0400 Subject: [PATCH 01/10] Embedded texture extraction. Caches textures to disk for shape formats that support embedded textures. Only the compressed texture codepath has been tested. There are a large number of binary gltf files with embedded (and pbr) textures for testing here: https://github.com/KhronosGroup/glTF-Sample-Models --- Engine/source/ts/assimp/assimpShapeLoader.cpp | 61 ++++++++++++++++++- Engine/source/ts/assimp/assimpShapeLoader.h | 2 + 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/Engine/source/ts/assimp/assimpShapeLoader.cpp b/Engine/source/ts/assimp/assimpShapeLoader.cpp index f0e543704..46596a92b 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.cpp +++ b/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 { @@ -393,6 +400,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) diff --git a/Engine/source/ts/assimp/assimpShapeLoader.h b/Engine/source/ts/assimp/assimpShapeLoader.h index 713e403cc..848baaaf9 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.h +++ b/Engine/source/ts/assimp/assimpShapeLoader.h @@ -26,6 +26,7 @@ #ifndef _TSSHAPELOADER_H_ #include "ts/loader/tsShapeLoader.h" #endif +#include //----------------------------------------------------------------------------- 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(); From ad29d3132ef5df4804a4a8266706657db87213a8 Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Mon, 22 Apr 2019 00:08:15 -0400 Subject: [PATCH 02/10] Material initialization. Fixes material initialization for texture and transparency assignments. Lists all available material properties to console in debug builds. Adds TORQUE_PBR_MATERIALS define for testing with PBR branches. --- Engine/source/ts/assimp/assimpAppMaterial.cpp | 326 ++++++++++++++---- Engine/source/ts/assimp/assimpAppMaterial.h | 15 +- 2 files changed, 277 insertions(+), 64 deletions(-) diff --git a/Engine/source/ts/assimp/assimpAppMaterial.cpp b/Engine/source/ts/assimp/assimpAppMaterial.cpp index 6fdc913e5..55ba12506 100644 --- a/Engine/source/ts/assimp/assimpAppMaterial.cpp +++ b/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] = ""; - - newMat->mDiffuse[0] = diffuseColor; - //newMat->mSpecular[0] = specularColor; - //newMat->mSpecularPower[0] = specularPower; - - newMat->mDoubleSided = doubleSided; - newMat->mTranslucent = (bool)(flags & TSMaterialList::Translucent); - newMat->mTranslucentBlendOp = blendOp; + initMaterial(path, newMat); return newMat; -} \ No newline at end of file +} + +void AssimpAppMaterial::initMaterial(const Torque::Path& path, Material* mat) const +{ + String cleanFile = cleanString(TSShapeLoader::getShapePath().getFileName()); + String cleanName = cleanString(getName()); + + // 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 \ No newline at end of file diff --git a/Engine/source/ts/assimp/assimpAppMaterial.h b/Engine/source/ts/assimp/assimpAppMaterial.h index 4f24873a6..64aaedf28 100644 --- a/Engine/source/ts/assimp/assimpAppMaterial.h +++ b/Engine/source/ts/assimp/assimpAppMaterial.h @@ -26,6 +26,7 @@ #ifndef _APPMATERIAL_H_ #include "ts/loader/appMaterial.h" #endif +#include 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_ From 6694d5f206d32319dd0759b9780142f717476d9a Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Mon, 22 Apr 2019 00:09:29 -0400 Subject: [PATCH 03/10] Adds Force Update Materials to import options gui. --- .../game/tools/gui/assimpImport.ed.gui | 22 +++++++++++++++++++ .../Full/game/tools/gui/assimpImport.ed.gui | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/Templates/BaseGame/game/tools/gui/assimpImport.ed.gui b/Templates/BaseGame/game/tools/gui/assimpImport.ed.gui index e42d9ab9d..981c1f827 100644 --- a/Templates/BaseGame/game/tools/gui/assimpImport.ed.gui +++ b/Templates/BaseGame/game/tools/gui/assimpImport.ed.gui @@ -582,6 +582,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"; diff --git a/Templates/Full/game/tools/gui/assimpImport.ed.gui b/Templates/Full/game/tools/gui/assimpImport.ed.gui index e42d9ab9d..981c1f827 100644 --- a/Templates/Full/game/tools/gui/assimpImport.ed.gui +++ b/Templates/Full/game/tools/gui/assimpImport.ed.gui @@ -582,6 +582,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"; From 04355156de4adbcba6cea238bb961e948b4702d9 Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Mon, 22 Apr 2019 00:12:49 -0400 Subject: [PATCH 04/10] Fixes crash from unnamed animation sequences. Unnamed sequences are now renamed 'ambient'. Crash found in shape models/glTF2/simple_skin/simple_skin.gltf. --- Engine/source/ts/assimp/assimpAppSequence.cpp | 4 ++++ Engine/source/ts/assimp/assimpAppSequence.h | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Engine/source/ts/assimp/assimpAppSequence.cpp b/Engine/source/ts/assimp/assimpAppSequence.cpp index a5a92460e..4b903d756 100644 --- a/Engine/source/ts/assimp/assimpAppSequence.cpp +++ b/Engine/source/ts/assimp/assimpAppSequence.cpp @@ -16,6 +16,10 @@ AssimpAppSequence::AssimpAppSequence(aiAnimation *a) : seqStart(0.0f), mAnim(a) { + mSequenceName = mAnim->mName.C_Str(); + if (mSequenceName.isEmpty()) + mSequenceName = "ambient"; + // 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. diff --git a/Engine/source/ts/assimp/assimpAppSequence.h b/Engine/source/ts/assimp/assimpAppSequence.h index 28072af7e..ee7d9c0ac 100644 --- a/Engine/source/ts/assimp/assimpAppSequence.h +++ b/Engine/source/ts/assimp/assimpAppSequence.h @@ -22,6 +22,7 @@ class AssimpAppSequence : public AppSequence { + String mSequenceName; F32 seqStart; F32 seqEnd; @@ -37,7 +38,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; } From 70bbad84111f23198c8f2091d2c49b4bec7d8c1b Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Mon, 22 Apr 2019 06:52:59 -0400 Subject: [PATCH 05/10] Sequence timing options. This commit deals with the problem that the keyframe timestamps are not standardized. Seconds, milliseconds and ticks are used depending on the import format. There is no metadata or property that specifies the format used, so the option is exposed to the user as part of the import options gui. --- Engine/source/ts/assimp/assimpAppNode.cpp | 7 +- Engine/source/ts/assimp/assimpAppNode.h | 1 + Engine/source/ts/assimp/assimpAppSequence.cpp | 36 ++++-- Engine/source/ts/assimp/assimpAppSequence.h | 1 + .../game/tools/gui/assimpImport.ed.gui | 116 +++++++++++++++++- .../Full/game/tools/gui/assimpImport.ed.gui | 116 +++++++++++++++++- 6 files changed, 263 insertions(+), 14 deletions(-) diff --git a/Engine/source/ts/assimp/assimpAppNode.cpp b/Engine/source/ts/assimp/assimpAppNode.cpp index 686c5bfa2..937659a36 100644 --- a/Engine/source/ts/assimp/assimpAppNode.cpp +++ b/Engine/source/ts/assimp/assimpAppNode.cpp @@ -32,6 +32,7 @@ #include aiAnimation* AssimpAppNode::sActiveSequence = NULL; +F32 AssimpAppNode::sTimeMultiplier = 1.0f; AssimpAppNode::AssimpAppNode(const struct aiScene* scene, const struct aiNode* node, AssimpAppNode* parent) : mInvertMeshes(false), @@ -132,7 +133,7 @@ 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) { @@ -161,7 +162,7 @@ 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) @@ -190,7 +191,7 @@ 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) { diff --git a/Engine/source/ts/assimp/assimpAppNode.h b/Engine/source/ts/assimp/assimpAppNode.h index 3fe04d43e..947cb894b 100644 --- a/Engine/source/ts/assimp/assimpAppNode.h +++ b/Engine/source/ts/assimp/assimpAppNode.h @@ -70,6 +70,7 @@ public: } static aiAnimation* sActiveSequence; + static F32 sTimeMultiplier; //----------------------------------------------------------------------- const char *getName() { return mName; } diff --git a/Engine/source/ts/assimp/assimpAppSequence.cpp b/Engine/source/ts/assimp/assimpAppSequence.cpp index 4b903d756..33d0cf9fb 100644 --- a/Engine/source/ts/assimp/assimpAppSequence.cpp +++ b/Engine/source/ts/assimp/assimpAppSequence.cpp @@ -20,19 +20,19 @@ AssimpAppSequence::AssimpAppSequence(aiAnimation *a) : if (mSequenceName.isEmpty()) mSequenceName = "ambient"; - // 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. 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) @@ -57,9 +57,24 @@ 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 + timeFactor = mClamp(timeFactor, 1, 1000); + minFrameTime /= (F32)timeFactor; + maxEndTime /= (F32)timeFactor; + fps = (minFrameTime > 0.0f) ? 1.0f / minFrameTime : fps; + seqEnd = maxEndTime; + mTimeMultiplier = 1.0f / timeFactor; + } } AssimpAppSequence::~AssimpAppSequence() @@ -69,7 +84,10 @@ AssimpAppSequence::~AssimpAppSequence() void AssimpAppSequence::setActive(bool active) { if (active) + { AssimpAppNode::sActiveSequence = mAnim; + AssimpAppNode::sTimeMultiplier = mTimeMultiplier; + } else { if (AssimpAppNode::sActiveSequence == mAnim) diff --git a/Engine/source/ts/assimp/assimpAppSequence.h b/Engine/source/ts/assimp/assimpAppSequence.h index ee7d9c0ac..be5c11025 100644 --- a/Engine/source/ts/assimp/assimpAppSequence.h +++ b/Engine/source/ts/assimp/assimpAppSequence.h @@ -25,6 +25,7 @@ class AssimpAppSequence : public AppSequence String mSequenceName; F32 seqStart; F32 seqEnd; + F32 mTimeMultiplier; // The factor needed to convert the sequence data timestamp to seconds public: diff --git a/Templates/BaseGame/game/tools/gui/assimpImport.ed.gui b/Templates/BaseGame/game/tools/gui/assimpImport.ed.gui index 981c1f827..22794f3ad 100644 --- a/Templates/BaseGame/game/tools/gui/assimpImport.ed.gui +++ b/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"; @@ -665,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(); @@ -678,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; @@ -703,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); diff --git a/Templates/Full/game/tools/gui/assimpImport.ed.gui b/Templates/Full/game/tools/gui/assimpImport.ed.gui index 981c1f827..22794f3ad 100644 --- a/Templates/Full/game/tools/gui/assimpImport.ed.gui +++ b/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"; @@ -665,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(); @@ -678,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; @@ -703,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); From a4a97cc3d33ded98cdb61b96a2d3031a3fae7383 Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Mon, 22 Apr 2019 06:53:29 -0400 Subject: [PATCH 06/10] Additional filters for material names. --- Engine/source/ts/assimp/assimpAppMaterial.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/source/ts/assimp/assimpAppMaterial.cpp b/Engine/source/ts/assimp/assimpAppMaterial.cpp index 55ba12506..713ca9b87 100644 --- a/Engine/source/ts/assimp/assimpAppMaterial.cpp +++ b/Engine/source/ts/assimp/assimpAppMaterial.cpp @@ -39,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], '_'); From 6be2989bbc604cf25cf059bde147cf234a6747f0 Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Thu, 25 Apr 2019 15:41:01 -0400 Subject: [PATCH 07/10] glTF2 Importer Update. Vertex weights are now correctly assigned for skinned meshes. Portions of the vertex weight fix were taken from: https://github.com/ConfettiFX/The-Forge/blob/master/Common_3/ThirdParty/OpenSource/assimp/4.1.0/code/glTF2Importer.cpp#L823-L860. Inverse bind matrices have been adjusted to be consistent with the other importers. All nodes, joints and meshes are now named so T3D can cross reference when loading animations. All T3D specific changes have been bracketed in //T3D_CHANGE_BEGIN and //T3D_CHANGE_END tags to make them easier to find when the assimp library is updated. --- Engine/lib/assimp/code/glTF2Asset.inl | 2 + Engine/lib/assimp/code/glTF2Importer.cpp | 144 +++++++++++++++++------ Engine/lib/assimp/code/glTFAsset.inl | 2 + 3 files changed, 112 insertions(+), 36 deletions(-) diff --git a/Engine/lib/assimp/code/glTF2Asset.inl b/Engine/lib/assimp/code/glTF2Asset.inl index 196d664cb..b12a92f3a 100644 --- a/Engine/lib/assimp/code/glTF2Asset.inl +++ b/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); diff --git a/Engine/lib/assimp/code/glTF2Importer.cpp b/Engine/lib/assimp/code/glTF2Importer.cpp index 4228db23f..c2acdcd66 100644 --- a/Engine/lib/assimp/code/glTF2Importer.cpp +++ b/Engine/lib/assimp/code/glTF2Importer.cpp @@ -822,8 +822,6 @@ aiNode* ImportNode(aiScene* pScene, glTF2::Asset& r, std::vector& if (node.skin) { for (int primitiveNo = 0; primitiveNo < count; ++primitiveNo) { aiMesh* mesh = pScene->mMeshes[meshOffsets[mesh_idx]+primitiveNo]; - mesh->mNumBones = static_cast(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& // 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> 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> weighting(mesh->mNumBones); + //BuildVertexWeightMapping(node.meshes[0]->primitives[primitiveNo], weighting); + + //for (uint32_t i = 0; i < mesh->mNumBones; ++i) { + // aiBone* bone = new aiBone(); + + // Ref 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& weights = weighting[i]; + + // bone->mNumWeights = static_cast(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> 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 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& weights = weighting[i]; - - bone->mNumWeights = static_cast(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; + // 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 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& weights = weighting[i]; + + bone->mNumWeights = static_cast(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; + } + } + } + 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 samplers = GatherSamplers(anim); - ai_anim->mNumChannels = static_cast(samplers.size()); + //T3D_CHANGE_BEGIN + //ai_anim->mNumChannels = static_cast(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; diff --git a/Engine/lib/assimp/code/glTFAsset.inl b/Engine/lib/assimp/code/glTFAsset.inl index 5c65767d1..10bc4d1ef 100644 --- a/Engine/lib/assimp/code/glTFAsset.inl +++ b/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); From 6660f253b58f257bea737cd0f5a22cc50d4baa8f Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Thu, 25 Apr 2019 16:02:22 -0400 Subject: [PATCH 08/10] Fix for importing animated skinned meshes. Scale is negated in inverse bind matrices. Vertex weights are normalized (glTF importer is limited to 4 weights per vert). Fixed interpolation for animations where the first frame is not at 0.0. Allows cached.dts version of assimp imported shapes to be loaded. --- Engine/source/ts/assimp/assimpAppMesh.cpp | 27 ++++++++++++++++--- Engine/source/ts/assimp/assimpAppNode.cpp | 12 ++++----- Engine/source/ts/assimp/assimpAppSequence.cpp | 2 ++ Engine/source/ts/assimp/assimpShapeLoader.cpp | 8 +++--- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/Engine/source/ts/assimp/assimpAppMesh.cpp b/Engine/source/ts/assimp/assimpAppMesh.cpp index cd9c79bcb..3ed762e57 100644 --- a/Engine/source/ts/assimp/assimpAppMesh.cpp +++ b/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 ) diff --git a/Engine/source/ts/assimp/assimpAppNode.cpp b/Engine/source/ts/assimp/assimpAppNode.cpp index 937659a36..ca87f9d0c 100644 --- a/Engine/source/ts/assimp/assimpAppNode.cpp +++ b/Engine/source/ts/assimp/assimpAppNode.cpp @@ -135,13 +135,13 @@ void AssimpAppNode::getAnimatedTransform(MatrixF& mat, F32 t, aiAnimation* animS { 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; @@ -165,13 +165,13 @@ void AssimpAppNode::getAnimatedTransform(MatrixF& mat, F32 t, aiAnimation* animS 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; @@ -193,13 +193,13 @@ void AssimpAppNode::getAnimatedTransform(MatrixF& mat, F32 t, aiAnimation* animS { 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; diff --git a/Engine/source/ts/assimp/assimpAppSequence.cpp b/Engine/source/ts/assimp/assimpAppSequence.cpp index 33d0cf9fb..c64e7d068 100644 --- a/Engine/source/ts/assimp/assimpAppSequence.cpp +++ b/Engine/source/ts/assimp/assimpAppSequence.cpp @@ -19,6 +19,7 @@ AssimpAppSequence::AssimpAppSequence(aiAnimation *a) : 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; @@ -72,6 +73,7 @@ AssimpAppSequence::AssimpAppSequence(aiAnimation *a) : 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; } diff --git a/Engine/source/ts/assimp/assimpShapeLoader.cpp b/Engine/source/ts/assimp/assimpShapeLoader.cpp index 46596a92b..570d4825f 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.cpp +++ b/Engine/source/ts/assimp/assimpShapeLoader.cpp @@ -329,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"); @@ -339,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; } } From 2638559f9458ddccd505f3a9cc2d164e90f3752e Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Thu, 25 Apr 2019 16:05:21 -0400 Subject: [PATCH 09/10] Fixes import dialog display after showing the shape changed prompt in the ShapeEditor. --- .../BaseGame/game/tools/shapeEditor/scripts/shapeEditor.ed.cs | 4 ++-- .../Full/game/tools/shapeEditor/scripts/shapeEditor.ed.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Templates/BaseGame/game/tools/shapeEditor/scripts/shapeEditor.ed.cs b/Templates/BaseGame/game/tools/shapeEditor/scripts/shapeEditor.ed.cs index b9551c983..ea70f7363 100644 --- a/Templates/BaseGame/game/tools/shapeEditor/scripts/shapeEditor.ed.cs +++ b/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 ); } } diff --git a/Templates/Full/game/tools/shapeEditor/scripts/shapeEditor.ed.cs b/Templates/Full/game/tools/shapeEditor/scripts/shapeEditor.ed.cs index 1bada1ad7..1e5f896cb 100644 --- a/Templates/Full/game/tools/shapeEditor/scripts/shapeEditor.ed.cs +++ b/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 From 8ecbe5992a36a08a5e70362065c631cd9a7458f6 Mon Sep 17 00:00:00 2001 From: OTHGMars Date: Thu, 25 Apr 2019 16:27:46 -0400 Subject: [PATCH 10/10] Sanity check for sequence times. If the sequence end time is greater than 1000, millisecond timestamps are automatically assigned. --- Engine/source/ts/assimp/assimpAppSequence.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Engine/source/ts/assimp/assimpAppSequence.cpp b/Engine/source/ts/assimp/assimpAppSequence.cpp index c64e7d068..65a1f4b9e 100644 --- a/Engine/source/ts/assimp/assimpAppSequence.cpp +++ b/Engine/source/ts/assimp/assimpAppSequence.cpp @@ -69,6 +69,12 @@ AssimpAppSequence::AssimpAppSequence(aiAnimation *a) : } 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;