Jelly Meshdeformation Part I – Basic Meshdeformation

If you have been following me for a while you might be aware that I had already published a version of this Jelly Meshdeformation stuff at one point. However, I decided that I would like to improve it. I am going to split this tutorial into a three part series where I am going to teach you basic Jelly Meshdeformation, as well as utilizing the new Unity Jobsystem for improved performance and how to make a project Modular enough to let it be handled through an Editor Script.

This first part will teach you the basics of Jelly Meshdeformation.

What to expect from following Part I:

Here is a GIF of the expected outcome from following this first part of the tutorial series. The beautiful Shader you see in action here is courtesy of my very talented friend Joyce and won’t be included in the repository.

You can get it here!

Preparations:

Since we are intending on using the Jobsystem and utilizing a custom editor at one point it is necessary to start out by making the project as modular as possible. To achieve modularity it is generally a good idea to have some kind of manager that will handle all of our jellies instead of having all of them act on their own, you might know the saying about too many cooks – this applies here.

JellyManagement:

We have to create an empty game object and add our JellyManagement.cs onto it. We should turn that into a prefab and place it in our Prefabs folder. Codewise we are starting out with a bunch of static variables and coordinated getters and setters for them, we are doing this, as I mentioned earlier, in an effort to make this project as modular and accessible as possible. This will make our lives much easier once we start to work on our custom editor. We also want to tag our manager as DontDestroyOnLoad, which we do again, for the sake of modularity, as this will allow us to do the entire setup of the jelly components in our editor.

As this is a Manager we will also need a list of all of our jellies and a function that’ll help us to add jellied objects to the formerly mentioned list. I usually like to refer to that as bookkeeping.

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

public class JellyManagement : MonoBehaviour {

  //Instance
  static JellyManagement instance;

  //MouseInput Settings:
  static bool useStandartMouseInput;
  static bool allowToPickUp;
  static float clickPressure;
  static float forceOffset;

  //Physics Settings:
  static bool reactToGravity;
  static bool allowRotation;

  //Consistency Settings:
  static bool useManualSettings;
  static float stiffness = 10f;
  static float attenuation = 5.5f;

  //Book Keeping
  [HideInInspector] 
  public List<GameObject> jelliedBodies = new List<GameObject>();

  //Getters & Setters
  public static JellyManagement Instance 
  {
    get{ return instance;} 
    set{ instance = value;}
  }

  public static bool UseStandartMouseInput 
  {
    get{ return useStandartMouseInput;} 
    set{ useStandartMouseInput = value;}
  }

  public static bool AllowToPickUp 
  {
    get{ return allowToPickUp;} 
    set{ allowToPickUp = value;}
  }

  public static float ClickPressure 
  {
    get{ return clickPressure;} 
    set{ clickPressure = value;}
  }

  public static float ForceOffset 
  {
    get{ return forceOffset;} 
    set{ forceOffset = value;}
  }

  public static bool ReactToGravity 
  {
    get{ return reactToGravity;} 
    set{ reactToGravity = value;}
  }

  public static bool AllowRotation 
  {
    get { return allowRotation;} 
    set{ allowRotation = value;}
  }

  public static bool UseManualSettings 
  {
    get { return useManualSettings;} 
    set{ useManualSettings = value;}
  }

  public static float Stiffness
  {
    get{ return stiffness;} 
    set{ stiffness = value;}
  }

  public static float Attenuation 
  {
    get{ return attenuation;} 
    set{ attenuation = value;}
  }

  void Awake()
  {
    instance = this;
    DontDestroyOnLoad(this);

    jelliedBodies.Clear();
  }

  public void AddJelly(GameObject _gameObject)
  {
    if(!jelliedBodies.Contains(_gameObject)){
      jelliedBodies.Add(_gameObject);
    }
  }

 

Our manager will not only have to deal with our bookkeeping but he’ll also have two additional tasks to perform for us. First of all, it needs to apply a force to the verts of our mesh that are currently influenced by physics.

The following function will ask for a list of influenced positions (the position where a contact between our jelly and another object happened), a force – which will be our initiator for the pressure and some general information about the requester.
Firstly we are looping over all of the given influenced positions, with every iteration we are using transform.InverseTransformPoint() to go from world space to local space for our following calculations. In another loop nested inside, we are now iterating over our array of displaced vertices, which for better understanding, could also be named current Vertices. We are now calculating the mere distance between our vertex and the current contact point, adding in a small factor to achieve an offset.

We are transforming our force, given the calculated position into a velocity for our vertex and that’s what will initiate our meshes deformation.

public void AddForceToVerts(Vector3[] _contactPoints, float _force, GameObject _jelliedObject, JellyBody _jellyBody)
{
  Vector3 currentPoint;

  for(int i = 0; i < _contactPoints.Length; i++)
  {
    currentPoint = 
    _jelliedObject.transform.InverseTransformPoint(_contactPoints[i]);
    for(int j = 0; j < _jellyBody.DisplacedVerts.Length; j++)
    {

      Vector3 pointToVert = (_jellyBody.DisplacedVerts[j] - currentPoint) * 
      _jellyBody.UnitformScale;

      if(!useManualSettings)
      {
        float attenuatedForce = (_force/((_jellyBody.Attenuation)/2f)) / 
        (1f + pointToVert.sqrMagnitude);
        float velocity = attenuatedForce * Time.deltaTime;
        _jellyBody.VertVelocities[j] += pointToVert.normalized * velocity;
      } 
      else 
      {
        float attenuatedForce = (_force/((attenuation)/2f)) / (1f + 
        pointToVert.sqrMagnitude);
        float velocity = attenuatedForce * Time.deltaTime;
        _jellyBody.VertVelocities[j] += pointToVert.normalized * velocity;
      }
    }
  }
}

 

Second of all, it needs to update our vertices at runtime. Which means in our case we will loop through all vertices, calculating the distance between the initial vertex and the deformed vertex. We will update the velocity depending on our jelly consistency setting. We will call this from Update for the time being.

void Update() 
  {
    for(int i = 0; i < jelliedBodies.Count; i++)
    {
      JellyBody currentJellybody = jelliedBodies[i].GetComponent<JellyBody>();
      UpdateVerts(currentJellybody);
    }	
  }

  void UpdateVerts(JellyBody _jellyBody)
  {
    for(int i = 0; i < _jellyBody.DisplacedVerts.Length; i++)
    {
      Vector3 velocity = _jellyBody.VertVelocities[i];
      Vector3 displacement = _jellyBody.DisplacedVerts[i] - 
      _jellyBody.InitialVerts[i];
      float springforce = (useManualSettings) ? _jellyBody.Stiffness : stiffness;
      float dampening = (useManualSettings) ? _jellyBody.Attenuation : attenuation;
      
      displacement *= _jellyBody.UnitformScale;
      velocity -= displacement * springforce * Time.deltaTime;
      velocity *= _jellyBody.UnitformScale - dampening * Time.deltaTime;

      _jellyBody.VertVelocities[i] = velocity;
      _jellyBody.DisplacedVerts[i] += velocity * (Time.deltaTime/
      _jellyBody.UnitformScale);
    }

    _jellyBody.JellyMesh.vertices = _jellyBody.DisplacedVerts;
    _jellyBody.JellyMesh.RecalculateNormals();
  }

 

 

JellyBody:

The next thing we will have to take care of is our JellyBody.cs – this script will be attached to every jellied object in our scene.
Just as for the JellyManagement class, for this one, we will write a bunch of getters/setters that will make our life much easier once we are starting to work with our Editor script.

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

public class JellyBody : MonoBehaviour {

  float stiffness;
  float attenuation;

  Vector3[] initialVerts;
  Vector3[] displacedVerts;
  Vector3[] vertVelocities;
  int[] tris;

  Mesh mesh;
  Rigidbody rigidBody;

  float volume;

  public Mesh JellyMesh 
  {
    get{ return mesh;} 
    set{ mesh = value;}
  }

  public float Stiffness
  {
    get{ return stiffness;} 
    set{ stiffness = value;}
  }


  public float Attenuation 
  {
    get{ return attenuation;} 
    set{ attenuation = value;}
  }

  public float UnitformScale 
  {
    get {return 1f;}
  }

  public Vector3[] InitialVerts 
  {
    get{ return initialVerts;} 
    set{ initialVerts = value;}
  }

  public Vector3[] DisplacedVerts 
  {
    get { return displacedVerts;} 
    set{ displacedVerts = value;}
  }

  public Vector3[] VertVelocities 
  {
    get { return vertVelocities;} 
    set{ vertVelocities = value;}
  }

 

In order to perform the Meshdeformation, we will need to track and handle a bunch of values regarding our vertices we are going to access from our JellyManagement class.

We will use our start function for caching all the necessary data. As you’ll see in the following code snippet, we are basically getting our mesh from the MeshFilter component that is attached to every game object that has a mesh. We then cache its vertices into an array of Vector3 which is called initial vertices. We need this in order to keep track of what every vertex’s position was before we deformed the mesh. We will also copy the initial vertices to an array of Vector3 which we call displaced vertices. In order to have a jellied effect, as mentioned earlier we will calculate the displacement given the initial vertices and the displaced vertices.

void Start()
{
  mesh = GetComponent<MeshFilter>().mesh;
  rigidBody = GetComponent<Rigidbody>();

  initialVerts = mesh.vertices;
  displacedVerts = new Vector3[initialVerts.Length];
  for(int i = 0; i < displacedVerts.Length; i++)
  {
    displacedVerts[i] = initialVerts[i];
  }

  vertVelocities = new Vector3[initialVerts.Length];
  tris = mesh.triangles;

  volume = AssumedVolume();
  stiffness = volume;
  attenuation = Dampening();
  rigidBody.mass = volume * 10f;

  JellyManagement.Instance.AddJelly(this.gameObject);
}

You might have noticed a few functions I am using in start to calculate values like the volume and the attenuation. I am basically doing this by assuming the volume using the mesh triangles.  I am also taking a procedural factor for the localScale into account. This will allow us to generate a relatively realistic outcome considering the size of our mesh. The bigger, the jigglier – but don’t worry – we will implement a way to set them up manually ignoring the size, aswell.

float AssumedVolume()
{
  float _volume = 0;
  float scaledVolume = 0;

  for(int i = 0; i < tris.Length; i+= 3)
  {
    Vector3 a = initialVerts[tris[i]];
    Vector3 b = initialVerts[tris[i+1]];
    Vector3 c = initialVerts[tris[i+2]];
    
    _volume += (Vector3.Dot(a, Vector3.Cross(b,c))/6.0f);
    scaledVolume = _volume + (_volume * ProcentualScaleFactor());
  }
  return Mathf.Abs(scaledVolume);
}

float ProcentualScaleFactor()
{
  Vector3 _localScale = transform.localScale;

  float sFx = (_localScale.x > 1.0f) ? _localScale.x - 1.0f : _localScale.x;
  float sFy = (_localScale.y > 1.0f) ? _localScale.y - 1.0f : _localScale.y;
  float sFz = (_localScale.z > 1.0f) ? _localScale.z - 1.0f : _localScale.z;

  return ((sFx + sFy + sFz)/3.0f);
}

float Dampening()
{
  return ((((1f/volume) * volume) * (1f-(ProcentualScaleFactor()/10f))) * 2);
}

 

If you have inspected the gif at the beginning of this entry you might have noticed that I am using clicks to basically poke my jelly. I’m doing this with a simple MouseInput script – but in order to do this, we will need a public function that will allow us to request the necessary deformation from the JellyManagement class.

public void AddPointForce(float _force, Vector3 _pressurePoint)
  {
    Vector3[] pressurePoints = new Vector3[1];
    pressurePoints[0] = _pressurePoint;
    JellyManagement.Instance.AddForceToVerts(pressurePoints,_force,gameObject,this);
  }

 

You might have also noticed that I am deforming the Mesh via PhysicsEngine input. I am doing this with Unity’s basic Collision detection.
I am achieving this by keeping track of all contact points by putting them into an array of Vector3. I modify each of the contact points by adding it’s normal to get the right direction. I also multiply this value by the mesh bounds maximum size for x to give myself some kind of offset. Having an offset is going to help us prevent getting weird behaviour if our jellied object happens to be straight.

void OnCollisionStay(Collision other) 
  {
    if(other.contacts.Length > 0)
    {
      Vector3[] contactPoints = new Vector3[other.contacts.Length];
      for(int i = 0; i < other.contacts.Length; i++)
      {
        Vector3 currentContactpoint = other.contacts[i].point;
        currentContactpoint += other.contacts[i].normal * mesh.bounds.max.x;
        contactPoints[i] = currentContactpoint;
      }

      JellyManagement.Instance.AddForceToVerts(contactPoints,rigidBody.mass,
      gameObject,this);
    }	
  }

 

 

MouseInput:

The last thing you’ll need to do for this first part of the tutorial is getting the mouse input and using it to add pressure to our jelly.
I wrote a simple script that uses raycasting to determine whether or not the Jelly has been clicked. If it has we are using the raycasts output to figure out the hitpoint, which will be multiplied by an offset to ensure that we will deform inwards in case our jellied object is a plane, to which we will then add our pressure force.

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

public class MouseInput : MonoBehaviour {

  [Header("Input Settings:")]
  public float pressureForce;

  protected RaycastHit hitInfo;
  protected Ray ray;

  protected Vector3 inputPoint;
  protected JellyBody jellyBody;
  protected GameObject jelly;

  public static MouseInput instance;

  private void Start(){
    instance = this;
  }

  private void Update(){
    CheckForMouseInput();	
  }

  private void CheckForMouseInput(){
    if(Input.GetMouseButton(0)){
      ray = Camera.main.ScreenPointToRay(Input.mousePosition);
      if(Physics.Raycast(ray,out hitInfo)){
        jellyBody = hitInfo.collider.gameObject.GetComponent<JellyBody>();
        if(jellyBody != null){
          inputPoint = hitInfo.point;
          inputPoint += hitInfo.normal * 0.1f;
          jellyBody.AddPointForce(pressureForce, inputPoint);
        }
      }
    }
  }
}

 

Get the Repository for Part I on GitHub!

Liked it? Take a second to support Kristin on Patreon!

4 comments

  1. Hi Kristin,
    I’m currently investigating performance of the Unity Job system and looking at different samples. So I just stumbled upon your sample. Thanks for sharing! I really like your idea of simulating Jelly 🙂

    Now I found some performance problems with your code if you don’t mind me pointing them out:

    So your jobs get scheduled in a loop (driven by Update() of a MonoBehaviour).

    1) You are scheduling your IJobParallelFor with the same batch size as you have entries in the array.
    And 2) you Complete() immediately after you Schedule().

    What that means is that your jobs essentially get completely serialized and have no concurrency besides it being not computed on the main thread. They might get executed on different worker threads per Update, but they will always be 1 Job per invocation per Jelly. You can see it very clearly in the timeline view of the Unity CPU profiler.

    ad 1)
    If you change the batchsize to something like 64, i.e.
    JobHandle meshDeformationJobHandle = meshDeformationJob.Schedule(_jellyBody.DisplacedVerts.Length, 64);
    at least every job in the loop in Update can go wide and you will see more speedup the bigger your meshes are and depending on your batch size parameter and your CPU core count / number of worker threads.

    ad 2)
    You want to avoid to schedule jobs and wait for their completion immediately after that. It gives the system no chance to execute independent jobs at the same time. In your case none of the jobs are dependent on each other. You only need to do some work after completion (the write back of the values). We can achieve that by splitting the job creation and scheduling from the 2nd part. With this we can schedule multiple jobs at once.

    struct JellyMeshDeformJobHandleAndResult
    {
    public JellyBody jellyBody;
    public JobHandle jobHandle;
    public NativeArray initialVertsAccess;
    public NativeArray displacedVertsAccess;
    public NativeArray vertVelocitiesAccess;
    }

    void Update()
    {
    JellyMeshDeformJobHandleAndResult[] jobHandles = new JellyMeshDeformJobHandleAndResult[jelliedBodies.Count];

    for (int i = 0; i < jelliedBodies.Count; i++)
    {
    JellyBody currentJellybody = jelliedBodies[i].GetComponent();
    if (currentJellybody.DisplacedVerts.Length != 0)
    {
    ScheduleMeshDeformationJob(currentJellybody, ref jobHandles[i]);
    }
    }

    for (int i = 0; i < jelliedBodies.Count; i++)
    {
    if (jobHandles[i].jellyBody != null)
    {
    CompleteJobAndCopyResultsBack(jobHandles[i]);
    }
    }
    }

    void ScheduleMeshDeformationJob(JellyBody _jellyBody, ref JellyMeshDeformJobHandleAndResult jobHandle)
    {
    jobHandle.jellyBody = _jellyBody;

    jobHandle.initialVertsAccess = new NativeArray(_jellyBody.InitialVerts, Allocator.TempJob);
    jobHandle.displacedVertsAccess = new NativeArray(_jellyBody.DisplacedVerts, Allocator.TempJob);
    jobHandle.vertVelocitiesAccess = new NativeArray(_jellyBody.VertVelocities, Allocator.TempJob);

    MeshDeformationJob meshDeformationJob = new MeshDeformationJob
    {
    initialVerts = jobHandle.initialVertsAccess,
    displacedVerts = jobHandle.displacedVertsAccess,
    vertVelocities = jobHandle.vertVelocitiesAccess,
    springforce = (useManualSettings) ? _jellyBody.Stiffness : stiffness,
    dampening = (useManualSettings) ? _jellyBody.Attenuation : attenuation,
    unitformScale = _jellyBody.UnitformScale,
    time = Time.deltaTime
    };

    jobHandle.jobHandle = meshDeformationJob.Schedule(_jellyBody.DisplacedVerts.Length, 64);
    }

    void CompleteJobAndCopyResultsBack(JellyMeshDeformJobHandleAndResult jobHandle)
    {
    jobHandle.jobHandle.Complete();

    jobHandle.initialVertsAccess.CopyTo(jobHandle.jellyBody.InitialVerts);
    jobHandle.initialVertsAccess.Dispose();

    jobHandle.displacedVertsAccess.CopyTo(jobHandle.jellyBody.DisplacedVerts);
    jobHandle.displacedVertsAccess.Dispose();

    jobHandle.vertVelocitiesAccess.CopyTo(jobHandle.jellyBody.VertVelocities);
    jobHandle.vertVelocitiesAccess.Dispose();

    jobHandle.jellyBody.JellyMesh.vertices = jobHandle.jellyBody.DisplacedVerts;
    }

    1) and 2) give me an order of magnitude speedup. If you look into the profiler after this change you see that all the jobs are now distributed over all worker threads and tightly packed. there is no idle time anymore.

    3) mark initialverts [ReadOnly] – makes it safer (compiler warnings if you break that promise) and enables optimizations

    4) put the [Unity.Burst.BurstCompile] (former [ComputeJobOptimization]) attribute on your MeshDeformationJob and enable Burst jobs. this gives me another order of magnitude speedup.

    In total I get a speed up of two orders of magnitude for the whole job span (~4ms down to 0.04ms, in the editor) on my 10 core machine. Now your test scene is well below 16.6ms frame time, so this does not improve anything visibly. But with this optimization you can now process so many more jellies and on older machines this will make a big difference for sure.

    Hope this helps! Let me know if you want more details. I made some screenshots of the profiler but can’t post here.

    Best,
    Didi

  2. I needed to create you that little bit of remark to thank you very much once again regarding the extraordinary methods you’ve contributed in this article. This has been so incredibly generous of you to make unhampered what a lot of people would’ve made available as an e book to help with making some profit for their own end, particularly given that you might well have tried it if you wanted. The tactics as well served as the fantastic way to fully grasp that someone else have the same keenness really like my personal own to find out way more around this problem. I think there are many more pleasant situations up front for folks who go through your blog.

Leave a Reply

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