diff --git a/Documentation/Examples/CMakeLists.txt b/Documentation/Examples/CMakeLists.txt
index 621cab01d1facaa687c12f287c12ed2d54a6501f..925cdac6ef4a98b4dc63b2c01696c1d82aad9bc2 100644
--- a/Documentation/Examples/CMakeLists.txt
+++ b/Documentation/Examples/CMakeLists.txt
@@ -30,7 +30,6 @@ target_link_libraries (cascade_example SuperStupidStack CORSIKAunits CORSIKAlogg
   ProcessStackInspector
   ProcessTrackWriter
   ProcessTrackingLine
-  ProcessHadronicElasticModel
   CORSIKAprocesses
   CORSIKAparticles
   CORSIKAgeometry
@@ -40,6 +39,23 @@ target_link_libraries (cascade_example SuperStupidStack CORSIKAunits CORSIKAlogg
 install (TARGETS cascade_example DESTINATION share/examples)
 CORSIKA_ADD_TEST (cascade_example)
 
+add_executable (boundary_example boundary_example.cc)
+target_compile_options(boundary_example PRIVATE -g) # do not skip asserts
+target_link_libraries (boundary_example SuperStupidStack CORSIKAunits CORSIKAlogging
+   CORSIKArandom
+   ProcessSibyll
+  CORSIKAcascade
+  ProcessTrackWriter
+  ProcessTrackingLine
+  CORSIKAprocesses
+  CORSIKAparticles
+  CORSIKAgeometry
+  CORSIKAenvironment
+  CORSIKAprocesssequence
+  )
+install (TARGETS boundary_example DESTINATION share/examples)
+CORSIKA_ADD_TEST (boundary_example)
+
 add_executable (staticsequence_example staticsequence_example.cc)
 target_compile_options(staticsequence_example PRIVATE -g) # do not skip asserts
 target_link_libraries (staticsequence_example
diff --git a/Documentation/Examples/boundary_example.cc b/Documentation/Examples/boundary_example.cc
new file mode 100644
index 0000000000000000000000000000000000000000..0c767160e08123292379ad330941c2f37479b627
--- /dev/null
+++ b/Documentation/Examples/boundary_example.cc
@@ -0,0 +1,342 @@
+
+/*
+ * (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * See file AUTHORS for a list of contributors.
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#include <corsika/cascade/Cascade.h>
+#include <corsika/process/ProcessSequence.h>
+#include <corsika/process/tracking_line/TrackingLine.h>
+
+#include <corsika/setup/SetupEnvironment.h>
+#include <corsika/setup/SetupStack.h>
+#include <corsika/setup/SetupTrajectory.h>
+
+#include <corsika/environment/Environment.h>
+#include <corsika/environment/HomogeneousMedium.h>
+#include <corsika/environment/NuclearComposition.h>
+
+#include <corsika/geometry/Sphere.h>
+
+#include <corsika/process/sibyll/Decay.h>
+#include <corsika/process/sibyll/Interaction.h>
+#include <corsika/process/sibyll/NuclearInteraction.h>
+
+#include <corsika/process/track_writer/TrackWriter.h>
+
+#include <corsika/units/PhysicalUnits.h>
+
+#include <corsika/random/RNGManager.h>
+
+#include <corsika/utl/CorsikaFenv.h>
+
+#include <iostream>
+#include <limits>
+#include <typeinfo>
+
+using namespace corsika;
+using namespace corsika::process;
+using namespace corsika::units;
+using namespace corsika::particles;
+using namespace corsika::random;
+using namespace corsika::setup;
+using namespace corsika::geometry;
+using namespace corsika::environment;
+
+using namespace std;
+using namespace corsika::units::si;
+
+class ProcessCut : public process::ContinuousProcess<ProcessCut> {
+
+  HEPEnergyType fECut;
+
+  HEPEnergyType fEnergy = 0_GeV;
+  HEPEnergyType fEmEnergy = 0_GeV;
+  int fEmCount = 0;
+  HEPEnergyType fInvEnergy = 0_GeV;
+  int fInvCount = 0;
+
+public:
+  ProcessCut(const HEPEnergyType cut)
+      : fECut(cut) {}
+
+  template <typename Particle>
+  bool isBelowEnergyCut(Particle& p) const {
+    // nuclei
+    if (p.GetPID() == particles::Code::Nucleus) {
+      auto const ElabNuc = p.GetEnergy() / p.GetNuclearA();
+      auto const EcmNN = sqrt(2. * ElabNuc * 0.93827_GeV);
+      if (ElabNuc < fECut || EcmNN < 10_GeV)
+        return true;
+      else
+        return false;
+    } else {
+      // TODO: center-of-mass energy hard coded
+      const HEPEnergyType Ecm = sqrt(2. * p.GetEnergy() * 0.93827_GeV);
+      if (p.GetEnergy() < fECut || Ecm < 10_GeV)
+        return true;
+      else
+        return false;
+    }
+  }
+
+  bool isEmParticle(Code pCode) const {
+    bool is_em = false;
+    // FOR NOW: switch
+    switch (pCode) {
+      case Code::Electron:
+        is_em = true;
+        break;
+      case Code::Positron:
+        is_em = true;
+        break;
+      case Code::Gamma:
+        is_em = true;
+        break;
+      default:
+        break;
+    }
+    return is_em;
+  }
+
+  void defineEmParticles() const {
+    // create bool array identifying em particles
+  }
+
+  bool isInvisible(Code pCode) const {
+    bool is_inv = false;
+    // FOR NOW: switch
+    switch (pCode) {
+      case Code::NuE:
+        is_inv = true;
+        break;
+      case Code::NuEBar:
+        is_inv = true;
+        break;
+      case Code::NuMu:
+        is_inv = true;
+        break;
+      case Code::NuMuBar:
+        is_inv = true;
+        break;
+      case Code::MuPlus:
+        is_inv = true;
+        break;
+      case Code::MuMinus:
+        is_inv = true;
+        break;
+
+      case Code::Neutron:
+        is_inv = true;
+        break;
+
+      case Code::AntiNeutron:
+        is_inv = true;
+        break;
+
+      default:
+        break;
+    }
+    return is_inv;
+  }
+
+  template <typename Particle>
+  LengthType MaxStepLength(Particle& p, setup::Trajectory&) const {
+    cout << "ProcessCut: MinStep: pid: " << p.GetPID() << endl;
+    cout << "ProcessCut: MinStep: energy (GeV): " << p.GetEnergy() / 1_GeV << endl;
+    const Code pid = p.GetPID();
+    if (isEmParticle(pid) || isInvisible(pid) || isBelowEnergyCut(p)) {
+      cout << "ProcessCut: MinStep: next cut: " << 0. << endl;
+      return 0_m;
+    } else {
+      LengthType next_step = 1_m * std::numeric_limits<double>::infinity();
+      cout << "ProcessCut: MinStep: next cut: " << next_step << endl;
+      return next_step;
+    }
+  }
+
+  template <typename Particle, typename Stack>
+  EProcessReturn DoContinuous(Particle& p, setup::Trajectory&, Stack&) {
+    const Code pid = p.GetPID();
+    HEPEnergyType energy = p.GetEnergy();
+    cout << "ProcessCut: DoContinuous: " << pid << " E= " << energy
+         << ", EcutTot=" << (fEmEnergy + fInvEnergy + fEnergy) / 1_GeV << " GeV" << endl;
+    EProcessReturn ret = EProcessReturn::eOk;
+    if (isEmParticle(pid)) {
+      cout << "removing em. particle..." << endl;
+      fEmEnergy += energy;
+      fEmCount += 1;
+      // p.Delete();
+      ret = EProcessReturn::eParticleAbsorbed;
+    } else if (isInvisible(pid)) {
+      cout << "removing inv. particle..." << endl;
+      fInvEnergy += energy;
+      fInvCount += 1;
+      // p.Delete();
+      ret = EProcessReturn::eParticleAbsorbed;
+    } else if (isBelowEnergyCut(p)) {
+      cout << "removing low en. particle..." << endl;
+      fEnergy += energy;
+      // p.Delete();
+      ret = EProcessReturn::eParticleAbsorbed;
+    }
+    return ret;
+  }
+
+  void Init() {
+    fEmEnergy = 0. * 1_GeV;
+    fEmCount = 0;
+    fInvEnergy = 0. * 1_GeV;
+    fInvCount = 0;
+    fEnergy = 0. * 1_GeV;
+    // defineEmParticles();
+  }
+
+  void ShowResults() {
+    cout << " ******************************" << endl
+         << " ParticleCut: " << endl
+         << " energy in em.  component (GeV):  " << fEmEnergy / 1_GeV << endl
+         << " no. of em.  particles injected:  " << fEmCount << endl
+         << " energy in inv. component (GeV):  " << fInvEnergy / 1_GeV << endl
+         << " no. of inv. particles injected:  " << fInvCount << endl
+         << " energy below particle cut (GeV): " << fEnergy / 1_GeV << endl
+         << " ******************************" << endl;
+  }
+
+  HEPEnergyType GetInvEnergy() const { return fInvEnergy; }
+  HEPEnergyType GetCutEnergy() const { return fEnergy; }
+  HEPEnergyType GetEmEnergy() const { return fEmEnergy; }
+};
+
+template <bool deleteParticle>
+struct MyBoundaryCrossingProcess
+    : public BoundaryCrossingProcess<MyBoundaryCrossingProcess<deleteParticle>> {
+
+  MyBoundaryCrossingProcess(std::string const& filename) { fFile.open(filename); }
+
+  template <typename Particle>
+  EProcessReturn DoBoundaryCrossing(Particle& p,
+                                    typename Particle::BaseNodeType const& from,
+                                    typename Particle::BaseNodeType const& to) {
+    std::cout << "boundary crossing! from: " << &from << "; to: " << &to << std::endl;
+
+    auto const& name = particles::GetName(p.GetPID());
+    auto const start = p.GetPosition().GetCoordinates();
+
+    fFile << name << "    " << start[0] / 1_m << ' ' << start[1] / 1_m << ' '
+          << start[2] / 1_m << '\n';
+
+    if constexpr (deleteParticle) { p.Delete(); }
+
+    return EProcessReturn::eOk;
+  }
+
+  void Init() {}
+
+private:
+  std::ofstream fFile;
+};
+
+//
+// The example main program for a particle cascade
+//
+int main() {
+  feenableexcept(FE_INVALID);
+  // initialize random number sequence(s)
+  random::RNGManager::GetInstance().RegisterRandomStream("cascade");
+
+  // setup environment, geometry
+  using EnvType = environment::Environment<setup::IEnvironmentModel>;
+  EnvType env;
+  auto& universe = *(env.GetUniverse());
+
+  const CoordinateSystem& rootCS = env.GetCoordinateSystem();
+
+  auto outerMedium = EnvType::CreateNode<Sphere>(
+      Point{rootCS, 0_m, 0_m, 0_m}, 1_km * std::numeric_limits<double>::infinity());
+
+  // fraction of oxygen
+  auto const props =
+      outerMedium
+          ->SetModelProperties<environment::HomogeneousMedium<setup::IEnvironmentModel>>(
+              1_kg / (1_m * 1_m * 1_m),
+              environment::NuclearComposition(
+                  std::vector<particles::Code>{particles::Code::Proton},
+                  std::vector<float>{1.f}));
+
+  auto innerMedium = EnvType::CreateNode<Sphere>(Point{rootCS, 0_m, 0_m, 0_m}, 5_km);
+
+  innerMedium->SetModelProperties(props);
+
+  outerMedium->AddChild(std::move(innerMedium));
+
+  universe.AddChild(std::move(outerMedium));
+
+  // setup processes, decays and interactions
+  tracking_line::TrackingLine tracking;
+
+  random::RNGManager::GetInstance().RegisterRandomStream("s_rndm");
+  process::sibyll::Interaction sibyll;
+  process::sibyll::NuclearInteraction sibyllNuc(sibyll);
+  process::sibyll::Decay decay;
+  ProcessCut cut(20_GeV);
+
+  process::TrackWriter::TrackWriter trackWriter("tracks.dat");
+  MyBoundaryCrossingProcess<true> boundaryCrossing("crossings.dat");
+
+  // assemble all processes into an ordered process list
+  auto sequence = sibyll << sibyllNuc << decay << cut << boundaryCrossing << trackWriter;
+
+  // setup particle stack, and add primary particles
+  setup::Stack stack;
+  stack.Clear();
+  const Code beamCode = Code::Proton;
+  const HEPMassType mass = particles::GetMass(Code::Proton);
+  const HEPEnergyType E0 = 50_TeV;
+
+  std::uniform_real_distribution distTheta(0., 180.);
+  std::uniform_real_distribution distPhi(0., 360.);
+  std::mt19937 rng;
+
+  for (int i = 0; i < 100; ++i) {
+    auto const theta = distTheta(rng);
+    auto const phi = distPhi(rng);
+
+    auto elab2plab = [](HEPEnergyType Elab, HEPMassType m) {
+      return sqrt((Elab - m) * (Elab + m));
+    };
+    HEPMomentumType P0 = elab2plab(E0, mass);
+    auto momentumComponents = [](double theta, double phi, HEPMomentumType ptot) {
+      return std::make_tuple(ptot * sin(theta) * cos(phi), ptot * sin(theta) * sin(phi),
+                             -ptot * cos(theta));
+    };
+    auto const [px, py, pz] =
+        momentumComponents(theta / 180. * M_PI, phi / 180. * M_PI, P0);
+    auto plab = corsika::stack::MomentumVector(rootCS, {px, py, pz});
+    cout << "input particle: " << beamCode << endl;
+    cout << "input angles: theta=" << theta << " phi=" << phi << endl;
+    cout << "input momentum: " << plab.GetComponents() / 1_GeV << endl;
+    Point pos(rootCS, 0_m, 0_m, 0_m);
+    stack.AddParticle(
+        std::tuple<particles::Code, units::si::HEPEnergyType,
+                   corsika::stack::MomentumVector, geometry::Point, units::si::TimeType>{
+            beamCode, E0, plab, pos, 0_ns});
+  }
+
+  // define air shower object, run simulation
+  cascade::Cascade EAS(env, tracking, sequence, stack);
+  EAS.Init();
+  EAS.Run();
+
+  cout << "Result: E0=" << E0 / 1_GeV << endl;
+  cut.ShowResults();
+  const HEPEnergyType Efinal =
+      cut.GetCutEnergy() + cut.GetInvEnergy() + cut.GetEmEnergy();
+  cout << "total energy (GeV): " << Efinal / 1_GeV << endl
+       << "relative difference (%): " << (Efinal / E0 - 1.) * 100 << endl;
+}
diff --git a/Documentation/Examples/cascade_example.cc b/Documentation/Examples/cascade_example.cc
index 18c03158ab46f9909d06860f04cb2a173a88ae00..67ff9c8fa14173764044fc5b60749e6dfd9e6606 100644
--- a/Documentation/Examples/cascade_example.cc
+++ b/Documentation/Examples/cascade_example.cc
@@ -11,8 +11,6 @@
 
 #include <corsika/cascade/Cascade.h>
 #include <corsika/process/ProcessSequence.h>
-#include <corsika/process/hadronic_elastic_model/HadronicElasticModel.h>
-#include <corsika/process/stack_inspector/StackInspector.h>
 #include <corsika/process/tracking_line/TrackingLine.h>
 
 #include <corsika/setup/SetupEnvironment.h>
@@ -21,7 +19,6 @@
 
 #include <corsika/environment/Environment.h>
 #include <corsika/environment/HomogeneousMedium.h>
-#include <corsika/environment/NameModel.h>
 #include <corsika/environment/NuclearComposition.h>
 
 #include <corsika/geometry/Sphere.h>
@@ -216,35 +213,6 @@ public:
   HEPEnergyType GetEmEnergy() const { return fEmEnergy; }
 };
 
-template <bool deleteParticle>
-struct MyBoundaryCrossingProcess
-    : public BoundaryCrossingProcess<MyBoundaryCrossingProcess<deleteParticle>> {
-
-  MyBoundaryCrossingProcess(std::string const& filename) { fFile.open(filename); }
-
-  template <typename Particle>
-  EProcessReturn DoBoundaryCrossing(Particle& p,
-                                    typename Particle::BaseNodeType const& from,
-                                    typename Particle::BaseNodeType const& to) {
-    std::cout << "boundary crossing! from: " << &from << "; to: " << &to << std::endl;
-
-    auto const& name = particles::GetName(p.GetPID());
-    auto const start = p.GetPosition().GetCoordinates();
-
-    fFile << name << "    " << start[0] / 1_m << ' ' << start[1] / 1_m << ' '
-          << start[2] / 1_m << '\n';
-
-    if constexpr (deleteParticle) { p.Delete(); }
-
-    return EProcessReturn::eOk;
-  }
-
-  void Init() {}
-
-private:
-  std::ofstream fFile;
-};
-
 //
 // The example main program for a particle cascade
 //
@@ -284,7 +252,6 @@ int main() {
 
   // setup processes, decays and interactions
   tracking_line::TrackingLine tracking;
-  stack_inspector::StackInspector<setup::Stack> p0(true);
 
   random::RNGManager::GetInstance().RegisterRandomStream("s_rndm");
   process::sibyll::Interaction sibyll;
@@ -293,42 +260,42 @@ int main() {
   ProcessCut cut(20_GeV);
 
   process::TrackWriter::TrackWriter trackWriter("tracks.dat");
-  MyBoundaryCrossingProcess<false> boundaryCrossing("crossings.dat");
 
   // assemble all processes into an ordered process list
-  auto sequence = sibyll << sibyllNuc << decay << cut << boundaryCrossing << trackWriter;
+  auto sequence = sibyll << sibyllNuc << decay << cut << trackWriter;
 
   // setup particle stack, and add primary particle
   setup::Stack stack;
   stack.Clear();
-  const Code beamCode = Code::Proton;
-  const int nuclA = 56;
+  const Code beamCode = Code::Nucleus;
+  const int nuclA = 4;
   const int nuclZ = int(nuclA / 2.15 + 0.7);
-  const HEPMassType mass = particles::Proton::GetMass() * nuclZ +
-                           (nuclA - nuclZ) * particles::Neutron::GetMass();
-  const HEPEnergyType E0 = nuclA * 100_TeV;
-  double const phi = 0;
-  double const theta = 0;
-  auto elab2plab = [](HEPEnergyType Elab, HEPMassType m) {
-    return sqrt(Elab * Elab - m * m);
-  };
-  HEPMomentumType P0 = elab2plab(E0, mass);
-  auto momentumComponents = [](double theta, double phi, HEPMomentumType ptot) {
-    return std::make_tuple(ptot * sin(theta) * cos(phi), ptot * sin(theta) * sin(phi),
-                           -ptot * cos(theta));
-  };
-  auto const [px, py, pz] =
-      momentumComponents(theta / 180. * M_PI, phi / 180. * M_PI, P0);
-  auto plab = corsika::stack::MomentumVector(rootCS, {px, py, pz});
-  cout << "input particle: " << beamCode << endl;
-  cout << "input angles: theta=" << theta << " phi=" << phi << endl;
-  cout << "input momentum: " << plab.GetComponents() / 1_GeV << endl;
-  Point pos(rootCS, 0_m, 0_m, 0_m);
-
-  stack.AddParticle(
-      std::tuple<particles::Code, units::si::HEPEnergyType,
-                 corsika::stack::MomentumVector, geometry::Point, units::si::TimeType>{
-          beamCode, E0, plab, pos, 0_ns});
+  const HEPMassType mass = nuclA * particles::GetMass(Code::Proton);
+  const HEPEnergyType E0 = nuclA * 25_TeV;
+  double theta = 0.;
+  double phi = 0.;
+
+  {
+    auto elab2plab = [](HEPEnergyType Elab, HEPMassType m) {
+      return sqrt((Elab - m) * (Elab + m));
+    };
+    HEPMomentumType P0 = elab2plab(E0, mass);
+    auto momentumComponents = [](double theta, double phi, HEPMomentumType ptot) {
+      return std::make_tuple(ptot * sin(theta) * cos(phi), ptot * sin(theta) * sin(phi),
+                             -ptot * cos(theta));
+    };
+    auto const [px, py, pz] =
+        momentumComponents(theta / 180. * M_PI, phi / 180. * M_PI, P0);
+    auto plab = corsika::stack::MomentumVector(rootCS, {px, py, pz});
+    cout << "input particle: " << beamCode << endl;
+    cout << "input angles: theta=" << theta << " phi=" << phi << endl;
+    cout << "input momentum: " << plab.GetComponents() / 1_GeV << endl;
+    Point pos(rootCS, 0_m, 0_m, 0_m);
+    stack.AddParticle(std::tuple<particles::Code, units::si::HEPEnergyType,
+                                 corsika::stack::MomentumVector, geometry::Point,
+                                 units::si::TimeType, unsigned short, unsigned short>{
+        beamCode, E0, plab, pos, 0_ns, nuclA, nuclZ});
+  }
 
   // define air shower object, run simulation
   cascade::Cascade EAS(env, tracking, sequence, stack);
diff --git a/Environment/VolumeTreeNode.h b/Environment/VolumeTreeNode.h
index a9b790035817c7010cd85537cc71d85594e286ba..54bc0cc76d05a473fcdcd2a8cfa3d390f32f865b 100644
--- a/Environment/VolumeTreeNode.h
+++ b/Environment/VolumeTreeNode.h
@@ -90,6 +90,8 @@ namespace corsika::environment {
     auto const& GetVolume() const { return *fGeoVolume; }
 
     auto const& GetModelProperties() const { return *fModelProperties; }
+    
+    auto const& HasModelProperties() const { return fModelProperties.get() != nullptr; }
 
     template <typename ModelProperties, typename... Args>
     auto SetModelProperties(Args&&... args) {
diff --git a/Framework/Cascade/Cascade.h b/Framework/Cascade/Cascade.h
index f47266e9e5012c35849dfa13f34c82071a0bebf6..4da4609aea684ec8dbefc4ca35230a527aec44a1 100644
--- a/Framework/Cascade/Cascade.h
+++ b/Framework/Cascade/Cascade.h
@@ -143,12 +143,10 @@ namespace corsika::cascade {
 
       auto const* currentLogicalNode = particle.GetNode();
 
-      auto const* currentNumericalNode =
-          fEnvironment.GetUniverse()->GetContainingNode(particle.GetPosition());
-
-      if (currentNumericalNode == &*fEnvironment.GetUniverse()) {
-        throw std::runtime_error("particle entered void Universe");
-      }
+      // assert that particle stays outside void Universe if it has no
+      // model properties set
+      assert(currentLogicalNode != &*fEnvironment.GetUniverse() ||
+             fEnvironment.GetUniverse()->HasModelProperties());
 
       // convert next_step from grammage to length
       LengthType const distance_interact =
@@ -230,7 +228,7 @@ namespace corsika::cascade {
 
         auto const assertion = [&] {
           auto const* numericalNodeAfterStep =
-              fEnvironment.GetUniverse()->GetContainingNode(particle.GetPosition()); 
+              fEnvironment.GetUniverse()->GetContainingNode(particle.GetPosition());
           return numericalNodeAfterStep == currentLogicalNode;
         };
 
@@ -238,7 +236,7 @@ namespace corsika::cascade {
       } else {               // boundary crossing, step is limited by volume boundary
         std::cout << "boundary crossing! next node = " << nextVol << std::endl;
         particle.SetNode(nextVol);
-	// DoBoundary may delete the particle (or not)
+        // DoBoundary may delete the particle (or not)
         fProcessSequence.DoBoundaryCrossing(particle, *currentLogicalNode, *nextVol);
       }
     }
diff --git a/Tools/CMakeLists.txt b/Tools/CMakeLists.txt
index c0e4c12b9088e5384462b527158d1315fb47322d..c1347108d1d451651bb8a6a593c4acd7c8aab66d 100644
--- a/Tools/CMakeLists.txt
+++ b/Tools/CMakeLists.txt
@@ -1,4 +1,4 @@
-set (TOOLS_FILES plot_tracks.sh)
+set (TOOLS_FILES plot_tracks.sh plot_crossings.sh)
 
 install (
   FILES ${TOOLS_FILES} 
diff --git a/Tools/plot_crossings.sh b/Tools/plot_crossings.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d4a318e20ab7fe013ed89afae57b5a3c9220ea98
--- /dev/null
+++ b/Tools/plot_crossings.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+# (c) Copyright 2018 CORSIKA Project, corsika-project@lists.kit.edu
+#
+# See file AUTHORS for a list of contributors.
+#
+# This software is distributed under the terms of the GNU General Public
+# Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+# the license.
+
+# with this script you can plot an animation of output of TrackWriter
+
+crossings_dat=$1
+if [ -z "$crossings_dat" ]; then
+  echo "usage: $0 <crossings.dat> [output.gif]" >&2
+  exit 1
+fi
+
+output=$2
+if [ -z "$output" ]; then
+  output="$crossings_dat.gif"
+fi
+
+cat <<EOF | gnuplot
+set term gif animate size 600,600
+set output "$output"
+
+set xlabel "x / m"
+set ylabel "y / m"
+set zlabel "z / m"
+set title "CORSIKA 8 preliminary"
+
+do for [t=0:360:1] {
+	set view 60, t
+        splot "$crossings_dat" u 2:3:4 w points t ""
+}
+EOF
+
+exit $?