]> git.mxchange.org Git - simgear.git/blob - simgear/scene/model/model.cxx
8a08e73a9645a77c213022170bcc9adc9f08d2ac
[simgear.git] / simgear / scene / model / model.cxx
1 // model.cxx - manage a 3D aircraft model.
2 // Written by David Megginson, started 2002.
3 //
4 // This file is in the Public Domain, and comes with no warranty.
5
6 #ifdef HAVE_CONFIG_H
7 #include <simgear_config.h>
8 #endif
9
10 #include <osg/observer_ptr>
11 #include <osg/ref_ptr>
12 #include <osg/Group>
13 #include <osg/NodeCallback>
14 #include <osg/Switch>
15 #include <osg/MatrixTransform>
16 #include <osgDB/Archive>
17 #include <osgDB/FileNameUtils>
18 #include <osgDB/FileUtils>
19 #include <osgDB/ReadFile>
20 #include <osgDB/WriteFile>
21 #include <osgDB/Registry>
22 #include <osgDB/SharedStateManager>
23 #include <osgUtil/Optimizer>
24
25 #include <simgear/scene/util/SGSceneFeatures.hxx>
26 #include <simgear/scene/util/SGStateAttributeVisitor.hxx>
27 #include <simgear/scene/util/SGTextureStateAttributeVisitor.hxx>
28
29 #include <simgear/structure/exception.hxx>
30 #include <simgear/props/props.hxx>
31 #include <simgear/props/props_io.hxx>
32 #include <simgear/props/condition.hxx>
33
34 #include "animation.hxx"
35 #include "model.hxx"
36
37 SG_USING_STD(vector);
38 SG_USING_STD(set);
39
40 // Little helper class that holds an extra reference to a
41 // loaded 3d model.
42 // Since we clone all structural nodes from our 3d models,
43 // the database pager will only see one single reference to
44 // top node of the model and expire it relatively fast.
45 // We attach that extra reference to every model cloned from
46 // a base model in the pager. When that cloned model is deleted
47 // this extra reference is deleted too. So if there are no
48 // cloned models left the model will expire.
49 class SGDatabaseReference : public osg::Observer {
50 public:
51   SGDatabaseReference(osg::Referenced* referenced) :
52     mReferenced(referenced)
53   { }
54   virtual void objectDeleted(void*)
55   {
56     mReferenced = 0;
57   }
58 private:
59   osg::ref_ptr<osg::Referenced> mReferenced;
60 };
61
62 // Visitor for 
63 class SGTextureUpdateVisitor : public SGTextureStateAttributeVisitor {
64 public:
65   SGTextureUpdateVisitor(const osgDB::FilePathList& pathList) :
66     mPathList(pathList)
67   { }
68   osg::Texture2D* textureReplace(int unit,
69                                  osg::StateSet::RefAttributePair& refAttr)
70   {
71     osg::Texture2D* texture;
72     texture = dynamic_cast<osg::Texture2D*>(refAttr.first.get());
73     if (!texture)
74       return 0;
75     
76     osg::ref_ptr<osg::Image> image = texture->getImage(0);
77     if (!image)
78       return 0;
79
80     // The currently loaded file name
81     std::string fullFilePath = image->getFileName();
82     // The short name
83     std::string fileName = osgDB::getSimpleFileName(fullFilePath);
84     // The name that should be found with the current database path
85     std::string fullLiveryFile = osgDB::findFileInPath(fileName, mPathList);
86     // If they are identical then there is nothing to do
87     if (fullLiveryFile == fullFilePath)
88       return 0;
89
90     image = osgDB::readImageFile(fullLiveryFile);
91     if (!image)
92       return 0;
93
94     osg::CopyOp copyOp(osg::CopyOp::DEEP_COPY_ALL &
95                        ~osg::CopyOp::DEEP_COPY_IMAGES);
96     texture = static_cast<osg::Texture2D*>(copyOp(texture));
97     if (!texture)
98       return 0;
99     texture->setImage(image.get());
100     return texture;
101   }
102   virtual void apply(osg::StateSet* stateSet)
103   {
104     if (!stateSet)
105       return;
106
107     // get a copy that we can safely modify the statesets values.
108     osg::StateSet::TextureAttributeList attrList;
109     attrList = stateSet->getTextureAttributeList();
110     for (unsigned unit = 0; unit < attrList.size(); ++unit) {
111       osg::StateSet::AttributeList::iterator i;
112       i = attrList[unit].begin();
113       while (i != attrList[unit].end()) {
114         osg::Texture2D* texture = textureReplace(unit, i->second);
115         if (texture) {
116           stateSet->removeTextureAttribute(unit, i->second.first.get());
117           stateSet->setTextureAttribute(unit, texture, i->second.second);
118           stateSet->setTextureMode(unit, GL_TEXTURE_2D,
119                                    osg::StateAttribute::ON);
120         }
121         ++i;
122       }
123     }
124   }
125
126 private:
127   osgDB::FilePathList mPathList;
128 };
129
130 class SGTexCompressionVisitor : public SGTextureStateAttributeVisitor {
131 public:
132   virtual void apply(int, osg::StateSet::RefAttributePair& refAttr)
133   {
134     osg::Texture2D* texture;
135     texture = dynamic_cast<osg::Texture2D*>(refAttr.first.get());
136     if (!texture)
137       return;
138     
139     osg::Image* image = texture->getImage(0);
140     if (!image)
141       return;
142
143     int s = image->s();
144     int t = image->t();
145
146     if (s <= t && 32 <= s) {
147       SGSceneFeatures::instance()->setTextureCompression(texture);
148     } else if (t < s && 32 <= t) {
149       SGSceneFeatures::instance()->setTextureCompression(texture);
150     }
151   }
152 };
153
154 class SGTexDataVarianceVisitor : public SGTextureStateAttributeVisitor {
155 public:
156   virtual void apply(int, osg::StateSet::RefAttributePair& refAttr)
157   {
158     osg::Texture* texture;
159     texture = dynamic_cast<osg::Texture*>(refAttr.first.get());
160     if (!texture)
161       return;
162     
163     texture->setDataVariance(osg::Object::STATIC);
164   }
165 };
166
167 class SGAcMaterialCrippleVisitor : public SGStateAttributeVisitor {
168 public:
169   virtual void apply(osg::StateSet::RefAttributePair& refAttr)
170   {
171     osg::Material* material;
172     material = dynamic_cast<osg::Material*>(refAttr.first.get());
173     if (!material)
174       return;
175     material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE);
176   }
177 };
178
179 class SGReadFileCallback :
180   public osgDB::Registry::ReadFileCallback {
181 public:
182   virtual osgDB::ReaderWriter::ReadResult
183   readImage(const std::string& fileName,
184             const osgDB::ReaderWriter::Options* opt)
185   {
186     std::string absFileName = osgDB::findDataFile(fileName);
187     if (!osgDB::fileExists(absFileName)) {
188       SG_LOG(SG_IO, SG_ALERT, "Cannot find image file \""
189              << fileName << "\"");
190       return osgDB::ReaderWriter::ReadResult::FILE_NOT_FOUND;
191     }
192
193     osgDB::Registry* registry = osgDB::Registry::instance();
194     osgDB::ReaderWriter::ReadResult res;
195     res = registry->readImageImplementation(absFileName, opt);
196     if (res.loadedFromCache())
197       SG_LOG(SG_IO, SG_INFO, "Returning cached image \""
198              << res.getImage()->getFileName() << "\"");
199     else
200       SG_LOG(SG_IO, SG_INFO, "Reading image \""
201              << res.getImage()->getFileName() << "\"");
202
203     return res;
204   }
205
206   virtual osgDB::ReaderWriter::ReadResult
207   readNode(const std::string& fileName,
208            const osgDB::ReaderWriter::Options* opt)
209   {
210     osgDB::Registry* registry = osgDB::Registry::instance();
211     osgDB::ReaderWriter::ReadResult res;
212     osg::Node* cached = 0;
213     // The BTG loader automatically looks for ".btg.gz" if a file with
214     // the .btg extension doesn't exist. Also, we don't want to add
215     // nodes, run the optimizer, etc. on the btg model.So, let it do
216     // its thing.
217     if (osgDB::equalCaseInsensitive(osgDB::getFileExtension(fileName), "btg")) {
218       return registry->readNodeImplementation(fileName, opt);
219     }
220     // First, look for a file with the same name, and the extension
221     // ".osg" and, if it exists, load it instead. This allows for
222     // substitution of optimized models for ones named in the scenery.
223     bool optimizeModel = true;
224     std::string fileSansExtension = osgDB::getNameLessExtension(fileName);
225     std::string osgFileName = fileSansExtension + ".osg";
226     std::string absFileName = osgDB::findDataFile(osgFileName);
227     if (osgDB::fileExists(absFileName)) {
228       optimizeModel = false;
229     } else {
230       absFileName = osgDB::findDataFile(fileName);
231     }
232     if (!osgDB::fileExists(absFileName)) {
233       SG_LOG(SG_IO, SG_ALERT, "Cannot find model file \""
234              << fileName << "\"");
235       return osgDB::ReaderWriter::ReadResult::FILE_NOT_FOUND;
236     }
237     cached
238         = dynamic_cast<osg::Node*>(registry->getFromObjectCache(absFileName));
239     if (cached) {
240       SG_LOG(SG_IO, SG_INFO, "Got cached model \""
241              << absFileName << "\"");
242     } else {
243       SG_LOG(SG_IO, SG_INFO, "Reading model \""
244              << absFileName << "\"");
245       res = registry->readNodeImplementation(absFileName, opt);
246       if (!res.validNode())
247         return res;
248
249       bool needTristrip = true;
250       if (osgDB::getLowerCaseFileExtension(fileName) == "ac") {
251         // we get optimal geometry from the loader.
252         needTristrip = false;
253         osg::Matrix m(1, 0, 0, 0,
254                       0, 0, 1, 0,
255                       0, -1, 0, 0,
256                       0, 0, 0, 1);
257         
258         osg::ref_ptr<osg::Group> root = new osg::Group;
259         osg::MatrixTransform* transform = new osg::MatrixTransform;
260         root->addChild(transform);
261         
262         transform->setDataVariance(osg::Object::STATIC);
263         transform->setMatrix(m);
264         transform->addChild(res.getNode());
265         
266         res = osgDB::ReaderWriter::ReadResult(0);
267
268         if (optimizeModel) {
269           osgUtil::Optimizer optimizer;
270           unsigned opts = osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS;
271           optimizer.optimize(root.get(), opts);
272         }
273
274         // strip away unneeded groups
275         if (root->getNumChildren() == 1 && root->getName().empty()) {
276           res = osgDB::ReaderWriter::ReadResult(root->getChild(0));
277         } else
278           res = osgDB::ReaderWriter::ReadResult(root.get());
279         
280         // Ok, this step is questionable.
281         // It is there to have the same visual appearance of ac objects for the
282         // first cut. Osg's ac3d loader will correctly set materials from the
283         // ac file. But the old plib loader used GL_AMBIENT_AND_DIFFUSE for the
284         // materials that in effect igored the ambient part specified in the
285         // file. We emulate that for the first cut here by changing all
286         // ac models here. But in the long term we should use the
287         // unchanged model and fix the input files instead ...
288         SGAcMaterialCrippleVisitor matCriple;
289         res.getNode()->accept(matCriple);
290       }
291
292       if (optimizeModel) {
293         osgUtil::Optimizer optimizer;
294         unsigned opts = 0;
295         // Don't use this one. It will break animation names ...
296         // opts |= osgUtil::Optimizer::REMOVE_REDUNDANT_NODES;
297
298         // opts |= osgUtil::Optimizer::REMOVE_LOADED_PROXY_NODES;
299         // opts |= osgUtil::Optimizer::COMBINE_ADJACENT_LODS;
300         // opts |= osgUtil::Optimizer::SHARE_DUPLICATE_STATE;
301         opts |= osgUtil::Optimizer::MERGE_GEOMETRY;
302         // opts |= osgUtil::Optimizer::CHECK_GEOMETRY;
303         // opts |= osgUtil::Optimizer::SPATIALIZE_GROUPS;
304         // opts |= osgUtil::Optimizer::COPY_SHARED_NODES;
305         opts |= osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS;
306         if (needTristrip)
307           opts |= osgUtil::Optimizer::TRISTRIP_GEOMETRY;
308         // opts |= osgUtil::Optimizer::TESSELATE_GEOMETRY;
309         // opts |= osgUtil::Optimizer::OPTIMIZE_TEXTURE_SETTINGS;
310         optimizer.optimize(res.getNode(), opts);
311       }
312       // Make sure the data variance of sharable objects is set to STATIC ...
313       SGTexDataVarianceVisitor dataVarianceVisitor;
314       res.getNode()->accept(dataVarianceVisitor);
315       // ... so that textures are now globally shared
316       registry->getSharedStateManager()->share(res.getNode());
317       
318       SGTexCompressionVisitor texComp;
319       res.getNode()->accept(texComp);
320       cached = res.getNode();
321       registry->addEntryToObjectCache(absFileName, cached);
322     }
323     // Add an extra reference to the model stored in the database.
324     // That it to avoid expiring the object from the cache even if it is still
325     // in use. Note that the object cache will think that a model is unused
326     // if the reference count is 1. If we clone all structural nodes here
327     // we need that extra reference to the original object
328     SGDatabaseReference* databaseReference;
329     databaseReference = new SGDatabaseReference(cached);
330     osg::CopyOp::CopyFlags flags = osg::CopyOp::DEEP_COPY_ALL;
331     flags &= ~osg::CopyOp::DEEP_COPY_TEXTURES;
332     flags &= ~osg::CopyOp::DEEP_COPY_IMAGES;
333     flags &= ~osg::CopyOp::DEEP_COPY_ARRAYS;
334     flags &= ~osg::CopyOp::DEEP_COPY_PRIMITIVES;
335     // This will safe display lists ...
336     flags &= ~osg::CopyOp::DEEP_COPY_DRAWABLES;
337     flags &= ~osg::CopyOp::DEEP_COPY_SHAPES;
338     res = osgDB::ReaderWriter::ReadResult(osg::CopyOp(flags)(cached));
339     res.getNode()->addObserver(databaseReference);
340
341     // Update liveries
342     SGTextureUpdateVisitor liveryUpdate(osgDB::getDataFilePathList());
343     res.getNode()->accept(liveryUpdate);
344
345     // Make sure the data variance of sharable objects is set to STATIC ...
346     SGTexDataVarianceVisitor dataVarianceVisitor;
347     res.getNode()->accept(dataVarianceVisitor);
348     // ... so that textures are now globally shared
349     registry->getOrCreateSharedStateManager()->share(res.getNode(), 0);
350
351     return res;
352   }
353 };
354
355 class SGReadCallbackInstaller {
356 public:
357   SGReadCallbackInstaller()
358   {
359     osg::Referenced::setThreadSafeReferenceCounting(true);
360
361     osgDB::Registry* registry = osgDB::Registry::instance();
362     osgDB::ReaderWriter::Options* options = new osgDB::ReaderWriter::Options;
363     // We manage node caching ourselves
364     int cacheOptions = osgDB::ReaderWriter::Options::CACHE_ALL
365       & ~osgDB::ReaderWriter::Options::CACHE_NODES;
366     options->
367       setObjectCacheHint((osgDB::ReaderWriter::Options::CacheHintOptions)cacheOptions);
368     registry->setOptions(options);
369     registry->getOrCreateSharedStateManager()->setShareMode(osgDB::SharedStateManager::SHARE_TEXTURES);
370     registry->setReadFileCallback(new SGReadFileCallback);
371   }
372 };
373
374 static SGReadCallbackInstaller readCallbackInstaller;
375
376 osg::Texture2D*
377 SGLoadTexture2D(const std::string& path, bool wrapu, bool wrapv, int)
378 {
379   osg::Image* image = osgDB::readImageFile(path);
380   osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
381   texture->setImage(image);
382   if (wrapu)
383     texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
384   else
385     texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP);
386   if (wrapv)
387     texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
388   else
389     texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP);
390
391   if (image) {
392     int s = image->s();
393     int t = image->t();
394
395     if (s <= t && 32 <= s) {
396       SGSceneFeatures::instance()->setTextureCompression(texture.get());
397     } else if (t < s && 32 <= t) {
398       SGSceneFeatures::instance()->setTextureCompression(texture.get());
399     }
400   }
401
402   // Make sure the texture is shared if we already have the same texture
403   // somewhere ...
404   {
405     osg::ref_ptr<osg::Node> tmpNode = new osg::Node;
406     osg::StateSet* stateSet = tmpNode->getOrCreateStateSet();
407     stateSet->setTextureAttribute(0, texture.get());
408
409     // OSGFIXME: don't forget that mutex here
410     osgDB::Registry* registry = osgDB::Registry::instance();
411     registry->getOrCreateSharedStateManager()->share(tmpNode.get(), 0);
412
413     // should be the same, but be paranoid ...
414     stateSet = tmpNode->getStateSet();
415     osg::StateAttribute* stateAttr;
416     stateAttr = stateSet->getTextureAttribute(0, osg::StateAttribute::TEXTURE);
417     osg::Texture2D* texture2D = dynamic_cast<osg::Texture2D*>(stateAttr);
418     if (texture2D)
419       texture = texture2D;
420   }
421
422   return texture.release();
423 }
424
425 class SGSwitchUpdateCallback : public osg::NodeCallback {
426 public:
427   SGSwitchUpdateCallback(SGCondition* condition) :
428     mCondition(condition) {}
429   virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
430   { 
431     assert(dynamic_cast<osg::Switch*>(node));
432     osg::Switch* s = static_cast<osg::Switch*>(node);
433
434     if (mCondition && mCondition->test()) {
435       s->setAllChildrenOn();
436       // note, callback is responsible for scenegraph traversal so
437       // should always include call traverse(node,nv) to ensure 
438       // that the rest of cullbacks and the scene graph are traversed.
439       traverse(node, nv);
440     } else
441       s->setAllChildrenOff();
442   }
443
444 private:
445   SGSharedPtr<SGCondition> mCondition;
446 };
447
448 \f
449 ////////////////////////////////////////////////////////////////////////
450 // Global functions.
451 ////////////////////////////////////////////////////////////////////////
452
453 osg::Node *
454 sgLoad3DModel( const string &fg_root, const string &path,
455                SGPropertyNode *prop_root,
456                double sim_time_sec, osg::Node *(*load_panel)(SGPropertyNode *),
457                SGModelData *data,
458                const SGPath& externalTexturePath )
459 {
460   osg::ref_ptr<osg::Node> model;
461   SGPropertyNode props;
462
463   // Load the 3D aircraft object itself
464   SGPath modelpath = path, texturepath = path;
465   if ( !ulIsAbsolutePathName( path.c_str() ) ) {
466     SGPath tmp = fg_root;
467     tmp.append(modelpath.str());
468     modelpath = texturepath = tmp;
469   }
470
471   // Check for an XML wrapper
472   if (modelpath.str().substr(modelpath.str().size() - 4, 4) == ".xml") {
473     readProperties(modelpath.str(), &props);
474     if (props.hasValue("/path")) {
475       modelpath = modelpath.dir();
476       modelpath.append(props.getStringValue("/path"));
477       if (props.hasValue("/texture-path")) {
478         texturepath = texturepath.dir();
479         texturepath.append(props.getStringValue("/texture-path"));
480       }
481     } else {
482       if (!model)
483         model = new osg::Switch;
484     }
485   }
486
487   osgDB::FilePathList pathList = osgDB::getDataFilePathList();
488   osgDB::Registry::instance()->initFilePathLists();
489
490   // Assume that textures are in
491   // the same location as the XML file.
492   if (!model) {
493     if (texturepath.extension() != "")
494           texturepath = texturepath.dir();
495
496     osgDB::Registry::instance()->getDataFilePathList().push_front(texturepath.str());
497
498     model = osgDB::readNodeFile(modelpath.str());
499     if (model == 0)
500       throw sg_io_exception("Failed to load 3D model", 
501                             sg_location(modelpath.str()));
502   }
503
504   osgDB::Registry::instance()->getDataFilePathList().push_front(externalTexturePath.str());
505
506   // Set up the alignment node
507   osg::ref_ptr<osg::MatrixTransform> alignmainmodel = new osg::MatrixTransform;
508   alignmainmodel->addChild(model.get());
509   osg::Matrix res_matrix;
510   res_matrix.makeRotate(
511     props.getFloatValue("/offsets/pitch-deg", 0.0)*SG_DEGREES_TO_RADIANS,
512     osg::Vec3(0, 1, 0),
513     props.getFloatValue("/offsets/roll-deg", 0.0)*SG_DEGREES_TO_RADIANS,
514     osg::Vec3(1, 0, 0),
515     props.getFloatValue("/offsets/heading-deg", 0.0)*SG_DEGREES_TO_RADIANS,
516     osg::Vec3(0, 0, 1));
517
518   osg::Matrix tmat;
519   tmat.makeTranslate(props.getFloatValue("/offsets/x-m", 0.0),
520                      props.getFloatValue("/offsets/y-m", 0.0),
521                      props.getFloatValue("/offsets/z-m", 0.0));
522   alignmainmodel->setMatrix(res_matrix*tmat);
523
524   // Load sub-models
525   vector<SGPropertyNode_ptr> model_nodes = props.getChildren("model");
526   for (unsigned i = 0; i < model_nodes.size(); i++) {
527     SGPropertyNode_ptr node = model_nodes[i];
528     osg::ref_ptr<osg::MatrixTransform> align = new osg::MatrixTransform;
529     res_matrix.makeIdentity();
530     res_matrix.makeRotate(
531       node->getDoubleValue("offsets/pitch-deg", 0.0)*SG_DEGREES_TO_RADIANS,
532       osg::Vec3(0, 1, 0),
533       node->getDoubleValue("offsets/roll-deg", 0.0)*SG_DEGREES_TO_RADIANS,
534       osg::Vec3(1, 0, 0),
535       node->getDoubleValue("offsets/heading-deg", 0.0)*SG_DEGREES_TO_RADIANS,
536       osg::Vec3(0, 0, 1));
537     
538     tmat.makeIdentity();
539     tmat.makeTranslate(node->getDoubleValue("offsets/x-m", 0),
540                        node->getDoubleValue("offsets/y-m", 0),
541                        node->getDoubleValue("offsets/z-m", 0));
542     align->setMatrix(res_matrix*tmat);
543
544     osg::ref_ptr<osg::Node> kid;
545     const char* submodel = node->getStringValue("path");
546     try {
547       kid = sgLoad3DModel( fg_root, submodel, prop_root, sim_time_sec, load_panel );
548
549     } catch (const sg_throwable &t) {
550       SG_LOG(SG_INPUT, SG_ALERT, "Failed to load submodel: " << t.getFormattedMessage());
551       throw;
552     }
553     align->addChild(kid.get());
554
555     align->setName(node->getStringValue("name", ""));
556
557     SGPropertyNode *cond = node->getNode("condition", false);
558     if (cond) {
559       osg::ref_ptr<osg::Switch> sw = new osg::Switch;
560       sw->setUpdateCallback(new SGSwitchUpdateCallback(sgReadCondition(prop_root, cond)));
561       alignmainmodel->addChild(sw.get());
562       sw->addChild(align.get());
563       sw->setName("submodel condition switch");
564     } else {
565       alignmainmodel->addChild(align.get());
566     }
567   }
568
569   if ( load_panel ) {
570     // Load panels
571     vector<SGPropertyNode_ptr> panel_nodes = props.getChildren("panel");
572     for (unsigned i = 0; i < panel_nodes.size(); i++) {
573         SG_LOG(SG_INPUT, SG_DEBUG, "Loading a panel");
574         osg::ref_ptr<osg::Node> panel = load_panel(panel_nodes[i]);
575         if (panel_nodes[i]->hasValue("name"))
576             panel->setName((char *)panel_nodes[i]->getStringValue("name"));
577         alignmainmodel->addChild(panel.get());
578     }
579   }
580
581   if (data) {
582     alignmainmodel->setUserData(data);
583     data->modelLoaded(path, &props, alignmainmodel.get());
584   }
585
586   std::vector<SGPropertyNode_ptr> animation_nodes;
587   animation_nodes = props.getChildren("animation");
588   for (unsigned i = 0; i < animation_nodes.size(); ++i)
589     /// OSGFIXME: duh, why not only model?????
590     SGAnimation::animate(alignmainmodel.get(), animation_nodes[i], prop_root);
591
592   // restore old path list
593   osgDB::setDataFilePathList(pathList);
594
595   if (props.hasChild("debug-outfile")) {
596     std::string outputfile = props.getStringValue("debug-outfile",
597                                                   "debug-model.osg");
598     osgDB::writeNodeFile(*alignmainmodel, outputfile);
599   }
600
601   return alignmainmodel.release();
602 }
603
604 // end of model.cxx