void
OSGTerrainEngineNode::updateTextureCombining()
{
    if ( _texCompositor.valid() )
    {
        int numImageLayers = _update_mapf->imageLayers().size();
        osg::StateSet* terrainStateSet = _terrain->getOrCreateStateSet();

        if ( _texCompositor->usesShaderComposition() )
        {
            // Creates or updates the shader components that are generated by the texture compositor.
            // These components reside in the CustomTerrain's stateset, and override the components
            // installed in the VP on the engine-node's stateset in installShaders().

            VirtualProgram* vp = new VirtualProgram() ;
            vp->setName( "engine_osgterrain:TerrainNode" );
            vp->installDefaultColoringShaders(numImageLayers);

            terrainStateSet->setAttributeAndModes( vp, osg::StateAttribute::ON );

            // first, update the default shader components based on the new layer count:
            const ShaderFactory* sf = Registry::instance()->getShaderFactory();
            
            // second, install the per-layer color filter functions.
            for( int i=0; i<numImageLayers; ++i )
            {
                std::string layerFilterFunc = Stringify() << "osgearth_runColorFilters_" << i;
                const ColorFilterChain& chain = _update_mapf->getImageLayerAt(i)->getColorFilters();

                // install the wrapper function that calls all the filters in turn:
                vp->setShader( layerFilterFunc, sf->createColorFilterChainFragmentShader(layerFilterFunc, chain) );

                // install each of the filter entry points:
                for( ColorFilterChain::const_iterator j = chain.begin(); j != chain.end(); ++j )
                {
                    const ColorFilter* filter = j->get();
                    filter->install( terrainStateSet );
                }
            }
        }

        // next, inform the compositor that it needs to update based on a new layer count:
        _texCompositor->updateMasterStateSet( terrainStateSet ); //, numImageLayers );
    }
}
osg::Node*
ExtrudeGeometryFilter::push( FeatureList& input, FilterContext& context )
{
    reset( context );

    // minimally, we require an extrusion symbol.
    if ( !_extrusionSymbol.valid() )
    {
        OE_WARN << LC << "Missing required extrusion symbolology; geometry will be empty" << std::endl;
        return new osg::Group();
    }

    // establish the active resource library, if applicable.
    _wallResLib = 0L;
    _roofResLib = 0L;

    const StyleSheet* sheet = context.getSession() ? context.getSession()->styles() : 0L;

    if ( sheet != 0L )
    {
        if ( _wallSkinSymbol.valid() && _wallSkinSymbol->libraryName().isSet() )
        {
            _wallResLib = sheet->getResourceLibrary( *_wallSkinSymbol->libraryName() );

            if ( !_wallResLib.valid() )
            {
                OE_WARN << LC << "Unable to load resource library '" << *_wallSkinSymbol->libraryName() << "'"
                    << "; wall geometry will not be textured." << std::endl;
                _wallSkinSymbol = 0L;
            }
        }

        if ( _roofSkinSymbol.valid() && _roofSkinSymbol->libraryName().isSet() )
        {
            _roofResLib = sheet->getResourceLibrary( *_roofSkinSymbol->libraryName() );
            if ( !_roofResLib.valid() )
            {
                OE_WARN << LC << "Unable to load resource library '" << *_roofSkinSymbol->libraryName() << "'"
                    << "; roof geometry will not be textured." << std::endl;
                _roofSkinSymbol = 0L;
            }
        }
    }

    // calculate the localization matrices (_local2world and _world2local)
    computeLocalizers( context );

    // push all the features through the extruder.
    bool ok = process( input, context );

    // convert everything to triangles and combine drawables.
    if ( _mergeGeometry == true && _featureNameExpr.empty() )
    {
        for( SortedGeodeMap::iterator i = _geodes.begin(); i != _geodes.end(); ++i )
        {
            MeshConsolidator::run( *i->second.get() );
        }
    }

    // parent geometry with a delocalizer (if necessary)
    osg::Group* group = createDelocalizeGroup();
    
    // combines geometries where the statesets are the same.
    for( SortedGeodeMap::iterator i = _geodes.begin(); i != _geodes.end(); ++i )
    {
        group->addChild( i->second.get() );
    }
    _geodes.clear();

    // if we drew outlines, apply a poly offset too.
    if ( _outlineSymbol.valid() )
    {
        osg::StateSet* groupStateSet = group->getOrCreateStateSet();
        groupStateSet->setAttributeAndModes( new osg::PolygonOffset(1,1), 1 );
        if ( _outlineSymbol->stroke()->width().isSet() )
            groupStateSet->setAttributeAndModes( new osg::LineWidth(*_outlineSymbol->stroke()->width()), 1 );
    }

    // if we have textures, install a shader to draw them
    if ( _wallSkinSymbol.valid() || _roofSkinSymbol.valid() )
    {
        osg::StateSet* stateSet = group->getOrCreateStateSet();

        VirtualProgram* vp = new VirtualProgram();
        vp->setName("ExtrudeGeomFilter");
        vp->installDefaultColoringShaders( 1 );
        stateSet->setAttributeAndModes( vp, osg::StateAttribute::ON );

        // a default empty texture will support any non-textured geometry 
        osg::Texture2D* tex = new osg::Texture2D( ImageUtils::createEmptyImage() );
        tex->setUnRefImageDataAfterApply( false );
        stateSet->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON);
        stateSet->getOrCreateUniform("tex0", osg::Uniform::SAMPLER_2D)->set(0);
    }

    return group;
}