├── Cubify.unity.meta ├── README.md.meta ├── Skull.meta ├── cubify.cs.meta ├── cubifyObject.cs.meta ├── .gitignore ├── README.md ├── cubifyObject.cs ├── Skull └── craneo.OBJ.meta └── cubify.cs /Cubify.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4fa71ab1150542418cc7cb56e9f2c3e 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d1806bd620ff7df4aa65af8d5d3b6bff 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Skull.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e07bbe732c476414ebb53d77d8483c9f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /cubify.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3aaf80b7b8cfab844aea3408db902ea6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /cubifyObject.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dd116a0e071485d44abad734e37f8dcb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | Assets/AssetStoreTools* 7 | 8 | # Visual Studio cache directory 9 | .vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | *.opendb 26 | 27 | # Unity3D generated meta files 28 | *.pidb.meta 29 | *.pdb.meta 30 | 31 | # Unity3D Generated File On Crash Reports 32 | sysinfo.txt 33 | 34 | # Builds 35 | *.apk 36 | *.unitypackage 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cubify 2 | Converts a mesh into a voxel mesh 3 | 4 | How to install: 5 | - cd into your unity project/Assets folder 6 | - Then run ==> git clone https://github.com/Andy-Roger/Cubify.git 7 | 8 | How to use: 9 | - Create a game object with a mesh collider that matches the mesh 10 | - Right click on the GameObject in the Hierarchy 11 | - Select "Cubify" 12 | - The Cubify window will appear 13 | - Choose a cubic resolution e.g. 10 14 | - Click Generate 15 | 16 | Notes: 17 | - Right now, only single mesh objects are supported/tested 18 | - Cubify uses the object collider component to generate the voxel mesh, so for best results use a mesh collider that uses the mesh of the object 19 | 20 | ![alt text](https://github.com/Andy-Roger/Images/blob/master/CubifyImage.png) 21 | Credit to Skull model: https://sketchfab.com/3d-models/skull-downloadable-1a9db900738d44298b0bc59f68123393 22 | ![alt text](https://github.com/Andy-Roger/Images/blob/master/CubifySkullSide.png) 23 | ![alt text](https://github.com/Andy-Roger/Images/blob/master/CubifySkull.png) 24 | -------------------------------------------------------------------------------- /cubifyObject.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | public class cubifyObject : MonoBehaviour { 4 | public void checkIfMeshesOverlap(int sqrResolution, BoxCollider totalVolumeCollider) { 5 | Collider[] neighbours; 6 | var thisCollider = GetComponent(); 7 | neighbours = new Collider[sqrResolution]; 8 | 9 | if (!thisCollider) 10 | return; // nothing to do without a Collider attached 11 | 12 | float radius = 3f; 13 | int count = Physics.OverlapSphereNonAlloc(transform.position, radius, neighbours); 14 | 15 | GameObject saveVoxelsGameObject = new GameObject("SavedVoxelParent"); 16 | Transform saveVoxelsParent = saveVoxelsGameObject.transform; 17 | saveVoxelsParent.transform.position = transform.position; 18 | 19 | //finds all voxels that overlap with the mesh 20 | for (int i = 0; i < count; ++i) { 21 | var collider = neighbours[i]; 22 | 23 | if (collider == thisCollider || collider == totalVolumeCollider) 24 | continue; // skip ourself and total volume collider 25 | 26 | Vector3 otherPosition = collider.gameObject.transform.position; 27 | Quaternion otherRotation = collider.gameObject.transform.rotation; 28 | 29 | Vector3 direction; 30 | float distance; 31 | 32 | bool overlapped = Physics.ComputePenetration( 33 | thisCollider, transform.position, transform.rotation, 34 | collider, otherPosition, otherRotation, 35 | out direction, out distance 36 | ); 37 | 38 | if (overlapped) { 39 | collider.transform.parent = saveVoxelsParent; 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Skull/craneo.OBJ.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 98e9b9601fbc13843a26fece11870af5 3 | ModelImporter: 4 | serializedVersion: 23 5 | fileIDToRecycleName: 6 | 100000: //RootNode 7 | 100002: Group5732 8 | 400000: //RootNode 9 | 400002: Group5732 10 | 2100000: Group5732Mat 11 | 2300000: Group5732 12 | 3300000: Group5732 13 | 4300000: Group5732 14 | 2186277476908879412: ImportLogs 15 | externalObjects: {} 16 | materials: 17 | importMaterials: 1 18 | materialName: 0 19 | materialSearch: 1 20 | materialLocation: 1 21 | animations: 22 | legacyGenerateAnimations: 4 23 | bakeSimulation: 0 24 | resampleCurves: 1 25 | optimizeGameObjects: 0 26 | motionNodeName: 27 | rigImportErrors: 28 | rigImportWarnings: 29 | animationImportErrors: 30 | animationImportWarnings: 31 | animationRetargetingWarnings: 32 | animationDoRetargetingWarnings: 0 33 | importAnimatedCustomProperties: 0 34 | importConstraints: 0 35 | animationCompression: 1 36 | animationRotationError: 0.5 37 | animationPositionError: 0.5 38 | animationScaleError: 0.5 39 | animationWrapMode: 0 40 | extraExposedTransformPaths: [] 41 | extraUserProperties: [] 42 | clipAnimations: [] 43 | isReadable: 1 44 | meshes: 45 | lODScreenPercentages: [] 46 | globalScale: 1 47 | meshCompression: 0 48 | addColliders: 0 49 | importVisibility: 1 50 | importBlendShapes: 1 51 | importCameras: 1 52 | importLights: 1 53 | swapUVChannels: 0 54 | generateSecondaryUV: 0 55 | useFileUnits: 1 56 | optimizeMeshForGPU: 1 57 | keepQuads: 0 58 | weldVertices: 1 59 | preserveHierarchy: 0 60 | indexFormat: 0 61 | secondaryUVAngleDistortion: 8 62 | secondaryUVAreaDistortion: 15.000001 63 | secondaryUVHardAngle: 88 64 | secondaryUVPackMargin: 4 65 | useFileScale: 1 66 | previousCalculatedGlobalScale: 1 67 | hasPreviousCalculatedGlobalScale: 0 68 | tangentSpace: 69 | normalSmoothAngle: 60 70 | normalImportMode: 0 71 | tangentImportMode: 3 72 | normalCalculationMode: 4 73 | importAnimation: 1 74 | copyAvatar: 0 75 | humanDescription: 76 | serializedVersion: 2 77 | human: [] 78 | skeleton: [] 79 | armTwist: 0.5 80 | foreArmTwist: 0.5 81 | upperLegTwist: 0.5 82 | legTwist: 0.5 83 | armStretch: 0.05 84 | legStretch: 0.05 85 | feetSpacing: 0 86 | rootMotionBoneName: 87 | hasTranslationDoF: 0 88 | hasExtraRoot: 0 89 | skeletonHasParents: 1 90 | lastHumanDescriptionAvatarSource: {instanceID: 0} 91 | animationType: 0 92 | humanoidOversampling: 1 93 | additionalBone: 0 94 | userData: 95 | assetBundleName: 96 | assetBundleVariant: 97 | -------------------------------------------------------------------------------- /cubify.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEditor; 5 | using System.Linq; 6 | 7 | public class cubify : EditorWindow { 8 | //cubic resolution 9 | private int resolution = 20; 10 | public enum VoxelTypes { 11 | Cube = 0, 12 | Sphere = 1, 13 | Cylinder = 2, 14 | Capsule = 3, 15 | Custom 16 | } 17 | public VoxelTypes voxelType; 18 | private GameObject cubifyObject; 19 | private GameObject customVoxel; 20 | 21 | private bool addVoxelsAtVerts = true; 22 | private float voxelSize = 0.1f; 23 | 24 | //game object context menu to open Cubify window 25 | [MenuItem("GameObject/Cubify", false, 10)] 26 | public static void runCubify() { 27 | openCubifyWindow(); 28 | } 29 | 30 | //opens Cubify window 31 | public static void openCubifyWindow() { 32 | GetWindow("Cubify"); 33 | GetWindow("Cubify").cubifyObject = (GameObject)Selection.activeObject; 34 | } 35 | 36 | void OnGUI() { 37 | //pass in the object with mesh that we want to cubify 38 | showGameObjectToCubify(); 39 | //voxel type menu 40 | showVoxelSelector(); 41 | //if custom voxel type, show custom voxel field 42 | showCustomVoxelFields(); 43 | //show the cubify at verts fields 44 | showGenerateFromVerts(); 45 | //cubic resolution 46 | showCubicResolutionField(); 47 | //generate, delete 48 | showButtons(); 49 | } 50 | 51 | //cleans up voxel parents one at a time 52 | void delete() { 53 | DestroyImmediate(GameObject.Find("SavedVoxelParent")); 54 | } 55 | 56 | //main method to start voxel generation 57 | void generate(GameObject cubifyObject, GameObject voxelObj) { 58 | //get center & size of mesh group 59 | var bounds = getBounds(cubifyObject.transform); 60 | Vector3 size = bounds.size; 61 | Vector3 center = bounds.center; 62 | 63 | //get longest side 64 | float maxDimension = Mathf.Max(Mathf.Max(size.x, size.y), size.z); 65 | 66 | //generate equally dimensioned box for creating a voxel grid inside 67 | var totalVolume = new GameObject("Total Volume"); 68 | BoxCollider totalVolumeBoxCol = totalVolume.AddComponent(); 69 | totalVolumeBoxCol.center = center; 70 | totalVolumeBoxCol.size = Vector3.one * maxDimension; 71 | 72 | //precompute constants 73 | Vector3 voxelSize = totalVolumeBoxCol.size / resolution; 74 | Vector3 startLocation = center - (size / 2); 75 | Vector3 shiftVoxelOffset = voxelSize / 2; 76 | 77 | //create voxel grid 78 | createVoxelsFromGrid(startLocation, shiftVoxelOffset, voxelObj, totalVolume, voxelSize, maxDimension); 79 | 80 | //add "cubifyObject.cs" to mesh scene instance to detect overlapping voxels 81 | cubifyObject cubifyObjectComponent = cubifyObject.GetComponent(); 82 | if (!cubifyObjectComponent) 83 | cubifyObjectComponent = cubifyObject.AddComponent(); 84 | cubifyObjectComponent.checkIfMeshesOverlap(Mathf.CeilToInt(Mathf.Pow(resolution, 3)), totalVolumeBoxCol); 85 | 86 | //clean up the scene objects after generation 87 | if (voxelType != VoxelTypes.Custom) DestroyImmediate(voxelObj); 88 | DestroyImmediate(totalVolume); 89 | DestroyImmediate(cubifyObjectComponent); 90 | } 91 | 92 | //generate voxel grid 93 | void createVoxelsFromGrid(Vector3 startLocation, Vector3 shiftVoxelOffset, GameObject voxelObj, GameObject totalVolume, Vector3 voxelSize, float maxDimension) { 94 | for (int x = 0; x < resolution; x++) { 95 | for (int y = 0; y < resolution; y++) { 96 | for (int z = 0; z < resolution; z++) { 97 | Vector3 normalizeOffset = ((new Vector3(x, y, z) / resolution) * maxDimension); 98 | Vector3 location = startLocation + normalizeOffset; 99 | Vector3 offsetLocation = location + shiftVoxelOffset; 100 | var voxel = Instantiate(voxelObj, offsetLocation, Quaternion.identity, totalVolume.transform); 101 | voxel.transform.localScale = (voxelSize / maxDimension) * maxDimension; 102 | } 103 | } 104 | } 105 | } 106 | 107 | //normalize vertice points to a grid and add voxels 108 | void generateVoxelFromVerts(GameObject voxelObj) { 109 | voxelSize = 2f / resolution; //20 * 0.005 = 0.01 110 | 111 | GameObject saveVoxelsGameObject = new GameObject("SavedVoxelParent"); 112 | Transform saveVoxelsParent = saveVoxelsGameObject.transform; 113 | 114 | Vector3[] verts = cubifyObject.GetComponent().mesh.vertices; 115 | List gridPoses = new List(); 116 | 117 | for (int i = 0; i < verts.Length; i++) { 118 | Vector3 vertexWorldPos = cubifyObject.transform.TransformPoint(verts[i]); 119 | float explodeAmount = 100; //use this perameter to inflate voxel mesh: bigger number = bigger voxel mesh 120 | Vector3 pos = vertexWorldPos + ((vertexWorldPos - cubifyObject.transform.position) / explodeAmount); 121 | 122 | float newX = Mathf.Round(pos.x / voxelSize) * voxelSize; 123 | float newY = Mathf.Round(pos.y / voxelSize) * voxelSize; 124 | float newZ = Mathf.Round(pos.z / voxelSize) * voxelSize; 125 | gridPoses.Add(new Vector3(newX, newY, newZ)); 126 | } 127 | 128 | gridPoses = gridPoses.Distinct().ToList(); //remove verts that snapped to the same grid pos as another 129 | 130 | for (int i = 0; i < gridPoses.Count; i++) { 131 | var newVoxel = Instantiate(voxelObj, gridPoses[i], Quaternion.identity, saveVoxelsParent); 132 | newVoxel.transform.localScale = Vector3.one * voxelSize; 133 | } 134 | if (voxelType != VoxelTypes.Custom) DestroyImmediate(voxelObj); //destroy original voxel 135 | } 136 | 137 | //grow a volume box over the total mesh 138 | public static Bounds getBounds(Transform loadedTransform) { 139 | Bounds bounds = new Bounds(getGroupedMeshCenter(loadedTransform), Vector3.zero); //center the bounds object on the model 140 | foreach (Renderer renderer in loadedTransform.GetComponentsInChildren()) //iterates over all child renderers and adjusts the bounds to fit over all of them 141 | bounds.Encapsulate(renderer.bounds); 142 | return bounds; 143 | } 144 | 145 | // gets average center point for bounds to center the voxel grid 146 | private static Vector3 getGroupedMeshCenter(Transform groupedMeshParent) { 147 | Vector3 vertSum = Vector3.zero; 148 | int count = 0; 149 | foreach (MeshFilter filter in groupedMeshParent.GetComponentsInChildren()) 150 | foreach (Vector3 pos in filter.sharedMesh.vertices) { 151 | vertSum += pos; 152 | count++; 153 | } 154 | if (count == 0) 155 | foreach (SkinnedMeshRenderer skinnedMeshRenderer in groupedMeshParent.GetComponentsInChildren()) 156 | foreach (Vector3 pos in skinnedMeshRenderer.sharedMesh.vertices) { 157 | vertSum += pos; 158 | count++; 159 | } 160 | return groupedMeshParent.TransformPoint(vertSum /= count); 161 | } 162 | 163 | //this method acts as a filter of primitive types, dont want quads and planes 164 | GameObject getVoxelType(VoxelTypes option) { 165 | switch (option) { 166 | case VoxelTypes.Cube: 167 | return GameObject.CreatePrimitive(PrimitiveType.Cube); 168 | case VoxelTypes.Sphere: 169 | return GameObject.CreatePrimitive(PrimitiveType.Sphere); 170 | case VoxelTypes.Cylinder: 171 | return GameObject.CreatePrimitive(PrimitiveType.Cylinder); 172 | case VoxelTypes.Capsule: 173 | return GameObject.CreatePrimitive(PrimitiveType.Capsule); 174 | case VoxelTypes.Custom: 175 | return customVoxel; 176 | default: 177 | return GameObject.CreatePrimitive(PrimitiveType.Cube); 178 | } 179 | } 180 | 181 | void showGameObjectToCubify() { 182 | EditorGUILayout.BeginHorizontal(); 183 | EditorGUILayout.LabelField("GameObject to Cubify"); 184 | cubifyObject = (GameObject)EditorGUILayout.ObjectField(cubifyObject, typeof(Object), true); 185 | EditorGUILayout.EndHorizontal(); 186 | } 187 | 188 | void showVoxelSelector() { 189 | EditorGUILayout.BeginHorizontal(); 190 | voxelType = (VoxelTypes)EditorGUILayout.EnumPopup("Voxel Type", voxelType); 191 | EditorGUILayout.EndHorizontal(); 192 | } 193 | 194 | void showCustomVoxelFields() { 195 | if (voxelType == VoxelTypes.Custom) { 196 | EditorGUILayout.BeginHorizontal(); 197 | EditorGUILayout.LabelField("Custom Voxel"); 198 | customVoxel = (GameObject)EditorGUILayout.ObjectField(customVoxel, typeof(Object), true); 199 | EditorGUILayout.EndHorizontal(); 200 | if (customVoxel != null) 201 | if (!customVoxel.GetComponent()) { 202 | EditorGUILayout.HelpBox("Add a mesh collider to this object before generating.", MessageType.Warning); 203 | } 204 | } 205 | } 206 | 207 | void showGenerateFromVerts() { 208 | EditorGUILayout.BeginHorizontal(); 209 | EditorGUILayout.LabelField("Generate at verticies (Fast)"); 210 | addVoxelsAtVerts = EditorGUILayout.Toggle("", addVoxelsAtVerts); 211 | EditorGUILayout.EndHorizontal(); 212 | } 213 | 214 | void showCubicResolutionField() { 215 | EditorGUILayout.BeginHorizontal(); 216 | EditorGUILayout.LabelField("Cubic Resolution"); 217 | resolution = EditorGUILayout.IntField(resolution); 218 | EditorGUILayout.EndHorizontal(); 219 | } 220 | 221 | void showButtons() { 222 | EditorGUILayout.BeginHorizontal(); 223 | double? timeElapsed = null; 224 | 225 | if (GUILayout.Button("Generate")) { 226 | if (!cubifyObject.GetComponent()) { 227 | Debug.LogError("Add a Collider to this GameObject before generating"); 228 | return; 229 | } 230 | var stopWatch = System.Diagnostics.Stopwatch.StartNew(); 231 | 232 | if (addVoxelsAtVerts) 233 | generateVoxelFromVerts(getVoxelType(voxelType)); 234 | else 235 | generate(cubifyObject, getVoxelType(voxelType)); 236 | 237 | timeElapsed = stopWatch.Elapsed.TotalSeconds; 238 | } 239 | if (GUILayout.Button("Delete")) { 240 | delete(); 241 | } 242 | if (timeElapsed != null) 243 | Debug.Log("Voxel generation took " + timeElapsed + " sec."); 244 | EditorGUILayout.EndHorizontal(); 245 | } 246 | } 247 | --------------------------------------------------------------------------------