[Marble-devel] [PATCH 01/13] Improve compositing of texture layers.

Jens-Michael Hoffmann jensmh at gmx.de
Tue Mar 30 20:26:55 CEST 2010


Improve compositing of texture layers.

This commit adds a number of blending algorithms which can be used to compose
texture layers. The blending algorithm can selected per texture layer in the
DGML file with the new element "blending".

Available blendings: Allanon, ArcusTangent, GeometricMean, LinearLight,
Overlay, Parallel, Texture, ColorBurn, Dark, Darken, Divide, GammaDark,
LinearBurn, Multiply, Subtractive, Additive, ColorDodge, GammaLight,
HardLight, Light, Lighten, PinLight, Screen, SoftLight, VividLight,
AdditiveSubtractive, Bleach, Difference, Equivalence, HalfDifference.
---
 marble/data/maps/earth/bluemarble/bluemarble.dgml  |    1 +
 .../maps/earth/openstreetmap/openstreetmap.dgml    |    2 +
 marble/src/lib/Blending.cpp                        |   25 ++
 marble/src/lib/Blending.h                          |   33 +++
 marble/src/lib/BlendingAlgorithms.cpp              |  279 ++++++++++++++++++++
 marble/src/lib/BlendingAlgorithms.h                |  246 +++++++++++++++++
 marble/src/lib/BlendingFactory.cpp                 |   86 ++++++
 marble/src/lib/BlendingFactory.h                   |   40 +++
 marble/src/lib/CMakeLists.txt                      |    3 +
 marble/src/lib/StackedTile.cpp                     |   41 +---
 marble/src/lib/StackedTileLoader.cpp               |   10 +-
 marble/src/lib/StackedTile_p.h                     |    1 -
 marble/src/lib/TextureTile.cpp                     |    4 +-
 marble/src/lib/TextureTile.h                       |   20 +-
 .../handlers/dgml/DgmlBlendingTagHandler.cpp       |   55 ++++
 .../geodata/handlers/dgml/DgmlBlendingTagHandler.h |   35 +++
 .../handlers/dgml/DgmlElementDictionary.cpp        |    1 +
 .../geodata/handlers/dgml/DgmlElementDictionary.h  |    1 +
 marble/src/lib/geodata/scene/GeoSceneTexture.cpp   |    1 +
 marble/src/lib/geodata/scene/GeoSceneTexture.h     |   15 +
 20 files changed, 845 insertions(+), 54 deletions(-)
 create mode 100644 marble/src/lib/Blending.cpp
 create mode 100644 marble/src/lib/Blending.h
 create mode 100644 marble/src/lib/BlendingAlgorithms.cpp
 create mode 100644 marble/src/lib/BlendingAlgorithms.h
 create mode 100644 marble/src/lib/BlendingFactory.cpp
 create mode 100644 marble/src/lib/BlendingFactory.h
 create mode 100644 marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.cpp
 create mode 100644 marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.h

diff --git a/marble/data/maps/earth/bluemarble/bluemarble.dgml b/marble/data/maps/earth/bluemarble/bluemarble.dgml
index 93b76e1..d4c5d5e 100644
--- a/marble/data/maps/earth/bluemarble/bluemarble.dgml
+++ b/marble/data/maps/earth/bluemarble/bluemarble.dgml
@@ -42,6 +42,7 @@
                     <sourcedir format="JPG"> earth/clouds </sourcedir>
                     <installmap> clouds.jpg </installmap>
                     <storageLayout maximumTileLevel="1"/>
+                    <blending name="CloudsBlending" />
                 </texture>
             </layer>
                             
diff --git a/marble/data/maps/earth/openstreetmap/openstreetmap.dgml b/marble/data/maps/earth/openstreetmap/openstreetmap.dgml
index d42b649..2d38acb 100644
--- a/marble/data/maps/earth/openstreetmap/openstreetmap.dgml
+++ b/marble/data/maps/earth/openstreetmap/openstreetmap.dgml
@@ -32,6 +32,8 @@
                     <downloadPolicy usage="Browse" maximumConnections="20" />
                     <downloadPolicy usage="Bulk" maximumConnections="2" />
                 </texture>
+
+
             </layer>
             <layer name="standardplaces" backend="geodata">
                <geodata name="cityplacemarks">
diff --git a/marble/src/lib/Blending.cpp b/marble/src/lib/Blending.cpp
new file mode 100644
index 0000000..6b3100d
--- /dev/null
+++ b/marble/src/lib/Blending.cpp
@@ -0,0 +1,25 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#include "Blending.h"
+
+namespace Marble
+{
+
+Blending::~Blending()
+{
+}
+
+}
diff --git a/marble/src/lib/Blending.h b/marble/src/lib/Blending.h
new file mode 100644
index 0000000..0dda3ec
--- /dev/null
+++ b/marble/src/lib/Blending.h
@@ -0,0 +1,33 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#ifndef MARBLE_BLENDING_H
+#define MARBLE_BLENDING_H
+
+class QImage;
+
+namespace Marble
+{
+
+class Blending
+{
+ public:
+    virtual ~Blending();
+    virtual void blend( QImage * const bottom, QImage const * const top ) const = 0;
+};
+
+}
+
+#endif
diff --git a/marble/src/lib/BlendingAlgorithms.cpp b/marble/src/lib/BlendingAlgorithms.cpp
new file mode 100644
index 0000000..33f5233
--- /dev/null
+++ b/marble/src/lib/BlendingAlgorithms.cpp
@@ -0,0 +1,279 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#include "BlendingAlgorithms.h"
+
+#include <cmath>
+
+#include <QtGui/QImage>
+
+namespace Marble
+{
+
+void IndependentChannelBlending::blend( QImage * const bottom, QImage const * const top ) const
+{
+    Q_ASSERT( bottom->size() == top->size() );
+    int const width = bottom->width();
+    int const height = bottom->height();
+    for ( int y = 0; y < height; ++y ) {
+        for ( int x = 0; x < width; ++x ) {
+            QRgb const bottomPixel = bottom->pixel( x, y );
+            QRgb const topPixel = top->pixel( x, y );
+            qreal const resultRed = blendChannel( qRed( bottomPixel ) / 255.0,
+                                                  qRed( topPixel ) / 255.0 );
+            qreal const resultGreen = blendChannel( qGreen( bottomPixel ) / 255.0,
+                                                    qGreen( topPixel ) / 255.0 );
+            qreal const resultBlue = blendChannel( qBlue( bottomPixel ) / 255.0,
+                                                   qBlue( topPixel ) / 255.0 );
+            bottom->setPixel( x, y, qRgb( resultRed * 255.0,
+                                          resultGreen * 255.0,
+                                          resultBlue * 255.0 ));
+        }
+    }
+}
+
+
+// Neutral blendings
+
+qreal AllanonBlending::blendChannel( qreal const bottomColorIntensity,
+                                     qreal const topColorIntensity ) const
+{
+    return ( bottomColorIntensity + topColorIntensity ) / 2.0;
+}
+
+qreal ArcusTangentBlending::blendChannel( qreal const bottomColorIntensity,
+                                          qreal const topColorIntensity ) const
+{
+    return 2.0 * atan( topColorIntensity / bottomColorIntensity ) / M_PI;
+}
+
+qreal GeometricMeanBlending::blendChannel( qreal const bottomColorIntensity,
+                                           qreal const topColorIntensity ) const
+{
+    return sqrt( bottomColorIntensity * topColorIntensity );
+}
+
+qreal LinearLightBlending::blendChannel( qreal const bottomColorIntensity,
+                                         qreal const topColorIntensity ) const
+{
+    return qMin( 1.0, qMax( 0.0, ( bottomColorIntensity + 2.0 * topColorIntensity ) - 1.0 ));
+}
+
+qreal OverlayBlending::blendChannel( qreal const bottomColorIntensity,
+                                     qreal const topColorIntensity ) const
+{
+    if ( bottomColorIntensity < 0.5 )
+        return 2.0 * bottomColorIntensity * topColorIntensity;
+    else
+        return 1.0 - 2.0 * ( 1.0 - bottomColorIntensity ) * ( 1.0 - topColorIntensity );
+}
+
+qreal ParallelBlending::blendChannel( qreal const bottomColorIntensity,
+                                      qreal const topColorIntensity ) const
+{
+    // FIXME:    return qMin( qMax( 2.0 / ( 1.0 / bottomColorIntensity + 1.0 / topColorIntensity )), 0.0, 1.0 );
+    return 0.0;
+}
+
+qreal TextureBlending::blendChannel( qreal const bottomColorIntensity,
+                                     qreal const topColorIntensity ) const
+{
+    // FIXME: return qMax( qMin( topColorIntensity + bottomColorIntensity ) - 0.5 ), 1.0 ), 0.0 );
+    return 0.0;
+}
+
+
+// Darkening blendings
+
+qreal ColorBurnBlending::blendChannel( qreal const bottomColorIntensity,
+                                       qreal const topColorIntensity ) const
+{
+    // FIXME: check if this formula makes sense
+    return qMin( 1.0, qMax( 0.0, 1.0 - ( 1.0 - bottomColorIntensity ) / topColorIntensity ));
+}
+
+qreal DarkBlending::blendChannel( qreal const bottomColorIntensity,
+                                  qreal const topColorIntensity ) const
+{
+    return ( bottomColorIntensity + 1.0 - topColorIntensity ) * topColorIntensity;
+}
+
+qreal DarkenBlending::blendChannel( qreal const bottomColorIntensity,
+                                    qreal const topColorIntensity ) const
+{
+    // FIXME: is this really ok? not vice versa?
+    return bottomColorIntensity > topColorIntensity ? topColorIntensity : bottomColorIntensity;
+}
+
+qreal DivideBlending::blendChannel( qreal const bottomColorIntensity,
+                                    qreal const topColorIntensity ) const
+{
+    return log2( 1.0 + bottomColorIntensity / ( 1.0 - topColorIntensity ) / 8.0 );
+}
+
+qreal GammaDarkBlending::blendChannel( qreal const bottomColorIntensity,
+                                       qreal const topColorIntensity ) const
+{
+    return pow( bottomColorIntensity, 1.0 / topColorIntensity );
+}
+
+qreal LinearBurnBlending::blendChannel( qreal const bottomColorIntensity,
+                                        qreal const topColorIntensity ) const
+{
+    return qMax( 0.0, bottomColorIntensity + topColorIntensity - 1.0 );
+}
+
+qreal MultiplyBlending::blendChannel( qreal const bottomColorIntensity,
+                                      qreal const topColorIntensity ) const
+{
+    return bottomColorIntensity * topColorIntensity;
+}
+
+qreal SubtractiveBlending::blendChannel( qreal const bottomColorIntensity,
+                                         qreal const topColorIntensity ) const
+{
+    return qMax( bottomColorIntensity - topColorIntensity, 0.0 );
+}
+
+
+// Lightening blendings
+
+qreal AdditiveBlending::blendChannel( qreal const bottomColorIntensity,
+                                      qreal const topColorIntensity ) const
+{
+    return qMin( topColorIntensity + bottomColorIntensity, 1.0 );
+}
+
+qreal ColorDodgeBlending::blendChannel( qreal const bottomColorIntensity,
+                                        qreal const topColorIntensity ) const
+{
+    return qMin( 1.0, qMax( 0.0, bottomColorIntensity / ( 1.0 - topColorIntensity )));
+}
+
+qreal GammaLightBlending::blendChannel( qreal const bottomColorIntensity,
+                                        qreal const topColorIntensity ) const
+{
+    return pow( bottomColorIntensity, topColorIntensity );
+}
+
+qreal HardLightBlending::blendChannel( qreal const bottomColorIntensity,
+                                       qreal const topColorIntensity ) const
+{
+    return topColorIntensity < 0.5
+        ? 2.0 * bottomColorIntensity * topColorIntensity
+        : 1.0 - 2.0 * ( 1.0 - bottomColorIntensity ) * ( 1.0 - topColorIntensity );
+}
+
+qreal LightBlending::blendChannel( qreal const bottomColorIntensity,
+                                   qreal const topColorIntensity ) const
+{
+    return bottomColorIntensity * ( 1.0 - topColorIntensity ) + pow( topColorIntensity, 2 );
+}
+
+qreal LightenBlending::blendChannel( qreal const bottomColorIntensity,
+                                     qreal const topColorIntensity ) const
+{
+    // is this ok?
+    return bottomColorIntensity < topColorIntensity ? topColorIntensity : bottomColorIntensity;
+}
+
+qreal PinLightBlending::blendChannel( qreal const bottomColorIntensity,
+                                      qreal const topColorIntensity ) const
+{
+    return qMax( 0.0, qMax( 2.0 + topColorIntensity - 1.0,
+                            qMin( bottomColorIntensity, 2.0 * topColorIntensity )));
+}
+
+qreal ScreenBlending::blendChannel( qreal const bottomColorIntensity,
+                                    qreal const topColorIntensity ) const
+{
+    return 1.0 - ( 1.0 - bottomColorIntensity ) * ( 1.0 - topColorIntensity );
+}
+
+qreal SoftLightBlending::blendChannel( qreal const bottomColorIntensity,
+                                       qreal const topColorIntensity ) const
+{
+    return pow( bottomColorIntensity, pow( 2.0, ( 2.0 * ( 0.5 - topColorIntensity ))));
+}
+
+qreal VividLightBlending::blendChannel( qreal const bottomColorIntensity,
+                                        qreal const topColorIntensity ) const
+{
+    return topColorIntensity < 0.5
+        ? qMin( 1.0, qMax( 0.0, 1.0 - ( 1.0 - bottomColorIntensity ) / ( 2.0 * topColorIntensity )))
+        : qMin( 1.0, qMax( 0.0, bottomColorIntensity / ( 2.0 * ( 1.0 - topColorIntensity ))));
+}
+
+
+// Inverter blendings
+
+qreal AdditiveSubtractiveBlending::blendChannel( qreal const bottomColorIntensity,
+                                                 qreal const topColorIntensity ) const
+{
+    // FIXME:
+    //    return qMin( 1.0, qMax( 0.0, abs( bottomColorIntensity * bottomColorIntensity
+    //                                      - topColorIntensity * topColorIntensity )));
+    return 0.0;
+}
+
+qreal BleachBlending::blendChannel( qreal const bottomColorIntensity,
+                                    qreal const topColorIntensity ) const
+{
+    // FIXME: "why this is the same formula as Screen Blending? Please correct.)"
+    return 1.0 - ( 1.0 - bottomColorIntensity ) * ( 1.0 - topColorIntensity );
+}
+
+qreal DifferenceBlending::blendChannel( qreal const bottomColorIntensity,
+                                        qreal const topColorIntensity ) const
+{
+    return qMax( qMin( 1.0, bottomColorIntensity - topColorIntensity + 0.5 ), 0.0 );
+}
+
+qreal EquivalenceBlending::blendChannel( qreal const bottomColorIntensity,
+                                         qreal const topColorIntensity ) const
+{
+    return 1.0 - abs( bottomColorIntensity - topColorIntensity );
+}
+
+qreal HalfDifferenceBlending::blendChannel( qreal const bottomColorIntensity,
+                                            qreal const topColorIntensity ) const
+{
+    return bottomColorIntensity + topColorIntensity
+        - 2.0 * ( bottomColorIntensity * topColorIntensity );
+}
+
+
+// Special purpose blendings
+
+void CloudsBlending::blend( QImage * const bottom, QImage const * const top ) const
+{
+    Q_ASSERT( bottom->size() == top->size() );
+    int const width = bottom->width();
+    int const height = bottom->height();
+    for ( int y = 0; y < height; ++y ) {
+        for ( int x = 0; x < width; ++x ) {
+            qreal const c = qRed( top->pixel( x, y )) / 255.0;
+            QRgb const bottomPixel = bottom->pixel( x, y );
+            int const bottomRed = qRed( bottomPixel );
+            int const bottomGreen = qGreen( bottomPixel );
+            int const bottomBlue = qBlue( bottomPixel );
+            bottom->setPixel( x, y, qRgb(( int )( bottomRed + ( 255 - bottomRed ) * c ),
+                                         ( int )( bottomGreen + ( 255 - bottomGreen ) * c ),
+                                         ( int )( bottomBlue + ( 255 - bottomBlue ) * c )));
+        }
+    }
+}
+
+}
diff --git a/marble/src/lib/BlendingAlgorithms.h b/marble/src/lib/BlendingAlgorithms.h
new file mode 100644
index 0000000..cb01f98
--- /dev/null
+++ b/marble/src/lib/BlendingAlgorithms.h
@@ -0,0 +1,246 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#ifndef MARBLE_BLENDING_ALGORITHMS_H
+#define MARBLE_BLENDING_ALGORITHMS_H
+
+#include <QtCore/QtGlobal>
+
+#include "Blending.h"
+
+namespace Marble
+{
+
+class IndependentChannelBlending: public Blending
+{
+ public:
+    virtual void blend( QImage * const bottom, QImage const * const top ) const;
+ private:
+    // bottomColorIntensity: intensity of one color channel (of one pixel) of the bottom image
+    // topColorIntensity: intensity of one color channel (of one pixel) of the top image
+    // return: intensity of the color channel (of a given pixel) of the result image
+    // all color intensity values are in the range 0..1
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const = 0;
+};
+
+
+// Neutral blendings
+
+class AllanonBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class ArcusTangentBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class GeometricMeanBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class LinearLightBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class NoiseBlending: public Blending // or IndependentChannelBlending?
+{
+};
+
+class OverlayBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class ParallelBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class TextureBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+
+// Darkening blendings
+
+class ColorBurnBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class DarkBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class DarkenBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class DivideBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class GammaDarkBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class LinearBurnBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class MultiplyBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class SubtractiveBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+
+// Lightening blendings
+
+class AdditiveBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class ColorDodgeBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class GammaLightBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class HardLightBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class LightBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class LightenBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class PinLightBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class ScreenBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class SoftLightBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class VividLightBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+
+// Inverter blendings
+
+class AdditiveSubtractiveBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class BleachBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class DifferenceBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class EquivalenceBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+class HalfDifferenceBlending: public IndependentChannelBlending
+{
+    virtual qreal blendChannel( qreal const bottomColorIntensity,
+                                qreal const topColorIntensity ) const;
+};
+
+
+// Special purpose blendings
+class CloudsBlending: public Blending
+{
+ public:
+    virtual void blend( QImage * const bottom, QImage const * const top ) const;
+};
+
+
+}
+
+#endif
diff --git a/marble/src/lib/BlendingFactory.cpp b/marble/src/lib/BlendingFactory.cpp
new file mode 100644
index 0000000..d8c21e6
--- /dev/null
+++ b/marble/src/lib/BlendingFactory.cpp
@@ -0,0 +1,86 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#include "BlendingFactory.h"
+
+#include "BlendingAlgorithms.h"
+#include "MarbleDebug.h"
+
+namespace Marble
+{
+
+BlendingFactory const * BlendingFactory::s_instance = 0;
+
+BlendingFactory const * BlendingFactory::instance()
+{
+    if ( s_instance == 0 )
+        s_instance = new BlendingFactory;
+    return s_instance;
+}
+
+Blending const * BlendingFactory::findBlending( QString const & name ) const
+{
+    Blending const * const result = m_blendings.value( name, 0 );
+    if ( !result )
+        mDebug() << "BlendingFactory::findBlending: unknown blending:" << name;
+    return result;
+}
+
+BlendingFactory::BlendingFactory()
+{
+    // Neutral blendings
+    m_blendings.insert( "AllanonBlending", new AllanonBlending );
+    m_blendings.insert( "ArcusTangentBlending", new ArcusTangentBlending );
+    m_blendings.insert( "GeometricMeanBlending", new GeometricMeanBlending );
+    m_blendings.insert( "LinearLightBlending", new LinearLightBlending );
+    //m_blendings.insert( "NoiseBlending", new NoiseBlending );
+    m_blendings.insert( "OverlayBlending", new OverlayBlending );
+    m_blendings.insert( "ParallelBlending", new ParallelBlending );
+    m_blendings.insert( "TextureBlending", new TextureBlending );
+
+    // Darkening blendings
+    m_blendings.insert( "ColorBurnBlending", new ColorBurnBlending );
+    m_blendings.insert( "DarkBlending", new DarkBlending );
+    m_blendings.insert( "DarkenBlending", new DarkenBlending );
+    m_blendings.insert( "DivideBlending", new DivideBlending );
+    m_blendings.insert( "GammaDarkBlending", new GammaDarkBlending );
+    m_blendings.insert( "LinearBurnBlending", new LinearBurnBlending );
+    m_blendings.insert( "MultiplyBlending", new MultiplyBlending );
+    m_blendings.insert( "SubtractiveBlending", new SubtractiveBlending );
+
+    // Lightening blendings
+    m_blendings.insert( "AdditiveBlending", new AdditiveBlending );
+    m_blendings.insert( "ColorDodgeBlending", new ColorDodgeBlending );
+    m_blendings.insert( "GammaLightBlending", new GammaLightBlending );
+    m_blendings.insert( "HardLightBlending", new HardLightBlending );
+    m_blendings.insert( "LightBlending", new LightBlending );
+    m_blendings.insert( "LightenBlending", new LightenBlending );
+    m_blendings.insert( "PinLightBlending", new PinLightBlending );
+    m_blendings.insert( "ScreenBlending", new  ScreenBlending);
+    m_blendings.insert( "SoftLightBlending", new SoftLightBlending );
+    m_blendings.insert( "VividLightBlending", new VividLightBlending );
+
+    // Inverter blendings
+    m_blendings.insert( "AdditiveSubtractiveBlending", new AdditiveSubtractiveBlending );
+    m_blendings.insert( "BleachBlending", new BleachBlending );
+    m_blendings.insert( "DifferenceBlending", new DifferenceBlending );
+    m_blendings.insert( "EquivalenceBlending", new EquivalenceBlending );
+    m_blendings.insert( "HalfDifferenceBlending", new HalfDifferenceBlending );
+
+    // Special purpose blendings
+    m_blendings.insert( "CloudsBlending", new CloudsBlending );
+}
+
+}
diff --git a/marble/src/lib/BlendingFactory.h b/marble/src/lib/BlendingFactory.h
new file mode 100644
index 0000000..d1bfc68
--- /dev/null
+++ b/marble/src/lib/BlendingFactory.h
@@ -0,0 +1,40 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#ifndef MARBLE_BLENDING_FACTORY_H
+#define MARBLE_BLENDING_FACTORY_H
+
+#include <QtCore/QHash>
+#include <QtCore/QString>
+
+namespace Marble
+{
+class Blending;
+
+class BlendingFactory
+{
+ public:
+    static BlendingFactory const * instance();
+    Blending const * findBlending( QString const & name ) const;
+
+ private:
+    static BlendingFactory const * s_instance;
+    BlendingFactory();
+    QHash<QString, Blending const *> m_blendings;
+};
+
+}
+
+#endif
diff --git a/marble/src/lib/CMakeLists.txt b/marble/src/lib/CMakeLists.txt
index c4ea55e..c9a6f31 100644
--- a/marble/src/lib/CMakeLists.txt
+++ b/marble/src/lib/CMakeLists.txt
@@ -59,6 +59,9 @@ set(marblewidget_SRCS
     ${geodata_SRCS}
     ${graphicsview_SRCS}
     ${screengraphicsitem_SRCS}
+    Blending.cpp
+    BlendingAlgorithms.cpp
+    BlendingFactory.cpp
     MarbleWidget.cpp
     MarbleModel.cpp
     MarbleMap.cpp
diff --git a/marble/src/lib/StackedTile.cpp b/marble/src/lib/StackedTile.cpp
index df93dfa..d44ef55 100644
--- a/marble/src/lib/StackedTile.cpp
+++ b/marble/src/lib/StackedTile.cpp
@@ -17,6 +17,7 @@
 
 #include <cmath>
 
+#include "Blending.h"
 #include "GeoSceneTexture.h"
 #include "MarbleDebug.h"
 #include "TextureTile.h"
@@ -210,30 +211,6 @@ inline void StackedTilePrivate::mergeCopyToResult( QSharedPointer<TextureTile> c
     m_resultTile = other->image()->copy();
 }
 
-void StackedTilePrivate::mergeMultiplyToResult( QSharedPointer<TextureTile> const & other )
-{
-    // for this operation we assume that the tiles have the same size
-    QImage const * const otherImage = other->image();
-    Q_ASSERT( m_resultTile.size() == otherImage->size() );
-    if ( m_resultTile.size() != otherImage->size() )
-        return;
-
-    int const width = m_resultTile.width();
-    int const height = m_resultTile.height();
-    for ( int y = 0; y < height; ++y ) {
-        for ( int x = 0; x < width; ++x ) {
-            qreal const c = qRed( otherImage->pixel( x, y )) / 255.0;
-            QRgb const oldPixel = m_resultTile.pixel( x, y );
-            int const oldRed = qRed( oldPixel );
-            int const oldGreen = qGreen( oldPixel );
-            int const oldBlue = qBlue( oldPixel );
-            m_resultTile.setPixel( x, y, qRgb(( int )( oldRed + ( 255 - oldRed ) * c ),
-                                              ( int )( oldGreen + ( 255 - oldGreen ) * c ),
-                                              ( int )( oldBlue + ( 255 - oldBlue ) * c )));
-        }
-    }
-}
-
 void StackedTilePrivate::calcByteCount()
 {
     int byteCount = m_resultTile.numBytes();
@@ -390,15 +367,17 @@ void StackedTile::initResultTile()
     QVector<QSharedPointer<TextureTile> >::const_iterator pos = d->m_tiles.constBegin();
     QVector<QSharedPointer<TextureTile> >::const_iterator const end = d->m_tiles.constEnd();
     for (; pos != end; ++pos )
-        if ( (*pos)->state() != TextureTile::StateEmpty )
-            switch ( (*pos)->mergeRule() ) {
-            case TextureTile::MergeCopy:
+        if ( (*pos)->state() != TextureTile::StateEmpty ) {
+            Blending const * const blending = (*pos)->blending();
+            if ( blending ) {
+                mDebug() << "StackedTile::initResultTile: blending";
+                blending->blend( &d->m_resultTile, (*pos)->image() );
+            }
+            else {
+                mDebug() << "StackedTile::initResultTile: no blending defined => copying top over bottom image";
                 d->mergeCopyToResult( *pos );
-                break;
-            case TextureTile::MergeMultiply:
-                d->mergeMultiplyToResult( *pos );
-                break;
             }
+        }
 
     initJumpTables();
 
diff --git a/marble/src/lib/StackedTileLoader.cpp b/marble/src/lib/StackedTileLoader.cpp
index 1117bcb..1df1a53 100644
--- a/marble/src/lib/StackedTileLoader.cpp
+++ b/marble/src/lib/StackedTileLoader.cpp
@@ -192,11 +192,8 @@ StackedTile* StackedTileLoader::loadTile( TileId const & stackedTileId,
         mDebug() << "StackedTileLoader::loadTile: tile" << textureLayer->sourceDir()
                  << tileId.toString();
         QSharedPointer<TextureTile> const tile = d->m_tileLoader->loadTile( stackedTileId, tileId );
-        // hack to try clouds, first tile is not handled here, MergeCopy is the default,
-        // the merge rule for following tiles is set to MergeMultiply here
         if ( tile ) {
-            if ( stackedTile->hasTiles() )
-                tile->setMergeRule( TextureTile::MergeMultiply );
+            tile->setBlending( textureLayer->blending() );
             stackedTile->addTile( tile );
         }
     }
@@ -244,11 +241,8 @@ StackedTile* StackedTileLoader::reloadTile( TileId const & stackedTileId )
         TileId const tileId( textureLayer->sourceDir(), stackedTileId.zoomLevel(),
                              stackedTileId.x(), stackedTileId.y() );
         QSharedPointer<TextureTile> const tile = d->m_tileLoader->reloadTile( stackedTileId, tileId );
-        // hack to try clouds, first tile is not handled here, MergeCopy is the default,
-        // the merge rule for following tiles is set to MergeMultiply here
         if ( tile ) {
-            if ( stackedTile->hasTiles() )
-                tile->setMergeRule( TextureTile::MergeMultiply );
+            tile->setBlending( textureLayer->blending() );
             stackedTile->addTile( tile );
         }
     }
diff --git a/marble/src/lib/StackedTile_p.h b/marble/src/lib/StackedTile_p.h
index 51a11f4..d17d5a2 100644
--- a/marble/src/lib/StackedTile_p.h
+++ b/marble/src/lib/StackedTile_p.h
@@ -50,7 +50,6 @@ class StackedTilePrivate : AbstractTilePrivate
     inline uint pixel( int x, int y ) const;
     inline uint pixelF( qreal x, qreal y, const QRgb& pixel ) const;
     void mergeCopyToResult( QSharedPointer<TextureTile> const & tile );
-    void mergeMultiplyToResult( QSharedPointer<TextureTile> const & tile );
     void calcByteCount();
 };
 
diff --git a/marble/src/lib/TextureTile.cpp b/marble/src/lib/TextureTile.cpp
index 8cc9c38..a21a8e8 100644
--- a/marble/src/lib/TextureTile.cpp
+++ b/marble/src/lib/TextureTile.cpp
@@ -25,7 +25,7 @@ namespace Marble
 TextureTile::TextureTile( TileId const & tileId )
     : m_id( tileId ),
       m_state( StateEmpty ),
-      m_mergeRule( MergeCopy ),
+      m_blending( 0 ),
       m_expireSecs( std::numeric_limits<int>::max() ),
       m_image( 0 )
 {
@@ -34,7 +34,7 @@ TextureTile::TextureTile( TileId const & tileId )
 TextureTile::TextureTile( TileId const & tileId, QString const & fileName )
     : m_id( tileId ),
       m_state( StateExpired ),
-      m_mergeRule( MergeCopy ),
+      m_blending( 0 ),
       m_expireSecs( std::numeric_limits<int>::max() ),
       m_image( new QImage( fileName ))
 {
diff --git a/marble/src/lib/TextureTile.h b/marble/src/lib/TextureTile.h
index 050827f..8fea7f1 100644
--- a/marble/src/lib/TextureTile.h
+++ b/marble/src/lib/TextureTile.h
@@ -26,6 +26,7 @@ class QImage;
 
 namespace Marble
 {
+class Blending;
 class TileLoader;
 class StackedTileLoader;
 
@@ -42,11 +43,6 @@ class TextureTile
         StateUptodate
     };
 
-    enum MergeRule {
-        MergeCopy,
-        MergeMultiply
-    };
-
     explicit TextureTile( TileId const & );
     TextureTile( TileId const & tileId, QString const & fileName );
     ~TextureTile();
@@ -58,7 +54,7 @@ class TextureTile
     QImage const * image() const;
     QImage * image();
     State state() const;
-    MergeRule mergeRule() const;
+    Blending const * blending() const;
     int byteCount() const;
 
  private:
@@ -67,7 +63,7 @@ class TextureTile
     void setState( State const );
     void setImage( QByteArray const & data );
     void setImage( QImage * const );
-    void setMergeRule( MergeRule const );
+    void setBlending( Blending const * const );
     void setStackedTileId( TileId const & );
     void setLastModified( QDateTime const & );
     void setExpireSecs( int const );
@@ -75,7 +71,7 @@ class TextureTile
     TileId const m_id;
     TileId m_stackedTileId;
     State m_state;
-    MergeRule m_mergeRule;
+    Blending const * m_blending;
     QDateTime m_lastModified;
     int m_expireSecs;
     QImage * m_image;
@@ -119,9 +115,9 @@ inline TextureTile::State TextureTile::state() const
     return m_state;
 }
 
-inline TextureTile::MergeRule TextureTile::mergeRule() const
+inline Blending const * TextureTile::blending() const
 {
-    return m_mergeRule;
+    return m_blending;
 }
 
 inline int TextureTile::byteCount() const
@@ -140,9 +136,9 @@ inline void TextureTile::setImage( QImage * const image )
     m_image = image;
 }
 
-inline void TextureTile::setMergeRule( MergeRule const mergeRule )
+inline void TextureTile::setBlending( Blending const * const blending )
 {
-    m_mergeRule = mergeRule;
+    m_blending = blending;
 }
 
 inline void TextureTile::setStackedTileId( TileId const & id )
diff --git a/marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.cpp b/marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.cpp
new file mode 100644
index 0000000..f085366
--- /dev/null
+++ b/marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.cpp
@@ -0,0 +1,55 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#include "DgmlBlendingTagHandler.h"
+
+#include "BlendingFactory.h"
+#include "DgmlAttributeDictionary.h"
+#include "DgmlElementDictionary.h"
+#include "GeoParser.h"
+#include "GeoSceneTexture.h"
+#include "MarbleDebug.h"
+
+namespace Marble
+{
+namespace dgml
+{
+static GeoTagHandlerRegistrar registrar( GeoTagHandler::QualifiedName( dgmlTag_Blending,
+                                                                       dgmlTag_nameSpace20 ),
+                                         new DgmlBlendingTagHandler );
+
+GeoNode* DgmlBlendingTagHandler::parse( GeoParser& parser ) const
+{
+    // Check whether the tag is valid
+    Q_ASSERT( parser.isStartElement() && parser.isValidElement( dgmlTag_Blending ));
+
+    // Checking for parent item
+    GeoStackItem parentItem = parser.parentElement();
+    if ( !parentItem.represents( dgmlTag_Texture ))
+        return 0;
+
+    // Attribute name, default to ""
+    const QString name = parser.attribute( dgmlAttr_name ).trimmed();
+    mDebug() << "DgmlBlendingTagHandler::parse" << name;
+    Blending const * const blending = BlendingFactory::instance()->findBlending( name );
+    if ( !blending )
+        parser.raiseWarning( QString( "Unknown (or no) blending algorithm: '%1'" ).arg( name ));
+    parentItem.nodeAs<GeoSceneTexture>()->setBlending( blending );
+    return 0;
+}
+
+
+}
+}
diff --git a/marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.h b/marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.h
new file mode 100644
index 0000000..73e4217
--- /dev/null
+++ b/marble/src/lib/geodata/handlers/dgml/DgmlBlendingTagHandler.h
@@ -0,0 +1,35 @@
+// Copyright 2010 Jens-Michael Hoffmann <jmho at c-xx.com>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+#ifndef MARBLE_DGML_BLENDING_TAG_HANDLER_H
+#define MARBLE_DGML_BLENDING_TAG_HANDLER_H
+
+#include "GeoTagHandler.h"
+
+namespace Marble
+{
+namespace dgml
+{
+
+class DgmlBlendingTagHandler: public GeoTagHandler
+{
+public:
+    virtual GeoNode* parse( GeoParser& ) const;
+};
+
+}
+}
+
+#endif
diff --git a/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.cpp b/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.cpp
index 51ef5c0..74a871d 100644
--- a/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.cpp
+++ b/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.cpp
@@ -31,6 +31,7 @@ namespace dgml
 const char* dgmlTag_nameSpace20 = "http://edu.kde.org/marble/dgml/2.0";
 
 const char* dgmlTag_Available = "available";
+const char* dgmlTag_Blending = "blending";
 const char* dgmlTag_Brush = "brush";
 const char* dgmlTag_Coastline = "coastline";
 const char* dgmlTag_Color = "color";
diff --git a/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.h b/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.h
index b8d740a..a60ae06 100644
--- a/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.h
+++ b/marble/src/lib/geodata/handlers/dgml/DgmlElementDictionary.h
@@ -34,6 +34,7 @@ namespace dgml
     extern const char* dgmlTag_nameSpace20;
 
     extern  const char* dgmlTag_Available;
+    extern  const char* dgmlTag_Blending;
     extern  const char* dgmlTag_Brush;
     extern  const char* dgmlTag_Coastline;
     extern  const char* dgmlTag_Color;
diff --git a/marble/src/lib/geodata/scene/GeoSceneTexture.cpp b/marble/src/lib/geodata/scene/GeoSceneTexture.cpp
index db6724f..4156df6 100644
--- a/marble/src/lib/geodata/scene/GeoSceneTexture.cpp
+++ b/marble/src/lib/geodata/scene/GeoSceneTexture.cpp
@@ -38,6 +38,7 @@ GeoSceneTexture::GeoSceneTexture( const QString& name )
       m_levelZeroRows( defaultLevelZeroRows ),
       m_maximumTileLevel( -1 ),
       m_projection( Equirectangular ),
+      m_blending( 0 ),
       m_downloadUrls(),
       m_nextUrl( m_downloadUrls.constEnd() )
 {
diff --git a/marble/src/lib/geodata/scene/GeoSceneTexture.h b/marble/src/lib/geodata/scene/GeoSceneTexture.h
index af28c44..e095fef 100644
--- a/marble/src/lib/geodata/scene/GeoSceneTexture.h
+++ b/marble/src/lib/geodata/scene/GeoSceneTexture.h
@@ -37,6 +37,7 @@
 
 namespace Marble
 {
+class Blending;
 class DownloadPolicy;
 
 class GeoSceneTexture : public GeoSceneAbstractDataset
@@ -73,6 +74,9 @@ class GeoSceneTexture : public GeoSceneAbstractDataset
     Projection projection() const;
     void setProjection( const Projection );
 
+    Blending const * blending() const;
+    void setBlending( Blending const * const );
+
     // this method is a little more than just a stupid getter,
     // it implements the round robin for the tile servers.
     // on each invocation the next url is returned
@@ -96,6 +100,7 @@ class GeoSceneTexture : public GeoSceneAbstractDataset
     int m_levelZeroRows;
     int m_maximumTileLevel;
     Projection m_projection;
+    Blending const * m_blending;
 
     /// List of Urls which are used in a round robin fashion
     QVector<QUrl> m_downloadUrls;
@@ -110,6 +115,16 @@ inline bool GeoSceneTexture::hasMaximumTileLevel() const
     return m_maximumTileLevel != -1;
 }
 
+inline Blending const * GeoSceneTexture::blending() const
+{
+    return m_blending;
+}
+
+inline void GeoSceneTexture::setBlending( Blending const * const blending )
+{
+    m_blending = blending;
+}
+
 }
 
 #endif // GEOSCENETEXTURE_H
-- 
1.7.0.3



More information about the Marble-devel mailing list