mirror of
				https://github.com/OpenMW/openmw.git
				synced 2025-11-04 13:26:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			440 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			440 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
  OpenMW - The completely unofficial reimplementation of Morrowind
 | 
						|
  Copyright (C) 2008-2010  Nicolay Korslund
 | 
						|
  Email: < korslund@gmail.com >
 | 
						|
  WWW: http://openmw.sourceforge.net/
 | 
						|
 | 
						|
  This file (nif_file.cpp) is part of the OpenMW package.
 | 
						|
 | 
						|
  OpenMW is distributed as free software: you can redistribute it
 | 
						|
  and/or modify it under the terms of the GNU General Public License
 | 
						|
  version 3, as published by the Free Software Foundation.
 | 
						|
 | 
						|
  This program is distributed in the hope that it will be useful, but
 | 
						|
  WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | 
						|
  General Public License for more details.
 | 
						|
 | 
						|
  You should have received a copy of the GNU General Public License
 | 
						|
  version 3 along with this program. If not, see
 | 
						|
  http://www.gnu.org/licenses/ .
 | 
						|
 | 
						|
 */
 | 
						|
 | 
						|
#include "niffile.hpp"
 | 
						|
#include "record.hpp"
 | 
						|
#include "components/misc/stringops.hpp"
 | 
						|
 | 
						|
#include "extra.hpp"
 | 
						|
#include "controlled.hpp"
 | 
						|
#include "node.hpp"
 | 
						|
#include "property.hpp"
 | 
						|
#include "data.hpp"
 | 
						|
#include "effect.hpp"
 | 
						|
#include "controller.hpp"
 | 
						|
 | 
						|
#include <iostream>
 | 
						|
 | 
						|
//TODO: when threading is needed, enable these
 | 
						|
//#include <boost/mutex.hpp>
 | 
						|
#include <boost/thread/locks.hpp>
 | 
						|
 | 
						|
namespace Nif
 | 
						|
{
 | 
						|
 | 
						|
class NIFFile::LoadedCache
 | 
						|
{
 | 
						|
    //TODO: enable this to make cache thread safe...
 | 
						|
    //typedef boost::mutex mutex;
 | 
						|
    
 | 
						|
    struct mutex
 | 
						|
    {
 | 
						|
        void lock () {};
 | 
						|
        void unlock () {}
 | 
						|
    };
 | 
						|
    
 | 
						|
    typedef boost::lock_guard <mutex> lock_guard;
 | 
						|
    typedef std::map < std::string, boost::weak_ptr <NIFFile> > loaded_map;
 | 
						|
    typedef std::vector < boost::shared_ptr <NIFFile> > locked_files;
 | 
						|
 | 
						|
    static int sLockLevel;
 | 
						|
    static mutex sProtector;
 | 
						|
    static loaded_map sLoadedMap;
 | 
						|
    static locked_files sLockedFiles;
 | 
						|
 | 
						|
public:
 | 
						|
 | 
						|
    static ptr create (const std::string &name)
 | 
						|
    {
 | 
						|
        lock_guard _ (sProtector);
 | 
						|
 | 
						|
        ptr result;
 | 
						|
 | 
						|
        // lookup the resource
 | 
						|
        loaded_map::iterator i = sLoadedMap.find (name);
 | 
						|
 | 
						|
        if (i == sLoadedMap.end ()) // it doesn't existing currently,
 | 
						|
        {                           // or hasn't in the very near past
 | 
						|
 | 
						|
            // create it now, for smoother threading if needed, the
 | 
						|
            // loading should be performed outside of the sLoaderMap
 | 
						|
            // lock and an alternate mechanism should be used to
 | 
						|
            // synchronize threads competing to load the same resource
 | 
						|
            result = boost::make_shared <NIFFile> (name, psudo_private_modifier());
 | 
						|
 | 
						|
            // if we are locking the cache add an extra reference
 | 
						|
            // to keep the file in memory
 | 
						|
            if (sLockLevel > 0)
 | 
						|
                sLockedFiles.push_back (result);
 | 
						|
 | 
						|
            // stash a reference to the resource so that future
 | 
						|
            // calls can benefit
 | 
						|
            sLoadedMap [name] = boost::weak_ptr <NIFFile> (result);
 | 
						|
        }
 | 
						|
        else // it may (probably) still exists
 | 
						|
        {
 | 
						|
            // attempt to get the reference
 | 
						|
            result = i->second.lock ();
 | 
						|
 | 
						|
            if (!result) // resource is in the process of being destroyed
 | 
						|
            {
 | 
						|
                // create a new instance, to replace the one that has
 | 
						|
                // begun the irreversible process of being destroyed
 | 
						|
                result = boost::make_shared <NIFFile> (name, psudo_private_modifier());
 | 
						|
 | 
						|
                // respect the cache lock...
 | 
						|
                if (sLockLevel > 0)
 | 
						|
                    sLockedFiles.push_back (result);
 | 
						|
 | 
						|
                // we potentially overwrite an expired pointer here
 | 
						|
                // but the other thread performing the delete on
 | 
						|
                // the previous copy of this resource will detect it
 | 
						|
                // and make sure not to erase the new reference
 | 
						|
                sLoadedMap [name] = boost::weak_ptr <NIFFile> (result);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // we made it!
 | 
						|
        return result;
 | 
						|
    }
 | 
						|
 | 
						|
    static void release (NIFFile * file)
 | 
						|
    {
 | 
						|
        lock_guard _ (sProtector);
 | 
						|
 | 
						|
        loaded_map::iterator i = sLoadedMap.find (file->filename);
 | 
						|
 | 
						|
        // its got to be in here, it just might not be us...
 | 
						|
        assert (i != sLoadedMap.end ());
 | 
						|
 | 
						|
        // if weak_ptr is still expired, this resource hasn't been recreated
 | 
						|
        // between the initiation of the final release due to destruction
 | 
						|
        // of the last shared pointer and this thread acquiring the lock on
 | 
						|
        // the loader map
 | 
						|
        if (i->second.expired ())
 | 
						|
            sLoadedMap.erase (i);
 | 
						|
    }
 | 
						|
 | 
						|
    static void lockCache ()
 | 
						|
    {
 | 
						|
        lock_guard _ (sProtector);
 | 
						|
 | 
						|
        sLockLevel++;
 | 
						|
    }
 | 
						|
 | 
						|
    static void unlockCache ()
 | 
						|
    {
 | 
						|
        locked_files resetList;
 | 
						|
 | 
						|
        {
 | 
						|
            lock_guard _ (sProtector);
 | 
						|
 | 
						|
            if (--sLockLevel)
 | 
						|
                sLockedFiles.swap(resetList);
 | 
						|
        }
 | 
						|
 | 
						|
        // this not necessary, but makes it clear that the
 | 
						|
        // deletion of the locked cache entries is being done
 | 
						|
        // outside the protection of sProtector
 | 
						|
        resetList.clear ();
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
int NIFFile::LoadedCache::sLockLevel = 0;
 | 
						|
NIFFile::LoadedCache::mutex NIFFile::LoadedCache::sProtector;
 | 
						|
NIFFile::LoadedCache::loaded_map NIFFile::LoadedCache::sLoadedMap;
 | 
						|
NIFFile::LoadedCache::locked_files NIFFile::LoadedCache::sLockedFiles;
 | 
						|
 | 
						|
// these three calls are forwarded to the cache implementation...
 | 
						|
void NIFFile::lockCache ()     { LoadedCache::lockCache (); }
 | 
						|
void NIFFile::unlockCache ()   { LoadedCache::unlockCache (); }
 | 
						|
NIFFile::ptr NIFFile::create (const std::string &name) { return LoadedCache::create  (name); }
 | 
						|
 | 
						|
/// Open a NIF stream. The name is used for error messages.
 | 
						|
NIFFile::NIFFile(const std::string &name, psudo_private_modifier)
 | 
						|
    : filename(name)
 | 
						|
{
 | 
						|
    parse();
 | 
						|
}
 | 
						|
 | 
						|
NIFFile::~NIFFile()
 | 
						|
{
 | 
						|
    LoadedCache::release (this);
 | 
						|
 | 
						|
    for(std::size_t i=0; i<records.size(); i++)
 | 
						|
        delete records[i];
 | 
						|
}
 | 
						|
 | 
						|
template <typename NodeType> static Record* construct() { return new NodeType; }
 | 
						|
 | 
						|
struct RecordFactoryEntry {
 | 
						|
 | 
						|
    typedef Record* (*create_t) ();
 | 
						|
 | 
						|
    char const *    mName;
 | 
						|
    create_t        mCreate;
 | 
						|
    RecordType      mType;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
/* These are all the record types we know how to read.
 | 
						|
 | 
						|
    This can be heavily optimized later if needed. For example, a
 | 
						|
    hash table or a FSM-based parser could be used to look up
 | 
						|
    node names.
 | 
						|
*/
 | 
						|
 | 
						|
static const RecordFactoryEntry recordFactories [] = {
 | 
						|
 | 
						|
    { "NiNode",                     &construct <NiNode                      >, RC_NiNode                        },
 | 
						|
    { "AvoidNode",                  &construct <NiNode                      >, RC_AvoidNode                     },
 | 
						|
    { "NiBSParticleNode",           &construct <NiNode                      >, RC_NiBSParticleNode              },
 | 
						|
    { "NiBSAnimationNode",          &construct <NiNode                      >, RC_NiBSAnimationNode             },
 | 
						|
    { "NiBillboardNode",            &construct <NiNode                      >, RC_NiNode                        },
 | 
						|
    { "NiTriShape",                 &construct <NiTriShape                  >, RC_NiTriShape                    },
 | 
						|
    { "NiRotatingParticles",        &construct <NiRotatingParticles         >, RC_NiRotatingParticles           },
 | 
						|
    { "NiAutoNormalParticles",      &construct <NiAutoNormalParticles       >, RC_NiAutoNormalParticles         },
 | 
						|
    { "NiCamera",                   &construct <NiCamera                    >, RC_NiCamera                      },
 | 
						|
    { "RootCollisionNode",          &construct <NiNode                      >, RC_RootCollisionNode             },
 | 
						|
    { "NiTexturingProperty",        &construct <NiTexturingProperty         >, RC_NiTexturingProperty           },
 | 
						|
    { "NiMaterialProperty",         &construct <NiMaterialProperty          >, RC_NiMaterialProperty            },
 | 
						|
    { "NiZBufferProperty",          &construct <NiZBufferProperty           >, RC_NiZBufferProperty             },
 | 
						|
    { "NiAlphaProperty",            &construct <NiAlphaProperty             >, RC_NiAlphaProperty               },
 | 
						|
    { "NiVertexColorProperty",      &construct <NiVertexColorProperty       >, RC_NiVertexColorProperty         },
 | 
						|
    { "NiShadeProperty",            &construct <NiShadeProperty             >, RC_NiShadeProperty               },
 | 
						|
    { "NiDitherProperty",           &construct <NiDitherProperty            >, RC_NiDitherProperty              },
 | 
						|
    { "NiWireframeProperty",        &construct <NiWireframeProperty         >, RC_NiWireframeProperty           },
 | 
						|
    { "NiSpecularProperty",         &construct <NiSpecularProperty          >, RC_NiSpecularProperty            },
 | 
						|
    { "NiStencilProperty",          &construct <NiStencilProperty           >, RC_NiStencilProperty             },
 | 
						|
    { "NiVisController",            &construct <NiVisController             >, RC_NiVisController               },
 | 
						|
    { "NiGeomMorpherController",    &construct <NiGeomMorpherController     >, RC_NiGeomMorpherController       },
 | 
						|
    { "NiKeyframeController",       &construct <NiKeyframeController        >, RC_NiKeyframeController          },
 | 
						|
    { "NiAlphaController",          &construct <NiAlphaController           >, RC_NiAlphaController             },
 | 
						|
    { "NiUVController",             &construct <NiUVController              >, RC_NiUVController                },
 | 
						|
    { "NiPathController",           &construct <NiPathController            >, RC_NiPathController              },
 | 
						|
    { "NiMaterialColorController",  &construct <NiMaterialColorController   >, RC_NiMaterialColorController     },
 | 
						|
    { "NiBSPArrayController",       &construct <NiBSPArrayController        >, RC_NiBSPArrayController          },
 | 
						|
    { "NiParticleSystemController", &construct <NiParticleSystemController  >, RC_NiParticleSystemController    },
 | 
						|
    { "NiFlipController",           &construct <NiFlipController            >, RC_NiFlipController              },
 | 
						|
    { "NiAmbientLight",             &construct <NiLight                     >, RC_NiLight                       },
 | 
						|
    { "NiDirectionalLight",         &construct <NiLight                     >, RC_NiLight                       },
 | 
						|
    { "NiTextureEffect",            &construct <NiTextureEffect             >, RC_NiTextureEffect               },
 | 
						|
    { "NiVertWeightsExtraData",     &construct <NiVertWeightsExtraData      >, RC_NiVertWeightsExtraData        },
 | 
						|
    { "NiTextKeyExtraData",         &construct <NiTextKeyExtraData          >, RC_NiTextKeyExtraData            },
 | 
						|
    { "NiStringExtraData",          &construct <NiStringExtraData           >, RC_NiStringExtraData             },
 | 
						|
    { "NiGravity",                  &construct <NiGravity                   >, RC_NiGravity                     },
 | 
						|
    { "NiPlanarCollider",           &construct <NiPlanarCollider            >, RC_NiPlanarCollider              },
 | 
						|
    { "NiParticleGrowFade",         &construct <NiParticleGrowFade          >, RC_NiParticleGrowFade            },
 | 
						|
    { "NiParticleColorModifier",    &construct <NiParticleColorModifier     >, RC_NiParticleColorModifier       },
 | 
						|
    { "NiParticleRotation",         &construct <NiParticleRotation          >, RC_NiParticleRotation            },
 | 
						|
    { "NiFloatData",                &construct <NiFloatData                 >, RC_NiFloatData                   },
 | 
						|
    { "NiTriShapeData",             &construct <NiTriShapeData              >, RC_NiTriShapeData                },
 | 
						|
    { "NiVisData",                  &construct <NiVisData                   >, RC_NiVisData                     },
 | 
						|
    { "NiColorData",                &construct <NiColorData                 >, RC_NiColorData                   },
 | 
						|
    { "NiPixelData",                &construct <NiPixelData                 >, RC_NiPixelData                   },
 | 
						|
    { "NiMorphData",                &construct <NiMorphData                 >, RC_NiMorphData                   },
 | 
						|
    { "NiKeyframeData",             &construct <NiKeyframeData              >, RC_NiKeyframeData                },
 | 
						|
    { "NiSkinData",                 &construct <NiSkinData                  >, RC_NiSkinData                    },
 | 
						|
    { "NiUVData",                   &construct <NiUVData                    >, RC_NiUVData                      },
 | 
						|
    { "NiPosData",                  &construct <NiPosData                   >, RC_NiPosData                     },
 | 
						|
    { "NiRotatingParticlesData",    &construct <NiRotatingParticlesData     >, RC_NiRotatingParticlesData       },
 | 
						|
    { "NiAutoNormalParticlesData",  &construct <NiAutoNormalParticlesData   >, RC_NiAutoNormalParticlesData     },
 | 
						|
    { "NiSequenceStreamHelper",     &construct <NiSequenceStreamHelper      >, RC_NiSequenceStreamHelper        },
 | 
						|
    { "NiSourceTexture",            &construct <NiSourceTexture             >, RC_NiSourceTexture               },
 | 
						|
    { "NiSkinInstance",             &construct <NiSkinInstance              >, RC_NiSkinInstance                },
 | 
						|
};
 | 
						|
 | 
						|
static RecordFactoryEntry const * recordFactories_begin = &recordFactories [0];
 | 
						|
static RecordFactoryEntry const * recordFactories_end   = &recordFactories [sizeof (recordFactories) / sizeof (recordFactories[0])];
 | 
						|
 | 
						|
RecordFactoryEntry const * lookupRecordFactory (char const * name)
 | 
						|
{
 | 
						|
    RecordFactoryEntry const * i;
 | 
						|
 | 
						|
    for (i = recordFactories_begin; i != recordFactories_end; ++i)
 | 
						|
        if (strcmp (name, i->mName) == 0)
 | 
						|
            break;
 | 
						|
 | 
						|
    if (i == recordFactories_end)
 | 
						|
        return NULL;
 | 
						|
 | 
						|
    return i;
 | 
						|
}
 | 
						|
 | 
						|
/* This file implements functions from the NIFFile class. It is also
 | 
						|
   where we stash all the functions we couldn't add as inline
 | 
						|
   definitions in the record types.
 | 
						|
 */
 | 
						|
 | 
						|
void NIFFile::parse()
 | 
						|
{
 | 
						|
    NIFStream nif (this, Ogre::ResourceGroupManager::getSingleton().openResource(filename));
 | 
						|
 | 
						|
  // Check the header string
 | 
						|
  std::string head = nif.getString(40);
 | 
						|
  if(head.compare(0, 22, "NetImmerse File Format") != 0)
 | 
						|
    fail("Invalid NIF header");
 | 
						|
 | 
						|
  // Get BCD version
 | 
						|
  ver = nif.getInt();
 | 
						|
  if(ver != VER_MW)
 | 
						|
    fail("Unsupported NIF version");
 | 
						|
 | 
						|
  // Number of records
 | 
						|
  size_t recNum = nif.getInt();
 | 
						|
  records.resize(recNum);
 | 
						|
 | 
						|
  /* The format for 10.0.1.0 seems to be a bit different. After the
 | 
						|
     header, it contains the number of records, r (int), just like
 | 
						|
     4.0.0.2, but following that it contains a short x, followed by x
 | 
						|
     strings. Then again by r shorts, one for each record, giving
 | 
						|
     which of the above strings to use to identify the record. After
 | 
						|
     this follows two ints (zero?) and then the record data. However
 | 
						|
     we do not support or plan to support other versions yet.
 | 
						|
  */
 | 
						|
 | 
						|
  for(size_t i = 0;i < recNum;i++)
 | 
						|
    {
 | 
						|
      Record *r = NULL;
 | 
						|
 | 
						|
      std::string rec = nif.getString();
 | 
						|
 | 
						|
      RecordFactoryEntry const * entry = lookupRecordFactory (rec.c_str ());
 | 
						|
 | 
						|
      if (entry != NULL)
 | 
						|
      {
 | 
						|
          r = entry->mCreate ();
 | 
						|
          r->recType = entry->mType;
 | 
						|
      }
 | 
						|
      else
 | 
						|
          fail("Unknown record type " + rec);
 | 
						|
 | 
						|
      assert(r != NULL);
 | 
						|
      assert(r->recType != RC_MISSING);
 | 
						|
      r->recName = rec;
 | 
						|
      r->recIndex = i;
 | 
						|
      records[i] = r;
 | 
						|
      r->read(&nif);
 | 
						|
 | 
						|
      // Discard tranformations for the root node, otherwise some meshes
 | 
						|
      // occasionally get wrong orientation. Only for NiNode-s for now, but
 | 
						|
      // can be expanded if needed.
 | 
						|
      // This should be rewritten when the method is cleaned up.
 | 
						|
      if (0 == i && rec == "NiNode")
 | 
						|
      {
 | 
						|
          static_cast<Nif::Node*>(r)->trafo = Nif::Transformation::getIdentity();
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    size_t rootNum = nif.getUInt();
 | 
						|
    roots.resize(rootNum);
 | 
						|
 | 
						|
    for(size_t i = 0;i < rootNum;i++)
 | 
						|
    {
 | 
						|
        intptr_t idx = nif.getInt();
 | 
						|
        roots[i] = ((idx >= 0) ? records.at(idx) : NULL);
 | 
						|
    }
 | 
						|
 | 
						|
    // Once parsing is done, do post-processing.
 | 
						|
    for(size_t i=0; i<recNum; i++)
 | 
						|
        records[i]->post(this);
 | 
						|
}
 | 
						|
 | 
						|
/// \todo move to the write cpp file
 | 
						|
 | 
						|
void NiSkinInstance::post(NIFFile *nif)
 | 
						|
{
 | 
						|
    data.post(nif);
 | 
						|
    root.post(nif);
 | 
						|
    bones.post(nif);
 | 
						|
 | 
						|
    if(data.empty() || root.empty())
 | 
						|
        nif->fail("NiSkinInstance missing root or data");
 | 
						|
 | 
						|
    size_t bnum = bones.length();
 | 
						|
    if(bnum != data->bones.size())
 | 
						|
        nif->fail("Mismatch in NiSkinData bone count");
 | 
						|
 | 
						|
    root->makeRootBone(&data->trafo);
 | 
						|
 | 
						|
    for(size_t i=0; i<bnum; i++)
 | 
						|
    {
 | 
						|
        if(bones[i].empty())
 | 
						|
            nif->fail("Oops: Missing bone! Don't know how to handle this.");
 | 
						|
        bones[i]->makeBone(i, data->bones[i]);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
void Node::getProperties(const Nif::NiTexturingProperty *&texprop,
 | 
						|
                         const Nif::NiMaterialProperty *&matprop,
 | 
						|
                         const Nif::NiAlphaProperty *&alphaprop,
 | 
						|
                         const Nif::NiVertexColorProperty *&vertprop,
 | 
						|
                         const Nif::NiZBufferProperty *&zprop,
 | 
						|
                         const Nif::NiSpecularProperty *&specprop,
 | 
						|
                         const Nif::NiWireframeProperty *&wireprop) const
 | 
						|
{
 | 
						|
    if(parent)
 | 
						|
        parent->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop);
 | 
						|
 | 
						|
    for(size_t i = 0;i < props.length();i++)
 | 
						|
    {
 | 
						|
        // Entries may be empty
 | 
						|
        if(props[i].empty())
 | 
						|
            continue;
 | 
						|
 | 
						|
        const Nif::Property *pr = props[i].getPtr();
 | 
						|
        if(pr->recType == Nif::RC_NiTexturingProperty)
 | 
						|
            texprop = static_cast<const Nif::NiTexturingProperty*>(pr);
 | 
						|
        else if(pr->recType == Nif::RC_NiMaterialProperty)
 | 
						|
            matprop = static_cast<const Nif::NiMaterialProperty*>(pr);
 | 
						|
        else if(pr->recType == Nif::RC_NiAlphaProperty)
 | 
						|
            alphaprop = static_cast<const Nif::NiAlphaProperty*>(pr);
 | 
						|
        else if(pr->recType == Nif::RC_NiVertexColorProperty)
 | 
						|
            vertprop = static_cast<const Nif::NiVertexColorProperty*>(pr);
 | 
						|
        else if(pr->recType == Nif::RC_NiZBufferProperty)
 | 
						|
            zprop = static_cast<const Nif::NiZBufferProperty*>(pr);
 | 
						|
        else if(pr->recType == Nif::RC_NiSpecularProperty)
 | 
						|
            specprop = static_cast<const Nif::NiSpecularProperty*>(pr);
 | 
						|
        else if(pr->recType == Nif::RC_NiWireframeProperty)
 | 
						|
            wireprop = static_cast<const Nif::NiWireframeProperty*>(pr);
 | 
						|
        else
 | 
						|
            std::cerr<< "Unhandled property type: "<<pr->recName <<std::endl;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
Ogre::Matrix4 Node::getLocalTransform() const
 | 
						|
{
 | 
						|
    Ogre::Matrix4 mat4 = Ogre::Matrix4(Ogre::Matrix4::IDENTITY);
 | 
						|
    mat4.makeTransform(trafo.pos, Ogre::Vector3(trafo.scale), Ogre::Quaternion(trafo.rotation));
 | 
						|
    return mat4;
 | 
						|
}
 | 
						|
 | 
						|
Ogre::Matrix4 Node::getWorldTransform() const
 | 
						|
{
 | 
						|
    if(parent != NULL)
 | 
						|
        return parent->getWorldTransform() * getLocalTransform();
 | 
						|
    return getLocalTransform();
 | 
						|
}
 | 
						|
 | 
						|
}
 |