You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

672 lines
26 KiB
C#

4 years ago
using System.Collections;
using System.Collections.Generic;
using InfinityPBR;
using UnityEditor;
using UnityEngine;
/*
* NOTE: This is "v2" of this script, the original one is "SFB_BlendShapesManager". This script replaces that, but you are
* able to export values from that script and import them into this script, to transition over. Please add both to the
* game object and view the inspector for this script. Thanks!
* www.InfinityPBR.com
*/
namespace InfinityPBR
{
public class BlendShapesManager : MonoBehaviour
{
public List<BlendShapeGameObject> blendShapeGameObjects = new List<BlendShapeGameObject>();
public bool userMatchRespectsLimits = true; // If true, controlled shapes will respect their set min/max limits, and end up a percentage of the parent shape value
public List<BlendShapePresetFile> rangeFiles = new List<BlendShapePresetFile>(); // Blend Shape range files
public List<BlendShapePresetFile> presetFiles = new List<BlendShapePresetFile>(); // Blend shape preset files
public string blendShapePrimaryPrefix = "SFB_BS_"; // This can be changed by the user in the inspector. Defaults to Infinity PBR values that we use!
public string blendShapeMatchPrefix = "SFB_BSM_"; // This can be changed by the user in the inspector. Defaults to Infinity PBR values that we use!
public (string, string) plusMinus = ("Plus", "Minus"); // This can be changed by the user in the inspector. Defaults to Infinity PBR values that we use!
[HideInInspector] public List<MatchList> matchList = new List<MatchList>();
[HideInInspector] public string[] matchListNames;
[HideInInspector] public int matchListIndex = 0;
[HideInInspector] public string exportPath = "";
[HideInInspector] public bool showHelpBoxes = true;
[HideInInspector] public bool showSetup = true;
[HideInInspector] public bool showPresets = true;
[HideInInspector] public bool showFullInspector = false;
[HideInInspector] public bool showBlendShapeObjects = true;
[HideInInspector] public bool showRangeFiles = true;
[HideInInspector] public bool showPresetFiles = true;
public void SetRandomShapeValue(BlendShapeValue value)
{
value.value = Random.Range(value.limitMin, value.limitMax);
value.lastValue = value.value;
}
public void TriggerShape(BlendShapeGameObject obj, BlendShapeValue value, bool triggerAutoMatches = true, bool triggerUserMatches = true)
{
if (!obj.gameObject) // If there is no gameObject (i.e. it may have been destroyed), return
return;
if (!obj.smr) // If the object doesn't have a skinned mesh renderer, return
return;
if (!obj.smr.sharedMesh) return;
var targetShapeIndex = obj.smr.sharedMesh.GetBlendShapeIndex(value.fullName);
if (targetShapeIndex == -1) return;
obj.smr.SetBlendShapeWeight(targetShapeIndex, value.value);
/*
* DID YOU GET AN ERROR ON THE FOLLOWING LINE??
*
* Check to make sure you don't have another object of the same name already attached to your prefab. Having
* two objects of the same name could cause this problem, i.e. if you have some pre-attached objects, and then
* you try to add another one.
*
* You may need to "reload blend shapes" after fixing this.
*
* NOTE: March 20, 2022 - I think I fixed this bug by replacing the commented out code below with the one above this
* comment block. I.e. the index was not always in the same order, so we need to get the
* index on the shape on the target object by name. If we can't find one, then we return out.
*
* This SHOULD work...but leaving this comment here in case it doesn't work as expected, so I can find it later.
*/
//obj.smr.SetBlendShapeWeight(value.index, value.value);
if (triggerAutoMatches)
TriggerAutoMatches(value.triggerName, value.value);
if (triggerUserMatches)
TriggerUserMatches(obj, value);
}
public void TriggerAutoMatches(string triggerName, float value)
{
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
for (int i = 0; i < blendShapeGameObjects[o].blendShapeValues.Count; i++)
{
if (blendShapeGameObjects[o] == null) continue;
if (blendShapeGameObjects[o].blendShapeValues[i].triggerName == triggerName)
{
if (blendShapeGameObjects[o].blendShapeValues[i] == null) continue;
//Debug.Log("Auto: " + blendShapeGameObjects[o].gameObjectName + " | " + blendShapeGameObjects[o].blendShapeValues[i].triggerName);
blendShapeGameObjects[o].blendShapeValues[i].value = blendShapeGameObjects[o].blendShapeValues[i].isMinus ? -value : value;
TriggerShape(blendShapeGameObjects[o], blendShapeGameObjects[o].blendShapeValues[i], false, false);
}
}
}
}
public void TriggerUserMatches(BlendShapeGameObject obj, BlendShapeValue value)
{
foreach (var match in value.otherValuesMatchThis)
{
var matchObj = blendShapeGameObjects[match.objectIndex];
var matchValue = matchObj.blendShapeValues[match.valueIndex];
float finalValue = userMatchRespectsLimits
? matchValue.limitMin + (AmountBetweenMinAndMax(matchValue) * PercentFromMin(value))
: value.value;
matchValue.value = finalValue;
matchValue.lastValue = finalValue;
TriggerShape(matchObj, matchValue,true, false);
}
}
public float PercentFromMin(BlendShapeValue value)
{
// example: -50 to 100, value is -10
float maxDif = AmountBetweenMinAndMax(value); // Example (100 - -50) = 150
float posFromMin = value.value - value.limitMin; // Example (-10 - -50) = 40
return Mathf.Clamp(posFromMin / maxDif, -1f, 1f); // Example ( 40 / 150) = 0.2666 = 26.7%
}
public float ValueFromMin(BlendShapeValue value, float percentFromMin)
{
return AmountBetweenMinAndMax(value) * percentFromMin;
}
public float AmountBetweenMinAndMax(BlendShapeValue value)
{
return value.limitMax - value.limitMin;
}
public void ResetAll()
{
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
BlendShapeGameObject obj = blendShapeGameObjects[o];
if (obj.displayableValues > 0)
{
for (int i = 0; i < obj.blendShapeValues.Count; i++)
{
BlendShapeValue value = obj.blendShapeValues[i];
if (value.display && !value.matchAnotherValue)
{
value.value = 0;
TriggerShape(obj, value);
}
}
}
}
}
/*
*
*/
public void SetAllShapeValues()
{
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
BlendShapeGameObject obj = blendShapeGameObjects[o];
if (obj.displayableValues > 0)
{
for (int i = 0; i < obj.blendShapeValues.Count; i++)
{
BlendShapeValue value = obj.blendShapeValues[i];
if (value.display && !value.matchAnotherValue)
{
TriggerShape(obj, value);
}
}
}
}
}
public void SetToAndFromValues(BlendShapePreset preset)
{
SaveAllShapeValuesInFromField();
SaveAllShapeValuesInToField(preset);
}
public void SaveAllShapeValuesInFromField()
{
var c = 0;
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
BlendShapeGameObject obj = blendShapeGameObjects[o];
if (obj.displayableValues > 0)
{
for (int i = 0; i < obj.blendShapeValues.Count; i++)
{
BlendShapeValue value = obj.blendShapeValues[i];
if (value.display && !value.matchAnotherValue)
{
value.FromValue = value.value;
c++;
}
}
}
}
//Debug.Log($"Set {c} from values");
}
public void SaveAllShapeValuesInToField(BlendShapePreset preset)
{
var c = 0;
foreach (var presetValue in preset.presetValues)
{
GetBlendShapeValue(presetValue.objectName, presetValue.valueTriggerName).ToValue
= presetValue.onTriggerMode == "Explicit"
? presetValue.shapeValue
: Random.Range(presetValue.limitMin, presetValue.limitMax);
c++;
}
//Debug.Log($"Set {c} to values");
}
public void LerpToValue(float lerpValue)
{
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
BlendShapeGameObject obj = blendShapeGameObjects[o];
if (obj.displayableValues > 0)
{
for (int i = 0; i < obj.blendShapeValues.Count; i++)
{
BlendShapeValue value = obj.blendShapeValues[i];
if (value.display && !value.matchAnotherValue)
{
value.value = Mathf.Lerp(value.FromValue, value.ToValue, lerpValue);
TriggerShape(obj, value);
}
}
}
}
}
public void ImportRangeFile(TextAsset textAsset)
{
rangeFiles.Add(new BlendShapePresetFile());
rangeFiles[rangeFiles.Count - 1].textAsset = textAsset;
}
public void LoadRangeFile(TextAsset rangeFile)
{
Debug.Log("Loading Range File: " + rangeFile.name);
string contents = rangeFile.text;
if (ImportRangeFileFromV2(rangeFile))
return;
if (ImportRangeFileFromV3(rangeFile))
return;
}
public bool ImportRangeFileFromV2(TextAsset textAsset)
{
string[] splitText = textAsset.text.Split(new string[] { "," }, System.StringSplitOptions.None);
if (splitText[0] == "SFB_BlendShapesManager V2")
{
for (int i = 1; i < splitText.Length; i++)
{
string fullName = splitText[i + 1];
string triggerName = fullName.Replace(plusMinus.Item1, "");
float minLimit = float.Parse(splitText[i + 2]);
float maxLimit = float.Parse(splitText[i + 3]);
var dataAttempt = TryGetShapeData(triggerName);
BlendShapeGameObject obj = dataAttempt.Item1;
BlendShapeValue value = dataAttempt.Item2;
if (obj != null && value != null)
{
value.limitMin = minLimit;
value.limitMax = maxLimit;
}
i++;
i++;
i++;
}
Debug.Log("Import Finished");
return true;
}
return false;
}
public bool ImportRangeFileFromV3(TextAsset textAsset)
{
string[] splitText = textAsset.text.Split(new string[] { "," }, System.StringSplitOptions.None);
if (splitText[0] == "InfinityPBR_BlendShapesManager_RangeFile")
{
Debug.Log("Range File Version: " + splitText[0]);
for (int i = 1; i < splitText.Length; i++)
{
string gameObjectName = splitText[i];
string fullName = splitText[i + 1];
string triggerName = fullName.Replace(plusMinus.Item1, "");
triggerName = triggerName.Replace(blendShapePrimaryPrefix, "");
float minLimit = float.Parse(splitText[i + 2]);
float maxLimit = float.Parse(splitText[i + 3]);
var dataAttempt = TryGetShapeData(gameObjectName, triggerName);
BlendShapeGameObject obj = dataAttempt.Item1;
BlendShapeValue value = dataAttempt.Item2;
if (obj != null && value != null)
{
value.limitMin = minLimit;
value.limitMax = maxLimit;
}
i++;
i++;
i++;
}
return true;
}
return false;
}
public (BlendShapeGameObject, BlendShapeValue) TryGetShapeData(string objGameObjectName, string triggerName)
{
BlendShapeGameObject obj = null;
BlendShapeValue value = null;
obj = GetBlendShapeObject(objGameObjectName);
for (int i = 0; i < obj.blendShapeValues.Count; i++)
{
BlendShapeValue thisValue = obj.blendShapeValues[i];
if (thisValue.display && thisValue.triggerName == triggerName)
{
#if UNITY_EDITOR
Debug.Log("Matched: " + triggerName);
#endif
return (obj, thisValue);
}
}
return (obj, value);
}
public (BlendShapeGameObject, BlendShapeValue) TryGetShapeData(string triggerName)
{
BlendShapeGameObject obj = null;
BlendShapeValue value = null;
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
BlendShapeGameObject thisObj = blendShapeGameObjects[o];
if (thisObj.displayableValues > 0)
{
for (int i = 0; i < thisObj.blendShapeValues.Count; i++)
{
BlendShapeValue thisValue = thisObj.blendShapeValues[i];
if (thisValue.display && thisValue.triggerName == triggerName)
{
#if UNITY_EDITOR
Debug.Log("Matched: " + triggerName);
#endif
return (thisObj, thisValue);
}
}
}
}
return (obj, value);
}
/*
* This is the same as clicking the button in the Blend Shapes Manager Inspector. It will first attempt to
* relink any missing game objects, which may have been turned off or deleted, but may be available now.
*
* Then it will load new shapes, remove unused objects, and finally set the values.
*/
public void LoadBlendShapeData()
{
Transform[] gameObjects = gameObject.GetComponentsInChildren<Transform>(true);
AttemptRelinks(gameObjects);
LoadNewShapes(gameObjects);
RemoveUnusedObjects(gameObjects);
SetAllShapeValues();
}
public void LoadNewShapes(Transform[] gameObjects)
{
foreach (Transform transform in gameObjects)
{
GameObject gameObject = transform.gameObject;
SkinnedMeshRenderer smr = GetSkinnedMeshRenderer(gameObject);
if (smr == null)
continue;
//Debug.Log("Adding " + gameObject.name + " | " + smr);
blendShapeGameObjects.Add(new BlendShapeGameObject());
int newObjectIndex = blendShapeGameObjects.Count - 1;
BlendShapeGameObject newObject = blendShapeGameObjects[newObjectIndex];
newObject.smr = smr;
newObject.gameObjectName = gameObject.name;
newObject.gameObject = gameObject;
AddShapesToList(smr, newObject, newObjectIndex);
}
}
public void AttemptRelinks(Transform[] gameObjects)
{
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
BlendShapeGameObject obj = blendShapeGameObjects[o];
if (obj.gameObject)
continue;
foreach (Transform transform in gameObjects)
{
if (transform.gameObject.name == obj.gameObjectName)
{
if (transform.gameObject.GetComponent<SkinnedMeshRenderer>())
obj.gameObject = transform.gameObject;
obj.smr = transform.gameObject.GetComponent<SkinnedMeshRenderer>();
}
}
}
}
private SkinnedMeshRenderer GetSkinnedMeshRenderer(GameObject gameObject)
{
if (IsGameObjectInList(gameObject))
return null;
if (!gameObject.GetComponent<SkinnedMeshRenderer>())
return null;
if (!gameObject.GetComponent<SkinnedMeshRenderer>().sharedMesh)
return null;
if (gameObject.GetComponent<SkinnedMeshRenderer>().sharedMesh.blendShapeCount == 0)
return null;
return gameObject.GetComponent<SkinnedMeshRenderer>();
}
private void RemoveUnusedObjects(Transform[] transforms)
{
for (int i = blendShapeGameObjects.Count - 1; i >= 0; i--)
{
if (blendShapeGameObjects[i].gameObject) // This will keep an object from being removed if it's gone missing, so it can be relinked later
{
#if UNITY_EDITOR
if (!ArrayUtility.Contains(transforms, blendShapeGameObjects[i].gameObject.transform))
blendShapeGameObjects.RemoveAt(i);
#endif
}
}
}
private void AddShapesToList(SkinnedMeshRenderer smr, BlendShapeGameObject newObject, int newObjectIndex)
{
for (int i = 0; i < smr.sharedMesh.blendShapeCount; i++)
{
newObject.blendShapeValues.Add(new BlendShapeValue());
BlendShapeValue newEntry = newObject.blendShapeValues[newObject.blendShapeValues.Count - 1];
string triggerName = GetHumanName(smr.sharedMesh.GetBlendShapeName(i));
if (triggerName == "")
continue;
if (triggerName.Contains(plusMinus.Item2))
newEntry.isMinus = true;
if (triggerName.Contains(plusMinus.Item2) || triggerName.Contains(plusMinus.Item1))
{
newEntry.min = -100f;
newEntry.limitMin = -100f;
}
if (smr.sharedMesh.GetBlendShapeName(i).Contains(blendShapePrimaryPrefix) && !newEntry.isMinus)
{
newEntry.display = true;
newObject.displayableValues++;
}
newEntry.index = i;
newEntry.fullName = smr.sharedMesh.GetBlendShapeName(i);
triggerName = triggerName.Replace(blendShapeMatchPrefix, "");
triggerName = triggerName.Replace(blendShapePrimaryPrefix, "");
triggerName = triggerName.Replace(plusMinus.Item1, "");
triggerName = triggerName.Replace(plusMinus.Item2, "");
newEntry.triggerName = triggerName;
newEntry.objectIndex = newObjectIndex;
}
}
public bool IsGameObjectInList(GameObject gameObject)
{
for (int i = 0; i < blendShapeGameObjects.Count; i++)
{
if (blendShapeGameObjects[i].gameObject == gameObject)
return true;
}
return false;
}
public string GetHumanName(string blendShapeName)
{
if (!blendShapeName.Contains(blendShapePrimaryPrefix) && !blendShapeName.Contains(blendShapeMatchPrefix))
return "";
string[] periodParse = blendShapeName.Split(new string[] { "." }, System.StringSplitOptions.None);
string[] stringParse = periodParse [periodParse.Length == 1 ? 0 : 1].Split(new string[] { "_" }, System.StringSplitOptions.None);
if (stringParse.Length >= 3 && blendShapeName.Contains(blendShapePrimaryPrefix))
return stringParse [2];
if (stringParse.Length >= 4 && blendShapeName.Contains(blendShapeMatchPrefix))
return stringParse [3];
return "";
}
public BlendShapeGameObject GetBlendShapeObject(string name)
{
for (int i = 0; i < blendShapeGameObjects.Count; i++)
{
if (blendShapeGameObjects[i].gameObjectName == name)
return blendShapeGameObjects[i];
}
Debug.LogWarning("Warning: Could not find a blend Shape Object named " + name);
return null;
}
public BlendShapeValue GetBlendShapeValue(BlendShapeGameObject obj, string triggerName, bool minusValuesOK = false)
{
//Debug.Log("Obj: " + obj.displayableValues);
for (int i = 0; i < obj.blendShapeValues.Count; i++)
{
if (obj.blendShapeValues[i].triggerName == triggerName)
{
if (!obj.blendShapeValues[i].isMinus || minusValuesOK)
return obj.blendShapeValues[i];
}
}
Debug.LogWarning("Warning: Was not able to get blend shape value for " + triggerName);
return null;
}
public BlendShapeValue GetBlendShapeValue(string objectName, string triggerName, bool minusValuesOK = false)
{
BlendShapeGameObject obj = GetBlendShapeObject(objectName);
if (obj == null)
{
Debug.LogWarning("Warning: Unable to get a BlendShapeObject for " + objectName + " (Trigger: " +
triggerName + ")");
return null;
}
return GetBlendShapeValue(obj, triggerName, minusValuesOK);
}
public void BuildMatchLists()
{
matchList.Clear();
matchListIndex = 0;
for (int o = 0; o < blendShapeGameObjects.Count; o++)
{
BlendShapeGameObject obj = blendShapeGameObjects[o];
if (obj.displayableValues == 0)
continue;
if (!obj.gameObject)
continue;
for (int i = 0; i < obj.blendShapeValues.Count; i++)
{
BlendShapeValue value = obj.blendShapeValues[i];
if (!value.display)
continue;
if (value.matchAnotherValue)
continue;
if (value.otherValuesMatchThis.Count > 0)
continue;
matchList.Add(new MatchList());
MatchList newMatchlist = matchList[matchList.Count - 1];
newMatchlist.name = obj.gameObject.name + " " + value.triggerName;
newMatchlist.objectIndex = o;
newMatchlist.valueIndex = i;
}
}
matchListNames = new string[matchList.Count];
for (int i = 0; i < matchList.Count; i++)
{
matchListNames[i] = matchList[i].name;
}
}
}
[System.Serializable]
public class BlendShapeGameObject
{
public string gameObjectName;
public GameObject gameObject;
public SkinnedMeshRenderer smr;
public int displayableValues = 0;
public List<BlendShapeValue> blendShapeValues = new List<BlendShapeValue>();
[HideInInspector] public bool showValues = false;
}
[System.Serializable]
public class BlendShapeValue
{
public int objectIndex;
public int index;
public bool display = false;
public string fullName;
public string triggerName;
public bool isMinus = false;
public float value = 0f;
public float min = 0f;
public float max = 100f;
[HideInInspector] public float limitMin = 0f;
[HideInInspector] public float limitMax = 100f;
[HideInInspector] public float lastValue = 0f;
[HideInInspector] public bool showValueOptions = false;
[HideInInspector] public bool isOpen = false;
public float FromValue { get; set; }
public float ToValue { get; set; }
// If this is being controlled by another shape, set these
public int matchThisObjectIndex = 0;
public int matchThisValueIndex = 0;
public bool matchAnotherValue = false;
// Shapes that this value controls will go here.
public List<MatchValue> otherValuesMatchThis = new List<MatchValue>();
}
[System.Serializable]
public class MatchValue
{
public int objectIndex;
public int valueIndex;
}
[System.Serializable]
public class BlendShapePresetFile
{
public string name;
public TextAsset textAsset;
}
[System.Serializable]
public class MatchList
{
public string name;
public int objectIndex;
public int valueIndex;
}
}