/*! Scans all meshes in the assimp scene and populates nameToBone and
jointGroups
*/
void SLAssimpImporter::findJoints(const aiScene* scene)
{
    for (SLuint i = 0; i < scene->mNumMeshes; i++)
    {
        aiMesh* mesh = scene->mMeshes[i];
        if(!mesh->HasBones())
            continue;

		logMessage(LV_normal, "   Mesh '%s' contains %d joints.\n", mesh->mName.C_Str(), mesh->mNumBones);
        
        for (SLuint j = 0; j < mesh->mNumBones; j++)
        {
			SLstring name = mesh->mBones[j]->mName.C_Str();
            std::map<SLstring, SLMat4f>::iterator it = _jointOffsets.find(name);
			if(it != _jointOffsets.end())
				continue;

            // add the offset matrix to our offset matrix map
			SLMat4f offsetMat;
			memcpy(&offsetMat, &mesh->mBones[j]->mOffsetMatrix, sizeof(SLMat4f));
			offsetMat.transpose();
			_jointOffsets[name] = offsetMat;


			logMessage(LV_detailed, "     Bone '%s' found.\n", name.c_str());
        }
    }
}
//----------------------------------------------------------------------------- 
//! SLGLShader::createAndCompile creates & compiles the OpenGL shader object
SLbool SLGLShader::createAndCompile()
{  
    // delete if object already exits
    if (_objectGL) glDeleteShader(_objectGL);

    if (_code!="")
    {  
        switch (_type)
        {   case VertexShader:
                _objectGL = glCreateShader(GL_VERTEX_SHADER); break;
            case FragmentShader:
                _objectGL = glCreateShader(GL_FRAGMENT_SHADER); break;
            default:
                SL_EXIT_MSG("SLGLShader::load: Unknown shader type.");
        }
      
        //SLstring verGLSL = SLGLState::getInstance()->glSLVersionNO();
        //SLstring srcVersion = "#version " + verGLSL + "\n";

        //if (verGLSL > "120")
        //{   if (_type == VertexShader)
        //    {   SLUtils::replaceString(_code, "attribute", "in");
        //        SLUtils::replaceString(_code, "varying", "out");
        //    }
        //    if (_type == FragmentShader)
        //    {   SLUtils::replaceString(_code, "varying", "in");
        //    }
        //}
        //SLstring scrComplete = srcVersion + _code;

        SLstring scrComplete = _code;

        const char* src = scrComplete.c_str();
        glShaderSource(_objectGL, 1, &src, 0);
        glCompileShader(_objectGL);

        // Check compiler log
        SLint compileSuccess = 0;
        glGetShaderiv(_objectGL, GL_COMPILE_STATUS, &compileSuccess);
        if (compileSuccess == GL_FALSE) 
        {   GLchar log[256];
            glGetShaderInfoLog(_objectGL, sizeof(log), 0, &log[0]);
            SL_LOG("*** COMPILER ERROR ***\n");
            SL_LOG("Source file: %s\n", _file.c_str());
            SL_LOG("%s\n\n", log);
            return false;
        }
        return true;
    } else SL_WARN_MSG("SLGLShader::createAndCompile: Nothing to compile!");
    return false;
}
/*! SLFileSystem::fileExists returns true if the file exists. This code works
only on windows because the file check is done case insensitive.
*/
SLbool SLFileSystem::fileExists(SLstring& pathfilename) 
{  
    struct stat stFileInfo;
    if (stat(pathfilename.c_str(), &stFileInfo) == 0)
        return true;
    return false;
}
//-----------------------------------------------------------------------------
//! Scans the assimp scene graph structure and populates nameToNode
void SLAssimpImporter::findNodes(aiNode* node, SLstring padding, SLbool lastChild)
{ 
    SLstring name = node->mName.C_Str();
    /*
    /// @todo we can't allow for duplicate node names, ever at the moment. The 'solution' below
    ///       only hides the problem and moves it to a different part.
    // rename duplicate node names
    SLstring renamedString;
    if (_nodeMap.find(name) != _nodeMap.end())
    {
        SLint index = 0;
        std::ostringstream ss;
        SLstring lastMatch = name;
        while (_nodeMap.find(lastMatch) != _nodeMap.end()) 
        {
            ss.str(SLstring());
            ss.clear();
            ss << name << "_" << std::setw( 2 ) << std::setfill( '0' ) << index;
            lastMatch = ss.str();
            index++;
        }
        ss.str(SLstring());
        ss.clear();
        ss << "(renamed from '" << name << "')";
        renamedString = ss.str();
        name = lastMatch;
    }*/

    // this should not happen
    assert(_nodeMap.find(name) == _nodeMap.end() && "Duplicated node name found!");
    _nodeMap[name] = node;

    //logMessage(LV_Detailed, "%s   |\n", padding.c_str());
    logMessage(LV_detailed, "%s  |-[%s]   (%d children, %d meshes)\n", 
               padding.c_str(), 
               name.c_str(),
               node->mNumChildren, 
               node->mNumMeshes);
    
    if (lastChild) padding += "   ";
    else padding += "  |";

    for (SLuint i = 0; i < node->mNumChildren; i++)
    {
        findNodes(node->mChildren[i], padding, (i == node->mNumChildren-1));
    }
}
//-----------------------------------------------------------------------------
//! SLGLShader::load loads a shader file into string _shaderSource
void SLGLShader::load(SLstring filename)
{  
    fstream shaderFile(filename.c_str(), ios::in);
    
    if (!shaderFile.is_open())
    {   SL_LOG("File open failed: %s\n", filename.c_str());
        exit(1);
    }
   
    std::stringstream buffer;
    buffer << shaderFile.rdbuf(); 

    // remove comments because some stupid ARM compiler can't handle GLSL comments
    #ifdef SL_OS_MACIOS
    _code = buffer.str();
    #else
    _code = SLUtils::removeComments(buffer.str());
    #endif
}
/*! 
SLAssimpImporter::checkFilePath tries to build the full absolut texture file path. 
Some file formats have absolute path stored, some have relative paths.
1st attempt: modelPath + aiTexFile
2nd attempt: aiTexFile
3rd attempt: modelPath + getFileName(aiTexFile)
If a model contains absolute path it is best to put all texture files beside the
model file in the same folder.
*/
SLstring SLAssimpImporter::checkFilePath(SLstring modelPath, SLstring aiTexFile)
{
    // Check path & file combination
    SLstring pathFile = modelPath + aiTexFile;
    if (SLFileSystem::fileExists(pathFile))
        return pathFile;

    // Check file alone
    if (SLFileSystem::fileExists(aiTexFile))
        return aiTexFile;

    // Check path & file combination
    pathFile = modelPath + SLUtils::getFileName(aiTexFile);
    if (SLFileSystem::fileExists(pathFile))
        return pathFile;

    SLstring msg = "SLAssimpImporter: Texture file not found: \n" + aiTexFile + 
                   "\non model path: " + modelPath + "\n";
    SL_WARN_MSG(msg.c_str());

    // Return path for texture not found image;
    return SLGLTexture::defaultPath + "TexNotFound.png";
}
/*!
SLAssimpImporter::loadAnimation loads the scene graph node tree recursively.
*/
SLAnimation* SLAssimpImporter::loadAnimation(aiAnimation* anim)
{
    int animCount = 0;
    if (_skeleton) animCount = _skeleton->numAnimations();
    ostringstream oss;
    oss << "unnamed_anim_" << animCount;
    SLstring animName = oss.str();
    SLfloat animTicksPerSec = (anim->mTicksPerSecond == 0) ? 30.0f : (SLfloat)anim->mTicksPerSecond;
    SLfloat animDuration = (SLfloat)anim->mDuration / animTicksPerSec;

    if (anim->mName.length > 0)
        animName = anim->mName.C_Str();

    // log
    logMessage(LV_minimal, "\nLoading animation %s\n", animName.c_str());
    logMessage(LV_normal, " Duration(seconds): %f \n", animDuration);
    logMessage(LV_normal, " Duration(ticks): %f \n", anim->mDuration);
    logMessage(LV_normal, " Ticks per second: %f \n", animTicksPerSec);
    logMessage(LV_normal, " Num channels: %d\n", anim->mNumChannels);
                
    // exit if we didn't load a skeleton but have animations for one
    if (_skinnedMeshes.size() > 0)
        assert(_skeleton != nullptr && "The skeleton wasn't impoted correctly."); 
    
    // create the animation
    SLAnimation* result;
    if (_skeleton)
        result = _skeleton->createAnimation(animName, animDuration);
    else
    {
        result = SLScene::current->animManager().createNodeAnimation(animName, animDuration);
        _nodeAnimations.push_back(result);
    }


    SLbool isSkeletonAnim = false;
    for (SLuint i = 0; i < anim->mNumChannels; i++)
    {
        aiNodeAnim* channel = anim->mChannels[i];

        // find the node that is animated by this channel
        SLstring nodeName = channel->mNodeName.C_Str();
        SLNode* affectedNode = _sceneRoot->find<SLNode>(nodeName);
        SLuint id = 0;
        SLbool isJointNode = (affectedNode == nullptr);

        // @todo: this is currently a work around but it can happen that we receive normal node animationtracks and joint animationtracks
        //        we don't allow node animation tracks in a skeleton animation, so we should split an animation in two seperate 
        //        animations if this happens. for now we just ignore node animation tracks if we already have joint tracks
        //        ofc this will crash if the first track is a node anim but its just temporary
        if (!isJointNode && isSkeletonAnim)
            continue;

        // is there a skeleton and is this animation channel not affecting a normal node?
        if (_skeletonRoot && !affectedNode)
        {
            isSkeletonAnim = true;
            SLJoint* affectedJoint = _skeleton->getJoint(nodeName);
            if (affectedJoint == nullptr)
                break;

            id = affectedJoint->id();
            // @todo warn if we find an animation with some node channels and some joint channels
            //       this shouldn't happen!

            /// @todo [high priority!] Address the problem of some bones not containing an animation channel
            ///         when importing. Current workaround is to set their reset position to their bind pose.
            ///         This will however fail if we have multiple animations affecting a single model and fading
            ///         some of them out or in. This will require us to provide animations that have a channel
            ///         for all bones even if they're just positional.
            // What does this next line do?
            //   
            //   The testimportfile we used (Astroboy.dae) has the following properties:
            //      > It has joints in the skeleton that aren't animated by any channel.
            //      > The joints need a reset position of (0, 0, 0) to work properly 
            //          because the joint position is contained in a single keyframe for every joint
            //
            //      Since some of the joints don't have a channel that animates them, they also lack
            //      the joint position that the other joints get from their animation channel.
            //      So we need to set the initial state for all joints that have a channel
            //      to identity.
            //      All joints that arent in a channel will receive their local joint bind pose as
            //      reset position.
            //
            //      The problem stems from the design desicion to reset a whole skeleton before applying 
            //      animations to it. If we were to reset each joint just before applying a channel to it
            //      we wouldn't have this problem. But we coulnd't blend animations as easily.
            //
            SLMat4f prevOM = affectedJoint->om();
            affectedJoint->om(SLMat4f());
            affectedJoint->setInitialState();
            affectedJoint->om(prevOM);
        }
                            
        // log
        logMessage(LV_normal, "\n  Channel %d %s", i, (isJointNode) ? "(joint animation)\n" : "\n");
        logMessage(LV_normal, "   Affected node: %s\n", channel->mNodeName.C_Str());
        logMessage(LV_detailed, "   Num position keys: %d\n", channel->mNumPositionKeys);
        logMessage(LV_detailed, "   Num rotation keys: %d\n", channel->mNumRotationKeys);
        logMessage(LV_detailed, "   Num scaling keys: %d\n", channel->mNumScalingKeys);

        
        // joint animation channels should receive the correct node id, normal node animations just get 0
        SLNodeAnimTrack* track = result->createNodeAnimationTrack(id);

        
        // this is a node animation only, so we add a reference to the affected node to the track
        if (affectedNode && !isSkeletonAnim) {
            track->animatedNode(affectedNode);
        }

        KeyframeMap keyframes;

        // add position keys
        for (SLuint i = 0; i < channel->mNumPositionKeys; i++)
        {
            SLfloat time = (SLfloat)channel->mPositionKeys[i].mTime; 
            keyframes[time] = SLImportKeyframe(&channel->mPositionKeys[i], nullptr, nullptr);
        }
        
        // add rotation keys
        for (SLuint i = 0; i < channel->mNumRotationKeys; i++)
        {
            SLfloat time = (SLfloat)channel->mRotationKeys[i].mTime;

            if (keyframes.find(time) == keyframes.end())
                keyframes[time] = SLImportKeyframe(nullptr, &channel->mRotationKeys[i], nullptr);
            else
            {
                // @todo this shouldn't abort but just throw an exception
                assert(keyframes[time].rotation == nullptr && "There were two rotation keys assigned to the same timestamp.");
                keyframes[time].rotation = &channel->mRotationKeys[i];
            }
        }
        
        // add scaleing keys
        for (SLuint i = 0; i < channel->mNumScalingKeys; i++)
        {
            SLfloat time = (SLfloat)channel->mScalingKeys[i].mTime; 

            if (keyframes.find(time) == keyframes.end())
                keyframes[time] = SLImportKeyframe(nullptr, nullptr, &channel->mScalingKeys[i]);
            else
            {
                // @todo this shouldn't abort but just throw an exception
                assert(keyframes[time].scaling == nullptr && "There were two scaling keys assigned to the same timestamp.");
                keyframes[time].scaling = &channel->mScalingKeys[i];
            }
        }

        logMessage(LV_normal, "   Found %d distinct keyframe timestamp(s).\n", 
                              keyframes.size());

        for (auto it : keyframes)
        {   SLTransformKeyframe* kf = track->createNodeKeyframe(it.first);         
            kf->translation(getTranslation(it.first, keyframes));
            kf->rotation(getRotation(it.first, keyframes));
            kf->scale(getScaling(it.first, keyframes));

            // log
            logMessage(LV_detailed, "\n   Generating keyframe at time '%.2f'\n", 
                       it.first);
            logMessage(LV_detailed, "    Translation: (%.2f, %.2f, %.2f) %s\n", 
                       kf->translation().x, 
                       kf->translation().y, 
                       kf->translation().z, 
                       (it.second.translation != nullptr) ? "imported" : "generated");
            logMessage(LV_detailed, "    Rotation: (%.2f, %.2f, %.2f, %.2f) %s\n", 
                       kf->rotation().x(), 
                       kf->rotation().y(), 
                       kf->rotation().z(), 
                       kf->rotation().w(), 
                       (it.second.rotation != nullptr) ? "imported" : "generated");
            logMessage(LV_detailed, "    Scale: (%.2f, %.2f, %.2f) %s\n", 
                       kf->scale().x, 
                       kf->scale().y, 
                       kf->scale().z, 
                       (it.second.scaling != nullptr) ? "imported" : "generated");
        }
    }
    
    return result;
}
/*!
SLAssimpImporter::loadMesh creates a new SLMesh an copies the meshs vertex data and
triangle face indices. Normals & tangents are not loaded. They are calculated
in SLMesh.
*/
SLMesh* SLAssimpImporter::loadMesh(aiMesh *mesh)
{
    // Count first the NO. of triangles in the mesh
    SLuint numTriangles = 0;
    for(unsigned int i = 0; i <  mesh->mNumFaces; ++i)
        if(mesh->mFaces[i].mNumIndices == 3)
            numTriangles++;

    // We only load meshes that contain triangles
    if (numTriangles==0 || mesh->mNumVertices==0)
        return nullptr; 

    // create a new mesh. 
    // The mesh pointer is added automatically to the SLScene::meshes vector.
    SLstring name = mesh->mName.data;
    SLMesh *m = new SLMesh(name.empty() ? "Imported Mesh" : name);

    // create position & normal vector
    m->P.clear(); m->P.resize(mesh->mNumVertices);

    // create normal vector
    if (mesh->HasNormals())
    {   m->N.clear();
        m->N.resize(m->P.size());
    }

    // allocate texCoord vector if needed
    if (mesh->HasTextureCoords(0))
    {   m->Tc.clear();
        m->Tc.resize(m->P.size());
    }

    // copy vertex positions & texCoord
    for(SLuint i = 0; i < m->P.size(); ++i)
    {   m->P[i].set(mesh->mVertices[i].x, 
        mesh->mVertices[i].y, 
        mesh->mVertices[i].z);
        if (m->N.size())
            m->N[i].set(mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i].z);
        if (m->Tc.size())
            m->Tc[i].set(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y);
    }

    // create face index vector
    if (m->P.size() < 65536)
    {   m->I16.clear();
        m->I16.resize(mesh->mNumFaces * 3);

        // load face triangle indices only
        SLuint j = 0;
        for(SLuint i = 0; i <  mesh->mNumFaces; ++i)
        {   if(mesh->mFaces[i].mNumIndices == 3)
            {   m->I16[j++] = mesh->mFaces[i].mIndices[0];
                m->I16[j++] = mesh->mFaces[i].mIndices[1];
                m->I16[j++] = mesh->mFaces[i].mIndices[2];
            }
        }
    } else 
    {   m->I32.clear();
        m->I32.resize(mesh->mNumFaces * 3);

        // load face triangle indices only
        SLuint j = 0;
        for(SLuint i = 0; i <  mesh->mNumFaces; ++i)
        {  if(mesh->mFaces[i].mNumIndices == 3)
            {   m->I32[j++] = mesh->mFaces[i].mIndices[0];
                m->I32[j++] = mesh->mFaces[i].mIndices[1];
                m->I32[j++] = mesh->mFaces[i].mIndices[2];
            }
        }
    }

    if (!m->N.size())
        m->calcNormals();

    // load joints
    if (mesh->HasBones())
    {
        _skinnedMeshes.push_back(m);
        m->skeleton(_skeleton);

        m->Ji.resize(m->P.size());
        m->Jw.resize(m->P.size());
        
        // make sure to initialize the weights with 0 vectors
        std::fill(m->Ji.begin(), m->Ji.end(), SLVec4f(0, 0, 0, 0));
        std::fill(m->Jw.begin(), m->Jw.end(), SLVec4f(0, 0, 0, 0));

        for (SLuint i = 0; i < mesh->mNumBones; i++)
        {
            aiBone* joint = mesh->mBones[i];
            SLJoint* slJoint = _skeleton->getJoint(joint->mName.C_Str());
            
            // @todo On OSX it happens from time to time that slJoint is nullptr
            if (slJoint)
            {
                SLuint jointId = slJoint->id();

                for (SLuint j = 0; j < joint->mNumWeights; j++)
                {
                    // add the weight
                    SLuint vertId = joint->mWeights[j].mVertexId;
                    SLfloat weight = joint->mWeights[j].mWeight;

                    m->addWeight(vertId, jointId, weight);

                    // check if the bones max radius changed
                    // @todo this is very specific to this loaded mesh,
                    //       when we add a skeleton instances class this radius
                    //       calculation has to be done on the instance!
                    slJoint->calcMaxRadius(SLVec3f(mesh->mVertices[vertId].x,
                                                   mesh->mVertices[vertId].y,
                                                   mesh->mVertices[vertId].z));
                }
            }
            else
            {   SL_LOG("Failed to load joint of skeleton in SLAssimpImporter::loadMesh: %s\n", joint->mName.C_Str());
                return nullptr;
            }
        }

    }
    return m;
}
/*!
SLAssimpImporter::loadMaterial loads the AssImp material an returns the SLMaterial.
The materials and textures are added to the SLScene material and texture 
vectors.
*/
SLMaterial* SLAssimpImporter::loadMaterial(SLint index, 
                                           aiMaterial *material,
                                           SLstring modelPath)
{
    // Get the materials name
    aiString matName;
    material->Get(AI_MATKEY_NAME, matName);
    SLstring name = matName.data;
    if (name.empty()) name = "Import Material";
   
    // Create SLMaterial instance. It is also added to the SLScene::_materials vector
    SLMaterial* mat = new SLMaterial(name.c_str());

    // set the texture types to import into our material
    const SLint		textureCount = 4;
    aiTextureType	textureTypes[textureCount];
    textureTypes[0] = aiTextureType_DIFFUSE;
    textureTypes[1] = aiTextureType_NORMALS;
    textureTypes[2] = aiTextureType_SPECULAR;
    textureTypes[3] = aiTextureType_HEIGHT;
   
    // load all the textures for this material and add it to the material vector
    for(SLint i = 0; i < textureCount; ++i) 
    {   if(material->GetTextureCount(textureTypes[i]) > 0) 
        {   aiString aipath;
            material->GetTexture(textureTypes[i], 0, &aipath, nullptr, nullptr, nullptr, nullptr, nullptr);
            SLTextureType texType = textureTypes[i]==aiTextureType_DIFFUSE  ? TT_color :
                                textureTypes[i]==aiTextureType_NORMALS  ? TT_normal :
                                textureTypes[i]==aiTextureType_SPECULAR ? TT_gloss :
                                textureTypes[i]==aiTextureType_HEIGHT   ? TT_height : 
                                TT_unknown;
            SLstring texFile = checkFilePath(modelPath, aipath.data);
            SLGLTexture* tex = loadTexture(texFile, texType);
            mat->textures().push_back(tex);
        }
    }
   
    // get color data
    aiColor3D ambient, diffuse, specular, emissive;
    SLfloat shininess, refracti, reflectivity, opacity;
    material->Get(AI_MATKEY_COLOR_AMBIENT, ambient);
    material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse);
    material->Get(AI_MATKEY_COLOR_SPECULAR, specular);
    material->Get(AI_MATKEY_COLOR_EMISSIVE, emissive);
    material->Get(AI_MATKEY_SHININESS, shininess);
    material->Get(AI_MATKEY_REFRACTI, refracti);
    material->Get(AI_MATKEY_REFLECTIVITY, reflectivity);
    material->Get(AI_MATKEY_OPACITY, opacity);

    // increase shininess if specular color is not low.
    // The material will otherwise be to bright
    if (specular.r > 0.5f &&
        specular.g > 0.5f &&
        specular.b > 0.5f &&
        shininess < 0.01f)
        shininess = 10.0f;

    // set color data
    mat->ambient(SLCol4f(ambient.r, ambient.g, ambient.b));
    mat->diffuse(SLCol4f(diffuse.r, diffuse.g, diffuse.b));
    mat->specular(SLCol4f(specular.r, specular.g, specular.b));
    mat->emission(SLCol4f(emissive.r, emissive.g, emissive.b));
    mat->shininess(shininess);
    //mat->kr(reflectivity);
    //mat->kt(1.0f-opacity);
    //mat->kn(refracti);

    return mat;
}
/*! Loads the scene from a file and creates materials with textures, the 
meshes and the nodes for the scene graph. Materials, textures and meshes are
added to the according vectors of SLScene for later deallocation.
*/
SLNode* SLAssimpImporter::load(SLstring file,           //!< File with path or on default path
                               SLbool loadMeshesOnly,   //!< Only load nodes with meshes
                               SLuint flags)            //!< Import flags (see assimp/postprocess.h)
{
    // clear the intermediate data
    clear();

    // Check existance
    if (!SLFileSystem::fileExists(file))
    {   file = defaultPath + file;
        if (!SLFileSystem::fileExists(file))
        {   SLstring msg = "SLAssimpImporter: File not found: " + file + "\n";
            SL_WARN_MSG(msg.c_str());
            return nullptr;
        }
    }

    // Import file with assimp importer
    Assimp::Importer ai;
    const aiScene* scene = ai.ReadFile(file.c_str(), (SLuint)flags);
    if (!scene)
    {   SLstring msg = "Failed to load file: " + file + "\n" + ai.GetErrorString() + "\n";
        SL_WARN_MSG(msg.c_str());
        return nullptr;
    }

    // initial scan of the scene
    performInitialScan(scene);

    // load skeleton
    loadSkeleton(nullptr, _skeletonRoot);

    // load materials
    SLstring modelPath = SLUtils::getPath(file);
    SLVMaterial materials;
    for(SLint i = 0; i < (SLint)scene->mNumMaterials; i++)
        materials.push_back(loadMaterial(i, scene->mMaterials[i], modelPath));

    // load meshes & set their material
    std::map<int, SLMesh*> meshMap;  // map from the ai index to our mesh
    for(SLint i = 0; i < (SLint)scene->mNumMeshes; i++)
    {   SLMesh* mesh = loadMesh(scene->mMeshes[i]);
        if (mesh != 0)
        {   mesh->mat = materials[scene->mMeshes[i]->mMaterialIndex];
            _meshes.push_back(mesh);
            meshMap[i] = mesh;
        } else SL_LOG("SLAsssimpImporter::load failed: %s\nin path: %s\n", file.c_str(), modelPath.c_str());
    }

    // load the scene nodes recursively
    _sceneRoot = loadNodesRec(nullptr, scene->mRootNode, meshMap, loadMeshesOnly);

    // load animations
    vector<SLAnimation*> animations;
    for (SLint i = 0; i < (SLint)scene->mNumAnimations; i++)
        animations.push_back(loadAnimation(scene->mAnimations[i]));

    logMessage(LV_minimal, "\n---------------------------\n\n");

    return _sceneRoot;
}