예제 #1
0
osg::Node*
SingleKeyNodeFactory::createTile(TileModel* model, bool setupChildrenIfNecessary)
{
    // compile the model into a node:
    TileNode* tileNode = _modelCompiler->compile( model, _frame );

    // see if this tile might have children.
    bool prepareForChildren =
        setupChildrenIfNecessary &&
        model->_tileKey.getLOD() < *_options.maxLOD();

    osg::Node* result = 0L;

    if ( prepareForChildren )
    {
        //Compute the min range based on the 2D size of the tile
        osg::BoundingSphere bs = tileNode->getBound();
        GeoExtent extent = model->_tileKey.getExtent();
        GeoPoint lowerLeft(extent.getSRS(), extent.xMin(), extent.yMin(), 0.0, ALTMODE_ABSOLUTE);
        GeoPoint upperRight(extent.getSRS(), extent.xMax(), extent.yMax(), 0.0, ALTMODE_ABSOLUTE);
        osg::Vec3d ll, ur;
        lowerLeft.toWorld( ll );
        upperRight.toWorld( ur );
        double radius = (ur - ll).length() / 2.0;
        float minRange = (float)(radius * _options.minTileRangeFactor().value());

        TilePagedLOD* plod = new TilePagedLOD( _engineUID, _liveTiles, _deadTiles );
        plod->setCenter  ( bs.center() );
        plod->addChild   ( tileNode );
        plod->setRange   ( 0, minRange, FLT_MAX );
        plod->setFileName( 1, Stringify() << tileNode->getKey().str() << "." << _engineUID << ".osgearth_engine_mp_tile" );
        plod->setRange   ( 1, 0, minRange );

#if USE_FILELOCATIONCALLBACK
        osgDB::Options* options = Registry::instance()->cloneOrCreateOptions();
        options->setFileLocationCallback( new FileLocationCallback() );
        plod->setDatabaseOptions( options );
#endif
        
        result = plod;

        // this one rejects back-facing tiles:
        if ( _frame.getMapInfo().isGeocentric() && _options.clusterCulling() == true )
        {
            osg::HeightField* hf =
                model->_elevationData.getHeightField();

            result->addCullCallback( HeightFieldUtils::createClusterCullingCallback(
                hf,
                tileNode->getKey().getProfile()->getSRS()->getEllipsoid(),
                *_options.verticalScale() ) );
        }
    }
    else
    {
        result = tileNode;
    }

    return result;
}
예제 #2
0
void
MPTerrainEngineNode::createTerrain()
{
    // scrub the heightfield cache.
    if (_tileModelFactory)
        _tileModelFactory->getHeightFieldCache()->clear();

    // New terrain
    _terrain = new TerrainNode( _deadTiles.get() );
    this->addChild( _terrain );

    // Enable blending on the terrain node; this will result in the underlying
    // "empty" globe being transparent instead of white.
    if (_terrainOptions.enableBlending().value())
    {
        _terrain->getOrCreateStateSet()->setMode(GL_BLEND , osg::StateAttribute::ON);
    }

    // Factory to create the root keys:
    KeyNodeFactory* factory = getKeyNodeFactory();

    // Build the first level of the terrain.
    // Collect the tile keys comprising the root tiles of the terrain.
    std::vector< TileKey > keys;
    _update_mapf->getProfile()->getAllKeysAtLOD( *_terrainOptions.firstLOD(), keys );

    // create a root node for each root tile key.
    OE_INFO << LC << "Creating " << keys.size() << " root keys.." << std::endl;

    TilePagedLOD* root = new TilePagedLOD( _uid, _liveTiles, _deadTiles );
    //osg::Group* root = new osg::Group();
    _terrain->addChild( root );

    osg::ref_ptr<osgDB::Options> dbOptions = Registry::instance()->cloneOrCreateOptions();

    unsigned child = 0;
    for( unsigned i=0; i<keys.size(); ++i )
    {
        osg::ref_ptr<osg::Node> node = factory->createNode( keys[i], true, 0L );
        if ( node.valid() )
        {
            root->addChild( node.get() );
            root->setRange( child++, 0.0f, FLT_MAX );
            root->setCenter( node->getBound().center() );
            root->setNumChildrenThatCannotBeExpired( child );
        }
        else
        {
            OE_WARN << LC << "Couldn't make tile for root key: " << keys[i].str() << std::endl;
        }
    }

    _rootTilesRegistered = false;

    updateShaders();
}
예제 #3
0
//---------------------------------------------
//$$$
void MPTerrainEngineNode::_createTerrainTilePagedLOD()
{
	// Factory to create the root keys:
	KeyNodeFactory* factory = getKeyNodeFactory();

	// Build the first level of the terrain.
	// Collect the tile keys comprising the root tiles of the terrain.
	std::vector< TileKey > keys;
	_update_mapf->getProfile()->getAllKeysAtLOD( *_terrainOptions.firstLOD(), keys );

	// create a root node for each root tile key.
	OE_INFO << LC << "Creating " << keys.size() << " root keys.." << std::endl;

	TilePagedLOD* root = new TilePagedLOD( _uid, _liveTiles, _deadTiles );
	//osg::Group* root = new osg::Group();

	osg::ref_ptr<osgDB::Options> dbOptions = Registry::instance()->cloneOrCreateOptions();

	unsigned child = 0;
	std::cout<<"MPTerrainEngineNode::refresh() ( @osgEarthDrivers\\..\\MPTerrainEngineNode.cpp ) : Begin create KeyNode"<<std::endl;//$$$
	for( unsigned i=0; i<keys.size(); ++i )
	{
		osg::ref_ptr<osg::Node> node = factory->createNode( keys[i], true, true, 0L );//$$$这里消耗太多时间了!!阻塞了渲染线程。
		if ( node.valid() )
		{
			root->addChild( node.get() );
			root->setRange( child++, 0.0f, FLT_MAX );
			root->setCenter( node->getBound().center() );
			root->setNumChildrenThatCannotBeExpired( child );
		}
		else
		{
			OE_WARN << LC << "Couldn't make tile for root key: " << keys[i].str() << std::endl;
		}
	}
	std::cout<<"After create KeyNode"<<std::endl;//$$$

	osgX::StaticOSGViewerAssistant::dealWithUpdateOperation( new RefreshTerrainOperation( this, root ) );
}
예제 #4
0
TileGroup::TileGroup(TileNode*         tilenode,
                     const UID&        engineUID,
                     TileNodeRegistry* live,
                     TileNodeRegistry* dead,
                     osgDB::Options*   dbOptions)
{
    _numSubtilesUpsampling = 0;
    _numSubtilesLoaded     = 0;
    _traverseSubtiles      = true;

    this->addChild( tilenode );
    _tilenode = tilenode;

    for(unsigned q=0; q<4; ++q)
    {
        TileKey subkey = tilenode->getKey().createChildKey(q);
        TilePagedLOD* lod = new TilePagedLOD(this, subkey, engineUID, live, dead);
        lod->setDatabaseOptions( dbOptions );
        lod->setCenter( tilenode->getBound().center() );
        lod->setRadius( tilenode->getBound().radius() );
        this->addChild( lod );
    }
}
예제 #5
0
void
MPTerrainEngineNode::dirtyTerrain()
{
    // scrub the heightfield cache.
    if (_tileModelFactory)
    {
        _tileModelFactory->clearCaches();
    }

    // remove existing:
    if ( _terrain )
    {
        this->removeChild( _terrain );
    }

    // New terrain
    _terrain = new TerrainNode( _deadTiles.get() );

#ifdef USE_RENDER_BINS
    _terrain->getOrCreateStateSet()->setRenderBinDetails( 0, _terrainRenderBinPrototype->getName() );
    _terrain->getOrCreateStateSet()->setNestRenderBins(false);
#else
    _terrain->getOrCreateStateSet()->setRenderBinDetails(0, "SORT_FRONT_TO_BACK");
#endif

    this->addChild( _terrain );

    // Factory to create the root keys:
    KeyNodeFactory* factory = getKeyNodeFactory();
    
    // Build the first level of the terrain.
    // Collect the tile keys comprising the root tiles of the terrain.
    std::vector< TileKey > keys;
    _update_mapf->getProfile()->getAllKeysAtLOD( *_terrainOptions.firstLOD(), keys );

    // create a root node for each root tile key.
    OE_INFO << LC << "Creating " << keys.size() << " root keys.." << std::endl;

    TilePagedLOD* root = new TilePagedLOD( _uid, _liveTiles, _deadTiles );
    _terrain->addChild( root );

    osg::ref_ptr<osgDB::Options> dbOptions = Registry::instance()->cloneOrCreateOptions();

    unsigned child = 0;
    for( unsigned i=0; i<keys.size(); ++i )
    {
        osg::ref_ptr<osg::Node> node = factory->createNode( keys[i], true, true, 0L );
        if ( node.valid() )
        {
            root->addChild( node.get() );
            root->setRange( child++, 0.0f, FLT_MAX );
            root->setCenter( node->getBound().center() );
            root->setNumChildrenThatCannotBeExpired( child );
        }
        else
        {
            OE_WARN << LC << "Couldn't make tile for root key: " << keys[i].str() << std::endl;
        }
    }

    _rootTilesRegistered = false;

    updateState();

    // Call the base class
    TerrainEngineNode::dirtyTerrain();
}
osg::Node*
SingleKeyNodeFactory::createTile(TileModel*        model,
                                 bool              setupChildrenIfNecessary,
                                 ProgressCallback* progress)
{
#ifdef EXPERIMENTAL_TILE_NODE_CACHE
    osg::ref_ptr<TileNode> tileNode;
    TileNodeCache::Record rec;
    cache.get(model->_tileKey, rec);
    if ( rec.valid() )
    {
        tileNode = rec.value().get();
    }
    else
    {
        tileNode = _modelCompiler->compile( model, _frame );
        cache.insert(model->_tileKey, tileNode);
    }
#else
    // compile the model into a node:
    TileNode* tileNode = _modelCompiler->compile(model, _frame, progress);
#endif

    // see if this tile might have children.
    bool prepareForChildren =
        setupChildrenIfNecessary &&
        model->_tileKey.getLOD() < *_options.maxLOD();

    osg::Node* result = 0L;

    if ( prepareForChildren )
    {
        osg::BoundingSphere bs = tileNode->getBound();
        TilePagedLOD* plod = new TilePagedLOD( _engineUID, _liveTiles, _deadTiles );
        plod->setCenter  ( bs.center() );
        plod->addChild   ( tileNode );
        plod->setFileName( 1, Stringify() << tileNode->getKey().str() << "." << _engineUID << ".osgearth_engine_mp_tile" );

        if ( _options.rangeMode().value() == osg::LOD::DISTANCE_FROM_EYE_POINT )
        {
            //Compute the min range based on the 2D size of the tile
            GeoExtent extent = model->_tileKey.getExtent();
            GeoPoint lowerLeft(extent.getSRS(), extent.xMin(), extent.yMin(), 0.0, ALTMODE_ABSOLUTE);
            GeoPoint upperRight(extent.getSRS(), extent.xMax(), extent.yMax(), 0.0, ALTMODE_ABSOLUTE);
            osg::Vec3d ll, ur;
            lowerLeft.toWorld( ll );
            upperRight.toWorld( ur );
            double radius = (ur - ll).length() / 2.0;
            float minRange = (float)(radius * _options.minTileRangeFactor().value());

            plod->setRange( 0, minRange, FLT_MAX );
            plod->setRange( 1, 0, minRange );
            plod->setRangeMode( osg::LOD::DISTANCE_FROM_EYE_POINT );
        }
        else
        {
            plod->setRange( 0, 0.0f, _options.tilePixelSize().value() );
            plod->setRange( 1, _options.tilePixelSize().value(), FLT_MAX );
            plod->setRangeMode( osg::LOD::PIXEL_SIZE_ON_SCREEN );
        }



        // DBPager will set a priority based on the ratio range/maxRange.
        // This will offset that number with a full LOD #, giving LOD precedence.
        // Experimental.
        //plod->setPriorityScale( 1, model->_tileKey.getLOD()+1 );

#if USE_FILELOCATIONCALLBACK
        osgDB::Options* options = plod->getOrCreateDBOptions();
        options->setFileLocationCallback( new FileLocationCallback() );
#endif
        
        result = plod;

        // this one rejects back-facing tiles:
        if ( _frame.getMapInfo().isGeocentric() && _options.clusterCulling() == true )
        {
            osg::HeightField* hf =
                model->_elevationData.getHeightField();

            result->addCullCallback( HeightFieldUtils::createClusterCullingCallback(
                hf,
                tileNode->getKey().getProfile()->getSRS()->getEllipsoid(),
                *_options.verticalScale() ) );
        }
    }
    else
    {
        result = tileNode;
    }

    return result;
}
osg::Node*
SingleKeyNodeFactory::createTile(TileModel*        model,
                                 bool              setupChildrenIfNecessary,
                                 ProgressCallback* progress)
{
#ifdef EXPERIMENTAL_TILE_NODE_CACHE
    osg::ref_ptr<TileNode> tileNode;
    TileNodeCache::Record rec;
    cache.get(model->_tileKey, rec);
    if ( rec.valid() )
    {
        tileNode = rec.value().get();
    }
    else
    {
        tileNode = _modelCompiler->compile( model, _frame );
        cache.insert(model->_tileKey, tileNode);
    }
#else
    // compile the model into a node:
    TileNode* tileNode = _modelCompiler->compile(model, _frame, progress);
    tileNode->setEngineUID( _engineUID );
#endif

    // see if this tile might have children.
    bool prepareForChildren =
        setupChildrenIfNecessary &&
        model->_tileKey.getLOD() < *_options.maxLOD();

    osg::Node* result = 0L;

    if ( prepareForChildren )
    {
        osg::BoundingSphere bs = tileNode->getBound();
        TilePagedLOD* plod = new TilePagedLOD( _engineUID, _liveTiles, _deadTiles );
        plod->setCenter  ( bs.center() );
        plod->addChild   ( tileNode );
        plod->setFileName( 1, Stringify() << tileNode->getKey().str() << "." << _engineUID << ".osgearth_engine_mp_tile" );
        plod->setDebug   ( _debug );

        if ( _options.rangeMode().value() == osg::LOD::DISTANCE_FROM_EYE_POINT )
        {
            //Compute the min range based on the 2D size of the tile
            GeoExtent extent = model->_tileKey.getExtent();
            double radius = 0.0;
            
#if 0
            // Test code to use the equitorial radius so that all of the tiles at the same level
            // have the same range.  This will make the poles page in more appropriately.
            if (_frame.getMapInfo().isGeocentric())
            {
                GeoExtent equatorialExtent(
                extent.getSRS(),
                extent.west(),
                -extent.height()/2.0,
                extent.east(),
                extent.height()/2.0 );
                radius = equatorialExtent.getBoundingGeoCircle().getRadius();
            }
            else
#endif
            {
                GeoPoint lowerLeft(extent.getSRS(), extent.xMin(), extent.yMin(), 0.0, ALTMODE_ABSOLUTE);
                GeoPoint upperRight(extent.getSRS(), extent.xMax(), extent.yMax(), 0.0, ALTMODE_ABSOLUTE);
                osg::Vec3d ll, ur;
                lowerLeft.toWorld( ll );
                upperRight.toWorld( ur );
                radius = (ur - ll).length() / 2.0;
            }
          
            float minRange = (float)(radius * _options.minTileRangeFactor().value());

            plod->setRange( 0, minRange, FLT_MAX );
            plod->setRange( 1, 0, minRange );
            plod->setRangeMode( osg::LOD::DISTANCE_FROM_EYE_POINT );
        }
        else
        {
            // the *2 is because we page in 4-tile sets, not individual tiles.
            float size = 2.0f * _options.tilePixelSize().value();
            plod->setRange( 0, 0.0f, size );
            plod->setRange( 1, size, FLT_MAX );
            plod->setRangeMode( osg::LOD::PIXEL_SIZE_ON_SCREEN );
        }
        
        // Install a tile-aligned bounding box in the pager node itself so we can do
        // visibility testing before paging in subtiles.
        plod->setChildBoundingBoxAndMatrix(
            1,
            tileNode->getTerrainBoundingBox(),
            tileNode->getMatrix() );

        // DBPager will set a priority based on the ratio range/maxRange.
        // This will offset that number with a full LOD #, giving LOD precedence.
        // Experimental.
        //plod->setPriorityScale( 1, model->_tileKey.getLOD()+1 );

#if USE_FILELOCATIONCALLBACK
        osgDB::Options* options = plod->getOrCreateDBOptions();
        options->setFileLocationCallback( new FileLocationCallback() );
#endif
        
        result = plod;

        // this one rejects back-facing tiles:
        if ( _frame.getMapInfo().isGeocentric() && _options.clusterCulling() == true )
        {

#if 1
            osg::HeightField* hf =
                model->_elevationData.getHeightField();

            result->addCullCallback( HeightFieldUtils::createClusterCullingCallback(
                hf,
                tileNode->getKey().getProfile()->getSRS()->getEllipsoid(),
                *_options.verticalScale() ) );
#else
            // This works, but isn't quite as tight at the cluster culler.
            // Re-evaluate down the road.
            result->addCullCallback( new HorizonTileCuller(
                _frame.getMapInfo().getSRS(),
                tileNode->getTerrainBoundingBox(),
                tileNode->getMatrix(), tileNode->getKey() ) );
#endif
        }
    }
    else
    {
        result = tileNode;
    }

    return result;
}
예제 #8
0
osg::Node*
SingleKeyNodeFactory::createTile(TileModel*        model,
                                 bool              setupChildrenIfNecessary,
                                 ProgressCallback* progress)
{
#ifdef EXPERIMENTAL_TILE_NODE_CACHE
    osg::ref_ptr<TileNode> tileNode;
    TileNodeCache::Record rec;
    cache.get(model->_tileKey, rec);
    if ( rec.valid() )
    {
        tileNode = rec.value().get();
    }
    else
    {
        tileNode = _modelCompiler->compile( model, _frame );
        cache.insert(model->_tileKey, tileNode);
    }
#else
    // compile the model into a node:
    osg::ref_ptr<TileNode> tileNode = _modelCompiler->compile(model, _frame, progress);
#endif

    // see if this tile might have children.
    bool prepareForChildren =
        setupChildrenIfNecessary &&
        model->_tileKey.getLOD() < *_options.maxLOD();

    osg::Node* result = 0L;

    if ( prepareForChildren )
    {
        osg::BoundingSphere bs = tileNode->getBound();
        TilePagedLOD* plod = new TilePagedLOD( _engine->getUID(), _liveTiles.get(), _releaser.get() );
        plod->setCenter  ( bs.center() );
        plod->addChild   ( tileNode.get() );
        plod->setFileName( 1, Stringify() << tileNode->getKey().str() << "." << _engine->getUID() << ".osgearth_engine_mp_tile" );
        
        double rangeFactor = _options.minTileRangeFactor().get();
        //if (_options.adaptivePolarRangeFactor() == true)
        //{
        //    double lat = model->_tileKey.getExtent().yMin() < 0 ? -model->_tileKey.getExtent().yMax() : model->_tileKey.getExtent().yMin();
        //    double latRad = osg::DegreesToRadians(lat);
        //    rangeFactor -= (rangeFactor - 1.0)*sin(latRad)*sin(latRad);
        //}
        plod->setRangeFactor(rangeFactor);

        // Setup expiration.
        if (_options.minExpiryFrames().isSet())
        {
            plod->setMinimumExpiryFrames(1, *_options.minExpiryFrames());
        }
        
        if (_options.minExpiryTime().isSet())
        {         
            plod->setMinimumExpiryTime(1, *_options.minExpiryTime());
        }      

        if ( _options.rangeMode().value() == osg::LOD::DISTANCE_FROM_EYE_POINT )
        {
            //Compute the min range based on the 2D size of the tile
            GeoExtent extent = model->_tileKey.getExtent();
            double radius = 0.0;
      
            GeoPoint lowerLeft(extent.getSRS(), extent.xMin(), extent.yMin(), 0.0, ALTMODE_ABSOLUTE);
            GeoPoint upperRight(extent.getSRS(), extent.xMax(), extent.yMax(), 0.0, ALTMODE_ABSOLUTE);
            osg::Vec3d ll, ur;
            lowerLeft.toWorld( ll );
            upperRight.toWorld( ur );
            double radiusDiag = (ur - ll).length() / 2.0;

            if (_options.adaptivePolarRangeFactor() == true )
            {
                GeoPoint left(extent.getSRS(), extent.xMin(), extent.yMin()+extent.height()*0.5, 0.0, ALTMODE_ABSOLUTE);
                GeoPoint right(extent.getSRS(), extent.xMax(), extent.yMin()+extent.height()*0.5, 0.0, ALTMODE_ABSOLUTE);
                osg::Vec3d l, r;
                left.toWorld(l);
                right.toWorld(r);
                double radiusHoriz = 1.4142 * (r - l).length() / 2.0;
                
                double lat = model->_tileKey.getExtent().yMin() < 0 ? -model->_tileKey.getExtent().yMax() : model->_tileKey.getExtent().yMin();
                double latRad = osg::DegreesToRadians(lat);

                // mix between diagonal radius and horizontal radius based on latitude
                double t = cos(latRad);
                t = 1.0-(1.0-t)*(1.0-t);    // decelerate t to weight the mix in favor of equator (diag radius)
                radius = t*radiusDiag + (1.0-t)*radiusHoriz;
            }
            else
            {
                radius = radiusDiag;
            }
          
            float minRange = radius;

            plod->setRange( 0, minRange, FLT_MAX );
            plod->setRange( 1, 0, minRange );
            plod->setRangeMode( osg::LOD::DISTANCE_FROM_EYE_POINT );
        }
        else
        {
            // the *2 is because we page in 4-tile sets, not individual tiles.
            float size = 2.0f * _options.tilePixelSize().value();
            plod->setRange( 0, 0.0f, size );
            plod->setRange( 1, size, FLT_MAX );
            plod->setRangeMode( osg::LOD::PIXEL_SIZE_ON_SCREEN );
        }
        
        // Install a tile-aligned bounding box in the pager node itself so we can do
        // visibility testing before paging in subtiles.
        plod->setChildBoundingBoxAndMatrix(
            1,
            tileNode->getTerrainBoundingBox(),
            tileNode->getMatrix() );

        // DBPager will set a priority based on the ratio range/maxRange.
        // This will offset that number with a full LOD #, giving LOD precedence.
        // Experimental.
        //plod->setPriorityScale( 1, model->_tileKey.getLOD()+1 );

#if USE_FILELOCATIONCALLBACK
        osgDB::Options* options = plod->getOrCreateDBOptions();
        options->setFileLocationCallback( new FileLocationCallback() );
#endif
        
        result = plod;

        // this one rejects back-facing tiles:
        if ( _frame.getMapInfo().isGeocentric() && _options.clusterCulling() == true )
        {
            osg::HeightField* hf =
                model->_elevationData.getHeightField();

            result->addCullCallback( HeightFieldUtils::createClusterCullingCallback(
                hf,
                tileNode->getKey().getProfile()->getSRS()->getEllipsoid(),
                *_options.verticalScale() ) );
        }
    }
    else
    {
        result = tileNode.release();
    }

    return result;
}
예제 #9
0
TileNode*
TileGroupFactory::createTileNodeGraph(TerrainTileModel* model,
                                      bool              setupChildrenIfNecessary,
                                      ProgressCallback* progress)
{
    // TODO: fix this
    const unsigned tileSize = 17;

    // Build the surface node.
    SurfaceNodeFactory factory(model, _frame, _renderBindings, _geometryPool, tileSize, _options);
    SurfaceNode* surfaceNode = factory.createSurfaceNode();

    surfaceNode->setEngineUID( _terrainEngine->getUID() );

    // see if this tile might have children.
    bool prepareForChildren =
        setupChildrenIfNecessary &&
        model->getKey().getLOD() < *_options.maxLOD();
    
    // Build the Tile Node that will hold all the textures and texture matrices.
    TileNode* tileNode = createTileNode(
        model,
        progress );

    // Build the paging node that will load subtiles, if necessary:
    if ( prepareForChildren )
    {
        osg::BoundingSphere bs = surfaceNode->getBound();
        TilePagedLOD* plod = new TilePagedLOD( _terrainEngine->getUID(), _liveTiles, _deadTiles );
        plod->setCenter  ( bs.center() );
        plod->addChild   ( surfaceNode );
        plod->setFileName( 1, Stringify() 
            << model->getKey().str()
            << "." << _terrainEngine->getUID()
            << ".osgearth_engine_mp_tile" );

        if ( _options.rangeMode().value() == osg::LOD::DISTANCE_FROM_EYE_POINT )
        {
            //Compute the min range based on the 2D size of the tile
            GeoExtent extent = model->getKey().getExtent();
            GeoPoint lowerLeft(extent.getSRS(), extent.xMin(), extent.yMin(), 0.0, ALTMODE_ABSOLUTE);
            GeoPoint upperRight(extent.getSRS(), extent.xMax(), extent.yMax(), 0.0, ALTMODE_ABSOLUTE);
            osg::Vec3d ll, ur;
            lowerLeft.toWorld( ll );
            upperRight.toWorld( ur );
            double radius = (ur - ll).length() / 2.0;
            float minRange = (float)(radius * _options.minTileRangeFactor().value());

            plod->setRange( 0, minRange, FLT_MAX );
            plod->setRange( 1, 0, minRange );
            plod->setRangeMode( osg::LOD::DISTANCE_FROM_EYE_POINT );
        }
        else
        {
            plod->setRange( 0, 0.0f, _options.tilePixelSize().value() );
            plod->setRange( 1, _options.tilePixelSize().value(), FLT_MAX );
            plod->setRangeMode( osg::LOD::PIXEL_SIZE_ON_SCREEN );
        }
        
#if 0 // TODO: reinstate this!

        // Install a tile-aligned bounding box in the pager node itself so we can do
        // visibility testing before paging in subtiles.
        plod->setChildBoundingBoxAndMatrix(
            1,
            surfaceNode->getTerrainBoundingBox(),
            surfaceNode->getLocalToWorldMatrix() );
#endif

#if USE_FILELOCATIONCALLBACK
        osgDB::Options* options = plod->getOrCreateDBOptions();
        options->setFileLocationCallback( new FileLocationCallback() );
#endif
        
        tileNode->addChild( plod );

        // Install a callback to reject back-facing tiles.
        if ( _frame.getMapInfo().isGeocentric() && _options.clusterCulling() == true )
        {
            const osg::HeightField* heightField = model->elevationModel()->getHeightField();
            if ( heightField )
            {
                tileNode->addCullCallback( HeightFieldUtils::createClusterCullingCallback(
                    heightField,
                    tileNode->getKey().getProfile()->getSRS()->getEllipsoid(),
                    *_options.verticalScale() ) );
            }
        }
    }
    else
    {
        tileNode->addChild( surfaceNode );
    }

    return tileNode;
}