/// <summary> /// クラスに含まれる処理を適用します。 /// </summary> /// <param name="avatar"></param> /// <param name="clips"></param> /// <param name="useAnimatorForBlinks"></param> /// <param name="useShapeKeyNormalsAndTangents"></param> /// <param name="vrmBlendShapeForFINGERPOINT"></param> internal static IEnumerable <Converter.Message> Apply( GameObject avatar, IEnumerable <VRMBlendShapeClip> clips, bool useAnimatorForBlinks, bool useShapeKeyNormalsAndTangents, VRMBlendShapeClip vrmBlendShapeForFINGERPOINT ) { var messages = new List <Converter.Message>(); SetLipSync(avatar, clips, useShapeKeyNormalsAndTangents); #if VRC_SDK_VRCSDK2 if (useAnimatorForBlinks) { SetNeutralAndBlink(avatar, clips, useShapeKeyNormalsAndTangents); } else { SetBlinkWithoutAnimator(avatar, clips, useShapeKeyNormalsAndTangents); SetNeutralWithoutAnimator(avatar: avatar, clips: clips); } #else SetNeutralWithoutAnimator(avatar, clips); EnableEyeLook(avatar, clips, useShapeKeyNormalsAndTangents); #endif SetFeelings(avatar, clips, vrmBlendShapeForFINGERPOINT); return(messages); }
/// <summary> /// 表情の設定を行うアニメーションクリップを作成します。 /// </summary> /// <param name="avatar"></param> /// <param name="vrmBlendShape"></param> /// <param name="clips"></param> /// <returns></returns> private static AnimationClip CreateFeeling( GameObject avatar, VRMBlendShapeClip clip, ref IEnumerable <VRMBlendShapeClip> clips ) { var fileName = clip.Preset + ".anim"; #if VRC_SDK_VRCSDK2 var anim = Duplicator.DuplicateAssetToFolder <AnimationClip>( source: UnityPath.FromUnityPath(Converter.RootFolderPath).Child("animations") .Child(BlendShapeReplacer.MappingBlendShapeToVRChatAnim[clip.Preset] + ".anim") .LoadAsset <AnimationClip>(), prefabInstance: avatar, fileName ); Transform transform = avatar.transform.Find(VRChatUtility.AutoBlinkMeshPath); if (transform.GetComponent <Animator>()) { var curve = new AnimationCurve(); foreach (var(seconds, value) in BlendShapeReplacer.NeutralAndBlinkStopperWeights) { curve.AddKey(new Keyframe(seconds, value)); } anim.SetCurve( VRChatUtility.AutoBlinkMeshPath, typeof(Behaviour), "m_Enabled", curve ); clips = BlendShapeReplacer.DuplicateShapeKeyToUnique(avatar, clip, clips); } #else var anim = new AnimationClip(); AssetDatabase.CreateAsset( anim, Duplicator.DetermineAssetPath(prefabInstance: avatar, typeof(AnimationClip), fileName) ); clips = BlendShapeReplacer.DuplicateShapeKeyToUnique(avatar, clip, clips); #endif SetBlendShapeCurves( avatar, animationClip: anim, clip: clip, keys: new Dictionary <float, float> { { 0, 1 }, { anim.length, 1 }, } ); return(anim); }
/// <summary> /// クラスに含まれる処理を適用します。 /// </summary> /// <param name="avatar"></param> /// <param name="clips"></param> /// <param name="useShapeKeyNormalsAndTangents"></param> /// <param name="vrmBlendShapeForFINGERPOINT"></param> internal static void Apply( GameObject avatar, IEnumerable <VRMBlendShapeClip> clips, bool useShapeKeyNormalsAndTangents, VRMBlendShapeClip vrmBlendShapeForFINGERPOINT ) { SetLipSync(avatar, clips, useShapeKeyNormalsAndTangents); EnableEyeLook(avatar, clips, useShapeKeyNormalsAndTangents); SetFeelings(avatar, clips, vrmBlendShapeForFINGERPOINT); }
/// <summary> /// 指定されたブレンドシェイプに、<see cref="BlendShapePreset.Neutral"/>、および<see cref="BlendShapePreset.Blink"/>に含まれるシェイプキーと /// 同一のシェイプキーが含まれていれば、そのシェイプキーを複製します。 /// </summary> /// <param name="avatar"></param> /// <param name="clip"></param> /// <param name="clips"></param> /// <returns>置換後のVRMのブレンドシェイプ一覧。</returns> private static IEnumerable <VRMBlendShapeClip> DuplicateShapeKeyToUnique( GameObject avatar, VRMBlendShapeClip clip, IEnumerable <VRMBlendShapeClip> clips ) { string[] neutralAndBlinkShapeKeyNames = new[] { BlendShapePreset.Neutral, BlendShapePreset.Blink } .Select(selector: blendShapePreset => clips.FirstOrDefault(c => c.Preset == blendShapePreset)) .SelectMany(selector: blendShapeClip => blendShapeClip ? blendShapeClip.ShapeKeyValues : new Dictionary <string, float>()) .Select(selector: nameAndWeight => nameAndWeight.Key) .ToArray(); Mesh mesh = avatar.transform.Find(VRChatUtility.AutoBlinkMeshPath).GetSharedMesh(); foreach (var(oldName, weight) in clip.ShapeKeyValues) { if (!neutralAndBlinkShapeKeyNames.Contains(oldName)) { continue; } var newName = BlendShapeReplacer.FeelingsShapeKeyPrefix + oldName; var index = mesh.GetBlendShapeIndex(oldName); var frameCount = mesh.GetBlendShapeFrameCount(index); for (var i = 0; i < frameCount; i++) { var deltaVertices = new Vector3[mesh.vertexCount]; var deltaNormals = new Vector3[mesh.vertexCount]; var deltaTangents = new Vector3[mesh.vertexCount]; mesh.GetBlendShapeFrameVertices(index, i, deltaVertices, deltaNormals, deltaTangents); mesh.AddBlendShapeFrame( newName, mesh.GetBlendShapeFrameWeight(shapeIndex: index, frameIndex: i), deltaVertices, deltaNormals, deltaTangents ); } EditorUtility.SetDirty(mesh); clips = VRMUtility.ReplaceShapeKeyName(clips, oldName, newName); } return(clips); }
/// <summary> /// アニメーションクリップに、指定されたブレンドシェイプを追加します。 /// </summary> /// <param name="avatar"></param> /// <param name="animationClip"></param> /// <param name="clip"></param> /// <param name="keys"></param> private static void SetBlendShapeCurves( GameObject avatar, AnimationClip animationClip, VRMBlendShapeClip clip, IDictionary <float, float> keys ) { foreach (var(name, weight) in clip.ShapeKeyValues) { BlendShapeReplacer.SetBlendShapeCurve(animationClip, name, weight, keys, setRelativePath: true); } foreach (var bindings in clip.MaterialValues.GroupBy(binding => binding.MaterialName)) { BlendShapeReplacer.SetBlendShapeCurve(animationClip, avatar, clip.BlendShapeName, bindings, keys.Keys); } }
/// <summary> /// 手の形に喜怒哀楽を割り当てます。 /// </summary> /// <param name="avatar"></param> /// <param name="clips"></param> /// <param name="vrmBlendShapeForFINGERPOINT"></param> private static void SetFeelings( GameObject avatar, IEnumerable <VRMBlendShapeClip> clips, VRMBlendShapeClip vrmBlendShapeForFINGERPOINT ) { #if VRC_SDK_VRCSDK2 VRChatUtility.AddCustomAnims(avatar: avatar); var avatarDescriptor = avatar.GetOrAddComponent <VRC_AvatarDescriptor>(); #elif VRC_SDK_VRCSDK3 var fxController = Duplicator.DuplicateAssetToFolder( source: AssetDatabase.LoadAssetAtPath <AnimatorController>( AssetDatabase.GUIDToAssetPath(BlendShapeReplacer.FXTemplateGUID) ), prefabInstance: avatar ); var avatarDescriptor = avatar.GetOrAddComponent <VRCAvatarDescriptor>(); avatarDescriptor.customizeAnimationLayers = true; avatarDescriptor.baseAnimationLayers = new[] { new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.Base, isDefault = true, }, new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.Additive, isDefault = true, }, new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.Gesture, isDefault = true, }, new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.Action, isDefault = true, }, new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.FX, animatorController = fxController, }, }; avatarDescriptor.specialAnimationLayers = new[] { new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.Sitting, isDefault = true, }, new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.TPose, isDefault = true, }, new VRCAvatarDescriptor.CustomAnimLayer() { type = VRCAvatarDescriptor.AnimLayerType.IKPose, isDefault = true, }, }; var states = fxController.layers[1].stateMachine.states.Select(childState => childState.state).ToList(); #endif foreach (var preset in BlendShapeReplacer.MappingBlendShapeToVRChatAnim.Keys) { VRMBlendShapeClip blendShapeClip = preset == BlendShapePreset.Unknown ? vrmBlendShapeForFINGERPOINT : clips.FirstOrDefault(c => c.Preset == preset); if (!blendShapeClip) { continue; } AnimationClip animationClip = CreateFeeling(avatar, blendShapeClip, ref clips); string anim = BlendShapeReplacer.MappingBlendShapeToVRChatAnim[preset].ToString(); #if VRC_SDK_VRCSDK2 avatarDescriptor.CustomStandingAnims[anim] = animationClip; avatarDescriptor.CustomSittingAnims[anim] = animationClip; #elif VRC_SDK_VRCSDK3 states.First(s => s.name.ToLower() == anim.ToLower()).motion = animationClip; #endif } }
/// <summary> /// 【SDK3】<see cref="BlendShapePreset.Blink"/>を変換し、視線追従を有効化します。 /// </summary> /// <param name="avatar"></param> /// <param name="clips"></param> /// <param name="useShapeKeyNormalsAndTangents"/> private static void EnableEyeLook( GameObject avatar, IEnumerable <VRMBlendShapeClip> clips, bool useShapeKeyNormalsAndTangents ) { VRMBlendShapeClip clip = clips.FirstOrDefault(c => c.Preset == BlendShapePreset.Blink); var lookAtBoneApplyer = avatar.GetComponent <VRMLookAtBoneApplyer>(); if (!clip && !lookAtBoneApplyer) { return; } var renderer = avatar.transform.Find(VRChatUtility.AutoBlinkMeshPath).GetComponent <SkinnedMeshRenderer>(); var mesh = renderer.sharedMesh; if (clip) { mesh.AddBlendShapeFrame( BlendShapeReplacer.BlinkShapeKeyName, 1, BlendShapeReplacer.GenerateShapeKey( clip.ShapeKeyValues, BlendShapeReplacer.GetAllShapeKeys(mesh, useShapeKeyNormalsAndTangents) ), null, null ); EditorUtility.SetDirty(mesh); } #if VRC_SDK_VRCSDK3 var descriptor = avatar.GetComponent <VRCAvatarDescriptor>(); descriptor.enableEyeLook = true; var settings = new VRCAvatarDescriptor.CustomEyeLookSettings(); if (clip) { settings.eyelidType = VRCAvatarDescriptor.EyelidType.Blendshapes; settings.eyelidsSkinnedMesh = renderer; settings.eyelidsBlendshapes = new[] { mesh.blendShapeCount - 1, -1, -1 }; } if (lookAtBoneApplyer) { settings.eyeMovement = new VRCAvatarDescriptor.CustomEyeLookSettings.EyeMovements() { excitement = 0.5f, confidence = 0, }; settings.leftEye = lookAtBoneApplyer.LeftEye.Transform; settings.rightEye = lookAtBoneApplyer.RightEye.Transform; settings.eyesLookingUp = new VRCAvatarDescriptor.CustomEyeLookSettings.EyeRotations() { left = Quaternion.Euler(x: -lookAtBoneApplyer.VerticalUp.CurveYRangeDegree, 0, 0), right = Quaternion.Euler(x: -lookAtBoneApplyer.VerticalUp.CurveYRangeDegree, 0, 0), }; settings.eyesLookingDown = new VRCAvatarDescriptor.CustomEyeLookSettings.EyeRotations() { left = Quaternion.Euler(x: lookAtBoneApplyer.VerticalUp.CurveYRangeDegree, 0, 0), right = Quaternion.Euler(x: lookAtBoneApplyer.VerticalUp.CurveYRangeDegree, 0, 0), }; settings.eyesLookingLeft = new VRCAvatarDescriptor.CustomEyeLookSettings.EyeRotations() { left = Quaternion.Euler(0, y: -lookAtBoneApplyer.HorizontalOuter.CurveYRangeDegree, 0), right = Quaternion.Euler(0, y: -lookAtBoneApplyer.HorizontalInner.CurveYRangeDegree, 0), }; settings.eyesLookingRight = new VRCAvatarDescriptor.CustomEyeLookSettings.EyeRotations() { left = Quaternion.Euler(0, y: lookAtBoneApplyer.HorizontalInner.CurveYRangeDegree, 0), right = Quaternion.Euler(0, y: lookAtBoneApplyer.HorizontalOuter.CurveYRangeDegree, 0), }; } descriptor.customEyeLookSettings = settings; #endif }
/// <summary> /// <see cref="BlendShapePreset.Neutral"/>、および<see cref="BlendShapePreset.Blink"/>を変換します。 /// </summary> /// <remarks> /// 参照: /// 最新版(10/02時点)自動まばたきの実装【VRChat技術情報】 — VRChatパブリックログ /// <https://jellyfish-qrage.hatenablog.com/entry/2018/10/02/152316> /// VRchatでMMDモデルをアバターとして使う方法——上級者編 — 東屋書店 /// <http://www.hushimero.xyz/entry/vrchat-EyeTracking#%E5%A4%A7%E5%8F%A3%E9%96%8B%E3%81%91%E3%82%8B%E5%95%8F%E9%A1%8C%E3%81%AE%E8%A7%A3%E6%B1%BA> /// 技術勢の元怒さんのツイート: “自動まばたきはまばたきシェイプキーを、まばたき防止のほうは自動まばたきのエナブルONOFFを操作してますね。 欲しければサンプル渡せますよ。… ” /// <https://twitter.com/gend_VRchat/status/1100155987216879621> /// momoma/ナル@VRChatter/VTuberさんのツイート: “3F目にBehavior 1のキーを追加したら重複しなくなったわ、なるほどな… ” /// <https://twitter.com/momoma_creative/status/1137917887262339073> /// </remarks> /// <param name="avatar"></param> /// <param name="clips"></param> /// <paramref name="useShapeKeyNormalsAndTangents"/> private static void SetNeutralAndBlink( GameObject avatar, IEnumerable <VRMBlendShapeClip> clips, bool useShapeKeyNormalsAndTangents ) { AnimatorController neutralAndBlinkController = BlendShapeReplacer.CreateSingleAnimatorController(avatar: avatar, name: "blink"); AnimationClip animationClip = neutralAndBlinkController.animationClips[0]; VRMBlendShapeClip blinkClip = null; foreach (var preset in new[] { BlendShapePreset.Blink, BlendShapePreset.Neutral }) { VRMBlendShapeClip clip = clips.FirstOrDefault(c => c.Preset == preset); if (!clip) { continue; } if (preset == BlendShapePreset.Blink) { blinkClip = clip; } foreach (var(shapeKeyName, shapeKeyWeight) in clip.ShapeKeyValues) { var keys = BlendShapeReplacer.BlinkWeights; if (preset == BlendShapePreset.Neutral) { if (blinkClip && blinkClip.ShapeKeyValues.ContainsKey(shapeKeyName)) { // NEUTRALとBlinkが同一のシェイプキーを参照していた場合 float blinkShapeKeyWeight = blinkClip.ShapeKeyValues[shapeKeyName]; var animationCurve = new AnimationCurve(); foreach (var(seconds, blendShapePreset) in BlendShapeReplacer.NeutralAndBlinkWeights) { float weight; switch (blendShapePreset) { case BlendShapePreset.Neutral: weight = shapeKeyWeight; break; case BlendShapePreset.Blink: weight = blinkShapeKeyWeight; break; default: weight = 0; break; } animationCurve.AddKey(new Keyframe(seconds, weight)); } animationClip.SetCurve( "", typeof(SkinnedMeshRenderer), "blendShape." + shapeKeyName, animationCurve ); continue; } keys = BlendShapeReplacer.NeutralWeights; } SetBlendShapeCurve(animationClip, shapeKeyName, shapeKeyWeight, keys, setRelativePath: false); } foreach (MaterialValueBinding binding in clip.MaterialValues) { // TODO } } Transform transform = avatar.transform.Find(VRChatUtility.AutoBlinkMeshPath); transform.gameObject.AddComponent <Animator>().runtimeAnimatorController = neutralAndBlinkController; // VRChat側の自動まばたきを回避 Mesh mesh = transform.GetSharedMesh(); IEnumerable <BlendShape> shapeKeys = BlendShapeReplacer.GetAllShapeKeys(mesh, useShapeKeyNormalsAndTangents); mesh.ClearBlendShapes(); foreach (string name in BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin) { BlendShapeReplacer.AddDummyShapeKey(mesh: mesh, name: name); } foreach (BlendShape shapeKey in shapeKeys) { if (BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.Contains(shapeKey.Name)) { continue; } mesh.AddBlendShapeFrame( shapeKey.Name, BlendShapeReplacer.MaxBlendShapeFrameWeight, shapeKey.Positions.ToArray(), shapeKey.Normals.ToArray(), shapeKey.Tangents.ToArray() ); } EditorUtility.SetDirty(mesh); }