avatar/Assets/VRCFury/Scripts/Editor/VF/Feature/BlendshapeOptimizerBuilder.cs

313 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using VF.Builder;
using VF.Feature.Base;
using VF.Inspector;
using VF.Model.Feature;
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
using Object = UnityEngine.Object;
namespace VF.Feature {
public class BlendshapeOptimizerBuilder : FeatureBuilder<BlendshapeOptimizer> {
public override string GetEditorTitle() {
return "Blendshape Optimizer";
}
public override VisualElement CreateEditor(SerializedProperty prop) {
var content = new VisualElement();
content.Add(VRCFuryEditorUtils.Info(
"This feature will automatically bake all non-animated blendshapes into the mesh," +
" saving VRAM for free!"
));
var adv = new Foldout {
text = "Advanced Options",
value = false
};
content.Add(adv);
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("keepMmdShapes"), "Keep MMD Blendshapes"));
return content;
}
public override bool AvailableOnProps() {
return false;
}
[FeatureBuilderAction(FeatureOrder.BlendshapeOptimizer)]
public void Apply() {
foreach (var mesh in GetAllSkinMeshes()) {
var blendshapeCount = mesh.blendShapeCount;
if (blendshapeCount == 0) continue;
var animatedBlendshapes = new HashSet<string>();
animatedBlendshapes.UnionWith(CollectAnimatedBlendshapesForMesh(mesh));
if (model.keepMmdShapes) {
animatedBlendshapes.UnionWith(mmdShapes);
}
var keepAll = true;
foreach (var name in Enumerable.Range(0, blendshapeCount).Select(i => mesh.GetBlendShapeName(i))) {
if (!animatedBlendshapes.Contains(name)) {
keepAll = false;
}
}
if (keepAll) continue;
var blendshapeIdsToKeep = Enumerable.Range(0, blendshapeCount)
.Where(id => animatedBlendshapes.Contains(mesh.GetBlendShapeName(id)))
.ToImmutableHashSet();
var skinsForMesh = CollectSkinsUsingMesh(mesh);
var savedWeights = skinsForMesh
.Select(skin => {
var weights = Enumerable.Range(0, blendshapeCount)
.Select(skin.GetBlendShapeWeight).ToArray();
return (skin, weights);
})
.ToArray();
var savedBlendshapes = Enumerable.Range(0, blendshapeCount)
.Select(id => new SavedBlendshape(mesh, id))
.ToArray();
var meshCopy = Object.Instantiate(mesh);
VRCFuryAssetDatabase.SaveAsset(meshCopy, tmpDir, "bsopt_" + meshCopy.name);
meshCopy.ClearBlendShapes();
for (var id = 0; id < blendshapeCount; id++) {
var savedBlendshape = savedBlendshapes[id];
var keep = blendshapeIdsToKeep.Contains(id);
if (keep) {
savedBlendshape.SaveTo(meshCopy);
} else {
var (_, firstSkinWeights) = savedWeights[0];
savedBlendshape.BakeTo(meshCopy, firstSkinWeights[id]);
}
}
EditorUtility.SetDirty(meshCopy);
var avatars = avatarObject.GetComponentsInChildren<VRCAvatarDescriptor>(true);
foreach (var (skin, weights) in savedWeights) {
skin.sharedMesh = meshCopy;
var newId = 0;
for (var id = 0; id < blendshapeCount; id++) {
var keep = blendshapeIdsToKeep.Contains(id);
if (keep) {
skin.SetBlendShapeWeight(newId, weights[id]);
foreach (var avatar in avatars) {
if (avatar.customEyeLookSettings.eyelidsSkinnedMesh == skin) {
for (var i = 0; i < avatar.customEyeLookSettings.eyelidsBlendshapes.Length; i++) {
if (avatar.customEyeLookSettings.eyelidsBlendshapes[i] == id) {
avatar.customEyeLookSettings.eyelidsBlendshapes[i] = newId;
EditorUtility.SetDirty(avatar);
}
}
}
}
newId++;
}
}
EditorUtility.SetDirty(skin);
}
}
}
private class SavedBlendshape {
private string name;
private List<Tuple<float, Vector3[], Vector3[], Vector3[]>> frames
= new List<Tuple<float, Vector3[], Vector3[], Vector3[]>>();
public SavedBlendshape(Mesh mesh, int id) {
name = mesh.GetBlendShapeName(id);
for (var i = 0; i < mesh.GetBlendShapeFrameCount(id); i++) {
var weight = mesh.GetBlendShapeFrameWeight(id, i);
var v = new Vector3[mesh.vertexCount];
var n = new Vector3[mesh.vertexCount];
var t = new Vector3[mesh.vertexCount];
mesh.GetBlendShapeFrameVertices(id, i, v, n, t);
frames.Add(Tuple.Create(weight, v, n, t));
}
}
public void SaveTo(Mesh mesh) {
foreach (var (w, v, n, t) in frames) {
mesh.AddBlendShapeFrame(name, w, v, n, t);
}
}
public void BakeTo(Mesh mesh, float weight100) {
// TODO: Is this how multiple frames work?
var lastFrame = frames[frames.Count - 1];
if (frames.Count == 0 || weight100 == 0) {
return;
} else if (frames.Count == 1 || weight100 < 0 || weight100 >= lastFrame.Item1) {
var (_, dv, dn, dt) = lastFrame;
BakeTo(mesh, dv, dn, dt, weight100);
} else {
var beforeFrame = Enumerable
.Range(0, frames.Count)
.First(frame => frame == frames.Count || weight100 <= frames.Count);
if (beforeFrame == 0) {
var (fw, fv, fn, ft) = frames[0];
BakeTo(mesh, fv, fn, ft, weight100 / fw);
} else {
var (fw1, fv1, fn1, ft1) = frames[beforeFrame-1];
var (fw2, fv2, fn2, ft2) = frames[beforeFrame];
var fraction = (weight100 - fw1) / (fw2 - fw1);
var dv = Enumerable.Zip(fv1, fv2, (a, b) => a + (b - a) * fraction).ToArray();
var dn = Enumerable.Zip(fn1, fn2, (a, b) => a + (b - a) * fraction).ToArray();
var dt = Enumerable.Zip(ft1, ft2, (a, b) => a + (b - a) * fraction).ToArray();
BakeTo(mesh, dv, dn, dt);
}
}
}
private static void BakeTo(Mesh mesh, Vector3[] dv, Vector3[] dn, Vector3[] dt, float weight100 = 100) {
var verts = mesh.vertices;
var normals = mesh.normals;
var tangents = mesh.tangents;
for (var i = 0; i < verts.Length && i < dv.Length; i++) {
verts[i] += dv[i] * (weight100 / 100);
}
for (var i = 0; i < normals.Length && i < dn.Length; i++) {
normals[i] += dn[i] * (weight100 / 100);
}
for (var i = 0; i < tangents.Length && i < dt.Length; i++) {
var d = dt[i] * (weight100 / 100);
tangents[i] += new Vector4(d.x, d.y, d.z, 0);
}
mesh.vertices = verts;
mesh.normals = normals;
mesh.tangents = tangents;
}
}
private ICollection<Mesh> GetAllSkinMeshes() {
return GetAllSkins()
.Select(skin => skin.sharedMesh)
.Where(mesh => mesh != null)
.ToImmutableHashSet();
}
private ICollection<SkinnedMeshRenderer> GetAllSkins() {
return avatarObject.GetComponentsInChildren<SkinnedMeshRenderer>(true);
}
private ICollection<SkinnedMeshRenderer> CollectSkinsUsingMesh(Mesh mesh) {
return GetAllSkins()
.Where(skin => skin.sharedMesh == mesh)
.ToImmutableHashSet();
}
private ICollection<string> CollectAnimatedBlendshapesForMesh(Mesh mesh) {
var animatedBindings = manager.GetAllUsedControllersRaw()
.Select(tuple => tuple.Item2)
.SelectMany(controller => {
var clipsInController = new List<AnimationClip>();
foreach (var layer in controller.layers) {
AnimatorIterator.ForEachClip(layer.stateMachine, clip => clipsInController.Add(clip));
}
return clipsInController;
})
.SelectMany(clip => {
var bindings = AnimationUtility.GetCurveBindings(clip);
return bindings.Select(b => (b, AnimationUtility.GetEditorCurve(clip, b)));
})
.ToList();
var skins = CollectSkinsUsingMesh(mesh);
var skinPaths = skins
.Select(skin => clipBuilder.GetPath(skin.transform))
.ToImmutableHashSet();
var blendshapeNames = new List<string>();
for (var i = 0; i < mesh.blendShapeCount; i++) {
blendshapeNames.Add(mesh.GetBlendShapeName(i));
}
var animatedBlendshapes = new HashSet<string>();
foreach (var tuple in animatedBindings) {
var (binding, curve) = tuple;
if (binding.type != typeof(SkinnedMeshRenderer)) continue;
if (!binding.propertyName.StartsWith("blendShape.")) continue;
if (!skinPaths.Contains(binding.path)) continue;
var blendshape = binding.propertyName.Substring(11);
var blendshapeId = mesh.GetBlendShapeIndex(blendshape);
var animatesToNondefaultValue = false;
if (blendshapeId >= 0) {
var skinDefaultValues = skins
.Select(skin => skin.GetBlendShapeWeight(blendshapeId))
.ToArray();
foreach (var frameValue in curve.keys.Select(key => key.value)) {
foreach (var skinDefaultValue in skinDefaultValues) {
if (!Mathf.Approximately(frameValue, skinDefaultValue)) {
animatesToNondefaultValue = true;
}
}
}
}
if (animatesToNondefaultValue) {
animatedBlendshapes.Add(blendshape);
}
}
foreach (var avatar in avatarObject.GetComponentsInChildren<VRCAvatarDescriptor>(true)) {
if (avatar.customEyeLookSettings.eyelidType == VRCAvatarDescriptor.EyelidType.Blendshapes) {
if (skins.Contains(avatar.customEyeLookSettings.eyelidsSkinnedMesh)) {
foreach (var b in avatar.customEyeLookSettings.eyelidsBlendshapes) {
if (b >= 0 && b < blendshapeNames.Count) {
animatedBlendshapes.Add(blendshapeNames[b]);
}
}
}
}
if (skins.Contains(avatar.VisemeSkinnedMesh)) {
if (avatar.lipSync == VRC_AvatarDescriptor.LipSyncStyle.JawFlapBlendShape) {
animatedBlendshapes.Add(avatar.MouthOpenBlendShapeName);
}
if (avatar.lipSync == VRC_AvatarDescriptor.LipSyncStyle.VisemeBlendShape) {
foreach (var b in avatar.VisemeBlendShapes) {
animatedBlendshapes.Add(b);
}
}
}
}
for (var i = 0; i < mesh.blendShapeCount; i++) {
var weightsUsedForBlendshape = skins
.Select(skin => skin.GetBlendShapeWeight(i))
.ToImmutableHashSet();
if (weightsUsedForBlendshape.Count > 1) {
animatedBlendshapes.Add(blendshapeNames[i]);
}
}
return animatedBlendshapes;
}
private static readonly HashSet<string> mmdShapes = new HashSet<string> {
"通常", "まばたき", "笑い", "ウィンク", "ウィンク右", "ウィンク2", "ウィンク2",
"ウィンク2右", "ウィンク2右", "なごみ", "はぅ", "びっくり", "じと目", "キリッ", "はちゅ目", "はちゅ目縦潰れ", "はちゅ目横潰れ", "星目",
"はぁと", "瞳小", "瞳縦潰れ", "光下", "恐ろしい子!", "ハイライト消し", "映り込み消し", "あ", "い", "う", "え",
"お", "あ2", "あ2", "ワ", "ω", "ω□", "にやり", "にやり2", "にやり2", "にっこり", "ぺろっ", "てへぺろ", "てへぺろ2", "てへぺろ2", "口角上げ",
"口角下げ", "口横広げ", "歯無し上", "歯無し下", "ハンサム", "真面目", "困る", "にこり", "怒り", "上", "下", "前",
"眉頭左", "眉頭右", "照れ", "涙", "がーん", "青ざめる", "髪影消", "輪郭", "メガネ", "みっぱい",
};
}
}