├── .gitignore ├── LICENSE.meta ├── README.md.meta ├── package.json.meta ├── Editor.meta ├── Runtime.meta ├── Runtime ├── MeshSplit.asmdef.meta ├── Scripts.meta ├── Utilities.meta ├── Scripts │ ├── MeshSplitter.cs.meta │ ├── SubMeshBuilder.cs.meta │ ├── MeshSplitController.cs.meta │ ├── MeshSplitParameters.cs.meta │ ├── MeshSplitParameters.cs │ ├── MeshSplitter.cs │ ├── SubMeshBuilder.cs │ └── MeshSplitController.cs ├── Utilities │ ├── PerformanceMonitor.cs.meta │ ├── ConversionUtilities.cs.meta │ ├── ConversionUtilities.cs │ └── PerformanceMonitor.cs └── MeshSplit.asmdef ├── Editor ├── MeshSplitEditor.asmdef.meta ├── MeshSplitControllerEditor.cs.meta ├── MeshSplitEditor.asmdef └── MeshSplitControllerEditor.cs ├── .gitattributes ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 632262a67db66a74d823cc57e87dc238 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b38e0542cc6fcdf4bac9b0ae8c6c5959 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0602cdf7a283a084fa34727634981a94 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 703b8cec7750ab44dab18edbd7f48749 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc424e4c7eea79047980f66dd5b43da8 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/MeshSplit.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 57cbbb73bd33f464883a7c0c5e75f932 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/MeshSplitEditor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 79233c6acfee5624fad537e19c915e5b 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ba59817bfbfe1fc4fae232b6497837a1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Utilities.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 568b202dd4420a44384194edaed81856 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Scripts/MeshSplitter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8ba6735a6b2724a4fa52b090fcab2f62 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Scripts/SubMeshBuilder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5f3ef02d1588fc64999c7e241b1adeb9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/MeshSplitControllerEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e66172d8e9579cf4ea800e389032a4e4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Scripts/MeshSplitController.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f8e4f077f4c48f647bbdb7fd286caf26 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Scripts/MeshSplitParameters.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 09de4834169eafa449b0261bb54f6c62 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Utilities/PerformanceMonitor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0398e76c5ebad74fb1106fb2d4ad77d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Utilities/ConversionUtilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2a9e76c2dc5fdae42934c1069b91a19b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /Editor/MeshSplitEditor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MeshSplitEditor", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:57cbbb73bd33f464883a7c0c5e75f932" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [], 17 | "noEngineReferences": false 18 | } -------------------------------------------------------------------------------- /Runtime/Utilities/ConversionUtilities.cs: -------------------------------------------------------------------------------- 1 | using Unity.Mathematics; 2 | using UnityEngine; 3 | 4 | namespace MeshSplit.Scripts.Utilities 5 | { 6 | public static class ConversionUtilities 7 | { 8 | public static half4 ToHalf4(this Vector2 v) 9 | { 10 | return new half4((half)v.x, (half)v.y, half.zero, half.zero); 11 | } 12 | 13 | public static float4 ToFloat4(this Vector3 v) 14 | { 15 | return new float4(v.x, v.y, v.z, 0); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.artnas.meshsplit", 3 | "displayName": "Mesh Split", 4 | "author": { 5 | "name": "Artur Nasiadko", 6 | "email": "Art.Nasiadko@gmail.com", 7 | "url": "https://github.com/artnas" 8 | }, 9 | "version": "1.3.1", 10 | "unity": "2021.2", 11 | "hideInEditor": false, 12 | "description": "Split meshes into chunks of a given size", 13 | "documentationUrl": "https://github.com/artnas/Unity-Plane-Mesh-Splitter", 14 | "dependencies": { 15 | "com.unity.burst": "1.5.1", 16 | "com.unity.collections": "1.1.0", 17 | "com.unity.mathematics": "1.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Runtime/Scripts/MeshSplitParameters.cs: -------------------------------------------------------------------------------- 1 | /* https://github.com/artnas/Unity-Plane-Mesh-Splitter */ 2 | 3 | using System; 4 | using Unity.Mathematics; 5 | using UnityEngine; 6 | 7 | namespace MeshSplit.Scripts 8 | { 9 | [Serializable] 10 | public class MeshSplitParameters 11 | { 12 | [Range(0.1f, 256)] 13 | public float GridSize = 16; 14 | public bool3 SplitAxes = new bool3(true, true, true); 15 | 16 | [Header("Parent attributes.")] 17 | public bool UseParentLayer = true; 18 | public bool UseParentStaticFlag = true; 19 | public bool UseParentMeshRendererSettings = true; 20 | 21 | [Header("Collisions.")] 22 | public bool GenerateColliders; 23 | public bool UseConvexColliders; 24 | } 25 | } -------------------------------------------------------------------------------- /Editor/MeshSplitControllerEditor.cs: -------------------------------------------------------------------------------- 1 | /* https://github.com/artnas/Unity-Plane-Mesh-Splitter */ 2 | 3 | using MeshSplit.Scripts; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace MeshSplit.Editor 8 | { 9 | [CustomEditor(typeof(MeshSplitController))] 10 | public class MeshSplitControllerEditor : UnityEditor.Editor 11 | { 12 | public override void OnInspectorGUI() 13 | { 14 | MeshSplitController instance = (MeshSplitController)target; 15 | 16 | DrawDefaultInspector(); 17 | 18 | if (GUILayout.Button("Create submeshes")) 19 | { 20 | Undo.RecordObject(instance, "Created submeshes"); 21 | instance.Split(); 22 | } 23 | 24 | if (GUILayout.Button("Clear submeshes")) 25 | { 26 | Undo.RecordObject(instance, "Cleared submeshes"); 27 | instance.Clear(); 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Runtime/MeshSplit.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MeshSplit", 3 | "rootNamespace": "", 4 | "references": [ 5 | "Unity.Mathematics", 6 | "Unity.Collections", 7 | "Unity.Burst" 8 | ], 9 | "includePlatforms": [], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": true, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [ 17 | { 18 | "name": "com.unity.burst", 19 | "expression": "1.5.1", 20 | "define": "Unity.Burst" 21 | }, 22 | { 23 | "name": "com.unity.collections", 24 | "expression": "1.1.0", 25 | "define": "Unity.Collections" 26 | }, 27 | { 28 | "name": "com.unity.mathematics", 29 | "expression": "1.2.0", 30 | "define": "Unity.Mathematics" 31 | } 32 | ], 33 | "noEngineReferences": false 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Artur Nasiadko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Runtime/Utilities/PerformanceMonitor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace MeshSplit.Scripts.Utilities 5 | { 6 | public static class PerformanceMonitor 7 | { 8 | private class Entry 9 | { 10 | public Stopwatch Stopwatch; 11 | public float TimeThreshold; 12 | } 13 | 14 | private static readonly Dictionary _stopwatches = new Dictionary(); 15 | 16 | public static void Start(string textIdentifier, float timeThreshold = 0f) 17 | { 18 | if (_stopwatches.ContainsKey(textIdentifier)) 19 | { 20 | _stopwatches[textIdentifier].Stopwatch.Restart(); 21 | } 22 | else 23 | { 24 | var stopWatch = new Stopwatch(); 25 | 26 | _stopwatches.Add(textIdentifier, new Entry 27 | { 28 | Stopwatch = stopWatch, 29 | TimeThreshold = timeThreshold 30 | }); 31 | 32 | stopWatch.Start(); 33 | } 34 | } 35 | 36 | public static void Stop(string textIdentifier, string additionalText = null) 37 | { 38 | if (_stopwatches.TryGetValue(textIdentifier, out var entry)) 39 | { 40 | var stopwatch = entry.Stopwatch; 41 | stopwatch.Stop(); 42 | 43 | var milliseconds = stopwatch.Elapsed.TotalMilliseconds; 44 | 45 | if (entry.TimeThreshold == 0 || milliseconds >= (entry.TimeThreshold * 1000)) 46 | { 47 | UnityEngine.Debug.Log($"{textIdentifier} {additionalText}\n\ttime: \t{milliseconds:n2} ms"); 48 | } 49 | 50 | _stopwatches.Remove(textIdentifier); 51 | } 52 | } 53 | 54 | public static void Stop(string textIdentifier, out double milliseconds) 55 | { 56 | if (_stopwatches.TryGetValue(textIdentifier, out var entry)) 57 | { 58 | var stopwatch = entry.Stopwatch; 59 | stopwatch.Stop(); 60 | 61 | milliseconds = stopwatch.Elapsed.TotalMilliseconds; 62 | 63 | _stopwatches.Remove(textIdentifier); 64 | } 65 | else 66 | { 67 | milliseconds = 0; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Plane Mesh Splitter 2 | 3 | ### What is the purpose of this tool? 4 | 5 | Say you have a gigantic terrain in a single mesh. Unity is going to process the entire mesh when rendering it even though only a small section in front of the camera is visible. 6 | 7 | This tool lets your split this large mesh into smaller submeshes which should greatly improve the performance thanks to the built-in Unity frustum culling (only visible meshes will be rendered). 8 | 9 | ### Features 10 | 11 | - Simple to use 12 | - Customization: 13 | - Grid size 14 | - Multiple axes (in any combination) 15 | - Generate convex/non convex colliders 16 | - Nice performance 17 | - Parallelized burst code 18 | - Pointers and memcpy 19 | - Maintains exact original mesh vertex data format 20 | - Automatic 16/32 bit indexing based on vertex count 21 | - Doesn't modify the existing mesh. 22 | - Can be used both in editor and at runtime. 23 | - Submeshes persist when saving the scene. 24 | 25 | ## Description 26 | 27 | A tool which lets you split any mesh into smaller submeshes. At first it was designed to work with imported Tiled2Unity terrains, but I rewrote it to work with everything you can throw at it. 28 | 29 | In 2022 I rewrote it again to improve it and solve some problems. I used the new mesh building API and simplified the vertex data copying process - it now copies data directly from the meshes vertex data buffer, retaining the same vertex data structure as the original mesh. Burst allowed me to improve performance and spread work across multiple threads. 30 | 31 | ## Installation 32 | 33 | Install this tool using Unity Package Manager: 34 | https://github.com/artnas/Unity-Plane-Mesh-Splitter.git 35 | 36 | ![image](https://user-images.githubusercontent.com/14143603/194191506-c25bcf37-284c-471f-8097-7e6049f7ed31.png) 37 | 38 | ![image](https://user-images.githubusercontent.com/14143603/194191568-492678bb-00b3-4cab-9b5a-507bb20e202f.png) 39 | 40 | for versions compatible with older versions of unity go to [Releases](https://github.com/artnas/Unity-Plane-Mesh-Splitter/releases) 41 | 42 | ### Compability 43 | 44 | - Release version 1.0 is compatible with older versions of Unity. 45 | - New releases work with Unity 2021.2 and above 46 | 47 | ## Usage 48 | 49 | ### MeshSplitController component 50 | 51 | Add the "MeshSplitController" component to the game object you want to split and press the "Create submeshes" button. Press "Clear submeshes" to revert. 52 | 53 | ### API 54 | 55 | ```csharp 56 | // mesh to split 57 | Mesh mesh; 58 | 59 | // create a mesh splitter with some parameters (see MeshSplitParameters.cs for default settings) 60 | var meshSplitter = new MeshSplitter(new MeshSplitParameters 61 | { 62 | GridSize = 32, 63 | GenerateColliders = true 64 | }); 65 | 66 | // split mesh into submeshes assigned to points 67 | var subMeshes = meshSplitter.Split(mesh); 68 | ``` 69 | 70 | ![alt tag](http://i.imgur.com/5PzoVFc.jpg) 71 | 72 | # TODO 73 | 74 | - Support for submeshes with various materials 75 | - Align the gizmo grid with split planes 76 | -------------------------------------------------------------------------------- /Runtime/Scripts/MeshSplitter.cs: -------------------------------------------------------------------------------- 1 | /* https://github.com/artnas/Unity-Plane-Mesh-Splitter */ 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using MeshSplit.Scripts.Utilities; 7 | using UnityEngine; 8 | using UnityEngine.Rendering; 9 | 10 | namespace MeshSplit.Scripts 11 | { 12 | public class MeshSplitter 13 | { 14 | private static readonly MeshUpdateFlags MeshUpdateFlags = MeshUpdateFlags.DontNotifyMeshUsers 15 | | MeshUpdateFlags.DontValidateIndices 16 | | MeshUpdateFlags.DontRecalculateBounds 17 | | MeshUpdateFlags.DontResetBoneBounds; 18 | 19 | private readonly MeshSplitParameters _parameters; 20 | private Mesh _sourceMesh; 21 | private readonly bool _verbose; 22 | 23 | private Dictionary> _pointIndicesMap; 24 | 25 | private byte[] _vertexData; 26 | private VertexAttributeDescriptor[] _sourceMeshVertexAttributes; 27 | 28 | public MeshSplitter(MeshSplitParameters parameters, bool verbose) 29 | { 30 | _parameters = parameters; 31 | _verbose = verbose; 32 | } 33 | 34 | public List<(Vector3Int gridPoint, Mesh mesh)> Split(Mesh mesh) 35 | { 36 | SetMesh(mesh); 37 | 38 | if (_verbose) PerformanceMonitor.Start("CreatePointIndicesMap"); 39 | CreatePointIndicesMap(); 40 | if (_verbose) PerformanceMonitor.Stop("CreatePointIndicesMap"); 41 | 42 | if (_verbose) PerformanceMonitor.Start("CreateChildMeshes"); 43 | var childMeshes = CreateChildMeshes(); 44 | if (_verbose) PerformanceMonitor.Stop("CreateChildMeshes"); 45 | 46 | return childMeshes; 47 | } 48 | 49 | private void SetMesh(Mesh mesh) 50 | { 51 | _sourceMesh = mesh; 52 | 53 | // get raw mesh vertex data 54 | var buffer = _sourceMesh.GetVertexBuffer(0); 55 | _vertexData = new byte[_sourceMesh.GetVertexBufferStride(0) * _sourceMesh.vertexCount]; 56 | buffer.GetData(_vertexData); 57 | buffer.Dispose(); 58 | 59 | // get mesh vertex attributes 60 | _sourceMeshVertexAttributes = _sourceMesh.GetVertexAttributes(); 61 | } 62 | 63 | private void CreatePointIndicesMap() 64 | { 65 | // Create a list of triangle indices from our mesh for every grid node 66 | _pointIndicesMap = new Dictionary>(); 67 | 68 | var meshIndices = _sourceMesh.triangles; 69 | var meshVertices = _sourceMesh.vertices; 70 | 71 | for (var i = 0; i < meshIndices.Length; i += 3) 72 | { 73 | // middle of the current triangle (average of its 3 verts). 74 | var triangleCenter = (meshVertices[meshIndices[i]] + meshVertices[meshIndices[i + 1]] + meshVertices[meshIndices[i + 2]]) / 3; 75 | 76 | // calculate coordinates of the closest grid node. 77 | // ignore an axis (set it to 0) if its not enabled 78 | var gridPos = new Vector3Int( 79 | _parameters.SplitAxes.x ? Mathf.FloorToInt(Mathf.Floor(triangleCenter.x / _parameters.GridSize) * _parameters.GridSize * MeshSplitController.GridSizeMultiplier) : 0, 80 | _parameters.SplitAxes.y ? Mathf.FloorToInt(Mathf.Floor(triangleCenter.y / _parameters.GridSize) * _parameters.GridSize * MeshSplitController.GridSizeMultiplier) : 0, 81 | _parameters.SplitAxes.z ? Mathf.FloorToInt(Mathf.Floor(triangleCenter.z / _parameters.GridSize) * _parameters.GridSize * MeshSplitController.GridSizeMultiplier) : 0 82 | ); 83 | 84 | if (!_pointIndicesMap.TryGetValue(gridPos, out var indicesList)) 85 | { 86 | indicesList = new List(); 87 | _pointIndicesMap.TryAdd(gridPos, indicesList); 88 | } 89 | 90 | // add these triangle indices to the list 91 | indicesList.Add(meshIndices[i]); 92 | indicesList.Add(meshIndices[i + 1]); 93 | indicesList.Add(meshIndices[i + 2]); 94 | } 95 | } 96 | 97 | private List<(Vector3Int gridPoint, Mesh mesh)> CreateChildMeshes() 98 | { 99 | var subMeshBuilder = new SubMeshBuilder(_pointIndicesMap, _vertexData, _sourceMesh.GetVertexBufferStride(0), _sourceMeshVertexAttributes); 100 | var meshDataArray = subMeshBuilder.Build(_sourceMesh); 101 | 102 | var meshes = new List(meshDataArray.Length); 103 | var gridPoints = _pointIndicesMap.Keys.ToArray(); 104 | 105 | // create a new mesh for each grid point 106 | foreach (var gridPoint in gridPoints) 107 | { 108 | meshes.Add(new Mesh 109 | { 110 | name = $"SubMesh {gridPoint}", 111 | }); 112 | } 113 | 114 | // write each mesh data to its corresponding mesh 115 | Mesh.ApplyAndDisposeWritableMeshData(meshDataArray, meshes, MeshUpdateFlags); 116 | 117 | // recalculate bounds 118 | foreach (var mesh in meshes) 119 | { 120 | mesh.RecalculateBounds(MeshUpdateFlags); 121 | } 122 | 123 | return new List<(Vector3Int gridPoint, Mesh mesh)>(gridPoints.Zip(meshes, (point, mesh) => (point, mesh))); 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /Runtime/Scripts/SubMeshBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Unity.Burst; 5 | using Unity.Collections; 6 | using Unity.Collections.LowLevel.Unsafe; 7 | using Unity.Jobs; 8 | using Unity.Mathematics; 9 | using UnityEngine; 10 | using UnityEngine.Rendering; 11 | 12 | namespace MeshSplit.Scripts 13 | { 14 | public class SubMeshBuilder 15 | { 16 | private readonly Dictionary> _pointIndices; 17 | private readonly byte[] _vertexData; 18 | private readonly int _vertexBufferStride; 19 | private readonly VertexAttributeDescriptor[] _vertexAttributeDescriptors; 20 | 21 | public SubMeshBuilder(Dictionary> pointIndices, byte[] vertexData, int vertexBufferStride, VertexAttributeDescriptor[] vertexAttributeDescriptors) 22 | { 23 | _pointIndices = pointIndices; 24 | _vertexData = vertexData; 25 | _vertexBufferStride = vertexBufferStride; 26 | _vertexAttributeDescriptors = vertexAttributeDescriptors; 27 | } 28 | 29 | private (NativeList allIndices, NativeList indexRangesArray) FlattenPointIndices() 30 | { 31 | var allIndices = new NativeList(100, Allocator.Persistent); 32 | var ranges = new NativeList(100, Allocator.Persistent); 33 | 34 | foreach (var entry in _pointIndices) 35 | { 36 | var gridPointIndices = new NativeArray(entry.Value.ToArray(), Allocator.Temp); 37 | 38 | ranges.Add(new int2(allIndices.Length, gridPointIndices.Length)); 39 | 40 | allIndices.AddRange(gridPointIndices); 41 | 42 | gridPointIndices.Dispose(); 43 | } 44 | 45 | return (allIndices, ranges); 46 | } 47 | 48 | public Mesh.MeshDataArray Build(Mesh mesh) 49 | { 50 | var gridPoints = new NativeArray(_pointIndices.Keys.ToArray(), Allocator.TempJob); 51 | 52 | (NativeList allIndices, NativeList indexRangesArray) = FlattenPointIndices(); 53 | 54 | var meshDataArray = Mesh.AllocateWritableMeshData(_pointIndices.Count); 55 | 56 | var sourceMeshDataArray = Mesh.AcquireReadOnlyMeshData(mesh); 57 | 58 | var vertexData = new NativeArray(_vertexData, Allocator.TempJob); 59 | var vertexAttributes = 60 | new NativeArray(_vertexAttributeDescriptors, Allocator.TempJob); 61 | 62 | JobHandle? jobHandle = null; 63 | 64 | var workSlice = sourceMeshDataArray.Length / (Mathf.Clamp(Environment.ProcessorCount, 1, 8)); 65 | 66 | for (var i = 0; i < sourceMeshDataArray.Length; i++) 67 | { 68 | var buildJob = new BuildSubMeshJob() 69 | { 70 | AllIndices = allIndices, 71 | IndexRanges = indexRangesArray, 72 | VertexData = vertexData, 73 | VertexStride = _vertexBufferStride, 74 | VertexAttributeDescriptors = vertexAttributes, 75 | SourceSubMeshIndex = i, 76 | TargetMeshDataArray = meshDataArray 77 | }; 78 | 79 | // schedule job 80 | jobHandle = jobHandle.HasValue 81 | ? buildJob.Schedule(gridPoints.Length, workSlice, jobHandle.Value) 82 | : buildJob.Schedule(gridPoints.Length, workSlice); 83 | } 84 | 85 | jobHandle?.Complete(); 86 | 87 | // dispose 88 | vertexData.Dispose(); 89 | vertexAttributes.Dispose(); 90 | allIndices.Dispose(); 91 | indexRangesArray.Dispose(); 92 | gridPoints.Dispose(); 93 | sourceMeshDataArray.Dispose(); 94 | 95 | return meshDataArray; 96 | } 97 | 98 | [BurstCompile] 99 | private unsafe struct BuildSubMeshJob : IJobParallelFor 100 | { 101 | private static readonly MeshUpdateFlags MeshUpdateFlags = MeshUpdateFlags.DontNotifyMeshUsers 102 | | MeshUpdateFlags.DontValidateIndices 103 | | MeshUpdateFlags.DontResetBoneBounds; 104 | public int SourceSubMeshIndex; 105 | [NativeDisableParallelForRestriction] 106 | public Mesh.MeshDataArray TargetMeshDataArray; 107 | 108 | [NativeDisableContainerSafetyRestriction] 109 | public NativeList AllIndices; 110 | [NativeDisableContainerSafetyRestriction] 111 | public NativeList IndexRanges; 112 | [NativeDisableContainerSafetyRestriction] 113 | public NativeArray VertexData; 114 | [NativeDisableContainerSafetyRestriction] 115 | public NativeArray VertexAttributeDescriptors; 116 | [ReadOnly] 117 | public int VertexStride; 118 | 119 | public void Execute(int index) 120 | { 121 | var writableMeshData = TargetMeshDataArray[index]; 122 | 123 | var indexOffset = IndexRanges[index].x; 124 | var vertexCount = IndexRanges[index].y; 125 | 126 | // var indices = new NativeList(100, Allocator.Temp); 127 | var indices = new NativeArray(vertexCount * 3, Allocator.Temp); 128 | 129 | var vertexData = new NativeArray(VertexStride * vertexCount, Allocator.Temp); 130 | 131 | var vertexIndex = 0; 132 | 133 | // iterate triangle indices in pairs of 3 134 | for (int i = 0; i < vertexCount; i += 3) 135 | { 136 | // indices of the triangle 137 | var a = indexOffset + i; 138 | var b = indexOffset + i + 1; 139 | var c = indexOffset + i + 2; 140 | 141 | AddVertex(vertexData, a, vertexIndex++); 142 | AddVertex(vertexData, b, vertexIndex++); 143 | AddVertex(vertexData, c, vertexIndex++); 144 | 145 | indices[i] = (uint)i; 146 | indices[i+1] = (uint)(i+1); 147 | indices[i+2] = (uint)(i+2); 148 | } 149 | 150 | // apply vertex data 151 | writableMeshData.SetVertexBufferParams(vertexCount, VertexAttributeDescriptors); 152 | var writableMeshVertexData = writableMeshData.GetVertexData(); 153 | writableMeshVertexData.CopyFrom(vertexData); 154 | 155 | // automatically use 16 or 32 bit indexing depending on the vertex count 156 | var indexFormat = indices.Length >= ushort.MaxValue ? IndexFormat.UInt32 : IndexFormat.UInt16; 157 | 158 | // apply index data 159 | writableMeshData.SetIndexBufferParams(indices.Length, indexFormat); 160 | 161 | switch (indexFormat) 162 | { 163 | case IndexFormat.UInt16: 164 | { 165 | // convert 32 bit indices to 16 bit 166 | var indexData = writableMeshData.GetIndexData(); 167 | var indices16 = new NativeArray(indices.Length, Allocator.Temp); 168 | for (var i = 0; i < indices.Length; i++) 169 | { 170 | indices16[i] = (ushort)indices[i]; 171 | } 172 | indexData.CopyFrom(indices16); 173 | indices16.Dispose(); 174 | break; 175 | } 176 | case IndexFormat.UInt32: 177 | { 178 | writableMeshData.GetIndexData().CopyFrom(indices); 179 | break; 180 | } 181 | } 182 | 183 | // writableMeshData.subMeshCount = SourceSubMeshIndex + 1; 184 | writableMeshData.subMeshCount = 1; 185 | writableMeshData.SetSubMesh(0, new SubMeshDescriptor(0, indices.Length), MeshUpdateFlags); 186 | 187 | // dispose 188 | indices.Dispose(); 189 | vertexData.Dispose(); 190 | } 191 | 192 | private void AddVertex(NativeArray targetVertexData, int sourceVertexIndex, int targetVertexIndex) 193 | { 194 | var sourceIndex = AllIndices[sourceVertexIndex]; 195 | 196 | var sourcePtr = (void*)IntPtr.Add((IntPtr)VertexData.GetUnsafePtr(), sourceIndex * VertexStride); 197 | var targetPtr = (void*)IntPtr.Add((IntPtr)targetVertexData.GetUnsafePtr(), targetVertexIndex * VertexStride); 198 | 199 | UnsafeUtility.MemCpy(targetPtr, sourcePtr, VertexStride); 200 | } 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /Runtime/Scripts/MeshSplitController.cs: -------------------------------------------------------------------------------- 1 | /* https://github.com/artnas/Unity-Plane-Mesh-Splitter */ 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Unity.Mathematics; 7 | using UnityEngine; 8 | 9 | namespace MeshSplit.Scripts 10 | { 11 | public class MeshSplitController : MonoBehaviour 12 | { 13 | /// 14 | /// Multiply grid size internally to still use Vector3Int even if the grid size is smaller than 1 (ex: 0.1) 15 | /// 16 | public static readonly int GridSizeMultiplier = 100; 17 | 18 | /// 19 | /// Multiply grid size internally to still use Vector3Int even if the grid size is smaller than 1 (ex: 0.1) 20 | /// 21 | private const int GizmosDisplayLimit = 100000; 22 | 23 | public bool Verbose; 24 | 25 | public MeshSplitParameters Parameters; 26 | public bool DrawGridGizmosWhenSelected; 27 | 28 | private Mesh _baseMesh; 29 | private MeshRenderer _baseRenderer; 30 | 31 | // generated children are kept here, so the script knows what to delete on Split() or Clear() 32 | [HideInInspector] [SerializeField] 33 | private List Children = new List(); 34 | 35 | public void Split() 36 | { 37 | DestroyChildren(); 38 | 39 | if (GetUsedAxisCount() < 1) 40 | { 41 | throw new Exception("You have to choose at least 1 axis."); 42 | } 43 | 44 | var meshFilter = GetComponent(); 45 | if (meshFilter) 46 | { 47 | _baseMesh = meshFilter.sharedMesh; 48 | } 49 | else 50 | { 51 | throw new Exception("MeshFilter component is required."); 52 | } 53 | 54 | if (_baseRenderer || TryGetComponent(out _baseRenderer)) 55 | { 56 | _baseRenderer.enabled = false; 57 | } 58 | 59 | CreateChildren(); 60 | } 61 | 62 | private void CreateChildren() 63 | { 64 | var meshSplitter = new MeshSplitter(Parameters, Verbose); 65 | var subMeshData = meshSplitter.Split(_baseMesh); 66 | 67 | // sort the children 68 | subMeshData.Sort(delegate((Vector3Int gridPoint, Mesh mesh) a, (Vector3Int gridPoint, Mesh mesh) b) 69 | { 70 | for (int i = 0; i < 3; i++) 71 | { 72 | var compare = a.gridPoint[i].CompareTo(b.gridPoint[i]); 73 | 74 | if (compare != 0) 75 | { 76 | return compare; 77 | } 78 | } 79 | 80 | return 0; 81 | }); 82 | 83 | foreach (var (gridPoint, mesh) in subMeshData) 84 | { 85 | if (mesh.vertexCount > 0) 86 | CreateChild(gridPoint, mesh); 87 | } 88 | } 89 | 90 | private void CreateChild(Vector3Int gridPoint, Mesh mesh) 91 | { 92 | // divide by multiplier and round to at moast 2 decimal places 93 | var pointString = $"({(float)gridPoint.x / GridSizeMultiplier:0.##}, {(float)gridPoint.y / GridSizeMultiplier:0.##}, {(float)gridPoint.z / GridSizeMultiplier:0.##})"; 94 | var newGameObject = new GameObject 95 | { 96 | name = $"SubMesh {pointString}" 97 | }; 98 | 99 | newGameObject.transform.SetParent(transform, false); 100 | if (Parameters.UseParentLayer) 101 | { 102 | newGameObject.layer = gameObject.layer; 103 | } 104 | if (Parameters.UseParentStaticFlag) 105 | { 106 | newGameObject.isStatic = gameObject.isStatic; 107 | } 108 | 109 | // assign the new mesh to this submeshes mesh filter 110 | var newMeshFilter = newGameObject.AddComponent(); 111 | newMeshFilter.sharedMesh = mesh; 112 | 113 | var newMeshRenderer = newGameObject.AddComponent(); 114 | if (Parameters.UseParentMeshRendererSettings && _baseRenderer) 115 | { 116 | newMeshRenderer.sharedMaterial = _baseRenderer.sharedMaterial; 117 | newMeshRenderer.sortingOrder = _baseRenderer.sortingOrder; 118 | newMeshRenderer.sortingLayerID = _baseRenderer.sortingLayerID; 119 | newMeshRenderer.shadowCastingMode = _baseRenderer.shadowCastingMode; 120 | newMeshRenderer.receiveShadows = _baseRenderer.receiveShadows; 121 | #if UNITY_EDITOR 122 | newMeshRenderer.receiveGI = _baseRenderer.receiveGI; 123 | #endif 124 | newMeshRenderer.lightProbeUsage = _baseRenderer.lightProbeUsage; 125 | newMeshRenderer.rayTracingMode = _baseRenderer.rayTracingMode; 126 | newMeshRenderer.reflectionProbeUsage = _baseRenderer.reflectionProbeUsage; 127 | newMeshRenderer.staticShadowCaster = _baseRenderer.staticShadowCaster; 128 | newMeshRenderer.motionVectorGenerationMode = _baseRenderer.motionVectorGenerationMode; 129 | newMeshRenderer.allowOcclusionWhenDynamic = _baseRenderer.allowOcclusionWhenDynamic; 130 | } 131 | 132 | if (Parameters.GenerateColliders) 133 | { 134 | var meshCollider = newGameObject.AddComponent(); 135 | meshCollider.convex = Parameters.UseConvexColliders; 136 | meshCollider.sharedMesh = mesh; 137 | } 138 | 139 | Children.Add(newGameObject); 140 | } 141 | 142 | private int GetUsedAxisCount() 143 | { 144 | return (Parameters.SplitAxes.x ? 1 : 0) + (Parameters.SplitAxes.y ? 1 : 0) + (Parameters.SplitAxes.z ? 1 : 0); 145 | } 146 | 147 | public void Clear() 148 | { 149 | DestroyChildren(); 150 | 151 | // reenable renderer 152 | if (_baseRenderer || TryGetComponent(out _baseRenderer)) 153 | { 154 | _baseRenderer.enabled = true; 155 | } 156 | } 157 | 158 | private void DestroyChildren() 159 | { 160 | // find child submeshes which are not in child list 161 | var childCount = transform.childCount; 162 | if (childCount != Children.Count) 163 | { 164 | var unassignedSubMeshes = GetComponentsInChildren() 165 | .Where(child => child.name.Contains("SubMesh") && !Children.Contains(child.gameObject)); 166 | 167 | var count = 0; 168 | 169 | foreach (var subMesh in unassignedSubMeshes) 170 | { 171 | Children.Add(subMesh.gameObject); 172 | count++; 173 | } 174 | 175 | if (Verbose) Debug.Log($"found {count} unassigned submeshes"); 176 | } 177 | 178 | foreach (var t in Children) 179 | { 180 | // destroy mesh 181 | DestroyImmediate(t.GetComponent().sharedMesh); 182 | DestroyImmediate(t); 183 | } 184 | 185 | if (Verbose) Debug.Log($"destroyed {Children.Count} submeshes"); 186 | 187 | Children.Clear(); 188 | } 189 | 190 | private void OnDrawGizmosSelected() 191 | { 192 | if (!DrawGridGizmosWhenSelected || !TryGetComponent(out var meshFilter) || !meshFilter.sharedMesh || !TryGetComponent(out _)) 193 | return; 194 | 195 | var t = transform; 196 | var bounds = meshFilter.sharedMesh.bounds; 197 | 198 | var xSize = Parameters.SplitAxes.x ? Mathf.Ceil(bounds.extents.x) : 1; 199 | var ySize = Parameters.SplitAxes.y ? Mathf.Ceil(bounds.extents.y) : 1; 200 | var zSize = Parameters.SplitAxes.z ? Mathf.Ceil(bounds.extents.z) : 1; 201 | 202 | // dont draw too many gizmos, this lags the editor to a crawl 203 | if ((xSize * ySize * zSize) / Parameters.GridSize > GizmosDisplayLimit) 204 | { 205 | return; 206 | } 207 | 208 | var center = bounds.center; 209 | 210 | // TODO improve grid alignment 211 | 212 | Gizmos.color = new Color(1, 1, 1, 0.3f); 213 | 214 | /* credit for this line drawing code goes to https://github.com/STARasGAMES */ 215 | 216 | // X aligned lines 217 | for (var y = -ySize; y <= ySize; y += Parameters.GridSize) 218 | { 219 | for (var z = -zSize; z <= zSize; z += Parameters.GridSize) 220 | { 221 | var start = t.TransformPoint(center + new Vector3(-xSize, y, z)); 222 | var end = t.TransformPoint(center + new Vector3(xSize, y, z)); 223 | Gizmos.DrawLine(start, end); 224 | } 225 | } 226 | 227 | // Y aligned lines 228 | for (var x = -xSize; x <= xSize; x += Parameters.GridSize) 229 | { 230 | for (var z = -zSize; z <= zSize; z += Parameters.GridSize) 231 | { 232 | var start = t.TransformPoint(center + new Vector3(x, -ySize, z)); 233 | var end = t.TransformPoint(center + new Vector3(x, ySize, z)); 234 | Gizmos.DrawLine(start, end); 235 | } 236 | } 237 | 238 | // Z aligned lines 239 | for (var y = -ySize; y <= ySize + 1; y += Parameters.GridSize) 240 | { 241 | for (var x = -xSize; x <= xSize + 1; x += Parameters.GridSize) 242 | { 243 | var start = t.TransformPoint(center + new Vector3(x, y, -zSize)); 244 | var end = t.TransformPoint(center + new Vector3(x, y, zSize)); 245 | Gizmos.DrawLine(start, end); 246 | } 247 | } 248 | } 249 | } 250 | } 251 | --------------------------------------------------------------------------------