public static void CreateSource(Light light) { // ASSUME: All children of light are sources foreach (var source in light.gameObject.Children()) { EP.Destroy(source); } // OPTION: Move light forward to prevent Z fighting when coplanar switch (light.type) { case LightType.Point: CreatePointSource(light); break; case LightType.Spot: CreateSpotSource(light); break; case LightType.Disc: CreateDiskSource(light); break; case LightType.Rectangle: CreateRectangleSource(light); break; default: // Directional light has no position source break; } }
// Derive transform information from a placeholder public static void ImportPlaceholder(MeshFilter meshFilter, bool rhinoBasis) { var sharedMesh = meshFilter.sharedMesh; var vertices = sharedMesh?.vertices; if (vertices == null || vertices.Length != 4) { Debug.LogWarning($"Inconsistent placeholder mesh: {meshFilter.Path()}.{sharedMesh.name}"); return; } var placeholder = meshFilter.transform; // OBSERVATION: At this stage of the import the layer parent transform is present // but on completion the layer may be absent, although the transform will persist. // NOTE: This also applies to the default import path. // Derive Rhino transform block basis in world coordinates var origin = placeholder.TransformPoint(vertices[0]); var basisX = placeholder.TransformPoint(vertices[1]) - origin; var basisY = placeholder.TransformPoint(vertices[2]) - origin; var basisZ = placeholder.TransformPoint(vertices[3]) - origin; if (rhinoBasis) { // Mesh basis describes transformation of Rhino basis var unityX = new Vector3(-basisX.x, basisX.y, -basisX.z); var unityY = new Vector3(-basisZ.x, basisZ.y, -basisZ.z); var unityZ = new Vector3(-basisY.x, basisY.y, -basisY.z); basisX = unityX; basisY = unityY; basisZ = unityZ; } // TODO: Use SVD to construct transform, which can include shear // TEMP: Assume transform is axial scaling followed by rotation only // NOTE: The origin and bases are simply the columns of an affine (3x4) transform matrix placeholder.localScale = new Vector3(basisX.magnitude, basisY.magnitude, basisZ.magnitude); placeholder.rotation = Quaternion.LookRotation(basisZ, basisY); placeholder.position = origin; // Remove meshes from placeholders // IMPORTANT: MeshRenderers must also be removed, in order to avoid // the application of renderer configations that could modify or remove the placeholder. var meshRenderer = meshFilter.GetComponent <MeshRenderer>(); if (meshRenderer) { EP.Destroy(meshRenderer); } EP.Destroy(meshFilter); }
/// <summary> /// Recursively configure all objects and components in hierarchy to be static /// </summary> /// <param name="gameObject">Root of hierarchy to be converted to static configuration</param> /// <param name="prefabs">Apply static conversion overrides to prefabs in hierarchy</param> public static void ApplyTo(GameObject gameObject, bool prefabs = false) { using (var editScope = new EP.EditGameObject(gameObject)) { var editObject = editScope.editObject; // Set static configurations GameObjectSetStatic(editObject); foreach (var renderer in editObject.GetComponents <Renderer>()) { RendererSetStatic(renderer); } foreach (var collider in editObject.GetComponents <Collider>()) { ColliderSetStatic(collider); } foreach (var light in editObject.GetComponents <Light>()) { LightSetStatic(light); } // Remove dynamic components foreach (var monoBehavior in editObject.GetComponents <MonoBehaviour>()) { EP.Destroy(monoBehavior); } foreach (var physics in editObject.GetComponents <Rigidbody>()) { EP.Destroy(physics); } foreach (var child in editObject.Children()) { // Limit prefab recursion var prefabAssetType = PrefabUtility.GetPrefabAssetType(child); if (prefabAssetType == PrefabAssetType.MissingAsset) { continue; } if (prefabAssetType != PrefabAssetType.NotAPrefab && !prefabs) { continue; } ApplyTo(child); } } }
/// <summary> /// Removes all empty branches in the hierarchy of a GameObject /// </summary> /// <remarks> /// Excluding cameras or lights from a model import removes components, /// but their associated GameObjects will persist in the hierarchy. /// </remarks> public static void RemoveEmpty(GameObject gameObject) { // IMPORTANT: Before counting children, apply RemoveEmpty to children // since their removal could result in children being empty var children = gameObject.transform.Children(); foreach (var child in children) { RemoveEmpty(child.gameObject); } // IMPORTANT: Before counting components, remove empty meshes // since this could result in components being empty var meshFilter = gameObject.GetComponent <MeshFilter>(); if (meshFilter) { var sharedMesh = meshFilter.sharedMesh; if (sharedMesh == null || sharedMesh.vertexCount == 0) { var meshRenderer = gameObject.GetComponent <MeshRenderer>(); if (meshRenderer != null) { EP.Destroy(meshRenderer); } var meshCollider = gameObject.GetComponent <MeshCollider>(); if (meshCollider != null) { EP.Destroy(meshCollider); } EP.Destroy(meshFilter); } } // Remove if no children and no components other than transform if ( gameObject.transform.childCount == 0 && gameObject.GetComponents <Component>().Length == 1 ) { EP.Destroy(gameObject); return; } }
// Find or make a prefab adjacent to the merged asset folder static void CreateMerged(string path, Dictionary <string, GameObject> mergedPrefabs) { if (!mergedPrefabs.ContainsKey(path)) { // Find or make a merged prefab var mergedPath = path + ".prefab"; var merged = AssetDatabase.LoadAssetAtPath <GameObject>(mergedPath); if (!merged) { var empty = EP.Instantiate(); empty.name = path.Substring((path.LastIndexOf('/') + 1)); // WARNING: SaveAsPrefabAsset will return null while AssetDatabase.StartAssetEditing() pertains merged = PrefabUtility.SaveAsPrefabAsset(empty, mergedPath); EP.Destroy(empty); //Debug.Log($"Created empty merged object: {pathPart}.prefab"); } mergedPrefabs.Add(path, merged); } }
// WARNING: If there is a name override, PathName will not resolve! // TODO: Find a way to clone prefab with overrides intact. // QUESTION: Is there a way to accomplish instatiation using object serializations? // Ideally, this would handle the connection and override persistence. // https://docs.unity3d.com/ScriptReference/SerializedObject.html // Construct, then iterate & copy? static GameObject InstantiateChild(GameObject original, GameObject parent) { GameObject child = null; // IMPORTANT: PrefabUtility.InstantiatePrefab applies only to assets, not to instances // IMPORTANT: PrefabUtility.GetCorrespondingObjectFromSource applies only to instances, not to assets var asset = PrefabUtility.GetCorrespondingObjectFromSource(parent); GameObject copy_asset = null; if (asset) { copy_asset = asset; } else { copy_asset = original; } var copy = PrefabUtility.InstantiatePrefab(copy_asset) as GameObject; if (parent != original) { var path = new PathName(original, parent); var find = path.Find(copy.transform); if (find.Length == 1) { child = find[0].gameObject; // Unpack to enable orphaning, only once since nearest root was instantiated var unpack = PrefabUtility.GetOutermostPrefabInstanceRoot(child); while (unpack) { PrefabUtility.UnpackPrefabInstance(unpack, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction); unpack = PrefabUtility.GetOutermostPrefabInstanceRoot(child); } child.transform.SetParent(null); } EP.Destroy(copy); } else { child = copy; } return(child); }
public static void ApplyTo(GameObject mergeTarget, params GameObject[] mergeSources) { using (var editScope = new EP.EditGameObject(mergeTarget)) { var targetEdit = editScope.editObject; foreach (var mergeSource in mergeSources) { var sourceCopy = EP.Instantiate(mergeSource); // Unpack only root model prefab, constituent prefab links will be retained // NOTE: Applying UnpackPrefabInstance to a non-prefab object results in a crash if (PrefabUtility.GetPrefabAssetType(sourceCopy) != PrefabAssetType.NotAPrefab) { PrefabUtility.UnpackPrefabInstance(sourceCopy, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction); } Merge(sourceCopy.transform, targetEdit.transform); EP.Destroy(sourceCopy); } } }
public static string CreateScene(string path, params GameObject[] gameObjects) { var scenePath = path + ".unity"; // PROBLEM: When using NewSceneMode.Single during import assertion "GetApplication().MayUpdate()" fails // SOLUTION: During import, using Additive loading works. // PROBLEM: InvalidOperationException: Cannot create a new scene additively with an untitled scene unsaved. // NOTE: This can occur when the previously opened scene has ceased to exist, in particular when a project is opened. var scene = EditorSceneManager.GetActiveScene(); var addNew = scene.name.Length > 0 && scene.path.Length > 0 && scene.path != scenePath; // scene.IsValid() will be true even when path and name are empty if (addNew) { scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive); } else { // Remove all default scene objects foreach (var rootObject in scene.GetRootGameObjects()) { EP.Destroy(rootObject); } } EditorSceneManager.SetActiveScene(scene); // Add objects to scene foreach (var gameObject in gameObjects) { EP.Instantiate(gameObject); } // WARNING: If scene is created during asset import physics computations will not be initialized // PROBLEM: At end of import the open scene will have been modified, so a pop-up will appear. // SOLUTION: After loading the scene in additive mode, close it. EditorSceneManager.SaveScene(scene, scenePath); if (addNew) { EditorSceneManager.CloseScene(scene, true); } return(scenePath); }
public static void ApplyTo(GameObject gameObject, string pathRoot) { var assetType = PrefabUtility.GetPrefabAssetType(gameObject); if (assetType == PrefabAssetType.MissingAsset) { return; } if (assetType == PrefabAssetType.Model) { // In the case of a model created an editable prefab var prefab = EP.Instantiate(gameObject); PrefabUtility.UnpackPrefabInstance(prefab, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction); gameObject = PrefabUtility.SaveAsPrefabAsset(prefab, pathRoot + ".prefab"); EP.Destroy(prefab); } // NOTE: Calling CopyAssets will be extremely slow if each asset is imported individually. // Instead, all of the copying will be done in one batch, after which assets will be imported. // Then, a new instance of AssetGatherer will find those copies and make replacements in // a second batch. try { AssetDatabase.StartAssetEditing(); var assertGatherer = new AssetGatherer(pathRoot); assertGatherer.CopyAssets(gameObject); } finally { AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } try { AssetDatabase.StartAssetEditing(); var assertGatherer = new AssetGatherer(pathRoot); assertGatherer.SwapAssets(gameObject); } finally { AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } }
static void ReplacePlaceholders(string prefabPath, GameObject gameObject) { Dictionary <string, CachedPrefab> prefabs = GetPrefabs(prefabPath); var children = gameObject.Children(true); foreach (var child in children) { // Do not modify existing child prefabs var childPrefab = PrefabUtility.GetNearestPrefabInstanceRoot(child); if (childPrefab != null && childPrefab != gameObject) { continue; } // Placeholder names are constructed as "prefab_name=object_name" var name_parts = child.name.Split('='); if (name_parts.Length == 1) { continue; } // Create an placeholder prefab that can be modified after import if (!prefabs.ContainsKey(name_parts[0])) { var placeholder = EP.Instantiate(); placeholder.name = name_parts[0]; var placeholderPath = prefabPath + "/" + placeholder.name + ".prefab"; var placeholderAsset = PrefabUtility.SaveAsPrefabAsset(placeholder, placeholderPath); prefabs[name_parts[0]] = new CachedPrefab(placeholderAsset); EP.Destroy(placeholder); //Debug.Log($"Missing prefab in {gameObject.name} for {child.Path()} -> created placeholder"); } ConfigurePrefab(child.transform, prefabs[name_parts[0]]); EP.Destroy(child); } }
/// <summary> /// Configure the light probe proxy volume for lower levels of detail in group /// </summary> public static void ConfigureLODGroup(LODGroup lodGroup) { // TODO: Check if group is static - if not, also configure the lowest level of detail. // Only lower levels of detail will use probes // IMPORTANT: Proxy volumes always update, so only generate them if they will be used. var lods = lodGroup.GetLODs(); if (lods.Length < 2) { return; } // FIXME: Local bounds are needed var worldBounds = RendererWorldBounds(lodGroup.gameObject); // TEMP: Assume that only axis swaps pertain var localBounds = new Bounds(); localBounds.center = lodGroup.transform.InverseTransformPoint(worldBounds.center); localBounds.size = lodGroup.transform.InverseTransformDirection(worldBounds.size); localBounds.size = new Vector3(Mathf.Abs(localBounds.size.x), Mathf.Abs(localBounds.size.y), Mathf.Abs(localBounds.size.z)); // If object bounds > probe spacing in any dimension use a proxy volume var useProxy = false; for (var i = 0; i < 3; ++i) { useProxy |= localBounds.size[i] > probeSpaces[i]; } var proxy = lodGroup.gameObject.GetComponent <LightProbeProxyVolume>(); if (useProxy) { if (!proxy) { proxy = EP.AddComponent <LightProbeProxyVolume>(lodGroup.gameObject); } // Configure proxy bounds proxy.boundingBoxMode = LightProbeProxyVolume.BoundingBoxMode.Custom; proxy.originCustom = localBounds.center; proxy.sizeCustom = localBounds.size; // Configure spacing proxy.probePositionMode = LightProbeProxyVolume.ProbePositionMode.CellCorner; proxy.resolutionMode = LightProbeProxyVolume.ResolutionMode.Custom; proxy.gridResolutionX = ProxyResolution(localBounds.size.x / probeSpaces.x); proxy.gridResolutionY = ProxyResolution(localBounds.size.y / probeSpaces.y); proxy.gridResolutionZ = ProxyResolution(localBounds.size.z / probeSpaces.z); // Remaining settings proxy.qualityMode = LightProbeProxyVolume.QualityMode.Normal; proxy.refreshMode = LightProbeProxyVolume.RefreshMode.Automatic; } else { if (proxy) { EP.Destroy(proxy); } proxy = null; } // Configure all lower levels of detail to use probes for (var l = 1; l < lods.Length; ++l) { foreach (var renderer in lods[l].renderers) { renderer.lightProbeUsage = useProxy ? UnityEngine.Rendering.LightProbeUsage.UseProxyVolume : UnityEngine.Rendering.LightProbeUsage.BlendProbes; renderer.lightProbeProxyVolumeOverride = lodGroup.gameObject; var meshRender = renderer as MeshRenderer; if (!meshRender) { continue; } meshRender.receiveGI = ReceiveGI.LightProbes; } } }
static void MergeGroup(string pathName, List <GameObject> group) { // Gather LODGroup and Renderers LODGroup lodGroup = null; var renderers = new List <RendererSort>(); foreach (var gameObject in group) { var renderer = gameObject.GetComponent <Renderer>(); if (renderer) { renderers.Add(new RendererSort(renderer)); } var isGroup = gameObject.GetComponent <LODGroup>(); if (isGroup) { var lods = isGroup.GetLODs(); foreach (var lod in lods) { foreach (var lodRenderer in lod.renderers) { // Renderers must begin as siblings of LODGroup EP.SetParent(lodRenderer.transform, isGroup.transform.parent); renderers.Add(new RendererSort(lodRenderer)); } } if (!!lodGroup || !!renderer) { // LODGroup manager cannot be duplicated, and cannot have renderer component Debug.LogWarning($"Removing LODGroup found on {gameObject.Path()}"); EP.Destroy(isGroup); continue; } lodGroup = isGroup; } } if (!lodGroup) { lodGroup = EP.AddComponent <LODGroup>(EP.Instantiate()); } // renderers[0] has the lowest vertex count renderers.Sort((l, m) => l.vertexCount - m.vertexCount); // Remove missing meshes and duplicate levels of detail var vertexCount = 0; var removeRenderers = new List <RendererSort>(); foreach (var renderer in renderers) { if (vertexCount == renderer.vertexCount) { removeRenderers.Add(renderer); continue; } vertexCount = renderer.vertexCount; } foreach (var renderer in removeRenderers) { renderers.Remove(renderer); EP.Destroy(renderer.renderer.gameObject); // NOTE: Duplicate mesh asset could be removed } if (renderers.Count == 0) { EP.Destroy(lodGroup.gameObject); return; } // renderers[0] has the highest vertrex count renderers.Reverse(); // Configure manager in hierarchy lodGroup.gameObject.name = pathName.Substring(pathName.LastIndexOf('/') + 1); EP.SetParent(lodGroup.transform, renderers[0].renderer.transform.parent); lodGroup.transform.localPosition = renderers[0].renderer.transform.localPosition; lodGroup.transform.localRotation = renderers[0].renderer.transform.localRotation; lodGroup.transform.localScale = renderers[0].renderer.transform.localScale; for (var r = 0; r < renderers.Count; ++r) { var renderer = renderers[r].renderer; // TODO: Used PathNameExtension for this! var lodIndex = renderer.gameObject.name.LastIndexOf(lodSuffix); if (lodIndex >= 0) { renderer.gameObject.name = renderer.gameObject.name.Substring(0, lodIndex); } renderer.gameObject.name += lodSuffix + r.ToString(); EP.SetParent(renderer.transform, lodGroup.transform); } // Configure the group var lodList = new LOD[renderers.Count]; for (var r = 0; r < renderers.Count; ++r) { lodList[r].renderers = new[] { renderers[r].renderer } } ; ConfigureLODGroup(lodGroup, lodList); // Configure the renderers and materials foreach (var lod in lodGroup.GetLODs()) { foreach (var renderer in lod.renderers) { SetLightmapScale(renderer); foreach (var material in renderer.sharedMaterials) { UseFadingShader(material); } } } }
// Derive light configuration from a placeholder public static void ConfigureLight(Light light) { var children = light.transform.parent.Children(); Transform placeholder = null; foreach (var child in children) { // NOTE: Use string.Contains since if (!child.name.Contains("=" + light.name)) { continue; } placeholder = child; break; } if (!placeholder) { Debug.LogWarning($"Missing placeholder for {light.Path()}"); return; } // Apply rigid transform light.transform.localPosition = placeholder.localPosition; light.transform.localRotation = placeholder.localRotation; light.transform.localScale = Vector3.one; // Configure using transform local scale parameters // NOTE: Disk Lights are not exported in Rhino // NOTE: Volume light type are not imported to Unity, and is not exported by Rhino var lightType = placeholder.name.Split('=')[0]; switch (lightType) { case "DirectionalLight": light.type = LightType.Directional; break; case "PointLight": light.type = LightType.Point; break; case "SpotLight": light.type = LightType.Spot; light.spotAngle = Mathf.Atan2(placeholder.localScale.z, placeholder.localScale.x) * Mathf.Rad2Deg * 2f; light.innerSpotAngle = Mathf.Atan2(placeholder.localScale.z, placeholder.localScale.y) * Mathf.Rad2Deg * 2f; break; case "RectangularLight": light.type = LightType.Rectangle; light.areaSize = new Vector2(placeholder.localScale.x * 2f, placeholder.localScale.y * 2f); break; case "LinearLight": // NOTE: Linear lights are not supported in Unity // A subsequent conversion to a collection of finite-size rectangular lights will be required light.type = LightType.Rectangle; light.areaSize = new Vector2(placeholder.localScale.x * 2f, 0f); break; default: Debug.LogWarning($"Unsupported light type {lightType} -> configure as point light"); light.type = LightType.Point; light.enabled = false; break; } EP.Destroy(placeholder.gameObject); }
/// <summary> /// Copies asset of type T to specified persistent path /// </summary> /// <remarks> /// If the specified path does not exist, it will be created using EP.CreatePersistentPath. /// The asset name will be a unique derivative of the name of the asset object. /// If assetSuffix is not specified the suffix corresponding to the asset type will be used. /// /// WARNING: When AssetDatabase.StartAssetEditing() has been called, AssetDatabase.CopyAsset /// and PrefabUtility.SaveAsPrefabAsset will not modify AssetDatabase, in which case the returned /// asset reference will be the asset argument. /// </remarks> /// <typeparam name="T">Asset type</typeparam> /// <param name="asset">Asset to be copied</param> /// <param name="assetPath">Path relative to persistent folder where asset will be created</param> /// <param name="assetSuffix">Optional override of default suffix for asset type</param> /// <returns>The asset copy, or the origin of AssetDatabase has not been updated</returns> public static T CopyAssetToPath <T>(T asset, string assetPath, string assetSuffix = null) where T : Object { // Ensure that the asset path folders exist EP.CreatePersistentPath(assetPath); T assetCopy = null; if (useEditorAction) { #if UNITY_EDITOR // Match asset and file type, and ensure that asset will be unique if (assetSuffix == null) { assetSuffix = ".asset"; var oldPath = AssetDatabase.GetAssetPath(asset); if (oldPath != null) { // Preserve existing file type (important for images and models) assetSuffix = oldPath.Substring(oldPath.LastIndexOf('.')); } else { // Use default file type for asset type // https://docs.unity3d.com/ScriptReference/AssetDatabase.CreateAsset.html if (asset is GameObject) { assetSuffix = ".prefab"; } if (asset is Material) { assetSuffix = ".mat"; } if (asset is Cubemap) { assetSuffix = ".cubemap"; } if (asset is GUISkin) { assetSuffix = ".GUISkin"; } if (asset is Animation) { assetSuffix = ".anim"; } // TODO: Cover other specialized types such as giparams } } var newPath = "Assets/" + assetPath + "/" + asset.name + assetSuffix; newPath = AssetDatabase.GenerateUniqueAssetPath(newPath); // WARNING: (Unity2019.4 undocumented) AssetDatabase.GenerateUniqueAssetPath will trim name spaces // Copy asset to new path // NOTE: The goal is to keep the original asset in place, // so AssetDatabase.ExtractAsset is not used. if (asset is GameObject) { var gameObject = asset as GameObject; if (PrefabUtility.GetPrefabAssetType(gameObject) == PrefabAssetType.NotAPrefab) { assetCopy = PrefabUtility.SaveAsPrefabAsset(gameObject, newPath) as T; } else { // NOTE: PrefabUtility.SaveAsPrefabAsset cannot save an asset reference as a prefab var copyObject = PrefabUtility.InstantiatePrefab(gameObject) as GameObject; // NOTE: Root must be unpacked, otherwise this will yield a prefab variant PrefabUtility.UnpackPrefabInstance(copyObject, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction); assetCopy = PrefabUtility.SaveAsPrefabAsset(copyObject, newPath) as T; EP.Destroy(copyObject); } } else { // IMPORTANT: SubAssets must be instantiated, otherise AssetDatabase.CreateAsset(asset, newPath) will fail // with error: "Couldn't add object to asset file because the Mesh [] is already an asset at [].fbx" // NOTE: AssetDatabase.ExtractAsset will register as a modification of model import settings if (AssetDatabase.IsSubAsset(asset)) { AssetDatabase.CreateAsset(Object.Instantiate(asset), newPath); } else { AssetDatabase.CopyAsset(AssetDatabase.GetAssetPath(asset), newPath); } } assetCopy = AssetDatabase.LoadAssetAtPath <T>(newPath); #endif } else { Debug.LogWarning("Runtime asset copying is not implemented"); // TODO: For runtime version simply copy the file! } if (!assetCopy) { return(asset); // Continue to use asset at previous path } return(assetCopy as T); }