/* Copyright (c) 2020 Omar Duarte Unauthorized copying of this file, via any medium is strictly prohibited. Writen by Omar Duarte, 2020. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System.Linq; using UnityEngine; namespace PluginMaster { [System.Serializable] public class SimulateGravityData { [SerializeField] private int _maxIterations = 1000; [SerializeField] private Vector3 _gravity = Physics.gravity; [SerializeField] private float _drag = 0f; [SerializeField] private float _angularDrag = 0.05f; [SerializeField] private float _maxSpeed = 100; private float _maxSpeedSquared = 10000; [SerializeField] private float _maxAngularSpeed = 10; private float _maxAngularSpeedSquared = 100; [SerializeField] private float _mass = 1f; [SerializeField] private bool _changeLayer = false; [SerializeField] private int _tempLayer = 0; [SerializeField] private bool _ignoreSceneColliders = false; public int maxIterations { get => _maxIterations; set { value = Mathf.Clamp(value, 1, 100000); if (_maxIterations == value) return; _maxIterations = value; } } public Vector3 gravity { get => _gravity; set { if (_gravity == value) return; _gravity = value; } } public float drag { get => _drag; set { value = Mathf.Max(value, 0f); if (_drag == value) return; _drag = value; } } public float angularDrag { get => _angularDrag; set { value = Mathf.Max(value, 0f); if (_angularDrag == value) return; _angularDrag = value; } } public float maxSpeed { get => _maxSpeed; set { value = Mathf.Max(value, 0f); if (_maxSpeed == value) return; _maxSpeed = value; _maxSpeedSquared = _maxSpeed * _maxSpeed; } } public float maxAngularSpeed { get => _maxAngularSpeed; set { value = Mathf.Max(value, 0f); if (_maxAngularSpeed == value) return; _maxAngularSpeed = value; _maxAngularSpeedSquared = _maxAngularSpeed * _maxAngularSpeed; } } public float maxSpeedSquared => _maxSpeedSquared; public float maxAngularSpeedSquared => _maxAngularSpeedSquared; public float mass { get => _mass; set { value = Mathf.Max(value, 1e-7f); if (_mass == value) return; _mass = value; } } public bool changeLayer { get => _changeLayer; set { if (_changeLayer == value) return; _changeLayer = value; } } public int tempLayer { get => _tempLayer; set { if (_tempLayer == value) return; _tempLayer = value; } } public bool ignoreSceneColliders { get => _ignoreSceneColliders; set { if (_ignoreSceneColliders == value) return; _ignoreSceneColliders = value; } } public void Copy(SimulateGravityData other) { _maxIterations = other._maxIterations; _gravity = other._gravity; _drag = other._drag; _angularDrag = other._angularDrag; _maxSpeed = other._maxSpeed; _maxSpeedSquared = other._maxSpeedSquared; _maxAngularSpeed = other._maxAngularSpeed; _maxAngularSpeedSquared = other._maxAngularSpeedSquared; _mass = other._mass; _changeLayer = other._changeLayer; _tempLayer = other._tempLayer; _ignoreSceneColliders = other._ignoreSceneColliders; } } public static class GravityUtils { private static bool _isPlaying = false; private static bool _stop = false; private static void AddCollider(GameObject obj, Mesh mesh, SimulateGravityData data) { var collider = MeshUtils.AddCollider(mesh, obj); if(collider is MeshCollider) (collider as MeshCollider).convex = true; if (data.changeLayer) obj.layer = data.tempLayer; } private static bool IsVisible(GameObject obj) { if (obj == null) return false; var target = obj; var parentRenderer = target.GetComponentInParent(); var parentTerrain = target.GetComponentInParent(); if (parentRenderer != null) target = parentRenderer.gameObject; else if (parentTerrain != null) target = parentTerrain.gameObject; else { var parent = target.transform.parent; if (parent != null) { var siblingRenderer = parent.GetComponentInChildren(); var siblingTerrain = parent.GetComponentInChildren(); if (siblingRenderer != null) target = parent.gameObject; else if (siblingTerrain != null) target = parent.gameObject; } } var renderers = target.GetComponentsInChildren(); if (renderers.Length > 0) { foreach (var renderer in renderers) if (renderer.enabled) return true; } var terrains = target.GetComponentsInChildren(); if (terrains.Length > 0) { foreach (var terrain in terrains) if (terrain.enabled) return true; } return false; } public static Pose[] SimulateGravity(GameObject[] selection, SimulateGravityData simData, bool recordAction) { if (_isPlaying && recordAction) return null; System.Collections.Generic.List tempColliders = null; Collider[] sceneColliders = null; Collider[] invisibleColliders = null; if (simData.ignoreSceneColliders) { sceneColliders = Object.FindObjectsOfType() .Where(sc => sc.enabled && sc.gameObject.activeInHierarchy && !sc.isTrigger).ToArray(); foreach (var sceneCollider in sceneColliders) sceneCollider.enabled = false; var sceneMeshFilters = Object.FindObjectsOfType() .Where(mf => mf.gameObject.activeInHierarchy && mf.sharedMesh != null).ToArray(); tempColliders = new System.Collections.Generic.List(); foreach (var meshFilter in sceneMeshFilters) { var mesh = meshFilter.sharedMesh; Collider tempCollider = MeshUtils.AddCollider(meshFilter.sharedMesh, meshFilter.gameObject); if(tempCollider != null) tempColliders.Add(tempCollider); } } else { invisibleColliders = Object.FindObjectsOfType() .Where(sc => sc.enabled && sc.gameObject.activeInHierarchy && !IsVisible(sc.gameObject)).ToArray(); foreach (var invisibleCollider in invisibleColliders) invisibleCollider.enabled = false; } var originalGravity = Physics.gravity; Physics.gravity = simData.gravity; var allBodies = Object.FindObjectsOfType(); var originalBodies = new System.Collections.Generic.List<(Rigidbody body, bool useGravity, bool isKinematic, float drag, float angularDrag, float mass, RigidbodyConstraints constraints, RigidbodyInterpolation interpolation, CollisionDetectionMode collisionDetectionMode)>(); foreach (var rigidBody in allBodies) { originalBodies.Add((rigidBody, rigidBody.useGravity, rigidBody.isKinematic, rigidBody.drag, rigidBody.angularDrag, rigidBody.mass, rigidBody.constraints, rigidBody.interpolation, rigidBody.collisionDetectionMode)); rigidBody.useGravity = false; rigidBody.isKinematic = true; rigidBody.drag = simData.drag; rigidBody.angularDrag = simData.angularDrag; rigidBody.mass = simData.mass; rigidBody.constraints = RigidbodyConstraints.None; rigidBody.interpolation = RigidbodyInterpolation.None; rigidBody.collisionDetectionMode = CollisionDetectionMode.Discrete; } var simBodies = new System.Collections.Generic.List(); var clones = new GameObject[selection.Length]; var animData = new System.Collections.Generic.List(); void AddColliders(GameObject source, GameObject dest) { var meshFilters = source.GetComponents(); foreach (var meshFilter in meshFilters) { var mesh = meshFilter.sharedMesh; if (mesh == null) continue; AddCollider(dest, mesh, simData); } var skinnedMeshRenderers = source.GetComponents(); foreach (var renderer in skinnedMeshRenderers) { var mesh = renderer.sharedMesh; if (mesh == null) continue; AddCollider(dest, mesh, simData); } foreach (Transform child in source.transform) { var destChild = new GameObject(); destChild.transform.SetParent(dest.transform); destChild.transform.localPosition = child.localPosition; destChild.transform.localRotation = child.localRotation; destChild.transform.localScale = child.localScale; AddColliders(child.gameObject, destChild); } } for (int i = 0; i < selection.Length; ++i) { animData.Add(new AnimData(selection[i])); var obj = new GameObject(); obj.layer = selection[i].layer; obj.transform.position = selection[i].transform.position; obj.transform.rotation = selection[i].transform.rotation; obj.transform.localScale = selection[i].transform.lossyScale; AddColliders(selection[i], obj); var magnitude = BoundsUtils.GetMagnitude(selection[i].transform); selection[i].transform.position += Vector3.up * (100 * magnitude); var rigidBody = obj.AddComponent(); if (simData.changeLayer) obj.layer = simData.tempLayer; simBodies.Add(rigidBody); rigidBody.useGravity = true; rigidBody.isKinematic = false; clones[i] = obj; } #if UNITY_2022_2_OR_NEWER var prevSimMode = Physics.simulationMode; Physics.simulationMode = SimulationMode.Script; #else Physics.autoSimulation = false; #endif for (int i = 0; i < simData.maxIterations; ++i) { Physics.Simulate(Time.fixedDeltaTime); for (int objIdx = 0; objIdx < selection.Length; ++objIdx) { var body = simBodies[objIdx]; if (body.velocity.sqrMagnitude > simData.maxSpeedSquared) body.velocity = body.velocity.normalized * simData.maxSpeed; if (body.angularVelocity.sqrMagnitude > simData.maxAngularSpeedSquared) body.angularVelocity = body.angularVelocity.normalized * simData.maxAngularSpeed; if (i % 10 == 0) animData[objIdx].poses.Add(new Pose(body.position, body.rotation)); } if (simBodies.All(rb => rb.IsSleeping())) break; } #if UNITY_2022_2_OR_NEWER Physics.simulationMode = prevSimMode; #else Physics.autoSimulation = true; #endif for (int i = 0; i < selection.Length; ++i) { selection[i].transform.position = clones[i].transform.position; selection[i].transform.rotation = clones[i].transform.rotation; animData[i].poses.Add(new Pose(selection[i].transform.position, selection[i].transform.rotation)); Object.DestroyImmediate(clones[i]); } foreach (var item in originalBodies) { if (item.body == null) continue; item.body.useGravity = item.useGravity; item.body.isKinematic = item.isKinematic; item.body.drag = item.drag; item.body.angularDrag = item.angularDrag; item.body.mass = item.mass; item.body.constraints = item.constraints; item.body.interpolation = item.interpolation; item.body.collisionDetectionMode = item.collisionDetectionMode; } Physics.gravity = originalGravity; if (simData.ignoreSceneColliders) { foreach (var sceneCollider in sceneColliders) sceneCollider.enabled = true; foreach (var tempCollider in tempColliders) Object.DestroyImmediate(tempCollider); } else { foreach (var invisibleCollider in invisibleColliders) invisibleCollider.enabled = true; } Animate(animData, simData, recordAction); var finalPoses = new System.Collections.Generic.List(); foreach (var d in animData) finalPoses.Add(d.poses.Last()); return finalPoses.ToArray(); } private class AnimData { public GameObject obj = null; public System.Collections.Generic.List poses = new System.Collections.Generic.List(); public System.Collections.Generic.List enabledColliders = new System.Collections.Generic.List(); public System.Collections.Generic.List tempColliders = new System.Collections.Generic.List(); public AnimData(GameObject obj) => this.obj = obj; } private static void Animate(System.Collections.Generic.List animData, SimulateGravityData simData, bool recordAction) { _stop = false; _isPlaying = true; foreach (var item in animData) { item.enabledColliders = item.obj.GetComponentsInChildren() .Where(collider => collider.enabled).ToList(); item.tempColliders.Clear(); var temp = new GameObject(); temp.hideFlags = HideFlags.HideAndDontSave; var lastPose = item.poses.Last(); temp.transform.position = lastPose.position; temp.transform.rotation = lastPose.rotation; temp.transform.localScale = item.obj.transform.lossyScale; var meshFilters = item.obj.GetComponentsInChildren(); foreach (var meshFilter in meshFilters) { var mesh = meshFilter.sharedMesh; if (mesh == null) continue; AddCollider(temp, mesh, simData); } var skinnedMeshRenderers = item.obj.GetComponentsInChildren(); foreach (var renderer in skinnedMeshRenderers) { var mesh = renderer.sharedMesh; if (mesh == null) continue; var meshFilter = renderer.gameObject.AddComponent(); meshFilter.sharedMesh = mesh; AddCollider(temp, mesh, simData); } item.tempColliders.Add(temp); foreach (var collider in item.enabledColliders) collider.enabled = false; } Animate(animData, 0, recordAction); } private async static void Animate(System.Collections.Generic.List data, int frame, bool recordAction) { void EnableColliders(AnimData item) { foreach (var collider in item.enabledColliders) { if (collider == null) continue; collider.enabled = true; } } void DestroyTempColliders(AnimData item) { foreach(var temp in item.tempColliders) UnityEngine.Object.DestroyImmediate(temp); } void Record() { foreach (var item in data) { if (item.obj == null) continue; EnableColliders(item); DestroyTempColliders(item); item.obj.transform.position = item.poses.First().position; item.obj.transform.rotation = item.poses.First().rotation; UnityEditor.Undo.RecordObject(item.obj.transform, "Simulate Gravity"); item.obj.transform.position = item.poses.Last().position; item.obj.transform.rotation = item.poses.Last().rotation; } } var isPlaying = false; if (_stop) { if (recordAction) Record(); return; } foreach (var item in data) { if (item.obj == null) break; if (frame >= item.poses.Count) { item.obj.transform.position = item.poses.Last().position; item.obj.transform.rotation = item.poses.Last().rotation; continue; } isPlaying = true; item.obj.transform.position = item.poses[frame].position; item.obj.transform.rotation = item.poses[frame].rotation; } if (isPlaying) { await System.Threading.Tasks.Task.Delay((int)(Time.fixedDeltaTime * 1000)); Animate(data, frame + 1, recordAction); } else { if (recordAction) Record(); else { foreach (var item in data) { if (item.obj == null) continue; EnableColliders(item); DestroyTempColliders(item); } } _isPlaying = false; } } [UnityEditor.InitializeOnLoad] private static class UndoEventHandler { static UndoEventHandler() => UnityEditor.Undo.undoRedoPerformed += OnUndoRedoPerformed; private static void OnUndoRedoPerformed() { _isPlaying = false; _stop = true; } } } }