Toqozz / blog-code

code from blog posts
https://toqoz.fyi
MIT License
90 stars 35 forks source link

Verlet Rope in Games #8

Open Toqozz opened 3 years ago

Toqozz commented 3 years ago

Discuss the blog post here; share your thoughts, improvements, or any issues you run into so that others can find them.

alicewithalex commented 3 years ago

Hello, I try to add each node has a radius. Now your code just push away particle don't take in account node radius, just it position. I implement this but have jittéring issue now and don't know how to solve it, can we communicate in any place, where I can send you code snippets of media examples of problem? Also I do in 3D :)

Toqozz commented 3 years ago

Hey, this is the best place to contact as others can benefit from the conversation as well.

You'll have to elaborate a bit more on what you've done and what's happening. When you say radius, are you referring to giving the rope a custom "thickness" value, and adjusting collisions so that they still push that rope out along the thickness? The main complication here IIRC is making sure you get the calculation right for both checking if a node is inside a collider, and for how far it should move out of the collider. This actually gets pretty tricky if you're calculating box collisions in local space, because you need to convert the rope thickness/node radius to local space as well to check the collision, and then convert the value back to get the correct world-space distance you need to move the node.

Feel free to post images and code right here.

alicewithalex commented 3 years ago

Thanks for answer here's example of Sphere Case Collision Handling: For other cases like capsule and box(cube) I do not introduce particles thickness now. If we can also talk about it later will be cool!

case ColliderType.Sphere:
                    {
    //Take sphere collider radius and multiply by max of XYZ scale of its transform
    float collisionRadius = collision.Size.x * Mathf.Max(
        Mathf.Max(collision.Transform.localScale.x, collision.Transform.localScale.y),
        collision.Transform.localScale.z);

    for (int j = 0; j < collision.CollisionAmount; j++)
    {
        Particle particle = particles[collision.CollidingNodes[j]];
        float distance = (collision.Transform.position - particle.current).magnitude;

        if (distance > collisionRadius + particleRadius)
        {
            continue;
        }

        //Direction along which we need to offset particle
        Vector3 direction = (particle.current -
           collision.Transform.position).normalized;

        //Calculate new position using direciton and sum of two radiuses
        Vector3 newPosition = collision.Transform.position +
            direction * (collisionRadius + particleRadius);

        particle.current = newPosition;
        particles[collision.CollidingNodes[j]] = particle;
    }
    break;
}

Also my fixed update loop(I know that you are using custom update loop, but for simplicity I choose this variant for now):

void FixedUpdate()
{
    UpdatePinned();

    CollectCollisionInfo();
    UpdatePositions();
    for (int i = 0; i < solveIterations; i++)
    {
        AdjustDistances();
        AdjustCollisions();
    }

    UpdateVisual();
}

Update Positions Method:

private void UpdatePositions()
{
    Vector3 velocity = Vector3.zero;

    for (int i = 0; i < pointCount; i++)
    {
        if ((i != 0 && i != pointCount - 1) || (i == 0 && !startPinned)
            || (i == pointCount - 1 && !endPinned))
        {
            velocity = particles[i].current - particles[i].previous
                    + Vector3.up * (gravity * Time.deltaTime);

            particles[i].previous = particles[i].current;
            particles[i].current += velocity * (1f - damping);
        }
    }
}

Adjust Distances Method:

private void AdjustDistances()
{
    float distance = 0f;
    float difference = 0f;

    Vector3 direction = Vector3.zero;

    for (int i = 0; i < pointCount - 1; i++)
    {
        distance = (particles[i].current - particles[i + 1].current).magnitude;
        difference = constraint - distance;

        direction = (particles[i + 1].current - particles[i].current).normalized;

        if (i == 0)
        {
            if (startPinned)
            {
                particles[i + 1].current += direction * (difference);
            }
            else
            {
                difference *= 0.5f;
                particles[i].current -= direction * (difference);
                particles[i + 1].current += direction * (difference);
            }
        }
        else
        {
            if (i + 1 == pointCount - 1)
            {
                difference *= 0.5f;
                particles[i].current -= direction * (difference);
                if (!endPinned)
                    particles[i + 1].current += direction * (difference);
            }
            else
            {
                difference *= 0.5f;
                particles[i].current -= direction * (difference);
                particles[i + 1].current += direction * (difference);
            }
        }

    }
}

movie_002

alicewithalex commented 3 years ago

Also what UTC +... you have?) I mean, that when we can be online at same time?) I have UTC+3

Toqozz commented 3 years ago

I can't see anything obviously wrong with your implementation. You have the right idea with the particleRadius in AdjustCollisions. I assume that everything works correctly without particleRadius in the mix, so the other code shouldn't be important; you should be able to accomplish adding rope thickness just by changing the AdjustCollisions function.

Since you don't seem to be using the jobs version, you should be able to trivially debug this further by stepping through one of the problematic nodes/particles in a debugger:

Try running with if (distance >= collisionRadius + particleRadius), notice the >= instead of >. Just an idea.

Also, how zoomed in is the gif? It's unlikely, but you could be seeing floating point errors.

alicewithalex commented 3 years ago

Thanks for answer!

>= doesn't change anything :( The camera is not so zoomed, big sphere has 4 scale and particle has 0.5 scale in Unity 1 unit size.

About debuging: 1) I try debug frameCount and sphere case at adjust collision method => every frame particle have exact amount of degug messages as solve iterration amount in the inspector. 2) I dont understand fully what you mean :) 3) I think that every frame,but I cant handle how debug this((

Also, I noticed, that if I set gravity to 0, then rope still colliding,but has no jittering at all!

Also if comment Adjust Distances method and try set gravity negative and place Collidable Sphere under place of spawn rope. Then rope nice and smoothly fall down along sphere normals without jittering :/

If you need examples (video,gif os images) about what I talking I send it as soon as possible to you)

alicewithalex commented 3 years ago

Toqozz, Hello! Did you get my messages?

Toqozz commented 3 years ago

Please don't bump, I'm just busy!

I added thickness for circles to my rope sim again (https://gist.github.com/Toqozz/23726eae4b39c0f7e314a83987f9a911) -- just added line 71, and changed line 327 -- and was unable to observe the results you're getting. I tried changing iterations, node distance, step time, and a few other things and couldn't get an unstable result.

https://user-images.githubusercontent.com/6947819/125451116-c63473d0-319c-474a-9760-52431ae9fcea.mp4

My best advice here is to take the gist I've posted above and work backwards towards your solution until the problem re-appears, then you can pinpoint where the issue actually is.

alicewithalex commented 3 years ago

Thanks, I will try do in in this way and write back then found a place where I will get an error

alicewithalex commented 3 years ago

Toqozz, I finally figured out what's wrong with my code!

I used thickness for both cases: collection collision info and offseting nodes onto surfaces... I saw that you using 2 different variables for each case and try it myself - and it works perfect! Just thickness+0.0001f is already works nice and have no jittering!

Thanks for your help!

Also if we are here, can I ask you for check this code, which handles Cube collision. I try to convert your Box version to this:


for (int j = 0; j < collision.CollisionAmount; j++)
{
    Node particle = nodes[collision.CollidingNodes[j]];

    //Local position of particle relative to Cube
    Vector3 localPos = collision.Transform.InverseTransformPoint(particle.current);

    //Half size of Cube
    Vector3 half = collision.Size * 0.5f;
    Vector3 scale = collision.Transform.localScale;

    float diffX = half.x - Mathf.Abs(localPos.x);

    if (diffX <= 0)
    {
        continue;
    }

    float diffY = half.y - Mathf.Abs(localPos.y);

    if (diffY <= 0)
    {
        continue;
    }

    float diffZ = half.z - Mathf.Abs(localPos.z);

    if (diffZ <= 0)
    {
        continue;
    }

    float diffXZ = diffX * scale.x - diffZ * scale.z;
    float diffXY = diffX * scale.x - diffY * scale.y;
    float diffZY = diffZ * scale.z - diffY * scale.y;

    if (diffXZ < 0)
    {
        //X < Z
        if (diffXY < 0)
        {
            localPos.x = (half.x) * Mathf.Sign(localPos.x);
        }
        else
        {
            localPos.y = (half.y) * Mathf.Sign(localPos.y);
        }
    }
    else if (diffXZ >= 0)
    {
        // X >= Z
        if (diffZY < 0)
        {
            localPos.z = (half.z) * Mathf.Sign(localPos.z);
        }
        else
        {
            localPos.y = (half.y) * Mathf.Sign(localPos.y);
        }
    }
    else if (diffXY < 0)
    {
        //X < Y
        if (diffXZ < 0)
        {
            localPos.x = (half.x) * Mathf.Sign(localPos.x);
        }
        else
        {
            localPos.z = (half.z) * Mathf.Sign(localPos.z);
        }
    }
    else
    {
        //X >= Y
        if (diffZY < 0)
        {
            localPos.z = (half.z) * Mathf.Sign(localPos.z);
        }
        else
        {
            localPos.y = (half.y) * Mathf.Sign(localPos.y);
        }
    }

    particle.current = collision.Transform.TransformPoint(localPos);

    nodes[collision.CollidingNodes[j]] = particle;
}
break;

And one more question, do you know how to prevent rope from tunneling? I have troubles now with very thick planes/cubes. I saw method named Speculative Contacts, link http://vodacek.zvb.cz/archiv/286.html

Toqozz commented 3 years ago

Great work!

Regarding collision thickness + box collisions, I never got it fully working, but I have something close: https://gist.github.com/Toqozz/ea6f4414a1298a8b309e5ddb94317d60

It has an issue with nodes "sticking" to the surface which seems to be related to float errors (probably because of transforming to box local space and back?), but hopefully it is helpful regardless. I think it could be made to work by just rotating and translating the point, to box local space, so you don't have to deal with the scale (and so, avoiding a matrix multiply).

Here's a quick draft on how that might look (completely untested):

// The collision data is different in blog and your post, I know.  This is just easiest to write.
if (node.collisionBuffer[j] is BoxCollider) {
    BoxCollider col = node.collisionBuffer[j] as BoxCollider;

    // Translate to box origin.
    Vector3 pos = node.position - col.transform.position;
    // Rotate pos by the same rotation as box.
    // Note this actually seems to be doing `rot * pos * rot^-1` internally by unity:  https://forum.unity.com/threads/rotate-vector-by-quaternion.21687/#post-6587407
    pos = col.transform.rotation * pos;

    // `pos` is now in local space, except for the scale.
    // Not respecting hierarchy.  Use lossyScale, or if that has float issues just calculate the scale yourself by multiplying parent transforms: https://gabormakesgames.com/blog_transforms_transforms.html
    Vector3 scale = col.transform.localScale;

    // Much faster ways to process and early out here, see blog post.
    Vector3 half = scale * 0.5f;
    if pos.x > half.x + skinWidth {
        pos.x = half.x + skinWidth;
    } else if pos.x < -half.x - skinWidth {
        pos.x = -half.x - skinWidth
    }
    if pos.y > half.y + skinWidth {
        pos.y = half.y + skinWidth;
    } else if pos.y < -half.y - skinWidth {
        pos.y = -half.y - skinWidth;
    }
    if pos.z > half.z + skinWidth {
        pos.z = half.z + skinWidth;
    } else if pos.z < -half.z - skinWidth {
        pos.z = -half.z - skinWidth;
    }

    // Transform back.
    pos = col.transform.rotation.inverse() * pos;
    pos += col.transform.position;
    node.position = pos;
}

Hopefully that's enough vague information to translate it into your solution.

Regarding tunneling, there's a section at the very end of the blog post talking about it, and I've also written a couple comments about it that still sum up my thoughts:

Ideally you raycast between previous positions, but this is slow if done naively and I haven't explored it much. I found that my issues were mostly resolved by simply lowering the maxSimMove parameter, which keeps nodes from moving too quickly between steps (nodes normally don't move much each step unless something weird is happening).

Tunneling is always a complicated issue. Your best quick and dirty solution is probably to increment between the current and previous node position in AdjustCollisions(), checking collisions until you find one. You could even program this so it increments a certain minimum distance -- this should actually be a pretty good solution.

alicewithalex commented 3 years ago

Thanks for your answer! I already somehow make this solution and it's works, but I definitely look into your solution,cause I have to many if statements.... I just convert nodeRadius (thickness) to Cube local space and use it in calculations. Also were a problem with negative values of it. Simply I take absolute value of this :)

for (int j = 0; j < collision.CollisionAmount; j++)
        {
            Node particle = nodes[collision.CollidingNodes[j]];

            //Local position of particle relative to Cube
            Vector3 localPos = collision.Transform.InverseTransformPoint(particle.current);

            //Half size of Cube
            Vector3 half = collision.Size * 0.5f;
            Vector3 nodeLocalSize = collision.Transform.InverseTransformVector(Vector3.one * nodeRadius).Absolute();
            Vector3 scale = collision.Transform.lossyScale;

            float diffX = half.x - Mathf.Abs(localPos.x) + nodeLocalSize.x;

            if (diffX <= 0)
            {
                continue;
            }

            float diffY = half.y - Mathf.Abs(localPos.y) + nodeLocalSize.y;

            if (diffY <= 0)
            {
                continue;
            }

            float diffZ = half.z - Mathf.Abs(localPos.z) + nodeLocalSize.z;

            if (diffZ <= 0)
            {
                continue;
            }

            float diffXZ = diffX * scale.x - diffZ * scale.z;
            float diffXY = diffX * scale.x - diffY * scale.y;
            float diffZY = diffZ * scale.z - diffY * scale.y;

            if (diffXZ < 0)
            {
                //X < Z
                if (diffXY < 0)
                {
                    localPos.x = (half.x+nodeLocalSize.x) * Mathf.Sign(localPos.x);
                }
                else
                {
                    localPos.y = (half.y + nodeLocalSize.y) * Mathf.Sign(localPos.y);
                }
            }
            else if (diffXZ >= 0)
            {
                // X >= Z
                if (diffZY < 0)
                {
                    localPos.z = (half.z + nodeLocalSize.z) * Mathf.Sign(localPos.z);
                }
                else
                {
                    localPos.y = (half.y + nodeLocalSize.y) * Mathf.Sign(localPos.y);
                }
            }
            else if (diffXY < 0)
            {
                //X < Y
                if (diffXZ < 0)
                {
                    localPos.x = (half.x + nodeLocalSize.x) * Mathf.Sign(localPos.x);
                }
                else
                {
                    localPos.z = (half.z + nodeLocalSize.z) * Mathf.Sign(localPos.z);
                }
            }
            else
            {
                //X >= Y
                if (diffZY < 0)
                {
                    localPos.z = (half.z + nodeLocalSize.z) * Mathf.Sign(localPos.z);
                }
                else
                {
                    localPos.y = (half.y + nodeLocalSize.y) * Mathf.Sign(localPos.y);
                }
            }

            particle.current = collision.Transform.TransformPoint(localPos);
            nodes[collision.CollidingNodes[j]] = particle;
alicewithalex commented 3 years ago

May I ask more question, I now try figured how to make mesh, and I need some sort of orientation for each particle and don't know source/paper to look at and learn from... You can look at image below, I manually rotate particle to desire rotation that I try to implement in Verlet Integration Screenshot_1 Screenshot_2

. I can calculate direction between particles, but there no other direciton vectors to calculate normals of rope or tangent. I mean now I have only forwarddirection and to calculate normal I need to have one more direction vector to use Vector3.Cross(). I look at papers about Verlet Integration and there are no information about handling node/particles orientaiton. I saw one about using elipsoids then sphere like we do here. But They use some sort of PBD and other technique... I try to keep all simple as can in my solution :) I just want to generate rope and be able to twist maybe and add rotation constraints to simulation that work like distance contraints but with angles) Also if I want to attach character to rope, I need to introduce mass to Vertlet integration? I mean that in ideal situtation rope is designed now for swinging. Player jumps on it, attached to some particle/node and this particle/node change mass equals to self mass+player mass. Also in real life rope will take in account player velocity and player on attached don't lose velocity at all. Same situation when player want to deattach from rope he must some how get rope velocity at this moment to continue move in direction which rope moves... My brain...so much work... Just dont know from what start and where find info...

alicewithalex commented 3 years ago

Mesh isn't big problem if I know orientation of particle, Then I just can take node position and normal vector and offset vertices by normal rotated around forward vector for deltaAngle = 360/resolution

alicewithalex commented 3 years ago

I can't stop thinking about it) If I some how can make that rope can attach object to itself, then when,for simplicity, box collider collides with any other collider I need to also calculate torque force and impact force to influence on rope... OMG...

Unity joints by default have this features, but implementing it from scratch is sound crazy...

alicewithalex commented 3 years ago

Thanks for your answer! I already somehow make this solution and it's works, but I definitely look into your solution,cause I have to many if statements.... I just convert nodeRadius (thickness) to Cube local space and use it in calculations. Also were a problem with negative values of it. Simply I take absolute value of this :)

for (int j = 0; j < collision.CollisionAmount; j++)
        {
            Node particle = nodes[collision.CollidingNodes[j]];

 ...

            particle.current = collision.Transform.TransformPoint(localPos);
            nodes[collision.CollidingNodes[j]] = particle;

Also I thought that worked... But on angle like 45,135,225 and etc. its convert nodeSizeLocal not as at 0,90,180,270,360 angles. It take in account sin,cos or something, that make nodeRadius to be not (0.1,0.1,0.1) but to be (0.1,0.1,0.05). Hm... trouble..

alicewithalex commented 3 years ago

I take image of what happened, It output, that nodeRadius has converted size z component equals zero. Also debug below shows how much offset in local space we apply. 0.5 mean half of local space,but must be 0.5+some value ( in this case value is 0)

Screenshot_3

Toqozz commented 2 years ago

I'm having some trouble understanding you, but I'll try answer as best I can.

I can calculate direction between particles, but there no other direciton vectors to calculate normals of rope or tangent. I mean now I have only forward direction and to calculate normal I need to have one more direction vector to use Vector3.Cross(). I look at papers about Verlet Integration and there are no information about handling node/particles orientaiton.

Since the rope is a cylinder, you should be able to calculate normals from the forward vector by just grabbing any other perpendicular vector:

// Lazy implementation to find some other perpendicular vector: https://stackoverflow.com/a/38407105/5628824
Vector3 somePerpendicular(Vector3 input) {
    if (Mathf.Abs(input.z) < Mathf.Abs(input.x) {
        return new Vector3(input.y, -input.x, 0);
    } else {
        return new Vector3(0, -input.z, input.y);
    }
}

Vector3 fwd = (nodes[i].position - nodes[i-1].position).normalized;
Vector3 perp = somePerpendicular(fwd);
Vector3 cross = Vector3.Cross(fwd, perp);

// You now have 3 axes, but you probably only need 2 anyway since the rope is a cylinder.

By the way, if you're just using transforms you can simply do transform.forward = fwd in Unity and it will figure out the other stuff for you.

Also if I want to attach character to rope, I need to introduce mass to Vertlet integration? I mean that in ideal situtation rope is designed now for swinging. Player jumps on it, attached to some particle/node and this particle/node change mass equals to self mass+player mass. Also in real life rope will take in account player velocity and player on attached don't lose velocity at all. Same situation when player want to deattach from rope he must some how get rope velocity at this moment to continue move in direction which rope moves... My brain...so much work... Just dont know from what start and where find info...

Yes, swinging with character input is deceptively difficult with a rope like this. There are 2 main options that I've come up with: 1) Ignore the rope physics and implement proper swinging mechanics separately (https://gamedevelopment.tutsplus.com/tutorials/swinging-physics-for-player-movement-as-seen-in-spider-man-2-and-energy-hook--gamedev-8782), and you just make sure that the rope is pulled tight while you swing. This introduces a lot of edge cases and it's probably not what you want unless your game is very controlled (and really nice swinging is important). 2) Implement weight for nodes, which for verlet, means they have more control over which way the rope moves, instead of just having a 50-50 split. When a character jumps onto the rope, transfer their weight to a node, and attach them to that node. You should get some basic swinging that works decently, and you can ignore communicating back and forth between the player and the rope, which is an endless cycle. Character collisions/object collisions while attached to the rope are an issue. Something I thought of was "attaching" the player collider to the node you attach to, and then doing a two-way collision check to place the node correctly in AdjustCollisions(), but you're going well into the weeds here...

I can't stop thinking about it) If I some how can make that rope can attach object to itself, then when,for simplicity, box collider collides with any other collider I need to also calculate torque force and impact force to influence on rope... OMG... Unity joints by default have this features, but implementing it from scratch is sound crazy...

If you want full physics then yeah, this solution probably isn't for you. This rope works best when you can control the interactions.

Toqozz commented 2 years ago

Merged some multi-comments into one to make things more readable.

ljhan85 commented 2 years ago

Hi, im a beginner... I try to read and understand how to implement simple rope using verlet method..But the problem is when i try to add gravity and run the rope simulation my rope is stuck at 90 degrees in the middle and it cannot swing.. please help.. Thank you

Toqozz commented 2 years ago

Sorry for the late reply.

The simulation as per the blog post already has gravity. What part are you struggling with?

Befezdow commented 2 years ago

@Toqozz, hi! Thank you very much for code samples, your collision handling approach is very useful. But there are solutions only for two types of colliders - CircleCollider2D and BoxCollider2D. Do you have any ideas about collision handling with PolygonCollider2D? I think you can use Collider2D.OverlapPoint to detect collisions and Collider2D.ClosestPoint to determine the final position, but this will probably have a bad effect on the performance of the code.

Toqozz commented 2 years ago

I actually do have some basic polygon collision code that was working, but I never ended up optimizing it properly and I don't promise that it's correct.

It should be a great starting point though.

Please let me know if I'm missing anything there, or if you need any more information, I posted this kind of hastily.

Here are the relevant parts:

AdjustCollisions (RopeJob.cs):

...
public struct RopeJob : IJob {
...
    [ReadOnly] public NativeList<float2> polygonPaths;
    [ReadOnly] public NativeList<int> polygonPathsNumPoints;
...

    private void AdjustCollisions() {
        // Loop through each collider.
        for (int i = 0; i < numCollisions; i++) {
            NodeCollisionInfo nc = collisionInfos[i];

            // Looping inside the switch statement is marginally faster than the other way around.
            switch (nc.colliderType) {
    ...
                case ColliderType.Polygon: {
                    for (int j = 0; j < nc.numCollisions; j++) {
                        VerletNode node = nodes[collidingNodes[i * nodes.Length + j]];

                        int indexStart = 0;
                        for (int k = 0; k < polygonPathsNumPoints.Length; k++) {
                            int numPoints = polygonPathsNumPoints[k];
                            NativeSlice<float2> path = new NativeSlice<float2>(polygonPaths, indexStart, numPoints);
                            if (BurstUtility.PointInPolygon(path, node.position)) {
                                // If we're inside the polygon, then push it out to the edge.
                                node.position = BurstUtility.ClosestPointOnPolygonEdge(path, node.position);
                            }

                            indexStart += numPoints;
                        }

                        nodes[collidingNodes[i * nodes.Length + j]] = node;
                    }
                } break;
            }
        }
    }
// http://alienryderflex.com/polygon/
/// <summary>
/// Determine if a point lies within a given polygon.
/// <param name="corners">Polygon vertices / corners.</param>
/// <param name="y">The point to determine.</param>
/// <returns>True if the point lies within the polygon, false if not.</returns>
public static bool PointInPolygon(NativeSlice<float2> corners, float2 point) {
    int j = corners.Length-1;
    bool oddNodes = false;

    for (int i = 0; i < corners.Length; i++) {
        if (corners[i].y < point.y && corners[j].y >= point.y ||
            corners[j].y < point.y && corners[i].y >= point.y &&
       (corners[i].x <= point.x || corners[j].x <= point.x)) {

            oddNodes ^= (corners[i].x + (point.y - corners[i].y) / (corners[j].y - corners[i].y) * (corners[j].x - corners[i].x) < point.x);
        }

        j = i;
    }
    return oddNodes;
}

/// <summary>
/// Push `point` to the closest edge of a polygon.
/// </summary>
/// <param name="corners">Polygon vertices / corners.</param>
/// <param name="point">The point to push against the polygon edge.</param>
/// <returns>`point` constrained to the edge of the polygon.</returns>
public static float2 ClosestPointOnPolygonEdge(NativeSlice<float2> corners, float2 point) {
    // Find closest edge.
    float2 projectionPoint = float2.zero;
    float closestDistance = float.PositiveInfinity;
    for (int i = 0; i < corners.Length; i++) {
    float2 p1 = corners[i];
    float2 p2;
    // The last point needs to wrap around to the first point.
    if (i == corners.Length - 1) {
        p2 = corners[0];
    } else {
        p2 = corners[i + 1];
    }

    // Find the minimum distance projection of our node onto the polygon edge.
    float2 projection = ClosestPointOnLine(p1, p2, point);
    float distance = math.distance(projection, point);
    if (distance < closestDistance) {
        closestDistance = distance;
        projectionPoint = projection;
    }
    }

    return projectionPoint;
}

/// <summary>
/// Given a point and a line, find the closest point which lies on the line segment in relation to the original point.
/// </summary>
/// <param name="v1">Line segment vertex 1.</param>
/// <param name="v2">Line segment vertex 2.</param>
/// <param name="p">The point.</param>
/// <returns>The closest point on the line.</returns>
public static float2 ClosestPointOnLine(float2 v1, float2 v2, float2 p) {
    float l2 = math.distancesq(v1, v2);
    // v1 == v2 case.
    if (l2 == 0f) {
        return v1; //math.distance(p, v1);
    }

    // Consider the line extending the segment, parameterized as v1 + t (v2 - v1).
    // We find projection of point p onto the line.
    // It falls where t = [(p-v1) . (v2-v1)] / |v2-v1|^2
    // We clamp t from [0,1] to handle points outside of the segment v1v2.
    float t = math.clamp(math.dot(p - v1, v2 - v1) / l2, 0f, 1f);
    float2 projection = v1 + t * (v2 - v1);  // Projection falls on the segment.
    return projection;
}

Setup (Rope.cs)

...
private NativeList<float2> polygonPaths;
private NativeList<int> polygonPathsNumPoints;
...
private void Setup() {
    ...
    polygonPaths = new NativeList<float2>(32, Allocator.Persistent);
    polygonPathsNumPoints = new NativeList<int>(8, Allocator.Persistent);
    ...
}

Snapshot Collision (Rope.cs):

private void SnapshotCollision() {
    // Clear polygon collider buffers.
    // This just sets the length to 0, no actual work is done, so we're good :).
    polygonPaths.Clear();
    polygonPathsNumPoints.Clear();

    // Update the colliders in range of each node.
    // A good optimization here would be to only run on the first fixed update.
    numCollisions = 0;
    for (int i = 0; i < nodes.Length; i++) {
        int collisions =
            Physics2D.OverlapCircleNonAlloc(nodes[i].position, COLLISION_RADIUS, colliderBuffer, filter.layerMask);

        for (int j = 0; j < collisions; j++) {
            // If we couldn't find the collider in the array, then it's new, and we need to set stuff up.
            if (idx < 0) {
..
                switch (col) {
...
                    case PolygonCollider2D p:
                        nc.colliderType = ColliderType.Polygon;
                        // Push all paths of this polygon onto our collision list.
                        for (int k = 0; k < p.pathCount; k++) {
                            // This generates garbage :(.
                            Vector2[] path = p.GetPath(k);
                            polygonPathsNumPoints.Add(path.Length);
                            for (int w = 0; w < path.Length; w++) {
                                polygonPaths.Add((Vector2)p.transform.TransformPoint(path[w]));
                            }
                        }

                        break;
...
}
Befezdow commented 2 years ago

Does ClosestPointOnLine function looks like this? Or is there some specificity? I didn't find the implementation in your comment.

Vector3 ClosestPointOnLine(Vector3 a, Vector3 b, Vector3 p) {
    return a + Vector3.Project(p - a, b - a);
}
Toqozz commented 2 years ago

Ahh! Sorry about that. I've updated the post to include that function.

Befezdow commented 2 years ago
public static float2 ClosestPointOnLine(float2 v1, float2 v2, float2 p) {
    float l2 = math.distancesq(v1, v2);
    // v1 == v2 case.
    if (l2 == 0f) {
        return v1; //math.distance(p, v1);
    }

    // Consider the line extending the segment, parameterized as v1 + t (v2 - v1).
    // We find projection of point p onto the line.
    // It falls where t = [(p-v1) . (v2-v1)] / |v2-v1|^2
    // We clamp t from [0,1] to handle points outside of the segment v1v2.
    float t = math.clamp(math.dot(p - v1, v2 - v1) / l2, 0f, 1f);
    float2 projection = v1 + t * (v2 - v1);  // Projection falls on the segment.
    return projection;
}

The code does not match the formula - you forget the squaring) There should be float t = math.clamp(math.dot(p - v1, v2 - v1) / (l2 * l2), 0f, 1f);

Toqozz commented 2 years ago

Isn't l2 already squared?

Befezdow commented 2 years ago

Hmm, yeah, you're right, sorry. I was confused by the documentation of the math package: v 1.0: image https://docs.unity3d.com/Packages/com.unity.mathematics@1.0/api/Unity.Mathematics.math.html#Unity_Mathematics_math_distancesq_Unity_Mathematics_float2_Unity_Mathematics_float2_

v 1.2: image https://docs.unity.cn/Packages/com.unity.mathematics@1.2/api/Unity.Mathematics.math.distancesq.html#Unity_Mathematics_math_distancesq_Unity_Mathematics_float2_Unity_Mathematics_float2_

Anyway, are you using mathematics package and native containers because of the burst compiler? Or are they faster than similar built-in methods in all situations?

Toqozz commented 2 years ago

A bit of both. You have to use them to use Burst, but also they give much more control over allocations which means you should be able to do better with them as well. I believe they also try to make more guarantees about memory layout, so should be faster for general usage as well.

The main performance boost is really from using Burst itself: https://www.jacksondunstan.com/articles/5211

Also: https://forum.unity.com/threads/native-arrays-approximately-an-order-of-magnitude-slower-than-arrays.535019/

Safety checks in the editor have a significant performance cost. The safety checks are disabled in the standalone player completely and in IL2CPP there is a fast path making builtin arrays and NativeArrays equally fast. The real performance gains of NativeArray are leveraged from the burst compiler, when writing primarily jobified code with the [ComputeOptimization] attribute. We expect that for any code that is performance sensitive that developers will write it to run in a job in burst. In burst the speed gains from using NativeArray are very significant. Usually on the order of 5-15x compared to il2cpp / mono. -- Joachim_Ante, Unity Technologies

ljhan85 commented 2 years ago

Hi, how to fix the stick constraint of verlet the will not stretch and sag?thank you

Toqozz commented 2 years ago

Hi, how to fix the stick constraint of verlet the will not stretch and sag?thank you

If you're asking how to make the rope stiffer, the general advice is to increase number of iterations and decrease number of nodes as much as possible. I'm not entirely sure what you mean because a rope will (and should!) always sag in the middle, otherwise it's not really a rope.

If you're asking how to stop/limit the rope from stretching past its limit, you probably have to get a bit creative with your usage code and how you handle moving the rope. An option here is manually measuring how long the rope should be and then restricting mouse movement past that length.

ljhan85 commented 2 years ago

Hi, i have a problem of self collision using verlet position based without storing velocity. how can i fix this?

On Thu, Dec 30, 2021, 9:13 PM Michael Palmos @.***> wrote:

Hi, how to fix the stick constraint of verlet the will not stretch and sag?thank you

If you're asking how to make the rope stiffer, the general advice is to increase number of iterations and decrease number of nodes as much as possible. I'm not entirely sure what you mean because a rope will (and should!) always sag in the middle, otherwise it's not really a rope.

If you're asking how to stop/limit the rope from stretching past its limit, you probably have to get a bit creative with your usage code and how you handle moving the rope. An option here is manually measuring how long the rope should be and then restricting mouse movement past that length.

— Reply to this email directly, view it on GitHub https://github.com/Toqozz/blog-code/issues/8#issuecomment-1002983371, or unsubscribe https://github.com/notifications/unsubscribe-auth/AVLVWCZHWFKJYPHD24GBU23UTQ5FVANCNFSM4YIDLHBA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

You are receiving this because you commented.Message ID: @.***>

aprius commented 1 year ago

Hi, i'm implementing rope for my game. and i found this great article, i have a question is there any way to hang an object on the rope and cut the rope?

Toqozz commented 1 year ago

Hi, i'm implementing rope for my game. and i found this great article, i have a question is there any way to hang an object on the rope and cut the rope?

Doing both of these should be pretty easy.

You can "hang" an object just by making it follow the end position, but you probably want to give it some weight too, which you can achieve by applying some multiplier to make it move less at the integration step.

To "cut" the rope, you'd probably want to just spawn another rope at the same initial position and copy the prev values over to give it the same initial velocity. Don't forget to also remove the nodes from the initial rope.

Detecting a cut depends on how you're going to be doing it, but you'll probably have to iterate over every node and check some condition -- e.g. find the closest node to the scissors.

aprius commented 1 year ago

Hi, i'm implementing rope for my game. and i found this great article, i have a question is there any way to hang an object on the rope and cut the rope?

Doing both of these should be pretty easy.

You can "hang" an object just by making it follow the end position, but you probably want to give it some weight too, which you can achieve by applying some multiplier to make it move less at the integration step.

To "cut" the rope, you'd probably want to just spawn another rope at the same initial position and copy the prev values over to give it the same initial velocity. Don't forget to also remove the nodes from the initial rope.

Detecting a cut depends on how you're going to be doing it, but you'll probably have to iterate over every node and check some condition -- e.g. find the closest node to the scissors.

Yes thank you, I thought so too at first but I wonder if there is any other way?.

I'll probably implement it first then think about it later.

Tivium commented 1 year ago

Hey, it's an amazing blog post. I am making a game and I want to make the rope not stretch after some length but I couldn't figure out the solution. I've tried some ways like

If you're asking how to stop/limit the rope from stretching past its limit, you probably have to get a bit creative with your usage code and how you handle moving the rope. An option here is manually measuring how long the rope should be and then restricting mouse movement past that length.

but it always ended up being messed up because I am not a very good programmer. Is there any way to implement this?

Toqozz commented 1 year ago

Hey, it's an amazing blog post. I am making a game and I want to make the rope not stretch after some length but I couldn't figure out the solution.

I've tried some ways like

If you're asking how to stop/limit the rope from stretching past its limit, you probably have to get a bit creative with your usage code and how you handle moving the rope. An option here is manually measuring how long the rope should be and then restricting mouse movement past that length.

but it always ended up being messed up because I am not a very good programmer. Is there any way to implement this?

To elaborate on the solution I was describing there:

Basically, measure the current length of the rope by adding the distance of each node together.

If the current length is >= the desired length, don't let whatever is stretching the rope stretch anymore.

It's an OK solution but doesn't really feel very robust, does it? Looking back on this now you might be better off just running a final pass over the rope after the simulation finishes:

  1. Get the current length like above.
  2. Get the percentage the rope needs to shorten by to match the desired length: desiredLen / currentLen.
  3. Move each node towards its previous node by that percentage. You should end up with a rope of the desired length, with the end shortened.

I'm not sure how well it will work in practice, but there's the two options I see for implementing it.

Toqozz commented 1 year ago

Just came across this today: https://www.gamedeveloper.com/programming/the-secrets-of-cloth-simulation-in-i-alan-wake-i-

Seems like in industry a few techniques are used to counteract the stretchiness of verlet things and make it a bit more robust.