Initial Commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under the Oculus SDK License Agreement (the "License");
|
||||
* you may not use the Oculus SDK except in compliance with the License,
|
||||
* which is provided at the time of installation or download, or which
|
||||
* otherwise accompanies this software in either electronic or hard copy form.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://developer.oculus.com/licenses/oculussdk/
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, the Oculus SDK
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Oculus.Interaction.Editor
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(ConditionalHideAttribute))]
|
||||
public class ConditionalHideDrawer : PropertyDrawer
|
||||
{
|
||||
bool FulfillsCondition(SerializedProperty property)
|
||||
{
|
||||
ConditionalHideAttribute hideAttribute = (ConditionalHideAttribute)attribute;
|
||||
|
||||
int index = property.propertyPath.LastIndexOf('.');
|
||||
string containerPath = property.propertyPath.Substring(0, index + 1);
|
||||
string conditionPath = containerPath + hideAttribute.ConditionalFieldPath;
|
||||
SerializedProperty conditionalProperty = property.serializedObject.FindProperty(conditionPath);
|
||||
|
||||
if (conditionalProperty.type == "Enum")
|
||||
{
|
||||
return conditionalProperty.enumValueIndex == (int)hideAttribute.HideValue;
|
||||
}
|
||||
if (conditionalProperty.type == "int")
|
||||
{
|
||||
return conditionalProperty.intValue == (int)hideAttribute.HideValue;
|
||||
}
|
||||
if (conditionalProperty.type == "float")
|
||||
{
|
||||
return conditionalProperty.floatValue == (float)hideAttribute.HideValue;
|
||||
}
|
||||
if (conditionalProperty.type == "string")
|
||||
{
|
||||
return conditionalProperty.stringValue == (string)hideAttribute.HideValue;
|
||||
}
|
||||
if (conditionalProperty.type == "double")
|
||||
{
|
||||
return conditionalProperty.doubleValue == (double)hideAttribute.HideValue;
|
||||
}
|
||||
if (conditionalProperty.type == "bool")
|
||||
{
|
||||
return conditionalProperty.boolValue == (bool)hideAttribute.HideValue;
|
||||
}
|
||||
|
||||
return conditionalProperty.objectReferenceValue == (object)hideAttribute.HideValue;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
if (FulfillsCondition(property))
|
||||
{
|
||||
EditorGUI.PropertyField(position, property, label, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
if (FulfillsCondition(property))
|
||||
{
|
||||
return EditorGUI.GetPropertyHeight(property, label, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return -EditorGUIUtility.standardVerticalSpacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 535f01ebc6c6b8847bc072a54a558e8e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under the Oculus SDK License Agreement (the "License");
|
||||
* you may not use the Oculus SDK except in compliance with the License,
|
||||
* which is provided at the time of installation or download, or which
|
||||
* otherwise accompanies this software in either electronic or hard copy form.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://developer.oculus.com/licenses/oculussdk/
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, the Oculus SDK
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Oculus.Interaction.Editor
|
||||
{
|
||||
public class EditorConstants
|
||||
{
|
||||
public static readonly Color PRIMARY_COLOR = new Color(0f, 1f, 1f, 0.5f);
|
||||
public static readonly Color PRIMARY_COLOR_DISABLED = new Color(0f, 1f, 1f, 0.1f);
|
||||
|
||||
public static readonly Color SECONDARY_COLOR = new Color(0.5f, 0.3f, 1f, 0.5f);
|
||||
public static readonly Color SECONDARY_COLOR_DISABLED = new Color(0.5f, 0.3f, 1f, 0.1f);
|
||||
|
||||
public static readonly Color ERROR_COLOR = new Color(0.9f, 0.1f, 0.1f, 0.5f);
|
||||
public static readonly Color ERROR_COLOR_DISABLED = new Color(0.9f, 0.1f, 0.1f, 0.1f);
|
||||
|
||||
public static readonly float LINE_THICKNESS = 2f;
|
||||
|
||||
public static readonly float ROW_HEIGHT = 20f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6098114538e7342429e2722e7766d1af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under the Oculus SDK License Agreement (the "License");
|
||||
* you may not use the Oculus SDK except in compliance with the License,
|
||||
* which is provided at the time of installation or download, or which
|
||||
* otherwise accompanies this software in either electronic or hard copy form.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://developer.oculus.com/licenses/oculussdk/
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, the Oculus SDK
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using Oculus.Interaction.Input;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Oculus.Interaction.HandGrab.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// This wizard helps creating a set of fixed Animation Clips using HandTracking
|
||||
/// to be used in a skinned synthetic hand with an Animator.
|
||||
/// Assign a HandSkeletonVisual and click the buttons as you perform the relevant
|
||||
/// poses with your tracked hand. The output will be an Animator that can be directly
|
||||
/// used in a Skinned hand. Once you are done you can automatically create the opposite
|
||||
/// hand data by providing the strings internally used for differentiating the left and
|
||||
/// right transforms. (typically _l_ and _r_)
|
||||
/// Works great in conjunction with FromOVRControllerHandData.cs
|
||||
/// </summary>
|
||||
public class HandAnimatorWizard : ScriptableWizard
|
||||
{
|
||||
[SerializeField]
|
||||
private HandVisual _handVisual;
|
||||
[SerializeField]
|
||||
private string _folder = "GeneratedAnimations";
|
||||
[SerializeField]
|
||||
private string _controllerName = "HandController";
|
||||
|
||||
[Space]
|
||||
[InspectorButton("RecordHandFist")]
|
||||
[SerializeField]
|
||||
private string _recordHandFist;
|
||||
[SerializeField]
|
||||
private AnimationClip _handFist;
|
||||
|
||||
[InspectorButton("RecordHand3qtrFist")]
|
||||
[SerializeField]
|
||||
private string _recordHand3qtrFist;
|
||||
[SerializeField]
|
||||
private AnimationClip _hand3qtrFist;
|
||||
|
||||
[InspectorButton("RecordHandMidFist")]
|
||||
[SerializeField]
|
||||
private string _recordHandMidFist;
|
||||
[SerializeField]
|
||||
private AnimationClip _handMidFist;
|
||||
|
||||
[InspectorButton("RecordHandPinch")]
|
||||
[SerializeField]
|
||||
private string _recordHandPinch;
|
||||
[SerializeField]
|
||||
private AnimationClip _handPinch;
|
||||
|
||||
[InspectorButton("RecordHandCap")]
|
||||
[SerializeField]
|
||||
private string _recordHandCap;
|
||||
[SerializeField]
|
||||
private AnimationClip _handCap;
|
||||
|
||||
[InspectorButton("RecordThumbUp")]
|
||||
[SerializeField]
|
||||
private string _recordThumbUp;
|
||||
[SerializeField]
|
||||
private AnimationClip _thumbUp;
|
||||
|
||||
[InspectorButton("RecordIndexPoint")]
|
||||
[SerializeField]
|
||||
private string _recordIndexPoint;
|
||||
[SerializeField]
|
||||
private AnimationClip _indexPoint;
|
||||
|
||||
[Space]
|
||||
[InspectorButton("GenerateMasks")]
|
||||
[SerializeField]
|
||||
private string _generateMasks;
|
||||
[SerializeField]
|
||||
private AvatarMask _indexMask;
|
||||
[SerializeField]
|
||||
private AvatarMask _thumbMask;
|
||||
|
||||
[Space]
|
||||
[InspectorButton("GenerateAnimatorAsset")]
|
||||
[SerializeField]
|
||||
private string _generateAnimator;
|
||||
|
||||
[Space(40f)]
|
||||
[SerializeField]
|
||||
private string _handLeftPrefix = "_l_";
|
||||
[SerializeField]
|
||||
private string _handRightPrefix = "_r_";
|
||||
[InspectorButton("GenerateMirrorAnimatorAsset")]
|
||||
[SerializeField]
|
||||
private string _generateMirrorAnimator;
|
||||
|
||||
private Transform Root => _handVisual.Joints[0].parent;
|
||||
|
||||
private static readonly List<HandJointId> INDEX_MASK = new List<HandJointId>()
|
||||
{
|
||||
HandJointId.HandIndex1,
|
||||
HandJointId.HandIndex2,
|
||||
HandJointId.HandIndex3,
|
||||
HandJointId.HandIndexTip
|
||||
};
|
||||
|
||||
private static readonly List<HandJointId> THUMB_MASK = new List<HandJointId>()
|
||||
{
|
||||
HandJointId.HandThumb0,
|
||||
HandJointId.HandThumb1,
|
||||
HandJointId.HandThumb2,
|
||||
HandJointId.HandThumb3,
|
||||
HandJointId.HandThumbTip
|
||||
};
|
||||
|
||||
private const string FLEX_PARAM = "Flex";
|
||||
private const string PINCH_PARAM = "Pinch";
|
||||
|
||||
[MenuItem("Oculus/Interaction/Hand Animator Generator")]
|
||||
private static void CreateWizard()
|
||||
{
|
||||
ScriptableWizard.DisplayWizard<HandAnimatorWizard>("Hand Animator Generator", "Close");
|
||||
}
|
||||
|
||||
private void OnWizardCreate() { }
|
||||
|
||||
private bool TryGetHand(out IHand hand)
|
||||
{
|
||||
hand = null;
|
||||
|
||||
if (_handVisual == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_handVisual.Hand != null)
|
||||
{
|
||||
hand = _handVisual.Hand;
|
||||
return true;
|
||||
}
|
||||
|
||||
System.Type targetObjectClassType = _handVisual.GetType();
|
||||
System.Reflection.FieldInfo field = targetObjectClassType.GetField("_hand",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
if (field != null)
|
||||
{
|
||||
object value = field.GetValue(_handVisual);
|
||||
hand = value as IHand;
|
||||
return hand != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RecordHandFist()
|
||||
{
|
||||
_handFist = GenerateClipAsset("HandFist");
|
||||
}
|
||||
|
||||
private void RecordHand3qtrFist()
|
||||
{
|
||||
_hand3qtrFist = GenerateClipAsset("Hand3qtrFist");
|
||||
}
|
||||
|
||||
private void RecordHandMidFist()
|
||||
{
|
||||
_handMidFist = GenerateClipAsset("HandMidFist");
|
||||
}
|
||||
|
||||
private void RecordHandPinch()
|
||||
{
|
||||
_handPinch = GenerateClipAsset("HandPinch");
|
||||
}
|
||||
|
||||
private void RecordHandCap()
|
||||
{
|
||||
_handCap = GenerateClipAsset("HandCap");
|
||||
}
|
||||
|
||||
private void RecordThumbUp()
|
||||
{
|
||||
_thumbUp = GenerateClipAsset("ThumbUp");
|
||||
}
|
||||
|
||||
private void RecordIndexPoint()
|
||||
{
|
||||
_indexPoint = GenerateClipAsset("IndexPoint");
|
||||
}
|
||||
|
||||
public void GenerateMasks()
|
||||
{
|
||||
_indexMask = GenerateMaskAsset(INDEX_MASK, "indexMask");
|
||||
_thumbMask = GenerateMaskAsset(THUMB_MASK, "thumbMask");
|
||||
}
|
||||
|
||||
private void GenerateAnimatorAsset()
|
||||
{
|
||||
HandClips clips = new HandClips()
|
||||
{
|
||||
handFist = _handFist,
|
||||
hand3qtrFist = _hand3qtrFist,
|
||||
handMidFist = _handMidFist,
|
||||
handPinch = _handPinch,
|
||||
handCap = _handCap,
|
||||
thumbUp = _thumbUp,
|
||||
indexPoint = _indexPoint,
|
||||
|
||||
indexMask = _indexMask,
|
||||
thumbMask = _thumbMask
|
||||
};
|
||||
|
||||
GetHandPrefixes(out string prefix, out string mirrorPrefix);
|
||||
string path = GenerateAnimatorPath(prefix);
|
||||
CreateAnimator(path, clips);
|
||||
}
|
||||
|
||||
private void GenerateMirrorAnimatorAsset()
|
||||
{
|
||||
AnimationClip handFist = GenerateMirrorClipAsset(_handFist);
|
||||
AnimationClip hand3qtrFist = GenerateMirrorClipAsset(_hand3qtrFist);
|
||||
AnimationClip handMidFist = GenerateMirrorClipAsset(_handMidFist);
|
||||
AnimationClip handPinch = GenerateMirrorClipAsset(_handPinch);
|
||||
AnimationClip handCap = GenerateMirrorClipAsset(_handCap);
|
||||
AnimationClip thumbUp = GenerateMirrorClipAsset(_thumbUp);
|
||||
AnimationClip indexPoint = GenerateMirrorClipAsset(_indexPoint);
|
||||
|
||||
AvatarMask indexMask = GenerateMirrorMaskAsset(_indexMask);
|
||||
AvatarMask thumbMask = GenerateMirrorMaskAsset(_thumbMask);
|
||||
|
||||
HandClips clips = new HandClips()
|
||||
{
|
||||
handFist = handFist,
|
||||
hand3qtrFist = hand3qtrFist,
|
||||
handMidFist = handMidFist,
|
||||
handPinch = handPinch,
|
||||
handCap = handCap,
|
||||
thumbUp = thumbUp,
|
||||
indexPoint = indexPoint,
|
||||
indexMask = indexMask,
|
||||
thumbMask = thumbMask
|
||||
};
|
||||
|
||||
GetHandPrefixes(out string prefix, out string mirrorPrefix);
|
||||
string path = GenerateAnimatorPath(mirrorPrefix);
|
||||
CreateAnimator(path, clips);
|
||||
}
|
||||
|
||||
private AnimationClip GenerateClipAsset(string title)
|
||||
{
|
||||
GetHandPrefixes(out string prefix, out string mirrorPrefix);
|
||||
AnimationClip clip = new AnimationClip();
|
||||
|
||||
for (int i = (int)HandJointId.HandStart; i < (int)HandJointId.HandEnd; ++i)
|
||||
{
|
||||
Transform jointTransform = _handVisual.Joints[i];
|
||||
string path = GetGameObjectPath(jointTransform, Root);
|
||||
RegisterLocalPose(ref clip, jointTransform.GetPose(Space.Self), path);
|
||||
}
|
||||
|
||||
StoreAsset(clip, $"{title}{prefix}.anim");
|
||||
return clip;
|
||||
}
|
||||
|
||||
private AvatarMask GenerateMaskAsset(List<HandJointId> maskData, string title)
|
||||
{
|
||||
GetHandPrefixes(out string prefix, out string mirrorPrefix);
|
||||
AvatarMask mask = new AvatarMask();
|
||||
List<string> paths = new List<string>(maskData.Count);
|
||||
|
||||
foreach (var maskJoints in maskData)
|
||||
{
|
||||
Transform jointTransform = _handVisual.Joints[(int)maskJoints];
|
||||
string localPath = GetGameObjectPath(jointTransform, Root);
|
||||
paths.Add(localPath);
|
||||
}
|
||||
|
||||
mask.transformCount = paths.Count;
|
||||
for (int i = 0; i < paths.Count; ++i)
|
||||
{
|
||||
mask.SetTransformPath(i, paths[i]);
|
||||
mask.SetTransformActive(i, true);
|
||||
}
|
||||
|
||||
StoreAsset(mask, $"{title}{prefix}.mask");
|
||||
return mask;
|
||||
}
|
||||
|
||||
private AnimationClip GenerateMirrorClipAsset(AnimationClip originalClip)
|
||||
{
|
||||
if (originalClip == null)
|
||||
{
|
||||
Debug.LogError("Please generate a valid Clip first");
|
||||
return null;
|
||||
}
|
||||
GetHandPrefixes(out string prefix, out string mirrorPrefix);
|
||||
|
||||
AnimationClip mirrorClip = new AnimationClip();
|
||||
|
||||
EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(originalClip);
|
||||
foreach (EditorCurveBinding curveBinding in curveBindings)
|
||||
{
|
||||
string mirrorPath = curveBinding.path.Replace(prefix, mirrorPrefix);
|
||||
AnimationCurve curve = AnimationUtility.GetEditorCurve(originalClip, curveBinding);
|
||||
float invertFactor = curveBinding.propertyName.Contains("LocalPosition") ? -1f : 1f;
|
||||
|
||||
AnimationCurve mirrorCurve = new AnimationCurve();
|
||||
|
||||
for (int i = 0; i < curve.length; i++)
|
||||
{
|
||||
mirrorCurve.AddKey(curve[i].time, curve[i].value * invertFactor);
|
||||
}
|
||||
mirrorClip.SetCurve(mirrorPath, curveBinding.type, curveBinding.propertyName, mirrorCurve);
|
||||
}
|
||||
StoreAsset(mirrorClip, $"{originalClip.name.Replace(prefix, mirrorPrefix)}.anim");
|
||||
return mirrorClip;
|
||||
}
|
||||
|
||||
private AvatarMask GenerateMirrorMaskAsset(AvatarMask originalMask)
|
||||
{
|
||||
if (originalMask == null)
|
||||
{
|
||||
Debug.LogError("Please generate a valid mask first");
|
||||
return null;
|
||||
}
|
||||
GetHandPrefixes(out string prefix, out string mirrorPrefix);
|
||||
|
||||
AvatarMask mirrorMask = new AvatarMask();
|
||||
|
||||
mirrorMask.transformCount = originalMask.transformCount;
|
||||
for (int i = 0; i < originalMask.transformCount; ++i)
|
||||
{
|
||||
string mirrorPath = originalMask.GetTransformPath(i).Replace(prefix, mirrorPrefix);
|
||||
bool active = originalMask.GetTransformActive(i);
|
||||
mirrorMask.SetTransformPath(i, mirrorPath);
|
||||
mirrorMask.SetTransformActive(i, active);
|
||||
}
|
||||
|
||||
StoreAsset(mirrorMask, $"{originalMask.name.Replace(prefix, mirrorPrefix)}.mask");
|
||||
|
||||
return mirrorMask;
|
||||
}
|
||||
|
||||
private void RegisterLocalPose(ref AnimationClip clip, Pose pose, string path)
|
||||
{
|
||||
Vector3 euler = pose.rotation.eulerAngles;
|
||||
clip.SetCurve(path, typeof(Transform), "localEulerAngles.x", AnimationCurve.Constant(0f, 0.01f, euler.x));
|
||||
clip.SetCurve(path, typeof(Transform), "localEulerAngles.y", AnimationCurve.Constant(0f, 0.01f, euler.y));
|
||||
clip.SetCurve(path, typeof(Transform), "localEulerAngles.z", AnimationCurve.Constant(0f, 0.01f, euler.z));
|
||||
|
||||
Vector3 pos = pose.position;
|
||||
clip.SetCurve(path, typeof(Transform), "localPosition.x", AnimationCurve.Constant(0f, 0.01f, pos.x));
|
||||
clip.SetCurve(path, typeof(Transform), "localPosition.y", AnimationCurve.Constant(0f, 0.01f, pos.y));
|
||||
clip.SetCurve(path, typeof(Transform), "localPosition.z", AnimationCurve.Constant(0f, 0.01f, pos.z));
|
||||
}
|
||||
|
||||
private AnimatorController CreateAnimator(string path, HandClips clips)
|
||||
{
|
||||
if (!clips.IsComplete())
|
||||
{
|
||||
Debug.LogError("Missing clips and masks to generate the animator");
|
||||
return null;
|
||||
}
|
||||
AnimatorController animator = AnimatorController.CreateAnimatorControllerAtPath(path);
|
||||
|
||||
animator.AddParameter(FLEX_PARAM, AnimatorControllerParameterType.Float);
|
||||
animator.AddParameter(PINCH_PARAM, AnimatorControllerParameterType.Float);
|
||||
|
||||
animator.RemoveLayer(0);
|
||||
|
||||
CreateLayer(animator, "Flex Layer", null);
|
||||
CreateLayer(animator, "Thumb Layer", clips.thumbMask);
|
||||
CreateLayer(animator, "Point Layer", clips.indexMask);
|
||||
|
||||
CreateFlexStates(animator, 0, clips);
|
||||
CreateThumbUpStates(animator, 1, clips);
|
||||
CreatePointStates(animator, 2, clips);
|
||||
|
||||
return animator;
|
||||
}
|
||||
|
||||
private AnimatorControllerLayer CreateLayer(AnimatorController animator, string layerName, AvatarMask mask = null)
|
||||
{
|
||||
AnimatorControllerLayer layer = new AnimatorControllerLayer();
|
||||
layer.name = layerName;
|
||||
AnimatorStateMachine stateMachine = new AnimatorStateMachine();
|
||||
stateMachine.name = layer.name;
|
||||
AssetDatabase.AddObjectToAsset(stateMachine, animator);
|
||||
stateMachine.hideFlags = HideFlags.HideInHierarchy;
|
||||
layer.stateMachine = stateMachine;
|
||||
layer.avatarMask = mask;
|
||||
animator.AddLayer(layer);
|
||||
return layer;
|
||||
}
|
||||
|
||||
private void CreateFlexStates(AnimatorController animator, int layerIndex, HandClips clips)
|
||||
{
|
||||
BlendTree blendTree;
|
||||
AnimatorState flexState = animator.CreateBlendTreeInController("Flex", out blendTree, layerIndex);
|
||||
blendTree.blendType = BlendTreeType.FreeformCartesian2D;
|
||||
blendTree.blendParameter = FLEX_PARAM;
|
||||
blendTree.blendParameterY = PINCH_PARAM;
|
||||
blendTree.AddChild(clips.handCap, new Vector2(0f, 0f));
|
||||
blendTree.AddChild(clips.handPinch, new Vector2(0f, 0.835f));
|
||||
blendTree.AddChild(clips.handPinch, new Vector2(0f, 1f));
|
||||
blendTree.AddChild(clips.handMidFist, new Vector2(0.5f, 0f));
|
||||
blendTree.AddChild(clips.handMidFist, new Vector2(0.5f, 1f));
|
||||
blendTree.AddChild(clips.hand3qtrFist, new Vector2(0.835f, 0f));
|
||||
blendTree.AddChild(clips.hand3qtrFist, new Vector2(0.835f, 1f));
|
||||
blendTree.AddChild(clips.handFist, new Vector2(1f, 0f));
|
||||
blendTree.AddChild(clips.handFist, new Vector2(1f, 1f));
|
||||
animator.layers[layerIndex].stateMachine.defaultState = flexState;
|
||||
}
|
||||
|
||||
private void CreateThumbUpStates(AnimatorController animator, int layerIndex, HandClips clips)
|
||||
{
|
||||
if (clips.thumbUp == null)
|
||||
{
|
||||
Debug.LogError("No thumb clip provided");
|
||||
return;
|
||||
}
|
||||
AnimatorState thumbupState = animator.AddMotion(clips.thumbUp, layerIndex);
|
||||
animator.layers[layerIndex].stateMachine.defaultState = thumbupState;
|
||||
}
|
||||
|
||||
private void CreatePointStates(AnimatorController animator, int layerIndex, HandClips clips)
|
||||
{
|
||||
BlendTree blendTree;
|
||||
AnimatorState flexState = animator.CreateBlendTreeInController("Point", out blendTree, layerIndex);
|
||||
blendTree.blendType = BlendTreeType.Simple1D;
|
||||
blendTree.blendParameter = FLEX_PARAM;
|
||||
blendTree.AddChild(clips.handCap, 0f);
|
||||
blendTree.AddChild(clips.indexPoint, 1f);
|
||||
blendTree.useAutomaticThresholds = true;
|
||||
animator.layers[layerIndex].stateMachine.defaultState = flexState;
|
||||
}
|
||||
|
||||
private void StoreAsset(Object asset, string name)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
string targetFolder = Path.Combine("Assets", _folder);
|
||||
CreateFolder(targetFolder);
|
||||
string path = Path.Combine(targetFolder, name);
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void CreateFolder(string targetFolder)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Directory.Exists(targetFolder))
|
||||
{
|
||||
Directory.CreateDirectory(targetFolder);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private string GenerateAnimatorPath(string prefix)
|
||||
{
|
||||
string targetFolder = Path.Combine("Assets", _folder);
|
||||
CreateFolder(targetFolder);
|
||||
string path = Path.Combine(targetFolder, $"{_controllerName}{prefix}.controller");
|
||||
return path;
|
||||
}
|
||||
|
||||
private static string GetGameObjectPath(Transform transform, Transform root)
|
||||
{
|
||||
string path = transform.name;
|
||||
while (transform.parent != null
|
||||
&& transform.parent != root)
|
||||
{
|
||||
transform = transform.parent;
|
||||
path = $"{transform.name}/{path}";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private void GetHandPrefixes(out string prefix, out string mirrorPrefix)
|
||||
{
|
||||
if (!TryGetHand(out IHand hand))
|
||||
{
|
||||
Debug.LogError("Hand not found");
|
||||
prefix = mirrorPrefix = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
Handedness originalHandedness = hand.Handedness;
|
||||
prefix = originalHandedness == Handedness.Left ? _handLeftPrefix : _handRightPrefix;
|
||||
mirrorPrefix = originalHandedness == Handedness.Left ? _handRightPrefix : _handLeftPrefix;
|
||||
}
|
||||
|
||||
private class HandClips
|
||||
{
|
||||
public AnimationClip handFist;
|
||||
public AnimationClip hand3qtrFist;
|
||||
public AnimationClip handMidFist;
|
||||
public AnimationClip handPinch;
|
||||
public AnimationClip handCap;
|
||||
public AnimationClip thumbUp;
|
||||
public AnimationClip indexPoint;
|
||||
|
||||
public AvatarMask indexMask;
|
||||
public AvatarMask thumbMask;
|
||||
|
||||
public bool IsComplete()
|
||||
{
|
||||
return handFist != null
|
||||
&& hand3qtrFist != null
|
||||
&& handMidFist != null
|
||||
&& handPinch != null
|
||||
&& handCap != null
|
||||
&& thumbUp != null
|
||||
&& indexPoint != null
|
||||
&& indexMask != null
|
||||
&& thumbMask != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d513303388f205408a199c8f7a88885
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under the Oculus SDK License Agreement (the "License");
|
||||
* you may not use the Oculus SDK except in compliance with the License,
|
||||
* which is provided at the time of installation or download, or which
|
||||
* otherwise accompanies this software in either electronic or hard copy form.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://developer.oculus.com/licenses/oculussdk/
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, the Oculus SDK
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using Oculus.Interaction.HandGrab.Visuals;
|
||||
using Oculus.Interaction.Input;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Oculus.Interaction.HandGrab.Editor
|
||||
{
|
||||
public class HandGrabPoseWizard : EditorWindow
|
||||
{ /// <summary>
|
||||
/// The Hand being used for recording HandGrabPoses
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private Hand _hand;
|
||||
[SerializeField]
|
||||
private int _handID = 0;
|
||||
public Hand Hand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hand == null && _handID != 0)
|
||||
{
|
||||
_hand = EditorUtility.InstanceIDToObject(_handID) as Hand;
|
||||
_handID = 0;
|
||||
}
|
||||
return _hand;
|
||||
}
|
||||
set
|
||||
{
|
||||
_hand = value;
|
||||
if (_hand != null)
|
||||
{
|
||||
_handID = value.GetInstanceID();
|
||||
}
|
||||
else
|
||||
{
|
||||
_handID = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Gameobject that the user is recording the HandGrabPose for. e.g. a key
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private Rigidbody _item;
|
||||
|
||||
/// <summary>
|
||||
/// References the hand prototypes used to represent the HandGrabInteractables. These are the
|
||||
/// static hands placed around the interactable to visualize the different holding hand-poses.
|
||||
/// Not mandatory.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private HandGhostProvider _ghostProvider;
|
||||
|
||||
/// <summary>
|
||||
/// This ScriptableObject stores the HandGrabInteractables generated at Play-Mode so it survives
|
||||
/// the Play-Edit cycle.
|
||||
/// Create a collection and assign it even in Play Mode and make sure to store here the
|
||||
/// interactables, then restore it in Edit-Mode to be serialized.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private HandGrabInteractableDataCollection _posesCollection;
|
||||
|
||||
/// <summary>
|
||||
/// The keyboard key that can be pressed for recording a hand grab pose
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private KeyCode _recordKey = KeyCode.Space;
|
||||
|
||||
private GUIStyle _richTextStyle;
|
||||
private Vector2 _scrollPos = Vector2.zero;
|
||||
|
||||
[MenuItem("Oculus/Interaction/Hand Grab Pose Recorder")]
|
||||
private static void CreateWizard()
|
||||
{
|
||||
HandGrabPoseWizard window = EditorWindow.GetWindow<HandGrabPoseWizard>();
|
||||
window.titleContent = new GUIContent("Hand Grab Pose Recorder");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_richTextStyle = EditorGUIUtility.GetBuiltinSkin(EditorGUIUtility.isProSkin ? EditorSkin.Scene : EditorSkin.Inspector).label;
|
||||
_richTextStyle.richText = true;
|
||||
_richTextStyle.wordWrap = true;
|
||||
if (_ghostProvider == null)
|
||||
{
|
||||
HandGhostProviderUtils.TryGetDefaultProvider(out _ghostProvider);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
Event e = Event.current;
|
||||
if (e.type == EventType.KeyDown
|
||||
&& e.keyCode == _recordKey)
|
||||
{
|
||||
RecordPose();
|
||||
e.Use();
|
||||
}
|
||||
GUILayout.Label("Generate HandGrabPoses for grabbing an item <b>using your Hand in Play Mode</b>.\nThen Store and retrieve them in Edit Mode to persist and tweak them.", _richTextStyle);
|
||||
|
||||
_scrollPos = GUILayout.BeginScrollView(_scrollPos);
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("<size=20>1</size>\nAssign the hand that will be tracked and the item for which you want to record HandGrabPoses", _richTextStyle);
|
||||
GUILayout.Label("Hand used for recording poses:");
|
||||
Hand = EditorGUILayout.ObjectField(Hand, typeof(Hand), true) as Hand;
|
||||
GUILayout.Label("GameObject to record the hand grab poses for:");
|
||||
GenerateObjectField(ref _item);
|
||||
GUILayout.Label("Prefabs provider for the hands (ghosts) to visualize the recorded poses:");
|
||||
GenerateObjectField(ref _ghostProvider);
|
||||
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("<size=20>2</size>\nGo to <b>Play Mode</b> and record as many poses as you need.", _richTextStyle);
|
||||
GUILayout.Label($"Press the big <b>Record</b> button with your free hand\nor the <b>{_recordKey}</b> key to record a HandGrabPose <b>(requires focus on this window)</b>.", _richTextStyle);
|
||||
_recordKey = (KeyCode)EditorGUILayout.EnumPopup(_recordKey);
|
||||
if (GUILayout.Button("Record HandGrabPose", GUILayout.Height(100)))
|
||||
{
|
||||
RecordPose();
|
||||
}
|
||||
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("<size=20>3</size>\nStore your poses before exiting <b>Play Mode</b>.\nIf no collection is provided <b>it will autogenerate one</b>", _richTextStyle);
|
||||
GenerateObjectField(ref _posesCollection);
|
||||
if (GUILayout.Button("Save To Collection"))
|
||||
{
|
||||
SaveToAsset();
|
||||
}
|
||||
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("<size=20>4</size>\nNow load the poses from the PosesCollection in <b>Edit Mode</b> to tweak and persist them as gameobjects", _richTextStyle);
|
||||
if (GUILayout.Button("Load From Collection"))
|
||||
{
|
||||
LoadFromAsset();
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void GenerateObjectField<T>(ref T obj) where T : Object
|
||||
{
|
||||
obj = EditorGUILayout.ObjectField(obj, typeof(T), true) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the nearest object that can be snapped to and adds a new HandGrabInteractable to
|
||||
/// it with the user hand representation.
|
||||
/// </summary>
|
||||
public void RecordPose()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Debug.LogError("Recording tracked hands only works in Play Mode!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Hand == null)
|
||||
{
|
||||
Debug.LogError("Missing Hand reference.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_item == null)
|
||||
{
|
||||
Debug.LogError("Missing recordable item", this);
|
||||
return;
|
||||
}
|
||||
|
||||
HandPose trackedHandPose = TrackedPose();
|
||||
if (trackedHandPose == null)
|
||||
{
|
||||
Debug.LogError("Tracked Pose could not be retrieved", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Hand.GetRootPose(out Pose handRoot))
|
||||
{
|
||||
Debug.LogError("Hand Root pose could not be retrieved", this);
|
||||
return;
|
||||
}
|
||||
|
||||
Pose gripPoint = PoseUtils.DeltaScaled(_item.transform, handRoot);
|
||||
HandGrabPose point = AddHandGrabPose(trackedHandPose, gripPoint);
|
||||
AttachGhost(point);
|
||||
}
|
||||
|
||||
private HandPose TrackedPose()
|
||||
{
|
||||
if (!Hand.GetJointPosesLocal(out ReadOnlyHandJointPoses localJoints))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
HandPose result = new HandPose(Hand.Handedness);
|
||||
for (int i = 0; i < FingersMetadata.HAND_JOINT_IDS.Length; ++i)
|
||||
{
|
||||
HandJointId jointID = FingersMetadata.HAND_JOINT_IDS[i];
|
||||
result.JointRotations[i] = localJoints[jointID].rotation;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void AttachGhost(HandGrabPose point)
|
||||
{
|
||||
if (_ghostProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
HandGhost ghostPrefab = _ghostProvider.GetHand(Hand.Handedness);
|
||||
HandGhost ghost = GameObject.Instantiate(ghostPrefab, point.transform);
|
||||
ghost.SetPose(point);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new HandGrabInteractable at the exact pose of a given hand.
|
||||
/// Mostly used with Hand-Tracking at Play-Mode
|
||||
/// </summary>
|
||||
/// <param name="rawPose">The user controlled hand pose.</param>
|
||||
/// <param name="snapPoint">The user controlled hand pose.</param>
|
||||
/// <returns>The generated HandGrabPose.</returns>
|
||||
private HandGrabPose AddHandGrabPose(HandPose rawPose, Pose snapPoint)
|
||||
{
|
||||
HandGrabInteractable interactable = HandGrabUtils.CreateHandGrabInteractable(_item.transform);
|
||||
var pointData = new HandGrabUtils.HandGrabPoseData()
|
||||
{
|
||||
handPose = rawPose,
|
||||
scale = Hand.Scale / interactable.RelativeTo.lossyScale.x,
|
||||
gripPose = snapPoint,
|
||||
};
|
||||
return HandGrabUtils.LoadHandGrabPose(interactable, pointData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new HandGrabInteractable from the stored data.
|
||||
/// Mostly used to restore a HandGrabInteractable that was stored during Play-Mode.
|
||||
/// </summary>
|
||||
/// <param name="data">The data of the HandGrabInteractable.</param>
|
||||
/// <returns>The generated HandGrabInteractable.</returns>
|
||||
private HandGrabInteractable LoadHandGrabInteractable(HandGrabUtils.HandGrabInteractableData data)
|
||||
{
|
||||
HandGrabInteractable interactable = HandGrabUtils.CreateHandGrabInteractable(_item.transform);
|
||||
HandGrabUtils.LoadData(interactable, data);
|
||||
return interactable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the interactables to a SerializedObject (the empty object must be
|
||||
/// provided in the inspector or one will be auto-generated). First it translates the HandGrabInteractable to a serialized
|
||||
/// form HandGrabbableData).
|
||||
/// This method is called from a button in the Inspector.
|
||||
/// </summary>
|
||||
private void SaveToAsset()
|
||||
{
|
||||
if (_posesCollection == null)
|
||||
{
|
||||
GenerateCollectionAsset();
|
||||
}
|
||||
var savedPoses = new List<HandGrabUtils.HandGrabInteractableData>();
|
||||
foreach (HandGrabInteractable snap in _item.GetComponentsInChildren<HandGrabInteractable>(false))
|
||||
{
|
||||
savedPoses.Add(HandGrabUtils.SaveData(snap));
|
||||
}
|
||||
_posesCollection.StoreInteractables(savedPoses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the HandGrabInteractable from a Collection.
|
||||
/// This method is called from a button in the Inspector and will load the posesCollection.
|
||||
/// </summary>
|
||||
private void LoadFromAsset()
|
||||
{
|
||||
if (_posesCollection == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var handPose in _posesCollection.InteractablesData)
|
||||
{
|
||||
LoadHandGrabInteractable(handPose);
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateCollectionAsset()
|
||||
{
|
||||
_posesCollection = ScriptableObject.CreateInstance<HandGrabInteractableDataCollection>();
|
||||
string parentDir = Path.Combine("Assets", "HandGrabInteractableDataCollection");
|
||||
if (!Directory.Exists(parentDir))
|
||||
{
|
||||
Directory.CreateDirectory(parentDir);
|
||||
}
|
||||
string name = _item != null ? _item.name : "Auto";
|
||||
AssetDatabase.CreateAsset(_posesCollection, Path.Combine(parentDir, $"{name}_HandGrabCollection.asset"));
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 998bdfaa2de43034b9c84c40a602b122
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under the Oculus SDK License Agreement (the "License");
|
||||
* you may not use the Oculus SDK except in compliance with the License,
|
||||
* which is provided at the time of installation or download, or which
|
||||
* otherwise accompanies this software in either electronic or hard copy form.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://developer.oculus.com/licenses/oculussdk/
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, the Oculus SDK
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Oculus.Interaction.Editor
|
||||
{
|
||||
public interface IRemoteDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// A draw method that can be called
|
||||
/// by another editor script.
|
||||
/// </summary>
|
||||
void DrawRemote();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d64e49672e02fd7409405312cf2bd546
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under the Oculus SDK License Agreement (the "License");
|
||||
* you may not use the Oculus SDK except in compliance with the License,
|
||||
* which is provided at the time of installation or download, or which
|
||||
* otherwise accompanies this software in either electronic or hard copy form.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://developer.oculus.com/licenses/oculussdk/
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, the Oculus SDK
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Oculus.Interaction.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an [Optional] label in the inspector over any SerializedField with this attribute.
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(OptionalAttribute))]
|
||||
public class OptionalDrawer : DecoratorDrawer
|
||||
{
|
||||
private static readonly float HEADER_MARGIN = 1f;
|
||||
|
||||
public override void OnGUI(Rect position)
|
||||
{
|
||||
position.y += HEADER_MARGIN;
|
||||
OptionalAttribute optional = attribute as OptionalAttribute;
|
||||
string text = "[Optional";
|
||||
if ((optional.Flags & OptionalAttribute.Flag.AutoGenerated) != 0)
|
||||
{
|
||||
text += ", Auto-Generated if missing";
|
||||
}
|
||||
text += "]";
|
||||
EditorGUI.LabelField(position, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f23e2cef5c87c842828dec6dd676b0b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under the Oculus SDK License Agreement (the "License");
|
||||
* you may not use the Oculus SDK except in compliance with the License,
|
||||
* which is provided at the time of installation or download, or which
|
||||
* otherwise accompanies this software in either electronic or hard copy form.
|
||||
*
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://developer.oculus.com/licenses/oculussdk/
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, the Oculus SDK
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
using Assert = NUnit.Framework.Assert;
|
||||
using Debug = UnityEngine.Debug;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Oculus.Interaction
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
public class PluginUpdater
|
||||
{
|
||||
private static bool _isRestarting = false;
|
||||
private static string _keyBase = "Oculus_Interaction_PluginUpdater";
|
||||
private static string _keyDontAsk = _keyBase + "_DontAsk";
|
||||
private static string _keyIsInstalling = _keyBase + "_IsRestarting";
|
||||
private static BuildTarget[] buildTargets = { BuildTarget.Android, BuildTarget.StandaloneWindows, BuildTarget.StandaloneWindows64 };
|
||||
private static string _baseDllName = "InteractionSdk";
|
||||
|
||||
static PluginUpdater()
|
||||
{
|
||||
EditorApplication.delayCall += HandleDelayCall;
|
||||
}
|
||||
|
||||
[MenuItem("Oculus/Interaction/Update Interaction SDK Plugin")]
|
||||
public static void UpdatePlugin()
|
||||
{
|
||||
PerformUpdate(verbose: true);
|
||||
}
|
||||
|
||||
private static string GetProjectPath()
|
||||
{
|
||||
string path = Application.dataPath;
|
||||
string token = "/Assets";
|
||||
if (path.EndsWith(token))
|
||||
{
|
||||
path = path.Substring(0, path.Length - token.Length);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static void HandleDelayCall()
|
||||
{
|
||||
if (!EditorApplication.isPlaying && !_isRestarting)
|
||||
{
|
||||
// first off, check to see if there's NO current dll for the build targets, and if so just copy the new one over right now since there won't be a chance of write blocking
|
||||
foreach (BuildTarget t in buildTargets)
|
||||
{
|
||||
if (IsNewLibraryPresent(t) && !IsCurrentLibraryPresent(t))
|
||||
{
|
||||
MoveNewLibary(t);
|
||||
}
|
||||
}
|
||||
|
||||
// only do the moving when not running and not called from command line in batch mode
|
||||
if (!Application.isBatchMode)
|
||||
{
|
||||
// has an install been started and Unity just got restarted?
|
||||
// If so, start moving the new libraries into the names where the old ones were located
|
||||
if (PlayerPrefs.GetInt(_keyIsInstalling, 0) > 0)
|
||||
{
|
||||
foreach (BuildTarget t in buildTargets)
|
||||
{
|
||||
if (IsNewLibraryPresent(t))
|
||||
{
|
||||
MoveNewLibary(t);
|
||||
}
|
||||
}
|
||||
|
||||
// mark as done for next time Unity starts
|
||||
PlayerPrefs.SetInt(_keyIsInstalling, 0);
|
||||
}
|
||||
else if (PlayerPrefs.GetInt(_keyDontAsk, 0) == 0)
|
||||
{
|
||||
// if user hasn't asked us to stop bugging them, go on and ask 'em
|
||||
PerformUpdate(verbose: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTargetFolderName(BuildTarget target)
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case BuildTarget.Android:
|
||||
return "Android";
|
||||
case BuildTarget.StandaloneOSX:
|
||||
return "OSX";
|
||||
case BuildTarget.StandaloneWindows:
|
||||
return "Win32";
|
||||
case BuildTarget.StandaloneWindows64:
|
||||
return "Win64";
|
||||
default:
|
||||
throw new ArgumentException("Attempted GetTargetFolderName() for unsupported BuildTarget: " + target);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTargetDllSuffix(BuildTarget target)
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case BuildTarget.Android:
|
||||
return ".aar";
|
||||
case BuildTarget.StandaloneOSX:
|
||||
return ".bundle";
|
||||
case BuildTarget.StandaloneWindows:
|
||||
return ".dll";
|
||||
case BuildTarget.StandaloneWindows64:
|
||||
return ".dll";
|
||||
default:
|
||||
throw new ArgumentException("Attempted GetTargetDllSuffix() for unsupported BuildTarget: " + target);
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] FindAllSdkDlls(BuildTarget target)
|
||||
{
|
||||
char[] slashes = new char[] { '/', '\\' };
|
||||
|
||||
List<string> dllPaths = new List<string>();
|
||||
string[] dlls = AssetDatabase.FindAssets(_baseDllName);
|
||||
string targetPath = GetTargetFolderName(target);
|
||||
foreach (string dll in dlls)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(dll);
|
||||
string suffix = GetTargetDllSuffix(target);
|
||||
|
||||
if (path.Contains(_baseDllName + suffix) && !path.Contains(_baseDllName + suffix + ".disabled"))
|
||||
{
|
||||
// see if the path contains the build target name (e.g. Win64) we're looking for
|
||||
string[] subPaths = path.Split(slashes);
|
||||
foreach(string subpath in subPaths)
|
||||
{
|
||||
if (subpath == targetPath)
|
||||
{
|
||||
dllPaths.Add(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
return dllPaths.ToArray();
|
||||
}
|
||||
|
||||
private static void PerformUpdate(bool verbose)
|
||||
{
|
||||
int newLibsFound = 0;
|
||||
|
||||
foreach (BuildTarget t in buildTargets)
|
||||
{
|
||||
if (IsNewLibraryPresent(t))
|
||||
{
|
||||
newLibsFound++;
|
||||
}
|
||||
}
|
||||
|
||||
if (newLibsFound > 0)
|
||||
{
|
||||
// the ordering of the dialog responses is to keep consistency with the OVRPluginUpdater dialog responses
|
||||
int response = EditorUtility.DisplayDialogComplex("Update Interaction SDK libraries?", "New versions of Interaction SDK libraries were found, it is recommended you update them now.", "Ok", "No, Don't ask again", "Not right now");
|
||||
switch (response)
|
||||
{
|
||||
case 0: // yes
|
||||
if (MoveAllNewLibraries() > 0)
|
||||
{
|
||||
PlayerPrefs.SetInt(_keyIsInstalling, 1);
|
||||
response = EditorUtility.DisplayDialogComplex("Restart Unity?", "Unity needs to be restarted in order for the Interaction SDK installation to complete", "Ok", "Not right now", "Cancel");
|
||||
|
||||
if (response == 0)
|
||||
RestartUnity();
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerPrefs.SetInt(_keyIsInstalling, 0);
|
||||
EditorUtility.DisplayDialog("Library update error", "There was an issue updating the new Interaction SDK libraries", "Ok");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // dont ask again
|
||||
PlayerPrefs.SetInt(_keyDontAsk, 1);
|
||||
break;
|
||||
|
||||
case 2: // no
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Interaction SDK Plugin Updater", "No new libraries were found to update", "Ok");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int MoveAllNewLibraries()
|
||||
{
|
||||
int found = 0;
|
||||
foreach (BuildTarget t in buildTargets)
|
||||
{
|
||||
if (IsNewLibraryPresent(t))
|
||||
{
|
||||
found++;
|
||||
if (IsCurrentLibraryPresent(t))
|
||||
{
|
||||
MoveCurrentLibrary(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private static string GetCurrentLibraryAssetPath(BuildTarget target, bool newVersion)
|
||||
{
|
||||
string[] dlls = FindAllSdkDlls(target);
|
||||
foreach (var dllName in dlls)
|
||||
{
|
||||
string suffix = GetTargetDllSuffix(target);
|
||||
|
||||
if (dllName.Contains(suffix))
|
||||
{
|
||||
bool isNew = dllName.Contains(suffix + ".new");
|
||||
if (newVersion == isNew)
|
||||
return dllName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsCurrentLibraryPresent(BuildTarget target)
|
||||
{
|
||||
return GetCurrentLibraryAssetPath(target, false) != null;
|
||||
}
|
||||
|
||||
private static bool IsNewLibraryPresent(BuildTarget target)
|
||||
{
|
||||
return GetCurrentLibraryAssetPath(target, true) != null;
|
||||
}
|
||||
|
||||
private static bool MoveCurrentLibrary(BuildTarget target)
|
||||
{
|
||||
string srcPath = GetCurrentLibraryAssetPath(target, false);
|
||||
bool success = MoveAsset(srcPath, srcPath + ".disabled", true);
|
||||
return success;
|
||||
}
|
||||
|
||||
private static bool MoveNewLibary(BuildTarget target)
|
||||
{
|
||||
string src = GetCurrentLibraryAssetPath(target, true);
|
||||
string dest = src;
|
||||
if (dest.EndsWith(".new"))
|
||||
{
|
||||
dest = dest.Substring(0, dest.Length - 4);
|
||||
}
|
||||
bool success = MoveAsset(src, dest, false);
|
||||
if (success)
|
||||
{
|
||||
PluginImporter pi = PluginImporter.GetAtPath(dest) as PluginImporter;
|
||||
pi.SetCompatibleWithEditor(false);
|
||||
pi.SetCompatibleWithAnyPlatform(false);
|
||||
pi.SetCompatibleWithPlatform(BuildTarget.StandaloneOSX, false);
|
||||
pi.SetCompatibleWithPlatform(BuildTarget.Android, false);
|
||||
pi.SetCompatibleWithPlatform(BuildTarget.StandaloneWindows, false);
|
||||
pi.SetCompatibleWithPlatform(BuildTarget.StandaloneWindows64, false);
|
||||
pi.SetCompatibleWithPlatform(target, true);
|
||||
pi.SetCompatibleWithEditor(true);
|
||||
|
||||
switch (target)
|
||||
{
|
||||
case BuildTarget.StandaloneOSX:
|
||||
pi.SetCompatibleWithEditor(true);
|
||||
pi.SetEditorData("CPU", "AnyCPU");
|
||||
pi.SetEditorData("OS", "OSX");
|
||||
pi.SetPlatformData("Editor", "CPU", "AnyCPU");
|
||||
pi.SetPlatformData("Editor", "OS", "OSX");
|
||||
break;
|
||||
case BuildTarget.StandaloneWindows:
|
||||
pi.SetEditorData("CPU", "X86");
|
||||
pi.SetEditorData("OS", "Windows");
|
||||
pi.SetPlatformData("Editor", "CPU", "X86");
|
||||
pi.SetPlatformData("Editor", "OS", "Windows");
|
||||
break;
|
||||
case BuildTarget.Android:
|
||||
break;
|
||||
case BuildTarget.StandaloneWindows64:
|
||||
pi.SetCompatibleWithPlatform(BuildTarget.StandaloneWindows64, true);
|
||||
pi.SetCompatibleWithEditor(true);
|
||||
pi.SetEditorData("CPU", "X86_64");
|
||||
pi.SetEditorData("OS", "Windows");
|
||||
pi.SetPlatformData("Editor", "CPU", "X86_64");
|
||||
pi.SetPlatformData("Editor", "OS", "Windows");
|
||||
break;
|
||||
}
|
||||
|
||||
ReimportAsset(dest);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogErrorFormat("ISDK PluginUpdater: Error copying {0} to {1}", src, dest);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private static void ReimportAsset(string path)
|
||||
{
|
||||
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
|
||||
AssetDatabase.Refresh();
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
private static bool MoveAsset(string srcPath, string destPath, bool reImport)
|
||||
{
|
||||
string fullDest = GetProjectPath() + "/" + destPath;
|
||||
if (File.Exists(fullDest))
|
||||
{
|
||||
File.Delete(fullDest);
|
||||
File.Delete(fullDest + ".meta");
|
||||
}
|
||||
|
||||
string errMsg = AssetDatabase.MoveAsset(srcPath, destPath);
|
||||
if (errMsg.Length > 0)
|
||||
{
|
||||
UnityEngine.Debug.LogError(errMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reImport)
|
||||
{
|
||||
ReimportAsset(destPath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void RestartUnity()
|
||||
{
|
||||
_isRestarting = true;
|
||||
EditorApplication.OpenProject(GetProjectPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd9fd0b607103c74db62fc7a640c4779
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user