
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
 Vector3values.- 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(); } curveTypeand generate it accordingly curve.DisplaceVertices(), inside the cycle operator, forbefore 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]); distanceand multiply its value yby forceto get increment.Vector3to store the new position of the current vertex and apply its Transform accordingly.




Source: https://habr.com/ru/post/428796/
All Articles