using UnityEngine; using System.Collections.Generic; namespace NobleMuffins.LimbHacker.Guts { public class LimbHackerAgent : MonoBehaviour { public Mesh[] preloadMeshes; public WorkerThreadMode workerThreadMode; private readonly HashSet jobStates = new HashSet (); private readonly List jobStateQueue = new List (); private readonly ICollection jobStateRemovalQueue = new List (); private readonly IDictionary preloadedMeshes = new Dictionary (); private static LimbHackerAgent _instance; public static LimbHackerAgent instance { get { if (_instance == null) { GameObject go = new GameObject (); _instance = go.AddComponent (); } return _instance; } } public void Awake () { #if UNITY_WEBGL if (workerThreadMode == WorkerThreadMode.Asynchronous) { Debug.LogWarning ("Turbo Slicer will run synchronously because WebGL does not support threads.", this); workerThreadMode = WorkerThreadMode.Synchronous; } #endif if (preloadMeshes != null) { for (int i = 0; i < preloadMeshes.Length; i++) { var mesh = preloadMeshes [i]; var indices = new int[mesh.subMeshCount][]; for (int j = 0; j < mesh.subMeshCount; j++) { indices [j] = mesh.GetIndices (j); } //Note that this is NOT a usable mesh snapshot. It will need to be combined with live data at runtime. var rom = new MeshSnapshot (null, mesh.vertices, mesh.normals, mesh.uv, mesh.tangents, mesh.boneWeights, new Material[0], new BoneMetadata[0], null, indices); preloadedMeshes [mesh.GetInstanceID ()] = rom; } } } // Use this for initialization void Start () { _instance = this; } //These buffers are used in ConsumeJobYield. This method is executed only on the event dispatch thread and therefore will not be clobbered. readonly Dictionary bonePresenceAlfaBuffer = new Dictionary (); readonly Dictionary bonePresenceBravoBuffer = new Dictionary (); void ConsumeJobYield (JobState jobState) { if (jobState.HasException) { Debug.LogException (jobState.Exception); } else { var jobSpecification = jobState.Specification; var jobYield = jobState.Yield; bonePresenceAlfaBuffer.Clear (); bonePresenceBravoBuffer.Clear (); foreach (var kvp in jobSpecification.NodeMetadata) { bonePresenceAlfaBuffer [kvp.Key] = kvp.Value.IsConsideredSevered; bonePresenceBravoBuffer [kvp.Key] = !kvp.Value.IsConsideredSevered; } var originalSubjectTransform = jobSpecification.Subject.transform; bool useAlternateForFront, useAlternateForBack; if (jobSpecification.Hackable.alternatePrefab == null) { useAlternateForFront = false; useAlternateForBack = false; } else { useAlternateForFront = jobSpecification.Hackable.cloneAlternate (bonePresenceAlfaBuffer); useAlternateForBack = jobSpecification.Hackable.cloneAlternate (bonePresenceBravoBuffer); } GameObject alfaObject, bravoObject; var backIsNew = useAlternateForBack; if (backIsNew) { var backSource = useAlternateForBack ? jobSpecification.Hackable.alternatePrefab : jobSpecification.Subject; bravoObject = (GameObject)Instantiate (backSource); bravoObject.name = string.Format ("{0} (Bravo)", jobSpecification.Subject); } else bravoObject = jobSpecification.Subject; var alfaSource = useAlternateForFront ? jobSpecification.Hackable.alternatePrefab : jobSpecification.Subject; alfaObject = (GameObject)Instantiate (alfaSource); HandleHierarchy (alfaObject.transform, bonePresenceAlfaBuffer, jobSpecification.NodeMetadata); HandleHierarchy (bravoObject.transform, bonePresenceBravoBuffer, jobSpecification.NodeMetadata); var parent = originalSubjectTransform.parent; var position = originalSubjectTransform.localPosition; var scale = originalSubjectTransform.localScale; var rotation = originalSubjectTransform.localRotation; alfaObject.transform.parent = parent; alfaObject.transform.localPosition = position; alfaObject.transform.localScale = scale; alfaObject.transform.localRotation = rotation; alfaObject.layer = jobSpecification.Subject.layer; alfaObject.name = string.Format ("{0} (Alfa)", jobSpecification.Subject); if (backIsNew) { bravoObject.transform.parent = parent; bravoObject.transform.localPosition = position; bravoObject.transform.localScale = scale; bravoObject.transform.localRotation = rotation; bravoObject.layer = jobSpecification.Subject.layer; } ApplySnapshotsToRoot (alfaObject, jobYield.Alfa); ApplySnapshotsToRoot (bravoObject, jobYield.Bravo); var results = new GameObject[] { alfaObject, bravoObject }; jobSpecification.Hackable.handleSlice (results, jobState.Yield.PlaneInWorldSpace, jobState.Yield.FocalPointInWorldSpace); if (backIsNew) { // print ("destroy sliced object value " + jobSpecification.Hackable.ignoreDestroyOriginalObject); if (jobSpecification.Hackable.ignoreDestroyOriginalObject) { jobSpecification.Hackable.eventsOnIgnoreDestroyOriginalObject.Invoke (); if (jobSpecification.Hackable.setCustomIDOnSliceSpieces) { Hackable alfaObjectHackable = alfaObject.GetComponent (); if (alfaObjectHackable != null) { alfaObjectHackable.setCustomIDOnSliceSpieces = true; alfaObjectHackable.setRandomString (jobSpecification.Hackable.getRandomString ()); } Hackable bravoObjectHackable = bravoObject.GetComponent (); if (bravoObjectHackable != null) { bravoObjectHackable.setCustomIDOnSliceSpieces = true; bravoObjectHackable.setRandomString (jobSpecification.Hackable.getRandomString ()); } } } else { Destroy (jobSpecification.Subject); } } } } //These buffers are used in HandleHierarchy. This method is executed only on the event dispatch thread and therefore will not be clobbered. readonly ICollection boneBuffer = new HashSet (); readonly ICollection rendererHolderBuffer = new HashSet (); readonly List childrenBuffer = new List (); private void HandleHierarchy (Transform root, Dictionary bonePresenceByName, IDictionary originalsByName) { boneBuffer.Clear (); var smrs = root.GetComponentsInChildren (); rendererHolderBuffer.Clear (); foreach (var smr in smrs) { rendererHolderBuffer.Add (smr.gameObject); var _bones = smr.bones; for (int i = 0; i < _bones.Length; i++) { var bone = _bones [i]; boneBuffer.Add (bone); // Hierarchies often have transforms between bones and the root that are not // part of the bones collection pulled from the SMR. However if we turn these // intermediaries off, the ragdoll will not work. For the purposes of this // procedure, we're going to treat these AS bones. boneBuffer.Add (bone.parent); } } childrenBuffer.Clear (); if (childrenBuffer.Capacity < bonePresenceByName.Count) { childrenBuffer.Capacity = bonePresenceByName.Count; } ConcatenateHierarchy (root, childrenBuffer); for (int i = 0; i < childrenBuffer.Count; i++) { var t = childrenBuffer [i]; var go = t.gameObject; var thisIsTheSkinnedMeshRenderer = rendererHolderBuffer.Contains (go); var shouldBePresent = true; var presenceKeySource = t; do { string presenceKey = presenceKeySource.name; if (bonePresenceByName.ContainsKey (presenceKey)) { shouldBePresent = bonePresenceByName [presenceKey]; break; } else { presenceKeySource = presenceKeySource.parent; } } while (childrenBuffer.Contains (presenceKeySource)); NodeMetadata sourceMetadata; if (originalsByName.TryGetValue (t.name, out sourceMetadata)) { t.localPosition = sourceMetadata.LocalPosition; t.localRotation = sourceMetadata.LocalRotation; t.localScale = sourceMetadata.LocalScale; shouldBePresent &= sourceMetadata.IsActive; } bool isBone = boneBuffer.Contains (t); if (!shouldBePresent && isBone) { var c = t.GetComponent (); if (c != null) { c.enabled = shouldBePresent; } var r = t.GetComponent (); if (r != null) { r.mass = float.Epsilon; } } else { shouldBePresent |= thisIsTheSkinnedMeshRenderer; go.SetActive (shouldBePresent || thisIsTheSkinnedMeshRenderer); } } } private ICollection GetConcatenatedHierarchy (Transform t) { var children = new HashSet () as ICollection; ConcatenateHierarchy (t, children); return children; } static void ConcatenateHierarchy (Transform root, ICollection resultBuffer) { for (int i = 0; i < root.childCount; i++) { var child = root.GetChild (i); resultBuffer.Add (child); ConcatenateHierarchy (child, resultBuffer); } } static void ApplySnapshotsToRoot (GameObject root, IEnumerable snapshots) { var skinnedMeshRenderers = root.GetComponentsInChildren (true); foreach (var snapshot in snapshots) { for (int i = 0; i < skinnedMeshRenderers.Length; i++) { if (skinnedMeshRenderers [i] != null) { if (skinnedMeshRenderers [i].name.Equals (snapshot.key)) { var skinnedMeshRenderer = skinnedMeshRenderers [i]; if (snapshot.vertices.Length > 0) { var bindPoses = new Matrix4x4[snapshot.boneMetadata.Length]; for (int j = 0; j < bindPoses.Length; j++) { bindPoses [j] = snapshot.boneMetadata [j].BindPose; } //Note that we do not explicitly call recalculate bounds because (as per the manual) this is implicit in an //assignment to vertices whenever the vertex count changes from zero to non-zero. var mesh = new Mesh (); skinnedMeshRenderer.materials = snapshot.materials; mesh.vertices = snapshot.vertices; mesh.normals = snapshot.normals; mesh.uv = snapshot.coords; mesh.boneWeights = snapshot.boneWeights; mesh.tangents = snapshot.tangents; mesh.subMeshCount = snapshot.indices.Length; mesh.bindposes = bindPoses; for (int j = 0; j < snapshot.indices.Length; j++) { mesh.SetTriangles (snapshot.indices [j], j, false); } mesh.UploadMeshData (true); skinnedMeshRenderer.sharedMesh = mesh; } else { DestroyImmediate (skinnedMeshRenderer); } break; } } } } var forwardPassAgent = root.GetComponent (); if (forwardPassAgent == null) forwardPassAgent = root.AddComponent (); forwardPassAgent.Snapshot = snapshots; } public static bool DetermineSlice (Hackable hackable, Vector3 pointInWorldSpace, ref string boneName, ref float rootTipProgression) { const int nothing = -1; var severables = hackable.severables; //in progress to add an option to priorize certain body parts to slice //Transform [] temporalSeverables = new Transform [2]; //for (var i = 0; i < temporalSeverables.Length; i++) { // temporalSeverables [i] = severables [i]; //} //severables = new Transform [temporalSeverables.Length]; //for (var i = 0; i < temporalSeverables.Length; i++) { // severables [i] = temporalSeverables [i]; //} //print (severables.Length); var indexByObject = new Dictionary (); for (var i = 0; i < severables.Length; i++) { indexByObject [severables [i]] = i; } var severablesInThreeSpace = new Vector3[severables.Length]; for (var i = 0; i < severables.Length; i++) { severablesInThreeSpace [i] = severables [i].position; } var deltas = new Vector3[severables.Length]; for (var i = 0; i < severables.Length; i++) { deltas [i] = severablesInThreeSpace [i] - pointInWorldSpace; } var mags = new float[severables.Length]; for (var i = 0; i < severables.Length; i++) { mags [i] = deltas [i].magnitude; } var indexOfNearestThing = nothing; var distanceToNearestThing = float.PositiveInfinity; for (var i = 0; i < severables.Length; i++) { if (mags [i] < distanceToNearestThing) { indexOfNearestThing = i; distanceToNearestThing = mags [i]; } } if (indexOfNearestThing != nothing) { var nearestThing = severables [indexOfNearestThing]; if (indexByObject.ContainsKey (nearestThing.parent)) { var parentIndex = indexByObject [nearestThing.parent]; var hereDelta = severablesInThreeSpace [indexOfNearestThing] - severablesInThreeSpace [parentIndex]; var touchDelta = pointInWorldSpace - severablesInThreeSpace [parentIndex]; //If the touch is closer to the parent than the severable is, than it's between them. //We'll use that and then use the root tip progression to slice just the right spot. if (touchDelta.magnitude < hereDelta.magnitude) { indexOfNearestThing = parentIndex; nearestThing = severables [indexOfNearestThing]; } } var childIndices = new List (); for (var i = 0; i < severables.Length; i++) { var candidate = severables [i]; if (candidate.parent == nearestThing) { childIndices.Add (i); } } rootTipProgression = 0f; if (childIndices.Count > 0) { var aggregatedChildPositions = Vector3.zero; foreach (var i in childIndices) { aggregatedChildPositions += severablesInThreeSpace [i]; } var meanChildPosition = aggregatedChildPositions / childIndices.Count; var alfa = (pointInWorldSpace - nearestThing.position).sqrMagnitude; var bravo = (pointInWorldSpace - meanChildPosition).sqrMagnitude; rootTipProgression = Mathf.Clamp (alfa / bravo, 0.0f, 0.99f); } boneName = nearestThing.name; return true; } else { return false; } } public void SeverByJoint (GameObject subject, string jointName, float rootTipProgression, Vector3? planeNormal) { //Sanity check: are we already slicing this? foreach (var extantState in jobStates) { if (ReferenceEquals (extantState.Specification.Subject, subject)) { //Debug.LogErrorFormat("Turbo Slicer was asked to slice '{0}' but this target is already enqueued.", subject.name); return; } } rootTipProgression = Mathf.Clamp01 (rootTipProgression); //These here are in local space because they're only used to copy to the resultant meshes; they're not used //to transform the vertices. We expect a world-space slice input. Hackable hackable = null; { var hackables = subject.GetComponentsInChildren (); if (hackables.Length > 0) { if (hackables.Length > 1) { Debug.LogWarning ("Limb Hacker found multiple slice configurations on object '" + subject.name + "'! Behavior is undefined."); } hackable = hackables [0]; } else { Debug.LogWarning ("Limb Hacker found no slice configuration on object '" + subject.name + "'."); return; } } //We need information about which BONES are getting severed. var metadataByNodeName = new Dictionary (); { var childTransformByName = new Dictionary (); var parentKeyByKey = new Dictionary (); foreach (Transform t in GetConcatenatedHierarchy(subject.transform)) { childTransformByName [t.name] = t; var parent = t.parent; if (t == subject.transform) parent = null; parentKeyByKey [t.name] = parent == null ? null : parent.name; } var severedByChildName = new Dictionary (); { foreach (string childName in childTransformByName.Keys) { severedByChildName [childName] = childName.Equals (jointName); } bool changesMade; do { changesMade = false; foreach (string childKey in childTransformByName.Keys) { bool severed = severedByChildName [childKey]; if (severed) continue; string parentKey = parentKeyByKey [childKey]; bool parentSevered; if (severedByChildName.TryGetValue (parentKey, out parentSevered) == false) continue; if (parentSevered) { severedByChildName [childKey] = true; changesMade = true; } } } while (changesMade); } foreach (var kvp in severedByChildName) { var t = childTransformByName [kvp.Key]; var isConsideredSevered = kvp.Value; metadataByNodeName [kvp.Key] = new NodeMetadata (t, isConsideredSevered); } } IEnumerable snapshots; var forwardPassAgent = subject.GetComponent (); if (forwardPassAgent == null) { var snapshotBuilder = new List (); var skinnedMeshRenderers = subject.GetComponentsInChildren (true); foreach (var smr in skinnedMeshRenderers) { var mesh = smr.sharedMesh; var boneMetadata = new BoneMetadata[smr.bones.Length]; var bones = smr.bones; var bindPoses = mesh.bindposes; for (int i = 0; i < boneMetadata.Length; i++) { boneMetadata [i] = new BoneMetadata (i, bones [i].name, bindPoses [i]); } int? infillIndex = null; if (hackable.infillMaterial != null) { var mats = smr.sharedMaterials; for (int i = 0; i < mats.Length; i++) { if (hackable.infillMaterial == mats [i]) { infillIndex = i; break; } } } MeshSnapshot snapshot; MeshSnapshot preloadedFragment; if (preloadedMeshes.TryGetValue (mesh.GetInstanceID (), out preloadedFragment)) { //The preloaded fragments are missing data which is particular to the SMR. We'll combine it with such data here. snapshot = preloadedFragment.WithKey (smr.name).WithMaterials (smr.sharedMaterials).WithBoneMetadata (boneMetadata).WithInfillIndex (infillIndex); } else { var indices = new int[mesh.subMeshCount][]; for (int i = 0; i < mesh.subMeshCount; i++) { indices [i] = mesh.GetIndices (i); } snapshot = new MeshSnapshot ( smr.name, mesh.vertices, mesh.normals, mesh.uv, mesh.tangents, mesh.boneWeights, smr.sharedMaterials, boneMetadata, infillIndex, indices); } snapshotBuilder.Add (snapshot); } snapshots = snapshotBuilder; } else { snapshots = forwardPassAgent.Snapshot; } var jobSpec = new JobSpecification (subject, hackable, snapshots, metadataByNodeName, hackable.infillMaterial, jointName, rootTipProgression, planeNormal, hackable.infillMode, true); var jobState = new JobState (jobSpec); try { switch (workerThreadMode) { case WorkerThreadMode.Asynchronous: jobStates.Add (jobState); #if NETFX_CORE && !UNITY_EDITOR System.Threading.Tasks.Task.Factory.StartNew(ThreadSafeHack.Slice, jobState); #else System.Threading.ThreadPool.QueueUserWorkItem (ThreadSafeHack.Slice, jobState); #endif break; case WorkerThreadMode.Synchronous: ThreadSafeHack.Slice (jobState); if (jobState.HasYield) { ConsumeJobYield (jobState); } else if (jobState.HasException) { throw jobState.Exception; } break; default: throw new System.NotImplementedException (); } } catch (System.Exception ex) { Debug.LogException (ex, subject); } } void Update () { jobStateRemovalQueue.Clear (); jobStateQueue.Clear (); jobStateQueue.AddRange (jobStates); foreach (var jobState in jobStateQueue) { if (jobState.IsDone) { try { if (jobState.HasYield) { ConsumeJobYield (jobState); } else if (jobState.HasException) { throw jobState.Exception; } } catch (System.Exception ex) { Debug.LogException (ex, jobState.Specification.Subject); } finally { jobStateRemovalQueue.Add (jobState); } } } jobStates.ExceptWith (jobStateRemovalQueue); } } }