Search Unity

Question Replacing Prismatic Meshes With Unity Primitive Cubes

Discussion in 'Scripting' started by aliozan7, Jan 29, 2024.

  1. aliozan7


    Jul 18, 2021
    I have a project where there are thousands of objects representing different building blocks. These objects are imported from an fbx file, and even though there are thousands of simple shaped prisms, they all have a different mesh, and also the vertices count is much more than necessary for each of them.

    I want to replace them with Unity primitive cubes to have a much more optimized and performant project.

    I want to create a script which will replace these rectangular prisms with Unity primitive cubes in runtime and let me copy them altogether as a prefab to use later. I just want this script to consider the rendered shape and fit a cube in it as well as possible. The script should not take the position and scale of the prism game object because they are all shifted and scaled weirdly in the original fbx. It should also consider the rotation of the prism.

    If anyone can help me it would be of enormous help for me.

    Ekran görüntüsü 2024-01-29 151626.png
    Ekran görüntüsü 2024-01-29 151649.png
  2. Kurt-Dekker


    Mar 16, 2013
    Is this level of geometry actually causing you a performance issue?

    For all performance and optimization issues, ALWAYS start by using the Profiler window:

    Window -> Analysis -> Profiler

    Generally optimization is:

    - avoid doing the thing that is slow
    - do it fewer times and store its result
    - do the slow thing less frequently over time
    - do the slow thing when nobody cares much (eg, during level loading)
    - find a faster way to do the thing (hardest)

    DO NOT OPTIMIZE "JUST BECAUSE..." If you don't have a problem, DO NOT OPTIMIZE!

    If you DO have a problem, there is only ONE way to find out: measuring with the profiler.

    Failure to use the profiler first means you're just guessing, making a mess of your code for no good reason.

    Not only that but performance on platform A will likely be completely different than platform B. Test on the platform(s) that you care about, and test to the extent that it is worth your effort, and no more.

    Remember that you are gathering information at this stage. You cannot FIX until you FIND.

    Remember that optimized code is ALWAYS harder to work with and more brittle, making subsequent feature development difficult or impossible, or incurring massive technical debt on future development.

    Don't forget about the Frame Debugger window either, available right near the Profiler in the menu system.

    Notes on optimizing UnityEngine.UI setups:

    At a minimum you want to clearly understand what performance issues you are having:

    - running too slowly?
    - loading too slowly?
    - using too much runtime memory?
    - final bundle too large?
    - too much network traffic?
    - something else?

    If you are unable to engage the profiler, then your next solution is gross guessing changes, such as "reimport all textures as 32x32 tiny textures" or "replace some complex 3D objects with cubes/capsules" to try and figure out what is bogging you down.

    Each experiment you do may give you intel about what is causing the performance issue that you identified. More importantly let you eliminate candidates for optimization. For instance if you swap out your biggest textures with 32x32 stamps and you STILL have a problem, you may be able to eliminate textures as an issue and move onto something else.

    This sort of speculative optimization assumes you're properly using source control so it takes one click to revert to the way your project was before if there is no improvement, while carefully making notes about what you have tried and more importantly what results it has had.

    "Software does not run in a magic fairy aether powered by the fevered dreams of CS PhDs." - Mike Acton


    If you want to persist in replacing everything with cubes above, judging from the density of extra geometry along the faces shown in the second image, you have several problems to solve in a row.

    First you would need a heuristic to simply group all the things you want into whatever you want to call a "cube."

    Second you would analyze each building as a candidate for replacement with a cube.

    Third you would construct a cube, rotated and scaled appropriately.

    You also might have issues with buildings composed of adjacent cubes and deciding where to draw the boundary.
  3. Bunny83


    Oct 18, 2010
    That's not an easy problem and highly depends on what is actually part of each mesh. Finding the convex hull of an arbitrary subdivided mesh is not that easy. However if each mesh essentially represents a cuboid, so all triangle faces essentially are part of one of 6 sides of such a cuboid, it's possible to search through all triangles and identify the dimensions of each face. Once that is done you could determine the orientation / major axes of the cuboid and once you have that, the 8 corners can be translated into that new coordinate space. As a result you get the orientation / rotation, the necessary scale and position of each cuboid.

    However if the meshes do not necessarily represent a perfect cuboid, this is not that simple. You essentially ask for a topographic simplifier. With arbitrary meshes it's kinda impossible to guarantee a certain outcome. A "simple" collapse into bounding faces by combining neighboring triangles into simpler shapes which can then be triangulated again could be a solution, but it all boils down how exact the vertices can be identifed to be part of the same face / plane
  4. mgear


    Aug 3, 2010
    pixyz studio has Select similar objects feature (with adjustable match %)
    (and replace with object feature)

    it might help if you just want to reduce vertices too. (has optimize tools)

    also good idea to check source program, if it offers any export options (like for .stp there might quality option)

    also would be interesting to test some AutoLOD tools or unity HLOD, to see what happens.
  5. aliozan7


    Jul 18, 2021
    Thank you guys for your replies, I have now solved the problem and I will share it here for anyone who might encounter a similar issue.

    Add this script as a component to any rectangular prism object that you want to replace with a unity primitive cube.

    I have used the following script to fit primitive cubes inside my prismatic objects using their actual vertexes and their coordinates.
    Then, I copied these primitive objects in runtime as a whole to create a prefab.
    Then, I just added them back into the scene and replaced them. Helped me a lot.

    This worked for me just fine, but I think there might be an edge case I was not able to handle. Among 7000+ prisms, around 100 of them did not fit correctly, they needed to be rotated 90 degrees manually. I have not figured out the cause but this was more than enough for me. If anyone can make improvements to it, I'd be glad to see it.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using NUnit.Framework.Constraints;
    5. using UnityEditor.ShaderKeywordFilter;
    7. public class FitCubeToPrism : MonoBehaviour
    8. {
    9.     void Start()
    10.     {
    11.         // Get the mesh filter of the current GameObject (prism)
    12.         MeshFilter prismMeshFilter = GetComponent<MeshFilter>();
    14.         if (prismMeshFilter != null)
    15.         {
    16.             // Get the mesh vertices in local space
    17.             Vector3[] localVertices = prismMeshFilter.mesh.vertices;
    19.             // Transform the local vertices to world space
    20.             Vector3[] worldVertices = new Vector3[localVertices.Length];
    21.             for (int i = 0; i < localVertices.Length; i++)
    22.             {
    23.                 worldVertices[i] = transform.TransformPoint(localVertices[i]);
    24.             }
    26.             // Find the corners of the cube
    27.             Vector3[] cubeCorners = FindCubeCorners(worldVertices);
    29.             // Calculate the center of the cube
    30.             Vector3 cubeCenter = CalculateCenter(cubeCorners);
    32.             // Calculate the size of the cube
    33.             Vector3 cubeSize = CalculateSize(cubeCorners, cubeCenter);
    35.             // Calculate the rotation of the cube
    36.             Quaternion cubeRotation = CalculateRotation(cubeCorners);
    38.             // Create a primitive cube
    39.             GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
    41.             // Set the cube's position, rotation, and scale to match the prism
    42.             cube.transform.position = cubeCenter;
    43.             cube.transform.rotation = cubeRotation;
    44.             cube.transform.localScale = cubeSize;
    47.             // Give the newly created cube the same name as its parent object
    48.    =;
    50.             // Make the cube object a child of the new game object
    51.             cube.transform.parent = GameObject.FindGameObjectWithTag("PrimitiveParent").transform;
    53.             //gameObject.GetComponent<BoxCollider>().enabled = false;
    54.         }
    55.         else
    56.         {
    57.             Debug.LogError("MeshFilter not found on the GameObject.");
    58.         }
    59.     }
    61.     Vector3[] FindCubeCorners(Vector3[] vertices)
    62.     {
    63.         Vector3[] uniqueVertices = vertices.Distinct().ToArray();
    65.         Dictionary<Vector3, float> vertexMaxDistances = new Dictionary<Vector3, float>();
    67.         // Calculate and store max distances for each unique vertex
    68.         for (int i = 0; i < uniqueVertices.Length; i++)
    69.         {
    70.             float maxDistance = 0f;
    72.             for (int j = 0; j < uniqueVertices.Length; j++)
    73.             {
    74.                 float distance = Vector3.Distance(uniqueVertices[i], uniqueVertices[j]);
    76.                 if (distance > maxDistance)
    77.                 {
    78.                     maxDistance = distance;
    79.                     vertexMaxDistances[uniqueVertices[i]] = maxDistance;
    80.                 }
    81.             }
    82.         }
    84.         // Sort the vertices based on max distances in descending order
    85.         List<KeyValuePair<Vector3, float>> sortedVertices = new List<KeyValuePair<Vector3, float>>(vertexMaxDistances);
    86.         sortedVertices.Sort((a, b) => b.Value.CompareTo(a.Value));
    88.         // Select the first 8 vertices as cube corners
    89.         Vector3[] cubeCorners = new Vector3[8];
    90.         for (int i = 0; i < Mathf.Min(8, sortedVertices.Count); i++)
    91.         {
    92.             cubeCorners[i] = sortedVertices[i].Key;
    93.         }
    95.         return cubeCorners;
    96.     }
    98.     Vector3 CalculateCenter(Vector3[] corners)
    99.     {
    100.         Vector3 center =;
    101.         foreach (Vector3 corner in corners)
    102.         {
    103.             center += corner;
    104.         }
    105.         return center / corners.Length;
    106.     }
    108.     Vector3 CalculateSize(Vector3[] corners, Vector3 center)
    109.     {
    110.         // Explanation: A rectangular prism has 8 corners. Once you select one corner, you can draw 7 vectors to other corners
    111.         // Those 7 vectors will include;
    112.         // 3 rectangular vectors coming out of the selected corner and,
    113.         // 4 other vectors that are actually hypotenuses of other edges.
    114.         // In that case, selecting the shortest 3 vector will give us the edges and their lengths we need.
    115.         // The case where there are two small edges and a very long edge results in 2 small edges and their hypotenuses to be selected.
    116.         // That edge case is also handled.
    119.         // Select one corner as a base
    120.         Vector3 baseCorner = corners[0];
    121.         float[] potentialDirectionMagnitudes = new float[7];
    123.         // Calculate vectors from the base corner to the other corners
    124.         for (int i = 1; i < 8; i++)
    125.         {
    126.             potentialDirectionMagnitudes[i - 1] = (corners[i] - baseCorner).magnitude;
    127.         }
    129.         // Sort the distances in ascending order
    130.         System.Array.Sort(potentialDirectionMagnitudes);
    133.         // The three shortest distances correspond to the dimensions of the prism
    134.         float length = potentialDirectionMagnitudes[0];
    135.         float height = potentialDirectionMagnitudes[1];
    136.         float width = 0f;
    138.         // this is where the edge case is handled
    139.         float tolerance = 0.1f;
    140.         if (Mathf.Abs(Mathf.Pow(potentialDirectionMagnitudes[0], 2f) +
    141.                       Mathf.Pow(potentialDirectionMagnitudes[1], 2f) -
    142.                       Mathf.Pow(potentialDirectionMagnitudes[2], 2f)) < tolerance)
    143.         {
    144.             width = potentialDirectionMagnitudes[3];
    145.         }
    146.         else
    147.             width = potentialDirectionMagnitudes[2];
    149.         return new Vector3(width, length, height);
    150.     }
    152.     Quaternion CalculateRotation(Vector3[] corners)
    153.     {
    154.         Vector3[] potentialDirections = new Vector3[7];
    156.         // Select one corner as a base
    157.         Vector3 baseCorner = corners[0];
    159.         // Calculate vectors from the base corner to the other corners
    160.         for (int i = 1; i < 8; i++)
    161.         {
    162.             potentialDirections[i - 1] = corners[i] - baseCorner;
    163.         }
    165.         Dictionary<Vector3, float> directionMagnitudePairs = new Dictionary<Vector3, float>();
    166.         for (int i = 0; i < potentialDirections.Length; i++)
    167.         {
    168.             float magnitude = potentialDirections[i].magnitude;
    169.             directionMagnitudePairs.Add(potentialDirections[i], magnitude);
    170.         }
    172.         List<KeyValuePair<Vector3, float>> sortedPairs = new List<KeyValuePair<Vector3, float>>(directionMagnitudePairs);
    173.         sortedPairs.Sort((a, b) => b.Value.CompareTo(a.Value));
    175.         Vector3 direction1 = sortedPairs[6].Key;
    176.         Vector3 direction2 = sortedPairs[5].Key;
    177.         Vector3 direction3 =;
    179.         float tolerance = 0.1f;
    180.         if (Mathf.Abs(Mathf.Pow(sortedPairs[0].Value, 2f) +
    181.                       Mathf.Pow(sortedPairs[1].Value, 2f) -
    182.                       Mathf.Pow(sortedPairs[2].Value, 2f)) < tolerance)
    183.         {
    184.             direction3 = sortedPairs[3].Key;
    185.         }
    186.         else
    187.             direction3 = sortedPairs[4].Key;
    191.         // Create rotation matrix
    192.         Matrix4x4 matrix = new Matrix4x4();
    193.         matrix.SetColumn(0, new Vector4(direction1.x, direction1.y, direction1.z, 0));
    194.         matrix.SetColumn(1, new Vector4(direction2.x, direction2.y, direction2.z, 0));
    195.         matrix.SetColumn(2, new Vector4(direction3.x, direction3.y, direction3.z, 0));
    196.         matrix.SetColumn(3, new Vector4(0, 0, 0, 1));
    198.         // Extract rotation quaternion from the matrix
    199.         Quaternion rotation = Quaternion.LookRotation(matrix.GetColumn(1), matrix.GetColumn(2));
    201.         return rotation;
    202.     }
    203. }

    Attached Files: