Generating Pipes

Made by Patrick Duijster 500775158

  • Introduction & idea
  • Start Research
  • Bezier curves and Extenders
    • Bezier curve
    • Blender
  • Generating Positions
  • Generating Vertices & Mesh Rotation
    • The Mesh
  • Pipe Preview: Shading and the bezier curve
    • Shader
  • Finishing touch: Extenders, bezier curve expansion and the Gimbal Lock
    • Bezier curve expansion
    • Gimbal Lock
  • Conclusion
  • Future Goals:
    • General
    • Visuals
  • Sources

Introduction & idea

Like most gamers I've been playing a lot of Deep rock Galactic a game where a team of miners go into unexplored caves to dig and mine its materials for their own uses. There are different kinds of mission types in this game and two of them use cylindric objects as a gameplay mechanic. One where you have to follow cables to get to the main objective of the mission and the one that we'll be focusing on which is pumping liquid minerals. In this mission type your main goal is to connect pipes from the pump to a refinery and to maintain your pipes while pumping said liquid minerals. This mechanic of generating pipes is what I'm going to recreate.
This blog is about how I went about creating my current final product. And the structure is to provide a comprehensive storyboard for it with every sub category going in more detail of the code.

Start Research

My First step was to figure out how I was gonna go about this so I started out by just making a small video in the Deep Rock Galactic mission type and just testing out the pipe system and from there figuring out how I could simulate that.(IFlippie, 2022) From there I discovered my first step was to implement bezier curves to find out if that's what I needed and it seemed to be an important part for later.

Bezier Curves and Extenders

Even though I had made bezier curves, there were bigger issues I had to work out first before I could actually implement it with a mesh. The issue being that having the mesh generated the main priority was and needing to do more research on how it would work together. The other point that came up early was deciding whether I was going to generate every extending piece between pipes or not. This decision was decided pretty quick when I made a decent model in 20 minutes and it fit my requirements for what I wanted. And now the next step is mesh generation research.

Bezier curve

The bezier curve in my product is a Quadratic bezier Curve which means that it's a linear interpolation between three points. Therefore the way of generating points in this Quadratic bezier Curve is by interpolating three times:

Vector3 p0 = Vector3.Lerp(startPos.transform.position, anchorPos.transform.position, i*(1f/stepSize));
Vector3 p1 = Vector3.Lerp(anchorPos.transform.position, endPos.transform.position, i * (1f / stepSize));
Vector3 p2 = Vector3.Lerp(p0, p1, i * (1f / stepSize));

Blender

Blender is not very intuitive to use for beginners and most game developers should rather use Probuilder or RealTimeCSG Unity plugins for any simple geometry. But on the off chance like me where you have to use it, the best tips are to purely use the modeling tab and use the snap feature.
In my example I used the cut tool and manually moved edges to get my result.

Generating Positions

With mesh generation we're looking at a few variables we want which are a starting position, a ending position and points between the beginning and end. The beginning point was easy enough by giving the extender a tag and when it's hit we assign the starting position based on the extenders center. The ending position is based on a raycast from your screen towards the direction of your mouse. Now that we have these 2 points we can calculate the direction and distance between these two points and stepwise create more points between the two. With this we've setup the positions that we'll use to create our mesh.

        float dist = Vector3.Distance(StartPoint.transform.position, hit.point);
        float stepDist = dist / stepSize;
        Vector3 dir = (hit.point - StartPoint.transform.position).normalized;

        GameObject startPos = new GameObject();
        startPos.transform.position = StartPoint.transform.position + StartPoint.transform.forward;
        startPos.transform.right = dir * -1;
        pipePoints.Add(startPos);

        for (int  i = 1;  i < stepSize;  i++)
        {
            GameObject pos = new GameObject();
            pos.transform.position = StartPoint.transform.position + dir * (stepDist * i);
            pos.transform.right = dir* - 1;
            pipePoints.Add(pos);
            GameObject dPos = new GameObject();
            dPos.transform.position = StartPoint.transform.position + dir * (stepDist * i);
            dPos.transform.forward = dir * -1;
            dirPoints.Add(dPos);
        }

        GameObject endPos = new GameObject();
        endPos.transform.position = hit.point;
        endPos.transform.right = dir * -1;
        pipePoints.Add(endPos);

Generating Vertices & Mesh Rotation

With our established points we can start generating vertices around them, we'll do this by creating circles based on the amount of vertices that we want but "oh no" how do we rotate these vertices correctly?
The GameObject that holds all of our positions is at 0,0,0 and so rotating that would rotate everything which isnt what we want so, we had to turn every position into a gameobject just so we could give them a rotation. With this we'll be able to rotate them into the correct direction and we can move on to the triangles, because of the way we've instantiated our vertices except for the final six in each ring we can easily calculate the triangles.
With this we now have a straight pipe that we can draw between two points and has the correct rotation.

The mesh

So let's go through the mesh code.
We start by creating the circumference of a circle divided by our vertices per ring.
We use Y and Z to determine the vertices positions while also deciding the radius of the ring.
We rotate the vertices using each pipe positions rotation.
And for the triangles we simply take vertices from the next ring in steps of the vertices per ring to form our triangles. For the last triangles per ring we have to use the first vertices of the rings so that's what the if statement is for.

float vStep = (2f * Mathf.PI) / verticesPerPoint;
pipeVertices = new Vector3[verticesPerPoint * layers];
        //print(pipeVertices.Length);
        for (int k = 0, j = 0; j < layers; j++)
        {
            for (int o = 0; o < verticesPerPoint; o++, k++)
            {
                Vector3 p;
                float r = pipeRadius * Mathf.Cos(o * vStep);
                p.x = pipePoints[j].transform.position.x;
                p.y = pipePoints[j].transform.position.y + (r * Mathf.Cos(0f));
                p.z = pipePoints[j].transform.position.z + (pipeRadius * Mathf.Sin(o * vStep));
                var vPos = p;
                pipeVertices[k] = vPos;

                Quaternion q = pipePoints[j].transform.rotation;
                pipeVertices[k] = q * (pipeVertices[k] - pipePoints[j].transform.position) + pipePoints[j].transform.position;
            }
        }
        me.vertices = pipeVertices;

        triangles = new int[verticesPerPoint * layers * 6];
        for (int ti = 0, vi = 0, z = 0; z < layers - 1; z++, vi++)
        {
            for (int x = 0; x < verticesPerPoint; x++, ti += 6)
            {
                if (x < verticesPerPoint - 1)
                {
                    triangles[ti] = vi;
                    triangles[ti + 2] = triangles[ti + 3] = vi + 1;
                    triangles[ti + 1] = triangles[ti + 4] = vi + verticesPerPoint;
                    triangles[ti + 5] = vi + verticesPerPoint + 1;
                    vi++;
                    me.triangles = triangles;
                }
                else
                {
                    triangles[ti] = vi;
                    triangles[ti + 2] = vi - verticesPerPoint + 1;
                    triangles[ti + 1] = vi + verticesPerPoint;
                    triangles[ti + 4] = vi + verticesPerPoint;
                    triangles[ti + 3] = vi - verticesPerPoint + 1;
                    triangles[ti + 5] = vi + 1;
                    me.triangles = triangles;
                }
            }
        }
        me.triangles = triangles;

Pipe Preview: Shading and the bezier curve

Now that we have a straight pipe we can move onto implementing the bezier curve again and with our already established pipe points we'll add a separate anchor point which we will set based on the y position of the endpoint so it'll always bend upwards if the endpoint goes up. So now that we have this generated pipe we would like to always see the preview of it and we can do this by having a separate class which clears up and recreates the mesh every time we move the mouse and at the point we press mouse one we disable the preview and instantiate a pipe. To distinguish the preview pipe like how it is in Deep Rock Galactic we made a shader that is semi transparent and we can change the color on it.
Changing the color also makes it so we can do checks to see if we can place the pipe and if it's not possible we change the color accordingly.

Shader

The shader code is very straight forward because all we need to do is apply shaderlab blend to make transparency work and then in the fragment shader we apply calculate the level of transparency.

Blend SrcAlpha OneMinusSrcAlpha
fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv) * _Color;
                
                return col;
            }

And for the check we can access the color by using Unity's SetColor method.

                if (upHit.point != hit.point)
                {
                    rend.material.SetColor("_Color", red);
                }
                else { rend.material.SetColor("_Color", green); }

Finishing touch: Extenders, bezier curve expansion and the Gimbal Lock

For our last step to make the pipes fully work like we want to we add in the extending pipe pieces that we've made in Blender they're going to be our starting points which means that the position and rotation will be used for the first ring of vertices. The same goes for the end position which will also line up nicely with the extending piece, with this we can place these pipes and attach new ones to them aswell. So now with the core done we can extend it by making the bezier curve more fancy and the one change we made is deciding where the anchor point is based. By making the anchor point perpendicular to the start and end point we can curve the pipe on its X and Z which makes it so the pipe will curve to left or right from the start point.
But in the last week a new problem showed up because I was using eulerangles to update the rotation of the points which lead to the rotation locking up because of the so called Gimbal lock. So in the final days I had to research how to implement the rotation with quaternions instead and after many attempts it's been fixed and ready for full 360 rotations now.

Bezier curve expansion

After trying to get a perpendicular point through means like vector3 cross didn't work I researched other ways of getting this point and found the solution in inverseTransformPoint. Which in simple terms means getting the position relative to another position which actually is enough because we only need the X from endPosition since we get our Z from startPosition.

Vector3 relativeEnd = startPos.transform.InverseTransformPoint(endPos.transform.position);
        Vector3 relativePos = new Vector3(relativeEnd.x, 0, 0);
        Vector3 newWorldPos = startPos.transform.TransformPoint(relativePos);
        print(newWorldPos);
        anchorPos.transform.position = newWorldPos;

Gimbal Lock

The Gimbal lock is when we're rotating using euler angles and we get to a point where 2 angles are parallel what happens then is that the rotation gets "locked" and we can only rotate in 2 directions on that point. This resulted in the pipe getting locked when it was parallel to Z because I used transform.right to rotate the pipe. To fix this we have to switch from euler angles(a three dimensional rotation) to quaternions(a three dimensional matrix rotation):

//It changed from:
pos.transform.right = dir* - 1;

//to:
rotation = Quaternion.LookRotation(dir);
rotation *= Quaternion.Euler(0, 90, 0);
endPos.transform.rotation = rotation;

Conclusion

And with this we've ended on a pipe generator that has a starting and end point which are represented by extending pieces made in Blender and every extending piece can start a new Pipe. Therefore along with the bezier curve implementation and the pipe preview it is in my opinion pretty close to what my initial goal was. Also on another note writing this document took way more time then expected. So it's a little bit late but hey it's here.

Future Goals:

Like everyone else I've worked and researched on a product and feel like if there was more time these are the things I would've liked to implement aswell and how I might be able to implement them.

General:

So currently our pipe curves nicely in every direction, but in the Y direction it will always go up depending on the end point Y which works but what if a scenario shows up where the Pipe should be down instead. The reason as to why I haven't implemented it beyond time constraints is determining when this should happen and how you would calculate it correctly. The solution could be in raycasts but I didn't find any info in my research on what the priority's would be and so on in such a scenario and that's why it's on here.
Our current pipe check is a line from beginning to end and if nothing blocks this line then it's allowed.
Updating this check could be done by going through each individual point in the pipe and checking lines between them for collisions.
The pipe can curve depending on it's placement but slightly behind a surface is not possible, it would've been nice to have a pipe bend around a surface so it would still be placeable but this goes back to our previous issue of how would this be correctly resolved.

Visuals

Visuals were never a priority but if I did have time for them these would've been features I'd implement:
A scrolling texture that goes towards the end point.
A shader with flowmaps so we could simulate fluids flowing through the pipes.
And the one visual that has had some work done on it was Flat shading:
Currently the pipes have smooth lighting on them which makes it that the mesh looks nicer because the sharp edges of a six sided pipe for example are far less noticeable but what I wanted to show off a rougher looking pipe.
After doing some research I found two ways of getting flat shading: a shader to recalculate the normals or increasing the amount of vertices so every triangle uses unique vertices therefore getting rid of sharing vertices but when both tests took too long I moved on to more important features.
The vertices part for flat shading:

       float vStep = (2f * Mathf.PI) / verticesPerPoint;

        //first part represents the amount of vertices in start and finish, second part represents all the vertices inbetween
        //is this correct???             5 * 3 = 15 * 2 = 30                       5 * 7 = 35 * 5 = 175
        pipeVertices = new Vector3[(2 * (verticesPerPoint * 3)) + ((verticesPerPoint * (layers-2)) * 5)];
        print("verticesperpoint " + verticesPerPoint);
        print("layers " + layers);
        print(pipeVertices.Length);//205
        for (int k = 0, c = 0; c < 4; c++)//5
        {
            for (int j = 0; j < layers; j++)//9
            {
                for (int o = 0; o < verticesPerPoint; o++, k++)//5
                {
                    //2 * 7 * 5 = 70
                    //3 * 9 * 5 = 135
                    if ((c == 3 && layers == 0) || (c == 4 && layers == layers - 1) || (c == 3 && layers == layers - 1) || (c == 4 && layers == 0)) { break; }
                    Vector3 p;
                    float r = pipeRadius * Mathf.Cos(o * vStep);
                    p.x = pipePoints[j].transform.position.x + (r * Mathf.Sin(0f));
                    p.y = pipePoints[j].transform.position.y + (r * Mathf.Cos(0f));
                    p.z = pipePoints[j].transform.position.z + (pipeRadius * Mathf.Sin(o * vStep));
                    var vPos = p;
                    pipeVertices[k] = vPos;
                    //print(k);

                    Quaternion q = pipePoints[j].transform.rotation;
                    pipeVertices[k] = q * (pipeVertices[k] - pipePoints[j].transform.position) + pipePoints[j].transform.position;
                }
            }
        }
        
        me.vertices = pipeVertices;

Sources

Add 90 Degrees to Transform.Rotation - Unity Answers. (z.d.). https://answers.unity.com/questions/187506/add-90-degrees-to-transformrotation.html

Cg Programming/Unity/Bézier Curves - Wikibooks, open books for an open world. (z.d.). https://en.wikibooks.org/wiki/Cg_Programming/Unity/B%C3%A9zier_Curves

Curves and Splines, making your own path. (z.d.). https://catlikecoding.com/unity/tutorials/curves-and-splines/

Flick, J. (z.d.). Swirly Pipe. https://catlikecoding.com/unity/tutorials/swirly-pipe/

Flick, J. (2017, 25 oktober). Flat and Wireframe Shading. https://catlikecoding.com/unity/tutorials/advanced-rendering/flat-and-wireframe-shading/

Hextant Studios. (2021, 15 februari). Rendering Flat-Shaded / Low-Poly Style Models in Unity. https://hextantstudios.com/unity-flat-low-poly-shader/

IFlippie. (2022, 2 oktober). Deep Rock Galactic GPE Research [Video]. YouTube. https://www.youtube.com/watch?v=yDFQIyEpJfw

Procedural generated mesh in Unity. (2012, 2 augustus). Morten Nobel’s Blog. https://blog.nobel-joergensen.com/2010/12/25/procedural-generated-mesh-in-unity/

Sebastian Lague. (2018, 21 januari). [Unity] 2D Curve Editor (E01: introduction and concepts) [Video]. YouTube. https://www.youtube.com/watch?v=RF04Fi9OCPc

Technologies, U. (z.d.-a). Unity - Manual: Rotation and orientation in Unity. https://docs.unity3d.com/Manual/QuaternionAndEulerRotationsInUnity.html

Technologies, U. (z.d.-b). Unity - Scripting API: Quaternion. https://docs.unity3d.com/ScriptReference/Quaternion.html

Technologies, U. (z.d.-c). Unity - Scripting API: Transform.InverseTransformPoint. https://docs.unity3d.com/ScriptReference/Transform.InverseTransformPoint.html

Technologies, U. (z.d.-d). Unity - Scripting API: Transform.right. https://docs.unity3d.com/ScriptReference/Transform-right.html

Technologies, U. (z.d.-e). Unity - Scripting API: Transform.Rotate. https://docs.unity3d.com/ScriptReference/Transform.Rotate.html

Unity3d: Position a gameobject such that its forms a right angled triangle with other two gameobjects. (2019, 21 juli). Game Development Stack Exchange. https://gamedev.stackexchange.com/questions/173956/unity3d-position-a-gameobject-such-that-its-forms-a-right-angled-triangle-with

Wikipedia contributors. (2022, 15 augustus). Gimbal lock. Wikipedia. https://en.wikipedia.org/wiki/Gimbal_lock

Technologies, U. (z.d.-b). Unity - Scripting API: GameObject.CreatePrimitive. https://docs.unity3d.com/ScriptReference/GameObject.CreatePrimitive.html

Wikipedia contributors. (2022b, oktober 5). Circumference. Wikipedia. https://en.wikipedia.org/wiki/Circumference

Technologies, U. (z.d.-b). Unity - Manual: ShaderLab command: Blend. https://docs.unity3d.com/Manual/SL-Blend.html

Drawing Circles. (z.d.). Computing and ICT in a Nutshell. https://www.advanced-ict.info/mathematics/circles.html

Technologies, U. (z.d.-e). Unity - Scripting API: Transform.eulerAngles. https://docs.unity3d.com/ScriptReference/Transform-eulerAngles.html

Tank, V. (2018, 6 augustus). How to work with Bezier Curve in Games with Unity. Game Developer. https://www.gamedeveloper.com/business/how-to-work-with-bezier-curve-in-games-with-unity



Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts