/// /// <summary> /// Create a new (auto generated) model group with default values /// </summary> /// <param name="name">The name of the new group</param> /// <param name="undoBehavior">Whether or not to register undo for this action</param> /// <returns>The new model</returns> public static ModelGroup CreateNewDefaultModelGroup(string name, UndoBehavior undoBehavior = UndoBehavior.withUndo) { ModelGroup group = null; var groupGO = new GameObject(); group = groupGO.AddComponent <ModelGroup>(); group.name = name; group.groupName = group.name; group.autoGenerated = true; // Add LEGOModelGroupAsset component. groupGO.AddComponent <LEGOModelGroupAsset>(); if (undoBehavior == UndoBehavior.withUndo) { Undo.RegisterCreatedObjectUndo(group.gameObject, "Creating new model group"); } return(group); }
public static void ProcessModelGroup(ModelGroup group, ref Vector2Int vertCount, ref Vector2Int triCount, ref Vector2Int meshCount, ref Vector2Int boxColliderCount) { bool collapseMesh = true; bool collapseCols = group.importSettings.colliders; // Keep track of how many of the removable meshes are left for each part. var partMeshCount = new Dictionary <Part, int>(); // Keep track of how many of the removable surface meshes (shell, colourChangeSurface) are left for each non-legacy part. var partSurfaceMeshCount = new Dictionary <Part, int>(); // TODO: Move orphaned colliders to group root var progress = 0; if (collapseMesh) { // Debug.Log("Collapsing Mesh for "+group.name); // Optimization settings bool doSort = group.optimizations.HasFlag(ModelGroup.Optimizations.SortFrontToBack); bool canRemoveTubesAndPins = group.optimizations.HasFlag(ModelGroup.Optimizations.RemoveTubesAndPins); bool canRemoveKnobs = group.optimizations.HasFlag(ModelGroup.Optimizations.RemoveKnobs); bool canRemoveCompletelyInvisible = group.optimizations.HasFlag(ModelGroup.Optimizations.RemoveInvisible); bool cullBackfaces = group.optimizations.HasFlag(ModelGroup.Optimizations.BackfaceCulling); bool randomizeNormals = group.randomizeNormals; // Split into different meshes depending on whether they have knobs or are transparent. // 0 = No knobs = Fast track to avoid normal mapping when it isn't required // 1 = Knobs = Slow track needs normal mapping // 2 = Transparent No Knobs = Fast track to avoid normal mapping when it isn't required. // 3 = Transparent Knobs = Slow track since we will not render transparent bricks during visibility tests. MeshHelper[] meshHelpers = new MeshHelper[] { new MeshHelper(), new MeshHelper(), new MeshHelper(), new MeshHelper() }; Matrix4x4 groupMatrix = group.transform.localToWorldMatrix; Matrix4x4 groupMatrixInv = groupMatrix.inverse; // Collect all parts. var parts = group.GetComponentsInChildren <Part>(); // Collect relevant part mesh renderers along with mesh type information and a randomized rotation for each part. List <PartMeshRenderer> mrs = new List <PartMeshRenderer>(); foreach (var part in parts) { if (progress++ % 200 == 0) { EditorUtility.DisplayProgressBar("Processing", "Collecting part renderers.", ((float)progress / parts.Length) * 0.05f); } partMeshCount[part] = 0; if (part.legacy) { // Legacy parts only have unstructured meshes. foreach (var renderer in part.GetComponentsInChildren <MeshRenderer>(true)) { var partMeshRenderer = new PartMeshRenderer() { part = part, rendererer = renderer, type = MeshType.Legacy }; mrs.Add(partMeshRenderer); partMeshCount[part]++; } } else { partSurfaceMeshCount[part] = 0; foreach (var renderer in part.GetComponentsInChildren <MeshRenderer>(true)) { var parentName = renderer.transform.parent.name; // First check if it is a decoration surface since we will not include them in the processing. if (parentName == "DecorationSurfaces") { continue; } var partMeshRenderer = new PartMeshRenderer() { part = part, rendererer = renderer.GetComponent <MeshRenderer>() }; if (parentName == "Knobs") // Is it a knob? { partMeshRenderer.type = MeshType.Knob; } else if (parentName == "Tubes") // Is it a tube? { partMeshRenderer.type = MeshType.Tube; } else if (parentName == "ColourChangeSurfaces") // Is it a colour change surface? { partMeshRenderer.type = MeshType.ColourChange; partSurfaceMeshCount[part]++; } else // It must be the shell. { partMeshRenderer.type = MeshType.Shell; partSurfaceMeshCount[part]++; } mrs.Add(partMeshRenderer); partMeshCount[part]++; } } } Vector3 camPos = Vector3.zero; Vector3 camDir = Vector3.zero; if (group.views.Count == 0 && Camera.main) { camPos = Camera.main.transform.position; camDir = Camera.main.transform.forward; } else if (group.views.Count > 0) { camPos = group.views[0].position; camDir = group.views[0].rotation * Vector3.forward; } else { doSort = false; Debug.LogError("No views specified for front-to-back geometry sorting. Disabling!"); } if (doSort) { Vector3 sortDir; if (group.views.Count == 0) { sortDir = camDir; } else { sortDir = group.views[0].rotation * Vector3.forward; } mrs.Sort(delegate(PartMeshRenderer a, PartMeshRenderer b) { float dA = Vector3.Dot(a.rendererer.bounds.center, sortDir); float dB = Vector3.Dot(b.rendererer.bounds.center, sortDir); return(dA.CompareTo(dB)); }); } // Multiple passes // - Collect all meshes // - Remove original meshfilters and renderers // - Determine which Optimization level can be used (for legacy meshes only) or if mesh can be discarded completely // - Remove backfaces // - Build combined mesh // Collect meshes List <MeshInstance> instances = new List <MeshInstance>(); for (int i = 0; i < mrs.Count; ++i) { if (i % 200 == 0) { EditorUtility.DisplayProgressBar("Processing", "Collecting part meshes.", 0.05f + (float)i / mrs.Count * 0.05f); } MeshFilter mf = mrs[i].rendererer.GetComponent <MeshFilter>(); if (mf) { if (mrs[i].rendererer.enabled && mrs[i].rendererer.gameObject.activeInHierarchy) { Mesh source = mf.sharedMesh; if (source) { meshCount.x++; triCount.x += (int)source.GetIndexCount(0) / 3; vertCount.x += source.vertexCount; Material material = mrs[i].rendererer.sharedMaterial; MeshInstance c = new MeshInstance(); c.mesh = source; c.matrix = groupMatrixInv * mf.transform.localToWorldMatrix; c.worldMatrix = mf.transform.localToWorldMatrix; c.material = material; c.transparent = material && material.color.a < 1.0; c.part = mrs[i].part; c.type = mrs[i].type; c.up = mf.transform.up; c.bounds = mrs[i].rendererer.bounds; instances.Add(c); } } Object.DestroyImmediate(mf); } Object.DestroyImmediate(mrs[i].rendererer); } List <Vector3> viewPositions = new List <Vector3>(); List <Vector3> viewDirections = new List <Vector3>(); List <Matrix4x4> viewMatrices = new List <Matrix4x4>(); List <Matrix4x4> projectionMatrices = new List <Matrix4x4>(); List <Plane[]> viewFrustums = new List <Plane[]>(); if (group.views.Count == 0 && Camera.main) { viewMatrices.Add(Camera.main.worldToCameraMatrix); if (Camera.main.orthographic) { viewDirections.Add(camDir); } else { viewPositions.Add(camPos); } viewFrustums.Add(GeometryUtility.CalculateFrustumPlanes(Camera.main)); projectionMatrices.Add(Camera.main.projectionMatrix); } else if (group.views.Count > 0) { for (int i = 0; i < group.views.Count; ++i) { var view = group.views[i]; var viewMatrix = Matrix4x4.TRS(view.position, view.rotation, Vector3.one).inverse; // Invert Z for metal/openGL if (SystemInfo.usesReversedZBuffer) { viewMatrix.SetRow(2, -viewMatrix.GetRow(2)); } viewMatrices.Add(viewMatrix); Matrix4x4 projectionMatrix; Plane[] frustumPlanes; if (view.perspective) { projectionMatrix = Matrix4x4.Perspective( view.fov, view.aspect, view.minRange, view.maxRange ); viewPositions.Add(view.position); frustumPlanes = MathUtils.GetFrustumPlanesPerspective(view.position, view.rotation, view.fov, view.aspect, view.minRange, view.maxRange); } else { projectionMatrix = Matrix4x4.Ortho( -view.size * view.aspect, view.size * view.aspect, -view.size, view.size, view.minRange, view.maxRange ); viewDirections.Add(view.rotation * Vector3.forward); frustumPlanes = MathUtils.GetFrustumPlanesOrtho(view.position, view.rotation, view.size, view.aspect, view.minRange, view.maxRange); } projectionMatrices.Add(projectionMatrix); viewFrustums.Add(frustumPlanes); } } else { if (canRemoveTubesAndPins || canRemoveKnobs || canRemoveCompletelyInvisible || cullBackfaces) { Debug.LogError("No views specified for backface culling and geometry removal. Disabling!"); canRemoveTubesAndPins = false; canRemoveKnobs = false; canRemoveCompletelyInvisible = false; cullBackfaces = false; } } if (instances.Count > 16777215) { Debug.LogError($"Group {group.groupName} contains too many meshes. Some meshes will not be optimized correctly. Please split the group into multiple groups."); } AnalyzeMeshes(viewMatrices, projectionMatrices, viewPositions, viewDirections, viewFrustums, instances, partMeshCount, partSurfaceMeshCount, canRemoveTubesAndPins, canRemoveKnobs, canRemoveCompletelyInvisible, group.importSettings.lod); // Combine instances to a single mesh for (int i = 0; i < instances.Count; ++i) { if (i % 200 == 0) { EditorUtility.DisplayProgressBar("Processing", "Combining part meshes.", 0.2f + (float)i / instances.Count * 0.05f); } Mesh source = instances[i].mesh; if (source == null) { continue; } Matrix4x4 matrix = instances[i].matrix; Material material = instances[i].material; int subMesh = instances[i].transparent ? (instances[i].type == MeshType.Knob ? 3 : 2) : instances[i].pixelKnobCount >= knobNormalMapThreshold ? 1 : 0; MeshHelper mh = new MeshHelper(source); mh.Transform(matrix); // Cull backfaces if (cullBackfaces) { for (int t = 0; t < mh.triangles.Count; t += 3) { int v0 = mh.triangles[t]; int v1 = mh.triangles[t + 1]; int v2 = mh.triangles[t + 2]; Vector3 triCen = groupMatrix.MultiplyPoint((mh.vertices[v0] + mh.vertices[v1] + mh.vertices[v2]) / 3); Vector3 triNor = groupMatrix.MultiplyVector(mh.normals[v0] + mh.normals[v1] + mh.normals[v2]);//.normalized; bool anyInView = false; for (int v = 0; v < viewPositions.Count; ++v) { anyInView |= (Vector3.Dot(viewPositions[v] - triCen, triNor) >= 0); } for (int v = 0; v < viewDirections.Count; ++v) { anyInView |= (Vector3.Dot(viewDirections[v], triNor) <= 0); } if (!anyInView) { mh.triangles[t] = -1; mh.triangles[t + 1] = -1; mh.triangles[t + 2] = -1; } } mh.triangles.RemoveAll((obj) => obj < 0); mh.RemoveUnusedVertices(); } // culled completely? if (mh.vertices.Count == 0) { partMeshCount[instances[i].part]--; continue; } meshCount.y++; triCount.y += mh.triangles.Count / 3; vertCount.y += mh.vertices.Count; // Store color in vertices mh.SetColor(material); // Randomize normals if (randomizeNormals) { mh.AddNormalNoise(); } meshHelpers[subMesh].Combine(mh); mh = null; } EditorUtility.DisplayProgressBar("Processing", "Building new meshes.", 0.3f); for (int i = 0; i < meshHelpers.Length; ++i) { if (meshHelpers[i].vertices.Count > 0) { GameObject target = group.gameObject; if (i > 0) { target = new GameObject(group.name + "_subMesh" + i); Undo.RegisterCreatedObjectUndo(target, "Create sub mesh"); target.transform.SetParent(group.transform, false); } MeshFilter mf = Undo.AddComponent <MeshFilter>(target); MeshRenderer mr = Undo.AddComponent <MeshRenderer>(target); Mesh m = new Mesh(); meshHelpers[i].ToMesh(m, group.importSettings.lightmapped); // Need tangents? if (i > 0) { m.RecalculateTangents(); } // Make static. target.isStatic = true; if (group.importSettings.lightmapped) { mr.receiveGI = ReceiveGI.Lightmaps; } else { mr.receiveGI = ReceiveGI.LightProbes; } mf.sharedMesh = m; switch (i) { case 0: { PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized.asset"); mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor.mat"); break; } case 1: { PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized_NormalMap.asset"); mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor_NormalMap.mat"); break; } case 2: { PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized_Transparent.asset"); mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor_Transparent.mat"); break; } case 3: { PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized_Transparent_NormalMap.asset"); mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor_Transparent_NormalMap.mat"); break; } } } } } // Remove decoration surfaces for non-legacy parts that have no surface meshes left. progress = 0; foreach (var entry in partSurfaceMeshCount) { if (progress++ % 200 == 0) { EditorUtility.DisplayProgressBar("Processing", "Removing unneeded decorations.", 0.3f + (float)progress / partSurfaceMeshCount.Count * 0.25f); } if (!entry.Key.legacy && entry.Value == 0) { var decorationSurfaces = entry.Key.transform.Find("DecorationSurfaces"); if (decorationSurfaces) { Undo.DestroyObjectImmediate(decorationSurfaces.gameObject); } } } // Remove parts that have no meshes left. progress = 0; foreach (var entry in partMeshCount) { if (progress++ % 200 == 0) { EditorUtility.DisplayProgressBar("Processing", "Removing empty parts.", 0.55f + (float)progress / partMeshCount.Count * 0.25f); } if (entry.Value == 0) { entry.Key.brick.parts.Remove(entry.Key); // If no parts are left, remove the brick. if (entry.Key.brick.parts.Count == 0) { Undo.DestroyObjectImmediate(entry.Key.brick.gameObject); } else { Undo.DestroyObjectImmediate(entry.Key.gameObject); } } } // Collapse remaining box colliders. if (collapseCols) { // FIXME Move to proper constant. var colliderSizeBias = 0.02f; var epsilon = 0.001f; var allColliders = group.GetComponentsInChildren <BoxCollider>(); // Filter out colliders that are part of connectivity features. var partColliders = allColliders.Where((c) => c.gameObject.layer != LayerMask.NameToLayer(Connection.connectivityReceptorLayerName) && c.gameObject.layer != LayerMask.NameToLayer(Connection.connectivityConnectorLayerName)).ToArray(); boxColliderCount.x = partColliders.Length; boxColliderCount.y = partColliders.Length; bool[] colDeleted = new bool[partColliders.Length]; var collapseHappened = true; var iterationCount = 0; while (collapseHappened) { collapseHappened = false; iterationCount++; for (var i = 0; i < partColliders.Length; ++i) { if (i % 200 == 0) { EditorUtility.DisplayProgressBar("Processing", "Collapsing colliders - iteration " + iterationCount + ".", 0.8f + (float)i / partColliders.Length * 0.05f); } if (colDeleted[i]) { continue; } for (var j = i + 1; j < partColliders.Length; ++j) { if (colDeleted[j]) { continue; } var colliderA = partColliders[i]; var colliderB = partColliders[j]; // Check that spaces match up. var colliderBRotationInALocalSpace = Quaternion.Inverse(colliderA.transform.rotation) * colliderB.transform.rotation; var euler = colliderBRotationInALocalSpace.eulerAngles; if (Mathf.Abs(Mathf.Round(euler.x / 90.0f) - euler.x / 90.0f) < epsilon && Mathf.Abs(Mathf.Round(euler.y / 90.0f) - euler.y / 90.0f) < epsilon && Mathf.Abs(Mathf.Round(euler.z / 90.0f) - euler.z / 90.0f) < epsilon) { // Check that centers match up. var colliderBCenterInColliderALocalSpace = colliderA.transform.InverseTransformPoint(colliderB.transform.TransformPoint(colliderB.center)); var centerDiff = colliderBCenterInColliderALocalSpace - colliderA.center; var axisToMatch = centerDiff.MajorAxis(); var centerProjectedToAxis = centerDiff.SnapToMajorAxis() * centerDiff.magnitude; if ((centerDiff - centerProjectedToAxis).sqrMagnitude < 0.01f) { //Debug.Log(GetGameObjectPath(colliderA.gameObject) + colliderA.transform.GetSiblingIndex() + "\n" + GetGameObjectPath(colliderB.gameObject) + colliderB.transform.GetSiblingIndex()); //Debug.Log("Matched centers on " + axisToMatch); // Check that size match up. var colliderBSizeInColliderALocalSpace = colliderA.transform.InverseTransformVector(colliderB.transform.TransformVector(colliderB.size)).Abs(); //Debug.Log("Collider b size in collider a local space: " + colliderBSizeInColliderALocalSpace); var otherAxis1 = (axisToMatch + 1) % 3; var otherAxis2 = (axisToMatch + 2) % 3; if (Mathf.Abs(centerDiff.magnitude - (colliderA.size[axisToMatch] + colliderBSizeInColliderALocalSpace[axisToMatch] + 2.0f * colliderSizeBias) * 0.5f) < epsilon) { //Debug.Log("Matched on axis length"); if (Mathf.Abs(colliderA.size[otherAxis1] - colliderBSizeInColliderALocalSpace[otherAxis1]) < epsilon && Mathf.Abs(colliderA.size[otherAxis2] - colliderBSizeInColliderALocalSpace[otherAxis2]) < epsilon) { //Debug.Log("Matched other axes"); // Merge collider B into collider A. colliderA.center += centerDiff.normalized * (colliderBSizeInColliderALocalSpace[axisToMatch] + colliderSizeBias) * 0.5f; var newSize = colliderA.size; newSize[axisToMatch] += colliderBSizeInColliderALocalSpace[axisToMatch] + colliderSizeBias; colliderA.size = newSize; // Update part and destroy collider game object. Empty parent Colliders game objects will be removed during part clean-up. var part = colliderB.GetComponentInParent <Part>(); part.colliders.Remove(colliderB); Undo.DestroyObjectImmediate(colliderB.gameObject); colDeleted[j] = true; boxColliderCount.y--; // Note that a change was made, and run over colliders again when done. collapseHappened = true; } } } } } } } } if (collapseMesh || collapseCols) { // Collect all remaining parts and clean them up. progress = 0; var parts = group.GetComponentsInChildren <Part>(); foreach (var part in parts) { if (progress++ % 200 == 0) { EditorUtility.DisplayProgressBar("Processing", "Cleaning up remaining parts.", 0.85f + (float)progress / parts.Length * 0.15f); } CleanupPartGeometryTransforms(part); } } EditorUtility.ClearProgressBar(); }
/// <summary> /// Recompute the model and model group hierarchy. /// Some rules: /// 1. A brick will always be in a model group, which will always be in a model. /// 2. Empty models and model groups are destroyed. /// 3. Bricks can not contain bricks, model groups can not contain model groups and models can not contain models. So they are flattened. /// </summary> /// <param name="bricks">The bricks that we need to check the hierarchy for</param> /// <param name="alignRotation">Whether or not we also align rotation when we recompute pivot</param> /// <param name="undoBehavior">Whether or not to register undo for the recomputation of hierarchy</param> public static void RecomputeHierarchy(IEnumerable <Brick> bricks, bool alignRotation = true, UndoBehavior undoBehavior = UndoBehavior.withUndo) { // Group numbers are numbers in parentheses // So group 1 with three bricks is shown as (1, 1, 1) // In case the group is part of a prefab it is suffixed with a p like (1p, 1p) // In case one of the bricks in a group is an override it is noted with a + as in (1p, 1p+) // Cases for splitting: // (1, 1) -> (1) (1) // (1, 1) (2) -> (1) (1, 2) -> (1) (2, 2) // Cases for unpacking: // (1p) (2p) -> (1, 2p) -> (2p+, 2p) // (1p, 1p+) (2p) -> (1, 1, 2p) -> (2p+, 2p+, 2p) // (1p, 1p) -> (1p) (1p) -> (1) (1) // Only set parent: // (1) (2) -> (1, 2) -> (2, 2) // (1p, 1p+) (2p) -> (1p) (1p+, 2p) -> (1p) (2p+, 2p) // (1p, 1p+) (2) -> (1p) (1p+, 2) -> (1p) (2, 2) if (PrefabStageUtility.GetCurrentPrefabStage() != null) { var rootObject = PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot; var brick = rootObject.GetComponent <Brick>(); if (brick) { return; } } // First we flatten models var modelsToCheck = new HashSet <Model>(); var groupsToCheck = new HashSet <ModelGroup>(); var bricksToCheck = new HashSet <Brick>(); foreach (var brick in bricks) { var bricksInParent = brick.GetComponentsInParent <Brick>(true); if (bricksInParent.Length > 1) { bricksToCheck.Add(brick); } var modelsInParent = brick.GetComponentsInParent <Model>(true); if (modelsInParent.Length > 1) { modelsToCheck.UnionWith(modelsInParent); } var groupsInParent = brick.GetComponentsInParent <ModelGroup>(true); if (groupsInParent.Length > 1) { groupsToCheck.UnionWith(groupsInParent); } } foreach (var model in modelsToCheck) { var modelsInModel = model.GetComponentsInChildren <Model>(true); foreach (var inModel in modelsInModel) { if (inModel == model) { continue; } var groupsInModel = inModel.GetComponentsInChildren <ModelGroup>(true); foreach (var group in groupsInModel) { SetParent(group.transform, model.transform, undoBehavior); } } } // Now flatten groups foreach (var group in groupsToCheck) { var groupsInGroup = group.GetComponentsInChildren <ModelGroup>(true); foreach (var inGroup in groupsInGroup) { if (inGroup == group) { continue; } var bricksInGroup = inGroup.GetComponentsInChildren <Brick>(true); foreach (var brick in bricksInGroup) { SetParent(brick.transform, group.transform, undoBehavior); } } } // Now flatten bricks foreach (var brick in bricksToCheck) { var group = GetGroupInParent(brick.transform); if (group) { SetParent(brick.transform, group.transform, undoBehavior); } } var connectedClusters = new List <HashSet <Brick> >(); // Collect all connected brick lists foreach (var brick in bricks) { if (connectedClusters.Any(x => x.Contains(brick))) { continue; } if (!brick.HasConnectivity()) { var group = GetGroupInParent(brick.transform); if (!group) { connectedClusters.Add(new HashSet <Brick> { brick }); } else { var model = GetModelInParent(brick.transform); if (!model) { var bricksInGroup = group.GetComponentsInChildren <Brick>(true); var connected = new HashSet <Brick>(); connected.Add(brick); connectedClusters.Add(connected); } } } else { var connected = brick.GetConnectedBricks(); connected.Add(brick); connectedClusters.Add(connected); } } // Now find all groups for each cluster var groupsPerCluster = new List <(HashSet <Brick>, HashSet <ModelGroup>, HashSet <Brick>)>(); foreach (var cluster in connectedClusters) { if (cluster.Count == 0) { continue; } var bricksNotInGroup = new HashSet <Brick>(); var groups = new HashSet <ModelGroup>(); foreach (var brick in cluster) { var group = GetGroupInParent(brick.transform); if (group) { groups.Add(group); } else { bricksNotInGroup.Add(brick); } } groupsPerCluster.Add((cluster, groups, bricksNotInGroup)); } // Sorting makes sure we merge before we split. Merging will make it easier to see what we need to split later. groupsPerCluster = groupsPerCluster.OrderByDescending(x => x.Item2.Count).ToList(); // Check through each of these groups in the cluster foreach (var groupPerCluster in groupsPerCluster) { var cluster = groupPerCluster.Item1; var groups = groupPerCluster.Item2; var notInGroup = groupPerCluster.Item3; // If the cluster has more than one group, we need to merge them if (groups.Count > 1) { // Merge some groups ModelGroup largestGroup = null; int largestGroupSize = 0; foreach (var group in groups) { var bricksInGroup = group.GetComponentsInChildren <Brick>(true); var bricksInCluster = bricksInGroup.Count(x => cluster.Contains(x)); if (bricksInCluster >= largestGroupSize) { largestGroup = group; largestGroupSize = bricksInCluster; } } foreach (var brick in cluster) { if (brick.transform.parent == largestGroup.transform) { continue; } if (IsPartOfPrefab(brick)) { UnpackPrefab(brick, undoBehavior); } SetParent(brick.transform, largestGroup.transform, undoBehavior); } RecomputePivot(largestGroup, alignRotation, undoBehavior); var modelGO = largestGroup.transform.parent; var model = modelGO.GetComponent <Model>(); if (model) { RecomputePivot(model, alignRotation, undoBehavior); } } else if (groups.Count == 1) // In case the cluster only has one group, we need to check if the group contains bricks not in this cluster { var group = groups.FirstOrDefault(); if (!group) { continue; } var bricksInGroup = group.GetComponentsInChildren <Brick>(true); var clustersForGroup = new HashSet <HashSet <Brick> >(); // If this group contains more than one cluster, split foreach (var brick in bricksInGroup) { if (!clustersForGroup.Any(x => x.Contains(brick))) { if (brick.HasConnectivity()) { var connected = brick.GetConnectedBricks(); connected.Add(brick); clustersForGroup.Add(connected); } } } // Only split if we found multiple clusters in group if (clustersForGroup.Count > 1) { // Get the model for the group var model = GetModelInParent(group.transform); // Find all prefabs we need to unpack by looking through the clusters in the group foreach (var clusterInGroup in clustersForGroup) { // Look through each brick in the cluster foreach (var brick in clusterInGroup) { // First check if there is a brick in this cluster that is part of a prefab and not an override // If there is, then check if there is another cluster containing a prefab that is not an override // In that case, we have to unpack, because we are changing the parents of gameobjects in a prefab if (IsPartOfPrefab(brick)) { if (clustersForGroup.Any(clust => clust != clusterInGroup && clust.Any(obj => !PrefabUtility.IsAddedGameObjectOverride(obj.gameObject)))) { UnpackPrefab(brick, undoBehavior); break; } } } } // Optimization: Check for largest group containing only a single cluster and keep that group intact HashSet <Brick> largestGroupBricks = null; ModelGroup largestGroup = null; int largestGroupSize = 0; var sharedParentClusters = new HashSet <ModelGroup>(); foreach (var clusterInGroup in clustersForGroup) { var parent = clusterInGroup.First().transform.parent; var modelGroup = parent.GetComponent <ModelGroup>(); if (!modelGroup) { continue; } var skip = false; foreach (var brick in clusterInGroup) { if (brick.transform.parent != parent) { skip = true; break; } } if (skip) { continue; } if (largestGroupSize < clusterInGroup.Count()) { largestGroupSize = clusterInGroup.Count(); largestGroupBricks = clusterInGroup; largestGroup = modelGroup; } } if (largestGroupBricks != null) { clustersForGroup.Remove(largestGroupBricks); } foreach (var clusterInGroup in clustersForGroup) { var newObject = new GameObject(); var newGroup = newObject.AddComponent <ModelGroup>(); // Add LEGOModelGroupAsset component. newObject.AddComponent <LEGOModelGroupAsset>(); if (undoBehavior == UndoBehavior.withUndo) { Undo.RegisterCreatedObjectUndo(newObject, "Created new group"); } newGroup.transform.position = group.transform.position; if (model) { SetParent(newGroup.transform, model.transform, undoBehavior); } newGroup.name = group.groupName; newGroup.groupName = group.groupName; newGroup.parentName = group.parentName; newGroup.optimizations = group.optimizations; newGroup.randomizeNormals = group.randomizeNormals; foreach (var view in group.views) { newGroup.views.Add(new CullingCameraConfig() { name = view.name, perspective = view.perspective, position = view.position, rotation = view.rotation, fov = view.fov, size = view.size, minRange = view.minRange, maxRange = view.maxRange, aspect = view.aspect }); } newGroup.autoGenerated = true; foreach (var brick in clusterInGroup) { SetParent(brick.transform, newGroup.transform, undoBehavior); } RecomputePivot(newGroup, alignRotation, undoBehavior); } if (model) { RecomputePivot(model, alignRotation, undoBehavior); } } else if (notInGroup.Count > 0) { foreach (var brick in notInGroup) { if (IsPartOfPrefab(brick)) { UnpackPrefab(brick, undoBehavior); } SetParent(brick.transform, group.transform, undoBehavior); } } else { RecomputePivot(group, alignRotation, undoBehavior); var modelGO = group.transform.parent; Model model = null; bool createNewModel = (PrefabStageUtility.GetCurrentPrefabStage() != null && modelGO && !modelGO.GetComponent <Model>()) || (PrefabStageUtility.GetCurrentPrefabStage() == null && (!modelGO || !modelGO.GetComponent <Model>())); if (createNewModel) { model = CreateNewDefaultModel(group.name, undoBehavior); if (modelGO != null) { if (PrefabStageUtility.GetCurrentPrefabStage() == null && PrefabUtility.IsPartOfAnyPrefab(modelGO)) { UnpackPrefab(modelGO.gameObject, undoBehavior); } SetParent(model.transform, modelGO.transform, undoBehavior); } SetParent(group.transform, model.transform, undoBehavior); EditorGUIUtility.PingObject(group.gameObject); } else { model = modelGO.GetComponent <Model>(); } if (model) { RecomputePivot(model, alignRotation, undoBehavior); } } } else // No groups. { var name = cluster.FirstOrDefault()?.name; Model model = null; if (PrefabStageUtility.GetCurrentPrefabStage() != null) { var rootObject = PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot; model = rootObject.GetComponent <Model>(); if (!model) { model = CreateNewDefaultModel(name); SetParent(model.transform, rootObject.transform, undoBehavior); } } else { model = CreateNewDefaultModel(name); } ModelGroup newGroup = CreateNewDefaultModelGroup(name); SetParent(newGroup.transform, model.transform, undoBehavior); var bounds = BrickBuildingUtility.ComputeBounds(cluster, Matrix4x4.identity); model.transform.position = new Vector3(bounds.center.x, bounds.min.y, bounds.center.z); Transform originalParent = null; foreach (var brick in cluster) { if (!originalParent) { originalParent = brick.transform.parent; } if (brick.transform.parent != originalParent) { originalParent = null; break; } } if (originalParent) { SetParent(model.transform, originalParent, undoBehavior); } foreach (var brick in cluster) { SetParent(brick.transform, newGroup.transform, undoBehavior); EditorGUIUtility.PingObject(brick.gameObject); } } } var modelsInScene = StageUtility.GetCurrentStageHandle().FindComponentsOfType <Model>(); foreach (var model in modelsInScene) { var children = model.GetComponentsInChildren <Brick>(true); if (children.Length == 0) { if (undoBehavior == UndoBehavior.withUndo) { Undo.DestroyObjectImmediate(model.gameObject); } else { Object.DestroyImmediate(model.gameObject); } } } var groupsInScene = StageUtility.GetCurrentStageHandle().FindComponentsOfType <ModelGroup>(); foreach (var group in groupsInScene) { var children = group.GetComponentsInChildren <Brick>(true); if (children.Length == 0) { if (undoBehavior == UndoBehavior.withUndo) { Undo.DestroyObjectImmediate(group.gameObject); } else { Object.DestroyImmediate(group.gameObject); } } } }
/// <summary> /// Recompute pivot for a model group according to the pivot type set on the model /// </summary> /// <param name="model">The model group to recompute pivot for</param> /// <param name="alignRotation">Whether or not to align the rotation according to the bricks in the model group</param> /// <param name="undoBehavior">Whether or not to register undo for this action</param> public static void RecomputePivot(ModelGroup group, bool alignRotation = true, UndoBehavior undoBehavior = UndoBehavior.withUndo) { RecomputePivot(group.transform, Model.Pivot.BottomCenter, alignRotation, undoBehavior); }
public static void ReimportModelGroup(LXFMLDoc lxfml, ModelGroup group, ModelGroupImportSettings importSettings, bool detectConnectivity = false) { // Assign the new group import settings to the group. group.importSettings = importSettings; // We assume that the group can be found, so reimport it. if (group.processed) { // Remove all processed meshes. var renderers = group.GetComponentsInChildren <MeshRenderer>(); foreach (var renderer in renderers) { // FIXME Destroy the mesh? Prevents undo.. var filter = renderer.GetComponent <MeshFilter>(); //Undo.DestroyObjectImmediate(filter.sharedMesh); if (renderer.GetComponent <ModelGroup>() == null) { // Destroy submesh game objects entirely. Undo.DestroyObjectImmediate(renderer.gameObject); } else { // Destroy mesh related components on group game object. Object.DestroyImmediate(filter); Object.DestroyImmediate(renderer); } } } // FIXME Check if bricks are referenced. // FIXME Check if bricks have custom components attached. // Remove group bricks. var existingBricks = group.GetComponentsInChildren <Brick>(); foreach (var brick in existingBricks) { Undo.DestroyObjectImmediate(brick.gameObject); } var groupLightMapped = group.importSettings.isStatic && group.importSettings.lightmapped; SetStaticAndGIParams(group.gameObject, group.importSettings.isStatic, groupLightMapped); // Move group to origo to ensure that bricks are instantiated in the correct positions. var originalGroupLocalPosition = group.transform.localPosition; var originalGroupLocalRotation = group.transform.localRotation; var originalGroupLocalScale = group.transform.localScale; var originalGroupParent = group.transform.parent; var originalGroupSiblingIndex = group.transform.GetSiblingIndex(); group.transform.SetParent(null); group.transform.localPosition = Vector3.zero; group.transform.localRotation = Quaternion.identity; group.transform.localScale = Vector3.one; // Create dictionary with just this group. var modelGroupImportSettingsDictionary = new DictionaryIntToModelGroupImportSettings(); modelGroupImportSettingsDictionary.Add(group.number, group.importSettings); // Instantiate group bricks. var resultBricks = new Dictionary <int, Brick>(lxfml.bricks.Count); InstantiateModelBricks(lxfml, modelGroupImportSettingsDictionary, ref resultBricks, group.number); // Assign bricks to group. foreach (var brick in resultBricks.Values) { brick.transform.SetParent(group.transform); } ModelGroupUtility.RecomputePivot(group, false, ModelGroupUtility.UndoBehavior.withoutUndo); // Move group back to original location. group.transform.SetParent(originalGroupParent); group.transform.SetSiblingIndex(originalGroupSiblingIndex); group.transform.localPosition = originalGroupLocalPosition; group.transform.localRotation = originalGroupLocalRotation; group.transform.localScale = originalGroupLocalScale; /*if (group.processed) * { * // Process the group again. * // FIXME Is this even a good idea? * if (group.type == GroupType.Environment || group.type == GroupType.Static) * { * Vector2Int vertCount = Vector2Int.zero; * Vector2Int triCount = Vector2Int.zero; * Vector2Int meshCount = Vector2Int.zero; * Vector2Int colliderCount = Vector2Int.zero; * ModelProcessor.ProcessModelGroup(group, ref vertCount, ref triCount, ref meshCount, ref colliderCount); * * Debug.Log($"Process result (before/after):\nVerts {vertCount.x}/{vertCount.y}, tris {triCount.x}/{triCount.y}, meshes {meshCount.x}/{meshCount.y}, colliders {colliderCount.x}/{colliderCount.y}"); * } * }*/ if (detectConnectivity && group.importSettings.connectivity) { var sceneBricks = new HashSet <Brick>(StageUtility.GetCurrentStageHandle().FindComponentsOfType <Brick>()); DetectConnectivity(new HashSet <Brick>(resultBricks.Values), sceneBricks); } EditorUtility.ClearProgressBar(); }
public static void ReimportModel(Model model, string newReimportPath = null) { var absoluteFilePath = model.absoluteFilePath; var relativeFilePath = model.relativeFilePath; // Update the file path if (newReimportPath != null) { absoluteFilePath = newReimportPath; relativeFilePath = PathUtils.GetRelativePath(Directory.GetCurrentDirectory(), newReimportPath); } lxfml = ReadFileLogic(relativeFilePath); if (lxfml == null) { lxfml = ReadFileLogic(absoluteFilePath); } if (lxfml != null) { pivot = model.pivot; // Store the new reimport path so it can be applied to the model if reimport is successful. filePath = newReimportPath; importSettings.Clear(); // Make sure we are compatible with models from before ModelGroupImportSettings by adding default import settings. if (model.importSettings.Keys.Count == 0) { foreach (var group in lxfml.groups) { importSettings.Add(group.number, new ModelGroupImportSettings()); } } else { foreach (var entry in model.importSettings) { importSettings.Add(entry.Key, entry.Value); } } foreach (var group in lxfml.groups) { if (!importSettings.ContainsKey(group.number)) { importSettings.Add(group.number, new ModelGroupImportSettings()); } } // Check if groups match up with the file. // FIXME Next version could include option to match groups up manually. // FIXME Check on group name? We do not have direct access to the groups from the model. var groupsMatch = true; foreach (var entry in importSettings) { if (entry.Key >= lxfml.groups.Length) { Debug.LogWarning("Group " + entry.Key + " does not match up with file."); groupsMatch = false; } } if (!groupsMatch) { EditorUtility.DisplayDialog("Reimport failed", "Model groups do not match up with groups in file. Check log for details", "Ok"); return; } ImportModel.model = model; group = null; GetWindow <ImportModel>(true, "LEGO Model Importer"); } else { ShowReadError(); } }
protected virtual void OnEnable() { modelGroup = (ModelGroup)target; processedProp = serializedObject.FindProperty("processed"); optimizationsProp = serializedObject.FindProperty("optimizations"); randomizeNormalsProp = serializedObject.FindProperty("randomizeNormals"); //imperfectionsProp = serializedObject.FindProperty("imperfections"); viewsProp = serializedObject.FindProperty("views"); // Collect views from other model groups. otherGroupViews.Clear(); var groups = GameObject.FindObjectsOfType <ModelGroup>(); foreach (var otherGroup in groups) { if (otherGroup != modelGroup) { foreach (var view in otherGroup.views) { if (!otherGroupViews.ContainsKey(otherGroup)) { otherGroupViews.Add(otherGroup, new List <CullingCameraConfig>()); } otherGroupViews[otherGroup].Add(view); } } } // Collect views from light sources. lightViews.Clear(); var lights = GameObject.FindObjectsOfType <Light>(); Bounds groupBounds = new Bounds(); bool hasComputedGroupBounds = false; foreach (var light in lights) { switch (light.type) { case LightType.Spot: { CullingCameraConfig view = new CullingCameraConfig() { name = light.name, perspective = true, position = light.transform.position, rotation = light.transform.rotation, fov = light.spotAngle, minRange = light.shadowNearPlane, maxRange = light.range, aspect = 1.0f }; lightViews.Add(light, view); break; } case LightType.Directional: { // Need to compute group bounds. if (!hasComputedGroupBounds) { var renderers = modelGroup.transform.GetComponentsInChildren <MeshRenderer>(); if (renderers.Length > 0) { groupBounds = new Bounds(renderers[0].bounds.center, renderers[0].bounds.size); foreach (var renderer in renderers) { groupBounds.Encapsulate(renderer.bounds); } } hasComputedGroupBounds = true; } // Find bounds' corners in light space. var corners = new List <Vector3>() { light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, -1, -1), groupBounds.extents)), light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, -1, 1), groupBounds.extents)), light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, 1, -1), groupBounds.extents)), light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, 1, 1), groupBounds.extents)), light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, -1, -1), groupBounds.extents)), light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, -1, 1), groupBounds.extents)), light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, 1, -1), groupBounds.extents)), light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, 1, 1), groupBounds.extents)) }; // Find min and max range for corners. var min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); var max = new Vector3(float.MinValue, float.MinValue, float.MinValue); foreach (var corner in corners) { min = Vector3.Min(min, corner); max = Vector3.Max(max, corner); } // Get size. Vector2 size = new Vector2(Mathf.Max(Mathf.Abs(min.x), Mathf.Abs(max.x)), Mathf.Max(Mathf.Abs(min.y), Mathf.Abs(max.y))); CullingCameraConfig view = new CullingCameraConfig() { name = light.name, perspective = false, position = light.transform.position, rotation = light.transform.rotation, size = size.y, minRange = min.z, maxRange = max.z, aspect = size.x / size.y }; lightViews.Add(light, view); break; } } } // Collect view from cameras. cameraViews.Clear(); var cameras = GameObject.FindObjectsOfType <Camera>(); foreach (var camera in cameras) { CullingCameraConfig view = new CullingCameraConfig() { name = camera.name, perspective = !camera.orthographic, position = camera.transform.position, rotation = camera.transform.rotation, size = camera.orthographicSize, fov = camera.fieldOfView, aspect = camera.aspect, minRange = camera.nearClipPlane, maxRange = camera.farClipPlane }; cameraViews.Add(camera, view); } }
public static void ReimportModelGroup(LXFMLDoc lxfml, ModelGroup group, ModelGroupImportSettings importSettings, bool detectConnectivity = false) { // Assign the new group import settings to the group. group.importSettings = importSettings; // We assume that the group can be found, so reimport it. if (group.processed) { // Remove all processed meshes. var renderers = group.GetComponentsInChildren <MeshRenderer>(); foreach (var renderer in renderers) { // FIXME Destroy the mesh? Prevents undo.. var filter = renderer.GetComponent <MeshFilter>(); //Undo.DestroyObjectImmediate(filter.sharedMesh); if (renderer.GetComponent <ModelGroup>() == null) { // Destroy submesh game objects entirely. Undo.DestroyObjectImmediate(renderer.gameObject); } else { // Destroy mesh related components on group game object. Object.DestroyImmediate(filter); Object.DestroyImmediate(renderer); } } } // FIXME Check if bricks are referenced. // FIXME Check if bricks have custom components attached. // Remove group bricks. var existingBricks = group.GetComponentsInChildren <Brick>(); foreach (var brick in existingBricks) { Undo.DestroyObjectImmediate(brick.gameObject); } var groupLightMapped = group.importSettings.isStatic && group.importSettings.lightmapped; SetStaticAndGIParams(group.gameObject, group.importSettings.isStatic, groupLightMapped); // Move group to origo to ensure that bricks are instantiated in the correct positions. var originalGroupParent = group.transform.parent; var originalGroupSiblingIndex = group.transform.GetSiblingIndex(); group.transform.parent = null; group.transform.localPosition = Vector3.zero; group.transform.localRotation = Quaternion.identity; group.transform.localScale = Vector3.one; // Create dictionary with just this group. var modelGroupImportSettingsDictionary = new DictionaryIntToModelGroupImportSettings(); modelGroupImportSettingsDictionary.Add(group.number, group.importSettings); // Instantiate group bricks. var resultBricks = new Dictionary <int, Brick>(lxfml.bricks.Count); InstantiateModelBricks(lxfml, modelGroupImportSettingsDictionary, ref resultBricks, group.number); // Assign bricks to group. foreach (var brick in resultBricks.Values) { brick.transform.SetParent(group.transform); } // Set parent of group back to original. group.transform.parent = originalGroupParent; group.transform.SetSiblingIndex(originalGroupSiblingIndex); if (detectConnectivity && group.importSettings.connectivity) { var sceneBricks = new HashSet <Brick>(StageUtility.GetCurrentStageHandle().FindComponentsOfType <Brick>()); DetectConnectivity(new HashSet <Brick>(resultBricks.Values), sceneBricks); } EditorUtility.ClearProgressBar(); }