Note: this tutorial is intended for users who are familiar with Unity's IDE and who have some C # programming experience. If you do not have this knowledge, then study the tutorials Introduction to Unity UI and Introduction to Unity Scripting first .
You will need Unity version not lower than 2017.3.1. The latest version of Unity can be downloaded here . In this tutorial, the custom editor is used, for more information about them, see the tutorial Extending the Unity Editor .
Editor
class, its CustomEditor
attribute tells the Editor
class what type of object it is an editor for. OnSceneGUI()
is an event method that allows drawing in the Scene window; OnInspectorGUI()
allows you to add additional GUI elements to the Inspector.
MeshInspector
class MeshInspector
add the following:
[CustomEditor(typeof(MeshStudy))]
CustomEditor
attribute tells Unity which type of object can modify the custom editor class.
OnSceneGUI()
before EditMesh()
add the following:
mesh = target as MeshStudy; Debug.Log("Custom editor is running");
Editor
class has a standard target
variable. Here, the target
is a conversion to MeshStudy
. Now the custom editor will draw all the GameObject and MeshStudy.cs attached to it in the Scene window. Adding debug messages allows you to verify in the console that the custom editor is actually running.
MeshFilter.sharedmesh
property and assign it to the mesh filter again.
MonoBehaviour
class, and its Start()
function is not executed in Edit mode.
MeshStudy
class MeshStudy
add the following:
[ExecuteInEditMode]
Start()
function will be executed in both Play mode and Edit mode. Now we can first create an instance of the mesh object and clone it.
InitMesh()
add the following code:
oMeshFilter = GetComponent<MeshFilter>(); oMesh = oMeshFilter.sharedMesh; //1 cMesh = new Mesh(); //2 cMesh.name = "clone"; cMesh.vertices = oMesh.vertices; cMesh.triangles = oMesh.triangles; cMesh.normals = oMesh.normals; cMesh.uv = oMesh.uv; oMeshFilter.mesh = cMesh; //3 vertices = cMesh.vertices; //4 triangles = cMesh.triangles; isCloned = true; Debug.Log("Init & Cloned");
oMesh
mesh from the MeshFilter
component.cMesh
mesh cMesh
.Cube
GameObject in the Hierarchy and check its properties in the Inspector . Mesh Filter should display an asset mesh called clone . Fine! This means that we have successfully cloned the mesh.
OnInspectorGUI()
, after the second line of code, add the following:
if (GUILayout.Button("Reset")) //1 { mesh.Reset(); //2 }
Reset()
function in MeshStudy.cs .Reset()
function:
if (cMesh != null && oMesh != null) //1 { cMesh.vertices = oMesh.vertices; //2 cMesh.triangles = oMesh.triangles; cMesh.normals = oMesh.normals; cMesh.uv = oMesh.uv; oMeshFilter.mesh = cMesh; //3 vertices = cMesh.vertices; //4 triangles = cMesh.triangles; }
cMesh
to the source mesh.cMesh
oMeshFilter
.Mesh class:
- Vertices are stored as an array of
Vector3
values.- Triangles are stored as an array of integer corresponding to the indices of the array of vertices.
That is, in a simple Quad mesh, consisting of four vertices and two triangles, the mesh data will look like this:
EditMesh()
function and add the following:
handleTransform = mesh.transform; //1 handleRotation = Tools.pivotRotation == PivotRotation.Local ? handleTransform.rotation : Quaternion.identity; //2 for (int i = 0; i < mesh.vertices.Length; i++) //3 { ShowPoint(i); }
handleTransform
gets Transform values ​​from mesh
.handleRotation
gets the current hinge Rotation mode.ShowPoint()
.ShowPoint()
function, immediately after the //draw dot
comment, add the following:
Vector3 point = handleTransform.TransformPoint(mesh.vertices[index]);
if
block, immediately after the newly added line of code, add the following:
Handles.color = Color.blue; point = Handles.FreeMoveHandle(point, handleRotation, mesh.handleSize, Vector3.zero, Handles.DotHandleCap);
Handles
auxiliary class.Handles.FreeMoveHandle()
creates an unrestricted motion manipulator that simplifies the drag and drop operation, which will come in handy in the next section.ShowPoint()
function, immediately after the //drag
comment and right before the closing brackets of the if
block, add the following:
if (GUI.changed) //1 { mesh.DoAction(index, handleTransform.InverseTransformPoint(point)); //2 }
GUI.changed
tracks all changes that occur with points, and works well with Handles.FreeMoveHandle()
to recognize the drag and drop operation.mesh.DoAction()
function receives its index and Transform values ​​as parameters. Since the Transform vertex values ​​are in world space, we transform them into local space using InverseTransformPoint()
.DoAction()
, after the opening parentheses add the following:
PullOneVertex(index, localPos);
PullOneVertex()
function:
vertices[index] = newPos; //1 cMesh.vertices = vertices; //2 cMesh.RecalculateNormals(); //3
newPos
.cMesh.vertices
values ​​of the updated vertices back to cMesh.vertices
.RecalculateNormals()
recalculate and redraw the mesh to match the changes.Start()
function and find the variable vertices
. We will see the following:
[HideInInspector] public Vector3[] vertices;
[HideInInspector]
hides the shared variable from the Inspector window.
//[HideInInspector] public Vector3[] vertices;
Note: Hiding vertex values ​​helps [HideInInspector]
in the case of more complex 3D meshes. Since the size of the array of vertices can reach thousands of elements, this can lead to Unity inhibition when trying to view the value of the array in the Inspector.
Vector3
array of Vector3
elements.
[HideInInspector]
.
DoAction()
function with the following:
PullSimilarVertices(index, localPos);
PullSimilarVertices()
function and add the following:
Vector3 targetVertexPos = vertices[index]; //1 List<int> relatedVertices = FindRelatedVertices(targetVertexPos, false); //2 foreach (int i in relatedVertices) //3 { vertices[i] = newPos; } cMesh.vertices = vertices; //4 cMesh.RecalculateNormals();
FindRelatedVertices()
method.newPos
.cMesh.vertices
updated vertices
back to cMesh.vertices
. Then call RecalculateNormals()
to redraw the mesh with the new values.ShowHandle()
function, add the following inside the if
block:
Handles.color = Color.blue; if (Handles.Button(point, handleRotation, mesh.pickSize, mesh.pickSize, Handles.DotHandleCap)) //1 { mesh.selectedIndices.Add(index); //2 }
Handles.Button
.mesh.selectedIndices
.OnInspectorGUI()
, before the closing bracket, add the following:
if (GUILayout.Button("Clear Selected Vertices")) { mesh.ClearAllData(); }
mesh.ClearAllData()
.
ClearAllData()
function, add the following:
selectedIndices = new List<int>(); targetIndex = 0; targetVertex = Vector3.zero;
selectedIndices
and targetIndex
. It also resets targetVertex
.
mVertices
.mVertices
calculations and change values ​​in mVertices
.mVertices
as they change at each stage and let Unity automatically calculate the normals.Start()
function:
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;
selectedIndices
list.Init()
function, before the if
block, add the following:
currentIndex = 0;
currentIndex
assigned the value 0 - the first index of the selectedIndices
list.
Init()
function, before the closing bracket of the else
block, add the following:
StartDisplacement();
StartDisplacement()
function if isEditMode
is false.
StartDisplacement()
function, add the following:
targetVertex = oVertices[selectedIndices[currentIndex]]; //1 starttime = Time.time; //2 isAnimate = true;
targetVertex
to start the animation.isAnimate
value to true.StartDisplacement()
function, we will create a FixedUpdate()
function with the following code:
void FixedUpdate() //1 { if (!isAnimate) //2 { return; } runtime = Time.time - starttime; //3 if (runtime < duration) //4 { Vector3 targetVertexPos = oFilter.transform.InverseTransformPoint(targetVertex); DisplaceVertices(targetVertexPos, pullvalue, radiusofeffect); } else //5 { currentIndex++; if (currentIndex < selectedIndices.Count) //6 { StartDisplacement(); } else //7 { oMesh = GetComponent<MeshFilter>().mesh; isAnimate = false; isMeshReady = true; } } }
FixedUpdate()
function is executed in a loop with a fixed FPS.isAnimate
is false, then skip the following code.runtime
animation.runtime
is within the duration
, then we get the world coordinates of targetVertex
and DisplaceVertices()
, covering the target vertex with the parameters pullvalue
and radiusofeffect
.currentIndex
unit.currentIndex
among selectedIndices
. Go to the next vertex in the list using StartDisplacement()
.oMesh
data to the current mesh and isAnimate
to false to stop the animation.DisplaceVertices()
:
Vector3 currentVertexPos = Vector3.zero; float sqrRadius = radius * radius; //1 for (int i = 0; i < mVertices.Length; i++) //2 { currentVertexPos = mVertices[i]; float sqrMagnitute = (currentVertexPos - targetVertexPos).sqrMagnitude; //3 if (sqrMagnitute > sqrRadius) { continue; //4 } float distance = Mathf.Sqrt(sqrMagnitute); //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); mVertices[i] = m.MultiplyPoint3x4(currentVertexPos); } oMesh.vertices = mVertices; //7 oMesh.RecalculateNormals();
sqrMagnitude
between currentVertexPos
and targetVertexPos
.sqrMagnitude
exceeds sqrRadius
, then go to the next vertex.falloff
value, depending on the distance
current vertex from the central point of the area of ​​action.Vector3
new position of Vector3
and apply its Transform to the current vertex.mVertices
values ​​to the mVertices
, and force Unity to recalculate the normals.Falloff source
The original formula is taken from the Procedural Examples asset package file, which can be downloaded for free from the Unity Asset Store.
OnInspectorGUI()
function, add the following before the closing bracket:
if (!mesh.isEditMode && mesh.isMeshReady) { string path = "Assets/Prefabs/CustomHeart.prefab"; //1 if (GUILayout.Button("Save Mesh")) { mesh.isMeshReady = false; Object pfObj = AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)); //2 Object pfRef = AssetDatabase.LoadAssetAtPath (path, typeof(GameObject)); GameObject gameObj = (GameObject)PrefabUtility.InstantiatePrefab(pfObj); Mesh pfMesh = (Mesh)AssetDatabase.LoadAssetAtPath(path, typeof(Mesh)); //3 if (!pfMesh) { pfMesh = new Mesh(); } else { pfMesh.Clear(); } pfMesh = mesh.SaveMesh(); //4 AssetDatabase.AddObjectToAsset(pfMesh, path); gameObj.GetComponentInChildren<MeshFilter>().mesh = pfMesh; //5 PrefabUtility.ReplacePrefab(gameObj, pfRef, ReplacePrefabOptions.Default); //6 Object.DestroyImmediate(gameObj); //7 } }
path
value of the CustomHeart prefab object path .pfObj
), the second as links ( pfRef
).pfMesh
. If not found, creates a new mesh, otherwise clears the existing data.pfMesh
with new mesh data, and then adds it as an asset to CustomHeart .
gameObj
with the gameObj
value.gameObj
matching pre-existing connections.gameObj
.SaveMesh()
method, after creating an instance of nMesh
add the following:
nMesh.name = "HeartMesh"; nMesh.vertices = oMesh.vertices; nMesh.triangles = oMesh.triangles; nMesh.normals = oMesh.normals;
DisplaceVertices()
function used the Falloff formula to determine the drag force that was applied to each vertex within a given radius. The “fall off” point at which the drag force begins to decrease depends on the type of falloff used: Linear, Gaussian, or Needle. Each type creates different results in the mesh.
Start()
Add the following before the function :
public enum CurveType { Curve1, Curve2 } public CurveType curveType; Curve curve;
CurveType
, after which it is made available from the Inspector .
CurveType1()
and add the following:
Vector3[] curvepoints = new Vector3[3]; //1 curvepoints[0] = new Vector3(0, 1, 0); curvepoints[1] = new Vector3(0.5f, 0.5f, 0); curvepoints[2] = new Vector3(1, 0, 0); curve = new Curve(curvepoints[0], curvepoints[1], curvepoints[2], false); //2
Curve()
and we assign its values curve
. The drawn curve can be displayed in the preview, if the last parameter is true.CurveType2()
and add the following:
Vector3[] curvepoints = new Vector3[3]; //1 curvepoints[0] = new Vector3(0, 0, 0); curvepoints[1] = new Vector3(0.5f, 1, 0); curvepoints[2] = new Vector3(1, 0, 0); curve = new Curve(curvepoints[0], curvepoints[1], curvepoints[2], false); //2
Curve()
and assign its values curve
. The drawn curve can be displayed in the preview, if the last parameter is true.StartDisplacement()
, before the closing bracket add the following:
if (curveType == CurveType.Curve1) { CurveType1(); } else if (curveType == CurveType.Curve2) { CurveType2(); }
curveType
and generate it accordingly curve
.
DisplaceVertices()
, inside the cycle operator, for
before the closing brackets add the following:
float increment = curve.GetPoint(distance).y * force; //1 Vector3 translate = (vert * increment) * Time.deltaTime; //2 Quaternion rotation = Quaternion.Euler(translate); Matrix4x4 m = Matrix4x4.TRS(translate, rotation, Vector3.one); mVertices[i] = m.MultiplyPoint3x4(mVertices[i]);
distance
and multiply its value y
by force
to get increment
.Vector3
to store the new position of the current vertex and apply its Transform accordingly.Source: https://habr.com/ru/post/428796/