1 // model.cxx - manage a 3D aircraft model.
2 // Written by David Megginson, started 2002.
4 // This file is in the Public Domain, and comes with no warranty.
7 #include <simgear_config.h>
10 #include <osg/observer_ptr>
11 #include <osg/ref_ptr>
13 #include <osg/NodeCallback>
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>
25 #include <simgear/scene/util/SGStateAttributeVisitor.hxx>
26 #include <simgear/scene/util/SGTextureStateAttributeVisitor.hxx>
28 #include <simgear/structure/exception.hxx>
29 #include <simgear/props/props.hxx>
30 #include <simgear/props/props_io.hxx>
31 #include <simgear/props/condition.hxx>
33 #include "animation.hxx"
39 // Little helper class that holds an extra reference to a
41 // Since we clone all structural nodes from our 3d models,
42 // the database pager will only see one single reference to
43 // top node of the model and expire it relatively fast.
44 // We attach that extra reference to every model cloned from
45 // a base model in the pager. When that cloned model is deleted
46 // this extra reference is deleted too. So if there are no
47 // cloned models left the model will expire.
48 class SGDatabaseReference : public osg::Observer {
50 SGDatabaseReference(osg::Referenced* referenced) :
51 mReferenced(referenced)
53 virtual void objectDeleted(void*)
58 osg::ref_ptr<osg::Referenced> mReferenced;
62 class SGTextureUpdateVisitor : public SGTextureStateAttributeVisitor {
64 SGTextureUpdateVisitor(const osgDB::FilePathList& pathList) :
67 osg::Texture2D* textureReplace(int unit,
68 osg::StateSet::RefAttributePair& refAttr)
70 osg::Texture2D* texture;
71 texture = dynamic_cast<osg::Texture2D*>(refAttr.first.get());
75 osg::ref_ptr<osg::Image> image = texture->getImage(0);
79 // The currently loaded file name
80 std::string fullFilePath = image->getFileName();
82 std::string fileName = osgDB::getSimpleFileName(fullFilePath);
83 // The name that should be found with the current database path
84 std::string fullLiveryFile = osgDB::findFileInPath(fileName, mPathList);
85 // If they are identical then there is nothing to do
86 if (fullLiveryFile == fullFilePath)
89 image = osgDB::readImageFile(fullLiveryFile);
93 osg::CopyOp copyOp(osg::CopyOp::DEEP_COPY_ALL &
94 ~osg::CopyOp::DEEP_COPY_IMAGES);
95 texture = static_cast<osg::Texture2D*>(copyOp(texture));
98 texture->setImage(image.get());
101 virtual void apply(osg::StateSet* stateSet)
106 // get a copy that we can safely modify the statesets values.
107 osg::StateSet::TextureAttributeList attrList;
108 attrList = stateSet->getTextureAttributeList();
109 for (unsigned unit = 0; unit < attrList.size(); ++unit) {
110 osg::StateSet::AttributeList::iterator i;
111 i = attrList[unit].begin();
112 while (i != attrList[unit].end()) {
113 osg::Texture2D* texture = textureReplace(unit, i->second);
115 stateSet->removeTextureAttribute(unit, i->second.first.get());
116 stateSet->setTextureAttribute(unit, texture, i->second.second);
117 stateSet->setTextureMode(unit, GL_TEXTURE_2D,
118 osg::StateAttribute::ON);
126 osgDB::FilePathList mPathList;
129 class SGTexCompressionVisitor : public SGTextureStateAttributeVisitor {
131 SGTexCompressionVisitor(osg::Texture::InternalFormatMode formatMode) :
132 mFormatMode(formatMode)
135 virtual void apply(int, osg::StateSet::RefAttributePair& refAttr)
137 osg::Texture2D* texture;
138 texture = dynamic_cast<osg::Texture2D*>(refAttr.first.get());
142 osg::Image* image = texture->getImage(0);
148 if (s <= t && 32 <= s) {
149 texture->setInternalFormatMode(mFormatMode);
150 } else if (t < s && 32 <= t) {
151 texture->setInternalFormatMode(mFormatMode);
156 osg::Texture::InternalFormatMode mFormatMode;
159 class SGAcMaterialCrippleVisitor : public SGStateAttributeVisitor {
161 virtual void apply(osg::StateSet::RefAttributePair& refAttr)
163 osg::Material* material;
164 material = dynamic_cast<osg::Material*>(refAttr.first.get());
167 material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE);
171 class SGReadFileCallback :
172 public osgDB::Registry::ReadFileCallback {
174 virtual osgDB::ReaderWriter::ReadResult
175 readImage(const std::string& fileName,
176 const osgDB::ReaderWriter::Options* opt)
178 std::string absFileName = osgDB::findDataFile(fileName);
179 if (!osgDB::fileExists(absFileName)) {
180 SG_LOG(SG_IO, SG_ALERT, "Cannot find image file \""
181 << fileName << "\"");
182 return osgDB::ReaderWriter::ReadResult::FILE_NOT_FOUND;
185 osgDB::Registry* registry = osgDB::Registry::instance();
186 osgDB::ReaderWriter::ReadResult res;
187 res = registry->readImageImplementation(absFileName, opt);
188 if (res.loadedFromCache())
189 SG_LOG(SG_IO, SG_INFO, "Returning cached image \""
190 << res.getImage()->getFileName() << "\"");
192 SG_LOG(SG_IO, SG_INFO, "Reading image \""
193 << res.getImage()->getFileName() << "\"");
198 virtual osgDB::ReaderWriter::ReadResult
199 readNode(const std::string& fileName,
200 const osgDB::ReaderWriter::Options* opt)
202 std::string absFileName = osgDB::findDataFile(fileName);
203 if (!osgDB::fileExists(absFileName)) {
204 SG_LOG(SG_IO, SG_ALERT, "Cannot find model file \""
205 << fileName << "\"");
206 return osgDB::ReaderWriter::ReadResult::FILE_NOT_FOUND;
209 osgDB::Registry* registry = osgDB::Registry::instance();
210 osgDB::ReaderWriter::ReadResult res;
211 res = registry->readNodeImplementation(absFileName, opt);
212 if (!res.validNode())
215 if (res.loadedFromCache()) {
216 SG_LOG(SG_IO, SG_INFO, "Returning cached model \""
217 << absFileName << "\"");
219 SG_LOG(SG_IO, SG_INFO, "Reading model \""
220 << absFileName << "\"");
222 bool needTristrip = true;
223 if (osgDB::getLowerCaseFileExtension(absFileName) == "ac") {
224 // we get optimal geometry from the loader.
225 needTristrip = false;
226 osg::Matrix m(1, 0, 0, 0,
231 osg::ref_ptr<osg::Group> root = new osg::Group;
232 osg::MatrixTransform* transform = new osg::MatrixTransform;
233 root->addChild(transform);
235 transform->setDataVariance(osg::Object::STATIC);
236 transform->setMatrix(m);
237 transform->addChild(res.getNode());
239 res = osgDB::ReaderWriter::ReadResult(0);
241 osgUtil::Optimizer optimizer;
242 unsigned opts = osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS;
243 optimizer.optimize(root.get(), opts);
245 // strip away unneeded groups
246 if (root->getNumChildren() == 1 && root->getName().empty()) {
247 res = osgDB::ReaderWriter::ReadResult(root->getChild(0));
249 res = osgDB::ReaderWriter::ReadResult(root.get());
251 // Ok, this step is questionable.
252 // It is there to have the same visual appearance of ac objects for the
253 // first cut. Osg's ac3d loader will correctly set materials from the
254 // ac file. But the old plib loader used GL_AMBIENT_AND_DIFFUSE for the
255 // materials that in effect igored the ambient part specified in the
256 // file. We emulate that for the first cut here by changing all
257 // ac models here. But in the long term we should use the
258 // unchanged model and fix the input files instead ...
259 SGAcMaterialCrippleVisitor matCriple;
260 res.getNode()->accept(matCriple);
263 osgUtil::Optimizer optimizer;
265 // Don't use this one. It will break animation names ...
266 // opts |= osgUtil::Optimizer::REMOVE_REDUNDANT_NODES;
268 // opts |= osgUtil::Optimizer::REMOVE_LOADED_PROXY_NODES;
269 // opts |= osgUtil::Optimizer::COMBINE_ADJACENT_LODS;
270 // opts |= osgUtil::Optimizer::SHARE_DUPLICATE_STATE;
271 opts |= osgUtil::Optimizer::MERGE_GEOMETRY;
272 // opts |= osgUtil::Optimizer::CHECK_GEOMETRY;
273 // opts |= osgUtil::Optimizer::SPATIALIZE_GROUPS;
274 // opts |= osgUtil::Optimizer::COPY_SHARED_NODES;
275 opts |= osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS;
277 opts |= osgUtil::Optimizer::TRISTRIP_GEOMETRY;
278 // opts |= osgUtil::Optimizer::TESSELATE_GEOMETRY;
279 opts |= osgUtil::Optimizer::OPTIMIZE_TEXTURE_SETTINGS;
280 optimizer.optimize(res.getNode(), opts);
283 registry->getSharedStateManager()->share(res.getNode());
285 // OSGFIXME: guard that with a flag
286 // OSGFIXME: in the long term it is unclear if we have an OpenGL
288 osg::Texture::Extensions* e = osg::Texture::getExtensions(0, true);
289 if (e->isTextureCompressionARBSupported()) {
290 SGTexCompressionVisitor texComp(osg::Texture::USE_ARB_COMPRESSION);
291 res.getNode()->accept(texComp);
292 } else if (e->isTextureCompressionS3TCSupported()) {
293 SGTexCompressionVisitor texComp(osg::Texture::USE_S3TC_DXT5_COMPRESSION);
294 res.getNode()->accept(texComp);
298 // Add an extra reference to the model stored in the database.
299 // That it to avoid expiring the object from the cache even if it is still
300 // in use. Note that the object cache will think that a model is unused
301 // if the reference count is 1. If we clone all structural nodes here
302 // we need that extra reference to the original object
303 SGDatabaseReference* databaseReference;
304 databaseReference = new SGDatabaseReference(res.getNode());
305 osg::CopyOp::CopyFlags flags = osg::CopyOp::DEEP_COPY_ALL;
306 flags &= ~osg::CopyOp::DEEP_COPY_TEXTURES;
307 flags &= ~osg::CopyOp::DEEP_COPY_IMAGES;
308 flags &= ~osg::CopyOp::DEEP_COPY_ARRAYS;
309 flags &= ~osg::CopyOp::DEEP_COPY_PRIMITIVES;
310 // This will safe display lists ...
311 flags &= ~osg::CopyOp::DEEP_COPY_DRAWABLES;
312 flags &= ~osg::CopyOp::DEEP_COPY_SHAPES;
313 res = osgDB::ReaderWriter::ReadResult(osg::CopyOp(flags)(res.getNode()));
314 res.getNode()->addObserver(databaseReference);
316 SGTextureUpdateVisitor liveryUpdate(osgDB::getDataFilePathList());
317 res.getNode()->accept(liveryUpdate);
319 // OSGFIXME: don't forget that mutex here
320 registry->getOrCreateSharedStateManager()->share(res.getNode(), 0);
326 class SGReadCallbackInstaller {
328 SGReadCallbackInstaller()
330 osg::Referenced::setThreadSafeReferenceCounting(true);
332 osgDB::Registry* registry = osgDB::Registry::instance();
333 osgDB::ReaderWriter::Options* options = new osgDB::ReaderWriter::Options;
334 options->setObjectCacheHint(osgDB::ReaderWriter::Options::CACHE_ALL);
335 registry->setOptions(options);
336 registry->getOrCreateSharedStateManager()->setShareMode(osgDB::SharedStateManager::SHARE_TEXTURES);
337 registry->setReadFileCallback(new SGReadFileCallback);
341 static SGReadCallbackInstaller readCallbackInstaller;
344 SGLoadTexture2D(const std::string& path, bool wrapu, bool wrapv, int)
346 osg::Image* image = osgDB::readImageFile(path);
347 osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
348 texture->setImage(image);
350 texture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
352 texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP);
354 texture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
356 texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP);
362 // OSGFIXME: guard with a flag
363 if (osg::Texture::getExtensions(0, true)->isTextureCompressionARBSupported()) {
364 if (s <= t && 32 <= s) {
365 texture->setInternalFormatMode(osg::Texture::USE_ARB_COMPRESSION);
366 } else if (t < s && 32 <= t) {
367 texture->setInternalFormatMode(osg::Texture::USE_ARB_COMPRESSION);
369 } else if (osg::Texture::getExtensions(0, true)->isTextureCompressionS3TCSupported()) {
370 if (s <= t && 32 <= s) {
371 texture->setInternalFormatMode(osg::Texture::USE_S3TC_DXT5_COMPRESSION);
372 } else if (t < s && 32 <= t) {
373 texture->setInternalFormatMode(osg::Texture::USE_S3TC_DXT5_COMPRESSION);
378 // Make sure the texture is shared if we already have the same texture
381 osg::ref_ptr<osg::Node> tmpNode = new osg::Node;
382 osg::StateSet* stateSet = tmpNode->getOrCreateStateSet();
383 stateSet->setTextureAttribute(0, texture.get());
385 // OSGFIXME: don't forget that mutex here
386 osgDB::Registry* registry = osgDB::Registry::instance();
387 registry->getOrCreateSharedStateManager()->share(tmpNode.get(), 0);
389 // should be the same, but be paranoid ...
390 stateSet = tmpNode->getStateSet();
391 osg::StateAttribute* stateAttr;
392 stateAttr = stateSet->getTextureAttribute(0, osg::StateAttribute::TEXTURE);
393 osg::Texture2D* texture2D = dynamic_cast<osg::Texture2D*>(stateAttr);
398 return texture.release();
401 class SGSwitchUpdateCallback : public osg::NodeCallback {
403 SGSwitchUpdateCallback(SGCondition* condition) :
404 mCondition(condition) {}
405 virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
407 assert(dynamic_cast<osg::Switch*>(node));
408 osg::Switch* s = static_cast<osg::Switch*>(node);
410 if (mCondition && mCondition->test()) {
411 s->setAllChildrenOn();
412 // note, callback is responsible for scenegraph traversal so
413 // should always include call traverse(node,nv) to ensure
414 // that the rest of cullbacks and the scene graph are traversed.
417 s->setAllChildrenOff();
421 SGSharedPtr<SGCondition> mCondition;
425 ////////////////////////////////////////////////////////////////////////
427 ////////////////////////////////////////////////////////////////////////
430 sgLoad3DModel( const string &fg_root, const string &path,
431 SGPropertyNode *prop_root,
432 double sim_time_sec, osg::Node *(*load_panel)(SGPropertyNode *),
434 const SGPath& externalTexturePath )
436 osg::Node* model = 0;
437 SGPropertyNode props;
439 // Load the 3D aircraft object itself
440 SGPath modelpath = path, texturepath = path;
441 if ( !ulIsAbsolutePathName( path.c_str() ) ) {
442 SGPath tmp = fg_root;
443 tmp.append(modelpath.str());
444 modelpath = texturepath = tmp;
447 // Check for an XML wrapper
448 if (modelpath.str().substr(modelpath.str().size() - 4, 4) == ".xml") {
449 readProperties(modelpath.str(), &props);
450 if (props.hasValue("/path")) {
451 modelpath = modelpath.dir();
452 modelpath.append(props.getStringValue("/path"));
453 if (props.hasValue("/texture-path")) {
454 texturepath = texturepath.dir();
455 texturepath.append(props.getStringValue("/texture-path"));
459 model = new osg::Switch;
463 osgDB::FilePathList pathList = osgDB::getDataFilePathList();
464 osgDB::Registry::instance()->initFilePathLists();
466 // Assume that textures are in
467 // the same location as the XML file.
469 if (texturepath.extension() != "")
470 texturepath = texturepath.dir();
472 osgDB::Registry::instance()->getDataFilePathList().push_front(texturepath.str());
474 model = osgDB::readNodeFile(modelpath.str());
476 throw sg_io_exception("Failed to load 3D model",
477 sg_location(modelpath.str()));
480 osgDB::Registry::instance()->getDataFilePathList().push_front(externalTexturePath.str());
482 // Set up the alignment node
483 osg::MatrixTransform* alignmainmodel = new osg::MatrixTransform;
484 alignmainmodel->addChild(model);
485 osg::Matrix res_matrix;
486 res_matrix.makeRotate(
487 props.getFloatValue("/offsets/pitch-deg", 0.0)*SG_DEGREES_TO_RADIANS,
489 props.getFloatValue("/offsets/roll-deg", 0.0)*SG_DEGREES_TO_RADIANS,
491 props.getFloatValue("/offsets/heading-deg", 0.0)*SG_DEGREES_TO_RADIANS,
495 tmat.makeTranslate(props.getFloatValue("/offsets/x-m", 0.0),
496 props.getFloatValue("/offsets/y-m", 0.0),
497 props.getFloatValue("/offsets/z-m", 0.0));
498 alignmainmodel->setMatrix(res_matrix*tmat);
501 vector<SGPropertyNode_ptr> model_nodes = props.getChildren("model");
502 for (unsigned i = 0; i < model_nodes.size(); i++) {
503 SGPropertyNode_ptr node = model_nodes[i];
504 osg::MatrixTransform* align = new osg::MatrixTransform;
505 res_matrix.makeIdentity();
506 res_matrix.makeRotate(
507 node->getDoubleValue("offsets/pitch-deg", 0.0)*SG_DEGREES_TO_RADIANS,
509 node->getDoubleValue("offsets/roll-deg", 0.0)*SG_DEGREES_TO_RADIANS,
511 node->getDoubleValue("offsets/heading-deg", 0.0)*SG_DEGREES_TO_RADIANS,
515 tmat.makeTranslate(node->getDoubleValue("offsets/x-m", 0),
516 node->getDoubleValue("offsets/y-m", 0),
517 node->getDoubleValue("offsets/z-m", 0));
518 align->setMatrix(res_matrix*tmat);
521 const char* submodel = node->getStringValue("path");
523 kid = sgLoad3DModel( fg_root, submodel, prop_root, sim_time_sec, load_panel );
525 } catch (const sg_throwable &t) {
526 SG_LOG(SG_INPUT, SG_ALERT, "Failed to load submodel: " << t.getFormattedMessage());
529 align->addChild(kid);
531 align->setName(node->getStringValue("name", ""));
533 SGPropertyNode *cond = node->getNode("condition", false);
535 osg::Switch* sw = new osg::Switch;
536 sw->setUpdateCallback(new SGSwitchUpdateCallback(sgReadCondition(prop_root, cond)));
537 alignmainmodel->addChild(sw);
539 sw->setName("submodel condition switch");
541 alignmainmodel->addChild(align);
547 vector<SGPropertyNode_ptr> panel_nodes = props.getChildren("panel");
548 for (unsigned i = 0; i < panel_nodes.size(); i++) {
549 SG_LOG(SG_INPUT, SG_DEBUG, "Loading a panel");
550 osg::Node * panel = load_panel(panel_nodes[i]);
551 if (panel_nodes[i]->hasValue("name"))
552 panel->setName((char *)panel_nodes[i]->getStringValue("name"));
553 alignmainmodel->addChild(panel);
558 alignmainmodel->setUserData(data);
559 data->modelLoaded(path, &props, alignmainmodel);
562 std::vector<SGPropertyNode_ptr> animation_nodes;
563 animation_nodes = props.getChildren("animation");
564 for (unsigned i = 0; i < animation_nodes.size(); ++i)
565 /// OSGFIXME: duh, why not only model?????
566 SGAnimation::animate(alignmainmodel, animation_nodes[i], prop_root);
568 // restore old path list
569 osgDB::setDataFilePathList(pathList);
571 if (props.hasChild("debug-outfile")) {
572 std::string outputfile = props.getStringValue("debug-outfile",
574 osgDB::writeNodeFile(*alignmainmodel, outputfile);
577 return alignmainmodel;