1 // stgmerge.cxx -- combined STG models
3 // Copyright (C) 2015 Stuart Buchanan
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License as
7 // published by the Free Software Foundation; either version 2 of the
8 // License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful, but
11 // WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
29 #include <osg/MatrixTransform>
30 #include <osg/ArgumentParser>
32 #include <osgViewer/Viewer>
33 #include <osgViewer/ViewerEventHandlers>
35 #include <osgGA/StateSetManipulator>
36 #include <osgGA/TrackballManipulator>
38 #include <osgDB/FileNameUtils>
39 #include <osgDB/FileUtils>
40 #include <osgDB/Registry>
41 #include <osgDB/ReaderWriter>
42 #include <osgDB/ReadFile>
43 #include <osgDB/WriteFile>
45 #include <osgUtil/Optimizer>
47 #include <simgear/bucket/newbucket.hxx>
48 #include <simgear/bvh/BVHNode.hxx>
49 #include <simgear/bvh/BVHLineSegmentVisitor.hxx>
50 #include <simgear/bvh/BVHPager.hxx>
51 #include <simgear/bvh/BVHPageNode.hxx>
52 #include <simgear/debug/logstream.hxx>
53 #include <simgear/math/SGGeodesy.hxx>
54 #include <simgear/misc/sg_path.hxx>
55 #include <simgear/misc/sgstream.hxx>
56 #include <simgear/misc/ResourceManager.hxx>
57 #include <simgear/props/props.hxx>
58 #include <simgear/props/props_io.hxx>
59 #include <simgear/scene/material/matlib.hxx>
60 #include <simgear/scene/model/BVHPageNodeOSG.hxx>
61 #include <simgear/scene/model/ModelRegistry.hxx>
62 #include <simgear/scene/tgdb/apt_signs.hxx>
63 #include <simgear/scene/tgdb/userdata.hxx>
64 #include <simgear/scene/util/OptionsReadFileCallback.hxx>
65 #include <simgear/scene/util/OsgMath.hxx>
66 #include <simgear/scene/util/RenderConstants.hxx>
67 #include <simgear/scene/util/SGReaderWriterOptions.hxx>
69 namespace sg = simgear;
71 struct _ObjectStatic {
73 _lon(0), _lat(0), _elev(0), _hdg(0), _pitch(0), _roll(0), _shared(false) {
77 double _lon, _lat, _elev;
78 double _hdg, _pitch, _roll;
80 osg::ref_ptr<sg::SGReaderWriterOptions> _options;
84 std::string fg_scenery;
87 bool display_viewer = false;
88 bool osg_optimizer = false;
89 bool copy_files = false;
90 int group_size = 5000;
92 sg::SGReaderWriterOptions* staticOptions(const std::string& filePath,
93 const osgDB::Options* options) {
94 osg::ref_ptr<sg::SGReaderWriterOptions> staticOptions;
95 staticOptions = sg::SGReaderWriterOptions::copyOrCreate(options);
96 staticOptions->getDatabasePathList().clear();
98 staticOptions->getDatabasePathList().push_back(filePath);
99 staticOptions->setObjectCacheHint(osgDB::Options::CACHE_NONE);
101 // Every model needs its own SGModelData to ensure load/unload is
103 staticOptions->setModelData(
104 staticOptions->getModelData() ?
105 staticOptions->getModelData()->clone() : 0);
107 return staticOptions.release();
110 sg::SGReaderWriterOptions* sharedOptions(const std::string& filePath,
111 const osgDB::Options* options) {
112 osg::ref_ptr<sg::SGReaderWriterOptions> sharedOptions;
113 sharedOptions = sg::SGReaderWriterOptions::copyOrCreate(options);
114 sharedOptions->getDatabasePathList().clear();
116 SGPath path = filePath;
120 sharedOptions->getDatabasePathList().push_back(path.str());
122 // ensure Models directory synced via TerraSync is searched before the copy in
123 // FG_ROOT, so that updated models can be used.
124 std::string terrasync_root = options->getPluginStringData(
125 "SimGear::TERRASYNC_ROOT");
126 if (!terrasync_root.empty()) {
127 sharedOptions->getDatabasePathList().push_back(terrasync_root);
130 std::string fg_root = options->getPluginStringData("SimGear::FG_ROOT");
131 sharedOptions->getDatabasePathList().push_back(fg_root);
133 // TODO how should we handle this for OBJECT_SHARED?
134 sharedOptions->setModelData(
135 sharedOptions->getModelData() ?
136 sharedOptions->getModelData()->clone() : 0);
138 return sharedOptions.release();
141 int processSTG(osg::ref_ptr<simgear::SGReaderWriterOptions> options,
145 SG_LOG(SG_TERRAIN, SG_ALERT, "STG file empty");
149 SG_LOG(SG_TERRAIN, SG_ALERT, "Loading stg file " << stg);
151 sg_gzifstream stream(stg);
152 if (!stream.is_open()) {
153 SG_LOG(SG_TERRAIN, SG_ALERT, "Unable to open STG file " << stg);
157 // Extract the bucket from the filename
158 std::istringstream ss(osgDB::getStrippedName(stg));
162 SG_LOG(SG_TERRAIN, SG_ALERT,
163 "Unable to determine bucket from STG filename " << ss);
167 SGBucket bucket = SGBucket(index);
169 // We will group the object into group_size x group_size
170 // block, so work out the number and dimensions in degrees
172 int lon_blocks = (int) ceil(bucket.get_width_m() / group_size);
173 int lat_blocks = (int) ceil(bucket.get_height_m() / group_size);
174 float lat_delta = bucket.get_height() / lat_blocks;
175 float lon_delta = bucket.get_width() / lon_blocks;
176 SG_LOG(SG_TERRAIN, SG_ALERT,
177 "Splitting into (" << lon_blocks << "," << lat_blocks << ") blocks"
178 << "of size (" << lon_delta << "," << lat_delta << ") degrees\n");
180 std::list<_ObjectStatic> _objectStaticList[lon_blocks][lat_blocks];
182 std::string filePath = osgDB::getFilePath(stg);
184 // Write out the STG files.
186 std::string outpath = SGPath(SGPath(output), stgpath.file()).c_str();
187 SG_LOG(SG_TERRAIN, SG_ALERT, "Writing to " << outpath);
188 std::ofstream stgout(outpath.c_str(), std::ofstream::out);
190 if (!stgout.is_open()) {
191 SG_LOG(SG_TERRAIN, SG_ALERT,
192 "Unable to open STG file to write " << outpath);
196 while (!stream.eof()) {
199 std::getline(stream, line);
202 std::string::size_type hash_pos = line.find('#');
205 stgout << line << "\n";
207 if (hash_pos != std::string::npos)
208 line.resize(hash_pos);
210 // and process further
211 std::stringstream in(line);
220 // Then there is always a name
224 SGPath path = filePath;
227 // The following tokens are ignored
228 // OBJECT_BASE - base scenery, not relevant
229 // OBJECT - airport BTG files
230 // OBJECT_STATIC_AGL - elevation needs to be calculated at runtime
231 // OBJECT_SHARED_AGL - elevation needs to be calculated at runtime
232 // OBJECT_SIGN - depends on materials library, which is runtime dependent
234 if ((token == "OBJECT_STATIC") || (token == "OBJECT_SHARED")) {
237 osg::ref_ptr<sg::SGReaderWriterOptions> opt;
239 if (token == "OBJECT_STATIC") {
240 opt = staticOptions(filePath, options);
241 //if (SGPath(name).lower_extension() == "ac")
242 //opt->setInstantiateEffects(true); // Is this output correctly?
244 } else if (token == "OBJECT_SHARED") {
245 opt = sharedOptions(filePath, options);
246 //if (SGPath(name).lower_extension() == "ac")
247 //opt->setInstantiateEffects(true); // Is this output correctly?
250 SG_LOG(SG_TERRAIN, SG_ALERT, "Broken code - unexpected object '" << token << "'");
256 in >> obj._lon >> obj._lat >> obj._elev >> obj._hdg >> obj._pitch
259 // Determine the correct bucket for this object.
260 int x = (int) floor((obj._lon - bucket.get_corner(0).getLongitudeDeg()) / lon_delta);
261 int y = (int) floor((obj._lat - bucket.get_corner(0).getLatitudeDeg()) / lat_delta);
262 SG_LOG(SG_TERRAIN, SG_INFO,
263 "Assigned (" << obj._lon << "," << obj._lat << ") to block (" << x << "," << y << ")");
265 _objectStaticList[x][y].push_back(obj);
267 SG_LOG(SG_TERRAIN, SG_ALERT, "Ignoring token '" << token << "'");
268 stgout << line << "\n";
274 for (int x = 0; x < lon_blocks; ++x) {
275 for (int y = 0; y < lat_blocks; ++y) {
277 if (_objectStaticList[x][y].size() == 0) {
278 // Nothing to do, so skip
282 //SG_LOG(SG_TERRAIN, SG_ALERT, "Object files " << _objectStaticList[x][y].size());
284 osg::ref_ptr<osg::Group> group = new osg::Group;
285 group->setName("STG merge");
286 group->setDataVariance(osg::Object::STATIC);
287 int files_loaded = 0;
289 // Calculate center of this block
290 const SGGeod center = SGGeod::fromDegM(
291 bucket.get_center_lon() - 0.5 * bucket.get_width() + (double) ((x +0.5) * lon_delta),
292 bucket.get_center_lat() - 0.5 * bucket.get_height() + (double) ((y + 0.5) * lat_delta),
295 //SG_LOG(SG_TERRAIN, SG_ALERT,
296 // "Center of block: " << center.getLongitudeDeg() << ", " << center.getLatitudeDeg());
298 // Inverse used to translate individual matrices
300 SGGeodesy::SGGeodToCart(center , shift);
302 for (std::list<_ObjectStatic>::iterator i = _objectStaticList[x][y].begin();
303 i != _objectStaticList[x][y].end(); ++i) {
305 // We don't process XML files, as they typically include animations which we can't output
307 if (SGPath(i->_name).lower_extension() == "xml") {
308 //SG_LOG(SG_TERRAIN, SG_ALERT, "Ignoring non-static "
309 // << i->_token << " '" << i->_name << "'");
310 std::cout << (i->_shared ? "OBJECT_SHARED " : "OBJECT_STATIC ");
311 std::cout << i->_name << " " << i->_lon << " " << i->_lat << " " << i->_elev << " " << i->_hdg;
312 std::cout << " " << i->_pitch << i->_roll << "\n";
317 SG_LOG(SG_TERRAIN, SG_INFO, "Processing " << i->_name);
319 osg::ref_ptr<osg::Node> node;
320 node = osgDB::readRefNodeFile(i->_name, i->_options.get());
322 SG_LOG(SG_TERRAIN, SG_ALERT,
323 stg << ": Failed to load " << i->_token << " '" << i->_name << "'");
328 if (SGPath(i->_name).lower_extension() == "ac")
329 node->setNodeMask(~sg::MODELLIGHT_BIT);
331 const SGGeod q = SGGeod::fromDegM(i->_lon, i->_lat, i->_elev);
333 SGGeodesy::SGGeodToCart(q, coord);
334 coord = coord - shift;
336 // Create an matrix to convert from global coordinates to the
337 // Z-Up local coordinate system used by scenery models.
338 // This is simply the inverse of the normal scenery model
340 osg::Matrix m = makeZUpFrameRelative(center);
341 osg::Matrix inv = osg::Matrix::inverse(m);
342 osg::Vec3f v = toOsg(coord) * inv;
346 matrix.preMultRotate(
347 osg::Quat(SGMiscd::deg2rad(i->_hdg), osg::Vec3(0, 0, 1)));
348 matrix.preMultRotate(
349 osg::Quat(SGMiscd::deg2rad(i->_pitch), osg::Vec3(0, 1, 0)));
350 matrix.preMultRotate(
351 osg::Quat(SGMiscd::deg2rad(i->_roll), osg::Vec3(1, 0, 0)));
353 osg::MatrixTransform* matrixTransform;
354 matrixTransform = new osg::MatrixTransform(matrix);
355 matrixTransform->setName("positionStaticObject");
356 matrixTransform->setDataVariance(osg::Object::STATIC);
357 matrixTransform->addChild(node.get());
359 // Shift the models so they are centered on the center of the block.
360 // We will place the object at the right position in the tile later.
361 group->addChild(matrixTransform);
364 osgViewer::Viewer viewer;
366 if (osg_optimizer || display_viewer) {
367 // Create a viewer - required for some Optimizers and if we are to display
369 viewer.setSceneData(group.get());
370 viewer.addEventHandler(new osgViewer::StatsHandler);
371 viewer.addEventHandler(new osgViewer::WindowSizeHandler);
372 viewer.addEventHandler(
373 new osgGA::StateSetManipulator(
374 viewer.getCamera()->getOrCreateStateSet()));
375 viewer.setCameraManipulator(new osgGA::TrackballManipulator());
381 osgUtil::Optimizer optimizer;
383 // See osgUtil::Optimizer for list of optimizations available.
384 //optimizer.optimize(group, osgUtil::Optimizer::ALL_OPTIMIZATIONS);
385 int optimizationOptions = osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS
386 | osgUtil::Optimizer::REMOVE_REDUNDANT_NODES
387 | osgUtil::Optimizer::COMBINE_ADJACENT_LODS
388 | osgUtil::Optimizer::SHARE_DUPLICATE_STATE
389 | osgUtil::Optimizer::MERGE_GEOMETRY
390 | osgUtil::Optimizer::MAKE_FAST_GEOMETRY
391 | osgUtil::Optimizer::SPATIALIZE_GROUPS
392 | osgUtil::Optimizer::OPTIMIZE_TEXTURE_SETTINGS
393 | osgUtil::Optimizer::TEXTURE_ATLAS_BUILDER
394 | osgUtil::Optimizer::CHECK_GEOMETRY
395 | osgUtil::Optimizer::STATIC_OBJECT_DETECTION;
397 optimizer.optimize(group, optimizationOptions);
400 // Serialize the result as a binary OSG file, including textures:
401 std::string filename = stgpath.file();
403 // Include both the STG name and the indexes for uniqueness.
404 // ostringstream required for compilers that don't support C++11
405 std::ostringstream oss;
407 filename.append(oss.str());
408 filename.append(".osg");
409 SGPath osgpath = SGPath(SGPath(output), filename);
410 osgDB::writeNodeFile(*group, osgpath.c_str(),
411 new osgDB::Options("WriteImageHint=IncludeData Compressor=zlib"));
413 // Write out the required STG entry for this merged set of objects, centered
414 // on the center of the tile.
415 stgout << "OBJECT_STATIC " << osgpath.file() << " " << center.getLongitudeDeg()
416 << " " << center.getLatitudeDeg() << " 0.0 0.0 0.0 0.0\n";
418 if (display_viewer) {
424 // Finished with this file.
431 int main(int argc, char** argv) {
432 osg::ApplicationUsage* usage = new osg::ApplicationUsage();
433 usage->setApplicationName("stgmerge");
434 usage->setCommandLineUsage(
435 "Merge static model files within a given STG file.");
436 usage->addCommandLineOption("--input <dir>", "Scenery directory to read");
437 usage->addCommandLineOption("--output <dir>",
438 "Output directory for STGs and merged models");
439 usage->addCommandLineOption("--fg-root <dir>", "FG root directory",
441 usage->addCommandLineOption("--fg-scenery <dir>", "FG scenery path",
443 usage->addCommandLineOption("--group-size <N>", "Group size (m)", "5000");
444 usage->addCommandLineOption("--optimize", "Optimize scene-graph");
445 usage->addCommandLineOption("--viewer", "Display loaded objects");
446 usage->addCommandLineOption("--copy-files", "Copy all contents of input directory into output directory");
448 // use an ArgumentParser object to manage the program arguments.
449 osg::ArgumentParser arguments(&argc, argv);
451 arguments.setApplicationUsage(usage);
453 if (arguments.read("--fg-root", fg_root)) {
454 } else if (const char *fg_root_env = std::getenv("FG_ROOT")) {
455 fg_root = fg_root_env;
460 if (arguments.read("--fg-scenery", fg_scenery)) {
461 } else if (const char *fg_scenery_env = std::getenv("FG_SCENERY")) {
462 fg_scenery = fg_scenery_env;
464 SGPath path(fg_root);
465 path.append("Scenery");
466 fg_scenery = path.str();
469 if (!arguments.read("--input", input)) {
470 arguments.reportError("--input argument required.");
474 arguments.reportError(
475 "--input directory does not exist or is not directory.");
476 } else if (!s.canRead()) {
477 arguments.reportError(
478 "--input directory cannot be read. Check permissions.");
482 if (!arguments.read("--output", output)) {
483 arguments.reportError("--output argument required.");
485 // Check directory exists, we can write to it, and we're not about to write
486 // to the same location as the STG file.
490 arguments.reportError("--output directory does not exist.");
494 arguments.reportError(
495 "--output directory is not writeable. Check permissions.");
499 arguments.reportError(
500 "--output directory must differ from STG directory.");
504 if (arguments.read("--group-size")) {
505 if (! arguments.read("--group-size", group_size)) {
506 arguments.reportError(
507 "--group-size argument number be a positive integer.");
511 if (arguments.read("--viewer")) {
512 display_viewer = true;
515 if (arguments.read("--optimize")) {
516 osg_optimizer = true;
519 if (arguments.read("--copy-files")) {
523 if (arguments.errors()) {
524 arguments.writeErrorMessages(std::cout);
525 arguments.getApplicationUsage()->write(std::cout,
526 osg::ApplicationUsage::COMMAND_LINE_OPTION
527 | osg::ApplicationUsage::ENVIRONMENTAL_VARIABLE, 80, true);
531 SGSharedPtr<SGPropertyNode> props = new SGPropertyNode;
533 SGPath preferencesFile = fg_root;
534 preferencesFile.append("preferences.xml");
535 readProperties(preferencesFile.str(), props);
537 // In case of an error, at least make summer :)
538 props->getNode("sim/startup/season", true)->setStringValue("summer");
540 SG_LOG(SG_GENERAL, SG_ALERT,
541 "Problems loading FlightGear preferences.\n" << "Probably FG_ROOT is not properly set.");
544 /// now set up the simgears required model stuff
546 simgear::ResourceManager::instance()->addBasePath(fg_root,
547 simgear::ResourceManager::PRIORITY_DEFAULT);
548 // Just reference simgears reader writer stuff so that the globals get
549 // pulled in by the linker ...
550 simgear::ModelRegistry::instance();
552 sgUserDataInit(props.get());
553 SGMaterialLibPtr ml = new SGMaterialLib;
554 SGPath mpath(fg_root);
556 // TODO: Pick up correct materials.xml file. Urrgh - this can't be runtime dependent...
557 mpath.append("Materials/default/materials.xml");
559 ml->load(fg_root, mpath.str(), props);
561 SG_LOG(SG_GENERAL, SG_ALERT,
562 "Problems loading FlightGear materials.\n" << "Probably FG_ROOT is not properly set.");
564 simgear::SGModelLib::init(fg_root, props);
566 // Set up the reader/writer options
567 osg::ref_ptr<simgear::SGReaderWriterOptions> options;
568 if (osgDB::Options* ropt = osgDB::Registry::instance()->getOptions())
569 options = new simgear::SGReaderWriterOptions(*ropt);
571 options = new simgear::SGReaderWriterOptions;
572 osgDB::convertStringPathIntoFilePathList(fg_scenery,
573 options->getDatabasePathList());
574 options->setMaterialLib(ml);
575 options->setPropertyNode(props);
576 options->setPluginStringData("SimGear::FG_ROOT", fg_root);
578 // Here, all arguments are processed
579 arguments.reportRemainingOptionsAsUnrecognized();
580 arguments.writeErrorMessages(std::cerr);
583 dir = opendir(input.c_str());
584 struct dirent *pent = NULL;
587 SG_LOG(SG_GENERAL, SG_ALERT, "Unable to open " << input);
591 // List of STG files to process
592 std::vector<std::string> stg_files;
596 while (pent != NULL) {
598 std::string fname = pent->d_name;
600 if (SGPath(fname).lower_extension() == "stg") {
601 SG_LOG(SG_GENERAL, SG_ALERT, "STG " << fname);
602 stg_files.push_back(fname);
603 } else if (copy_files) {
604 // Copy it over if we're copying all files.
605 SGPath source = SGPath(input);
606 source.append(fname);
607 SGPath destination = SGPath(output);
608 destination.append(fname);
609 //SG_LOG(SG_GENERAL, SG_ALERT, "Copying " << source.c_str() << " to " << destination.c_str());
610 std::ifstream src(source.c_str(), std::ios::binary);
611 std::ofstream dst(destination.c_str(), std::ios::binary);
620 // Now we've copied the data, process the STG files
621 std::vector<std::string>::const_iterator iter;
623 for (iter = stg_files.begin(); iter != stg_files.end(); ++iter) {
624 SGPath stg = SGPath(input);
626 processSTG(options, stg.c_str());