Runtime Mesh Manipulation With Unity
One of the benefits of using Unity as your game development platform is its powerful 3D engine. In this tutorial, you’ll get an introduction to the world of 3D objects and mesh manipulation. By Sean Duffy.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Runtime Mesh Manipulation With Unity
35 mins
- Getting Started
- Understanding Meshes
- Setting Up the Project
- Poking and Prodding Meshes With a Custom Editor
- Customizing the Editor Script
- Cloning a Mesh
- Resetting a Mesh
- Understanding Vertices and Triangles With Unity
- Visualizing Vertices
- Moving a Single Vertex
- Looking at the Vertices Array
- Finding All Similar Vertices
- Manipulating Meshes
- Collecting the Selected Indices
- Deforming the Sphere Into a Heart Shape
- Making the Vertices Move Smoothly
- Saving Your Mesh in Real Time
- Putting It All Together
- Using the Curve Method
- Where to Go From Here?
Deforming the Sphere Into a Heart Shape
Updating mesh vertices in real time requires three steps:
- Copy the current mesh vertices (before animation) to
modifiedVertices
. - Do calculations and update values on
modifiedVertices
. - Copy
modifiedVertices
to the current mesh on every step change and get Unity to redraw the mesh.
Go to HeartMesh.cs and add the following variables before Start
:
public float radiusOfEffect = 0.3f; //1
public float pullValue = 0.3f; //2
public float duration = 1.2f; //3
int currentIndex = 0; //4
bool isAnimate = false;
float startTime = 0f;
float runTime = 0f;
Moving a vertex should have some influence on the vertices around it to maintain a smooth shape. These variables control that effect.
- Radius of area affected by the targeted vertex.
- The strength of the pull.
- How long the animation will run.
- Current index of the
selectedIndices
list.
In Init
, before the if
statement, add:
currentIndex = 0;
This sets currentIndex
— the first index of the selectedIndices
list — to 0 at the beginning of the game.
Still in Init
, before the closing braces of the else
statement, add:
StartDisplacement();
StartDisplacement
is what actually moves the vertices. It only runs when isEditMode
is false.
Right now, this method does nothing, so add the following to StartDisplacement
:
targetVertex = originalVertices[selectedIndices[currentIndex]]; //1
startTime = Time.time; //2
isAnimate = true;
- Singles out the
targetVertex
from theoriginalVertices
array to start the animation. Remember, each array item is a List of integer values. - Sets the start time to the current time and changes
isAnimate
totrue
.
After StartDisplacement
, create a new method called FixedUpdate
with the following code:
protected void FixedUpdate() //1
{
if (!isAnimate) //2
{
return;
}
runTime = Time.time - startTime; //3
if (runTime < duration) //4
{
Vector3 targetVertexPos =
meshFilter.transform.InverseTransformPoint(targetVertex);
DisplaceVertices(targetVertexPos, pullValue, radiusOfEffect);
}
else //5
{
currentIndex++;
if (currentIndex < selectedIndices.Count) //6
{
StartDisplacement();
}
else //7
{
originalMesh = GetComponent<MeshFilter>().mesh;
isAnimate = false;
isMeshReady = true;
}
}
}
Here's what the code is doing:
- The
FixedUpdate
method runs on a fixed interval, meaning that it's frame rate independent. Read more about it here. - If
isAnimate
is false, it won't do anything. - Keeps track of how long the animation has been running.
- If the animation hasn't been running too long, it continues the animation by getting the world space coordinates of
targetVertex
and callingDisplaceVertices
. - Otherwise, time's up! Adds one to
currentIndex
to start processing the animation for the next selected vertex. - Checks if all the selected vertices have been processed. If not, calls
StartDisplacement
with the latest vertex. - Otherwise, you've reached the end of the list of selected vertices. This line makes a copy of the current mesh and sets
isAnimate
tofalse
to stop the animation.
Making the Vertices Move Smoothly
In DisplaceVertices
, add the following:
Vector3 currentVertexPos = Vector3.zero;
float sqrRadius = radius * radius; //1
for (int i = 0; i < modifiedVertices.Length; i++) //2
{
currentVertexPos = modifiedVertices[i];
float sqrMagnitude = (currentVertexPos - targetVertexPos).sqrMagnitude; //3
if (sqrMagnitude > sqrRadius)
{
continue; //4
}
float distance = Mathf.Sqrt(sqrMagnitude); //5
float falloff = GaussFalloff(distance, radius);
Vector3 translate = (currentVertexPos * force) * falloff; //6
translate.z = 0f;
Quaternion rotation = Quaternion.Euler(translate);
Matrix4x4 m = Matrix4x4.TRS(translate, rotation, Vector3.one);
modifiedVertices[i] = m.MultiplyPoint3x4(currentVertexPos);
}
originalMesh.vertices = modifiedVertices; //7
originalMesh.RecalculateNormals();
This code loops over every vertex in the mesh and displaces those that are close to the ones you selected in the editor. It does some math tricks to create a smooth, organic effect, like pushing your thumb into clay. You'll learn more about this later on.
Here's a closer look at what this code does:
- Gets the square of the radius.
- Loops through every vertex in the mesh.
- Finds the distance between the current vertex and the target vertex and squares it.
- If this vertex is outside the area of effect, exits the loop early and continues to the next vertex.
- Otherwise, calculates the
falloff
value based on the distance. Gaussian functions create a smooth bell curve. - Calculates how far to move based on distance, then sets the rotation (direction of displacement) based on the result. This makes the vertex move "outward", that is, directly away from the targetVertex, making it seem to puff out from the center.
- On exiting the loop, stores the updated
modifiedVertices
in the original mesh and has Unity recalculate the normals.
Save your file and return to Unity. Select the Sphere, go to the HeartMesh component, and try adding some vertices into your Selected Indices property. Turn off Is Edit mode and press Play to preview your work.
Play around with the Radius Of Effect, Pull Value and Duration settings to see different results. When you are ready, update the settings per the screenshot below.
Press Play. Did your sphere balloon into a heart shape?
Congratulations! In the next section, you will learn how to save the mesh for further use.
Saving Your Mesh in Real Time
Right now, your heart comes and goes whenever you push the Play button. If you want a love that lasts, you need a way to write the mesh to a file.
A simple way is to set up a placeholder prefab that has a 3D object as its child, then replace its mesh asset with your heart (...er, your Heart mesh) via a script.
In the Project view, find Prefabs/CustomHeart. Double-click the prefab to open it in Prefab Editing mode.
Click on the Arrow icon to expand its contents in the Hierarchy and select Child. This is where you'll store your generated mesh.
Exit prefab editing mode and open HeartMeshInspector.cs. At the end of OnInspectorGUI
, before the closing braces, add the following:
if (!mesh.isEditMode && mesh.isMeshReady)
{
string path = "Assets/RW/Prefabs/CustomHeart.prefab"; //1
if (GUILayout.Button("Save Mesh"))
{
mesh.isMeshReady = false;
Object prefabToInstantiate =
AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)); //2
Object referencePrefab =
AssetDatabase.LoadAssetAtPath (path, typeof(GameObject));
GameObject gameObj =
(GameObject)PrefabUtility.InstantiatePrefab(prefabToInstantiate);
Mesh prefabMesh = (Mesh)AssetDatabase.LoadAssetAtPath(path,
typeof(Mesh)); //3
if (!prefabMesh)
{
prefabMesh = new Mesh();
AssetDatabase.AddObjectToAsset(prefabMesh, path);
}
else
{
prefabMesh.Clear();
}
prefabMesh = mesh.SaveMesh(prefabMesh); //4
AssetDatabase.AddObjectToAsset(prefabMesh, path);
gameObj.GetComponentInChildren<MeshFilter>().mesh = prefabMesh; //5
PrefabUtility.SaveAsPrefabAsset(gameObj, path); //6
Object.DestroyImmediate(gameObj); //7
}
}
Here's what the code does:
- Stores the CustomHeart prefab object asset path, which you need to be able to write to the file.
- Creates two objects from the CustomHeart prefab, one as a GameObject and the other as a reference.
- Creates an instance of the mesh asset
prefabMesh
from CustomHeart. If it finds the asset, it clears its data; otherwise, it creates a new empty mesh. - Updates
prefabMesh
with new mesh data and associates it with the CustomHeart asset. - Updates the GameObject's mesh asset with
prefabMesh
. - Creates a Prefab Asset at the given path from the given
gameObj
, including any children in the scene. This replaces whatever was in the CustomHeart prefab. - Destroys
gameObj
immediately.
Save your file and go to HeartMesh.cs. Replace the body of SaveMesh
with the following:
Mesh nMesh = new Mesh();
nMesh.name = "HeartMesh";
nMesh.vertices = originalMesh.vertices;
nMesh.triangles = originalMesh.triangles;
nMesh.normals = originalMesh.normals;
return nMesh;
This will return a mesh asset based on the heart-shaped mesh.
Save the file and return to Unity. Press Play. When the animation ends, a Save Mesh button will appear in the Inspector. Click on the button to save your new mesh, then stop the player.
Find Prefabs/CustomHeart again in the Project view and open it in Prefab Editing mode. You will see a brand spanking new heart-shaped mesh has been saved in your prefab!