diff --git a/applications/utilities/postProcessing/lagrangian/particleTracks/createFields.H b/applications/utilities/postProcessing/lagrangian/particleTracks/createFields.H
index 742fe261d2f6411e6323295a1e6d9fb57ffae4ad..36186a9e92c846f7aa3321b152220cdd8bc1956d 100644
--- a/applications/utilities/postProcessing/lagrangian/particleTracks/createFields.H
+++ b/applications/utilities/postProcessing/lagrangian/particleTracks/createFields.H
@@ -15,4 +15,12 @@ const label sampleFrequency(propsDict.get<label>("sampleFrequency"));
 
 const label maxPositions(propsDict.get<label>("maxPositions"));
 
+const label maxTracks(propsDict.getOrDefault<label>("maxTracks", -1));
+
 const word setFormat(propsDict.getOrDefault<word>("setFormat", "vtk"));
+
+const wordRes fieldNames(propsDict.getOrDefault<wordRes>("fields", wordRes()));
+
+const word UName(propsDict.getOrDefault<word>("U", "U"));
+
+const dictionary formatOptions = propsDict.subOrEmptyDict("formatOptions");
diff --git a/applications/utilities/postProcessing/lagrangian/particleTracks/particleTracks.C b/applications/utilities/postProcessing/lagrangian/particleTracks/particleTracks.C
index e247d87021bf24936553544bc04016602a79f23f..c66fe1995fc33174d23d35cbf2de11b1595983fd 100644
--- a/applications/utilities/postProcessing/lagrangian/particleTracks/particleTracks.C
+++ b/applications/utilities/postProcessing/lagrangian/particleTracks/particleTracks.C
@@ -6,6 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2016 OpenFOAM Foundation
+    Copyright (C) 2021 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -30,8 +31,8 @@ Group
     grpPostProcessingUtilities
 
 Description
-    Generate a legacy VTK file of particle tracks for cases that were
-    computed using a tracked-parcel-type cloud.
+    Generate particle tracks for cases that were computed using a
+    tracked-parcel-type cloud.
 
 \*---------------------------------------------------------------------------*/
 
@@ -44,16 +45,206 @@ Description
 #include "OFstream.H"
 #include "passiveParticleCloud.H"
 #include "writer.H"
+#include "ListOps.H"
+
+#define createTrack(field, trackValues)                                        \
+createTrackField                                                               \
+(                                                                              \
+    field,                                                                     \
+    sampleFrequency,                                                           \
+    maxPositions,                                                              \
+    startIds,                                                                  \
+    allOrigProcs,                                                              \
+    allOrigIds,                                                                \
+    trackValues                                                                \
+);
+
+#define setFields(fields, fieldNames)                                          \
+setTrackFields                                                                 \
+(                                                                              \
+    obr,                                                                       \
+    fields,                                                                    \
+    fieldNames,                                                                \
+    nTracks,                                                                   \
+    startIds,                                                                  \
+    allOrigProcs,                                                              \
+    allOrigIds,                                                                \
+    maxPositions,                                                              \
+    sampleFrequency                                                            \
+);
+
+#define writeFields(fields, fieldNames, tracks, times, dirs)                   \
+writeTrackFields                                                               \
+(                                                                              \
+    fields,                                                                    \
+    fieldNames,                                                                \
+    tracks,                                                                    \
+    times,                                                                     \
+    dirs,                                                                      \
+    setFormat,                                                                 \
+    formatOptions,                                                             \
+    cloudName                                                                  \
+);
 
 using namespace Foam;
 
+template<class Type>
+void createTrackField
+(
+    const Field<Type>& values,
+    const label sampleFrequency,
+    const label maxPositions,
+    const labelList& startIds,
+    const List<labelField>& allOrigProcs,
+    const List<labelField>& allOrigIds,
+    List<DynamicList<Type>>& trackValues
+)
+{
+    List<Field<Type>> procField(Pstream::nProcs());
+    procField[Pstream::myProcNo()] = values;
+    Pstream::gatherList(procField);
+
+    if (!Pstream::master())
+    {
+        return;
+    }
+
+    const label nTracks = trackValues.size();
+
+    forAll(procField, proci)
+    {
+        forAll(procField[proci], i)
+        {
+            const label globalId =
+                startIds[allOrigProcs[proci][i]] + allOrigIds[proci][i];
+
+            if (globalId % sampleFrequency == 0)
+            {
+                const label trackId = globalId/sampleFrequency;
+
+                if
+                (
+                    trackId < nTracks
+                 && trackValues[trackId].size() < maxPositions
+                )
+                {
+                    trackValues[trackId].append(procField[proci][i]);
+                }
+            }
+        }
+    }
+}
+
+
+template<class Type>
+void writeTrackFields
+(
+    List<List<DynamicList<Type>>>& fieldValues,
+    const wordList& fieldNames,
+    const PtrList<coordSet>& tracks,
+    const List<scalarField>& times,
+    const List<vectorField>& dirs,
+    const word& setFormat,
+    const dictionary& formatOptions,
+    const word& cloudName
+)
+{
+    if (fieldValues.empty())
+    {
+        return;
+    }
+
+    auto writerPtr = writer<Type>::New(setFormat, formatOptions);
+
+    const fileName outFile(writerPtr().getFileName(tracks[0], wordList(0)));
+
+    const fileName outPath
+    (
+        functionObject::outputPrefix/cloud::prefix/cloudName/"particleTracks"
+    );
+    mkDir(outPath);
+
+    OFstream os(outPath/(pTraits<Type>::typeName & "tracks." + outFile.ext()));
+
+    Info<< "Writing " << pTraits<Type>::typeName << " particle tracks in "
+        << setFormat << " format to " << os.name() << endl;
+
+
+    List<List<Field<Type>>> fields(fieldValues.size());
+    forAll(fields, fieldi)
+    {
+        fields[fieldi].setSize(fieldValues[fieldi].size());
+        forAll(fields[fieldi], tracki)
+        {
+            fields[fieldi][tracki].transfer(fieldValues[fieldi][tracki]);
+        }
+    }
+
+    writerPtr().write(true, times, tracks, fieldNames, fields, os);
+}
+
+
+template<class Type>
+Foam::label setTrackFields
+(
+    const objectRegistry& obr,
+    List<List<DynamicList<Type>>>& fields,
+    List<word>& fieldNames,
+    const label nTracks,
+    const labelList& startIds,
+    const List<labelField>& allOrigProcs,
+    const List<labelField>& allOrigIds,
+    const label maxPositions,
+    const label sampleFrequency
+)
+{
+    const auto availableFieldPtrs = obr.lookupClass<IOField<Type>>();
+
+    fieldNames = availableFieldPtrs.toc();
+
+    if (Pstream::parRun())
+    {
+        Pstream::combineGather(fieldNames, ListOps::uniqueEqOp<word>());
+        Pstream::combineScatter(fieldNames);
+        Foam::sort(fieldNames);
+    }
+
+    const label nFields = fieldNames.size();
+
+    if (fields.empty())
+    {
+        fields.setSize(nFields);
+        fieldNames.setSize(nFields);
+        forAll(fields, i)
+        {
+            fields[i].setSize(nTracks);
+        }
+    }
+
+    forAll(fieldNames, fieldi)
+    {
+        const word& fieldName = fieldNames[fieldi];
+
+        const auto* fldPtr = obr.cfindObject<IOField<Type>>(fieldName);
+
+        createTrack
+        (
+            fldPtr ? static_cast<const Field<Type>>(*fldPtr) : Field<Type>(),
+            fields[fieldi]
+        );
+    }
+
+    return nFields;
+}
+
+
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 
 int main(int argc, char *argv[])
 {
     argList::addNote
     (
-        "Generate a legacy VTK file of particle tracks for cases that were"
+        "Generate a file of particle tracks for cases that were"
         " computed using a tracked-parcel-type cloud"
     );
 
@@ -76,9 +267,9 @@ int main(int argc, char *argv[])
         << nl << endl;
 
     labelList maxIds(Pstream::nProcs(), -1);
-    forAll(timeDirs, timeI)
+    forAll(timeDirs, timei)
     {
-        runTime.setTime(timeDirs[timeI], timeI);
+        runTime.setTime(timeDirs[timei], timei);
         Info<< "Time = " << runTime.timeName() << endl;
 
         Info<< "    Reading particle positions" << endl;
@@ -92,6 +283,8 @@ int main(int argc, char *argv[])
             const label origId = p.origId();
             const label origProc = p.origProc();
 
+            // Handle case where we are processing particle data generated in
+            // parallel using more cores than when running this application.
             if (origProc >= maxIds.size())
             {
                 maxIds.setSize(origProc+1, -1);
@@ -124,7 +317,7 @@ int main(int argc, char *argv[])
 
     // Calculate starting ids for particles on each processor
     labelList startIds(numIds.size(), Zero);
-    for (label i = 0; i < numIds.size()-1; i++)
+    for (label i = 0; i < numIds.size()-1; ++i)
     {
         startIds[i+1] += startIds[i] + numIds[i];
     }
@@ -132,130 +325,135 @@ int main(int argc, char *argv[])
 
 
     // Number of tracks to generate
-    label nTracks = nParticle/sampleFrequency;
+    const label nTracks =
+        maxTracks > 0
+      ? min(nParticle/sampleFrequency, maxTracks)
+      : nParticle/sampleFrequency;
 
     // Storage for all particle tracks
     List<DynamicList<vector>> allTracks(nTracks);
+    List<DynamicList<scalar>> allTrackTimes(nTracks);
+
+    // Lists of field, tracki, trackValues
+    //List<List<DynamicList<label>>> labelFields;
+    List<List<DynamicList<scalar>>> scalarFields;
+    List<List<DynamicList<vector>>> vectorFields;
+    List<List<DynamicList<sphericalTensor>>> sphTensorFields;
+    List<List<DynamicList<symmTensor>>> symTensorFields;
+    List<List<DynamicList<tensor>>> tensorFields;
+    //List<word> labelFieldNames;
+    List<word> scalarFieldNames;
+    List<word> vectorFieldNames;
+    List<word> sphTensorFieldNames;
+    List<word> symTensorFieldNames;
+    List<word> tensorFieldNames;
 
     Info<< "\nGenerating " << nTracks << " particle tracks for cloud "
         << cloudName << nl << endl;
 
-    forAll(timeDirs, timeI)
+    forAll(timeDirs, timei)
     {
-        runTime.setTime(timeDirs[timeI], timeI);
+        runTime.setTime(timeDirs[timei], timei);
         Info<< "Time = " << runTime.timeName() << endl;
 
-        List<pointField> allPositions(Pstream::nProcs());
-        List<labelField> allOrigIds(Pstream::nProcs());
-        List<labelField> allOrigProcs(Pstream::nProcs());
-
         // Read particles. Will be size 0 if no particles.
         Info<< "    Reading particle positions" << endl;
         passiveParticleCloud myCloud(mesh, cloudName);
 
+        pointField localPositions(myCloud.size(), Zero);
+        scalarField localTimes(myCloud.size(), Zero);
+
+        List<labelField> allOrigIds(Pstream::nProcs());
+        List<labelField> allOrigProcs(Pstream::nProcs());
+
         // Collect the track data on all processors that have positions
-        allPositions[Pstream::myProcNo()].setSize
-        (
-            myCloud.size(),
-            point::zero
-        );
         allOrigIds[Pstream::myProcNo()].setSize(myCloud.size(), Zero);
         allOrigProcs[Pstream::myProcNo()].setSize(myCloud.size(), Zero);
 
         label i = 0;
         for (const passiveParticle& p : myCloud)
         {
-            allPositions[Pstream::myProcNo()][i] = p.position();
             allOrigIds[Pstream::myProcNo()][i] = p.origId();
             allOrigProcs[Pstream::myProcNo()][i] = p.origProc();
+            localPositions[i] = p.position();
+            localTimes[i] = runTime.value();
             ++i;
         }
 
         // Collect the track data on the master processor
-        Pstream::gatherList(allPositions);
         Pstream::gatherList(allOrigIds);
         Pstream::gatherList(allOrigProcs);
 
-        Info<< "    Constructing tracks" << nl << endl;
-        if (Pstream::master())
-        {
-            forAll(allPositions, proci)
-            {
-                forAll(allPositions[proci], i)
-                {
-                    label globalId =
-                        startIds[allOrigProcs[proci][i]]
-                      + allOrigIds[proci][i];
-
-                    if (globalId % sampleFrequency == 0)
-                    {
-                        label trackId = globalId/sampleFrequency;
-                        if (allTracks[trackId].size() < maxPositions)
-                        {
-                            allTracks[trackId].append
-                            (
-                                allPositions[proci][i]
-                            );
-                        }
-                    }
-                }
-            }
-        }
+        objectRegistry obr
+        (
+            IOobject
+            (
+                "cloudFields",
+                runTime.timeName(),
+                runTime
+            )
+        );
+
+        myCloud.readFromFiles(obr, fieldNames);
+
+        // Create track positions and track time fields
+        // (not registered as IOFields)
+        // Note: createTrack is a local #define to reduce arg count...
+        createTrack(localPositions, allTracks);
+        createTrack(localTimes, allTrackTimes);
+
+        // Create the track fields
+        // Note: setFields is a local #define to reduce arg count...
+        //setFields(labelFields, labelFieldNames);
+        setFields(scalarFields, scalarFieldNames);
+        setFields(vectorFields, vectorFieldNames);
+        setFields(sphTensorFields, sphTensorFieldNames);
+        setFields(symTensorFields, symTensorFieldNames);
+        setFields(tensorFields, tensorFieldNames);
     }
 
 
     if (Pstream::master())
     {
         PtrList<coordSet> tracks(allTracks.size());
-        forAll(allTracks, trackI)
+        List<scalarField> times(tracks.size());
+        forAll(tracks, tracki)
         {
             tracks.set
             (
-                trackI,
-                new coordSet
-                (
-                    "track" + Foam::name(trackI),
-                    "distance"
-                )
+                tracki,
+                new coordSet("track" + Foam::name(tracki), "distance")
             );
-            tracks[trackI].transfer(allTracks[trackI]);
+            tracks[tracki].transfer(allTracks[tracki]);
+            times[tracki].transfer(allTrackTimes[tracki]);
         }
 
-        autoPtr<writer<scalar>> scalarFormatterPtr = writer<scalar>::New
-        (
-            setFormat
-        );
+        Info<< nl;
 
-        //OFstream vtkTracks(vtkPath/"particleTracks.vtk");
-        fileName vtkFile
-        (
-            scalarFormatterPtr().getFileName
-            (
-                tracks[0],
-                wordList(0)
-            )
-        );
-
-        OFstream vtkTracks
-        (
-            vtkPath/("particleTracks." + vtkFile.ext())
-        );
+        const label Uid = vectorFieldNames.find(UName);
+        List<vectorField> dirs(nTracks);
 
-        Info<< "\nWriting particle tracks in " << setFormat
-            << " format to " << vtkTracks.name()
-            << nl << endl;
+        if (Uid != -1)
+        {
+            const auto& UTracks = vectorFields[Uid];
+            forAll(UTracks, tracki)
+            {
+                const auto& U = UTracks[tracki];
+                dirs[tracki] = U/(mag(U) + ROOTVSMALL);
+            }
+        }
 
-        scalarFormatterPtr().write
-        (
-            true,
-            tracks,
-            wordList(0),
-            List<List<scalarField>>(0),
-            vtkTracks
-        );
+        // Write track fields
+        // Note: writeFields is a local #define to reduce arg count...
+        //writeFields(allLabelFields, labelFieldNames, tracks);
+        writeFields(scalarFields, scalarFieldNames, tracks, times, dirs);
+        writeFields(vectorFields, vectorFieldNames, tracks, times, dirs);
+        writeFields(sphTensorFields, sphTensorFieldNames, tracks, times, dirs);
+        writeFields(symTensorFields, symTensorFieldNames, tracks, times, dirs);
+        writeFields(tensorFields, tensorFieldNames, tracks, times, dirs);
     }
 
-    Info<< "End\n" << endl;
+    Info<< nl << "End\n" << endl;
 
     return 0;
 }
diff --git a/applications/utilities/postProcessing/lagrangian/steadyParticleTracks/steadyParticleTracks.C b/applications/utilities/postProcessing/lagrangian/steadyParticleTracks/steadyParticleTracks.C
index bc26c09774d02eeb2982679f815a6705378bc6d9..8da9c0715144e7ac52a467011083f9c686a9479a 100644
--- a/applications/utilities/postProcessing/lagrangian/steadyParticleTracks/steadyParticleTracks.C
+++ b/applications/utilities/postProcessing/lagrangian/steadyParticleTracks/steadyParticleTracks.C
@@ -27,7 +27,7 @@ Application
     steadyParticleTracks
 
 Group
-    grpPostProcessingUtilitie
+    grpPostProcessingUtilities
 
 Description
     Generate a legacy VTK file of particle tracks for cases that were