Initial Commit

This commit is contained in:
Lillith Rose 2026-05-19 11:07:58 -04:00
parent 9089cf343f
commit accf7e787f
20 changed files with 623 additions and 0 deletions

287
Editor/Core.cs Normal file
View file

@ -0,0 +1,287 @@
using System.Linq;
using System.Diagnostics;
using AnimatorAsCode.V1;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Dynamics.Constraint.Components;
using System.Collections.Generic;
using UnityEditor.Animations;
namespace gay.lilyy.SoldAvatarBootstrap
{
public class AvatarAssets
{
public AacFlBase aac;
public GameObject root;
public AacFlController fx;
public AacFlClip emptyClip;
public bool experimentalEnabled;
public bool isPC;
}
public static class AvatarUtils
{
public static GameObject FindChildRecursive(GameObject parent, string name)
{
return FindChildRecursive(parent.transform, name)?.gameObject;
}
public static Transform FindChildRecursive(Transform parent, string name)
{
return parent.GetComponentsInChildren<Transform>(true).FirstOrDefault(t => t.gameObject.name == name);
}
public static Transform[] FindChildrenRecursive(Transform parent, string name)
{
return parent.GetComponentsInChildren<Transform>(true).Where(t => t.gameObject.name == name).ToArray();
}
public static AacFlClip CreateConstraintWeightClip(AvatarAssets assets, VRCParentConstraint constraint, int index, int indexCount)
{
return assets.aac.NewClip()
.Animating(action =>
{
action.Animates(constraint, $"Sources.source{index}.Weight").WithOneFrame(1f);
for (int i = 0; i < indexCount; i++)
{
if (i == index) continue;
action.Animates(constraint, $"Sources.source{i}.Weight").WithOneFrame(0f);
}
});
}
public static AacFlClip CreateEmptyClipWithFrames(AvatarAssets assets, int frames)
{
if (frames == 0) return assets.emptyClip;
var emptyGo = new GameObject();
emptyGo.name = "_EmptyClipInstant";
emptyGo.transform.SetParent(assets.root.transform);
var clip = assets.aac.NewClip().Animating(action =>
{
action.Animates(emptyGo.transform, "m_LocalScale.z").WithFrameCountUnit(unit =>
{
unit.Constant(0, 0);
unit.Constant(frames, 1);
});
});
Object.DestroyImmediate(emptyGo);
return clip;
}
public static AacFlClip CreateEmptyClipWithSeconds(AvatarAssets assets, float seconds)
{
if (seconds == 0) return assets.emptyClip;
var emptyGo = new GameObject();
emptyGo.name = "_EmptyClipInstant";
emptyGo.transform.SetParent(assets.root.transform);
var clip = assets.aac.NewClip().Animating(action =>
{
action.Animates(emptyGo.transform, "m_LocalScale.z").WithFrameCountUnit(unit =>
{
unit.Constant(0, 0);
unit.Constant(seconds, 1);
});
});
Object.DestroyImmediate(emptyGo);
return clip;
}
public static List<SkinnedMeshRenderer> FindWithBlendshape(AvatarAssets assets, string shapeName)
{
var list = new List<SkinnedMeshRenderer>();
foreach (var smr in assets.root.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
var mesh = smr.sharedMesh;
if (!mesh) continue;
int count = mesh.blendShapeCount;
for (int i = 0; i < count; i++)
{
if (mesh.GetBlendShapeName(i) == shapeName)
{
list.Add(smr);
break;
}
}
}
return list;
}
public static List<T> FindNamedComponents<T>(Transform parent, string name) where T : Component
{
var list = new List<T>();
foreach (var component in parent.GetComponentsInChildren<T>(true))
{
if (component.gameObject.name == name)
{
list.Add(component);
}
}
UnityEngine.Debug.Log($"Found {list.Count} {name} components");
foreach (var component in list)
{
UnityEngine.Debug.Log($"Component: {component.gameObject.name}");
}
return list;
}
public static List<T> FindNamedComponents<T>(AvatarAssets assets, string name) where T : Component
{
return FindNamedComponents<T>(assets.root.transform, name);
}
}
public class AvatarBootstrapCore
{
private static AnimatorController GetController(AvatarDefinition definition)
{
string metaPath = definition.FXLayerPath + ".meta";
string originalGuid = null;
if (System.IO.File.Exists(metaPath))
{
foreach (var line in System.IO.File.ReadAllLines(metaPath))
{
if (line.StartsWith("guid: "))
{
originalGuid = line.Substring(6).Trim();
break;
}
}
}
if (AssetDatabase.LoadAssetAtPath<AnimatorController>(definition.FXLayerPath) != null)
{
AssetDatabase.DeleteAsset(definition.FXLayerPath);
AssetDatabase.Refresh();
}
var newController = AnimatorController.CreateAnimatorControllerAtPath(definition.FXLayerPath);
if (originalGuid != null && System.IO.File.Exists(metaPath))
{
var lines = System.IO.File.ReadAllLines(metaPath);
for (int i = 0; i < lines.Length; ++i)
{
if (lines[i].StartsWith("guid: "))
{
lines[i] = "guid: " + originalGuid;
}
}
System.IO.File.WriteAllLines(metaPath, lines);
AssetDatabase.Refresh();
}
return AssetDatabase.LoadAssetAtPath<AnimatorController>(definition.FXLayerPath);
}
public static void RunForCurrentAvatar(AvatarDefinition definition)
{
var allLayerGroups = LayerGroup.Instances.ToList();
var layerGroups = allLayerGroups
.Where(lg => lg.TargetDefinitions.Contains(definition))
.ToList();
// Start overall timer
var overallStopwatch = Stopwatch.StartNew();
AvatarLogger.LogInfo("Starting SoldAvatarBootstrap generation for definition: " + definition.DisplayName);
GameObject root = null;
foreach (var candidate in GameObject.FindObjectsOfType<VRCAvatarDescriptor>(true))
{
if (definition.IsApplicable(candidate.gameObject))
{
AvatarLogger.LogInfo($"Found suitable avatar: {candidate.gameObject.name}");
root = candidate.gameObject;
break;
}
}
if (root == null)
{
AvatarLogger.LogError($"No suitable avatar found for definition: {definition.DisplayName}");
return;
}
// Skip if the root GameObject or the component itself is disabled
if (!root.activeSelf || !root.gameObject.activeInHierarchy)
{
AvatarLogger.LogWarning($"Avatar root '{root.name}' is disabled or inactive. Skipping.");
return;
}
AvatarAssets assets = new();
assets.root = root;
assets.isPC = EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows
|| EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows64;
// Time AAC initialization
var initStopwatch = Stopwatch.StartNew();
AvatarLogger.LogInfo("Initializing Animator As Code...");
assets.aac = AacV1.Create(new AacConfiguration
{
SystemName = definition.SystemName,
AnimatorRoot = root.transform,
DefaultValueRoot = root.transform,
AssetKey = GUID.Generate().ToString(),
AssetContainer = GetController(definition),
ContainerMode = AacConfiguration.Container.Everything,
DefaultsProvider = new AacDefaultsProvider(true)
});
assets.emptyClip = assets.aac.NewClip();
assets.fx = assets.aac.NewAnimatorController();
initStopwatch.Stop();
AvatarLogger.LogInfo($"AAC initialization completed in {initStopwatch.ElapsedMilliseconds}ms");
// Process layer groups with individual timing
// Filter out the template LayerGroup (by SystemName)
// AACLogger.LogInfo($"Processing {layerGroups.Count} layer groups...");
AvatarLogger.LogInfo($"Processing {layerGroups.Count} layer groups...");
var totalLayerGroupTime = 0L;
var processedCount = 0;
var skippedCount = 0;
foreach (var layerGroup in layerGroups)
{
var layerStopwatch = Stopwatch.StartNew();
bool skipped = false;
if (!layerGroup.enabled)
{
AvatarLogger.LogWarning($"Skipping layer group: {layerGroup.DisplayName} (disabled)");
skipped = true;
}
if (!skipped)
{
AvatarLogger.LogInfo($"Running layer group: {layerGroup.DisplayName}");
layerGroup.Run(assets);
processedCount++;
}
else
{
skippedCount++;
}
layerStopwatch.Stop();
totalLayerGroupTime += layerStopwatch.ElapsedMilliseconds;
AvatarLogger.LogInfo($"Layer group '{layerGroup.DisplayName}' completed in {layerStopwatch.ElapsedMilliseconds}ms");
}
// Final timing summary
overallStopwatch.Stop();
AvatarLogger.LogSuccess($"SoldAvatarBootstrap generation completed successfully!");
AvatarLogger.LogInfo($"=== TIMING SUMMARY ===");
AvatarLogger.LogInfo($"Total time: {overallStopwatch.ElapsedMilliseconds}ms ({overallStopwatch.Elapsed.TotalSeconds:F2}s)");
AvatarLogger.LogInfo($"AAC initialization: {initStopwatch.ElapsedMilliseconds}ms");
AvatarLogger.LogInfo($"Layer groups: {totalLayerGroupTime}ms (processed: {processedCount}, skipped: {skippedCount})");
AvatarLogger.LogInfo($"Average per layer group: {(processedCount > 0 ? totalLayerGroupTime / processedCount : 0)}ms");
}
}
}