/// <summary> /// オートアイムーブメントが有効化される条件を揃えます。 /// </summary> /// <remarks> /// 参照: /// 100の人さんのツイート: “Body当たりでした! オートアイムーブメントの条件解明! • ルート直下に、BlendShapeが4つ以上設定された「Body」という名前のオブジェクトが存在する • ルート直下に Armature/Hips/Spine/Chest/Neck/Head/RightEyeとLeftEye ※すべて空のオブジェクトで良い ※目のボーンの名称は何でも良い… https://t.co/dLnHl7QjJk” /// <https://twitter.com/esperecyan/status/1045713562348347392> /// </remarks> /// <param name="avatar"></param> private static void EnableAutoEyeMovement(GameObject avatar) { // ダミーの階層構造の作成 foreach (var path in VRChatUtility.RequiredPathForAutoEyeMovement.Concat(new string[] { VRChatUtility.AutoBlinkMeshPath })) { var current = avatar.transform; foreach (var name in path.Split(separator: '/')) { Transform child = current.Find(name); if (!child) { child = new GameObject(name).transform; child.parent = current; } current = child; } } // ダミーのまばたき用ブレンドシェイプの作成 Mesh mesh = avatar.transform.Find(VRChatUtility.AutoBlinkMeshPath).GetSharedMesh(); if (mesh.blendShapeCount >= BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.Count()) { return; } foreach (var name in BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.Skip(count: mesh.blendShapeCount)) { BlendShapeReplacer.AddDummyShapeKey(mesh: mesh, name: name); } EditorUtility.SetDirty(mesh); }
/// <summary> /// Animatorコンポーネントを使用せずに、<see cref="BlendShapePreset.Neutral"/>、および<see cref="BlendShapePreset.Blink"/>を変換します。 /// </summary> /// <remarks> /// <see cref="BlendShapePreset.Blink"/>が関連付けられたメッシュが見つからない、またはそのメッシュに /// <see cref="BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin"/>がそろっていれば何もしません。 /// それらのキーが存在せず、<see cref="BlendShapePreset.Blink_L"/>、<see cref="BlendShapePreset.Blink_R"/>がいずれも設定されていればそれを優先します。 /// </remarks> /// <param name="avatar"></param> /// <param name="clips"></param> /// <param name="useShapeKeyNormalsAndTangents"></param> private static void SetBlinkWithoutAnimator( GameObject avatar, IEnumerable <VRMBlendShapeClip> clips, bool useShapeKeyNormalsAndTangents ) { var renderer = avatar.transform.Find(VRChatUtility.AutoBlinkMeshPath).GetComponent <SkinnedMeshRenderer>(); Mesh mesh = renderer.sharedMesh; if (BlendShapeReplacer.GetBlendShapeNames(mesh: mesh) .Take(BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.Count()) .SequenceEqual(BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin)) { return; } IEnumerable <BlendShape> shapeKeys = BlendShapeReplacer.GetAllShapeKeys(mesh, useShapeKeyNormalsAndTangents); mesh.ClearBlendShapes(); var dummyShapeKeyNames = new List <string>(); if (clips.FirstOrDefault(c => c.Preset == BlendShapePreset.Blink_L) && clips.FirstOrDefault(c => c.Preset == BlendShapePreset.Blink_R)) { mesh.AddBlendShapeFrame( BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.ElementAt(0), BlendShapeReplacer.MaxBlendShapeFrameWeight, BlendShapeReplacer.GenerateShapeKey( namesAndWeights: clips.First(c => c.Preset == BlendShapePreset.Blink_L).ShapeKeyValues, shapeKeys: shapeKeys ), null, null ); mesh.AddBlendShapeFrame( BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.ElementAt(1), BlendShapeReplacer.MaxBlendShapeFrameWeight, BlendShapeReplacer.GenerateShapeKey( namesAndWeights: clips.First(c => c.Preset == BlendShapePreset.Blink_R).ShapeKeyValues, shapeKeys: shapeKeys ), null, null ); } else { mesh.AddBlendShapeFrame( BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.ElementAt(0), BlendShapeReplacer.MaxBlendShapeFrameWeight, BlendShapeReplacer.GenerateShapeKey( namesAndWeights: clips.First(c => c.Preset == BlendShapePreset.Blink).ShapeKeyValues, shapeKeys: shapeKeys ), null, null ); dummyShapeKeyNames.Add(BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.ElementAt(1)); } dummyShapeKeyNames.AddRange(BlendShapeReplacer.OrderedBlinkGeneratedByCatsBlenderPlugin.Skip(2)); foreach (string name in dummyShapeKeyNames) { 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); }
/// <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); }