1
votes

I am making a game in Unity with a rubber ball for one of the objects. I have looked through various soft body physics assets available and none of them look particularly what I am looking for. I wish to program a ball that will bounce, and sort of stretch as it hits the ground. This will be a center point for my game and I wish it to look as good as I can get it, however all the solutions I am finding online are small patches or poorly written. I also would like to study the solution so I can adapt and learn from it in the future, I do not want to just purchase an asset and throw it into a game.

I first was drawn to World of Zero and their mesh deformation videos. These seemed exceptionally complex for my needs however, and I was drawn to his Squishy Sphere videos where they did not work quite right. Eventually I found Catlike Coding and their mesh deformations based on vectors and such. It works really well visually, but adapting it to my needs it considerably more complex then I would have hoped.

Edit 2: I added the rest of this class to better show the functions

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

public class MeshDeformerInput : MonoBehaviour
{

public float force = 10f;
public float forceOffset = 0.01f;

void HandleInput(Ray inputRay)
{
    RaycastHit hit;

    Debug.DrawRay(inputRay.origin, inputRay.direction * 50000000, Color.red);

    if (Physics.Raycast(inputRay, out hit))
    {
        MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();

        if (deformer)
        {
            Vector3 point = hit.point;
            point += hit.normal * forceOffset;
            deformer.AddDeformingForce(point, force);
        }
    }
}

void OnCollisionStay(Collision collision)
{
    foreach (ContactPoint contact in collision.contacts)
    {
        Debug.Log("Hit!");
        // Visualize the contact point
        Debug.DrawRay(contact.point, contact.normal, Color.white);

        Ray newRay = new Ray(contact.point, contact.normal);
        HandleInput(newRay);
    }
}

I had issues with OnCollisionEnter because it would not continue the collision detection properly as the ball rolled. I switched to stay hoping to change the vectors as the ball rolled around, instead of at a single point, it worked a bit better.

After it calls HandleInput is takes the ray and a static force into my mesh deformer, from here it is identical to the code from this tutorial

https://catlikecoding.com/unity/tutorials/mesh-deformation/

The problems I am having is first, the contact point is moving too slowly as the ball rolls, resulting in wrongly placed depressions. The spring physics are too slow to respond, and increasing the speed causes the mesh physics to fall apart completely. Secondly, the ball hovers above the contact point because the collider is still not moving, this would later cause major issues with my game. I guess what I am asking is if there are any alternatives to this approach, or if this one is fine and I am just approaching this wrong? I have read that Unity does not come with a standard soft body physics, but is this a feature that I will never be able to quite get right simply because it does not have one by default, or am I just looking in the wrong places?

Edit 1: The link going dead is a fair point so here is the code. I will try to include it all as it is fairly thick and I feel like describing it would be insufficient.

To start is my actual mesh deformer. The code uses a rounded cube that is build with code I put at the end of this, hence why I am having issues. Each vertices is coded in a loop to be indented based on the distance from the collision point. Originally it was from a ray drawn to the place you click from the camera, I adjusted this to instead draw a ray from the point of contact with an object. It seems to work in the right location at least, just not right away and stays dented longer then it should.

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

[RequireComponent(typeof(MeshFilter))]
public class MeshDeformer : MonoBehaviour 
{


Mesh deformingMesh;
Vector3[] originalVertices, displacedVertices, vertexVelocities;

public float forceOffset = 0.1f;
public float springForce = 20f;
public float damping = 5f;
float uniformScale = 1f;

void Start()
{
    deformingMesh = GetComponent<MeshFilter>().mesh;
    originalVertices = deformingMesh.vertices;
    displacedVertices = new Vector3[originalVertices.Length];
    vertexVelocities = new Vector3[originalVertices.Length];

    for (int i = 0; i < originalVertices.Length; i++)
    {
        displacedVertices[i] = originalVertices[i];
    }
}

public void AddDeformingForce(Vector3 point, float force)
{
    Debug.Log("Deformer called with: " + point + " point");
    point = transform.InverseTransformPoint(point);
    for (int i = 0; i < displacedVertices.Length; i++)
    {
        AddForceToVertex(i, point, force);
    }
}
void AddForceToVertex(int i, Vector3 point, float force)
{
    Vector3 pointToVertex = displacedVertices[i] - point;
    pointToVertex *= uniformScale;
    float attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
    float velocity = attenuatedForce * Time.deltaTime;
    vertexVelocities[i] += pointToVertex.normalized * velocity;
}

void Update()
{
    uniformScale = transform.localScale.x;
    for (int i = 0; i < displacedVertices.Length; i++)
    {
        UpdateVertex(i);
    }
    deformingMesh.vertices = displacedVertices;
    deformingMesh.RecalculateNormals();
}
void UpdateVertex(int i)
{
    Vector3 velocity = vertexVelocities[i];
    Vector3 displacement = displacedVertices[i] - originalVertices[i];
    displacement *= uniformScale;
    velocity -= displacement * springForce * Time.deltaTime;
    velocity *= 1f - damping * Time.deltaTime;
    vertexVelocities[i] = velocity;
    displacedVertices[i] += velocity * (Time.deltaTime / uniformScale);
}
}

Not sure if this will help but here is the sphere coding in case it makes a difference.

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



[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class CubeSphere : MonoBehaviour
{
public int gridSize;
public float radius = 1f;

private Mesh mesh;
private Vector3[] vertices;
private Vector3[] normals;
private Color32[] cubeUV;



private void Awake()
{
    Generate();
}

private void Generate()
{
    GetComponent<MeshFilter>().mesh = mesh = new Mesh();
    mesh.name = "Procedural Cube";
    CreateVertices();
    CreateTriangles();
    CreateColliders();

}

private void CreateVertices()
{
    int cornerVertices = 8;
    int edgeVertices = (gridSize + gridSize + gridSize - 3) * 4;
    int faceVertices = (
        (gridSize - 1) * (gridSize - 1) +
        (gridSize - 1) * (gridSize - 1) +
        (gridSize - 1) * (gridSize - 1)) * 2;
    vertices = new Vector3[cornerVertices + edgeVertices + faceVertices];
    normals = new Vector3[vertices.Length];
    cubeUV = new Color32[vertices.Length];

    int v = 0;
    for (int y = 0; y <= gridSize; y++)
    {
        for (int x = 0; x <= gridSize; x++)
        {
            SetVertex(v++, x, y, 0);
        }
        for (int z = 1; z <= gridSize; z++)
        {
            SetVertex(v++, gridSize, y, z);
        }
        for (int x = gridSize - 1; x >= 0; x--)
        {
            SetVertex(v++, x, y, gridSize);
        }
        for (int z = gridSize - 1; z > 0; z--)
        {
            SetVertex(v++, 0, y, z);
        }
    }
    for (int z = 1; z < gridSize; z++)
    {
        for (int x = 1; x < gridSize; x++)
        {
            SetVertex(v++, x, gridSize, z);
        }
    }
    for (int z = 1; z < gridSize; z++)
    {
        for (int x = 1; x < gridSize; x++)
        {
            SetVertex(v++, x, 0, z);
        }
    }

    mesh.vertices = vertices;
    mesh.normals = normals;
    mesh.colors32 = cubeUV;
}

private void SetVertex(int i, int x, int y, int z)
{
    Vector3 v = new Vector3(x, y, z) * 2f / gridSize - Vector3.one;
    float x2 = v.x * v.x;
    float y2 = v.y * v.y;
    float z2 = v.z * v.z;
    Vector3 s;
    s.x = v.x * Mathf.Sqrt(1f - y2 / 2f - z2 / 2f + y2 * z2 / 3f);
    s.y = v.y * Mathf.Sqrt(1f - x2 / 2f - z2 / 2f + x2 * z2 / 3f);
    s.z = v.z * Mathf.Sqrt(1f - x2 / 2f - y2 / 2f + x2 * y2 / 3f);
    normals[i] = s;
    vertices[i] = normals[i] * radius;
    cubeUV[i] = new Color32((byte)x, (byte)y, (byte)z, 0);
}

private void CreateTriangles()
{
    int[] trianglesZ = new int[(gridSize * gridSize) * 12];
    int[] trianglesX = new int[(gridSize * gridSize) * 12];
    int[] trianglesY = new int[(gridSize * gridSize) * 12];
    int ring = (gridSize + gridSize) * 2;
    int tZ = 0, tX = 0, tY = 0, v = 0;

    for (int y = 0; y < gridSize; y++, v++)
    {
        for (int q = 0; q < gridSize; q++, v++)
        {
            tZ = SetQuad(trianglesZ, tZ, v, v + 1, v + ring, v + ring + 1);
        }
        for (int q = 0; q < gridSize; q++, v++)
        {
            tX = SetQuad(trianglesX, tX, v, v + 1, v + ring, v + ring + 1);
        }
        for (int q = 0; q < gridSize; q++, v++)
        {
            tZ = SetQuad(trianglesZ, tZ, v, v + 1, v + ring, v + ring + 1);
        }
        for (int q = 0; q < gridSize - 1; q++, v++)
        {
            tX = SetQuad(trianglesX, tX, v, v + 1, v + ring, v + ring + 1);
        }
        tX = SetQuad(trianglesX, tX, v, v - ring + 1, v + ring, v + 1);
    }

    tY = CreateTopFace(trianglesY, tY, ring);
    tY = CreatebottomFace(trianglesY, tY, ring);

    mesh.subMeshCount = 3;
    mesh.SetTriangles(trianglesZ, 0);
    mesh.SetTriangles(trianglesX, 1);
    mesh.SetTriangles(trianglesY, 2);

}
private int CreateTopFace(int[] triangles, int t, int ring)
{
    int v = ring * gridSize;
    for (int x = 0; x < gridSize - 1; x++, v++)
    {
        t = SetQuad(triangles, t, v, v + 1, v + ring - 1, v + ring);
    }
    t = SetQuad(triangles, t, v, v + 1, v + ring - 1, v + 2);

    int vMin = ring * (gridSize + 1) - 1;
    int vMid = vMin + 1;
    int vMax = v + 2;

    for (int z = 1; z < gridSize - 1; z++, vMin--, vMid++, vMax++)
    {
        t = SetQuad(triangles, t, vMin, vMid, vMin - 1, vMid + gridSize - 1);
        for (int x = 1; x < gridSize - 1; x++, vMid++)
        {
            t = SetQuad(
                triangles, t,
                vMid, vMid + 1, vMid + gridSize - 1, vMid + gridSize);
        }
        t = SetQuad(triangles, t, vMid, vMax, vMid + gridSize - 1, vMax + 1);
    }

    int vTop = vMin - 2;
    t = SetQuad(triangles, t, vMin, vMid, vMin - 1, vMin - 2);
    for (int x = 1; x < gridSize - 1; x++, vTop--, vMid++)
    {
        t = SetQuad(triangles, t, vMid, vMid + 1, vTop, vTop - 1);
    }
    t = SetQuad(triangles, t, vMid, vTop - 2, vTop, vTop - 1);
    return t;
}

private int CreatebottomFace (int[] triangles, int t, int ring)
{
    int v = 1;
    int vMid = vertices.Length - (gridSize - 1) * (gridSize - 1);
    t = SetQuad(triangles, t, ring - 1, vMid, 0, 1);
    for (int x = 1; x < gridSize - 1; x++, v++, vMid++)
    {
        t = SetQuad(triangles, t, vMid, vMid + 1, v, v + 1);
    }
    t = SetQuad(triangles, t, vMid, v + 2, v, v + 1);

    int vMin = ring - 2;
    vMid -= gridSize - 2;
    int vMax = v + 2;

    for (int z = 1; z < gridSize - 1; z++, vMin--, vMid++, vMax++)
    {
        t = SetQuad(triangles, t, vMin, vMid + gridSize - 1, vMin + 1, vMid);
        for (int x = 1; x < gridSize - 1; x++, vMid++)
        {
            t = SetQuad(
                triangles, t,
                vMid + gridSize - 1, vMid + gridSize, vMid, vMid + 1);
        }
        t = SetQuad(triangles, t, vMid + gridSize - 1, vMax + 1, vMid, vMax);
    }

    int vTop = vMin - 1;
    t = SetQuad(triangles, t, vTop + 1, vTop, vTop + 2, vMid);
    for (int x = 1; x < gridSize - 1; x++, vTop--, vMid++)
    {
        t = SetQuad(triangles, t, vTop, vTop - 1, vMid, vMid + 1);
    }
    t = SetQuad(triangles, t, vTop, vTop - 1, vMid, vTop - 2);

    return t;
}

private void OnDrawGizmos()
{
    if (vertices == null)
    {
        Debug.Log("Returning");
        return;
    }
    for (int i = 0; i < vertices.Length; i++)
    {
        Gizmos.color = Color.black;
        //Gizmos.DrawSphere(vertices[i], 0.1f);
        Gizmos.color = Color.yellow;
        //Gizmos.DrawRay(vertices[i], normals[i]);
    }
}

private void CreateColliders()
{
    gameObject.AddComponent<SphereCollider>();
}

private static int SetQuad(int[] triangles, int i, int v00, int v10, int v01, int v11)
{
    triangles[i] = v00;
    triangles[i + 1] = triangles[i + 4] = v01;
    triangles[i + 2] = triangles[i + 3] = v10;
    triangles[i + 5] = v11;
    return i + 6;
}


}

Now as some of you might see the numbers from the offset, damping, and scale and such are not quite correct. I have been editing them in an attempt to get the vertices to speed up their movement. However because of the spring affect that it does when they move too fast in order to get it to deform faster to match the actual point of contact as the ball rolls, it causes it to bounce all over the place. The damping was originally meant to stop this, however it does it to the point that it once again slows it down.

Another issue is that if the ball stands still, the deformation returns to normal even as its in contact with the ground. I considered adding a statement checking for existing contact but worried that differentiating between objects in order to maintain the form would lead to increased complexity that would in the end not work anyway.

Lastly the actual collider does not change with the deformation. As the directions of the vertices are somewhat based on the collider location I am not sure how to go about solving this issue. Hence by dilemma, I am not sure if it is worth abandoning this technique entirely, or if there is a solution I can take to approach this.

1
Please add the code you use to the question instead of linkin us somewhere else. This will make it possible in the future for people who have the same question to find it using text searches. It also prevents your post from becoming totally useless if for some reason the URL doesn't work anymore in the future - derHugo
How do you call AddDeformingForce? I can't find it from the this thread. - shingo
I just added that. It was a small function in my input handling class so I originally didn't think to add it. - Matt Keita Ingraham

1 Answers

1
votes

I continued looking into alternatives and the only one that I have really seen that works well is the one from the asset store under Bullet Physics. It includes scripts for softbody spheres, and might work for my case. I will be modifying it to attempt to get it to work properly, but here is the link for anyone that is brought to this page.

https://assetstore.unity.com/packages/tools/physics/bullet-physics-for-unity-62991

Screenshot

I will keep checking back over the next few days in case anyone has a better solution.