/// <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);
        }
Example #3
0
        /// <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);
        }