/// <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); }
/// <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> /// リップシンクの設定を行います。 /// </summary> /// <remarks> /// <see cref="BlendShapePreset.A"/>、<see cref="BlendShapePreset.I"/>、<see cref="BlendShapePreset.O"/>が /// 単一のフレームを持つシェイプキーが存在しない場合、設定を行いません。 /// 生成するシェイプキー名と同じシェイプキーが存在する場合、それを利用します。 /// </remarks> /// <param name="avatar"></param> /// <param name="clips"></param> /// <param name="useShapeKeyNormalsAndTangents"></param> private static void SetLipSync( GameObject avatar, IEnumerable <VRMBlendShapeClip> clips, bool useShapeKeyNormalsAndTangents ) { Transform transform = avatar.transform.Find(VRChatUtility.AutoBlinkMeshPath); var renderer = transform.GetComponent <SkinnedMeshRenderer>(); Mesh mesh = renderer.sharedMesh; foreach (var preset in new[] { BlendShapePreset.A, BlendShapePreset.I, BlendShapePreset.O }) { if (!clips.FirstOrDefault(c => c.Preset == preset)) { return; } } IEnumerable <BlendShape> shapeKeys = BlendShapeReplacer.GetAllShapeKeys(mesh, useShapeKeyNormalsAndTangents); foreach (var(newName, values) in BlendShapeReplacer.VisemeShapeKeyNamesAndValues) { if (mesh.GetBlendShapeIndex(newName) != -1) { continue; } Vector3[] deltaVertices = null; foreach (Vector3[] vertices in values.SelectMany(presetAndWeight => clips.First(clip => clip.Preset == presetAndWeight.Key).ShapeKeyValues.Select( shapeKeyNameAndWeight => shapeKeys .First(shapeKey => shapeKey.Name == shapeKeyNameAndWeight.Key) .Positions .Select(vertix => vertix * (shapeKeyNameAndWeight.Value / VRMUtility.MaxBlendShapeBindingWeight * presetAndWeight.Value)) .ToArray() ) )) { if (deltaVertices == null) { deltaVertices = vertices; continue; } for (var i = 0; i < deltaVertices.Length; i++) { deltaVertices[i] += vertices[i]; } } mesh.AddBlendShapeFrame( newName, BlendShapeReplacer.MaxBlendShapeFrameWeight, deltaVertices, null, null ); } EditorUtility.SetDirty(mesh); #if VRC_SDK_VRCSDK2 || VRC_SDK_VRCSDK3 var avatarDescriptor = avatar.GetComponent <VRC_AvatarDescriptor>(); avatarDescriptor.lipSync = VRC_AvatarDescriptor.LipSyncStyle.VisemeBlendShape; avatarDescriptor.VisemeSkinnedMesh = renderer; avatarDescriptor.VisemeBlendShapes = BlendShapeReplacer.VisemeShapeKeyNamesAndValues.Select(nameAndValues => nameAndValues.Key).ToArray(); #endif }