diff --git a/applications/test/colourTables/Make/files b/applications/test/colourTables/Make/files
new file mode 100644
index 0000000000000000000000000000000000000000..4ab74f8a9dae1a36e3ec49bbb04e2188ba177326
--- /dev/null
+++ b/applications/test/colourTables/Make/files
@@ -0,0 +1,3 @@
+Test-colourTables.C
+
+EXE = $(FOAM_USER_APPBIN)/Test-colourTables
diff --git a/applications/test/colourTables/Make/options b/applications/test/colourTables/Make/options
new file mode 100644
index 0000000000000000000000000000000000000000..7ce182425d9bee4bef91ba2a36b6b8475fc1aeb2
--- /dev/null
+++ b/applications/test/colourTables/Make/options
@@ -0,0 +1,5 @@
+EXE_INC = \
+    -I$(LIB_SRC)/fileFormats/lnInclude
+
+EXE_LIBS = \
+    -lfileFormats
diff --git a/applications/test/colourTables/Test-colourTables.C b/applications/test/colourTables/Test-colourTables.C
new file mode 100644
index 0000000000000000000000000000000000000000..4bb53ceabd0f5461a8a7382e43d27dbf87f2d912
--- /dev/null
+++ b/applications/test/colourTables/Test-colourTables.C
@@ -0,0 +1,66 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+#include "colourTable.H"
+#include "IOstreams.H"
+
+using namespace Foam;
+
+void dumpTable(const colourTable& tbl, const label n=128)
+{
+    Info<< tbl.table(n) << nl;
+}
+
+
+void dumpTable(const colourTable* tbl, const label n=128)
+{
+    if (tbl)
+    {
+        Info<< tbl->table(n) << nl;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+//  Main program:
+
+int main()
+{
+    // dumpTable(colourTable::ptr(colourTable::RAINBOW));
+    dumpTable(colourTable::ptr(colourTable::COOL_WARM));
+
+//     forAllConstIters(colourTable::tables(), iter)
+//     {
+//         Info<< nl << iter.key() << nl;
+//         dumpTable(iter.val());
+//     }
+
+    Info<< "\nDone\n";
+
+    return 0;
+}
+
+
+// ************************************************************************* //
diff --git a/etc/colourTables b/etc/colourTables
new file mode 100644
index 0000000000000000000000000000000000000000..8466eab299e1d3234c6dfd0269e5237065a80b67
--- /dev/null
+++ b/etc/colourTables
@@ -0,0 +1,96 @@
+/*--------------------------------*- C++ -*----------------------------------*\
+| =========                 |                                                 |
+| \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox           |
+|  \\    /   O peration     | Version:  v1812                                 |
+|   \\  /    A nd           | Web:      www.OpenFOAM.com                      |
+|    \\/     M anipulation  |                                                 |
+\*---------------------------------------------------------------------------*/
+
+// An OpenFOAM dictionary of colourTables.
+// The names should match those in the colourTables class.
+
+6
+(
+
+coolToWarm
+{
+    // ParaView: "Cool To Warm"
+    interpolate diverging;
+
+    table
+    (
+        ( 0.0 ( 0.231372 0.298039 0.752941 ) )
+        ( 0.5 ( 0.865003 0.865003 0.865003 ) )
+        ( 1.0 ( 0.705882 0.0156863 0.14902 ) )
+    );
+}
+
+coldAndHot
+{
+    // ParaView : "Cold and Hot"
+    interpolate diverging;
+
+    table
+    (
+        ( 0    ( 0 1 1 ) )
+        ( 0.45 ( 0 0 1 ) )
+        ( 0.5  ( 0 0 0.5019608) )
+        ( 0.55 ( 1 0 0 ) )
+        ( 1    ( 1 1 0 ) )
+    );
+}
+
+
+fire
+{
+    // ParaView: Black-Body Radiation
+    interpolate rbg;
+
+    table
+    (
+        ( 0   ( 0 0 0 ) )
+        ( 0.4 ( 0.901961 0 0 ) )
+        ( 0.8 ( 0.901961 0.901961 0 ) )
+        ( 1   ( 1 1 1 ) )
+    );
+}
+
+rainbow
+{
+    interpolate hsv;
+
+    table
+    (
+        ( 0   ( 0 0 1 ) )
+        ( 0.5 ( 0 1 0 ) )
+        ( 1   ( 1 0 0 ) )
+    );
+}
+
+greyscale
+{
+    // ParaView: grayscale
+    interpolate rbg;
+
+    table
+    (
+        ( 0   ( 0 0 0 ) )
+        ( 1   ( 1 1 1 ) )
+    );
+}
+
+xray
+{
+    // ParaView: "X ray"
+    interpolate rbg;
+
+    table
+    (
+        ( 0   ( 1 1 1 ) )
+        ( 1   ( 0 0 0 ) )
+    );
+}
+
+)
+
+// ************************************************************************* //
diff --git a/src/fileFormats/Make/files b/src/fileFormats/Make/files
index 3bf374c8ffcf803d826d8c2b3613c91505deb9e4..103b0235a6bc09449eab34f6889da45b3c7b9086 100644
--- a/src/fileFormats/Make/files
+++ b/src/fileFormats/Make/files
@@ -1,3 +1,7 @@
+colours/colourTable.C
+colours/colourTables.C
+colours/colourTools.C
+
 ensight/file/ensightCase.C
 ensight/file/ensightCaseOptions.C
 ensight/file/ensightFile.C
diff --git a/src/fileFormats/colours/colourTable.C b/src/fileFormats/colours/colourTable.C
new file mode 100644
index 0000000000000000000000000000000000000000..6ddd674411f7313f94965c957cfb93c889a1f866
--- /dev/null
+++ b/src/fileFormats/colours/colourTable.C
@@ -0,0 +1,182 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+#include "colourTable.H"
+#include "colourTools.H"
+#include "ListOps.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+const Foam::Enum
+<
+    Foam::colourTable::interpolationType
+>
+Foam::colourTable::interpolationTypeNames
+({
+    { interpolationType::RGB, "rgb" },
+    { interpolationType::HSV, "hsv" },
+    { interpolationType::DIVERGING, "diverging" },
+});
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::colourTable::colourTable
+(
+    const List<Tuple2<scalar, vector>>& values,
+    const interpolationType interp
+)
+:
+    table_(values),
+    interp_(interp)
+{}
+
+
+Foam::colourTable::colourTable
+(
+    List<Tuple2<scalar, vector>>&& values,
+    const interpolationType interp
+)
+:
+    table_(std::move(values)),
+    interp_(interp)
+{}
+
+
+Foam::colourTable::colourTable
+(
+    const dictionary& dict,
+    const interpolationType interp
+)
+:
+    table_(),
+    interp_(interp)
+{
+    dict.readEntry("table", table_);
+    interpolationTypeNames.readIfPresent("interpolate", dict, interp_);
+}
+
+
+// * * * * * * * * * * * * * * * * Selectors * * * * * * * * * * * * * * * * //
+
+Foam::autoPtr<Foam::colourTable> Foam::colourTable::New(Istream& is)
+{
+    return autoPtr<colourTable>::New(dictionary(is));
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+Foam::vector Foam::colourTable::value(const scalar x) const
+{
+    if (x <= 0)
+    {
+        return table_.first().second();
+    }
+
+    if (x >= 1)
+    {
+        return table_.last().second();
+    }
+
+
+    label idx = findLower
+    (
+        table_, x, 0,
+        [](const pair_type& pr, const scalar& val)
+        {
+            // Test first element
+            return (pr.first() <= val);
+        }
+    );
+
+    if (idx == -1)
+    {
+        // Use first element only
+        return table_.first().second();
+    }
+    else if (idx == table_.size()-1)
+    {
+        // Use last element only
+        return table_.last().second();
+    }
+
+    const scalar t0 = table_[idx].first();
+    const scalar t1 = table_[idx+1].first();
+
+    const scalar s = (x - t0)/(t1 - t0);
+
+    const vector& rgb0 = table_[idx].second();
+    const vector& rgb1 = table_[idx+1].second();
+
+    if (interp_ == DIVERGING)
+    {
+        return colourTools::interpolateDiverging(s, rgb0, rgb1);
+    }
+    else if (interp_ == HSV)
+    {
+        return colourTools::interpolateHSV(s, rgb0, rgb1);
+    }
+
+    return colourTools::interpolateRGB(s, rgb0, rgb1);
+}
+
+
+Foam::List<Foam::Tuple2<Foam::scalar, Foam::vector>>
+Foam::colourTable::table(const label nColours) const
+{
+    List<Tuple2<scalar, vector>> lut(nColours);
+
+    for (label i=0; i < nColours; ++i)
+    {
+        const scalar x = scalar(i)/scalar(nColours-1);
+
+        lut[i] = pair_type(x, value(x));
+    }
+
+    return lut;
+}
+
+
+Foam::Ostream& Foam::colourTable::writeDict(Ostream& os) const
+{
+    os.beginBlock();
+    os.writeEntry("interpolate", interpolationTypeNames[interp_]);
+    os.writeEntry("table", table_);
+    os.endBlock();
+
+    return os;
+}
+
+
+Foam::Ostream& Foam::operator<<(Ostream& os, const colourTable& tbl)
+{
+    tbl.writeDict(os);
+
+    return os;
+}
+
+
+// ************************************************************************* //
diff --git a/src/fileFormats/colours/colourTable.H b/src/fileFormats/colours/colourTable.H
new file mode 100644
index 0000000000000000000000000000000000000000..e92d76f3e115334ae522fc3d34c0e309b1076567
--- /dev/null
+++ b/src/fileFormats/colours/colourTable.H
@@ -0,0 +1,207 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Class
+    Foam::colourTable
+
+Description
+    Base class for generating a colour table from node points.
+
+    Dictionary definition
+    \table
+        Property    | Description                           | Required | Default
+        interpolate | rgb/hsv/diverging                     | no  | rgb
+        table       | Node points for the colour table      | yes |
+    \endtable
+
+Predefined colour tables (in "etc/colourTables") include
+"coolToWarm", "coldAndHot", "fire", "rainbow", "greyscale", "xray".
+
+SourceFiles
+    colourTable.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef colourTable_H
+#define colourTable_H
+
+#include "Enum.H"
+#include "List.H"
+#include "Tuple2.H"
+#include "vector.H"
+#include "HashPtrTable.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+/*---------------------------------------------------------------------------*\
+                         Class colourTable Declaration
+\*---------------------------------------------------------------------------*/
+
+class colourTable
+{
+public:
+
+        //- Internal interpolation type
+        enum interpolationType
+        {
+            RGB,
+            HSV,
+            DIVERGING
+        };
+
+        //- Enumeration of commonly used colour tables.
+        //  The indices must match those in "etc/colourTables"
+        enum predefinedType
+        {
+            COOL_WARM,        //!< "coolToWarm"
+            COLD_HOT,         //!< "coldAndHot"
+            FIRE,             //!< "fire" - ParaView "Black-Body Radiation"
+            RAINBOW,          //!< "rainbow"
+            GREYSCALE,        //!< greyscale - ParaView "Grayscale"
+            XRAY              //!< "xray" - ParaView "X Ray"
+        };
+
+
+        //- Enumeration names for interpolationType
+        static const Enum<interpolationType> interpolationTypeNames;
+
+        //- Enumeration names for predefinedType
+        static const Enum<predefinedType> predefinedNames;
+
+        //- The data lookup type
+        typedef Tuple2<scalar, vector> pair_type;
+
+
+    // Lookup Colour Tables
+
+        //- Look up pointer to colourTable by name, or nullptr on failure.
+        static const colourTable* ptr(const word& tableName);
+
+        //- Look up pointer to colourTable by type, or nullptr on failure.
+        static const colourTable* ptr(const predefinedType tbl);
+
+        //- Look up pointer to colourTable by name. Fatal on failure
+        static const colourTable& ref(const word& tableName);
+
+        //- Look up pointer to colourTable by type. Fatal on failure
+        static const colourTable& ref(const predefinedType tbl);
+
+
+private:
+
+    // Private Static Data
+
+        //- Predefined tables
+        static HashPtrTable<colourTable> tables_;
+
+
+    // Private Data
+
+        //- The table control points
+        List<pair_type> table_;
+
+        //- Interpolator type
+        interpolationType interp_;
+
+
+    // Private Member Functions
+
+        //- Construct from central "etc/colourTables" file.
+        static void constructTables();
+
+
+public:
+
+    // Constructors
+
+        //- Copy construct from table values
+        explicit colourTable
+        (
+            const List<Tuple2<scalar, vector>>& values,
+            const interpolationType interp = interpolationType::RGB
+        );
+
+        //- Copy construct from table values
+        explicit colourTable
+        (
+            List<Tuple2<scalar, vector>>&& values,
+            const interpolationType interp = interpolationType::RGB
+        );
+
+        //- Read construct from dictionary
+        explicit colourTable
+        (
+            const dictionary& dict,
+            const interpolationType interp = interpolationType::RGB
+        );
+
+
+    // Selectors
+
+        //- Read as dictionary content
+        static autoPtr<colourTable> New(Istream& is);
+
+
+    //- Destructor
+    virtual ~colourTable() = default;
+
+
+    // Member Functions
+
+    // Access
+
+        //- Predefined tables
+        static const HashPtrTable<colourTable>& tables();
+
+        //- Return the colour at x (within 0-1 range)
+        vector value(const scalar x) const;
+
+        //- Return a discrete lookup table of colours
+        List<Tuple2<scalar, vector>> table(const label nColours) const;
+
+
+    // IO
+
+        //- Write as dictionary format
+        Ostream& writeDict(Ostream& os) const;
+};
+
+
+//- Write as dictionary format
+Ostream& operator<<(Ostream& os, const colourTable& tbl);
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/fileFormats/colours/colourTables.C b/src/fileFormats/colours/colourTables.C
new file mode 100644
index 0000000000000000000000000000000000000000..e0679ec140f0f1ed87531df73e6257c7cd0f6a16
--- /dev/null
+++ b/src/fileFormats/colours/colourTables.C
@@ -0,0 +1,132 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+#include "colourTable.H"
+#include "etcFiles.H"
+#include "IFstream.H"
+#include "HashSet.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+Foam::HashPtrTable<Foam::colourTable> Foam::colourTable::tables_;
+
+
+
+const Foam::Enum
+<
+    Foam::colourTable::predefinedType
+>
+Foam::colourTable::predefinedNames
+({
+    { predefinedType::COOL_WARM, "coolToWarm" },
+    { predefinedType::COLD_HOT, "coldAndHot" },
+    { predefinedType::FIRE, "fire" },
+    { predefinedType::RAINBOW, "rainbow" },
+    { predefinedType::GREYSCALE, "greyscale" },
+    { predefinedType::XRAY, "xray" },
+});
+
+
+// * * * * * * * * * * * * * Static Member Functions * * * * * * * * * * * * //
+
+void Foam::colourTable::constructTables()
+{
+    if (tables_.size())
+    {
+        FatalErrorInFunction
+            << "attempt to re-construct colourTables when they already exist"
+            << exit(FatalError);
+    }
+
+    IFstream is(findEtcFile("colourTables", true));  // Mandatory file
+
+    HashPtrTable<colourTable> newEntries(is);
+    tables_.swap(newEntries);
+
+    Info<< "loaded " << tables_.sortedToc()
+        << " from etc/colourTable" << endl;
+
+    Info<< "== " << tables_ << nl;
+}
+
+
+const Foam::HashPtrTable<Foam::colourTable>& Foam::colourTable::tables()
+{
+    if (tables_.empty())
+    {
+        constructTables();
+    }
+
+    return tables_;
+}
+
+
+const Foam::colourTable* Foam::colourTable::ptr(const word& tableName)
+{
+    if (tables_.empty())
+    {
+        constructTables();
+    }
+
+    const auto iter = tables_.cfind(tableName);
+
+    if (iter.good())
+    {
+        const colourTable* p = iter.val();
+        return p;
+    }
+
+    return nullptr;
+}
+
+
+const Foam::colourTable* Foam::colourTable::ptr(const predefinedType tbl)
+{
+    return ptr(predefinedNames[tbl]);
+}
+
+
+const Foam::colourTable& Foam::colourTable::ref(const word& tableName)
+{
+    const colourTable* p = ptr(tableName);
+
+    if (!p)
+    {
+        FatalErrorInFunction
+            << "No such colourTable: " << tableName
+            << exit(FatalError);
+    }
+
+    return *p;
+}
+
+
+const Foam::colourTable& Foam::colourTable::ref(const predefinedType tbl)
+{
+    return ref(predefinedNames[tbl]);
+}
+
+
+// ************************************************************************* //
diff --git a/src/fileFormats/colours/colourTools.C b/src/fileFormats/colours/colourTools.C
new file mode 100644
index 0000000000000000000000000000000000000000..db27ecaa963e94ae535c53adaeea5f5647ce7c55
--- /dev/null
+++ b/src/fileFormats/colours/colourTools.C
@@ -0,0 +1,501 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Note
+    Some implementation details from VTK vtkColorTransferFunction.cxx
+    vtkMath.cxx
+
+\*---------------------------------------------------------------------------*/
+
+#include "colourTools.H"
+#include "mathematicalConstants.H"
+
+using namespace Foam::constant;
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+static constexpr scalar oneThird = 1.0 / 3.0;
+static constexpr scalar oneSixth = 1.0 / 6.0;
+static constexpr scalar twoThird = 2.0 / 3.0;
+static constexpr scalar fiveSixth = 5.0 / 6.0;
+
+
+// Compute HSV from RGB
+static inline void RGB_to_HSV
+(
+    const scalar r, const scalar g, const scalar b,
+    scalar& h, scalar& s, scalar& v
+)
+{
+    scalar cmin = r, cmax = r;
+
+    if (g > cmax)
+    {
+        cmax = g;
+    }
+    else if (g < cmin)
+    {
+        cmin = g;
+    }
+    if (b > cmax)
+    {
+        cmax = b;
+    }
+    else if (b < cmin)
+    {
+        cmin = b;
+    }
+
+    v = cmax;
+    s = (v > 0.0) ? ((cmax - cmin) / cmax) : 0.0;
+
+    if (s > 0.0)
+    {
+        if (r == cmax)
+        {
+            h = oneSixth * (g - b) / (cmax - cmin);
+        }
+        else if (g == cmax)
+        {
+            h = oneThird + oneSixth * (b - r) / (cmax - cmin);
+        }
+        else
+        {
+            h = twoThird + oneSixth * (r - g) / (cmax - cmin);
+        }
+
+        if (h < 0.0)
+        {
+            h += 1.0;
+        }
+    }
+    else
+    {
+        h = 0.0;
+    }
+}
+
+
+// Compute HSV to RGB
+static inline void HSV_to_RGB
+(
+    const scalar h, const scalar s, const scalar v,
+    scalar& r, scalar& g, scalar& b
+)
+{
+    if (h > oneSixth && h <= oneThird)
+    {
+        // green/red
+        g = 1.0;
+        r = (oneThird - h) / oneSixth;
+        b = 0.0;
+    }
+    else if (h > oneThird && h <= 0.5)
+    {
+        // green/blue
+        g = 1.0;
+        b = (h - oneThird) / oneSixth;
+        r = 0.0;
+    }
+    else if (h > 0.5 && h <= twoThird)
+    {
+        // blue/green
+        b = 1.0;
+        g = (twoThird - h) / oneSixth;
+        r = 0.0;
+    }
+    else if (h > twoThird && h <= fiveSixth)
+    {
+        // blue/red
+        b = 1.0;
+        r = (h - twoThird) / oneSixth;
+        g = 0.0;
+    }
+    else if (h > fiveSixth && h <= 1.0)
+    {
+        // red/blue
+        r = 1.0;
+        b = (1.0 - h) / oneSixth;
+        g = 0.0;
+    }
+    else
+    {
+        // red/green
+        r = 1.0;
+        g = h / oneSixth;
+        b = 0.0;
+    }
+
+    // Add saturation
+    r = (s * r + (1.0 - s));
+    g = (s * g + (1.0 - s));
+    b = (s * b + (1.0 - s));
+
+    r *= v;
+    g *= v;
+    b *= v;
+}
+
+
+// Intermediate calculation to XYZ
+static inline scalar to_XYZ(scalar val)
+{
+    const scalar p3 = pow3(val);
+    return (p3 > 0.008856 ? p3 : (val - 16.0 / 116.0) / 7.787);
+}
+
+
+// Intermediate calculation from XYZ
+static inline scalar from_XYZ(scalar val)
+{
+    return (val > 0.008856) ? cbrt(val) : (7.787 * val) + (16.0 / 116.0);
+}
+
+// Observer= 2 deg Illuminant= D65
+static constexpr scalar ref_X = 0.9505;
+static constexpr scalar ref_Y = 1.000;
+static constexpr scalar ref_Z = 1.089;
+
+static inline void LAB_to_XYZ
+(
+    const scalar L, const scalar a, const scalar b,
+    scalar& x, scalar& y, scalar& z
+)
+{
+    const scalar var_Y = (L + 16.0) / 116.0;
+    const scalar var_X = a / 500 + var_Y;
+    const scalar var_Z = var_Y - b / 200;
+
+    x = ref_X * to_XYZ(var_X);
+    y = ref_Y * to_XYZ(var_Y);
+    z = ref_Z * to_XYZ(var_Z);
+}
+
+
+static inline void XYZ_to_LAB
+(
+    const scalar x, const scalar y, const scalar z,
+    scalar& L, scalar& a, scalar& b
+)
+{
+    const scalar var_X = from_XYZ(x / ref_X);
+    const scalar var_Y = from_XYZ(y / ref_Y);
+    const scalar var_Z = from_XYZ(z / ref_Z);
+
+    L = (116 * var_Y) - 16;
+    a = 500 * (var_X - var_Y);
+    b = 200 * (var_Y - var_Z);
+}
+
+
+
+// "Gamma correction" specified by the sRGB color space.
+
+static inline scalar gamma_from_xyz(const scalar val)
+{
+    return
+    (
+        val > 0.0031308
+      ? (1.055 * (pow(val, 1.0/2.4)) - 0.055)
+      : 12.92 * val
+    );
+}
+
+
+static inline scalar gamma_to_xyz(const scalar val)
+{
+    return
+    (
+        val > 0.04045
+      ? (pow((val + 0.055) / 1.055, 2.4))
+      : val / 12.92
+    );
+}
+
+
+
+static inline void XYZ_to_RGB
+(
+    const scalar x, const scalar y, const scalar z,
+    scalar& r, scalar& g, scalar& b
+)
+{
+    r = gamma_from_xyz(x *  3.2406 + y * -1.5372 + z * -0.4986);
+    g = gamma_from_xyz(x * -0.9689 + y *  1.8758 + z *  0.0415);
+    b = gamma_from_xyz(x *  0.0557 + y * -0.2040 + z *  1.0570);
+
+    // Clip colour range
+    scalar cmax = r;
+    if (cmax < g) cmax = g;
+    if (cmax < b) cmax = b;
+    if (cmax > 1.0)
+    {
+        r /= cmax;
+        g /= cmax;
+        b /= cmax;
+    }
+
+    if (r < 0) r = 0;
+    if (g < 0) g = 0;
+    if (b < 0) b = 0;
+}
+
+
+static inline void RGB_to_XYZ
+(
+    scalar r, scalar g, scalar b,
+    scalar& x, scalar& y, scalar& z
+)
+{
+    r = gamma_to_xyz(r);
+    g = gamma_to_xyz(g);
+    b = gamma_to_xyz(b);
+
+    x = r * 0.4124 + g * 0.3576 + b * 0.1805;
+    y = r * 0.2126 + g * 0.7152 + b * 0.0722;
+    z = r * 0.0193 + g * 0.1192 + b * 0.9505;
+}
+
+
+
+//- Convert to special polar version of CIELAB
+//  (for creating continuous diverging color maps).
+inline void labToMsh(const vector& lab, vector& msh)
+{
+    const scalar& L = lab[0];
+    const scalar& a = lab[1];
+    const scalar& b = lab[2];
+
+    msh[0] = sqrt(L*L + a*a + b*b);
+    msh[1] = (msh[0] > 0.001) ? acos(L / msh[0]) : 0.0;
+    msh[2] = (msh[1] > 0.001) ? atan2(b,a) : 0.0;
+}
+
+
+//- Convert from special polar version of CIELAB
+inline void mshToLab(const vector& msh, vector& lab)
+{
+    lab[0] = msh[0]*cos(msh[1]);
+    lab[1] = msh[0]*sin(msh[1])*cos(msh[2]);
+    lab[2] = msh[0]*sin(msh[1])*sin(msh[2]);
+}
+
+
+// Return the smallest angle between the two
+static inline scalar angleDiff(scalar angle1, scalar angle2)
+{
+    scalar adiff = angle1 - angle1;
+    if (adiff < 0.0) adiff = -adiff;
+
+    while (adiff >= mathematical::twoPi) adiff -= mathematical::twoPi;
+    if (adiff > mathematical::pi) adiff = (mathematical::twoPi - adiff);
+    return adiff;
+}
+
+
+// For the case when interpolating from a saturated color to an unsaturated
+// color, find a hue for the unsaturated color that makes sense.
+static inline scalar adjustHue(const vector& msh, scalar unsatM)
+{
+    if (msh[0] >= unsatM - 0.1)
+    {
+        // The best we can do is hold hue constant.
+        return msh[2];
+    }
+
+    // This equation is designed to make the perceptual change of the
+    // interpolation to be close to constant.
+    const scalar hueSpin =
+        msh[1]*sqrt(unsatM*unsatM - msh[0]*msh[0]) / (msh[0]*sin(msh[1]));
+
+    // Spin hue away from 0 except in purple hues.
+    if (msh[2] > -0.3*mathematical::pi)
+    {
+        return msh[2] + hueSpin;
+    }
+    else
+    {
+        return msh[2] - hueSpin;
+    }
+}
+
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::colourTools::rgbToHsv(const vector& rgb, vector& hsv)
+{
+    RGB_to_HSV(rgb[0], rgb[1], rgb[2], hsv[0], hsv[1], hsv[2]);
+}
+
+void Foam::colourTools::hsvToRgb(const vector& hsv, vector& rgb)
+{
+    HSV_to_RGB(hsv[0], hsv[1], hsv[2], rgb[0], rgb[1], rgb[2]);
+}
+
+
+void Foam::colourTools::rgbToXyz(const vector& rgb, vector& xyz)
+{
+    RGB_to_XYZ(rgb[0], rgb[1], rgb[2], xyz[0], xyz[1], xyz[2]);
+}
+
+void Foam::colourTools::xyzToRgb(const vector& xyz, vector& rgb)
+{
+    XYZ_to_RGB(xyz[0], xyz[1], xyz[2], rgb[0], rgb[1], rgb[2]);
+}
+
+
+void Foam::colourTools::labToXyz(const vector& lab, vector& xyz)
+{
+    LAB_to_XYZ(lab[0], lab[1], lab[2], xyz[0], xyz[1], xyz[2]);
+}
+
+
+void Foam::colourTools::xyzToLab(const vector& xyz, vector& lab)
+{
+    XYZ_to_LAB(xyz[0], xyz[1], xyz[2], lab[0], lab[1], lab[2]);
+}
+
+
+void Foam::colourTools::rgbToLab(const vector& rgb, vector& lab)
+{
+    vector xyz;
+    RGB_to_XYZ(rgb[0], rgb[1], rgb[2], xyz[0], xyz[1], xyz[2]);
+    XYZ_to_LAB(xyz[0], xyz[1], xyz[2], lab[0], lab[1], lab[2]);
+}
+
+
+void Foam::colourTools::labToRgb(const vector& lab, vector& rgb)
+{
+    vector xyz;
+    labToXyz(lab, xyz);
+    xyzToRgb(xyz, rgb);
+}
+
+
+void Foam::colourTools::interpolateDiverging
+(
+    scalar s,
+    const vector& rgb1,
+    const vector& rgb2,
+    vector& result
+)
+{
+    vector lab1, lab2;
+    rgbToLab(rgb1, lab1);
+    rgbToLab(rgb2, lab2);
+
+    vector msh1, msh2;
+    labToMsh(lab1, msh1);
+    labToMsh(lab2, msh2);
+
+    // If the endpoints are distinct saturated colors,
+    // then place white in between them.
+    if
+    (
+        msh1[1] > 0.05
+     && msh2[1] > 0.05
+     && angleDiff(msh1[2], msh2[2]) > mathematical::pi/3.0
+    )
+    {
+        // Insert the white midpoint by setting one end to white and
+        // adjusting the scalar value.
+
+        scalar Mmid = std::max(msh1[0], msh2[0]);
+        Mmid = std::max(88.0, Mmid);
+        if (s < 0.5)
+        {
+            msh2[0] = Mmid; msh2[1] = 0; msh2[2] = 0;
+            s = 2.0*s;
+        }
+        else
+        {
+            msh1[0] = Mmid; msh1[1] = 0; msh1[2] = 0;
+            s = 2.0*s - 1.0;
+        }
+    }
+
+    // If one color has no saturation, then its hue value is invalid.
+    // In this case, we want to set it to something logical so the
+    // interpolation of hue makes sense.
+    if ((msh1[1] < 0.05) && (msh2[1] > 0.05))
+    {
+        msh1[2] = adjustHue(msh2, msh1[0]);
+    }
+    else if ((msh2[1] < 0.05) && (msh1[1] > 0.05))
+    {
+        msh2[2] = adjustHue(msh1, msh2[0]);
+    }
+
+    // Msh tmp
+    vector mshTmp((1-s)*msh1 + s*msh2);
+
+    // Convert back to RGB
+    vector lab;
+    mshToLab(mshTmp, lab);
+    labToRgb(lab, result);
+}
+
+
+void Foam::colourTools::interpolateHSV
+(
+    scalar s,
+    const vector& rgb1,
+    const vector& rgb2,
+    vector& result
+)
+{
+    vector hsv1, hsv2;
+    rgbToHsv(rgb1, hsv1);
+    rgbToHsv(rgb2, hsv2);
+
+    // Wrap HSV?
+    if (hsv1[0] - hsv2[0] > 0.5 || hsv2[0] - hsv1[0] > 0.5)
+    {
+        if (hsv1[0] > hsv2[0])
+        {
+            hsv1[0] -= 1.0;
+        }
+        else
+        {
+            hsv2[0] -= 1.0;
+        }
+    }
+
+    vector hsvTmp((1-s)*hsv1 + s*hsv2);
+
+    if (hsvTmp[0] < 0.0)
+    {
+        hsvTmp[0] += 1.0;
+    }
+
+    // Convert back to RGB
+    hsvToRgb(hsvTmp, result);
+}
+
+
+// ************************************************************************* //
diff --git a/src/fileFormats/colours/colourTools.H b/src/fileFormats/colours/colourTools.H
new file mode 100644
index 0000000000000000000000000000000000000000..e2040dca50c196ec93ecc0d49671302f9a4e2f5e
--- /dev/null
+++ b/src/fileFormats/colours/colourTools.H
@@ -0,0 +1,214 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Namespace
+    Foam::colourTools
+
+Description
+    Utility methods for colours and colour spaces
+
+SourceFiles
+    colourTools.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef colourTools_H
+#define colourTools_H
+
+#include "vector.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace colourTools
+{
+
+/*---------------------------------------------------------------------------*\
+                             Namespace colourTools
+\*---------------------------------------------------------------------------*/
+
+//- Convert RGB to HSV
+void rgbToHsv(const vector& rgb, vector& hsv);
+
+//- Convert RGB to HSV
+inline vector rgbToHsv(const vector& rgb)
+{
+    vector hsv;
+    rgbToHsv(rgb, hsv);
+    return hsv;
+}
+
+
+//- Convert HSV to RGB
+void hsvToRgb(const vector& hsv, vector& rgb);
+
+//- Convert HSV to RGB
+inline vector hsvToRgb(const vector& hsv)
+{
+    vector rgb;
+    hsvToRgb(hsv, rgb);
+    return rgb;
+}
+
+
+//- Convert RGB to XYZ
+void rgbToXyz(const vector& rgb, vector& xyz);
+
+//- Convert RGB to XYZ
+inline vector rgbToXyz(const vector& rgb)
+{
+    vector xyz;
+    rgbToXyz(rgb, xyz);
+    return xyz;
+}
+
+//- Convert XYZ to RGB
+void xyzToRgb(const vector& xyz, vector& rgb);
+
+//- Convert XYZ to RGB
+inline vector xyzToRgb(const vector& xyz)
+{
+    vector rgb;
+    xyzToRgb(xyz, rgb);
+    return rgb;
+}
+
+
+//- Convert LAB to XYZ
+void labToXyz(const vector& lab, vector& xyz);
+
+//- Convert LAB to XYZ
+inline vector labToXyz(const vector& lab)
+{
+    vector xyz;
+    labToXyz(lab, xyz);
+    return xyz;
+}
+
+
+//- Convert XYZ to LAB
+void xyzToLab(const vector& xyz, vector& lab);
+
+//- Convert XYZ to LAB
+inline vector xyzToLab(const vector& xyz)
+{
+    vector lab;
+    xyzToLab(xyz, lab);
+    return lab;
+}
+
+
+//- Convert RGB to LAB
+void rgbToLab(const vector& rgb, vector& lab);
+
+//- Convert RGB to LAB
+inline vector rgbToLab(const vector& rgb)
+{
+    vector lab;
+    rgbToLab(rgb, lab);
+    return lab;
+}
+
+
+//- Convert LAB to RGB
+void labToRgb(const vector& lab, vector& rgb);
+
+//- Convert LAB to RGB
+inline vector labToRgb(const vector& lab)
+{
+    vector rgb;
+    labToRgb(lab, rgb);
+    return rgb;
+}
+
+
+//- Interpolate RGB values with diverging color map
+void interpolateDiverging
+(
+    scalar s,
+    const vector& rgb1,
+    const vector& rgb2,
+    vector& result
+);
+
+//- Interpolate RGB values with diverging color map
+inline vector interpolateDiverging
+(
+    scalar s,
+    const vector& rgb1,
+    const vector& rgb2
+)
+{
+    vector result;
+    interpolateDiverging(s, rgb1, rgb2, result);
+    return result;
+}
+
+
+//- Interpolate RGB values in HSV colourspace
+void interpolateHSV
+(
+    scalar s,
+    const vector& rgb1,
+    const vector& rgb2,
+    vector& result
+);
+
+//- Interpolate RGB values in HSV colourspace
+inline vector interpolateHSV
+(
+    scalar s,
+    const vector& rgb1,
+    const vector& rgb2
+)
+{
+    vector result;
+    interpolateHSV(s, rgb1, rgb2, result);
+    return result;
+}
+
+
+//- Interpolate RGB values in RGB colourspace
+inline vector interpolateRGB
+(
+    scalar s,
+    const vector& rgb1,
+    const vector& rgb2
+)
+{
+    return ((1-s)*rgb1 + s*rgb2);
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace colourTools
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/surfMesh/Make/files b/src/surfMesh/Make/files
index 4891715e49de82e90f67fa0fd546e7fd6cf10c01..d629b1ccd2d9fc7ff01835e3651cebbd5bfe0513 100644
--- a/src/surfMesh/Make/files
+++ b/src/surfMesh/Make/files
@@ -70,7 +70,7 @@ $(writers)/proxy/proxySurfaceWriter.C
 $(writers)/raw/rawSurfaceWriter.C
 $(writers)/starcd/starcdSurfaceWriter.C
 $(writers)/vtk/vtkSurfaceWriter.C
-/* $(writers)/x3d/x3dSurfaceWriter.C */
+$(writers)/x3d/x3dSurfaceWriter.C
 
 
 LIB = $(FOAM_LIBBIN)/libsurfMesh
diff --git a/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormat.C b/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormat.C
index 99d975762538d92a22140b470044992cd7afa784..62caeba061c1154dc85f5a72efdc795faf3d4590 100644
--- a/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormat.C
+++ b/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormat.C
@@ -61,14 +61,9 @@ void Foam::fileFormats::X3DsurfaceFormat<Face>::write
     }
 
     writeHeader(os);
-
-    os  << "\n"
-        "<Group>\n"
-        " <Shape>\n";
-
+    beginGroup(os);
     writeAppearance(os);
 
-
     // NOTE: we could provide an optimized IndexedTriangleSet output for
     // triangulated surfaces too
 
@@ -108,21 +103,16 @@ void Foam::fileFormats::X3DsurfaceFormat<Face>::write
         }
     }
 
-    os <<
-        "' >\n"
-        "    <Coordinate point='\n";
+    os  <<
+        "' >\n";
 
-    for (const point& p : pointLst)
-    {
-        os  << p.x() << ' ' << p.y() << ' ' << p.z() << nl;
-    }
+    writePoints(os, pointLst);
 
     os  <<
-        "' />\n"                       // end Coordinate
-        "   </IndexedFaceSet>\n"
-        "  </Shape>\n"
-        " </Group>\n"
-        "</X3D>\n";
+        "   </IndexedFaceSet>\n";
+
+    endGroup(os);
+    writeFooter(os);
 }
 
 
diff --git a/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.C b/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.C
index 4bff32656e56c7b4084980aca8c278f47aa5e7f3..6d343377829baaa934cc5459f52d443aff52d81d 100644
--- a/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.C
+++ b/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.C
@@ -2,7 +2,7 @@
   =========                 |
   \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
    \\    /   O peration     |
-    \\  /    A nd           | Copyright (C) 2009-2010 OpenCFD Ltd.
+    \\  /    A nd           | Copyright (C) 2009-2010, 2019 OpenCFD Ltd.
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
                             | Copyright (C) 2011-2016 OpenFOAM Foundation
@@ -26,7 +26,7 @@ License
 \*---------------------------------------------------------------------------*/
 
 #include "X3DsurfaceFormatCore.H"
-#include "clock.H"
+#include "Ostream.H"
 
 // * * * * * * * * * * * * Protected Member Functions  * * * * * * * * * * * //
 
@@ -49,6 +49,38 @@ void Foam::fileFormats::X3DsurfaceFormatCore::writeHeader
 }
 
 
+void Foam::fileFormats::X3DsurfaceFormatCore::writeFooter
+(
+    Ostream& os
+)
+{
+    os  <<
+        "</X3D>\n";
+}
+
+
+void Foam::fileFormats::X3DsurfaceFormatCore::beginGroup
+(
+    Ostream& os
+)
+{
+    os  <<
+        "<Group>\n"
+        " <Shape>\n";
+}
+
+
+void Foam::fileFormats::X3DsurfaceFormatCore::endGroup
+(
+    Ostream& os
+)
+{
+    os  <<
+        "  </Shape>\n"
+        " </Group>\n";
+}
+
+
 void Foam::fileFormats::X3DsurfaceFormatCore::writeAppearance
 (
     Ostream& os
@@ -57,13 +89,34 @@ void Foam::fileFormats::X3DsurfaceFormatCore::writeAppearance
     os  <<
         "  <Appearance>\n"
         "   <Material"
-        " diffuseColor='0.8 0.8 0.8'"
-        " specularColor='1.0 1.0 1.0'"
-        " shininess='0.5'"
-        " transparency='0.0'"
-        " />\n"           // end material
+        " ambientIntensity='0'"
+        " diffuseColor='1 1 1'" // Default: '0.8 0.8 0.8'
+        // Default: " emissiveColor='0 0 0'"
+        // Default: " specularColor='0 0 0'"
+        " shininess='0.8'"      // Default: 0.2
+        " transparency='0'"
+        " />\n"           // Material
         "  </Appearance>\n";
 }
 
 
+void Foam::fileFormats::X3DsurfaceFormatCore::writePoints
+(
+    Ostream& os,
+    const UList<point>& pts
+)
+{
+    os  <<
+        "    <Coordinate point='\n";
+
+    for (const point& p : pts)
+    {
+        os  << p.x() << ' ' << p.y() << ' ' << p.z() << ',' << nl;
+    }
+
+    os  <<
+        "' />\n";
+}
+
+
 // ************************************************************************* //
diff --git a/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.H b/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.H
index b9fb256109bed0b24f0a2a2cf39de6208f4f92d2..d87332cf01662320eb8844f3f25509a11689e879 100644
--- a/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.H
+++ b/src/surfMesh/surfaceFormats/x3d/X3DsurfaceFormatCore.H
@@ -37,7 +37,7 @@ SourceFiles
 #ifndef X3DsurfaceFormatCore_H
 #define X3DsurfaceFormatCore_H
 
-#include "Ostream.H"
+#include "pointField.H"
 
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 
@@ -59,9 +59,20 @@ protected:
     //- Write file header
     static void writeHeader(Ostream& os);
 
+    //- Write file header
+    static void writeFooter(Ostream& os);
+
+    //- Begin Group/Shape node
+    static void beginGroup(Ostream& os);
+
+    //- End Group/Shape node
+    static void endGroup(Ostream& os);
+
     //- Write appearance node
     static void writeAppearance(Ostream& os);
 
+    //- Write points (Coordinate)
+    static void writePoints(Ostream& os, const UList<point>& pts);
 };
 
 
diff --git a/src/surfMesh/writers/x3d/x3dSurfaceWriter.C b/src/surfMesh/writers/x3d/x3dSurfaceWriter.C
new file mode 100644
index 0000000000000000000000000000000000000000..534c744b252d90e32767db4ca3a384c4d475935a
--- /dev/null
+++ b/src/surfMesh/writers/x3d/x3dSurfaceWriter.C
@@ -0,0 +1,342 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+#include "x3dSurfaceWriter.H"
+#include "OFstream.H"
+#include "OSspecific.H"
+#include "MeshedSurfaceProxy.H"
+#include "surfaceWriterMethods.H"
+#include "addToRunTimeSelectionTable.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace surfaceWriters
+{
+    defineTypeName(x3dWriter);
+    addToRunTimeSelectionTable(surfaceWriter, x3dWriter, word);
+    addToRunTimeSelectionTable(surfaceWriter, x3dWriter, wordDict);
+}
+}
+
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+//- A (0-1) range for colouring
+template<class Type>
+static inline scalar rangex(const scalarMinMax& range, const Type& val)
+{
+    scalar x = Foam::mag(val);
+
+    return (x - range.min()) / (range.max() - range.min());
+}
+
+
+//- A (0-1) range for colouring
+template<>
+inline scalar rangex(const scalarMinMax& range, const scalar& val)
+{
+    scalar x = val;
+    return (x - range.min()) / (range.max() - range.min());
+}
+
+
+//- A (0-1) range for colouring
+template<>
+inline scalar rangex(const scalarMinMax& range, const label& val)
+{
+    scalar x = val;
+    return (x - range.min()) / (range.max() - range.min());
+}
+
+
+static inline void printColour(Ostream& os, const vector& rgb)
+{
+    os  << rgb[0] << ' ' << rgb[1] << ' ' << rgb[2] << ',' << nl;
+}
+
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::surfaceWriters::x3dWriter::x3dWriter()
+:
+    surfaceWriter(),
+    range_(),
+    colourTablePtr_(nullptr)
+{}
+
+
+Foam::surfaceWriters::x3dWriter::x3dWriter
+(
+    const dictionary& options
+)
+:
+    surfaceWriter(options),
+    range_(),
+    colourTablePtr_(nullptr)
+{
+    verbose_ = true;
+
+    options.readIfPresent("range", range_);
+
+    word tableName;
+    if (options.readIfPresent("colourMap", tableName))
+    {
+        colourTablePtr_ = colourTable::ptr(tableName);
+        if (!colourTablePtr_)
+        {
+            WarningInFunction
+                << "No colourMap " << tableName << " using default" << nl;
+        }
+    }
+
+    if (!colourTablePtr_)
+    {
+        tableName = colourTable::predefinedNames[colourTable::COOL_WARM];
+        colourTablePtr_ = colourTable::ptr(colourTable::COOL_WARM);
+    }
+
+    if (verbose_)
+    {
+        Info<< "X3D with colourMap '" << tableName << "' and range ";
+
+        if (range_.valid())
+        {
+            Info<< range_;
+        }
+        else
+        {
+            Info<< "auto";
+        }
+        Info<< nl;
+    }
+}
+
+
+Foam::surfaceWriters::x3dWriter::x3dWriter
+(
+    const meshedSurf& surf,
+    const fileName& outputPath,
+    bool parallel,
+    const dictionary& options
+)
+:
+    x3dWriter(options)
+{
+    open(surf, outputPath, parallel);
+}
+
+
+Foam::surfaceWriters::x3dWriter::x3dWriter
+(
+    const pointField& points,
+    const faceList& faces,
+    const fileName& outputPath,
+    bool parallel,
+    const dictionary& options
+)
+:
+    x3dWriter(options)
+{
+    open(points, faces, outputPath, parallel);
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+Foam::fileName Foam::surfaceWriters::x3dWriter::write()
+{
+    checkOpen();
+
+    // Geometry:  rootdir/<TIME>/surfaceName.x3d
+
+    fileName outputFile = outputPath_;
+    if (useTimeDir() && !timeName().empty())
+    {
+        // Splice in time-directory
+        outputFile = outputPath_.path() / timeName() / outputPath_.name();
+    }
+    outputFile.ext("x3d");
+
+    if (verbose_)
+    {
+        Info<< "Writing geometry to " << outputFile << endl;
+    }
+
+    const meshedSurf& surf = surface();
+
+    if (Pstream::master() || !parallel_)
+    {
+        if (!isDir(outputFile.path()))
+        {
+            mkDir(outputFile.path());
+        }
+
+        MeshedSurfaceProxy<face>
+        (
+            surf.points(),
+            surf.faces()
+        ).write(outputFile, "x3d");
+    }
+
+    wroteGeom_ = true;
+    return outputFile;
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+template<class Type>
+Foam::fileName Foam::surfaceWriters::x3dWriter::writeTemplate
+(
+    const word& fieldName,
+    const Field<Type>& localValues
+)
+{
+    if (!colourTablePtr_)
+    {
+        // Write geometry only if there are no colours to use
+        WarningInFunction
+            << "No output colours set" << endl;
+
+        return this->write();
+    }
+
+    checkOpen();
+
+    // Field:  rootdir/<TIME>/<field>_surfaceName.x3d
+
+    fileName outputFile = outputPath_.path();
+    if (useTimeDir() && !timeName().empty())
+    {
+        // Splice in time-directory
+        outputFile /= timeName();
+    }
+
+    // Append <field>_surfaceName.usr
+    outputFile /= fieldName + '_' + outputPath_.name();
+    outputFile.ext("x3d");
+
+    if (verbose_)
+    {
+        Info<< "Writing field " << fieldName << " to " << outputFile << endl;
+    }
+
+    const meshedSurf& surf = surface();
+
+    // geometry merge() implicit
+    tmp<Field<Type>> tfield = mergeField(localValues);
+
+    if (Pstream::master() || !parallel_)
+    {
+        const auto& values = tfield();
+
+        scalarMinMax range(range_);
+
+        if (!range.valid())
+        {
+            range = minMaxMag(values);
+        }
+
+        if (!isDir(outputFile.path()))
+        {
+            mkDir(outputFile.path());
+        }
+
+        OFstream os(outputFile);
+
+        writeHeader(os);
+        beginGroup(os);
+        writeAppearance(os);
+
+        // For point field: "colorPerVetex=true"
+        os  << "  <IndexedFaceSet"
+            << " colorPerVertex='" << Switch(this->isPointData()) << "'"
+            << " coordIndex='" << nl;
+
+        for (const auto& f : surf.faces())
+        {
+            for (const label vrti : f)
+            {
+                os << vrti << ' ';
+            }
+            os << "-1\n";
+        }
+        os  << "'";
+
+        // Colour indices for face fields
+        if (!this->isPointData())
+        {
+            const label nFaces = surf.faces().size();
+
+            os  << " colorIndex='";
+
+            for (label i=0; i < nFaces; ++i)
+            {
+                os << i << ' ';
+            }
+            os  << "'";
+        }
+
+        os  << " >\n";  // IndexedFaceSet
+
+        writePoints(os, surf.points());
+
+        os << "<Color color='" << nl;
+
+        // writeColours(os, values, range, colorBar);
+
+        for (const Type& val : values)
+        {
+            const scalar x = rangex(range, val);
+            vector rgb = colourTablePtr_->value(x);
+            printColour(os, rgb);
+        }
+
+        os << "' />" << nl;  // Color
+
+        os  <<
+            "   </IndexedFaceSet>\n";
+
+        endGroup(os);
+        writeFooter(os);
+    }
+
+    wroteGeom_ = true;
+    return outputFile;
+}
+
+
+// Field writing methods
+defineSurfaceWriterWriteFields(Foam::surfaceWriters::x3dWriter);
+
+
+// ************************************************************************* //
diff --git a/src/surfMesh/writers/x3d/x3dSurfaceWriter.H b/src/surfMesh/writers/x3d/x3dSurfaceWriter.H
new file mode 100644
index 0000000000000000000000000000000000000000..d12c90dfac2e6cf3a2a05059c19858904edbce49
--- /dev/null
+++ b/src/surfMesh/writers/x3d/x3dSurfaceWriter.H
@@ -0,0 +1,169 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2019 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM 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 General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Class
+    Foam::surfaceWriters::x3dWriter
+
+Description
+    A surfaceWriter for X3D files.
+
+    The formatOptions for x3d:
+    \table
+        Property    | Description                           | Required | Default
+        range       | The min/max range for colour table    | no  | automatic
+        colourMap   | The colour map for rendering          | no  | coolToWarm
+    \endtable
+
+    \heading Output file locations
+
+    The \c rootdir normally corresponds to something like
+    \c postProcessing/\<name\>
+
+    \subheading Geometry
+    \verbatim
+    rootdir
+    `-- timeName
+        `-- surfaceName.x3d
+    \endverbatim
+
+    \subheading Fields
+    \verbatim
+    rootdir
+    `-- timeName
+        |-- <field0>_surfaceName.x3d
+        `-- <field1>_surfaceName.x3d
+    \endverbatim
+
+SourceFiles
+    x3dSurfaceWriter.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef x3dSurfaceWriter_H
+#define x3dSurfaceWriter_H
+
+#include "surfaceWriter.H"
+#include "X3DsurfaceFormatCore.H"
+#include "colourTable.H"
+#include "MinMax.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace surfaceWriters
+{
+
+/*---------------------------------------------------------------------------*\
+                          Class x3dWriter Declaration
+\*---------------------------------------------------------------------------*/
+
+class x3dWriter
+:
+    public surfaceWriter,
+    protected fileFormats::X3DsurfaceFormatCore
+{
+    // Private Data
+
+        //- Range of values
+        //  The lower/upper limits for the colour table output
+        //  Undefined means calculate from the data
+        scalarMinMax range_;
+
+        //- Selected colour table
+        const colourTable* colourTablePtr_;
+
+
+    // Private Member Functions
+
+        //- Templated write operation
+        template<class Type>
+        fileName writeTemplate
+        (
+            const word& fieldName,          //!< Name of field
+            const Field<Type>& localValues  //!< Local field values to write
+        );
+
+
+public:
+
+    //- Runtime type information
+    TypeNameNoDebug("x3d");
+
+
+    // Constructors
+
+        //- Construct null
+        x3dWriter();
+
+        //- Construct with some output options
+        explicit x3dWriter(const dictionary& options);
+
+        //- Construct from components
+        x3dWriter
+        (
+            const meshedSurf& surf,
+            const fileName& outputPath,
+            bool parallel = Pstream::parRun(),
+            const dictionary& options = dictionary()
+        );
+
+        //- Construct from components
+        x3dWriter
+        (
+            const pointField& points,
+            const faceList& faces,
+            const fileName& outputPath,
+            bool parallel = Pstream::parRun(),
+            const dictionary& options = dictionary()
+        );
+
+
+    //- Destructor
+    virtual ~x3dWriter() = default;
+
+
+    // Member Functions
+
+        //- Write surface geometry to file.
+        virtual fileName write(); // override
+
+        declareSurfaceWriterWriteMethod(label);
+        declareSurfaceWriterWriteMethod(scalar);
+        declareSurfaceWriterWriteMethod(vector);
+        declareSurfaceWriterWriteMethod(sphericalTensor);
+        declareSurfaceWriterWriteMethod(symmTensor);
+        declareSurfaceWriterWriteMethod(tensor);
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace surfaceWriters
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //