1 // ReaderWriterSPT.cxx -- Provide a paged database for flightgear scenery.
2 //
3 // Copyright (C) 2010 - 2013  Mathias Froehlich
4 //
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.
9 //
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.
14 //
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.
18 //
19 
20 #ifdef HAVE_CONFIG_H
21 #  include <simgear_config.h>
22 #endif
23 
24 #include "ReaderWriterSPT.hxx"
25 
26 #include <cassert>
27 
28 #include <osg/Version>
29 #include <osg/CullFace>
30 #include <osg/PagedLOD>
31 #include <osg/MatrixTransform>
32 #include <osg/Texture2D>
33 
34 #include <osgDB/FileNameUtils>
35 #include <osgDB/FileUtils>
36 #include <osgDB/ReadFile>
37 
38 #include <simgear/scene/util/OsgMath.hxx>
39 
40 #include "BucketBox.hxx"
41 
42 namespace simgear {
43 
44 // Cull away tiles that we watch from downside
45 struct ReaderWriterSPT::CullCallback : public osg::NodeCallback {
~CullCallbacksimgear::ReaderWriterSPT::CullCallback46     virtual ~CullCallback()
47     { }
operator ()simgear::ReaderWriterSPT::CullCallback48     virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
49     {
50         const osg::BoundingSphere& nodeBound = node->getBound();
51         // If the bounding sphere of the node is empty, there is nothing to do
52         if (!nodeBound.valid())
53             return;
54 
55         // Culling away tiles that we look at from the downside.
56         // This is done by computing the maximum distance we can
57         // see something from the current eyepoint. If the sphere
58         // that is defined by this radius does no intersects the
59         // nodes sphere, then this tile is culled away.
60         // Computing this radius happens by two rectangular triangles:
61         // Let r be the view point. rmin is the minimum radius we find
62         // a ground surface we need to look above. rmax is the
63         // maximum object radius we expect any object.
64         //
65         //    d1   d2
66         //  x----x----x
67         //  r\  rmin /rmax
68         //    \  |  /
69         //     \ | /
70         //      \|/
71         //
72         // The distance from the eyepoint to the point
73         // where the line of sight is perpandicular to
74         // the radius vector with minimal height is
75         // d1 = sqrt(r^2 - rmin^2).
76         // The distance from the point where the line of sight
77         // is perpandicular to the radius vector with minimal height
78         // to the highest possible object on earth with radius rmax is
79         // d2 = sqrt(rmax^2 - rmin^2).
80         // So the maximum distance we can see something on the earth
81         // from a viewpoint r is
82         // d = d1 + d2
83 
84         // This is the equatorial earth radius minus 450m,
85         // little lower than Dead Sea.
86         float rmin = 6378137 - 450;
87         float rmin2 = rmin*rmin;
88         // This is the equatorial earth radius plus 9000m,
89         // little higher than Mount Everest.
90         float rmax = 6378137 + 9000;
91         float rmax2 = rmax*rmax;
92 
93         // Check if we are looking from below any ground
94         osg::Vec3 viewPoint = nv->getViewPoint();
95         // blow the viewpoint up to a spherical earth with equatorial radius:
96         osg::Vec3 sphericViewPoint = viewPoint;
97         sphericViewPoint[2] *= 1.0033641;
98         float r2 = sphericViewPoint.length2();
99         if (r2 <= rmin2)
100             return;
101 
102         // Due to this line of sight computation, the visible tiles
103         // are limited to be within a sphere with radius d1 + d2.
104         float d1 = sqrtf(r2 - rmin2);
105         float d2 = sqrtf(rmax2 - rmin2);
106         // Note that we again base the sphere around elliptic view point,
107         // but use the radius from the spherical computation.
108         if (!nodeBound.intersects(osg::BoundingSphere(viewPoint, d1 + d2)))
109             return;
110 
111         traverse(node, nv);
112     }
113 };
114 
115 struct ReaderWriterSPT::LocalOptions {
LocalOptionssimgear::ReaderWriterSPT::LocalOptions116     LocalOptions(const osgDB::Options* options) :
117         _options(options)
118     {
119         std::string pageLevelsString;
120         if (_options)
121             pageLevelsString = _options->getPluginStringData("SimGear::SPT_PAGE_LEVELS");
122 
123         // Get the default if nothing given from outside
124         if (pageLevelsString.empty()) {
125             // We want an other level of indirection for paging
126             // Here we get about 12x12 deg tiles
127             _pageLevels.push_back(3);
128             // We want an other level of indirection for paging
129             // Here we get about 2x2 deg tiles
130             _pageLevels.push_back(5);
131             // We want an other level of indirection for paging
132             // Here we get about 0.5x0.5 deg tiles
133             _pageLevels.push_back(7);
134         } else {
135             // If configured from outside
136             std::stringstream ss(pageLevelsString);
137             while (ss.good()) {
138                 unsigned level = ~0u;
139                 ss >> level;
140                 _pageLevels.push_back(level);
141             }
142         }
143     }
144 
isPageLevelsimgear::ReaderWriterSPT::LocalOptions145     bool isPageLevel(unsigned level) const
146     {
147         return std::find(_pageLevels.begin(), _pageLevels.end(), level) != _pageLevels.end();
148     }
149 
getLodPathForBucketBoxsimgear::ReaderWriterSPT::LocalOptions150     std::string getLodPathForBucketBox(const BucketBox& bucketBox) const
151     {
152         std::stringstream ss;
153         ss << "LOD/";
154         for (std::vector<unsigned>::const_iterator i = _pageLevels.begin(); i != _pageLevels.end(); ++i) {
155             if (bucketBox.getStartLevel() <= *i)
156                 break;
157             ss << bucketBox.getParentBox(*i) << "/";
158         }
159         ss << bucketBox;
160         return ss.str();
161     }
162 
getRangeMultipliersimgear::ReaderWriterSPT::LocalOptions163     float getRangeMultiplier() const
164     {
165         float rangeMultiplier = 2;
166         if (!_options)
167             return rangeMultiplier;
168         std::stringstream ss(_options->getPluginStringData("SimGear::SPT_RANGE_MULTIPLIER"));
169         ss >> rangeMultiplier;
170         return rangeMultiplier;
171     }
172 
173     const osgDB::Options* _options;
174     std::vector<unsigned> _pageLevels;
175 };
176 
ReaderWriterSPT()177 ReaderWriterSPT::ReaderWriterSPT()
178 {
179     supportsExtension("spt", "SimGear paged terrain meta database.");
180 }
181 
~ReaderWriterSPT()182 ReaderWriterSPT::~ReaderWriterSPT()
183 {
184 }
185 
186 const char*
className() const187 ReaderWriterSPT::className() const
188 {
189     return "simgear::ReaderWriterSPT";
190 }
191 
192 osgDB::ReaderWriter::ReadResult
readObject(const std::string & fileName,const osgDB::Options * options) const193 ReaderWriterSPT::readObject(const std::string& fileName, const osgDB::Options* options) const
194 {
195     // We get called with different extensions. To make sure search continues,
196     // we need to return FILE_NOT_HANDLED in this case.
197     if (osgDB::getLowerCaseFileExtension(fileName) != "spt")
198         return ReadResult(ReadResult::FILE_NOT_HANDLED);
199     if (fileName != "state.spt")
200         return ReadResult(ReadResult::FILE_NOT_FOUND);
201 
202     osg::StateSet* stateSet = new osg::StateSet;
203     stateSet->setAttributeAndModes(new osg::CullFace);
204 
205     std::string imageFileName = options->getPluginStringData("SimGear::FG_WORLD_TEXTURE");
206     if (imageFileName.empty()) {
207         imageFileName = options->getPluginStringData("SimGear::FG_ROOT");
208         imageFileName = osgDB::concatPaths(imageFileName, "Textures");
209         imageFileName = osgDB::concatPaths(imageFileName, "Globe");
210         imageFileName = osgDB::concatPaths(imageFileName, "world.topo.bathy.200407.3x4096x2048.png");
211     }
212 #if OSG_VERSION_LESS_THAN(3,4,0)
213     if (osg::Image* image = osgDB::readImageFile(imageFileName, options)) {
214 #else
215     if (osg::Image* image = osgDB::readRefImageFile(imageFileName, options)) {
216 #endif
217         osg::Texture2D* texture = new osg::Texture2D;
218         texture->setImage(image);
219         texture->setWrap(osg::Texture2D::WRAP_S, osg::Texture2D::REPEAT);
220         texture->setWrap(osg::Texture2D::WRAP_T, osg::Texture2D::CLAMP);
221         stateSet->setTextureAttributeAndModes(0, texture);
222     }
223 
224     return stateSet;
225 }
226 
227 osgDB::ReaderWriter::ReadResult
228 ReaderWriterSPT::readNode(const std::string& fileName, const osgDB::Options* options) const
229 {
230     LocalOptions localOptions(options);
231 
232     // The file name without path and without the spt extension
233     std::string strippedFileName = osgDB::getStrippedName(fileName);
234     if (strippedFileName == "earth")
235         return ReadResult(createTree(BucketBox(-180, -90, 360, 180), localOptions, true));
236 
237     std::stringstream ss(strippedFileName);
238     BucketBox bucketBox;
239     ss >> bucketBox;
240     if (ss.fail())
241         return ReadResult::FILE_NOT_FOUND;
242 
243     BucketBox bucketBoxList[2];
244     unsigned bucketBoxListSize = bucketBox.periodicSplit(bucketBoxList);
245     if (bucketBoxListSize == 0)
246         return ReadResult::FILE_NOT_FOUND;
247 
248     if (bucketBoxListSize == 1)
249         return ReadResult(createTree(bucketBoxList[0], localOptions, true));
250 
251     assert(bucketBoxListSize == 2);
252     osg::ref_ptr<osg::Group> group = new osg::Group;
253     group->addChild(createTree(bucketBoxList[0], localOptions, true));
254     group->addChild(createTree(bucketBoxList[1], localOptions, true));
255     return ReadResult(group);
256 }
257 
258 osg::ref_ptr<osg::Node>
259 ReaderWriterSPT::createTree(const BucketBox& bucketBox, const LocalOptions& options, bool topLevel) const
260 {
261     if (bucketBox.getIsBucketSize()) {
262         std::string fileName;
263         fileName = bucketBox.getBucket().gen_index_str() + std::string(".stg");
264 #if OSG_VERSION_LESS_THAN(3,4,0)
265         return osgDB::readNodeFile(fileName, options._options);
266 #else
267         return osgDB::readRefNodeFile(fileName, options._options);
268 #endif
269     } else if (!topLevel && options.isPageLevel(bucketBox.getStartLevel())) {
270         return createPagedLOD(bucketBox, options);
271     } else {
272         BucketBox bucketBoxList[100];
273         unsigned numTiles = bucketBox.getSubDivision(bucketBoxList, 100);
274         if (numTiles == 0)
275             return 0;
276 
277         if (numTiles == 1)
278             return createTree(bucketBoxList[0], options, false);
279 
280         osg::ref_ptr<osg::Group> group = new osg::Group;
281         for (unsigned i = 0; i < numTiles; ++i) {
282             osg::ref_ptr<osg::Node> node = createTree(bucketBoxList[i], options, false);
283             if (!node.valid())
284                 continue;
285             group->addChild(node.get());
286         }
287         if (!group->getNumChildren())
288             return 0;
289 
290         return group;
291     }
292 }
293 
294 osg::ref_ptr<osg::Node>
295 ReaderWriterSPT::createPagedLOD(const BucketBox& bucketBox, const LocalOptions& options) const
296 {
297     osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD;
298 
299     pagedLOD->setCenterMode(osg::PagedLOD::USER_DEFINED_CENTER);
300     SGSpheref sphere = bucketBox.getBoundingSphere();
301     pagedLOD->setCenter(toOsg(sphere.getCenter()));
302     pagedLOD->setRadius(sphere.getRadius());
303 
304     pagedLOD->setCullCallback(new CullCallback);
305 
306     osg::ref_ptr<osgDB::Options> localOptions;
307     localOptions = static_cast<osgDB::Options*>(options._options->clone(osg::CopyOp()));
308     // FIXME:
309     // The particle systems have nodes with culling disabled.
310     // PagedLOD nodes with childnodes like this will never expire.
311     // So, for now switch them off.
312     localOptions->setPluginStringData("SimGear::PARTICLESYSTEM", "OFF");
313     pagedLOD->setDatabaseOptions(localOptions.get());
314 
315     // The break point for the low level of detail to the high level of detail
316     float rangeMultiplier = options.getRangeMultiplier();
317     float range = rangeMultiplier*sphere.getRadius();
318 
319     // Look for a low level of detail tile
320     std::string lodPath = options.getLodPathForBucketBox(bucketBox);
321     const char* extensions[] = { ".btg.gz", ".flt" };
322     for (unsigned i = 0; i < sizeof(extensions)/sizeof(extensions[0]); ++i) {
323         std::string fileName = osgDB::findDataFile(lodPath + extensions[i], options._options);
324         if (fileName.empty())
325             continue;
326 #if OSG_VERSION_LESS_THAN(3,4,0)
327         osg::ref_ptr<osg::Node> node = osgDB::readNodeFile(fileName, options._options);
328 #else
329         osg::ref_ptr<osg::Node> node = osgDB::readRefNodeFile(fileName, options._options);
330 #endif
331         if (!node.valid())
332             continue;
333         pagedLOD->addChild(node.get(), range, std::numeric_limits<float>::max());
334         break;
335     }
336     // Add the static sea level textured shell if there is nothing found
337     if (pagedLOD->getNumChildren() == 0) {
338         osg::ref_ptr<osg::Node> node = createSeaLevelTile(bucketBox, options._options);
339         if (node.valid())
340             pagedLOD->addChild(node.get(), range, std::numeric_limits<float>::max());
341     }
342 
343     // Add the paged file name that creates the subtrees on demand
344     std::stringstream ss;
345     ss << bucketBox << ".spt";
346     pagedLOD->setFileName(pagedLOD->getNumChildren(), ss.str());
347     pagedLOD->setRange(pagedLOD->getNumChildren(), 0.0, range);
348 
349     return pagedLOD;
350 }
351 
352 osg::ref_ptr<osg::Node>
353 ReaderWriterSPT::createSeaLevelTile(const BucketBox& bucketBox, const LocalOptions& options) const
354 {
355     if (options._options->getPluginStringData("SimGear::FG_EARTH") != "ON")
356         return 0;
357 
358     SGSpheref sphere = bucketBox.getBoundingSphere();
359     osg::Matrixd transform;
360     transform.makeTranslate(toOsg(-sphere.getCenter()));
361 
362     osg::Vec3Array* vertices = new osg::Vec3Array;
363     osg::Vec3Array* normals = new osg::Vec3Array;
364     osg::Vec2Array* texCoords = new osg::Vec2Array;
365 
366     unsigned widthLevel = bucketBox.getWidthLevel();
367     unsigned heightLevel = bucketBox.getHeightLevel();
368 
369     unsigned incx = bucketBox.getWidthIncrement(widthLevel + 2);
370     incx = std::min(incx, bucketBox.getSize(0));
371     for (unsigned i = 0; incx != 0;) {
372         unsigned incy = bucketBox.getHeightIncrement(heightLevel + 2);
373         incy = std::min(incy, bucketBox.getSize(1));
374         for (unsigned j = 0; incy != 0;) {
375             SGVec3f v[6], n[6];
376             SGVec2f t[6];
377             unsigned num = bucketBox.getTileTriangles(i, j, incx, incy, v, n, t);
378             for (unsigned k = 0; k < num; ++k) {
379                 vertices->push_back(transform.preMult(toOsg(v[k])));
380                 normals->push_back(toOsg(n[k]));
381                 texCoords->push_back(toOsg(t[k]));
382             }
383             j += incy;
384             incy = std::min(incy, bucketBox.getSize(1) - j);
385         }
386         i += incx;
387         incx = std::min(incx, bucketBox.getSize(0) - i);
388     }
389 
390     osg::Vec4Array* colors = new osg::Vec4Array;
391     colors->push_back(osg::Vec4(1, 1, 1, 1));
392 
393     osg::Geometry* geometry = new osg::Geometry;
394     geometry->setDataVariance(osg::Object::STATIC);
395     geometry->setUseVertexBufferObjects(true);
396     geometry->setVertexArray(vertices);
397     geometry->setNormalArray(normals);
398     geometry->setNormalBinding(osg::Geometry::BIND_PER_VERTEX);
399     geometry->setColorArray(colors);
400     geometry->setColorBinding(osg::Geometry::BIND_OVERALL);
401     geometry->setTexCoordArray(0, texCoords);
402 
403     osg::DrawArrays* drawArrays = new osg::DrawArrays(osg::DrawArrays::TRIANGLES, 0, vertices->size());
404     drawArrays->setDataVariance(osg::Object::STATIC);
405     geometry->addPrimitiveSet(drawArrays);
406 
407     osg::Geode* geode = new osg::Geode;
408     geode->setDataVariance(osg::Object::STATIC);
409     geode->addDrawable(geometry);
410     osg::ref_ptr<osg::StateSet> stateSet = getLowLODStateSet(options);
411     geode->setStateSet(stateSet.get());
412 
413     transform.makeTranslate(toOsg(sphere.getCenter()));
414     osg::MatrixTransform* matrixTransform = new osg::MatrixTransform(transform);
415     matrixTransform->setDataVariance(osg::Object::STATIC);
416     matrixTransform->addChild(geode);
417 
418     return matrixTransform;
419 }
420 
421 osg::ref_ptr<osg::StateSet>
422 ReaderWriterSPT::getLowLODStateSet(const LocalOptions& options) const
423 {
424     osg::ref_ptr<osgDB::Options> localOptions;
425     localOptions = static_cast<osgDB::Options*>(options._options->clone(osg::CopyOp()));
426     localOptions->setObjectCacheHint(osgDB::Options::CACHE_ALL);
427 
428 #if OSG_VERSION_LESS_THAN(3,4,0)
429     osg::ref_ptr<osg::Object> object = osgDB::readObjectFile("state.spt", localOptions.get());
430 #else
431     osg::ref_ptr<osg::Object> object = osgDB::readRefObjectFile("state.spt", localOptions.get());
432 #endif
433     if (!dynamic_cast<osg::StateSet*>(object.get()))
434         return 0;
435 
436     return static_cast<osg::StateSet*>(object.get());
437 }
438 
439 } // namespace simgear
440 
441