osg::Node* GeometryCompiler::compile(FeatureList& workingSet, const Style& style, const FilterContext& context) { #ifdef PROFILING osg::Timer_t p_start = osg::Timer::instance()->tick(); unsigned p_features = workingSet.size(); #endif // for debugging/validation. std::vector<std::string> history; bool trackHistory = (_options.validate() == true); osg::ref_ptr<osg::Group> resultGroup = new osg::Group(); // create a filter context that will track feature data through the process FilterContext sharedCX = context; if ( !sharedCX.extent().isSet() && sharedCX.profile() ) { sharedCX.extent() = sharedCX.profile()->getExtent(); } // ref_ptr's to hold defaults in case we need them. osg::ref_ptr<PointSymbol> defaultPoint; osg::ref_ptr<LineSymbol> defaultLine; osg::ref_ptr<PolygonSymbol> defaultPolygon; // go through the Style and figure out which filters to use. const PointSymbol* point = style.get<PointSymbol>(); const LineSymbol* line = style.get<LineSymbol>(); const PolygonSymbol* polygon = style.get<PolygonSymbol>(); const ExtrusionSymbol* extrusion = style.get<ExtrusionSymbol>(); const AltitudeSymbol* altitude = style.get<AltitudeSymbol>(); const TextSymbol* text = style.get<TextSymbol>(); const MarkerSymbol* marker = style.get<MarkerSymbol>(); // to be deprecated const IconSymbol* icon = style.get<IconSymbol>(); const ModelSymbol* model = style.get<ModelSymbol>(); // Perform tessellation first. if ( line ) { if ( line->tessellation().isSet() ) { TemplateFeatureFilter<TessellateOperator> filter; filter.setNumPartitions( *line->tessellation() ); filter.setDefaultGeoInterp( _options.geoInterp().get() ); sharedCX = filter.push( workingSet, sharedCX ); if ( trackHistory ) history.push_back( "tessellation" ); } else if ( line->tessellationSize().isSet() ) { TemplateFeatureFilter<TessellateOperator> filter; filter.setMaxPartitionSize( *line->tessellationSize() ); filter.setDefaultGeoInterp( _options.geoInterp().get() ); sharedCX = filter.push( workingSet, sharedCX ); if ( trackHistory ) history.push_back( "tessellationSize" ); } } // if the style was empty, use some defaults based on the geometry type of the // first feature. if ( !point && !line && !polygon && !marker && !extrusion && !text && !model && !icon && workingSet.size() > 0 ) { Feature* first = workingSet.begin()->get(); Geometry* geom = first->getGeometry(); if ( geom ) { switch( geom->getComponentType() ) { case Geometry::TYPE_LINESTRING: case Geometry::TYPE_RING: defaultLine = new LineSymbol(); line = defaultLine.get(); break; case Geometry::TYPE_POINTSET: defaultPoint = new PointSymbol(); point = defaultPoint.get(); break; case Geometry::TYPE_POLYGON: defaultPolygon = new PolygonSymbol(); polygon = defaultPolygon.get(); break; case Geometry::TYPE_MULTI: case Geometry::TYPE_UNKNOWN: break; } } } // resample the geometry if necessary: if (_options.resampleMode().isSet()) { ResampleFilter resample; resample.resampleMode() = *_options.resampleMode(); if (_options.resampleMaxLength().isSet()) { resample.maxLength() = *_options.resampleMaxLength(); } sharedCX = resample.push( workingSet, sharedCX ); if ( trackHistory ) history.push_back( "resample" ); } // check whether we need to do elevation clamping: bool altRequired = _options.ignoreAltitudeSymbol() != true && altitude && ( altitude->clamping() != AltitudeSymbol::CLAMP_NONE || altitude->verticalOffset().isSet() || altitude->verticalScale().isSet() || altitude->script().isSet() ); // marker substitution -- to be deprecated in favor of model/icon if ( marker ) { if ( trackHistory ) history.push_back( "marker" ); // use a separate filter context since we'll be munging the data FilterContext markerCX = sharedCX; if ( marker->placement() == MarkerSymbol::PLACEMENT_RANDOM || marker->placement() == MarkerSymbol::PLACEMENT_INTERVAL ) { ScatterFilter scatter; scatter.setDensity( *marker->density() ); scatter.setRandom( marker->placement() == MarkerSymbol::PLACEMENT_RANDOM ); scatter.setRandomSeed( *marker->randomSeed() ); markerCX = scatter.push( workingSet, markerCX ); if ( trackHistory ) history.push_back( "scatter" ); } else if ( marker->placement() == MarkerSymbol::PLACEMENT_CENTROID ) { CentroidFilter centroid; markerCX = centroid.push( workingSet, markerCX ); if ( trackHistory ) history.push_back( "centroid" ); } if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); markerCX = clamp.push( workingSet, markerCX ); if ( trackHistory ) history.push_back( "altitude" ); // don't set this; we changed the input data. //altRequired = false; } SubstituteModelFilter sub( style ); sub.setClustering( *_options.clustering() ); sub.setUseDrawInstanced( *_options.instancing() ); if ( _options.featureName().isSet() ) sub.setFeatureNameExpr( *_options.featureName() ); osg::Node* node = sub.push( workingSet, markerCX ); if ( node ) { if ( trackHistory ) history.push_back( "substitute" ); resultGroup->addChild( node ); } } // instance substitution (replaces marker) else if ( model ) { const InstanceSymbol* instance = model ? (const InstanceSymbol*)model : (const InstanceSymbol*)icon; // use a separate filter context since we'll be munging the data FilterContext localCX = sharedCX; if ( trackHistory ) history.push_back( "model"); if ( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM || instance->placement() == InstanceSymbol::PLACEMENT_INTERVAL ) { ScatterFilter scatter; scatter.setDensity( *instance->density() ); scatter.setRandom( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM ); scatter.setRandomSeed( *instance->randomSeed() ); localCX = scatter.push( workingSet, localCX ); if ( trackHistory ) history.push_back( "scatter" ); } else if ( instance->placement() == InstanceSymbol::PLACEMENT_CENTROID ) { CentroidFilter centroid; localCX = centroid.push( workingSet, localCX ); if ( trackHistory ) history.push_back( "centroid" ); } if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); localCX = clamp.push( workingSet, localCX ); if ( trackHistory ) history.push_back( "altitude" ); } SubstituteModelFilter sub( style ); // activate clustering sub.setClustering( *_options.clustering() ); // activate draw-instancing sub.setUseDrawInstanced( *_options.instancing() ); // activate feature naming if ( _options.featureName().isSet() ) sub.setFeatureNameExpr( *_options.featureName() ); osg::Node* node = sub.push( workingSet, localCX ); if ( node ) { if ( trackHistory ) history.push_back( "substitute" ); resultGroup->addChild( node ); // enable auto scaling on the group? if ( model && model->autoScale() == true ) { resultGroup->getOrCreateStateSet()->setRenderBinDetails(0, osgEarth::AUTO_SCALE_BIN ); } } } // extruded geometry if ( extrusion ) { if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); sharedCX = clamp.push( workingSet, sharedCX ); if ( trackHistory ) history.push_back( "altitude" ); altRequired = false; } ExtrudeGeometryFilter extrude; extrude.setStyle( style ); // apply per-feature naming if requested. if ( _options.featureName().isSet() ) extrude.setFeatureNameExpr( *_options.featureName() ); if ( _options.mergeGeometry().isSet() ) extrude.setMergeGeometry( *_options.mergeGeometry() ); osg::Node* node = extrude.push( workingSet, sharedCX ); if ( node ) { if ( trackHistory ) history.push_back( "extrude" ); resultGroup->addChild( node ); } } // simple geometry else if ( point || line || polygon ) { if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); sharedCX = clamp.push( workingSet, sharedCX ); if ( trackHistory ) history.push_back( "altitude" ); altRequired = false; } BuildGeometryFilter filter( style ); filter.maxGranularity() = *_options.maxGranularity(); filter.geoInterp() = *_options.geoInterp(); if ( _options.featureName().isSet() ) filter.featureName() = *_options.featureName(); osg::Node* node = filter.push( workingSet, sharedCX ); if ( node ) { if ( trackHistory ) history.push_back( "geometry" ); resultGroup->addChild( node ); } } if ( text || icon ) { if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); sharedCX = clamp.push( workingSet, sharedCX ); if ( trackHistory ) history.push_back( "altitude" ); altRequired = false; } BuildTextFilter filter( style ); osg::Node* node = filter.push( workingSet, sharedCX ); if ( node ) { if ( trackHistory ) history.push_back( "text" ); resultGroup->addChild( node ); } } if (Registry::capabilities().supportsGLSL()) { if ( _options.shaderPolicy() == SHADERPOLICY_GENERATE ) { // no ss cache because we will optimize later. Registry::shaderGenerator().run( resultGroup.get(), "osgEarth.GeomCompiler" ); } else if ( _options.shaderPolicy() == SHADERPOLICY_DISABLE ) { resultGroup->getOrCreateStateSet()->setAttributeAndModes( new osg::Program(), osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE ); if ( trackHistory ) history.push_back( "no shaders" ); } } // Optimize stateset sharing. if ( _options.optimizeStateSharing() == true ) { // Common state set cache? osg::ref_ptr<StateSetCache> sscache; if ( sharedCX.getSession() ) { // with a shared cache, don't combine statesets. They may be // in the live graph sscache = sharedCX.getSession()->getStateSetCache(); sscache->consolidateStateAttributes( resultGroup.get() ); } else { // isolated: perform full optimization sscache = new StateSetCache(); sscache->optimize( resultGroup.get() ); } if ( trackHistory ) history.push_back( "share state" ); } if ( _options.optimize() == true ) { OE_DEBUG << LC << "optimize begin" << std::endl; // Run the optimizer on the resulting graph int optimizations = osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS | osgUtil::Optimizer::REMOVE_REDUNDANT_NODES | osgUtil::Optimizer::COMBINE_ADJACENT_LODS | osgUtil::Optimizer::SHARE_DUPLICATE_STATE | osgUtil::Optimizer::MERGE_GEOMETRY | osgUtil::Optimizer::CHECK_GEOMETRY | osgUtil::Optimizer::MERGE_GEODES | osgUtil::Optimizer::STATIC_OBJECT_DETECTION; osgUtil::Optimizer opt; opt.optimize(resultGroup.get(), optimizations); OE_DEBUG << LC << "optimize complete" << std::endl; if ( trackHistory ) history.push_back( "optimize" ); } //test: dump the tile to disk //osgDB::writeNodeFile( *(resultGroup.get()), "out.osg" ); #ifdef PROFILING static double totalTime = 0.0; static Threading::Mutex totalTimeMutex; osg::Timer_t p_end = osg::Timer::instance()->tick(); double t = osg::Timer::instance()->delta_s(p_start, p_end); totalTimeMutex.lock(); totalTime += t; totalTimeMutex.unlock(); OE_INFO << LC << "features = " << p_features << ", time = " << t << " s. cummulative = " << totalTime << " s." << std::endl; #endif if ( _options.validate() == true ) { OE_NOTICE << LC << "-- Start Debugging --\n"; std::stringstream buf; buf << "HISTORY "; for(std::vector<std::string>::iterator h = history.begin(); h != history.end(); ++h) buf << ".. " << *h; OE_NOTICE << LC << buf.str() << "\n"; osgEarth::GeometryValidator validator; resultGroup->accept(validator); OE_NOTICE << LC << "-- End Debugging --\n"; } return resultGroup.release(); }
osg::Node* Graticule::createGridLevel( unsigned int levelNum ) const { if ( !_map->isGeocentric() ) { OE_WARN << "Graticule: only supports geocentric maps" << std::endl; return 0L; } Graticule::Level level; if ( !getLevel( levelNum, level ) ) return 0L; OE_DEBUG << "Graticule: creating grid level " << levelNum << std::endl; osg::Group* group = new osg::Group(); const Profile* mapProfile = _map->getProfile(); const GeoExtent& pex = mapProfile->getExtent(); double tw = pex.width() / (double)level._cellsX; double th = pex.height() / (double)level._cellsY; for( unsigned int x=0; x<level._cellsX; ++x ) { for( unsigned int y=0; y<level._cellsY; ++y ) { GeoExtent tex( mapProfile->getSRS(), pex.xMin() + tw * (double)x, pex.yMin() + th * (double)y, pex.xMin() + tw * (double)(x+1), pex.yMin() + th * (double)(y+1) ); Geometry* geom = createCellGeometry( tex, level._lineWidth, pex, _map->isGeocentric() ); Feature* feature = new Feature(); feature->setGeometry( geom ); FeatureList features; features.push_back( feature ); FilterContext cx; cx.profile() = new FeatureProfile( tex ); //cx.isGeocentric() = _map->isGeocentric(); if ( _map->isGeocentric() ) { // We need to make sure that on a round globe, the points are sampled such that // long segments follow the curvature of the earth. ResampleFilter resample; resample.maxLength() = tex.width() / 10.0; cx = resample.push( features, cx ); } TransformFilter xform( mapProfile->getSRS() ); //xform.setMakeGeocentric( _map->isGeocentric() ); //xform.setLocalizeCoordinates( true ); cx = xform.push( features, cx ); osg::ref_ptr<osg::Node> output; BuildGeometryFilter bg; bg.setStyle( _lineStyle ); //cx = bg.push( features, cx ); output = bg.push( features, cx ); //.getNode(); if ( _map->isGeocentric() ) { // get the geocentric control point: double cplon, cplat, cpx, cpy, cpz; tex.getCentroid( cplon, cplat ); tex.getSRS()->getEllipsoid()->convertLatLongHeightToXYZ( osg::DegreesToRadians( cplat ), osg::DegreesToRadians( cplon ), 0.0, cpx, cpy, cpz ); osg::Vec3 controlPoint(cpx, cpy, cpz); // get the horizon point: tex.getSRS()->getEllipsoid()->convertLatLongHeightToXYZ( osg::DegreesToRadians( tex.yMin() ), osg::DegreesToRadians( tex.xMin() ), 0.0, cpx, cpy, cpz ); osg::Vec3 horizonPoint(cpx, cpy, cpz); // the deviation is the dot product of the control vector and the vector from the // control point to the horizon point. osg::Vec3 controlPointNorm = controlPoint; controlPointNorm.normalize(); osg::Vec3 horizonVecNorm = horizonPoint - controlPoint; horizonVecNorm.normalize(); float deviation = controlPointNorm * horizonVecNorm; // construct the culling callback using the deviation. osg::ClusterCullingCallback* ccc = new osg::ClusterCullingCallback(); ccc->set( controlPoint, controlPointNorm, deviation, (controlPoint-horizonPoint).length() ); // need a new group, because never put a cluster culler on a matrixtransform (doesn't work) osg::Group* me = new osg::Group(); me->setCullCallback( ccc ); me->addChild( output.get() ); output = me; } group->addChild( output.get() ); } } // organize it for better culling osgUtil::Optimizer opt; opt.optimize( group, osgUtil::Optimizer::SPATIALIZE_GROUPS ); osg::Node* result = group; if ( levelNum < getNumLevels() ) { Graticule::Level nextLevel; if ( getLevel( levelNum+1, nextLevel ) ) { osg::PagedLOD* plod = new osg::PagedLOD(); plod->addChild( group, nextLevel._maxRange, level._maxRange ); std::stringstream buf; buf << levelNum+1 << "_" << getID() << "." << GRID_MARKER << "." << GRATICLE_EXTENSION; std::string bufStr = buf.str(); plod->setFileName( 1, bufStr ); plod->setRange( 1, 0, nextLevel._maxRange ); result = plod; } } return result; }
osg::Node* GeometryCompiler::compile(FeatureList& workingSet, const Style& style, const FilterContext& context) { osg::ref_ptr<osg::Group> resultGroup = new osg::Group(); // create a filter context that will track feature data through the process FilterContext sharedCX = context; if ( !sharedCX.extent().isSet() ) sharedCX.extent() = sharedCX.profile()->getExtent(); // only localize coordinates if the map is geocentric AND the extent is // less than 180 degrees. const MapInfo& mi = sharedCX.getSession()->getMapInfo(); GeoExtent workingExtent = sharedCX.extent()->transform( sharedCX.profile()->getSRS()->getGeographicSRS() ); bool localize = mi.isGeocentric() && workingExtent.width() < 180.0; // go through the Style and figure out which filters to use. const MarkerSymbol* marker = style.get<MarkerSymbol>(); const PointSymbol* point = style.get<PointSymbol>(); const LineSymbol* line = style.get<LineSymbol>(); const PolygonSymbol* polygon = style.get<PolygonSymbol>(); const ExtrusionSymbol* extrusion = style.get<ExtrusionSymbol>(); const AltitudeSymbol* altitude = style.get<AltitudeSymbol>(); const TextSymbol* text = style.get<TextSymbol>(); if (_options.resampleMode().isSet()) { ResampleFilter resample; resample.resampleMode() = *_options.resampleMode(); if (_options.resampleMaxLength().isSet()) { resample.maxLength() = *_options.resampleMaxLength(); } sharedCX = resample.push( workingSet, sharedCX ); } bool clampRequired = altitude && altitude->clamping() != AltitudeSymbol::CLAMP_NONE; // transform the features into the map profile TransformFilter xform( mi.getProfile()->getSRS(), mi.isGeocentric() ); xform.setLocalizeCoordinates( localize ); if ( altitude && altitude->verticalOffset().isSet() && !clampRequired ) xform.setMatrix( osg::Matrixd::translate(0, 0, *altitude->verticalOffset()) ); sharedCX = xform.push( workingSet, sharedCX ); // model substitution if ( marker ) { // use a separate filter context since we'll be munging the data FilterContext markerCX = sharedCX; if ( marker->placement() == MarkerSymbol::PLACEMENT_RANDOM || marker->placement() == MarkerSymbol::PLACEMENT_INTERVAL ) { ScatterFilter scatter; scatter.setDensity( *marker->density() ); scatter.setRandom( marker->placement() == MarkerSymbol::PLACEMENT_RANDOM ); scatter.setRandomSeed( *marker->randomSeed() ); markerCX = scatter.push( workingSet, markerCX ); } if ( clampRequired ) { ClampFilter clamp; clamp.setIgnoreZ( altitude->clamping() == AltitudeSymbol::CLAMP_TO_TERRAIN ); clamp.setOffsetZ( *altitude->verticalOffset() ); markerCX = clamp.push( workingSet, markerCX ); // don't set this; we changed the input data. //clampRequired = false; } SubstituteModelFilter sub( style ); sub.setClustering( *_options.clustering() ); if ( marker->scale().isSet() ) sub.setModelMatrix( osg::Matrixd::scale( *marker->scale() ) ); if ( _options.featureName().isSet() ) sub.setFeatureNameExpr( *_options.featureName() ); markerCX = sub.push( workingSet, markerCX ); osg::Node* node = sub.getNode(); if ( node ) { if ( markerCX.hasReferenceFrame() ) { osg::MatrixTransform* delocalizer = new osg::MatrixTransform( markerCX.inverseReferenceFrame() ); delocalizer->addChild( node ); node = delocalizer; } resultGroup->addChild( node ); } } // extruded geometry if ( extrusion && ( line || polygon ) ) { if ( clampRequired ) { ClampFilter clamp; clamp.setIgnoreZ( altitude->clamping() == AltitudeSymbol::CLAMP_TO_TERRAIN ); if ( extrusion->heightReference() == ExtrusionSymbol::HEIGHT_REFERENCE_MSL ) clamp.setMaxZAttributeName( "__max_z"); clamp.setOffsetZ( *altitude->verticalOffset() ); sharedCX = clamp.push( workingSet, sharedCX ); clampRequired = false; } ExtrudeGeometryFilter extrude; if ( extrusion ) { if ( extrusion->height().isSet() ) extrude.setExtrusionHeight( *extrusion->height() ); if ( extrusion->heightExpression().isSet() ) extrude.setExtrusionExpr( *extrusion->heightExpression() ); if ( extrusion->heightReference() == ExtrusionSymbol::HEIGHT_REFERENCE_MSL ) extrude.setHeightOffsetExpression( NumericExpression("[__max_z]") ); if ( _options.featureName().isSet() ) extrude.setFeatureNameExpr( *_options.featureName() ); extrude.setFlatten( *extrusion->flatten() ); } if ( polygon ) { extrude.setColor( polygon->fill()->color() ); } osg::Node* node = extrude.push( workingSet, sharedCX ); if ( node ) { if ( sharedCX.hasReferenceFrame() ) { osg::MatrixTransform* delocalizer = new osg::MatrixTransform( sharedCX.inverseReferenceFrame() ); delocalizer->addChild( node ); node = delocalizer; } resultGroup->addChild( node ); } } // simple geometry else if ( point || line || polygon ) { if ( clampRequired ) { ClampFilter clamp; clamp.setIgnoreZ( altitude->clamping() == AltitudeSymbol::CLAMP_TO_TERRAIN ); clamp.setOffsetZ( *altitude->verticalOffset() ); sharedCX = clamp.push( workingSet, sharedCX ); clampRequired = false; } BuildGeometryFilter filter( style ); if ( _options.maxGranularity().isSet() ) filter.maxGranularity() = *_options.maxGranularity(); if ( _options.geoInterp().isSet() ) filter.geoInterp() = *_options.geoInterp(); if ( _options.mergeGeometry().isSet() ) filter.mergeGeometry() = *_options.mergeGeometry(); if ( _options.featureName().isSet() ) filter.featureName() = *_options.featureName(); sharedCX = filter.push( workingSet, sharedCX ); osg::Node* node = filter.getNode(); if ( node ) { if ( sharedCX.hasReferenceFrame() ) { osg::MatrixTransform* delocalizer = new osg::MatrixTransform( sharedCX.inverseReferenceFrame() ); delocalizer->addChild( node ); node = delocalizer; } resultGroup->addChild( node ); } } if ( text ) { if ( clampRequired ) { ClampFilter clamp; clamp.setIgnoreZ( altitude->clamping() == AltitudeSymbol::CLAMP_TO_TERRAIN ); clamp.setOffsetZ( *altitude->verticalOffset() ); sharedCX = clamp.push( workingSet, sharedCX ); clampRequired = false; } BuildTextFilter filter( style ); sharedCX = filter.push( workingSet, sharedCX ); osg::Node* node = filter.takeNode(); if ( node ) { if ( sharedCX.hasReferenceFrame() ) { osg::MatrixTransform* delocalizer = new osg::MatrixTransform( sharedCX.inverseReferenceFrame() ); delocalizer->addChild( node ); node = delocalizer; } resultGroup->addChild( node ); } } //else // insufficient symbology //{ // OE_WARN << LC << "Insufficient symbology; no geometry created" << std::endl; //} #if 0 // install the localization transform if necessary. if ( cx.hasReferenceFrame() ) { osg::MatrixTransform* delocalizer = new osg::MatrixTransform( cx.inverseReferenceFrame() ); delocalizer->addChild( resultGroup.get() ); resultGroup = delocalizer; } #endif resultGroup->getOrCreateStateSet()->setMode( GL_BLEND, 1 ); //osgDB::writeNodeFile( *(resultGroup.get()), "out.osg" ); return resultGroup.release(); }
//override bool renderFeaturesForStyle( const Style& style, const FeatureList& features, osg::Referenced* buildData, const GeoExtent& imageExtent, osg::Image* image ) { // A processing context to use with the filters: FilterContext context; context.setProfile( getFeatureSource()->getFeatureProfile() ); const LineSymbol* masterLine = style.getSymbol<LineSymbol>(); const PolygonSymbol* masterPoly = style.getSymbol<PolygonSymbol>(); // sort into bins, making a copy for lines that require buffering. FeatureList polygons; FeatureList lines; for(FeatureList::const_iterator f = features.begin(); f != features.end(); ++f) { if ( f->get()->getGeometry() ) { if ( masterPoly || f->get()->style()->has<PolygonSymbol>() ) { polygons.push_back( f->get() ); } if ( masterLine || f->get()->style()->has<LineSymbol>() ) { Feature* newFeature = new Feature( *f->get() ); if ( !newFeature->getGeometry()->isLinear() ) { newFeature->setGeometry( newFeature->getGeometry()->cloneAs(Geometry::TYPE_RING) ); } lines.push_back( newFeature ); } } } // initialize: RenderFrame frame; frame.xmin = imageExtent.xMin(); frame.ymin = imageExtent.yMin(); frame.xf = (double)image->s() / imageExtent.width(); frame.yf = (double)image->t() / imageExtent.height(); if ( lines.size() > 0 ) { // We are buffering in the features native extent, so we need to use the // transformed extent to get the proper "resolution" for the image const SpatialReference* featureSRS = context.profile()->getSRS(); GeoExtent transformedExtent = imageExtent.transform(featureSRS); double trans_xf = (double)image->s() / transformedExtent.width(); double trans_yf = (double)image->t() / transformedExtent.height(); // resolution of the image (pixel extents): double xres = 1.0/trans_xf; double yres = 1.0/trans_yf; // downsample the line data so that it is no higher resolution than to image to which // we intend to rasterize it. If you don't do this, you run the risk of the buffer // operation taking forever on very high-res input data. if ( _options.optimizeLineSampling() == true ) { ResampleFilter resample; resample.minLength() = osg::minimum( xres, yres ); context = resample.push( lines, context ); } // now run the buffer operation on all lines: BufferFilter buffer; double lineWidth = 1.0; if ( masterLine ) { buffer.capStyle() = masterLine->stroke()->lineCap().value(); if ( masterLine->stroke()->width().isSet() ) { lineWidth = masterLine->stroke()->width().value(); GeoExtent imageExtentInFeatureSRS = imageExtent.transform(featureSRS); double pixelWidth = imageExtentInFeatureSRS.width() / (double)image->s(); // if the width units are specified, process them: if (masterLine->stroke()->widthUnits().isSet() && masterLine->stroke()->widthUnits().get() != Units::PIXELS) { const Units& featureUnits = featureSRS->getUnits(); const Units& strokeUnits = masterLine->stroke()->widthUnits().value(); // if the units are different than those of the feature data, we need to // do a units conversion. if ( featureUnits != strokeUnits ) { if ( Units::canConvert(strokeUnits, featureUnits) ) { // linear to linear, no problem lineWidth = strokeUnits.convertTo( featureUnits, lineWidth ); } else if ( strokeUnits.isLinear() && featureUnits.isAngular() ) { // linear to angular? approximate degrees per meter at the // latitude of the tile's centroid. lineWidth = masterLine->stroke()->widthUnits()->convertTo(Units::METERS, lineWidth); double circ = featureSRS->getEllipsoid()->getRadiusEquator() * 2.0 * osg::PI; double x, y; context.profile()->getExtent().getCentroid(x, y); double radians = (lineWidth/circ) * cos(osg::DegreesToRadians(y)); lineWidth = osg::RadiansToDegrees(radians); } } // enfore a minimum width of one pixel. float minPixels = masterLine->stroke()->minPixels().getOrUse( 1.0f ); lineWidth = osg::clampAbove(lineWidth, pixelWidth*minPixels); } else // pixels { lineWidth *= pixelWidth; } } } buffer.distance() = lineWidth * 0.5; // since the distance is for one side buffer.push( lines, context ); } // Transform the features into the map's SRS: TransformFilter xform( imageExtent.getSRS() ); xform.setLocalizeCoordinates( false ); FilterContext polysContext = xform.push( polygons, context ); FilterContext linesContext = xform.push( lines, context ); // set up the AGG renderer: agg::rendering_buffer rbuf( image->data(), image->s(), image->t(), image->s()*4 ); // Create the renderer and the rasterizer agg::renderer<agg::span_abgr32> ren(rbuf); agg::rasterizer ras; // Setup the rasterizer ras.gamma(1.3); ras.filling_rule(agg::fill_even_odd); // construct an extent for cropping the geometry to our tile. // extend just outside the actual extents so we don't get edge artifacts: GeoExtent cropExtent = GeoExtent(imageExtent); cropExtent.scale(1.1, 1.1); osg::ref_ptr<Symbology::Polygon> cropPoly = new Symbology::Polygon( 4 ); cropPoly->push_back( osg::Vec3d( cropExtent.xMin(), cropExtent.yMin(), 0 )); cropPoly->push_back( osg::Vec3d( cropExtent.xMax(), cropExtent.yMin(), 0 )); cropPoly->push_back( osg::Vec3d( cropExtent.xMax(), cropExtent.yMax(), 0 )); cropPoly->push_back( osg::Vec3d( cropExtent.xMin(), cropExtent.yMax(), 0 )); // render the polygons for(FeatureList::iterator i = polygons.begin(); i != polygons.end(); i++) { Feature* feature = i->get(); Geometry* geometry = feature->getGeometry(); osg::ref_ptr<Geometry> croppedGeometry; if ( geometry->crop( cropPoly.get(), croppedGeometry ) ) { const PolygonSymbol* poly = feature->style().isSet() && feature->style()->has<PolygonSymbol>() ? feature->style()->get<PolygonSymbol>() : masterPoly; const osg::Vec4 color = poly ? static_cast<osg::Vec4>(poly->fill()->color()) : osg::Vec4(1,1,1,1); rasterize(croppedGeometry.get(), color, frame, ras, ren); } } // render the lines for(FeatureList::iterator i = lines.begin(); i != lines.end(); i++) { Feature* feature = i->get(); Geometry* geometry = feature->getGeometry(); osg::ref_ptr<Geometry> croppedGeometry; if ( geometry->crop( cropPoly.get(), croppedGeometry ) ) { const LineSymbol* line = feature->style().isSet() && feature->style()->has<LineSymbol>() ? feature->style()->get<LineSymbol>() : masterLine; const osg::Vec4 color = line ? static_cast<osg::Vec4>(line->stroke()->color()) : osg::Vec4(1,1,1,1); rasterize(croppedGeometry.get(), color, frame, ras, ren); } } return true; }
osg::Node* GeometryCompiler::compile(FeatureList& workingSet, const Style& style, const FilterContext& context) { #ifdef PROFILING osg::Timer_t p_start = osg::Timer::instance()->tick(); unsigned p_features = workingSet.size(); #endif osg::ref_ptr<osg::Group> resultGroup = new osg::Group(); // create a filter context that will track feature data through the process FilterContext sharedCX = context; if ( !sharedCX.extent().isSet() && sharedCX.profile() ) { sharedCX.extent() = sharedCX.profile()->getExtent(); } // ref_ptr's to hold defaults in case we need them. osg::ref_ptr<PointSymbol> defaultPoint; osg::ref_ptr<LineSymbol> defaultLine; osg::ref_ptr<PolygonSymbol> defaultPolygon; // go through the Style and figure out which filters to use. const PointSymbol* point = style.get<PointSymbol>(); const LineSymbol* line = style.get<LineSymbol>(); const PolygonSymbol* polygon = style.get<PolygonSymbol>(); const ExtrusionSymbol* extrusion = style.get<ExtrusionSymbol>(); const AltitudeSymbol* altitude = style.get<AltitudeSymbol>(); const TextSymbol* text = style.get<TextSymbol>(); const MarkerSymbol* marker = style.get<MarkerSymbol>(); // to be deprecated const IconSymbol* icon = style.get<IconSymbol>(); const ModelSymbol* model = style.get<ModelSymbol>(); // check whether we need tessellation: if ( line && line->tessellation().isSet() ) { TemplateFeatureFilter<TessellateOperator> filter; filter.setNumPartitions( *line->tessellation() ); sharedCX = filter.push( workingSet, sharedCX ); } // if the style was empty, use some defaults based on the geometry type of the // first feature. if ( !point && !line && !polygon && !marker && !extrusion && !text && !model && !icon && workingSet.size() > 0 ) { Feature* first = workingSet.begin()->get(); Geometry* geom = first->getGeometry(); if ( geom ) { switch( geom->getComponentType() ) { case Geometry::TYPE_LINESTRING: case Geometry::TYPE_RING: defaultLine = new LineSymbol(); line = defaultLine.get(); break; case Geometry::TYPE_POINTSET: defaultPoint = new PointSymbol(); point = defaultPoint.get(); break; case Geometry::TYPE_POLYGON: defaultPolygon = new PolygonSymbol(); polygon = defaultPolygon.get(); break; case Geometry::TYPE_MULTI: case Geometry::TYPE_UNKNOWN: break; } } } // resample the geometry if necessary: if (_options.resampleMode().isSet()) { ResampleFilter resample; resample.resampleMode() = *_options.resampleMode(); if (_options.resampleMaxLength().isSet()) { resample.maxLength() = *_options.resampleMaxLength(); } sharedCX = resample.push( workingSet, sharedCX ); } // check whether we need to do elevation clamping: bool altRequired = _options.ignoreAltitudeSymbol() != true && altitude && ( altitude->clamping() != AltitudeSymbol::CLAMP_NONE || altitude->verticalOffset().isSet() || altitude->verticalScale().isSet() || altitude->script().isSet() ); // marker substitution -- to be deprecated in favor of model/icon if ( marker ) { // use a separate filter context since we'll be munging the data FilterContext markerCX = sharedCX; if ( marker->placement() == MarkerSymbol::PLACEMENT_RANDOM || marker->placement() == MarkerSymbol::PLACEMENT_INTERVAL ) { ScatterFilter scatter; scatter.setDensity( *marker->density() ); scatter.setRandom( marker->placement() == MarkerSymbol::PLACEMENT_RANDOM ); scatter.setRandomSeed( *marker->randomSeed() ); markerCX = scatter.push( workingSet, markerCX ); } else if ( marker->placement() == MarkerSymbol::PLACEMENT_CENTROID ) { CentroidFilter centroid; centroid.push( workingSet, markerCX ); } if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); markerCX = clamp.push( workingSet, markerCX ); // don't set this; we changed the input data. //altRequired = false; } SubstituteModelFilter sub( style ); sub.setClustering( *_options.clustering() ); sub.setUseDrawInstanced( *_options.instancing() ); if ( _options.featureName().isSet() ) sub.setFeatureNameExpr( *_options.featureName() ); osg::Node* node = sub.push( workingSet, markerCX ); if ( node ) { resultGroup->addChild( node ); } } // instance substitution (replaces marker) else if ( model ) { const InstanceSymbol* instance = model ? (const InstanceSymbol*)model : (const InstanceSymbol*)icon; // use a separate filter context since we'll be munging the data FilterContext localCX = sharedCX; if ( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM || instance->placement() == InstanceSymbol::PLACEMENT_INTERVAL ) { ScatterFilter scatter; scatter.setDensity( *instance->density() ); scatter.setRandom( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM ); scatter.setRandomSeed( *instance->randomSeed() ); localCX = scatter.push( workingSet, localCX ); } else if ( instance->placement() == InstanceSymbol::PLACEMENT_CENTROID ) { CentroidFilter centroid; centroid.push( workingSet, localCX ); } if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); localCX = clamp.push( workingSet, localCX ); } SubstituteModelFilter sub( style ); // activate clustering sub.setClustering( *_options.clustering() ); // activate draw-instancing sub.setUseDrawInstanced( *_options.instancing() ); // activate feature naming if ( _options.featureName().isSet() ) sub.setFeatureNameExpr( *_options.featureName() ); osg::Node* node = sub.push( workingSet, localCX ); if ( node ) { resultGroup->addChild( node ); // enable auto scaling on the group? if ( model && model->autoScale() == true ) { resultGroup->getOrCreateStateSet()->setRenderBinDetails(0, osgEarth::AUTO_SCALE_BIN ); } } } // extruded geometry if ( extrusion ) { if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); sharedCX = clamp.push( workingSet, sharedCX ); altRequired = false; } ExtrudeGeometryFilter extrude; extrude.setStyle( style ); // Activate texture arrays if the GPU supports them and if the user // hasn't disabled them. extrude.useTextureArrays() = Registry::capabilities().supportsTextureArrays() && _options.useTextureArrays() == true; // apply per-feature naming if requested. if ( _options.featureName().isSet() ) extrude.setFeatureNameExpr( *_options.featureName() ); if ( _options.useVertexBufferObjects().isSet()) extrude.useVertexBufferObjects() = *_options.useVertexBufferObjects(); osg::Node* node = extrude.push( workingSet, sharedCX ); if ( node ) { resultGroup->addChild( node ); } } // simple geometry else if ( point || line || polygon ) { if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); sharedCX = clamp.push( workingSet, sharedCX ); altRequired = false; } BuildGeometryFilter filter( style ); if ( _options.maxGranularity().isSet() ) filter.maxGranularity() = *_options.maxGranularity(); if ( _options.geoInterp().isSet() ) filter.geoInterp() = *_options.geoInterp(); if ( _options.featureName().isSet() ) filter.featureName() = *_options.featureName(); osg::Node* node = filter.push( workingSet, sharedCX ); if ( node ) { resultGroup->addChild( node ); } } if ( text || icon ) { if ( altRequired ) { AltitudeFilter clamp; clamp.setPropertiesFromStyle( style ); sharedCX = clamp.push( workingSet, sharedCX ); altRequired = false; } BuildTextFilter filter( style ); osg::Node* node = filter.push( workingSet, sharedCX ); if ( node ) { resultGroup->addChild( node ); } } if (Registry::capabilities().supportsGLSL()) { if ( _options.shaderPolicy() == SHADERPOLICY_GENERATE ) { // no ss cache because we will optimize later. Registry::shaderGenerator().run( resultGroup.get(), "osgEarth.GeomCompiler" ); } else if ( _options.shaderPolicy() == SHADERPOLICY_DISABLE ) { resultGroup->getOrCreateStateSet()->setAttributeAndModes( new osg::Program(), osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE ); } } // Optimize stateset sharing. if ( _options.optimizeStateSharing() == true ) { // Common state set cache? osg::ref_ptr<StateSetCache> sscache; if ( sharedCX.getSession() ) { // with a shared cache, don't combine statesets. They may be // in the live graph sscache = sharedCX.getSession()->getStateSetCache(); sscache->consolidateStateAttributes( resultGroup.get() ); } else { // isolated: perform full optimization sscache = new StateSetCache(); sscache->optimize( resultGroup.get() ); } } //test: dump the tile to disk //osgDB::writeNodeFile( *(resultGroup.get()), "out.osg" ); #ifdef PROFILING osg::Timer_t p_end = osg::Timer::instance()->tick(); OE_INFO << LC << "features = " << p_features << ", time = " << osg::Timer::instance()->delta_s(p_start, p_end) << " s." << std::endl; #endif return resultGroup.release(); }
//override bool renderFeaturesForStyle( const Style& style, const FeatureList& inFeatures, osg::Referenced* buildData, const GeoExtent& imageExtent, osg::Image* image ) { // local copy of the features that we can process FeatureList features = inFeatures; BuildData* bd = static_cast<BuildData*>( buildData ); // A processing context to use with the filters: FilterContext context; context.profile() = getFeatureSource()->getFeatureProfile(); const LineSymbol* masterLine = style.getSymbol<LineSymbol>(); const PolygonSymbol* masterPoly = style.getSymbol<PolygonSymbol>(); //bool embeddedStyles = getFeatureSource()->hasEmbeddedStyles(); // if only a line symbol exists, and there are polygons in the mix, draw them // as outlines (line rings). //OE_INFO << LC << "Line Symbol = " << (masterLine == 0L ? "null" : masterLine->getConfig().toString()) << std::endl; //OE_INFO << LC << "Poly SYmbol = " << (masterPoly == 0L ? "null" : masterPoly->getConfig().toString()) << std::endl; //bool convertPolysToRings = poly == 0L && line != 0L; //if ( convertPolysToRings ) // OE_INFO << LC << "No PolygonSymbol; will draw polygons to rings" << std::endl; // initialize: double xmin = imageExtent.xMin(); double ymin = imageExtent.yMin(); //double s = (double)image->s(); //double t = (double)image->t(); double xf = (double)image->s() / imageExtent.width(); double yf = (double)image->t() / imageExtent.height(); // strictly speaking we should iterate over the features and buffer each one that's a line, // rather then checking for the existence of a LineSymbol. FeatureList linesToBuffer; for(FeatureList::iterator i = features.begin(); i != features.end(); i++) { Feature* feature = i->get(); Geometry* geom = feature->getGeometry(); if ( geom ) { // check for an embedded style: const LineSymbol* line = feature->style().isSet() ? feature->style()->getSymbol<LineSymbol>() : masterLine; const PolygonSymbol* poly = feature->style().isSet() ? feature->style()->getSymbol<PolygonSymbol>() : masterPoly; // if we have polygons but only a LineSymbol, draw the poly as a line. if ( geom->getComponentType() == Geometry::TYPE_POLYGON ) { if ( !poly && line ) { Feature* outline = new Feature( *feature ); geom = geom->cloneAs( Geometry::TYPE_RING ); outline->setGeometry( geom ); *i = outline; feature = outline; } //TODO: fix to enable outlined polys. doesn't work, not sure why -gw //else if ( poly && line ) //{ // Feature* outline = new Feature(); // geom = geom->cloneAs( Geometry::TYPE_LINESTRING ); // outline->setGeometry( geom ); // features.push_back( outline ); //} } bool needsBuffering = geom->getComponentType() == Geometry::TYPE_LINESTRING || geom->getComponentType() == Geometry::TYPE_RING; if ( needsBuffering ) { linesToBuffer.push_back( feature ); } } } if ( linesToBuffer.size() > 0 ) { //We are buffering in the features native extent, so we need to use the transform extent to get the proper "resolution" for the image GeoExtent transformedExtent = imageExtent.transform(context.profile()->getSRS()); double trans_xf = (double)image->s() / transformedExtent.width(); double trans_yf = (double)image->t() / transformedExtent.height(); // resolution of the image (pixel extents): double xres = 1.0/trans_xf; double yres = 1.0/trans_yf; // downsample the line data so that it is no higher resolution than to image to which // we intend to rasterize it. If you don't do this, you run the risk of the buffer // operation taking forever on very high-res input data. if ( _options.optimizeLineSampling() == true ) { ResampleFilter resample; resample.minLength() = osg::minimum( xres, yres ); context = resample.push( linesToBuffer, context ); } // now run the buffer operation on all lines: BufferFilter buffer; float lineWidth = 0.5; if ( masterLine ) { buffer.capStyle() = masterLine->stroke()->lineCap().value(); if ( masterLine->stroke()->width().isSet() ) lineWidth = masterLine->stroke()->width().value(); } // "relative line size" means that the line width is expressed in (approx) pixels // rather than in map units if ( _options.relativeLineSize() == true ) buffer.distance() = xres * lineWidth; else buffer.distance() = lineWidth; buffer.push( linesToBuffer, context ); } // First, transform the features into the map's SRS: TransformFilter xform( imageExtent.getSRS() ); xform.setLocalizeCoordinates( false ); context = xform.push( features, context ); // set up the AGG renderer: agg::rendering_buffer rbuf( image->data(), image->s(), image->t(), image->s()*4 ); // Create the renderer and the rasterizer agg::renderer<agg::span_abgr32> ren(rbuf); agg::rasterizer ras; // Setup the rasterizer ras.gamma(1.3); ras.filling_rule(agg::fill_even_odd); GeoExtent cropExtent = GeoExtent(imageExtent); cropExtent.scale(1.1, 1.1); osg::ref_ptr<Symbology::Polygon> cropPoly = new Symbology::Polygon( 4 ); cropPoly->push_back( osg::Vec3d( cropExtent.xMin(), cropExtent.yMin(), 0 )); cropPoly->push_back( osg::Vec3d( cropExtent.xMax(), cropExtent.yMin(), 0 )); cropPoly->push_back( osg::Vec3d( cropExtent.xMax(), cropExtent.yMax(), 0 )); cropPoly->push_back( osg::Vec3d( cropExtent.xMin(), cropExtent.yMax(), 0 )); double lineWidth = 1.0; if ( masterLine ) lineWidth = (double)masterLine->stroke()->width().value(); osg::Vec4 color = osg::Vec4(1, 1, 1, 1); if ( masterLine ) color = masterLine->stroke()->color(); // render the features for(FeatureList::iterator i = features.begin(); i != features.end(); i++) { Feature* feature = i->get(); //bool first = bd->_pass == 0 && i == features.begin(); Geometry* geometry = feature->getGeometry(); osg::ref_ptr< Geometry > croppedGeometry; if ( ! geometry->crop( cropPoly.get(), croppedGeometry ) ) continue; // set up a default color: osg::Vec4 c = color; unsigned int a = (unsigned int)(127+(c.a()*255)/2); // scale alpha up agg::rgba8 fgColor( (unsigned int)(c.r()*255), (unsigned int)(c.g()*255), (unsigned int)(c.b()*255), a ); GeometryIterator gi( croppedGeometry.get() ); while( gi.hasMore() ) { c = color; Geometry* g = gi.next(); const LineSymbol* line = feature->style().isSet() ? feature->style()->getSymbol<LineSymbol>() : masterLine; const PolygonSymbol* poly = feature->style().isSet() ? feature->style()->getSymbol<PolygonSymbol>() : masterPoly; if (g->getType() == Geometry::TYPE_RING || g->getType() == Geometry::TYPE_LINESTRING) { if ( line ) c = line->stroke()->color(); else if ( poly ) c = poly->fill()->color(); } else if ( g->getType() == Geometry::TYPE_POLYGON ) { if ( poly ) c = poly->fill()->color(); else if ( line ) c = line->stroke()->color(); } a = (unsigned int)(127+(c.a()*255)/2); // scale alpha up fgColor = agg::rgba8( (unsigned int)(c.r()*255), (unsigned int)(c.g()*255), (unsigned int)(c.b()*255), a ); ras.filling_rule( agg::fill_even_odd ); for( Geometry::iterator p = g->begin(); p != g->end(); p++ ) { const osg::Vec3d& p0 = *p; double x0 = xf*(p0.x()-xmin); double y0 = yf*(p0.y()-ymin); //const osg::Vec3d& p1 = p+1 != g->end()? *(p+1) : g->front(); //double x1 = xf*(p1.x()-xmin); //double y1 = yf*(p1.y()-ymin); if ( p == g->begin() ) ras.move_to_d( x0, y0 ); else ras.line_to_d( x0, y0 ); } } ras.render(ren, fgColor); ras.reset(); } bd->_pass++; return true; }
osg::Node* Graticule::createGridLevel( unsigned int levelNum ) const { if ( !_map->isGeocentric() ) { OE_WARN << "Graticule: only supports geocentric maps" << std::endl; return 0L; } Graticule::Level level; if ( !getLevel( levelNum, level ) ) return 0L; OE_DEBUG << "Graticule: creating grid level " << levelNum << std::endl; osg::Group* group = new osg::Group(); const Profile* mapProfile = _map->getProfile(); const GeoExtent& pex = mapProfile->getExtent(); double tw = pex.width() / (double)level._cellsX; double th = pex.height() / (double)level._cellsY; for( unsigned int x=0; x<level._cellsX; ++x ) { for( unsigned int y=0; y<level._cellsY; ++y ) { GeoExtent tex( mapProfile->getSRS(), pex.xMin() + tw * (double)x, pex.yMin() + th * (double)y, pex.xMin() + tw * (double)(x+1), pex.yMin() + th * (double)(y+1) ); double ox = level._lineWidth; double oy = level._lineWidth; Geometry* geom = createCellGeometry( tex, level._lineWidth, pex, _map->isGeocentric() ); Feature* feature = new Feature(); feature->setGeometry( geom ); FeatureList features; features.push_back( feature ); FilterContext cx; cx.profile() = new FeatureProfile( tex ); cx.isGeocentric() = _map->isGeocentric(); if ( _map->isGeocentric() ) { // We need to make sure that on a round globe, the points are sampled such that // long segments follow the curvature of the earth. ResampleFilter resample; resample.maxLength() = tex.width() / 10.0; resample.perturbationThreshold() = level._lineWidth/1000.0; cx = resample.push( features, cx ); } TransformFilter xform( mapProfile->getSRS() ); xform.setMakeGeocentric( _map->isGeocentric() ); cx = xform.push( features, cx ); Bounds bounds = feature->getGeometry()->getBounds(); double exDist = bounds.radius()/2.0; osg::Node* cellVolume = createVolume( feature->getGeometry(), -exDist, exDist*2, cx ); osg::Node* child = cellVolume; if ( cx.hasReferenceFrame() ) { osg::MatrixTransform* xform = new osg::MatrixTransform( cx.inverseReferenceFrame() ); xform->addChild( child ); // the transform matrix here does NOT include a rotation, so we need to get the normal // for the cull plane callback. osg::Vec3d normal = xform->getBound().center(); xform->setCullCallback( new CullPlaneCallback( normal ) ); child = xform; } group->addChild( child ); } } // organize it for better culling osgUtil::Optimizer opt; opt.optimize( group, osgUtil::Optimizer::SPATIALIZE_GROUPS ); osg::Node* result = group; if ( levelNum+1 < getNumLevels() ) { Graticule::Level nextLevel; if ( getLevel( levelNum+1, nextLevel ) ) { osg::PagedLOD* plod = new osg::PagedLOD(); plod->addChild( group, nextLevel._maxRange, level._maxRange ); std::stringstream buf; buf << levelNum+1 << "_" << getID() << "." << GRID_MARKER << "." << GRATICLE_EXTENSION; std::string bufStr = buf.str(); plod->setFileName( 1, bufStr ); plod->setRange( 1, 0, nextLevel._maxRange ); result = plod; } } return result; }