Unity3D: Optimal Mesh Deformation

Creating a custom mesh deformer based on force and direction can be a powerful tool for dynamically altering 3D model meshes in your game.

Recently I’ve been playing with the idea of using this as a mechanic in a new game, and my experience so far has been tricky performance-wise, and I wanted to share how I was able to speed up the process and reduce calculation overhead with a couple of handy tools in our Unity arsenal!

We’re going to look at a very simple example of how to create one using the Unity engine, Unity's Job System and the Burst compiler to calculate the mesh deformation based on the given force and direction of the impact.

Let’s get started:

  1. Create a new script in Unity and call it "ForceDeformer."

  2. In the script, create a public variable called "force" and a public variable called "direction." These variables will determine the strength and direction of the deformation. Also, create a public variable called "impactRadius" which will determine the radius of the impact.

Let’s create a struct called "DeformJob" inside of “ForceDeformer” which will contain the data for the job and the execute function:

[BurstCompile]
struct DeformJob : IJobParallelFor
{
    public float force;
    public Vector3 direction;
    public float impactRadius;
    public NativeArray<Vector3> vertices;

    public void Execute(int i)
    {
        Vector3 vertex = vertices[i];
        float distance = Vector3.Distance(vertex, transform.position);
        if (distance < impactRadius)
        {
            float effect = 1 - (distance / impactRadius);
            vertices[i] += direction * force * effect;
        }
    }
}

"DeformJob" is defined as a struct because it will be used with the Unity Job System, which is designed to work with structs rather than classes.

This is because structs are value types, which means they are stored on the stack instead of the heap, resulting in lower overhead and better performance. This is particularly beneficial when passing large amounts of data to the jobs, as the data does not need to be copied and garbage collection is not required.

Structs also have the advantage of being passed by value, that is, a copy of the struct is passed to the job, this allows for better control of the data, and also allows for better cache coherency.

A struct is a good choice for this particular job as it only stores data and does not have any other functionality, and it is only used for the Job System, so it does not need to be a class.

Remember, a struct should not contain any references to other objects. When working with Unity's Job System, structs are a good choice, but when working with other systems or in other cases, classes may be more appropriate.


Now, in the Update() function, create a new DeformJob and schedule it using the Job System. Also, create a NativeArray to hold the vertices of the mesh, as this type is required for the Job System.

    void Update()
    {
        // Get the mesh and its vertices
        Mesh mesh = GetComponent<MeshFilter>().mesh;
        Vector3[] meshVertices = mesh.vertices;
        NativeArray<Vector3> vertices = new NativeArray<Vector3>(meshVertices, Allocator.TempJob);

        // Create and schedule the job
        DeformJob deformJob = new DeformJob
        {
            force = force,
            direction = direction,
            impactRadius = impactRadius,
            vertices = vertices
        };
        JobHandle handle = deformJob.Schedule(vertices.Length, 64);

        // Wait for the job to complete
        handle.Complete();

        // Copy the deformed vertices back to the mesh
        meshVertices = vertices.ToArray();
        mesh.vertices = meshVertices;
        mesh.RecalculateNormals();

        // Dispose of the NativeArray
        vertices.Dispose();
    }

Lastly, you can attach the “ForceDeformer” script to the game object that you want to deform and set the "force", "direction" and "impactRadius" variables in the Unity editor.

Our example here can calculate the effect and radius of the deformation in a more performant way than trying to run it every frame in Update(). The Job System allows us to parallelize the deformation calculation, and the Burst compiler helps us optimize the code for performance.

Specifically, the Burst compiler can significantly improve performance in situations where the same computation is performed on many elements, such as in the case of a mesh deformer, where the same deformation calculation is applied to each vertex of the mesh. By using the Burst compiler, the code is optimized for the specific platform and the job system parallelization allows for the calculation to be done in parallel for different vertices, resulting in a big performance boost.

As icing on the cake, the Burst compiler can also reduce memory overhead and power consumption which is a big benefit in mobile and other low-power devices.

This is quite a basic example, and maybe you expected more, but it should give you an idea of how to create a custom mesh deformer based on force and direction. You can modify the code to suit your needs, for example, by adding more complex forces or constraints, by using the forces over time, etc.


Here is what the final script might look like. Please note that this particular test script will just continuously apply the deformation to what you put it on, it would be ideal to trigger the deformation based on some kind of action. In this example, an object hits your player mesh and uses the force and direction to pass through this calculation:

using UnityEngine;
using Unity.Jobs;
using Unity.Burst;
using Unity.Collections;

[RequireComponent(typeof(MeshFilter))]
public class ForceDeformer : MonoBehaviour
{
    public float force;
    public Vector3 direction;
    public float impactRadius;

    [BurstCompile]
    struct DeformJob : IJobParallelFor
    {
        public float force;
        public Vector3 direction;
        public float impactRadius;
        public NativeArray<Vector3> vertices;

        public void Execute(int i)
        {
            Vector3 vertex = vertices[i];
            float distance = Vector3.Distance(vertex, transform.position);
            if (distance < impactRadius)
            {
                float effect = 1 - (distance / impactRadius);
                vertices[i] += direction * force * effect;
            }
        }
    }

    private void Update()
    {
        // Get the mesh and its vertices
        Mesh mesh = GetComponent<MeshFilter>().mesh;
        Vector3[] meshVertices = mesh.vertices;
        NativeArray<Vector3> vertices = new NativeArray<Vector3>(meshVertices, Allocator.TempJob);

        // Create and schedule the job
        DeformJob deformJob = new DeformJob
        {
            force = force,
            direction = direction,
            impactRadius = impactRadius,
            vertices = vertices
        };
        JobHandle handle = deformJob.Schedule(vertices.Length, 64);

        // Wait for the job to complete
        handle.Complete();

        // Copy the deformed vertices back to the mesh
        meshVertices = vertices.ToArray();
        mesh.vertices = meshVertices;
        mesh.RecalculateNormals();

        // Dispose of the NativeArray
        vertices.Dispose();
    }
}

Keep a look out for more posts about the unity job system and burst compiler as I navigate these new seas!

Until next time!

Previous
Previous

Unity: The Benefits of Additive Scenes