1 // ReaderWriterSPT.cxx -- Provide a paged database for flightgear scenery.
3 // Copyright (C) 2010 - 2013 Mathias Froehlich
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.
21 # include <simgear_config.h>
24 #include "ReaderWriterSPT.hxx"
28 #include <osg/CullFace>
29 #include <osg/PagedLOD>
30 #include <osg/MatrixTransform>
31 #include <osg/Texture2D>
33 #include <osgDB/FileNameUtils>
34 #include <osgDB/FileUtils>
35 #include <osgDB/ReadFile>
37 #include <simgear/scene/util/OsgMath.hxx>
39 #include "BucketBox.hxx"
43 // Cull away tiles that we watch from downside
44 struct ReaderWriterSPT::CullCallback : public osg::NodeCallback {
45 virtual ~CullCallback()
47 virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
49 const osg::BoundingSphere& nodeBound = node->getBound();
50 // If the bounding sphere of the node is empty, there is nothing to do
51 if (!nodeBound.valid())
54 // Culling away tiles that we look at from the downside.
55 // This is done by computing the maximum distance we can
56 // see something from the current eyepoint. If the sphere
57 // that is defined by this radius does no intersects the
58 // nodes sphere, then this tile is culled away.
59 // Computing this radius happens by two rectangular triangles:
60 // Let r be the view point. rmin is the minimum radius we find
61 // a ground surface we need to look above. rmax is the
62 // maximum object radius we expect any object.
71 // The distance from the eyepoint to the point
72 // where the line of sight is perpandicular to
73 // the radius vector with minimal height is
74 // d1 = sqrt(r^2 - rmin^2).
75 // The distance from the point where the line of sight
76 // is perpandicular to the radius vector with minimal height
77 // to the highest possible object on earth with radius rmax is
78 // d2 = sqrt(rmax^2 - rmin^2).
79 // So the maximum distance we can see something on the earth
80 // from a viewpoint r is
83 // This is the equatorial earth radius minus 450m,
84 // little lower than Dead Sea.
85 float rmin = 6378137 - 450;
86 float rmin2 = rmin*rmin;
87 // This is the equatorial earth radius plus 9000m,
88 // little higher than Mount Everest.
89 float rmax = 6378137 + 9000;
90 float rmax2 = rmax*rmax;
92 // Check if we are looking from below any ground
93 osg::Vec3 viewPoint = nv->getViewPoint();
94 // blow the viewpoint up to a spherical earth with equatorial radius:
95 osg::Vec3 sphericViewPoint = viewPoint;
96 sphericViewPoint[2] *= 1.0033641;
97 float r2 = sphericViewPoint.length2();
101 // Due to this line of sight computation, the visible tiles
102 // are limited to be within a sphere with radius d1 + d2.
103 float d1 = sqrtf(r2 - rmin2);
104 float d2 = sqrtf(rmax2 - rmin2);
105 // Note that we again base the sphere around elliptic view point,
106 // but use the radius from the spherical computation.
107 if (!nodeBound.intersects(osg::BoundingSphere(viewPoint, d1 + d2)))
114 struct ReaderWriterSPT::LocalOptions {
115 LocalOptions(const osgDB::Options* options) :
118 std::string pageLevelsString;
120 pageLevelsString = _options->getPluginStringData("SimGear::SPT_PAGE_LEVELS");
122 // Get the default if nothing given from outside
123 if (pageLevelsString.empty()) {
124 // We want an other level of indirection for paging
125 // Here we get about 12x12 deg tiles
126 _pageLevels.push_back(3);
127 // We want an other level of indirection for paging
128 // Here we get about 2x2 deg tiles
129 _pageLevels.push_back(5);
130 // We want an other level of indirection for paging
131 // Here we get about 0.5x0.5 deg tiles
132 _pageLevels.push_back(7);
134 // If configured from outside
135 std::stringstream ss(pageLevelsString);
137 unsigned level = ~0u;
139 _pageLevels.push_back(level);
144 bool isPageLevel(unsigned level) const
146 return std::find(_pageLevels.begin(), _pageLevels.end(), level) != _pageLevels.end();
149 std::string getLodPathForBucketBox(const BucketBox& bucketBox) const
151 std::stringstream ss;
153 for (std::vector<unsigned>::const_iterator i = _pageLevels.begin(); i != _pageLevels.end(); ++i) {
154 if (bucketBox.getStartLevel() <= *i)
156 ss << bucketBox.getParentBox(*i) << "/";
162 float getRangeMultiplier() const
164 float rangeMultiplier = 2;
166 return rangeMultiplier;
167 std::stringstream ss(_options->getPluginStringData("SimGear::SPT_RANGE_MULTIPLIER"));
168 ss >> rangeMultiplier;
169 return rangeMultiplier;
172 const osgDB::Options* _options;
173 std::vector<unsigned> _pageLevels;
176 ReaderWriterSPT::ReaderWriterSPT()
178 supportsExtension("spt", "SimGear paged terrain meta database.");
181 ReaderWriterSPT::~ReaderWriterSPT()
186 ReaderWriterSPT::className() const
188 return "simgear::ReaderWriterSPT";
191 osgDB::ReaderWriter::ReadResult
192 ReaderWriterSPT::readObject(const std::string& fileName, const osgDB::Options* options) const
194 // We get called with different extensions. To make sure search continues,
195 // we need to return FILE_NOT_HANDLED in this case.
196 if (osgDB::getLowerCaseFileExtension(fileName) != "spt")
197 return ReadResult(ReadResult::FILE_NOT_HANDLED);
198 if (fileName != "state.spt")
199 return ReadResult(ReadResult::FILE_NOT_FOUND);
201 osg::StateSet* stateSet = new osg::StateSet;
202 stateSet->setAttributeAndModes(new osg::CullFace);
204 std::string imageFileName = options->getPluginStringData("SimGear::FG_WORLD_TEXTURE");
205 if (imageFileName.empty()) {
206 imageFileName = options->getPluginStringData("SimGear::FG_ROOT");
207 imageFileName = osgDB::concatPaths(imageFileName, "Textures");
208 imageFileName = osgDB::concatPaths(imageFileName, "Globe");
209 imageFileName = osgDB::concatPaths(imageFileName, "world.topo.bathy.200407.3x4096x2048.png");
211 if (osg::Image* image = osgDB::readImageFile(imageFileName, options)) {
212 osg::Texture2D* texture = new osg::Texture2D;
213 texture->setImage(image);
214 texture->setWrap(osg::Texture2D::WRAP_S, osg::Texture2D::REPEAT);
215 texture->setWrap(osg::Texture2D::WRAP_T, osg::Texture2D::CLAMP);
216 stateSet->setTextureAttributeAndModes(0, texture);
222 osgDB::ReaderWriter::ReadResult
223 ReaderWriterSPT::readNode(const std::string& fileName, const osgDB::Options* options) const
225 LocalOptions localOptions(options);
227 // The file name without path and without the spt extension
228 std::string strippedFileName = osgDB::getStrippedName(fileName);
229 if (strippedFileName == "earth")
230 return ReadResult(createTree(BucketBox(-180, -90, 360, 180), localOptions, true));
232 std::stringstream ss(strippedFileName);
236 return ReadResult::FILE_NOT_FOUND;
238 BucketBox bucketBoxList[2];
239 unsigned bucketBoxListSize = bucketBox.periodicSplit(bucketBoxList);
240 if (bucketBoxListSize == 0)
241 return ReadResult::FILE_NOT_FOUND;
243 if (bucketBoxListSize == 1)
244 return ReadResult(createTree(bucketBoxList[0], localOptions, true));
246 assert(bucketBoxListSize == 2);
247 osg::ref_ptr<osg::Group> group = new osg::Group;
248 group->addChild(createTree(bucketBoxList[0], localOptions, true));
249 group->addChild(createTree(bucketBoxList[1], localOptions, true));
250 return ReadResult(group);
253 osg::ref_ptr<osg::Node>
254 ReaderWriterSPT::createTree(const BucketBox& bucketBox, const LocalOptions& options, bool topLevel) const
256 if (bucketBox.getIsBucketSize()) {
257 std::string fileName;
258 fileName = bucketBox.getBucket().gen_index_str() + std::string(".stg");
259 return osgDB::readRefNodeFile(fileName, options._options);
260 } else if (!topLevel && options.isPageLevel(bucketBox.getStartLevel())) {
261 return createPagedLOD(bucketBox, options);
263 BucketBox bucketBoxList[100];
264 unsigned numTiles = bucketBox.getSubDivision(bucketBoxList, 100);
269 return createTree(bucketBoxList[0], options, false);
271 osg::ref_ptr<osg::Group> group = new osg::Group;
272 for (unsigned i = 0; i < numTiles; ++i) {
273 osg::ref_ptr<osg::Node> node = createTree(bucketBoxList[i], options, false);
276 group->addChild(node.get());
278 if (!group->getNumChildren())
285 osg::ref_ptr<osg::Node>
286 ReaderWriterSPT::createPagedLOD(const BucketBox& bucketBox, const LocalOptions& options) const
288 osg::PagedLOD* pagedLOD = new osg::PagedLOD;
290 pagedLOD->setCenterMode(osg::PagedLOD::USER_DEFINED_CENTER);
291 SGSpheref sphere = bucketBox.getBoundingSphere();
292 pagedLOD->setCenter(toOsg(sphere.getCenter()));
293 pagedLOD->setRadius(sphere.getRadius());
295 pagedLOD->setCullCallback(new CullCallback);
297 osg::ref_ptr<osgDB::Options> localOptions;
298 localOptions = static_cast<osgDB::Options*>(options._options->clone(osg::CopyOp()));
300 // The particle systems have nodes with culling disabled.
301 // PagedLOD nodes with childnodes like this will never expire.
302 // So, for now switch them off.
303 localOptions->setPluginStringData("SimGear::PARTICLESYSTEM", "OFF");
304 pagedLOD->setDatabaseOptions(localOptions.get());
306 // The break point for the low level of detail to the high level of detail
307 float rangeMultiplier = options.getRangeMultiplier();
308 float range = rangeMultiplier*sphere.getRadius();
310 // Look for a low level of detail tile
311 std::string lodPath = options.getLodPathForBucketBox(bucketBox);
312 const char* extensions[] = { ".btg.gz", ".flt" };
313 for (unsigned i = 0; i < sizeof(extensions)/sizeof(extensions[0]); ++i) {
314 std::string fileName = osgDB::findDataFile(lodPath + extensions[i], options._options);
315 if (fileName.empty())
317 osg::ref_ptr<osg::Node> node = osgDB::readRefNodeFile(fileName, options._options);
320 pagedLOD->addChild(node.get(), range, std::numeric_limits<float>::max());
323 // Add the static sea level textured shell if there is nothing found
324 if (pagedLOD->getNumChildren() == 0) {
325 osg::ref_ptr<osg::Node> node = createSeaLevelTile(bucketBox, options._options);
327 pagedLOD->addChild(node.get(), range, std::numeric_limits<float>::max());
330 // Add the paged file name that creates the subtrees on demand
331 std::stringstream ss;
332 ss << bucketBox << ".spt";
333 pagedLOD->setFileName(pagedLOD->getNumChildren(), ss.str());
334 pagedLOD->setRange(pagedLOD->getNumChildren(), 0.0, range);
339 osg::ref_ptr<osg::Node>
340 ReaderWriterSPT::createSeaLevelTile(const BucketBox& bucketBox, const LocalOptions& options) const
342 if (options._options->getPluginStringData("SimGear::FG_EARTH") != "ON")
345 SGSpheref sphere = bucketBox.getBoundingSphere();
346 osg::Matrixd transform;
347 transform.makeTranslate(toOsg(-sphere.getCenter()));
349 osg::Vec3Array* vertices = new osg::Vec3Array;
350 osg::Vec3Array* normals = new osg::Vec3Array;
351 osg::Vec2Array* texCoords = new osg::Vec2Array;
353 unsigned widthLevel = bucketBox.getWidthLevel();
354 unsigned heightLevel = bucketBox.getHeightLevel();
356 unsigned incx = bucketBox.getWidthIncrement(widthLevel + 2);
357 incx = std::min(incx, bucketBox.getSize(0));
358 for (unsigned i = 0; incx != 0;) {
359 unsigned incy = bucketBox.getHeightIncrement(heightLevel + 2);
360 incy = std::min(incy, bucketBox.getSize(1));
361 for (unsigned j = 0; incy != 0;) {
364 unsigned num = bucketBox.getTileTriangles(i, j, incx, incy, v, n, t);
365 for (unsigned k = 0; k < num; ++k) {
366 vertices->push_back(transform.preMult(toOsg(v[k])));
367 normals->push_back(toOsg(n[k]));
368 texCoords->push_back(toOsg(t[k]));
371 incy = std::min(incy, bucketBox.getSize(1) - j);
374 incx = std::min(incx, bucketBox.getSize(0) - i);
377 osg::Vec4Array* colors = new osg::Vec4Array;
378 colors->push_back(osg::Vec4(1, 1, 1, 1));
380 osg::Geometry* geometry = new osg::Geometry;
381 geometry->setDataVariance(osg::Object::STATIC);
382 geometry->setUseVertexBufferObjects(true);
383 geometry->setVertexArray(vertices);
384 geometry->setNormalArray(normals);
385 geometry->setNormalBinding(osg::Geometry::BIND_PER_VERTEX);
386 geometry->setColorArray(colors);
387 geometry->setColorBinding(osg::Geometry::BIND_OVERALL);
388 geometry->setTexCoordArray(0, texCoords);
390 osg::DrawArrays* drawArrays = new osg::DrawArrays(osg::DrawArrays::TRIANGLES, 0, vertices->size());
391 drawArrays->setDataVariance(osg::Object::STATIC);
392 geometry->addPrimitiveSet(drawArrays);
394 osg::Geode* geode = new osg::Geode;
395 geode->setDataVariance(osg::Object::STATIC);
396 geode->addDrawable(geometry);
397 osg::ref_ptr<osg::StateSet> stateSet = getLowLODStateSet(options);
398 geode->setStateSet(stateSet.get());
400 transform.makeTranslate(toOsg(sphere.getCenter()));
401 osg::MatrixTransform* matrixTransform = new osg::MatrixTransform(transform);
402 matrixTransform->setDataVariance(osg::Object::STATIC);
403 matrixTransform->addChild(geode);
405 return matrixTransform;
408 osg::ref_ptr<osg::StateSet>
409 ReaderWriterSPT::getLowLODStateSet(const LocalOptions& options) const
411 osg::ref_ptr<osgDB::Options> localOptions;
412 localOptions = static_cast<osgDB::Options*>(options._options->clone(osg::CopyOp()));
413 localOptions->setObjectCacheHint(osgDB::Options::CACHE_ALL);
415 osg::ref_ptr<osg::Object> object = osgDB::readRefObjectFile("state.spt", localOptions.get());
416 if (!dynamic_cast<osg::StateSet*>(object.get()))
419 return static_cast<osg::StateSet*>(object.get());
422 } // namespace simgear