Billboarded Cutaway Visualization for Subsurface Urban Sewer Networks

Flood Simulation embedded in Unity3D

SewerManager.cs
/* Copyright (C) Manuel Matusich
* Unauthorized copying of this file, via any medium is strictly prohibited
* Written by Manuel Matusich , Juli 2017
*/

using UnityEngine;
using System;
using System.IO;
using System.Xml;
using System.Collections.Generic;

public class SewerManager: MonoBehaviour {

  public Material terrainMaterial;
  public Material groundZeroMaterial;
  public Material manholeMaterial;
  public Material shaftMaterial;
  public Material shaftInsideMaterial;
  public Material shaftPipeCancelMaterial;
  public Material shaftWaterMaterial;
  public Material buildingsMaterial;
  public Texture DTglobalTex;

  //for more details look into the method refineNodes
  private static int removedNodes = 0;
  private static int addedNodes = 0;
  private static int pipeNodesCounter = 0;

  private static int maxThreads = 20;

  //Timestep for simulation data
  public static int currenTimestep = 0;

  private int UpdateCounterPipeNodeID = 0;
  private bool computeDTtex = true;
  private data[] dataForPipeNodeBuffer;
  private GameObject mainCam;
  private Texture2D DTglobalTex_dt;
  private ComputeBuffer pipeNodesVec3ReadBuffer;
  private ComputeBuffer RWpipeNodesUVBuffer;
  private SurroundingsCommands surroundingsCommands;
  private Transform initialCamPos;

  struct data {
    public Vector3 pos;
    public int pipeID;
    public int nodeID;
  };

  struct uvData {
    public Vector2 uvPos;
    public int pipeID;
    public int nodeID;
  };

  //Awake will be called first
  void Awake() {
    //while parsing, all data will be added to the static class SewerSystem
    string sewerDataPath = Application.dataPath + "/Worringen Visdom Exports/sewer network/21_global/Sewer Network";

    //get the number of lines from the xml description document
    int nOflinkLines = getNumOfLines(sewerDataPath + "/dataDescription.xml", "links");
    int nOfnodeLines = getNumOfLines(sewerDataPath + "/dataDescription.xml", "nodes");

    //pass number of lines and read binary files
    parseShafts(sewerDataPath + "/nodes/lines.bin", nOfnodeLines);
    parseLinks(sewerDataPath + "/links/lines.bin", nOflinkLines);

    //needs to be called after parsing shafts and pipes and sets references between pipes and shafts
    parseNodeIndicesPerLink(sewerDataPath + "/nodeIndicesPerLink/data.bin", nOflinkLines);

    //pipe radius will be broadcastet to connected pipes as well
    parselinkRadii(sewerDataPath + "/linkRadii/data.bin", nOflinkLines);
    float parseStartTime = Time.realtimeSinceStartup;

    // Parsing simulation data is threaded since this would take pretty long otherwise
    /************ NODE DATA ************/

    List < ParseJob > parseJobs = new List < ParseJob > ();

    //create a job for each timestep
    for (int timestep = 0; timestep <= SewerSystem.maxSimulationTimeStep; timestep += 10) {
      ParseJob myJob = new ParseJob(nOfnodeLines, timestep, "Node");
      parseJobs.Add(myJob);
    }

    ParseJobManager parseJobManager = new ParseJobManager(maxThreads, parseJobs);
    while (!parseJobManager.update()) {
      /*wait until all jobs are finished*/
    }

    //assign data from parse jobs
    foreach(ParseJob job in parseJobs) {
      int timestep = job.timestep / 10;
      for (int i = 0; i < job.numLines; i++) {
        SewerSystem.getShaftbyIndex(i).setDepthForTimestep(job.depthDataOut[i], timestep);
        SewerSystem.getShaftbyIndex(i).setDepthColorForTimestep(job.depthColorDataOut[i], timestep);
        SewerSystem.getShaftbyIndex(i).setDischargeColorForTimestep(job.dischargeColorDataOut[i], timestep);
      }
    }

    /************ LINK DATA COLORS ************/

    parseJobs.Clear();

    //create a job for each timestep
    for (int timestep = 0; timestep <= SewerSystem.maxSimulationTimeStep; timestep += 10) {
      ParseJob myJob = new ParseJob(nOflinkLines, timestep, "Link");
      parseJobs.Add(myJob);
    }

    parseJobManager = new ParseJobManager(maxThreads, parseJobs);
    while (!parseJobManager.update()) {
      /*wait until all jobs are finished*/
    }

    //assign data from parse jobs
    foreach(ParseJob job in parseJobs) {
      int timestep = job.timestep / 10;
      for (int i = 0; i < job.numLines; i++) {
        SewerSystem.getPipebyIndex(i).setDepthForTimestep(job.depthDataOut[i], timestep);
        SewerSystem.getPipebyIndex(i).setDepthColorForTimestep(job.depthColorDataOut[i], timestep);
        SewerSystem.getPipebyIndex(i).setDischargeColorForTimestep(job.dischargeColorDataOut[i], timestep);
      }
    }

    parseJobs.Clear();

    Debug.Log("ParseTime: " + (Time.realtimeSinceStartup - parseStartTime));

    foreach(Pipe pipe in SewerSystem.getPipes()) {
      List < Vector3 > refinedPipeNodes = refineNodes(pipe.getNodes());
      pipe.addAllNodes(refinedPipeNodes);
    }

    Debug.Log("removed PipeNodes: " + removedNodes);
    Debug.Log("added PipeNodes: " + addedNodes);
    Debug.Log("PipeNodes: " + pipeNodesCounter);

    SewerSystem.intializeAfterParsing();
  }

  //Start will be called after Awake or when the GameObject, 
  //this script is attached to, is activated
  void Start() {
    //keyboard input commands for surroundings
    GameObject surroundings = GameObject.FindGameObjectWithTag("surroundings");
    surroundingsCommands = surroundings.GetComponent < SurroundingsCommands > ();

    mainCam = Camera.main.gameObject;
    initialCamPos = mainCam.transform;

    setRenderQueues();

    Texture2D textureDT = LoadPNG(Application.dataPath + "/../AfterDT.png");

    // in case there is no .png file with the distance to pipe information stored it has to be computed and stored
    if (textureDT == null) {
      // 12 Bytes for float3, 8 for float2 .... plus 4 byte per int (two integers)
      computePipeNodesForBuffer();
      pipeNodesVec3ReadBuffer = new ComputeBuffer(SewerSystem.getNumberOfPipeNodes(), sizeof(float) * 3 + sizeof(int) * 2);
      pipeNodesVec3ReadBuffer.SetData(dataForPipeNodeBuffer);
      Shader.SetGlobalBuffer("pipeNodesVec3ReadBuffer", pipeNodesVec3ReadBuffer);

      RWpipeNodesUVBuffer = new ComputeBuffer(1, sizeof(float) * 2 + sizeof(int) * 2);
      Shader.SetGlobalBuffer("RWpipeNodesUVBuffer", RWpipeNodesUVBuffer);
      Graphics.SetRandomWriteTarget(1, RWpipeNodesUVBuffer, true);

      Shader.SetGlobalInt("scanTexCoords", 1); //set to true
      Shader.SetGlobalTexture("DTglobalTex", DTglobalTex);

      surroundingsCommands.setOnlyTerrainActive();
      SewerSystem.setActive(false);

    } else {
      computeDTtex = false;
      Shader.SetGlobalTexture("DTglobalTex", textureDT);
    }

    GameObject[] groundZero = GameObject.FindGameObjectsWithTag("terrainzero");
    foreach(GameObject go in groundZero) {
      go.AddComponent < FlattenTerrain > ();
      go.BroadcastMessage("flatten");
    }

  }

  public Texture2D LoadPNG(string filePath) {
    Texture2D tex = null;
    byte[] fileData;

    if (File.Exists(filePath)) {
      fileData = File.ReadAllBytes(filePath);
      tex = new Texture2D(2, 2);
      //will auto-resize the texture dimensions.
      tex.LoadImage(fileData);
    }
    return tex;
  }

  private void distanceTransform() {
    DTglobalTex_dt = DistanceTransform.transform(DTglobalTex as Texture2D);
    byte[] bytes = DTglobalTex_dt.EncodeToPNG();
    File.WriteAllBytes(Application.dataPath + "/../AfterDT.png", bytes);
    Shader.SetGlobalTexture("DTglobalTex", DTglobalTex_dt);
  }

  private void computePipeNodesForBuffer() {
    List < Pipe > allPipes = SewerSystem.getPipes();
    List < data > nodesData = new List < data > ();

    foreach(Pipe pipe in allPipes) {
      Vector3[] nodes = pipe.getNodes().ToArray();
      for (int i = 0; i < (nodes.Length); i++) {
        data data = new data();
        data.pos = nodes[i];
        data.pipeID = pipe.id;
        data.nodeID = i;
        nodesData.Add(data);
      }
    }
    dataForPipeNodeBuffer = nodesData.ToArray();
  }

  private void setRenderQueues() {
    GameObject[] terrain = GameObject.FindGameObjectsWithTag("terrain");
    GameObject[] terrainZero = GameObject.FindGameObjectsWithTag("terrainzero");
    GameObject[] buildings = GameObject.FindGameObjectsWithTag("buildings");
    GameObject[] trees = GameObject.FindGameObjectsWithTag("tree");

    GameObject water = GameObject.FindGameObjectWithTag("water");
    water.GetComponent < Renderer > ().sharedMaterial.renderQueue = 3001;

    foreach(GameObject go in terrainZero) {
      groundZeroMaterial.renderQueue = 2005;
      go.GetComponent < Renderer > ().sharedMaterial = groundZeroMaterial;
      go.GetComponent < Renderer > ().shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
    }

    foreach(GameObject go in terrain) {
      terrainMaterial.renderQueue = 3000;
      go.GetComponent < Renderer > ().sharedMaterial = terrainMaterial;
      go.GetComponent < Renderer > ().shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
    }

    shaftMaterial.renderQueue = 2013;
    shaftInsideMaterial.renderQueue = 2013;
    manholeMaterial.renderQueue = 2014;
    shaftPipeCancelMaterial.renderQueue = 2016;
    shaftWaterMaterial.renderQueue = 2015;

    foreach(GameObject go in buildings) {
      go.GetComponent < Renderer > ().sharedMaterial = buildingsMaterial;
      go.GetComponent < Renderer > ().sharedMaterial.renderQueue = 2018;
      go.GetComponent < Renderer > ().shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
    }

    foreach(GameObject go in trees) {
      go.GetComponent < Renderer > ().sharedMaterial.renderQueue = 2020;
    }
  }

  // Update is called once per frame
  void Update() {
    if (computeDTtex) {
      if (UpdateCounterPipeNodeID < dataForPipeNodeBuffer.Length) {
        // SETUP FOR SNAPSHOT
        Shader.SetGlobalInt("UpdateCounterPipeNodeID", UpdateCounterPipeNodeID);
        Vector3 vec = dataForPipeNodeBuffer[UpdateCounterPipeNodeID++].pos;
        vec.y = 40;
        mainCam.transform.position = vec;
        mainCam.transform.LookAt(vec + Vector3.down);

        // READ
        uvData[] uvDatas = new uvData[1];
        RWpipeNodesUVBuffer.GetData(uvDatas);
        uvData uv = uvDatas[0];

        // SET
        if (uv.pipeID != 0) {
          //Debug.Log("looking for pipeID: " + uv.pipeID);
          Pipe pipe = SewerSystem.getPipeByID(uv.pipeID);
          if (pipe != null) pipe.setUV(uv.uvPos, uv.nodeID);
        }

      }
      else if (UpdateCounterPipeNodeID == dataForPipeNodeBuffer.Length) {
        Shader.SetGlobalInt("scanTexCoords", 0); //set to false
        distanceTransform();
        surroundingsCommands.setAllActive();
        SewerSystem.setActive(true);
        UpdateCounterPipeNodeID++;
        computeDTtex = false;
        mainCam.transform.position = initialCamPos.position;
      }
    }
  }

  // Fixed timestep of 0.05 seconds
  void FixedUpdate() {
    currenTimestep++;
    if (currenTimestep >= SewerSystem.simulationSteps) {
      currenTimestep = 0;
    }
    foreach(Shaft shaft in SewerSystem.getShafts()) {
      // used to update water levels in shaft
      shaft.setTimeStep(currenTimestep);
    }
  }

  // returns how many lines the binary files have
  // which is defined in xml
  static int getNumOfLines(string path, string dataType) {
    // Create an XML reader for this file.
    using(XmlReader reader = XmlReader.Create(path)) {
      bool match = false;
      while (reader.Read()) {
        // Only detect start elements.
        if (reader.IsStartElement()) {
          if (reader.Name == "name") {
            // check if this is the element we are looking for
            if (reader.ReadInnerXml() == dataType) {
              match = true;
            }
          }

          // we found a match
          if (reader.Name == "numLines" && match) {
            return Int32.Parse(reader.ReadInnerXml());
          }
        }
      }
      //in case nothing was found -1 will skip the parsing algorithm
      Debug.LogError("Failed to read the desired string in XML, the file may be missing.");
      return - 1;
    }
  }

  void parseShafts(string path, int numLines) {

    // Read the data.
    using(BinaryReader bReader = new BinaryReader(File.Open(path, FileMode.Open))) {

      //Parse for each single Line, always 2 positions
      for (int index = 0; index < numLines; index++) {
        // The first element is the id of the line
        int id = bReader.ReadInt32();
        // The second element is the number of positions in the line, shoudl be 2 here
        int numPositions = bReader.ReadInt32();
        if (numPositions != 2) Debug.LogError("SHAFT numPositions != 2 ERROR");

        //first position is the lower one
        Shaft shaft = new Shaft(id, this.gameObject, shaftMaterial, shaftInsideMaterial, manholeMaterial, shaftWaterMaterial, shaftPipeCancelMaterial);
        Transform dummyTransform = new GameObject().transform;

        // Three doubles per position (x,y,z) times number of positions to read
        float x = (float) bReader.ReadDouble();
        float y = (float) bReader.ReadDouble();
        float z = (float) bReader.ReadDouble();

        //for some reason y needs to be inverted to make it fit properly in unity
        y *= -1;

        //and some rotations are needed to make it fit to the terrain

        dummyTransform.position = new Vector3(x, y, z);
        dummyTransform.RotateAround(new Vector3(0f, 0f, 0f), new Vector3(1f, 0f, 0f), -90f);
        dummyTransform.RotateAround(new Vector3(0f, 0f, 0f), new Vector3(0f, 1f, 0f), 180f);

        shaft.setLower(dummyTransform.position);

        // Three doubles per position (x,y,z) times number of positions to read
        x = (float) bReader.ReadDouble();
        y = (float) bReader.ReadDouble();
        z = (float) bReader.ReadDouble();

        //for some reason y needs to be inverted to make it fit properly in unity
        y *= -1;

        //and some rotations are needed to make it fit to the terrain
        dummyTransform.position = new Vector3(x, y, z);
        dummyTransform.RotateAround(new Vector3(0f, 0f, 0f), new Vector3(1f, 0f, 0f), -90f);
        dummyTransform.RotateAround(new Vector3(0f, 0f, 0f), new Vector3(0f, 1f, 0f), 180f);

        shaft.setUpper(dummyTransform.position);

        Destroy(dummyTransform.gameObject);

        SewerSystem.addShaft(shaft);

      }
    }
  }

  void parseLinks(string path, int numLines) {

    // Read the data.
    using(BinaryReader bReader = new BinaryReader(File.Open(path, FileMode.Open))) {

      //Parse for each single Line, there might be >= 2 positions per line
      for (int i = 0; i < numLines; i++) {
        // The first element is the id of the line
        int id = bReader.ReadInt32();
        // The second element is the number of positions in the line
        int numPositions = bReader.ReadInt32();

        Pipe pipe = new Pipe(id, this.gameObject);
        List < Vector3 > pipeNodes = new List < Vector3 > ();

        // Three doubles per position (x,y,z) times number of positions to read
        for (int j = 0; j < numPositions; j++) {

          float x = (float) bReader.ReadDouble();
          float y = (float) bReader.ReadDouble();
          float z = (float) bReader.ReadDouble();

          //for some reason y needs to be inverted to make it fit properly in unity
          y *= -1;

          //and some rotations are needed to make it fit to the terrain
          Transform dummyTransform = new GameObject().transform;
          dummyTransform.position = new Vector3(x, y, z);
          dummyTransform.RotateAround(new Vector3(0f, 0f, 0f), new Vector3(1f, 0f, 0f), -90f);
          dummyTransform.RotateAround(new Vector3(0f, 0f, 0f), new Vector3(0f, 1f, 0f), 180f);

          //finally pass the node positions to pipe class
          pipeNodes.Add(dummyTransform.position);
          Destroy(dummyTransform.gameObject);
        }

        //remove or add points without interfering with semantic structure
        pipe.addAllNodes(pipeNodes);

        // and add the pipe to the sewer system class
        SewerSystem.addPipe(pipe);

      }
    }

  }

  // this method will remove unnecessary nodes 
  // and add those wich are needed for the cutaways of the pipes
  private List < Vector3 > refineNodes(List < Vector3 > nodes) {

    if (nodes.Count > 2) {
      int prev = 0;
      int current = 1;
      int next = 2;

      //remove nodes
      while (next < nodes.Count && prev < current && current < next) {
        Vector3 directionBig = nodes[next] - nodes[prev];
        Vector3 directionSmall = nodes[current] - nodes[prev];
        float dot = Vector3.Dot(directionBig.normalized, directionSmall.normalized);

        if (dot > 0.999f || dot < 0.6f) {
          nodes.RemoveAt(current);
          removedNodes++;
        } else {
          next++;
          current++;
          prev++;
        }
      }

    }

    //add nodes for cut
    if ((nodes[nodes.Count - 1] - nodes[0]).magnitude > 7) {

      if (nodes.Count == 2) {
        Vector3 direction = nodes[1] - nodes[0];

        Vector3 tempLast = nodes[1]; //save last
        nodes.Insert(1, nodes[0] + direction.normalized * direction.magnitude / 4);
        nodes.Insert(2, tempLast - direction.normalized * direction.magnitude / 4);

        addedNodes += 2;

      } else if (nodes.Count == 3) {
        Vector3 firstDirection = (nodes[1] - nodes[0]);
        Vector3 secondDirection = (nodes[2] - nodes[1]);

        nodes.Insert(1, nodes[0] + firstDirection.normalized * firstDirection.magnitude / 2);
        nodes.Insert(3, nodes[2] + secondDirection.normalized * secondDirection.magnitude / 2);

      } else if (nodes.Count >= 4) {
        Vector3 firstDirection = (nodes[1] - nodes[0]);
        if (firstDirection.magnitude > 3) {
          nodes.Insert(1, nodes[0] + firstDirection.normalized * firstDirection.magnitude / 2);
        }

        Vector3 secondDirection = (nodes[nodes.Count - 1] - nodes[nodes.Count - 2]);
        if (secondDirection.magnitude > 3) {
          nodes.Insert(nodes.Count - 1, nodes[nodes.Count - 1] - secondDirection.normalized * secondDirection.magnitude / 2);
        }
      }
    }

    pipeNodesCounter += nodes.Count;

    return nodes;
  }

  void parseNodeIndicesPerLink(string path, int numLines) {

    // Read the data.
    using(BinaryReader bReader = new BinaryReader(File.Open(path, FileMode.Open))) {

      //Parse for each single Line
      // add startShaft and endShaft to its Pipes and add Pipes connected to a shaft to this shaft as well
      for (int i = 0; i < numLines; i++) {

        //indices are in the same order as the pipes where parsed with (hopefully), same for shafts
        Pipe pipe = SewerSystem.getPipebyIndex(i);

        //index of the first shaft the pipe is connected to
        int startIndex = bReader.ReadInt32();
        Shaft shaft = SewerSystem.getShaftbyIndex(startIndex);
        pipe.setStartShaft(shaft);
        shaft.addConnectedPipe(pipe);

        //index of the second shaft the pipe is connected to
        int endIndex = bReader.ReadInt32();
        shaft = SewerSystem.getShaftbyIndex(endIndex);
        pipe.setEndShaft(shaft);
        shaft.addConnectedPipe(pipe);
      }
    }
  }

  void parselinkRadii(string path, int numLines) {
    // Read the data.
    using(BinaryReader bReader = new BinaryReader(File.Open(path, FileMode.Open))) {
      // parse for each single for a pipes radius
      for (int i = 0; i < numLines; i++) {
        Pipe pipe = SewerSystem.getPipebyIndex(i);
        //indices are in the same order as the pipes where parsed with (hopefully)
        pipe.radius = bReader.ReadSingle();
        pipe.broadcastRadiusToShafts();

      }
    }
  }

  void OnDestroy() {
    if (pipeNodesVec3ReadBuffer != null) pipeNodesVec3ReadBuffer.Release();
    pipeNodesVec3ReadBuffer = null;

    if (RWpipeNodesUVBuffer != null) RWpipeNodesUVBuffer.Release();
    RWpipeNodesUVBuffer = null;
  }
}
SewerSystem.cs
/* Copyright (C) Manuel Matusich
* Unauthorized copying of this file, via any medium is strictly prohibited
* Written by Manuel Matusich , Juli 2017
*/

using System.Collections.Generic;
using UnityEngine;

public static class SewerSystem {

  private static List < Pipe > pipes = new List < Pipe > ();
  private static List < Shaft > shafts = new List < Shaft > ();
  private static int numberOfPipeNodes = 0;
  private static int minNumberOfNodesPerPipe = int.MaxValue;
  public static int maxSimulationTimeStep = 7200;
  public static int simulationSteps = 721;

  public static void setActive(bool active) {
    foreach(Pipe pipe in pipes) {
      pipe.setActive(active);
    }

    foreach(Shaft shaft in shafts) {
      shaft.setActive(active);
    }
  }

  public static int getNumberOfPipeNodes() {
    if (numberOfPipeNodes == 0) {
      foreach(Pipe pipe in pipes) {
        numberOfPipeNodes += pipe.getNodeCount();

        if (pipe.getNodeCount() < minNumberOfNodesPerPipe) minNumberOfNodesPerPipe = pipe.getNodeCount();
      }

    }
    return numberOfPipeNodes;
  }

  public static void addShaft(Shaft shaft) {
    shafts.Add(shaft);
  }

  public static void addPipe(Pipe pipe) {
    pipes.Add(pipe);
  }

  public static List < Pipe > getPipes() {
    return pipes;
  }

  public static List < Shaft > getShafts() {
    return shafts;
  }

  public static Shaft getShaftbyIndex(int index) {
    return shafts[index];
  }

  public static Pipe getPipebyIndex(int index) {
    return pipes[index];
  }

  public static Pipe getPipeByID(int id) {
    foreach(Pipe pipe in pipes) {
      if (pipe.id == id) {
        return pipe;
      }
    }

    return null;
  }

  //sort pipes by height, to ensure the are drawn in this order by geometryshader
  private static void sortPipesByHeight() {
    pipes.Sort(delegate(Pipe a, Pipe b) {
      return (b.position.y).CompareTo(a.position.y);
    });
  }

  public static void intializeAfterParsing() {
    foreach(Pipe pipe in pipes) {
      pipe.initializAfterParsing();
    }

    sortPipesByHeight();

    foreach(Shaft shaft in shafts) {
      shaft.createShaft();
      shaft.createWater();
    }

  }

}
Pipe.cs
/* Copyright (C) Manuel Matusich
* Unauthorized copying of this file, via any medium is strictly prohibited
* Written by Manuel Matusich , Juli 2017
*/

using UnityEngine;
using System.Collections.Generic;
using System.Collections;

public class Pipe {
  private GameObject pipeGameObject;

  private List < Vector3 > pipeNodes;
  private Vector2[] UVs = null;

  private Shaft startShaft;
  private Shaft endShaft;

  public Vector3 position = Vector3.one;
  public float radius = 0;
  public int id;

  public float[] depthTimeline;
  public Color[] depthColorTimeline;
  public Color[] dischargeColorTimeline;

  public Pipe(int id, GameObject parent) {
    this.id = id;
    pipeNodes = new List < Vector3 > ();

    depthTimeline = new float[SewerSystem.simulationSteps];
    depthColorTimeline = new Color[SewerSystem.simulationSteps];
    dischargeColorTimeline = new Color[SewerSystem.simulationSteps];

    pipeGameObject = new GameObject();
    pipeGameObject.name = "Pipe ID: " + this.id;
    pipeGameObject.transform.SetParent(parent.transform);
    pipeGameObject.tag = "pipe";

  }

  public void setDepthForTimestep(float depth, int timestep) {
    depthTimeline[timestep] = depth;
  }

  public void setDepthColorForTimestep(Color color, int timestep) {
    depthColorTimeline[timestep] = color;
  }

  public void setDischargeColorForTimestep(Color color, int timestep) {
    dischargeColorTimeline[timestep] = color;
  }

  public void broadcastRadiusToShafts() {
    startShaft.increaseWidth(radius);
    endShaft.increaseWidth(radius);
  }

  private void initiateUVs() {
    if (UVs == null) UVs = new Vector2[pipeNodes.Count];
  }

  public void setUV(Vector2 uv, int nodeID) {
    initiateUVs();
    this.UVs[nodeID] = uv;
  }

  public Vector2[] getUVs() {
    return this.UVs;
  }

  public void setActive(bool active) {
    pipeGameObject.SetActive(active);
  }

  public void initializAfterParsing() {
    swapShaftsIfNeeded();
    position.x = (pipeNodes[0].x + pipeNodes[pipeNodes.Count - 1].x) / 2;
    position.y = (pipeNodes[0].y + pipeNodes[pipeNodes.Count - 1].y) / 2;
    position.z = (pipeNodes[0].z + pipeNodes[pipeNodes.Count - 1].z) / 2;
  }

  public void setStartShaft(Shaft shaft) {
    startShaft = shaft;
  }

  public void setEndShaft(Shaft shaft) {
    endShaft = shaft;
  }

  public float getHeightOfPipeEndToConnectedShaft(Shaft shaft) {
    if (shaft.getID() == startShaft.getID()) {
      return pipeNodes[0].y;
    } else if (shaft.getID() == endShaft.getID()) {
      return pipeNodes[pipeNodes.Count - 1].y;
    } else {
      Debug.Log("shaft error: given shaft is neither endshaft or startshaft");
      return - 1000000;
    }
  }

  //Make shure that start shaft is closer to index 0 of pipeNodes
  public void swapShaftsIfNeeded() {
    float distStart = Vector3.Distance(startShaft.getPos(), pipeNodes[0]);
    float distEnd = Vector3.Distance(endShaft.getPos(), pipeNodes[0]);
    if (distStart > distEnd) {
      Shaft temp = startShaft;
      startShaft = endShaft;
      endShaft = temp;
    }
  }

  public List < Vector3 > getNodes() {
    return pipeNodes;
  }

  public void addNode(Vector3 node) {
    pipeNodes.Add(node);
  }

  public void addAllNodes(List < Vector3 > nodes) {
    pipeNodes = nodes;
  }

  public int getNodeCount() {
    return pipeNodes.Count;
  }

}
Shaft.cs
/* Copyright (C) Manuel Matusich
* Unauthorized copying of this file, via any medium is strictly prohibited
* Written by Manuel Matusich , Juli 2017
*/

using UnityEngine;
using System.Collections.Generic;

public class Shaft {

  static private float wallThickness = 0.1f;
  static private float waterOffset = 0.01f;

  private GameObject shaft;
  private GameObject water;
  private GameObject cylinder;
  private GameObject cylinderInside;
  private GameObject manhole;
  private GameObject pipeCancel;
  private Material shaftMaterial;
  private Material manholeMaterial;
  private Material waterMaterial;
  private Material shaftInsideMaterial;
  private Material pipeCancelMaterial;

  private Vector3 upperPoint;
  private Vector3 lowerPoint;

  private float[] depthTimeline;
  private Color[] depthColorTimeline;
  private Color[] dischargeColorTimeline;

  private List < Pipe > connectedPipes;
  private int id;
  private float width;

  private float waterFillPercent;

  public Shaft(int id, GameObject parent, Material shaftMaterial, Material shaftInsideMaterial, Material manholeMaterial, Material waterMaterial, Material pipeCancelMaterial) {
    this.id = id;
    depthTimeline = new float[SewerSystem.simulationSteps];
    depthColorTimeline = new Color[SewerSystem.simulationSteps];
    dischargeColorTimeline = new Color[SewerSystem.simulationSteps];

    shaft = new GameObject();
    shaft.transform.SetParent(parent.transform);
    shaft.name = "Shaft ID: " + this.id;

    this.shaftMaterial = shaftMaterial;
    this.manholeMaterial = manholeMaterial;
    this.waterMaterial = waterMaterial;
    this.shaftInsideMaterial = shaftInsideMaterial;
    this.pipeCancelMaterial = pipeCancelMaterial;

    this.width = 0.7f;

    upperPoint = new Vector3();
    lowerPoint = new Vector3();
    connectedPipes = new List < Pipe > ();

    cylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
    cylinder.transform.SetParent(shaft.transform);
    cylinder.name = "Shaft Cylinder";
    cylinder.tag = "shaft";

    pipeCancel = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
    pipeCancel.transform.SetParent(shaft.transform);
    pipeCancel.name = "Shaft Pipe Cancel";
    pipeCancel.tag = "shaft";

    cylinderInside = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
    cylinderInside.transform.SetParent(shaft.transform);
    cylinderInside.name = "Shaft Cylinder Inside";
    cylinderInside.tag = "shaft";

    water = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
    water.transform.SetParent(shaft.transform);
    water.name = "Water";
    waterFillPercent = Random.Range(0.0f, 1.0f);

    manhole = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
    manhole.transform.SetParent(cylinder.transform);
    manhole.name = "Manhole";
  }

  public int getID() {
    return this.id;
  }

  public void setDepthForTimestep(float depth, int timestep) {
    depthTimeline[timestep] = depth;
  }

  public void setDepthColorForTimestep(Color color, int timestep) {
    depthColorTimeline[timestep] = color;
  }

  public void setDischargeColorForTimestep(Color color, int timestep) {
    dischargeColorTimeline[timestep] = color;
  }

  public void increaseWidth(float radius) {
    if (radius * 2.5f > width) width = radius * 2.5f;
  }

  // called at the end of start when all is set up
  public void createShaft() {
    Vector3 offset = lowerPoint - upperPoint;
    Vector3 scale = new Vector3(width, offset.magnitude / 2.0f, width);
    cylinder.transform.position = upperPoint + new Vector3(offset.x / 2.0f, offset.y / 2.0f, offset.z / 2.0f);
    cylinder.GetComponent < MeshRenderer > ().sharedMaterial = shaftMaterial;
    cylinder.transform.up = offset;
    cylinder.transform.localScale = scale;
    cylinder.isStatic = true;

    pipeCancel.transform.position = upperPoint + new Vector3(offset.x / 2.0f, offset.y / 2.0f, offset.z / 2.0f);
    pipeCancel.GetComponent < MeshRenderer > ().sharedMaterial = pipeCancelMaterial;
    pipeCancel.transform.up = offset;
    pipeCancel.transform.localScale = scale;
    pipeCancel.isStatic = true;

    manhole.transform.position = upperPoint + new Vector3(0, 0.05f, 0);
    manhole.GetComponent < MeshRenderer > ().sharedMaterial = manholeMaterial;
    manhole.transform.localScale = new Vector3(0.9f, 0.02f, 0.9f);
    manhole.isStatic = true;

    float widthModifier = 1 - wallThickness;
    Vector3 upperPointInner = upperPoint + Vector3.down * waterOffset / 2;
    Vector3 lowerPointInner = lowerPoint + Vector3.up * wallThickness / 2;
    offset = lowerPointInner - upperPointInner;
    scale = new Vector3(width * widthModifier, offset.magnitude / 2.0f, width * widthModifier);
    cylinderInside.transform.position = upperPointInner + new Vector3(offset.x / 2.0f, offset.y / 2.0f, offset.z / 2.0f);
    cylinderInside.GetComponent < MeshRenderer > ().sharedMaterial = shaftInsideMaterial;
    cylinderInside.transform.up = offset;
    cylinderInside.transform.localScale = scale;
    cylinderInside.isStatic = true;
  }

  public void createWater() {
    Vector3 waterMax = upperPoint + Vector3.down * (wallThickness / 2 + waterOffset);
    Vector3 waterMin = lowerPoint + Vector3.up * (wallThickness / 2 + waterOffset);
    float widthModifier = 1 - wallThickness - waterOffset * 10;

    //float maxDist = Vector3.Distance(waterMin, waterMax);

    Vector3 offset = (waterMin - waterMax);
    Vector3 scale = new Vector3(width * widthModifier, offset.magnitude / 2.0f, width * widthModifier);
    water.transform.position = waterMax + new Vector3(offset.x / 2.0f, offset.y / 2.0f, offset.z / 2.0f);
    //GameObject cylinder = (GameObject)UnityEngine.Object.Instantiate(cylinderPrefab, position, Quaternion.identity);
    water.GetComponent < MeshRenderer > ().sharedMaterial = waterMaterial;
    water.transform.up = offset;
    water.transform.localScale = scale;
  }

  public void setTimeStep(int timestep) {
    float waterHeight = depthTimeline[timestep];
    float minHeight = 100000000.0f;

    //move this to initialzation
    foreach(Pipe pipe in connectedPipes) {
      float height = pipe.getHeightOfPipeEndToConnectedShaft(this);
      if (height < minHeight) minHeight = height;
    }
    if (minHeight < lowerPoint.y) minHeight = lowerPoint.y;
    waterFillPercent = ((minHeight - lowerPoint.y) + waterHeight) / Vector3.Distance(upperPoint, lowerPoint);
    if (waterFillPercent > 1) waterFillPercent = 1.0f;
    waterMaterial.SetColor("_Color", depthColorTimeline[timestep]);

    rescaleWater();
  }

  public void rescaleWater() {
    Vector3 waterMax = upperPoint + Vector3.down * (wallThickness / 2 + waterOffset);
    Vector3 waterMin = lowerPoint + Vector3.up * (wallThickness / 2 + waterOffset);
    float widthModifier = 1 - wallThickness - waterOffset * 10;

    float maxDist = Vector3.Distance(waterMin, waterMax);
    Vector3 waterLevel = waterMin + Vector3.up * maxDist * waterFillPercent;

    Vector3 offset = (waterMin - waterLevel);
    Vector3 scale = new Vector3(width * widthModifier, offset.magnitude / 2.0f, width * widthModifier);
    water.transform.position = waterLevel + new Vector3(offset.x / 2.0f, offset.y / 2.0f, offset.z / 2.0f);
    water.transform.up = offset;
    water.transform.localScale = scale;
  }

  public void setActive(bool active) {
    shaft.SetActive(active);
  }

  public void addConnectedPipe(Pipe pipe) {
    if (!connectedPipes.Contains(pipe)) connectedPipes.Add(pipe);
  }

  public void setLower(Vector3 lower) {
    lowerPoint = lower + Vector3.down;
  }

  //setUpper must be called after setLower for each shaft respectively
  public void setUpper(Vector3 upper) {
    if (upper.y < lowerPoint.y && lowerPoint.y != 0) {
      upperPoint = lowerPoint;
      lowerPoint = upper;
    } else {
      upperPoint = upper;
    }
  }

  public Vector3 getPos() {
    Vector3 position = new Vector3(0f, 0f, 0f);
    position.x = (lowerPoint.x + upperPoint.x) / 2;
    position.y = (lowerPoint.y + upperPoint.y) / 2;
    position.z = (lowerPoint.z + upperPoint.z) / 2;
    return position;
  }

}