diff --git a/.docs/content/docs/developers/_index.ja.md b/.docs/content/docs/developers/_index.ja.md new file mode 100644 index 000000000..403557dc4 --- /dev/null +++ b/.docs/content/docs/developers/_index.ja.md @@ -0,0 +1,5 @@ +--- +weight: 3 +bookFlatSection: true +title: "開発者向け" +--- \ No newline at end of file diff --git a/.docs/content/docs/developers/_index.md b/.docs/content/docs/developers/_index.md new file mode 100644 index 000000000..f72abeafc --- /dev/null +++ b/.docs/content/docs/developers/_index.md @@ -0,0 +1,5 @@ +--- +weight: 3 +bookFlatSection: true +title: "For Developers" +--- \ No newline at end of file diff --git a/.docs/content/docs/developers/make-your-tool-compatible-with-aao/index.ja.md b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/index.ja.md new file mode 100644 index 000000000..3df8fbfb3 --- /dev/null +++ b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/index.ja.md @@ -0,0 +1,121 @@ +--- +title: ツールをAvatar Optimizerとの互換性をもたせる +--- + +# ツールをAvatar Optimizerとの互換性をもたせる + +このページでは以下の2つのことを説明します。 + +- どのようなときにツールがAvatar Optimizerと非互換になるか +- どのように互換性を改善するか + +もし質問があれば[fediverseで`@anatawa12@misskey.niri.la`][fediverse]に気軽に聞いてください。 + +## どのようなときにツールがAvatar Optimizerと非互換になるか {#when-incompatible} + +もしあなたのツールがコンポーネントを追加せず、ビルド時に何もしないのであれば、すでにAvatarOptimizerと互換性があります! + +もしあなたのツールがコンポーネントをアバターに追加する場合、そのツールはAvatar Optimizerと非互換である可能性があります + +Avatar Optimizerはコンポーネントに対するガベージコレクションを実装しているため、Avatar Optimizerはアバターに存在する +すべてのコンポーネントについて知る必要があります。 + +未知なコンポーネントの問題を避けるため、Avatar Optimizerは道のコンポーネントが以下のようなものであると仮定します。(この仮定は将来的に変更される可能性があります。) +- 副作用があるコンポーネントである +- コンポーネントによって参照されてるコンポーネントに依存している + +しかし、この仮定は正しくない可能性があるので、Avatar Optimizerは未知のコンポーネントを見つけた場合、以下のような警告を生成します。 + +![unknown-component-warning](unknown-component-warning.png) + +もしあなたのツールがNDMF[^NDMF]をベースにしていない非破壊ツールで、Play modeに入るときに適用されるツールの場合、 Avatar +Optimizerがそのツールより前に適用される可能性があるため、非互換です。 + +## どのように互換性を改善するか {#improve-compatibility} + +### NDMFを使用した非破壊ツールの場合 {#improve-compatibility-ndmf-based} + +もしあなたのツールがNDMF[^NDMF]を使用した非破壊ツールの場合、Avatar Optimizerが実行される前にそのツールのコンポーネントを削除してください。 + +Avatar OptimizerはOptimization passに処理を実行するため、もしあなたのツールがOptimization passに何もしないのであれば、特に問題がありません。 +もしあなたのツールがOptimization passになにか実行する場合、[`BeforePlugin`][ndmf-BeforePlugin]を用いてAvatar Optimizerの前に実行するようにしてください。 +Avatar OptimizerのNDMFのQualifiedNameは`com.anatawa12.avatar-optimizer`です。 + +もしあなたのツールがAvatar Optimizerよりあとにあなたのコンポーネントを用いて実行する必要がある場合、 Avatar Optimizerに[コンポーネントを登録][register-component]してください. + +### NDMFを使用していない非破壊ツールの場合 {#improve-compatibility-non-ndmf-based} + +もしあなたのツールがNDMF[^NDMF]を使用していない非破壊ツールの場合、NDMFを使用するのを検討してください。 + +もしあなたのツールがPlayモードに入るときに適用される場合、Avatar Optimizerとの処理順を保証するためにはNDMFを使用する必要があります。 +もしあなたのツールがPlayモードに入るときに適用されない場合、NDMFを使用しなくても問題ないことは多いです。 + +もしあなたのツールでNDMFを使用したくない場合、Avatar Optimizerが実行される前にそのツールのコンポーネントを削除してください。 +これを達成するためにはNDMFのOptimization passの実行より前にあなたのツールを実行するようにしてください。 +現在、NDMFはOptimization passをVRCSDKの`RemoveAvatarEditorOnly`の直前のorder `-1025`で実行するので、 +あなたのツールの`IVRCSDKPreprocessAvatarCallback`をそれより小さい`callbackOrder`で登録してください。 + +もしあなたのツールがAvatarOptimizerの実行後(NDMFのOptimization passのあと)に処理したい場合、Avatar Optimizerに[コンポーネントを登録][register-component]してください. + +### データを保持するだけのコンポーネントを持つツールの場合 {#non-destructive-tools} + +もしあなたのツールのコンポーネントがビルド時に意味を持たず、情報保持のための場合、 +AvatarOptimizerの処理の前に`IVRCSDKPreprocessAvatarCallback`でコンポーネントを削除するか、Avatar Optimizerにコンポーネントを登録してください。 + +もし`IVRCSDKPreprocessAvatarCallback`でコンポーネントを削除する場合、[この部分](#improve-compatibility-non-ndmf-based)を参照してください。 + +もしAvatar Optimizerにコンポーネントを登録する場合、[この部分][register-component]を参照してください。 + +## コンポーネントを登録する {#register-component} + +もしあなたのツールのコンポーネントをAvatar Optimizerの処理後まで保持したり、またはAvatar Optimizerによって削除されたい場合、 +Avatar Optimizerにコンポーネントの情報を登録できます。 + +Avatar OptimizerのAPIを呼び出すため、まず初めにassembly definition file[^asmdef]を存在しない場合作成してください。 + +次に、asmdefファイルのアセンブリ参照に`com.anatawa12.avatar-optimizer.api.editor`を追加してください。 +もしあなたのツールがAvatar Optimizerに依存したくない場合、[Version Defines]を使用してください。 +Avatar Optimizer 1.6.0より前では公開APIがなく、またAvatar Optimizer v2.0.0でAPIを破壊する可能性があるため、 +バージョンの範囲を`[1.6,2.0)`(やもっと厳しい `[1.7,2.0)`など)のように指定するのを推奨します。 + +![version-defines.png](version-defines.png) + +次に、あなたのコンポーネントに関する`CompoinentInformation`を定義してください。 + +```csharp +#if AVATAR_OPTIMIZER && UNITY_EDITOR + +[ComponentInformation(typeof(YourComponent))] +internal class YourComponentInformation : ComponentInformation +{ + protected override void CollectMutations(YourComponent component, ComponentMutationsCollector collector) + { + // call methods on the collector to tell about the component + } + + protected override void CollectDependency(YourComponent component, ComponentDependencyCollector collector) + { + // call methods on the collector to tell about the component + } +} + +#endif +``` + +`CollectDependency`ではビルド時、または実行時のあなたのコンポーネントの依存関係を登録してください。 +`CollectMutations`ではあなたのコンポーネントが実行時に変更する可能性があるプロパティを登録してください。 +詳しくはxmldocやメソッド名を参照してください。 + +もしあなたのコンポーネントがエディタ上のツールのためのデータを保持するだけの場合、どちらも空にします。 + +[fediverse]: https://misskey.niri.la/@anatawa12 +[ndmf-BeforePlugin]: https://ndmf.nadena.dev/api/nadena.dev.ndmf.fluent.Sequence.html#nadena_dev_ndmf_fluent_Sequence_BeforePlugin_System_String_System_String_System_Int32_ +[register-component]: #register-component + +[^asmdef]: Assembly-CSharp以外のアセンブリを定義するためのファイル。[unity docs](https://docs.unity3d.com/2019.4/Documentation/Manual/ScriptCompilationAssemblyDefinitionFiles.html)を参照してください。. +[^NDMF]: [NDMF] (Non-Destructive Modular Framework)は非破壊改変ツールのためのbdunderscoreさんによるフレームワークです。 +Avatar Optimizerはこのフレームワークを他の非破壊改変ツールとの互換性のために使用しています。 + +[NDMF]: https://ndmf.nadena.dev/ +[modular-avatar]: https://modular-avatar.nadena.dev/ +[Version Defines]: https://docs.unity3d.com/2019.4/Documentation/Manual/ScriptCompilationAssemblyDefinitionFiles.html#define-symbols diff --git a/.docs/content/docs/developers/make-your-tool-compatible-with-aao/index.md b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/index.md new file mode 100644 index 000000000..cf41f329c --- /dev/null +++ b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/index.md @@ -0,0 +1,130 @@ +--- +title: Make your tool compatible with Avatar Optimizer +--- + +# Make your tool compatible with Avatar Optimizer + +This page describes the following two things. + +- When your tool can be incompatible with Avatar Optimizer? +- How to improve the compatibility? + +If you have some question, please feel free to ask [`@anatawa12@misskey.niri.la` on fediverse][fediverse]. + +## When your tool can be incompatible with Avatar Optimizer? {#when-incompatible} + +If your tool doesn't add any components to the avatar and does nothing on the build time, +your tool is already compatible with Avatar Optimizer! + +If your tool adds some components to some portion of Avatar, your tool can be incompatible with Avatar Optimizer. + +Since Avatar Optimizer has Garbage Collection system for Components and else, Avatar Optimizer have to +know about all existing components in your Avatar at the optimization. + +To avoid problem with unknown components, the Avatar Optimizer currently assumes unknown components +- have some side-effects. +- will have dependency relationship to all components referenced in the component. + (They can be changed in the future.) + +However, the assumption can be incorrect so Avatar Optimizer will generate the following warning. + +![unknown-component-warning](unknown-component-warning.png) + +If your tool is non-NDMF[^NDMF]-based non-destructive tools that will also be applied on entering play mode, +Avatar Optimizer might be proceed before applying your plugin. + +## How to improve the compatibility? {#improve-compatibility} + +### For NDMF based non-destructive tools {#improve-compatibility-ndmf-based} + +If your tool is a non-destructive tools based on NDMF[^NDMF], please remove your components before +Avatar Optimizer process. Avatar Optimizer does most thing in Optimization pass +so if your plugin do nothing in Optimization pass, nothing is problem. +If your tool needs your components in Optimization pass, +please execute before Avatar Optimizer with [`BeforePlugin`][ndmf-BeforePlugin]. +QualifiedName of Avatar Optimizer in NDMF is `com.anatawa12.avatar-optimizer`. + +If your tool actually want to do something with your components in Optimization pass, +please [register your component][register-component] to Avatar Optimizer. + +### For non-NDMF based non-destructive tools {#improve-compatibility-non-ndmf-based} + +If your tool is a non-destructive tools not based on NDMF[^NDMF], please consider +make your tool based on NDMF. + +If your tool is applied on play, to ensure compatibility with Avatar Optimizer, you have to use NDMF to +guarantee applying ordering between Avatar Optimizer and your tool. +If your tool does something only on building avatar, making your tool based on NDMF is not required. + +If you don't want to make your took based on NDMF, please remove your component before processing Avatar Optimizer. +To achieve this, please execute your tool before NDMF's Optimization pass. +Currently NDMF executes Optimization passes in order `-1025`, JUST before VRCSDK's `RemoveAvatarEditorOnly` callback so +your tool should register `IVRCSDKPreprocessAvatarCallback` with smaller `callbackOrder`. + +If your tool actually want to do something with your components after Avatar Optimizer (Optimization pass in NDMF), +please [register your component][register-component] to Avatar Optimizer. + +### For other tools that just holds data with components. {#non-destructive-tools} + +If your tool holds some information with components and doesn't have meaning at the build time, +please remove your component before Avatar Optimizer with `IVRCSDKPreprocessAvatarCallback` or +register your component to Avatar Optimizer. + +When you want to remove your component with `IVRCSDKPreprocessAvatarCallback`, please refer [this section](#improve-compatibility-non-ndmf-based). + +When you want to register your component to Avatar Optimizer, please refer [this section][register-component]. + +### Registering your components {#register-component} + +If your tool want to keep your component after processing Avatar Optimizer, or want to removed by Avatar Optimizer, +you can register your component to Avatar Optimizer to tell about your component. + +To call APIs in Avatar Optimizer, first, Please make assembly definition file[^asmdef] if your tool doesn't have. + +Next, add `com.anatawa12.avatar-optimizer.api.editor` to assembly references in asmdef file. +If your tool doesn't want to depends on Avatar Optimizer, please use [Version Defines]. +Because Avatar Optimizer didn't have public API piror to 1.6.0 and will break api in 2.0.0, +it's recommended to add version range like `[1.6,2.0)` (or more stricter like `[1.7,2.0)`). + +![version-defines.png](version-defines.png) + +Then, define `CompoinentInformation` for your component in your assembly. + +```csharp +#if AVATAR_OPTIMIZER && UNITY_EDITOR + +[ComponentInformation(typeof(YourComponent))] +internal class YourComponentInformation : ComponentInformation +{ + protected override void CollectMutations(YourComponent component, ComponentMutationsCollector collector) + { + // call methods on the collector to tell about the component + } + + protected override void CollectDependency(YourComponent component, ComponentDependencyCollector collector) + { + // call methods on the collector to tell about the component + } +} + +#endif +``` + +In `CollectDependency`, you should register build-time or run-time dependencies of your component. +In `CollectMutations`, you should register any mutation your component may do. +Please refer xmldoc and method name for more datails. + +If your component is just for keeping data for your in-editor tools, both will be empty method. + +[fediverse]: https://misskey.niri.la/@anatawa12 +[ndmf-BeforePlugin]: https://ndmf.nadena.dev/api/nadena.dev.ndmf.fluent.Sequence.html#nadena_dev_ndmf_fluent_Sequence_BeforePlugin_System_String_System_String_System_Int32_ +[register-component]: #register-component + +[^asmdef]: The file defines assembly other than Assembly-CSharp. Please refer [unity docs](https://docs.unity3d.com/2019.4/Documentation/Manual/ScriptCompilationAssemblyDefinitionFiles.html). +[^NDMF]: [NDMF], Non-Destructive Modular Framework, is a framework for running non-destructive build plugins when +building avatars by bdunderscore. Avatar Optimizer uses that framework for compatibility +with many non-destructive tools based on NDMF. + +[NDMF]: https://ndmf.nadena.dev/ +[modular-avatar]: https://modular-avatar.nadena.dev/ +[Version Defines]: https://docs.unity3d.com/2019.4/Documentation/Manual/ScriptCompilationAssemblyDefinitionFiles.html#define-symbols diff --git a/.docs/content/docs/developers/make-your-tool-compatible-with-aao/unknown-component-warning.png b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/unknown-component-warning.png new file mode 100644 index 000000000..095e66596 Binary files /dev/null and b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/unknown-component-warning.png differ diff --git a/.docs/content/docs/developers/make-your-tool-compatible-with-aao/version-defines.png b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/version-defines.png new file mode 100644 index 000000000..4ec5f136a Binary files /dev/null and b/.docs/content/docs/developers/make-your-tool-compatible-with-aao/version-defines.png differ diff --git a/API-Editor.meta b/API-Editor.meta new file mode 100644 index 000000000..367610f0d --- /dev/null +++ b/API-Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 97e9bf2846434ee58fcd391b0391f746 +timeCreated: 1697680182 \ No newline at end of file diff --git a/API-Editor/AssemblyInfo.cs b/API-Editor/AssemblyInfo.cs new file mode 100644 index 000000000..9bbe4ea76 --- /dev/null +++ b/API-Editor/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("com.anatawa12.avatar-optimizer.editor")] diff --git a/API-Editor/AssemblyInfo.cs.meta b/API-Editor/AssemblyInfo.cs.meta new file mode 100644 index 000000000..42f07878d --- /dev/null +++ b/API-Editor/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a1d01a558e4741d4b4c830445d4641c8 +timeCreated: 1697680408 \ No newline at end of file diff --git a/API-Editor/ComponentInformation.cs b/API-Editor/ComponentInformation.cs new file mode 100644 index 000000000..b801a1a8e --- /dev/null +++ b/API-Editor/ComponentInformation.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.API +{ + /// + /// This attribute declares the class provides information about the type. + /// Your class MUST derive and MUST have constructor without parameters. + /// The must be assignable to TComponent of + /// + /// Your class MAY have one type parameter. + /// When your class have type parameter, the type parameter MUST be passed to + /// and must be assignable from . + /// + /// It's REQUIRED to be exists only one ComponentInformation for one type. + /// So, you SHOULD NOT declare ComponentInformation for external components. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + [MeansImplicitUse] + [BaseTypeRequired(typeof(ComponentInformation<>))] + public sealed class ComponentInformationAttribute : APIInternal.ComponentInformationAttributeBase + { + public Type TargetType { get; } + + [PublicAPI] + public ComponentInformationAttribute(Type targetType) + { + TargetType = targetType; + } + + internal override Type GetTargetType() => TargetType; + } + + [PublicAPI] + public abstract class ComponentInformation : + APIInternal.ComponentInformation, + APIInternal.IComponentInformation + where TComponent : Component + { + internal sealed override void CollectDependencyInternal(Component component, + ComponentDependencyCollector collector) => + CollectDependency((TComponent)component, collector); + + internal sealed override void CollectMutationsInternal(Component component, + ComponentMutationsCollector collector) => + CollectMutations((TComponent)component, collector); + + /// + /// Collects runtime mutations by . + /// You have to call . + /// for all property mutations by the component. + /// + /// The component. + /// The callback. + [PublicAPI] + protected abstract void CollectDependency(TComponent component, ComponentDependencyCollector collector); + + /// + /// Collects runtime mutations by . + /// You have to call . + /// for all property mutations by the component. + /// + /// The component. + /// The callback. + [PublicAPI] + protected virtual void CollectMutations(TComponent component, ComponentMutationsCollector collector) + { + } + + // Important note for future AAO developer + // 1. You MUST NOT add abstract method onto this class + // 2. When you added some virtual methods onto this class, implementer MAY NOT implement that method + // so we have to add some way to detect if implemented AND must fallback to default implementation. + // One way I can implement is add some internal method to the collector, call it in default implementation, + // and do fallback process in that method. + } + + public abstract class ComponentDependencyCollector + { + internal ComponentDependencyCollector() + { + } + + /// + /// Marks this component as EntryPoint component, which means has some effects to non-avatar components. + /// For example, Renderer components have side-effects because it renders something. + /// VRC Contacts have some effects to other avatars in the instance so they have side-effects. + /// + /// If your component has some effects only for some specific component(s), you should not mark your component + /// as EntryPoint. You should make dependency relationship from affecting component to your component instead. + /// + /// One of such components is VRCPhysBone without animating parameters. + /// VRCPhysBone has effects if and only if some other EntryPoint components are attached to PB-affected + /// transforms or their children, or PB-affected transforms are dependencies of some EntryPoint components. + /// So, for VRCPhysBone, There are bidirectional dependency relationship between PB and PB-affected transforms + /// and VRCPhysBone is not a EntryPoint component. + /// + /// With same reasons, Constraints are not treated as EntryPoint, they have bidirectional + /// dependency relationship between Constraintas and transform instead. + /// + [PublicAPI] + public abstract void MarkEntrypoint(); + + /// + /// Marks this component as Behaviour component, which means has some effects to components in the avatars. + /// When you mark some components Behaviour component, Avatar Optimizer will generate animation that disables + /// the component when entrypoint is not active / enabled. + /// + /// If your component is not marked as Behaviour, enable-ness / activeness will not changed by Avatar Optimizer. + /// If your component have some runtime load and can be skipped if your component is not needed by all + /// EntryPoint components, you should mark your component as Behaviour for runtime-load optimization. + /// + /// For example, VRCPhysBone and Constraints are marked as Behaviour. + /// + [PublicAPI] + public abstract void MarkBehaviour(); + + /// + /// Adds as dependencies of . + /// The dependency will be assumed as the dependant will have dependency if dependant is enabled and + /// even if dependency is disabled. You can change the settings by . + /// + /// WARNING: the instance can be shared between multiple AddDependency + /// invocation so you MUST NOT call methods on IComponentDependencyInfo after calling AddDependency other time. + /// + /// The dependant + /// The dependency + /// The object to configure the dependency + [PublicAPI] + public abstract ComponentDependencyInfo AddDependency(Component dependant, Component dependency); + + /// + /// Adds as dependencies of current component. + /// Same as calling + /// with current component but this might be optimized more. + /// + /// + /// The dependency + /// The object to configure the dependency + [PublicAPI] + public abstract ComponentDependencyInfo AddDependency(Component dependency); + } + + public abstract class ComponentDependencyInfo + { + internal ComponentDependencyInfo() + { + } + + /// + /// Indicates the dependency is required even if dependant component is disabled. + /// + /// this object for method chain + [PublicAPI] + public abstract ComponentDependencyInfo EvenIfDependantDisabled(); + + /// + /// Indicates the dependency is not required if dependency component is disabled. + /// + /// this object for method chain + [PublicAPI] + public abstract ComponentDependencyInfo OnlyIfTargetCanBeEnable(); + } + + /// + /// + /// protected override void CollectMutations(AimConstraint component, IComponentMutationsCollector collector) + /// { + /// collector.ModifyProperties(component.transform, new [] { "m_LocalRotation.x", "m_LocalRotation.y", "m_LocalRotation.z", "m_LocalRotation.w" }); + /// } + /// + /// + public abstract class ComponentMutationsCollector + { + internal ComponentMutationsCollector() + { + } + + [PublicAPI] + public abstract void ModifyProperties([NotNull] Component component, [NotNull] IEnumerable properties); + } +} diff --git a/API-Editor/ComponentInformation.cs.meta b/API-Editor/ComponentInformation.cs.meta new file mode 100644 index 000000000..81be90af3 --- /dev/null +++ b/API-Editor/ComponentInformation.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6c93fc7c413e4e39acb3e9f367005387 +timeCreated: 1696042138 \ No newline at end of file diff --git a/API-Editor/Internal.meta b/API-Editor/Internal.meta new file mode 100644 index 000000000..55ad4c2ca --- /dev/null +++ b/API-Editor/Internal.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 488c3944c5114816a8b0a47724ada120 +timeCreated: 1697680380 \ No newline at end of file diff --git a/API-Editor/Internal/InternalParts.cs b/API-Editor/Internal/InternalParts.cs new file mode 100644 index 000000000..8037d1505 --- /dev/null +++ b/API-Editor/Internal/InternalParts.cs @@ -0,0 +1,31 @@ +using System; +using Anatawa12.AvatarOptimizer.API; +using JetBrains.Annotations; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.APIInternal +{ + public abstract class ComponentInformationAttributeBase : Attribute + { + internal ComponentInformationAttributeBase() { } + [CanBeNull] internal abstract Type GetTargetType(); + } + + /// + /// Marker Interface for type check + /// + /// + internal interface IComponentInformation + { + } + + public abstract class ComponentInformation + { + internal ComponentInformation() + { + } + + internal abstract void CollectDependencyInternal(Component component, ComponentDependencyCollector collector); + internal abstract void CollectMutationsInternal(Component component, ComponentMutationsCollector collector); + } +} \ No newline at end of file diff --git a/API-Editor/Internal/InternalParts.cs.meta b/API-Editor/Internal/InternalParts.cs.meta new file mode 100644 index 000000000..205d346c7 --- /dev/null +++ b/API-Editor/Internal/InternalParts.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1a8905e6f3c44842833a34f78d8ad808 +timeCreated: 1696163257 \ No newline at end of file diff --git a/API-Editor/com.anatawa12.avatar-optimizer.api.editor.asmdef b/API-Editor/com.anatawa12.avatar-optimizer.api.editor.asmdef new file mode 100644 index 000000000..e4829a5ca --- /dev/null +++ b/API-Editor/com.anatawa12.avatar-optimizer.api.editor.asmdef @@ -0,0 +1,15 @@ +{ + "name": "com.anatawa12.avatar-optimizer.api.editor", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/API-Editor/com.anatawa12.avatar-optimizer.api.editor.asmdef.meta b/API-Editor/com.anatawa12.avatar-optimizer.api.editor.asmdef.meta new file mode 100644 index 000000000..1962c5319 --- /dev/null +++ b/API-Editor/com.anatawa12.avatar-optimizer.api.editor.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ac4815c6727e4e34b9e13bb042cbc029 +timeCreated: 1697680215 \ No newline at end of file diff --git a/CHANGELOG-PRERELEASE.md b/CHANGELOG-PRERELEASE.md index 51ba66336..908d13706 100644 --- a/CHANGELOG-PRERELEASE.md +++ b/CHANGELOG-PRERELEASE.md @@ -8,14 +8,17 @@ The format is based on [Keep a Changelog]. ## [Unreleased] ### Added +- Small performance improve `#641` ### Changed +- All logs passed to ErrorReport is now shown on the console log `#643` ### Deprecated ### Removed ### Fixed +- Prefab blinks when we see editor of PrefabSafeSet of prefab asset `#645` ### Security @@ -24,6 +27,20 @@ The format is based on [Keep a Changelog]. ### Fixed - Animation clip length can be changed [`#647`](https://github.com/anatawa12/AvatarOptimizer/pull/647) +## [1.6.0-beta.1] - 2023-10-25 +### Added +- Public API for registering component information [`#623`](https://github.com/anatawa12/AvatarOptimizer/pull/623) +- Documentation for developers about compatibility with Avatar Optimizer [`#623`](https://github.com/anatawa12/AvatarOptimizer/pull/623) +- Disabling PhysBone animation based on mesh renderer enabled animation [`#640`](https://github.com/anatawa12/AvatarOptimizer/pull/640) + - If you toggles your clothes with simple toggle, PhysBones on the your avatar will also be toggled automatically! + +### Removed +- Legacy GC [`#633`](https://github.com/anatawa12/AvatarOptimizer/pull/633) + +### Fixed +- Improve support of newer Unity versions [`#608`](https://github.com/anatawa12/AvatarOptimizer/pull/608) +- Improve support of projects without VRCSDK [`#609`](https://github.com/anatawa12/AvatarOptimizer/pull/609) [`#625`](https://github.com/anatawa12/AvatarOptimizer/pull/625) [`#627`](https://github.com/anatawa12/AvatarOptimizer/pull/627) + ## [1.5.8] - 2023-10-20 ## [1.5.8-rc.1] - 2023-10-20 ### Fixed @@ -83,7 +100,7 @@ The format is based on [Keep a Changelog]. - Error with MeshRenderer without MeshFilter [`#581`](https://github.com/anatawa12/AvatarOptimizer/pull/581) - Preview not working with VRMConverter [`#582`](https://github.com/anatawa12/AvatarOptimizer/pull/582) - AvatarMask about HumanoidBone broken [`#586`](https://github.com/anatawa12/AvatarOptimizer/pull/586) -- Unused Homanoid Bones can be removed [`#587`](https://github.com/anatawa12/AvatarOptimizer/pull/587) +- Unused Humanoid Bones can be removed [`#587`](https://github.com/anatawa12/AvatarOptimizer/pull/587) ## [1.5.3] - 2023-10-11 ### Changed @@ -939,7 +956,8 @@ This release is mistake. - Merge Bone - Clear Endpoint Position -[Unreleased]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.9...HEAD +[Unreleased]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.6.0-beta.1...HEAD +[1.6.0-beta.1]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.9...v1.6.0-beta.1 [1.5.9]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.9-rc.1...v1.5.9 [1.5.9-rc.1]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.8...v1.5.9-rc.1 [1.5.8]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.8-rc.1...v1.5.8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba0fd0db..2a9135977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,23 @@ The format is based on [Keep a Changelog]. ## [Unreleased] ### Added +- Public API for registering component information `#632` +- Disabling PhysBone animation based on mesh renderer enabled animation `#640` + - If you toggles your clothes with simple toggle, PhysBones on the your avatar will also be toggled automatically! +- Small performance improve `#641` ### Changed +- All logs passed to ErrorReport is now shown on the console log `#643` ### Deprecated ### Removed +- Legacy GC `#633` ### Fixed +- Improve support of newer Unity versions `#608` +- Improve support of projects without VRCSDK `#609` `#625` `#627` +- Prefab blinks when we see editor of PrefabSafeSet of prefab asset `#645` ### Security @@ -68,7 +77,7 @@ The format is based on [Keep a Changelog]. - Error with MeshRenderer without MeshFilter [`#581`](https://github.com/anatawa12/AvatarOptimizer/pull/581) - Preview not working with VRMConverter [`#582`](https://github.com/anatawa12/AvatarOptimizer/pull/582) - AvatarMask about HumanoidBone broken [`#586`](https://github.com/anatawa12/AvatarOptimizer/pull/586) -- Unused Homanoid Bones can be removed [`#587`](https://github.com/anatawa12/AvatarOptimizer/pull/587) +- Unused Humanoid Bones can be removed [`#587`](https://github.com/anatawa12/AvatarOptimizer/pull/587) ## [1.5.3] - 2023-10-11 ### Changed diff --git a/Editor/.MergePhysBoneEditorModificationUtils.ts b/Editor/.MergePhysBoneEditorModificationUtils.ts index 6bff24a28..295b67a33 100644 --- a/Editor/.MergePhysBoneEditorModificationUtils.ts +++ b/Editor/.MergePhysBoneEditorModificationUtils.ts @@ -44,6 +44,8 @@ const config: Config = { console.log("// ") console.log("// generated by .MergePhysBoneEditorModificationUtils.ts") +console.log("#if AAO_VRCSDK3_AVATARS") +console.log("") console.log("using System.Collections.Generic;") console.log("using UnityEditor;") console.log("using JetBrains.Annotations;") @@ -103,3 +105,5 @@ for (let [type, info] of Object.entries(config)) { } console.log(" }") console.log("}") +console.log("") +console.log("#endif") diff --git a/Editor/APIInternal.meta b/Editor/APIInternal.meta new file mode 100644 index 000000000..50ed89a12 --- /dev/null +++ b/Editor/APIInternal.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2a7ed540e42541938219697df3892105 +timeCreated: 1696052255 \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInfoRegistry.cs b/Editor/APIInternal/ComponentInfoRegistry.cs new file mode 100644 index 000000000..7ce572638 --- /dev/null +++ b/Editor/APIInternal/ComponentInfoRegistry.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.APIInternal +{ + internal static class ComponentInfoRegistry + { + private static readonly Dictionary InformationByType = + new Dictionary(); + + [InitializeOnLoadMethod] + static void FindAllInfoImplements() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + foreach (var type in assembly.GetTypes()) + foreach (ComponentInformationAttributeBase attribute in type.GetCustomAttributes( + typeof(ComponentInformationAttributeBase), false)) + { + try + { + LoadType(type, attribute); + } + catch (Exception e) + { + try + { + Debug.LogError($"Processing type {type}"); + Debug.LogException(e); + } + catch (Exception e1) + { + Debug.LogException(new AggregateException(e, e1)); + } + } + } + } + + [InitializeOnLoadMethod] + static void NDMFComponents() + { + var contextHolder = typeof(nadena.dev.ndmf.BuildContext).Assembly + .GetType("nadena.dev.ndmf.VRChat.ContextHolder"); + // nadena.dev.ndmf.VRChat.ContextHolder is internal so I use reflection + if (contextHolder != null) + { + InformationByType.Add(contextHolder, new EntrypointComponentInformation()); + } + } + + private static void LoadType(Type type, ComponentInformationAttributeBase attribute) + { + var targetType = attribute.GetTargetType(); + if (targetType == null) return; + var informationType = typeof(IComponentInformation<>).MakeGenericType(targetType); + if (type.ContainsGenericParameters) + { + if (type.GetGenericArguments().Length != 1) + throw new Exception("Unsupported type : More than 1 Generic Parameters"); + + type = type.MakeGenericType(targetType); + } + + if (!informationType.IsAssignableFrom(type)) + throw new Exception("Unsupported type : Not Extends from ComponentInformation"); + if (InformationByType.ContainsKey(targetType)) + throw new Exception($"Target Type duplicated: {targetType}"); + + var instance = (ComponentInformation)System.Activator.CreateInstance(type); + InformationByType.Add(targetType, instance); + } + + internal static bool TryGetInformation(Type type, out ComponentInformation information) => + InformationByType.TryGetValue(type, out information); + } +} + diff --git a/Editor/APIInternal/ComponentInfoRegistry.cs.meta b/Editor/APIInternal/ComponentInfoRegistry.cs.meta new file mode 100644 index 000000000..01319f5e3 --- /dev/null +++ b/Editor/APIInternal/ComponentInfoRegistry.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 281d676e6c444e07a23d4b5464a64863 +timeCreated: 1696052435 \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInformationWithGUIDAttribute.cs b/Editor/APIInternal/ComponentInformationWithGUIDAttribute.cs new file mode 100644 index 000000000..b9bfcc8fe --- /dev/null +++ b/Editor/APIInternal/ComponentInformationWithGUIDAttribute.cs @@ -0,0 +1,30 @@ +using System; +using Anatawa12.AvatarOptimizer.API; +using JetBrains.Annotations; +using UnityEditor; + +namespace Anatawa12.AvatarOptimizer.APIInternal +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + [MeansImplicitUse] + [BaseTypeRequired(typeof(ComponentInformation<>))] + internal sealed class ComponentInformationWithGUIDAttribute : ComponentInformationAttributeBase + { + public string Guid { get; } + public int FileID { get; } + + public ComponentInformationWithGUIDAttribute(string guid, int fileID) + { + Guid = guid; + FileID = fileID; + } + + internal override Type GetTargetType() + { + if (!GlobalObjectId.TryParse($"GlobalObjectId_V1-{1}-{Guid}-{(uint)FileID}-{0}", out var id)) return null; + var script = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(id) as MonoScript; + if (!script) return null; + return script.GetClass(); + } + } +} \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInformationWithGUIDAttribute.cs.meta b/Editor/APIInternal/ComponentInformationWithGUIDAttribute.cs.meta new file mode 100644 index 000000000..571453a76 --- /dev/null +++ b/Editor/APIInternal/ComponentInformationWithGUIDAttribute.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 08ea4643452048a7ad78a8c684f0bbbf +timeCreated: 1697285174 \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInfos.cs b/Editor/APIInternal/ComponentInfos.cs new file mode 100644 index 000000000..61bb23cd3 --- /dev/null +++ b/Editor/APIInternal/ComponentInfos.cs @@ -0,0 +1,685 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Anatawa12.AvatarOptimizer.API; +using UnityEngine; +using UnityEngine.Animations; +using UnityEngine.Rendering; + +#if AAO_VRCSDK3_AVATARS +using VRC.SDK3; +using VRC.Core; +using VRC.Dynamics; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Dynamics.Contact.Components; +using VRC.SDK3.Dynamics.PhysBone.Components; +using VRC.SDKBase; +#endif + +namespace Anatawa12.AvatarOptimizer.APIInternal +{ + [ComponentInformation(typeof(Light))] + [ComponentInformation(typeof(Camera))] + [ComponentInformation(typeof(Animation))] + [ComponentInformation(typeof(AudioSource))] +#if AAO_VRCSDK3_AVATARS + [ComponentInformation(typeof(VRCTestMarker))] +#pragma warning disable CS0618 + [ComponentInformation(typeof(PipelineSaver))] +#pragma warning restore CS0618 + [ComponentInformation(typeof(PipelineManager))] + [ComponentInformation(typeof(VRCSpatialAudioSource))] + [ComponentInformation(typeof(VRC_SpatialAudioSource))] +#endif + [ComponentInformation(typeof(nadena.dev.ndmf.runtime.AvatarActivator))] + // nadena.dev.ndmf.VRChat.ContextHolder with reflection + internal class EntrypointComponentInformation : ComponentInformation + { + protected override void CollectDependency(Component component, ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + } + } + + [ComponentInformation(typeof(Transform))] + internal class TransformInformation : ComponentInformation + { + protected override void CollectDependency(Transform component, ComponentDependencyCollector collector) + { + var casted = (Processors.TraceAndOptimizes.ComponentDependencyCollector.Collector)collector; + casted.AddParentDependency(component); + // For compatibility with UnusedBonesByReferenceTool + // https://github.com/anatawa12/AvatarOptimizer/issues/429 + if (casted.PreserveEndBone && + component.name.EndsWith("end", StringComparison.OrdinalIgnoreCase)) + { + collector.AddDependency(component.parent, component).EvenIfDependantDisabled(); + } + } + } + + [ComponentInformation(typeof(Animator))] + internal class AnimatorInformation : ComponentInformation + { + // Animator does not do much for motion, just changes states of other components. + // All State / Motion Changes are collected separately + protected override void CollectDependency(Animator component, ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + + for (var bone = HumanBodyBones.Hips; bone < HumanBodyBones.LastBone; bone++) + { + var boneTransform = component.GetBoneTransform(bone); + foreach (var transform in boneTransform.ParentEnumerable()) + { + if (transform == component.transform) break; + collector.AddDependency(transform); + } + } + } + } + + internal class RendererInformation : ComponentInformation where T : Renderer + { + protected override void CollectDependency(T component, ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + // anchor proves + if (component.reflectionProbeUsage != ReflectionProbeUsage.Off || + component.lightProbeUsage != LightProbeUsage.Off) + collector.AddDependency(component.probeAnchor); + if (component.lightProbeUsage == LightProbeUsage.UseProxyVolume) + collector.AddDependency(component.lightProbeProxyVolumeOverride.transform); + } + } + + [ComponentInformation(typeof(SkinnedMeshRenderer))] + internal class SkinnedMeshRendererInformation : RendererInformation + { + protected override void CollectDependency(SkinnedMeshRenderer component, + ComponentDependencyCollector collector) + { + base.CollectDependency(component, collector); + + var casted = (Processors.TraceAndOptimizes.ComponentDependencyCollector.Collector)collector; + + var meshInfo2 = casted.GetMeshInfoFor(component); + foreach (var bone in meshInfo2.Bones) + casted.AddBoneDependency(bone.Transform); + collector.AddDependency(meshInfo2.RootBone); + } + } + + [ComponentInformation(typeof(MeshRenderer))] + internal class MeshRendererInformation : RendererInformation + { + protected override void CollectDependency(MeshRenderer component, ComponentDependencyCollector collector) + { + base.CollectDependency(component, collector); + collector.AddDependency(component.GetComponent()); + } + } + + [ComponentInformation(typeof(MeshFilter))] + internal class MeshFilterInformation : ComponentInformation + { + protected override void CollectDependency(MeshFilter component, ComponentDependencyCollector collector) + { + } + } + + [ComponentInformation(typeof(ParticleSystem))] + internal class ParticleSystemInformation : ComponentInformation + { + protected override void CollectDependency(ParticleSystem component, ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + + if (component.main.simulationSpace == ParticleSystemSimulationSpace.Custom) + collector.AddDependency(component.main.customSimulationSpace); + if (component.shape.enabled) + { + switch (component.shape.shapeType) + { + case ParticleSystemShapeType.MeshRenderer: + collector.AddDependency(component.shape.meshRenderer); + break; + case ParticleSystemShapeType.SkinnedMeshRenderer: + collector.AddDependency(component.shape.skinnedMeshRenderer); + break; + case ParticleSystemShapeType.SpriteRenderer: + collector.AddDependency(component.shape.spriteRenderer); + break; +#pragma warning disable CS0618 + case ParticleSystemShapeType.Sphere: + case ParticleSystemShapeType.SphereShell: + case ParticleSystemShapeType.Hemisphere: + case ParticleSystemShapeType.HemisphereShell: + case ParticleSystemShapeType.Cone: + case ParticleSystemShapeType.Box: + case ParticleSystemShapeType.Mesh: + case ParticleSystemShapeType.ConeShell: + case ParticleSystemShapeType.ConeVolume: + case ParticleSystemShapeType.ConeVolumeShell: + case ParticleSystemShapeType.Circle: + case ParticleSystemShapeType.CircleEdge: + case ParticleSystemShapeType.SingleSidedEdge: + case ParticleSystemShapeType.BoxShell: + case ParticleSystemShapeType.BoxEdge: + case ParticleSystemShapeType.Donut: + case ParticleSystemShapeType.Rectangle: + case ParticleSystemShapeType.Sprite: + default: +#pragma warning restore CS0618 + break; + } + } + + if (component.collision.enabled) + { + switch (component.collision.type) + { + case ParticleSystemCollisionType.Planes: +#if UNITY_2020_2_OR_NEWER + for (var i = 0; i < component.collision.planeCount; i++) +#else + for (var i = 0; i < component.collision.maxPlaneCount; i++) +#endif + collector.AddDependency(component.collision.GetPlane(i)); + break; + case ParticleSystemCollisionType.World: + default: + break; + } + } + + if (component.trigger.enabled) + { +#if UNITY_2020_2_OR_NEWER + for (var i = 0; i < component.trigger.colliderCount; i++) +#else + for (var i = 0; i < component.trigger.maxColliderCount; i++) +#endif + collector.AddDependency(component.trigger.GetCollider(i)); + } + + if (component.subEmitters.enabled) + { + for (var i = 0; i < component.subEmitters.subEmittersCount; i++) + collector.AddDependency(component.subEmitters.GetSubEmitterSystem(i)); + } + + if (component.lights.enabled) + { + collector.AddDependency(component.lights.light); + } + + collector.AddDependency(component.GetComponent()).EvenIfDependantDisabled(); + } + } + + [ComponentInformation(typeof(ParticleSystemRenderer))] + internal class ParticleSystemRendererInformation : RendererInformation + { + protected override void CollectDependency(ParticleSystemRenderer component, + ComponentDependencyCollector collector) + { + base.CollectDependency(component, collector); + collector.AddDependency(component.GetComponent()).EvenIfDependantDisabled(); + } + } + + [ComponentInformation(typeof(TrailRenderer))] + internal class TrailRendererInformation : RendererInformation + { + } + + [ComponentInformation(typeof(LineRenderer))] + internal class LineRendererInformation : RendererInformation + { + } + + [ComponentInformation(typeof(Cloth))] + internal class ClothInformation : ComponentInformation + { + protected override void CollectDependency(Cloth component, ComponentDependencyCollector collector) + { + // If Cloth is disabled, SMR work as SMR without Cloth + // If Cloth is enabled and SMR is disabled, SMR draw nothing. + var skinnedMesh = component.GetComponent(); + collector.AddDependency(skinnedMesh, component).EvenIfDependantDisabled(); + foreach (var collider in component.capsuleColliders) + collector.AddDependency(collider); + foreach (var collider in component.sphereColliders) + { + collector.AddDependency(collider.first); + collector.AddDependency(collider.second); + } + } + } + + [ComponentInformation(typeof(Collider))] + [ComponentInformation(typeof(TerrainCollider))] + [ComponentInformation(typeof(BoxCollider))] + [ComponentInformation(typeof(SphereCollider))] + [ComponentInformation(typeof(MeshCollider))] + [ComponentInformation(typeof(CapsuleCollider))] + [ComponentInformation(typeof(WheelCollider))] + internal class ColliderInformation : ComponentInformation + { + protected override void CollectDependency(Collider component, ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + var rigidbody = component.GetComponentInParent(); + if (rigidbody) collector.AddDependency(rigidbody, component).OnlyIfTargetCanBeEnable(); + } + } + + [ComponentInformation(typeof(Joint))] + [ComponentInformation(typeof(CharacterJoint))] + [ComponentInformation(typeof(ConfigurableJoint))] + [ComponentInformation(typeof(FixedJoint))] + [ComponentInformation(typeof(HingeJoint))] + [ComponentInformation(typeof(SpringJoint))] + internal class JointInformation : ComponentInformation + { + protected override void CollectDependency(Joint component, ComponentDependencyCollector collector) + { + collector.AddDependency(component.GetComponent(), component); + collector.AddDependency(component.connectedBody); + } + } + + [ComponentInformation(typeof(Rigidbody))] + internal class RigidbodyInformation : ComponentInformation + { + protected override void CollectDependency(Rigidbody component, ComponentDependencyCollector collector) + { + collector.AddDependency(component.transform, component).EvenIfDependantDisabled().OnlyIfTargetCanBeEnable(); + } + + protected override void CollectMutations(Rigidbody component, ComponentMutationsCollector collector) + { + collector.TransformPositionAndRotation(component.transform); + } + } + + [ComponentInformation(typeof(FlareLayer))] + internal class FlareLayerInformation : ComponentInformation + { + protected override void CollectDependency(FlareLayer component, ComponentDependencyCollector collector) + { + collector.AddDependency(component.GetComponent(), component); + } + } + + internal class ConstraintInformation : ComponentInformation where T : Component, IConstraint + { + protected override void CollectDependency(T component, ComponentDependencyCollector collector) + { + collector.AddDependency(component.transform, component) + .OnlyIfTargetCanBeEnable() + .EvenIfDependantDisabled(); + for (var i = 0; i < component.sourceCount; i++) + collector.AddDependency(component.GetSource(i).sourceTransform); + collector.MarkBehaviour(); + } + } + + [ComponentInformation(typeof(AimConstraint))] + internal class AimConstraintInformation : ConstraintInformation + { + protected override void CollectDependency(AimConstraint component, ComponentDependencyCollector collector) + { + base.CollectDependency(component, collector); + collector.AddDependency(component.worldUpObject); + } + + protected override void CollectMutations(AimConstraint component, ComponentMutationsCollector collector) + { + collector.TransformRotation(component.transform); + } + } + + [ComponentInformation(typeof(LookAtConstraint))] + internal class LookAtConstraintInformation : ConstraintInformation + { + protected override void CollectDependency(LookAtConstraint component, ComponentDependencyCollector collector) + { + base.CollectDependency(component, collector); + collector.AddDependency(component.worldUpObject); + } + + protected override void CollectMutations(LookAtConstraint component, ComponentMutationsCollector collector) + { + collector.TransformRotation(component.transform); + } + } + + [ComponentInformation(typeof(ParentConstraint))] + internal class ParentConstraintInformation : ConstraintInformation + { + protected override void CollectMutations(ParentConstraint component, ComponentMutationsCollector collector) + { + collector.TransformPositionAndRotation(component.transform); + } + } + + [ComponentInformation(typeof(RotationConstraint))] + internal class RotationConstraintInformation : ConstraintInformation + { + protected override void CollectMutations(RotationConstraint component, ComponentMutationsCollector collector) + { + collector.TransformRotation(component.transform); + } + } + + [ComponentInformation(typeof(PositionConstraint))] + internal class PositionConstraintInformation : ConstraintInformation + { + protected override void CollectMutations(PositionConstraint component, ComponentMutationsCollector collector) + { + collector.TransformPosition(component.transform); + } + } + + [ComponentInformation(typeof(ScaleConstraint))] + internal class ScaleConstraintInformation : ConstraintInformation + { + protected override void CollectMutations(ScaleConstraint component, ComponentMutationsCollector collector) + { + collector.TransformScale(component.transform); + } + } + +#if AAO_VRCSDK3_AVATARS + [ComponentInformation(typeof(VRC_AvatarDescriptor))] + internal class VRCAvatarDescriptorInformation : ComponentInformation where T : VRC_AvatarDescriptor + { + protected override void CollectDependency(T component, + ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + collector.AddDependency(component.GetComponent()).EvenIfDependantDisabled(); + } + } + + [ComponentInformation(typeof(VRCAvatarDescriptor))] + internal class VRCAvatarDescriptorInformation : VRCAvatarDescriptorInformation + { + protected override void CollectDependency(VRCAvatarDescriptor component, + ComponentDependencyCollector collector) + { + base.CollectDependency(component, collector); + + AddCollider(component.collider_head); + AddCollider(component.collider_torso); + AddCollider(component.collider_footR); + AddCollider(component.collider_footL); + AddCollider(component.collider_handR); + AddCollider(component.collider_handL); + AddCollider(component.collider_fingerIndexL); + AddCollider(component.collider_fingerMiddleL); + AddCollider(component.collider_fingerRingL); + AddCollider(component.collider_fingerLittleL); + AddCollider(component.collider_fingerIndexR); + AddCollider(component.collider_fingerMiddleR); + AddCollider(component.collider_fingerRingR); + AddCollider(component.collider_fingerLittleR); + void AddCollider(VRCAvatarDescriptor.ColliderConfig collider) + { + switch (collider.state) + { + case VRCAvatarDescriptor.ColliderConfig.State.Automatic: + case VRCAvatarDescriptor.ColliderConfig.State.Custom: + collector.AddDependency(collider.transform).EvenIfDependantDisabled(); + break; + case VRCAvatarDescriptor.ColliderConfig.State.Disabled: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + [ComponentInformation(typeof(VRC.SDKBase.VRCStation))] + [ComponentInformation(typeof(VRC.SDK3.Avatars.Components.VRCStation))] + internal class VRCStationInformation : ComponentInformation + { + protected override void CollectDependency(VRC.SDKBase.VRCStation component, ComponentDependencyCollector collector) + { + // first, Transform <=> PhysBone + // Transform is used even if the bone is inactive so Transform => PB is always dependency + // PhysBone works only if enabled so PB => Transform is active dependency + collector.MarkEntrypoint(); + collector.AddDependency(component.stationEnterPlayerLocation); + collector.AddDependency(component.stationExitPlayerLocation); + collector.AddDependency(component.GetComponentInChildren()); + } + + protected override void CollectMutations(VRC.SDKBase.VRCStation component, ComponentMutationsCollector collector) + { + } + } + + [ComponentInformation(typeof(VRCPhysBoneBase))] + [ComponentInformation(typeof(VRCPhysBone))] + internal class VRCPhysBoneInformation : ComponentInformation + { + protected override void CollectDependency(VRCPhysBoneBase component, ComponentDependencyCollector collector) + { + // first, Transform <=> PhysBone + // Transform is used even if the bone is inactive so Transform => PB is always dependency + // PhysBone works only if enabled so PB => Transform is active dependency + var ignoreTransforms = new HashSet(component.ignoreTransforms); + CollectTransforms(component.GetTarget()); + + void CollectTransforms(Transform bone) + { + collector.AddDependency(bone, component).EvenIfDependantDisabled().OnlyIfTargetCanBeEnable(); + collector.AddDependency(bone); + foreach (var child in bone.DirectChildrenEnumerable()) + { + if (!ignoreTransforms.Contains(child)) + CollectTransforms(child); + } + } + + // then, PB => Collider + // in PB, PB Colliders work only if Colliders are enabled + foreach (var physBoneCollider in component.colliders) + collector.AddDependency(physBoneCollider).OnlyIfTargetCanBeEnable(); + + collector.MarkBehaviour(); + + // If parameter is not empty, the PB can be required for Animator Parameter so it's Entrypoint Component + // https://github.com/anatawa12/AvatarOptimizer/issues/450 + if (!string.IsNullOrEmpty(component.parameter)) + collector.MarkEntrypoint(); + } + + protected override void CollectMutations(VRCPhysBoneBase component, ComponentMutationsCollector collector) + { + foreach (var transform in component.GetAffectedTransforms()) + collector.TransformPositionAndRotation(transform); + } + } + + [ComponentInformation(typeof(VRCPhysBoneColliderBase))] + [ComponentInformation(typeof(VRCPhysBoneCollider))] + internal class VRCPhysBoneColliderInformation : ComponentInformation + { + protected override void CollectDependency(VRCPhysBoneColliderBase component, + ComponentDependencyCollector collector) + { + collector.AddDependency(component.rootTransform); + } + } + + [ComponentInformation(typeof(ContactBase))] + [ComponentInformation(typeof(ContactReceiver))] + [ComponentInformation(typeof(VRCContactReceiver))] + [ComponentInformation(typeof(ContactSender))] + [ComponentInformation(typeof(VRCContactSender))] + internal class ContactBaseInformation : ComponentInformation + { + protected override void CollectDependency(ContactBase component, ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + collector.AddDependency(component.rootTransform); + } + } +#endif + + [ComponentInformation(typeof(RemoveMeshByBlendShape))] + internal class RemoveMeshByBlendShapeInformation : ComponentInformation + { + protected override void CollectDependency(RemoveMeshByBlendShape component, ComponentDependencyCollector collector) + { + } + + protected override void CollectMutations(RemoveMeshByBlendShape component, ComponentMutationsCollector collector) + { + var blendShapes = component.RemovingShapeKeys; + { + collector.ModifyProperties(component.GetComponent(), + blendShapes.Select(blendShape => $"blendShape.{blendShape}")); + } + + DeriveMergeSkinnedMeshProperties(component.GetComponent()); + + void DeriveMergeSkinnedMeshProperties(MergeSkinnedMesh mergeSkinnedMesh) + { + if (mergeSkinnedMesh == null) return; + + foreach (var renderer in mergeSkinnedMesh.renderersSet.GetAsSet()) + { + collector.ModifyProperties(renderer, blendShapes.Select(blendShape => $"blendShape.{blendShape}")); + + DeriveMergeSkinnedMeshProperties(renderer.GetComponent()); + } + } + } + } + + [ComponentInformation(typeof(MergeBone))] + internal class MergeBoneInformation : ComponentInformation + { + protected override void CollectDependency(MergeBone component, ComponentDependencyCollector collector) + { + collector.AddDependency(component.transform, component) + .EvenIfDependantDisabled(); + } + + protected override void CollectMutations(MergeBone component, ComponentMutationsCollector collector) + { + } + } + + [ComponentInformationWithGUID("f9ac8d30c6a0d9642a11e5be4c440740", 11500000)] + internal class DynamicBoneInformation : ComponentInformation + { + protected override void CollectDependency(Component component, ComponentDependencyCollector collector) + { + collector.MarkBehaviour(); + + foreach (var transform in GetAffectedTransforms(component)) + { + collector.AddDependency(transform, component) + .EvenIfDependantDisabled() + .OnlyIfTargetCanBeEnable(); + collector.AddDependency(transform); + } + + foreach (var collider in (IReadOnlyList)((dynamic)component).m_Colliders) + { + // DynamicBone ignores enabled/disabled of Collider Component AFAIK + collector.AddDependency(collider); + } + } + + protected override void CollectMutations(Component component, ComponentMutationsCollector collector) + { + foreach (var transform in GetAffectedTransforms(component)) + collector.TransformRotation(transform); + } + + private static IEnumerable GetAffectedTransforms(dynamic dynamicBone) + { + var ignores = new HashSet(dynamicBone.m_Exclusions); + var queue = new Queue(); + Transform root = dynamicBone.m_Root; + queue.Enqueue(root ? root : (Transform)dynamicBone.transform); + + while (queue.Count != 0) + { + var transform = queue.Dequeue(); + yield return transform; + + foreach (var child in transform.DirectChildrenEnumerable()) + if (!ignores.Contains(child)) + queue.Enqueue(child); + } + } + } + + [ComponentInformationWithGUID("baedd976e12657241bf7ff2d1c685342", 11500000)] + internal class DynamicBoneColliderInformation : ComponentInformation + { + protected override void CollectDependency(Component component, ComponentDependencyCollector collector) + { + } + } + + #region Satania's KiseteneEx Components + [ComponentInformationWithGUID("e78466b6bcd24e5409dca557eb81d45b", 11500000)] // KiseteneComponent + [ComponentInformationWithGUID("7f9c3fe1cfb9d1843a9dc7da26352ce2", 11500000)] // FlyAvatarSetupTool + [ComponentInformationWithGUID("95f6e1368d803614f8a351322ab09bac", 11500000)] // BlendShapeOverrider + internal class SataniaKiseteneExComponents : ComponentInformation + { + protected override void CollectDependency(Component component, ComponentDependencyCollector collector) + { + } + } + #endregion + + + #region VRCQuestTools + + [ComponentInformationWithGUID("f055e14e1beba894ea68aedffde8ada6", 11500000)] // VertexColorRemover + internal class VRCQuestToolsComponents : ComponentInformation + { + protected override void CollectDependency(Component component, ComponentDependencyCollector collector) + { + } + } + + #endregion + + internal static class ComponentInformationExtensions + { + public static void TransformPositionAndRotation(this ComponentMutationsCollector collector, + Transform transform) => + collector.ModifyProperties(transform, + TransformPositionAnimationKeys.Concat(TransformRotationAnimationKeys)); + + public static void TransformRotation(this ComponentMutationsCollector collector, Transform transform) => + collector.ModifyProperties(transform, TransformRotationAnimationKeys); + + public static void TransformPosition(this ComponentMutationsCollector collector, Transform transform) => + collector.ModifyProperties(transform, TransformPositionAnimationKeys); + + public static void TransformScale(this ComponentMutationsCollector collector, Transform transform) => + collector.ModifyProperties(transform, TransformScaleAnimationKeys); + + private static readonly string[] TransformRotationAnimationKeys = + { "m_LocalRotation.x", "m_LocalRotation.y", "m_LocalRotation.z", "m_LocalRotation.w" }; + + private static readonly string[] TransformPositionAnimationKeys = + { "m_LocalPosition.x", "m_LocalPosition.y", "m_LocalPosition.z" }; + + private static readonly string[] TransformScaleAnimationKeys = + { "m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z" }; + } +} diff --git a/Editor/APIInternal/ComponentInfos.cs.meta b/Editor/APIInternal/ComponentInfos.cs.meta new file mode 100644 index 000000000..fc36b05f7 --- /dev/null +++ b/Editor/APIInternal/ComponentInfos.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3ccdf76384bc490d88e96b3e6b6f55b2 +timeCreated: 1696044689 \ No newline at end of file diff --git a/Editor/AutomaticConfiguration.cs b/Editor/AutomaticConfiguration.cs index 967027fa7..c95e69941 100644 --- a/Editor/AutomaticConfiguration.cs +++ b/Editor/AutomaticConfiguration.cs @@ -11,7 +11,6 @@ internal class TraceAndOptimizeEditor : AvatarGlobalComponentEditorBase private SerializedProperty _removeUnusedObjects; private SerializedProperty _preserveEndBone; private SerializedProperty _mmdWorldCompatibility; - private SerializedProperty _advancedAnimatorParser; private SerializedProperty _advancedSettings; private GUIContent _advancedSettingsLabel = new GUIContent(); @@ -21,7 +20,6 @@ private void OnEnable() _removeUnusedObjects = serializedObject.FindProperty(nameof(TraceAndOptimize.removeUnusedObjects)); _preserveEndBone = serializedObject.FindProperty(nameof(TraceAndOptimize.preserveEndBone)); _mmdWorldCompatibility = serializedObject.FindProperty(nameof(TraceAndOptimize.mmdWorldCompatibility)); - _advancedAnimatorParser = serializedObject.FindProperty(nameof(TraceAndOptimize.advancedAnimatorParser)); _advancedSettings = serializedObject.FindProperty(nameof(TraceAndOptimize.advancedSettings)); } @@ -47,7 +45,6 @@ protected override void OnInspectorGUIInner() { EditorGUI.indentLevel++; EditorGUILayout.HelpBox(CL4EE.Tr("TraceAndOptimize:warn:advancedSettings"), MessageType.Warning); - EditorGUILayout.PropertyField(_advancedAnimatorParser); var iterator = _advancedSettings.Copy(); var enterChildren = true; while (iterator.NextVisible(enterChildren)) diff --git a/Editor/AvatarGlobalComponentEditorBase.cs b/Editor/AvatarGlobalComponentEditorBase.cs index 96a6ba1d2..75208ca58 100644 --- a/Editor/AvatarGlobalComponentEditorBase.cs +++ b/Editor/AvatarGlobalComponentEditorBase.cs @@ -2,29 +2,41 @@ using CustomLocalization4EditorExtension; using UnityEditor; using UnityEngine; + +#if AAO_VRCSDK3_AVATARS using VRC.Core; using VRC.SDK3.Avatars.Components; +#endif namespace Anatawa12.AvatarOptimizer { - [InitializeOnLoad] abstract class AvatarGlobalComponentEditorBase : AvatarTagComponentEditorBase { static AvatarGlobalComponentEditorBase() { ComponentValidation.RegisterValidator(component => { +#if AAO_VRCSDK3_AVATARS if (!component.GetComponent()) return new[] { ErrorLog.Validation("AvatarGlobalComponent:NotOnAvatarDescriptor") }; +#else + if (!nadena.dev.ndmf.runtime.RuntimeUtil.IsAvatarRoot(component.transform)) + return new[] { ErrorLog.Validation("AvatarGlobalComponent:NotOnAvatarRoot") }; +#endif return null; }); } - protected override void OnInspectorGUIInner() { +#if AAO_VRCSDK3_AVATARS if (!((Component)serializedObject.targetObject).GetComponent()) EditorGUILayout.HelpBox(CL4EE.Tr("AvatarGlobalComponent:NotOnAvatarDescriptor"), MessageType.Error); +#else + if (!nadena.dev.ndmf.runtime.RuntimeUtil.IsAvatarRoot(((Component)serializedObject.targetObject).transform)) + EditorGUILayout.HelpBox(CL4EE.Tr("AvatarGlobalComponent:NotOnAvatarRoot"), + MessageType.Error); +#endif } } diff --git a/Editor/ClearEndpointPositionEditor.cs b/Editor/ClearEndpointPositionEditor.cs index 9c5aa30a0..e9127f6f2 100644 --- a/Editor/ClearEndpointPositionEditor.cs +++ b/Editor/ClearEndpointPositionEditor.cs @@ -1,3 +1,5 @@ +#if AAO_VRCSDK3_AVATARS + using Anatawa12.AvatarOptimizer.Processors; using CustomLocalization4EditorExtension; using UnityEditor; @@ -32,3 +34,5 @@ protected override void OnInspectorGUIInner() } } } + +#endif \ No newline at end of file diff --git a/Editor/ContextExtensions.cs b/Editor/ContextExtensions.cs new file mode 100644 index 000000000..e0fea6cb9 --- /dev/null +++ b/Editor/ContextExtensions.cs @@ -0,0 +1,51 @@ +using System; +using Anatawa12.AvatarOptimizer.Processors; +using Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes; +using JetBrains.Annotations; +using nadena.dev.ndmf; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer +{ + internal static class ContextExtensions + { + public static T[] GetComponents([NotNull] this BuildContext context) where T : Component + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return context.AvatarRootObject.GetComponentsInChildren(true); + } + + private static MeshInfo2Holder GetHolder([NotNull] this BuildContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return context.Extension().Holder; + } + + public static MeshInfo2 GetMeshInfoFor([NotNull] this BuildContext context, SkinnedMeshRenderer renderer) => + context.GetHolder().GetMeshInfoFor(renderer); + + public static MeshInfo2 GetMeshInfoFor([NotNull] this BuildContext context, MeshRenderer renderer) => + context.GetHolder().GetMeshInfoFor(renderer); + + private static ObjectMappingBuilder GetMappingBuilder([NotNull] this BuildContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return context.Extension().MappingBuilder; + } + + public static void RecordMergeComponent([NotNull] this BuildContext context, T from, T mergeTo) + where T : Component => + GetMappingBuilder(context).RecordMergeComponent(from, mergeTo); + + public static void RecordMoveProperties([NotNull] this BuildContext context, Component from, + params (string old, string @new)[] props) => + GetMappingBuilder(context).RecordMoveProperties(from, props); + + public static void RecordMoveProperty([NotNull] this BuildContext context, Component from, string oldProp, + string newProp) => + GetMappingBuilder(context).RecordMoveProperty(from, oldProp, newProp); + + public static void RecordRemoveProperty([NotNull] this BuildContext context, Component from, string oldProp) => + GetMappingBuilder(context).RecordRemoveProperty(from, oldProp); + } +} \ No newline at end of file diff --git a/Editor/ContextExtensions.cs.meta b/Editor/ContextExtensions.cs.meta new file mode 100644 index 000000000..242ba2858 --- /dev/null +++ b/Editor/ContextExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fb412e4993704a36927936a75c81fdf3 +timeCreated: 1695868322 \ No newline at end of file diff --git a/Editor/ExternalLibraryAccessor.cs b/Editor/ExternalLibraryAccessor.cs deleted file mode 100644 index dff8d8865..000000000 --- a/Editor/ExternalLibraryAccessor.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using JetBrains.Annotations; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace Anatawa12.AvatarOptimizer -{ - internal readonly struct DynamicBone : IEquatable - { - private readonly Component _object; - - // ReSharper disable once PossibleNullReferenceException - public List Exclusions => _object == null - ? throw new NullReferenceException() - : (List)ExternalLibraryAccessor.DynamicBone.Exclusions.GetValue(_object); - - // ReSharper disable once PossibleNullReferenceException - [CanBeNull] - public Transform Root => _object == null - ? throw new NullReferenceException() - : (Transform)ExternalLibraryAccessor.DynamicBone.Root.GetValue(_object); - - // ReSharper disable once PossibleNullReferenceException - public IReadOnlyList Colliders => _object == null - ? throw new NullReferenceException() - : (IReadOnlyList)ExternalLibraryAccessor.DynamicBone.Colliders.GetValue(_object); - - private DynamicBone(Component o) - { - _object = o; - } - - public static bool TryCast(Object component, out DynamicBone dynamicBone) - { - dynamicBone = default; - var classes = ExternalLibraryAccessor.DynamicBone; - if (classes == null) return false; - if (!classes.DynamicBoneType.IsInstanceOfType(component)) return false; - dynamicBone = new DynamicBone((Component)component); - return true; - } - - public IEnumerable GetAffectedTransforms() - { - var ignores = new HashSet(Exclusions); - var queue = new Queue(); - queue.Enqueue(Root ? Root : _object.transform); - - while (queue.Count != 0) - { - var transform = queue.Dequeue(); - yield return transform; - - foreach (var child in transform.DirectChildrenEnumerable()) - if (!ignores.Contains(child)) - queue.Enqueue(child); - } - } - - public bool Equals(DynamicBone other) => Equals(_object, other._object); - public override bool Equals(object obj) => obj is DynamicBone other && Equals(other); - public override int GetHashCode() => _object != null ? _object.GetHashCode() : 0; - - public static Type Type => ExternalLibraryAccessor.DynamicBone?.DynamicBoneType; - } - - static class ExternalLibraryAccessor - { - [CanBeNull] public static readonly DynamicBoneClasses DynamicBone = DynamicBoneClasses.Create(); - - public class DynamicBoneClasses - { - [NotNull] public readonly Type DynamicBoneType; - [NotNull] public readonly Type ColliderType; - [NotNull] public readonly FieldInfo Exclusions; - [NotNull] public readonly FieldInfo Root; - [NotNull] public readonly FieldInfo Colliders; - - private DynamicBoneClasses() - { - DynamicBoneType = Utils.GetTypeFromName("DynamicBone") ?? throw new Exception(); - ColliderType = Utils.GetTypeFromName("DynamicBoneCollider") ?? throw new Exception(); - - Exclusions = DynamicBoneType.GetField("m_Exclusions", BindingFlags.Instance | BindingFlags.Public) ?? - throw new Exception(); - if (Exclusions.FieldType != typeof(List)) throw new Exception(); - - Root = DynamicBoneType.GetField("m_Root", BindingFlags.Instance | BindingFlags.Public) ?? - throw new Exception(); - if (Root.FieldType != typeof(Transform)) throw new Exception(); - - Colliders = DynamicBoneType.GetField("m_Colliders", BindingFlags.Instance | BindingFlags.Public) ?? - throw new Exception(); - if (Colliders.FieldType != typeof(List<>).MakeGenericType(ColliderType)) throw new Exception(); - } - - - [CanBeNull] - public static DynamicBoneClasses Create() - { - try - { - return new DynamicBoneClasses(); - } - catch - { - return null; - } - } - } - } -} \ No newline at end of file diff --git a/Editor/ExternalLibraryAccessor.cs.meta b/Editor/ExternalLibraryAccessor.cs.meta deleted file mode 100644 index 15d51078b..000000000 --- a/Editor/ExternalLibraryAccessor.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 644e19c4d8e44e3b82e368773cbb72f9 -timeCreated: 1693815396 \ No newline at end of file diff --git a/Editor/MergePhysBoneEditor.cs b/Editor/MergePhysBoneEditor.cs index b563bac08..7c03e833a 100644 --- a/Editor/MergePhysBoneEditor.cs +++ b/Editor/MergePhysBoneEditor.cs @@ -1,3 +1,5 @@ +#if AAO_VRCSDK3_AVATARS + using System; using System.Collections.Generic; using System.Linq; @@ -657,3 +659,5 @@ protected override void CollidersProp(string label, CollidersConfigProp prop) } } } + +#endif \ No newline at end of file diff --git a/Editor/MergePhysBoneEditorModificationUtils.cs b/Editor/MergePhysBoneEditorModificationUtils.cs index e8e5b0d01..552a6d92e 100644 --- a/Editor/MergePhysBoneEditorModificationUtils.cs +++ b/Editor/MergePhysBoneEditorModificationUtils.cs @@ -1,3 +1,5 @@ +#if AAO_VRCSDK3_AVATARS + using System; using System.Collections.Generic; using System.Linq; @@ -401,3 +403,5 @@ internal override void UpdateSource(SerializedObject sourcePb) } } } + +#endif diff --git a/Editor/MergePhysBoneEditorModificationUtils.generated.cs b/Editor/MergePhysBoneEditorModificationUtils.generated.cs index f2648baec..abc60de80 100644 --- a/Editor/MergePhysBoneEditorModificationUtils.generated.cs +++ b/Editor/MergePhysBoneEditorModificationUtils.generated.cs @@ -1,5 +1,7 @@ // // generated by .MergePhysBoneEditorModificationUtils.ts +#if AAO_VRCSDK3_AVATARS + using System.Collections.Generic; using UnityEditor; using JetBrains.Annotations; @@ -193,3 +195,5 @@ internal override void UpdateSource(SerializedObject sourcePb) } } } + +#endif \ No newline at end of file diff --git a/Editor/ObjectMapping/AnimationObjectMapper.cs b/Editor/ObjectMapping/AnimationObjectMapper.cs index 06c5b9c35..0a1002652 100644 --- a/Editor/ObjectMapping/AnimationObjectMapper.cs +++ b/Editor/ObjectMapping/AnimationObjectMapper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes; using JetBrains.Annotations; using UnityEditor; using UnityEngine; @@ -93,7 +94,7 @@ public string MapPath(string srcPath, Type type) if (componentInfo != null) { - var component = EditorUtility.InstanceIDToObject(componentInfo.MergedInto) as Component; + var component = new ComponentOrGameObject(EditorUtility.InstanceIDToObject(componentInfo.MergedInto)); // there's mapping about component. // this means the component is merged or some prop has mapping if (!component) return null; // this means removed. @@ -106,57 +107,83 @@ public string MapPath(string srcPath, Type type) else { // The component is not merged & no prop mapping so process GameObject mapping - - if (type != typeof(GameObject)) - { - var component = EditorUtility.InstanceIDToObject(instanceId) as Component; - if (!component) return null; // this means removed - } + var component = EditorUtility.InstanceIDToObject(instanceId); + if (!component) return null; // this means removed if (gameObjectInfo.NewPath == null) return null; return gameObjectInfo.NewPath; } } - public EditorCurveBinding MapBinding(EditorCurveBinding binding) + [CanBeNull] + public EditorCurveBinding[] MapBinding(EditorCurveBinding binding) { var gameObjectInfo = GetGameObjectInfo(binding.path); - if (gameObjectInfo == null) return binding; + if (gameObjectInfo == null) + return null; var (instanceId, componentInfo) = gameObjectInfo.GetComponentByType(binding.type); if (componentInfo != null) { - var component = EditorUtility.InstanceIDToObject(componentInfo.MergedInto) as Component; // there's mapping about component. // this means the component is merged or some prop has mapping - if (!component) return default; // this means removed. - - var newPath = Utils.RelativePath(_rootGameObject.transform, component.transform); - if (newPath == null) return default; // this means moved to out of the animator scope - - binding.path = newPath; if (componentInfo.PropertyMapping.TryGetValue(binding.propertyName, out var newProp)) { - if (newProp == null) return default; - binding.propertyName = newProp; + // there are mapping for property + var curveBindings = new EditorCurveBinding[newProp.AllCopiedTo.Length]; + var copiedToIndex = 0; + for (var i = 0; i < newProp.AllCopiedTo.Length; i++) + { + var descriptor = newProp.AllCopiedTo[copiedToIndex++]; + var component = new ComponentOrGameObject(EditorUtility.InstanceIDToObject(descriptor.InstanceId)); + // this means removed. + if (!component) + { + copiedToIndex -= 1; + continue; + } + + var newPath = Utils.RelativePath(_rootGameObject.transform, component.transform); + + // this means moved to out of the animator scope + // TODO: add warning + if (newPath == null) return Array.Empty(); + + binding.path = newPath; + binding.type = descriptor.Type; + binding.propertyName = descriptor.Name; + curveBindings[i] = binding; // copy + } + + if (copiedToIndex != curveBindings.Length) + return curveBindings.AsSpan().Slice(0, copiedToIndex).ToArray(); + return curveBindings; + } + else + { + var component = new ComponentOrGameObject(EditorUtility.InstanceIDToObject(componentInfo.MergedInto)); + if (!component) return Array.Empty(); // this means removed. + + var newPath = Utils.RelativePath(_rootGameObject.transform, component.transform); + if (newPath == null) return Array.Empty(); // this means moved to out of the animator scope + if (binding.path == newPath) return null; + binding.path = newPath; + return new []{ binding }; } } else { // The component is not merged & no prop mapping so process GameObject mapping - if (binding.type != typeof(GameObject)) - { - var component = EditorUtility.InstanceIDToObject(instanceId) as Component; - if (!component) return default; // this means removed - } + var component = EditorUtility.InstanceIDToObject(instanceId); + if (!component) return Array.Empty(); // this means removed - if (gameObjectInfo.NewPath == null) return default; + if (gameObjectInfo.NewPath == null) return Array.Empty(); + if (binding.path == gameObjectInfo.NewPath) return null; binding.path = gameObjectInfo.NewPath; + return new[] { binding }; } - - return binding; } } } diff --git a/Editor/ObjectMapping/ObjectMapping.cs b/Editor/ObjectMapping/ObjectMapping.cs index a2b8c0fe5..b22119211 100644 --- a/Editor/ObjectMapping/ObjectMapping.cs +++ b/Editor/ObjectMapping/ObjectMapping.cs @@ -94,6 +94,8 @@ public BeforeGameObjectTree(GameObject gameObject) } } + componentByType[typeof(GameObject)] = InstanceId; + ComponentInstanceIdByType = componentByType; } } @@ -103,9 +105,10 @@ class ComponentInfo public readonly int InstanceId; public readonly int MergedInto; public readonly Type Type; - public readonly IReadOnlyDictionary PropertyMapping; + public readonly IReadOnlyDictionary PropertyMapping; - public ComponentInfo(int instanceId, int mergedInto, Type type, IReadOnlyDictionary propertyMapping) + public ComponentInfo(int instanceId, int mergedInto, Type type, + IReadOnlyDictionary propertyMapping) { InstanceId = instanceId; MergedInto = mergedInto; @@ -114,6 +117,64 @@ public ComponentInfo(int instanceId, int mergedInto, Type type, IReadOnlyDiction } } + readonly struct PropertyDescriptor : IEquatable + { + public static readonly PropertyDescriptor Removed = default; + public readonly int InstanceId; + [NotNull] public readonly Type Type; + [NotNull] public readonly string Name; + + public PropertyDescriptor(int instanceId, Type type, string name) + { + InstanceId = instanceId; + Type = type; + Name = name; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = InstanceId; + hashCode = (hashCode * 397) ^ Type.GetHashCode(); + hashCode = (hashCode * 397) ^ Name.GetHashCode(); + return hashCode; + } + } + + public bool Equals(PropertyDescriptor other) => + InstanceId == other.InstanceId && Type == other.Type && Name == other.Name; + public override bool Equals(object obj) => obj is PropertyDescriptor other && Equals(other); + public static bool operator ==(PropertyDescriptor left, PropertyDescriptor right) => left.Equals(right); + public static bool operator !=(PropertyDescriptor left, PropertyDescriptor right) => !left.Equals(right); + } + + readonly struct MappedPropertyInfo + { + public static readonly MappedPropertyInfo Removed = default; + public readonly PropertyDescriptor MappedProperty; + private readonly PropertyDescriptor[] _copiedTo; + + public PropertyDescriptor[] AllCopiedTo => _copiedTo ?? Array.Empty(); + + public MappedPropertyInfo(PropertyDescriptor property, PropertyDescriptor[] copiedTo) + { + MappedProperty = property; + _copiedTo = copiedTo; + } + + public MappedPropertyInfo(int mappedInstanceId, Type mappedType, string mappedName) : this( + new PropertyDescriptor(mappedInstanceId, mappedType, mappedName)) + { + } + + public MappedPropertyInfo(PropertyDescriptor property) + { + MappedProperty = property; + _copiedTo = new[] { property }; + } + } + static class VProp { private const string ExtraProps = "AvatarOptimizerExtraProps"; diff --git a/Editor/ObjectMapping/ObjectMappingBuilder.cs b/Editor/ObjectMapping/ObjectMappingBuilder.cs index abd828d60..703d7b2d4 100644 --- a/Editor/ObjectMapping/ObjectMappingBuilder.cs +++ b/Editor/ObjectMapping/ObjectMappingBuilder.cs @@ -19,6 +19,7 @@ internal class ObjectMappingBuilder private readonly IReadOnlyDictionary _beforeGameObjectInfos; // key: instanceId + private readonly Dictionary _originalComponentInfos = new Dictionary(); private readonly Dictionary _componentInfos = new Dictionary(); public ObjectMappingBuilder([NotNull] GameObject rootObject) @@ -45,22 +46,47 @@ public ObjectMappingBuilder([NotNull] GameObject rootObject) #endif } - public void RecordMergeComponent(T from, T mergeTo) where T: Component => - GetComponentInfo(from).MergedTo(GetComponentInfo(mergeTo)); + public void RecordMergeComponent(T from, T mergeTo) where T: Component + { + if (!_componentInfos.TryGetValue(mergeTo.GetInstanceID(), out var mergeToInfo)) + { + var newMergeToInfo = new BuildingComponentInfo(mergeTo); + _originalComponentInfos.Add(mergeTo.GetInstanceID(), newMergeToInfo); + _componentInfos.Add(mergeTo.GetInstanceID(), newMergeToInfo); + GetComponentInfo(from).MergedTo(newMergeToInfo); + } + else + { + var newMergeToInfo = new BuildingComponentInfo(mergeTo); + _componentInfos[mergeTo.GetInstanceID()]= newMergeToInfo; + mergeToInfo.MergedTo(newMergeToInfo); + GetComponentInfo(from).MergedTo(newMergeToInfo); + } + } - public void RecordMoveProperties(Component from, params (string old, string @new)[] props) => + public void RecordMoveProperties(ComponentOrGameObject from, params (string old, string @new)[] props) => GetComponentInfo(from).MoveProperties(props); - public void RecordMoveProperty(Component from, string oldProp, string newProp) => + public void RecordMoveProperty(ComponentOrGameObject from, string oldProp, string newProp) => GetComponentInfo(from).MoveProperties((oldProp, newProp)); - public void RecordRemoveProperty(Component from, string oldProp) => + public void RecordMoveProperty(ComponentOrGameObject fromComponent, string oldProp, ComponentOrGameObject toComponent, string newProp) => + GetComponentInfo(fromComponent).MoveProperty(GetComponentInfo(toComponent), oldProp, newProp); + + public void RecordCopyProperty(ComponentOrGameObject fromComponent, string oldProp, ComponentOrGameObject toComponent, string newProp) => + GetComponentInfo(fromComponent).CopyProperty(GetComponentInfo(toComponent), oldProp, newProp); + + public void RecordRemoveProperty(ComponentOrGameObject from, string oldProp) => GetComponentInfo(from).RemoveProperty(oldProp); - private BuildingComponentInfo GetComponentInfo(Component component) + private BuildingComponentInfo GetComponentInfo(ComponentOrGameObject component) { if (!_componentInfos.TryGetValue(component.GetInstanceID(), out var info)) - _componentInfos.Add(component.GetInstanceID(), info = new BuildingComponentInfo(component)); + { + info = new BuildingComponentInfo(component); + _originalComponentInfos.Add(component.GetInstanceID(), info); + _componentInfos.Add(component.GetInstanceID(), info); + } return info; } @@ -68,104 +94,173 @@ public ObjectMapping BuildObjectMapping() { return new ObjectMapping( _beforeGameObjectInfos, - _componentInfos.ToDictionary(p => p.Key, p => p.Value.Build())); + _originalComponentInfos.ToDictionary(p => p.Key, p => p.Value.Build())); + } + + class AnimationProperty + { + [CanBeNull] public readonly BuildingComponentInfo Component; + [CanBeNull] public readonly string Name; + [CanBeNull] public AnimationProperty MergedTo; + private MappedPropertyInfo? _mappedPropertyInfo; + [CanBeNull] public List CopiedTo; + + public AnimationProperty([NotNull] BuildingComponentInfo component, [NotNull] string name) + { + Component = component ?? throw new ArgumentNullException(nameof(component)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + private AnimationProperty() + { + } + + public static readonly AnimationProperty RemovedMarker = new AnimationProperty(); + + public MappedPropertyInfo GetMappedInfo() + { + if (_mappedPropertyInfo is MappedPropertyInfo property) return property; + property = ComputeMappedInfo(); + _mappedPropertyInfo = property; + return property; + } + + private MappedPropertyInfo ComputeMappedInfo() + { + if (this == RemovedMarker) return MappedPropertyInfo.Removed; + + System.Diagnostics.Debug.Assert(Component != null, nameof(Component) + " != null"); + + if (MergedTo != null) + { + var merged = MergedTo.GetMappedInfo(); + + if (CopiedTo == null || CopiedTo.Count == 0) + return merged; + + var copied = new List(); + copied.AddRange(merged.AllCopiedTo); + foreach (var copiedTo in CopiedTo) + copied.AddRange(copiedTo.GetMappedInfo().AllCopiedTo); + + return new MappedPropertyInfo(merged.MappedProperty, copied.ToArray()); + } + else + { + // this is edge + if (CopiedTo == null || CopiedTo.Count == 0) + return new MappedPropertyInfo(Component.InstanceId, Component.Type, Name); + + var descriptor = new PropertyDescriptor(Component.InstanceId, Component.Type, Name); + + var copied = new List { descriptor }; + foreach (var copiedTo in CopiedTo) + copied.AddRange(copiedTo.GetMappedInfo().AllCopiedTo); + + return new MappedPropertyInfo(descriptor, copied.ToArray()); + } + } } class BuildingComponentInfo { - private readonly int _instanceId; - private readonly Type _type; - private readonly List MergeSources = new List(); + internal readonly int InstanceId; + internal readonly Type Type; // id in this -> id in merged private BuildingComponentInfo _mergedInto; - // renaming property tracker - private int _nextPropertyId = 1; - private readonly Dictionary _beforePropertyIds = new Dictionary(); - private readonly Dictionary _afterPropertyIds = new Dictionary(); + private readonly Dictionary _beforePropertyIds = + new Dictionary(); + + private readonly Dictionary _afterPropertyIds = + new Dictionary(); - public BuildingComponentInfo(Component component) + public BuildingComponentInfo(ComponentOrGameObject component) { - _instanceId = component.GetInstanceID(); - _type = component.GetType(); + InstanceId = component.GetInstanceID(); + Type = component.Value.GetType(); + } + + internal bool IsMerged => _mergedInto != null; + + [NotNull] + private AnimationProperty GetProperty(string name, bool remove = false) + { + if (_afterPropertyIds.TryGetValue(name, out var prop)) + { + if (remove) _afterPropertyIds.Remove(name); + return prop; + } + else + { + var newProp = new AnimationProperty(this, name); + if (!remove) _afterPropertyIds.Add(name, newProp); + if (!_beforePropertyIds.ContainsKey(name)) + _beforePropertyIds.Add(name, newProp); + return newProp; + } } public void MergedTo([NotNull] BuildingComponentInfo mergeTo) { - if (_type == typeof(Transform)) throw new Exception("Merging Transform is not supported!"); - if (mergeTo == null) throw new ArgumentNullException(nameof(mergeTo)); + if (Type == typeof(Transform)) throw new Exception("Merging Transform is not supported!"); if (_mergedInto != null) throw new InvalidOperationException("Already merged"); - mergeTo.MergeSources.Add(this); - _mergedInto = mergeTo; + _mergedInto = mergeTo ?? throw new ArgumentNullException(nameof(mergeTo)); + foreach (var property in _afterPropertyIds.Values) + property.MergedTo = mergeTo.GetProperty(property.Name); + _afterPropertyIds.Clear(); } public void MoveProperties(params (string old, string @new)[] props) { - if (_type == typeof(Transform)) throw new Exception("Move properties of Transform is not supported!"); - foreach (var mergeSource in MergeSources) mergeSource.MoveProperties(props); + if (Type == typeof(Transform)) throw new Exception("Move properties of Transform is not supported!"); + if (_mergedInto != null) throw new Exception("Already Merged"); - var propertyIds = new int[props.Length]; + var propertyIds = new AnimationProperty[props.Length]; for (var i = 0; i < props.Length; i++) - { - var (oldProp, newProp) = props[i]; - if (_afterPropertyIds.TryGetValue(oldProp, out var propId)) - { - propertyIds[i] = propId; - } - else - { - if (!_beforePropertyIds.ContainsKey(oldProp)) - { - propertyIds[i] = _nextPropertyId++; - } - } - } + propertyIds[i] = GetProperty(props[i].old, remove: true); for (var i = 0; i < propertyIds.Length; i++) - { - var propId = propertyIds[i]; - var (oldProp, _) = props[i]; - if (propId == 0) continue; - _afterPropertyIds.Remove(oldProp); - } + propertyIds[i].MergedTo = GetProperty(props[i].@new); + } - for (var i = 0; i < propertyIds.Length; i++) - { - var propId = propertyIds[i]; - var (oldProp, newProp) = props[i]; - if (propId == 0) continue; - _afterPropertyIds[newProp] = propId; - if (!_beforePropertyIds.ContainsKey(oldProp)) - _beforePropertyIds.Add(oldProp, propId); - } + public void MoveProperty(BuildingComponentInfo toComponent, string oldProp, string newProp) + { + if (Type == typeof(Transform)) throw new Exception("Move properties of Transform is not supported!"); + GetProperty(oldProp, remove: true).MergedTo = toComponent.GetProperty(newProp); } - public void RemoveProperty(string oldProp) + public void CopyProperty(BuildingComponentInfo toComponent, string oldProp, string newProp) { - if (_type == typeof(Transform)) throw new Exception("Removing properties of Transform is not supported!"); - foreach (var mergeSource in MergeSources) mergeSource.RemoveProperty(oldProp); - // if (_afterPropertyIds.ContainsKey(oldProp)) - // _afterPropertyIds.Remove(oldProp); - // else - // if (!_beforePropertyIds.ContainsKey(oldProp)) - // _beforePropertyIds.Add(oldProp, _nextPropertyId++); - if (!_afterPropertyIds.Remove(oldProp)) - if (!_beforePropertyIds.ContainsKey(oldProp)) - _beforePropertyIds.Add(oldProp, _nextPropertyId++); + var prop = GetProperty(oldProp); + if (prop.CopiedTo == null) + prop.CopiedTo = new List(); + prop.CopiedTo.Add(toComponent.GetProperty(newProp)); + } + + public void RemoveProperty(string property) + { + if (Type == typeof(Transform)) throw new Exception("Removing properties of Transform is not supported!"); + if (_mergedInto != null) throw new Exception("Already Merged"); + + GetProperty(property, remove: true).MergedTo = AnimationProperty.RemovedMarker; } public ComponentInfo Build() { + var propertyMapping = _beforePropertyIds.ToDictionary(p => p.Key, + p => p.Value.GetMappedInfo()); var mergedInfo = this; while (mergedInfo._mergedInto != null) + { mergedInfo = mergedInfo._mergedInto; + foreach (var (key, value) in mergedInfo._beforePropertyIds) + if (!propertyMapping.ContainsKey(key)) + propertyMapping.Add(key, value.GetMappedInfo()); + } - var idToAfterName = _afterPropertyIds.ToDictionary(p => p.Value, p => p.Key); - var propertyMapping = _beforePropertyIds.ToDictionary(p => p.Key, - p => idToAfterName.TryGetValue(p.Value, out var name) ? name : null); - - return new ComponentInfo(_instanceId, mergedInfo._instanceId, _type, propertyMapping); + return new ComponentInfo(InstanceId, mergedInfo.InstanceId, Type, propertyMapping); } } } diff --git a/Editor/Processors/ApplyObjectMapping.cs b/Editor/ObjectMapping/ObjectMappingContext.cs similarity index 67% rename from Editor/Processors/ApplyObjectMapping.cs rename to Editor/ObjectMapping/ObjectMappingContext.cs index d62d6e88a..065f90f3a 100644 --- a/Editor/Processors/ApplyObjectMapping.cs +++ b/Editor/ObjectMapping/ObjectMappingContext.cs @@ -2,88 +2,58 @@ using System.Collections.Generic; using System.Linq; using Anatawa12.AvatarOptimizer.ErrorReporting; +using nadena.dev.ndmf; using UnityEditor; using UnityEditor.Animations; using UnityEngine; + +#if AAO_VRCSDK3_AVATARS using VRC.SDK3.Avatars.Components; +#endif + using Object = UnityEngine.Object; -namespace Anatawa12.AvatarOptimizer.Processors +namespace Anatawa12.AvatarOptimizer { - internal class ApplyObjectMapping + internal class ObjectMappingContext : IExtensionContext { - public void Apply(OptimizerSession session) + public ObjectMappingBuilder MappingBuilder { get; private set; } + + public void OnActivate(BuildContext context) { - var mapping = session.MappingBuilder.BuildObjectMapping(); + MappingBuilder = new ObjectMappingBuilder(context.AvatarRootObject); + } + + public void OnDeactivate(BuildContext context) + { + var mapping = MappingBuilder.BuildObjectMapping(); // replace all objects - BuildReport.ReportingObjects(session.GetComponents(), component => + BuildReport.ReportingObjects(context.GetComponents(), component => { if (component is Transform) return; var serialized = new SerializedObject(component); AnimatorControllerMapper mapper = null; SpecialMappingApplier.Apply(component.GetType(), serialized, mapping, ref mapper); - var p = serialized.GetIterator(); - var enterChildren = true; - while (p.Next(enterChildren)) + foreach (var p in serialized.ObjectReferenceProperties()) { - if (p.propertyType == SerializedPropertyType.ObjectReference) - { - if (mapping.MapComponentInstance(p.objectReferenceInstanceIDValue, out var mappedComponent)) - p.objectReferenceValue = mappedComponent; + if (mapping.MapComponentInstance(p.objectReferenceInstanceIDValue, out var mappedComponent)) + p.objectReferenceValue = mappedComponent; - if (p.objectReferenceValue is RuntimeAnimatorController controller) - { - if (mapper == null) - mapper = new AnimatorControllerMapper( - mapping.CreateAnimationMapper(component.gameObject), - session.RelativePath(component.transform), session); - - // ReSharper disable once AccessToModifiedClosure - var mapped = BuildReport.ReportingObject(controller, - () => mapper.MapAnimatorController(controller)); - if (mapped != controller) - p.objectReferenceValue = mapped; - } - } - - switch (p.propertyType) + if (p.objectReferenceValue is RuntimeAnimatorController controller) { - case SerializedPropertyType.String: - case SerializedPropertyType.Integer: - case SerializedPropertyType.Boolean: - case SerializedPropertyType.Float: - case SerializedPropertyType.Color: - case SerializedPropertyType.ObjectReference: - case SerializedPropertyType.LayerMask: - case SerializedPropertyType.Enum: - case SerializedPropertyType.Vector2: - case SerializedPropertyType.Vector3: - case SerializedPropertyType.Vector4: - case SerializedPropertyType.Rect: - case SerializedPropertyType.ArraySize: - case SerializedPropertyType.Character: - case SerializedPropertyType.AnimationCurve: - case SerializedPropertyType.Bounds: - case SerializedPropertyType.Gradient: - case SerializedPropertyType.Quaternion: - case SerializedPropertyType.FixedBufferSize: - case SerializedPropertyType.Vector2Int: - case SerializedPropertyType.Vector3Int: - case SerializedPropertyType.RectInt: - case SerializedPropertyType.BoundsInt: - enterChildren = false; - break; - case SerializedPropertyType.Generic: - case SerializedPropertyType.ExposedReference: - case SerializedPropertyType.ManagedReference: - default: - enterChildren = true; - break; + if (mapper == null) + mapper = new AnimatorControllerMapper(mapping.CreateAnimationMapper(component.gameObject)); + + // ReSharper disable once AccessToModifiedClosure + var mapped = BuildReport.ReportingObject(controller, + () => mapper.MapAnimatorController(controller)); + if (mapped != controller) + p.objectReferenceValue = mapped; } } - serialized.ApplyModifiedProperties(); + serialized.ApplyModifiedPropertiesWithoutUndo(); }); } } @@ -93,10 +63,13 @@ internal static class SpecialMappingApplier public static void Apply(Type type, SerializedObject serialized, ObjectMapping mapping, ref AnimatorControllerMapper mapper) { +#if AAO_VRCSDK3_AVATARS if (type.IsAssignableFrom(typeof(VRCAvatarDescriptor))) VRCAvatarDescriptor(serialized, mapping, ref mapper); +#endif } +#if AAO_VRCSDK3_AVATARS // customEyeLookSettings.eyelidsBlendshapes is index private static void VRCAvatarDescriptor(SerializedObject serialized, ObjectMapping mapping, ref AnimatorControllerMapper mapper) @@ -119,33 +92,30 @@ private static void VRCAvatarDescriptor(SerializedObject serialized, var indexProp = eyelidsBlendshapes.GetArrayElementAtIndex(i); if (info.PropertyMapping.TryGetValue( VProp.BlendShapeIndex(indexProp.intValue), - out var mappedPropName)) + out var mappedProp)) { - if (mappedPropName == null) + if (mappedProp.MappedProperty.Name == null) { BuildReport.LogFatal("ApplyObjectMapping:VRCAvatarDescriptor:eyelids BlendShape Removed"); return; } - indexProp.intValue = VProp.ParseBlendShapeIndex(mappedPropName); + indexProp.intValue = VProp.ParseBlendShapeIndex(mappedProp.MappedProperty.Name); } } } } +#endif } internal class AnimatorControllerMapper { private readonly AnimationObjectMapper _mapping; private readonly Dictionary _cache = new Dictionary(); - private readonly OptimizerSession _session; - private readonly string _rootPath; private bool _mapped = false; - public AnimatorControllerMapper(AnimationObjectMapper mapping, string rootPath, OptimizerSession session) + public AnimatorControllerMapper(AnimationObjectMapper mapping) { - _session = session; _mapping = mapping; - _rootPath = rootPath; } public T MapAnimatorController(T controller) where T : RuntimeAnimatorController => @@ -172,20 +142,40 @@ private Object CustomClone(Object o) foreach (var binding in AnimationUtility.GetCurveBindings(clip)) { - var newBinding = _mapping.MapBinding(binding); - _mapped |= newBinding != binding; - if (newBinding.type == null) continue; - newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName, - AnimationUtility.GetEditorCurve(clip, binding)); + var newBindings = _mapping.MapBinding(binding); + if (newBindings == null) + { + newClip.SetCurve(binding.path, binding.type, binding.propertyName, + AnimationUtility.GetEditorCurve(clip, binding)); + } + else + { + _mapped = true; + foreach (var newBinding in newBindings) + { + newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName, + AnimationUtility.GetEditorCurve(clip, binding)); + } + } } foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip)) { - var newBinding = _mapping.MapBinding(binding); - _mapped |= newBinding != binding; - if (newBinding.type == null) continue; - AnimationUtility.SetObjectReferenceCurve(newClip, newBinding, - AnimationUtility.GetObjectReferenceCurve(clip, binding)); + var newBindings = _mapping.MapBinding(binding); + if (newBindings == null) + { + AnimationUtility.SetObjectReferenceCurve(newClip, binding, + AnimationUtility.GetObjectReferenceCurve(clip, binding)); + } + else + { + _mapped = true; + foreach (var newBinding in newBindings) + { + AnimationUtility.SetObjectReferenceCurve(newClip, newBinding, + AnimationUtility.GetObjectReferenceCurve(clip, binding)); + } + } } // ReSharper disable once CompareOfFloatsByEqualityOperator @@ -332,28 +322,15 @@ private T DefaultDeepClone(T original, Func visitor) where T _cache[original] = obj; _cache[obj] = obj; - SerializedObject so = new SerializedObject(obj); - SerializedProperty prop = so.GetIterator(); - - bool enterChildren = true; - while (prop.Next(enterChildren)) + using (var so = new SerializedObject(obj)) { - enterChildren = true; - switch (prop.propertyType) - { - case SerializedPropertyType.ObjectReference: - prop.objectReferenceValue = DeepClone(prop.objectReferenceValue, visitor); - break; - // Iterating strings can get super slow... - case SerializedPropertyType.String: - enterChildren = false; - break; - } - } + foreach (var prop in so.ObjectReferenceProperties()) + prop.objectReferenceValue = DeepClone(prop.objectReferenceValue, visitor); - so.ApplyModifiedPropertiesWithoutUndo(); + so.ApplyModifiedPropertiesWithoutUndo(); + } return (T)obj; } } -} +} \ No newline at end of file diff --git a/Editor/Processors/ApplyObjectMapping.cs.meta b/Editor/ObjectMapping/ObjectMappingContext.cs.meta similarity index 100% rename from Editor/Processors/ApplyObjectMapping.cs.meta rename to Editor/ObjectMapping/ObjectMappingContext.cs.meta diff --git a/Editor/OptimizerPlugin.cs b/Editor/OptimizerPlugin.cs new file mode 100644 index 000000000..8df3fdfb8 --- /dev/null +++ b/Editor/OptimizerPlugin.cs @@ -0,0 +1,72 @@ +using System; +using Anatawa12.AvatarOptimizer.ErrorReporting; +using Anatawa12.AvatarOptimizer.ndmf; +using nadena.dev.ndmf; +using nadena.dev.ndmf.builtin; + +[assembly: ExportsPlugin(typeof(OptimizerPlugin))] + +namespace Anatawa12.AvatarOptimizer.ndmf +{ + internal class OptimizerPlugin : Plugin + { + public override string DisplayName => "Anatawa12's Avatar Optimizer"; + + public override string QualifiedName => "com.anatawa12.avatar-optimizer"; + + protected override void Configure() + { + // Run early steps before EditorOnly objects are purged + InPhase(BuildPhase.Resolving) + .WithRequiredExtensions(new [] {typeof(BuildReportContext)}, seq => + { + seq.Run("Info if AAO is Out of Date", ctx => + { + // we skip check for update + var components = ctx.AvatarRootObject.GetComponentInChildren(true); + if (components && CheckForUpdate.OutOfDate) + BuildReport.LogInfo("CheckForUpdate:out-of-date", + CheckForUpdate.LatestVersionName, CheckForUpdate.CurrentVersionName); + }) + .Then.Run(Processors.UnusedBonesByReferencesToolEarlyProcessor.Instance) + .Then.Run("Early: MakeChildren", + ctx => new Processors.MakeChildrenProcessor(early: true).Process(ctx) + ) + .BeforePass(RemoveEditorOnlyPass.Instance); + }); + + // Run everything else in the optimize phase + InPhase(BuildPhase.Optimizing) + .WithRequiredExtension(typeof(BuildReportContext), seq => + { + seq.Run("EmptyPass for Context Ordering", _ => {}); + seq.WithRequiredExtensions(new[] + { + typeof(Processors.MeshInfo2Context), + typeof(ObjectMappingContext), + }, _ => + { + seq.Run(Processors.TraceAndOptimizes.LoadTraceAndOptimizeConfiguration.Instance) + .Then.Run(Processors.TraceAndOptimizes.ParseAnimator.Instance) + .Then.Run(Processors.TraceAndOptimizes.AutoFreezeBlendShape.Instance) +#if AAO_VRCSDK3_AVATARS + .Then.Run(Processors.ClearEndpointPositionProcessor.Instance) + .Then.Run(Processors.MergePhysBoneProcessor.Instance) +#endif + .Then.Run(Processors.EditSkinnedMeshComponentProcessor.Instance) + .Then.Run("MakeChildrenProcessor", + ctx => new Processors.MakeChildrenProcessor(early: false).Process(ctx) + ) + .Then.Run(Processors.TraceAndOptimizes.FindUnusedObjects.Instance) + .Then.Run(Processors.MergeBoneProcessor.Instance); + }); + seq.Run("EmptyPass for Context Ordering", _ => {}); + }); + } + + protected override void OnUnhandledException(Exception e) + { + BuildReport.ReportInternalError(e); + } + } +} diff --git a/Editor/Processors/OptimizerPlugin.cs.meta b/Editor/OptimizerPlugin.cs.meta similarity index 100% rename from Editor/Processors/OptimizerPlugin.cs.meta rename to Editor/OptimizerPlugin.cs.meta diff --git a/Editor/OptimizerSession.cs b/Editor/OptimizerSession.cs deleted file mode 100644 index 198493a25..000000000 --- a/Editor/OptimizerSession.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Anatawa12.AvatarOptimizer.Processors; -using Anatawa12.AvatarOptimizer.ndmf; -using nadena.dev.ndmf; -using UnityEditor; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace Anatawa12.AvatarOptimizer -{ - internal class OptimizerSession - { - private readonly GameObject _rootObject; - public bool IsTest { get; } - public ObjectMappingBuilder MappingBuilder { get; } - public MeshInfo2Holder MeshInfo2Holder { get; private set; } - - public readonly Dictionary> PreserveBlendShapes = - new Dictionary>(); - - public static implicit operator OptimizerSession(BuildContext context) - { - return context.Extension().session; - } - - public OptimizerSession(GameObject rootObject, bool isTest) - { - IsTest = isTest; - _rootObject = rootObject; - MappingBuilder = new ObjectMappingBuilder(rootObject); - MeshInfo2Holder = new MeshInfo2Holder(rootObject); - } - - public T GetRootComponent() where T : Component - { - return _rootObject != null ? _rootObject.GetComponent() : null; - } - - public IEnumerable GetComponents() where T : Component - { - return (_rootObject != null ? _rootObject.GetComponentsInChildren(true) : Object.FindObjectsOfType()) - .Where(x => x); - } - - public string RelativePath(Transform child) - { - return Utils.RelativePath(_rootObject.transform, child) ?? - throw new ArgumentException("child is not child of rootObject", nameof(child)); - } - - public void SaveMeshInfo2() - { - MeshInfo2Holder.SaveToMesh(this); - MeshInfo2Holder = null; - } - } -} diff --git a/Editor/OptimizerSession.cs.meta b/Editor/OptimizerSession.cs.meta deleted file mode 100644 index 7c633e885..000000000 --- a/Editor/OptimizerSession.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 7d650b788d844c8eaecd0f1781dd0b1a -timeCreated: 1672460390 \ No newline at end of file diff --git a/Editor/PreventRemoveAvatarEditorOnly.cs b/Editor/PreventRemoveAvatarEditorOnly.cs index 7e751b68f..4f4d6e9da 100644 --- a/Editor/PreventRemoveAvatarEditorOnly.cs +++ b/Editor/PreventRemoveAvatarEditorOnly.cs @@ -1,3 +1,5 @@ +#if AAO_VRCSDK3_AVATARS + using System.Collections.Generic; using System.Reflection; using UnityEditor; @@ -67,3 +69,5 @@ public bool OnPreprocessAvatar(GameObject avatarGameObject) } } } + +#endif \ No newline at end of file diff --git a/Editor/Processors/ClearEndpointPositionProcessor.cs b/Editor/Processors/ClearEndpointPositionProcessor.cs index 8e987b9fe..a4c150095 100644 --- a/Editor/Processors/ClearEndpointPositionProcessor.cs +++ b/Editor/Processors/ClearEndpointPositionProcessor.cs @@ -1,16 +1,21 @@ +#if AAO_VRCSDK3_AVATARS + using System; using Anatawa12.AvatarOptimizer.ErrorReporting; +using nadena.dev.ndmf; using UnityEditor; using UnityEngine; using VRC.Dynamics; namespace Anatawa12.AvatarOptimizer.Processors { - internal class ClearEndpointPositionProcessor + internal class ClearEndpointPositionProcessor : Pass { - public void Process(OptimizerSession session) + public override string DisplayName => "ClearEndpointPosition"; + + protected override void Execute(BuildContext context) { - BuildReport.ReportingObjects(session.GetComponents(), + BuildReport.ReportingObjects(context.GetComponents(), component => BuildReport.ReportingObjects(component.GetComponents(), Process)); } @@ -47,3 +52,5 @@ internal static bool WalkChildrenAndSetEndpoint(Transform target, VRCPhysBoneBas } } } + +#endif \ No newline at end of file diff --git a/Editor/Processors/EditSkinnedMeshComponentProcessor.cs b/Editor/Processors/EditSkinnedMeshComponentProcessor.cs index a6dca2655..18cc3626b 100644 --- a/Editor/Processors/EditSkinnedMeshComponentProcessor.cs +++ b/Editor/Processors/EditSkinnedMeshComponentProcessor.cs @@ -1,31 +1,41 @@ using Anatawa12.AvatarOptimizer.ErrorReporting; +using Anatawa12.AvatarOptimizer.ndmf; +using nadena.dev.ndmf; using UnityEngine; +using UnityEngine.Profiling; namespace Anatawa12.AvatarOptimizer.Processors { - internal class EditSkinnedMeshComponentProcessor + internal class EditSkinnedMeshComponentProcessor : Pass { - public void Process(OptimizerSession session) + public override string DisplayName => "EditSkinnedMeshComponent"; + + protected override void Execute(BuildContext context) { + Profiler.BeginSample("Initialize SkinnedMeshEditorSorter"); var graph = new SkinnedMeshEditorSorter(); - foreach (var component in session.GetComponents()) + foreach (var component in context.GetComponents()) graph.AddComponent(component); + Profiler.EndSample(); - var renderers = session.GetComponents(); + var renderers = context.GetComponents(); var processorLists = graph.GetSortedProcessors(renderers); foreach (var processors in processorLists) { - var target = session.MeshInfo2Holder.GetMeshInfoFor(processors.Target); + Profiler.BeginSample($"EditSkinnedMeshComponents: {processors.Target.name}"); + var target = context.GetMeshInfoFor(processors.Target); foreach (var processor in processors.GetSorted()) { - // TODO - BuildReport.ReportingObject(processor.Component, () => processor.Process(session, target)); + Profiler.BeginSample($"{processor.GetType().Name}: {processors.Target.name}"); + BuildReport.ReportingObject(processor.Component, () => processor.Process(context, target)); target.AssertInvariantContract( $"after {processor.GetType().Name} " + $"for {processor.Target.gameObject.name}"); Object.DestroyImmediate(processor.Component); + Profiler.EndSample(); } + Profiler.EndSample(); } } } diff --git a/Editor/Processors/MakeChildrenProcessor.cs b/Editor/Processors/MakeChildrenProcessor.cs index 693481af7..349eed6b1 100644 --- a/Editor/Processors/MakeChildrenProcessor.cs +++ b/Editor/Processors/MakeChildrenProcessor.cs @@ -1,5 +1,6 @@ using Anatawa12.AvatarOptimizer.ErrorReporting; using System.Linq; +using nadena.dev.ndmf; using UnityEngine; namespace Anatawa12.AvatarOptimizer.Processors @@ -13,9 +14,9 @@ public MakeChildrenProcessor(bool early) _early = early; } - public void Process(OptimizerSession session) + public void Process(BuildContext context) { - BuildReport.ReportingObjects(session.GetComponents(), makeChildren => + BuildReport.ReportingObjects(context.GetComponents(), makeChildren => { if (makeChildren.executeEarly != _early) return; foreach (var makeChildrenChild in makeChildren.children.GetAsSet().Where(x => x)) diff --git a/Editor/Processors/MergeBoneProcessor.cs b/Editor/Processors/MergeBoneProcessor.cs index 0256cb223..f4b8b22b3 100644 --- a/Editor/Processors/MergeBoneProcessor.cs +++ b/Editor/Processors/MergeBoneProcessor.cs @@ -4,13 +4,14 @@ using Anatawa12.AvatarOptimizer.ErrorReporting; using Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes; using JetBrains.Annotations; +using nadena.dev.ndmf; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace Anatawa12.AvatarOptimizer.Processors { - internal class MergeBoneProcessor + internal class MergeBoneProcessor : Pass { [InitializeOnLoadMethod] private static void RegisterValidator() @@ -51,11 +52,11 @@ public static bool ScaledEvenly(Vector3 localScale) CheckScale(localScale.y / localScale.z); } - public void Process(OptimizerSession session) + protected override void Execute(BuildContext context) { // merge from -> merge into var mergeMapping = new Dictionary(); - foreach (var component in session.GetComponents()) + foreach (var component in context.GetComponents()) { var transform = component.transform; mergeMapping[transform] = transform.parent; @@ -66,9 +67,9 @@ public void Process(OptimizerSession session) if (mergeMapping.Count == 0) return; - BuildReport.ReportingObjects(session.GetComponents(), renderer => + BuildReport.ReportingObjects(context.GetComponents(), renderer => { - var meshInfo2 = session.MeshInfo2Holder.GetMeshInfoFor(renderer); + var meshInfo2 = context.GetMeshInfoFor(renderer); if (meshInfo2.Bones.Any(x => x.Transform && mergeMapping.ContainsKey(x.Transform))) DoBoneMap2(meshInfo2, mergeMapping); }); diff --git a/Editor/Processors/MergePhysBoneProcessor.cs b/Editor/Processors/MergePhysBoneProcessor.cs index 0a96da6a9..1b8c74157 100644 --- a/Editor/Processors/MergePhysBoneProcessor.cs +++ b/Editor/Processors/MergePhysBoneProcessor.cs @@ -1,7 +1,10 @@ +#if AAO_VRCSDK3_AVATARS + using System; using System.Collections.Generic; using System.Linq; using Anatawa12.AvatarOptimizer.ErrorReporting; +using nadena.dev.ndmf; using UnityEditor; using UnityEngine; using VRC.Dynamics; @@ -10,13 +13,15 @@ namespace Anatawa12.AvatarOptimizer.Processors { - internal class MergePhysBoneProcessor + internal class MergePhysBoneProcessor : Pass { - public void Process(OptimizerSession session) + public override string DisplayName => "MergePhysBone"; + + protected override void Execute(BuildContext context) { - BuildReport.ReportingObjects(session.GetComponents(), mergePhysBone => + BuildReport.ReportingObjects(context.GetComponents(), mergePhysBone => { - DoMerge(mergePhysBone, session); + DoMerge(mergePhysBone); Object.DestroyImmediate(mergePhysBone); }); } @@ -24,7 +29,7 @@ public void Process(OptimizerSession session) private static bool SetEq(IEnumerable a, IEnumerable b) => new HashSet(a).SetEquals(b); - internal static void DoMerge(MergePhysBone merge, OptimizerSession session) + internal static void DoMerge(MergePhysBone merge) { var sourceComponents = merge.componentsSet.GetAsList(); if (sourceComponents.Count == 0) return; @@ -204,3 +209,5 @@ protected override void CollidersProp(string label, CollidersConfigProp prop) } } } + +#endif \ No newline at end of file diff --git a/Editor/Processors/MeshInfo2Holder.cs b/Editor/Processors/MeshInfo2Holder.cs index fba2af777..a6ec5b533 100644 --- a/Editor/Processors/MeshInfo2Holder.cs +++ b/Editor/Processors/MeshInfo2Holder.cs @@ -2,10 +2,30 @@ using System.Linq; using Anatawa12.AvatarOptimizer.ErrorReporting; using Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes; +using JetBrains.Annotations; +using nadena.dev.ndmf; using UnityEngine; +using UnityEngine.Profiling; +using Debug = System.Diagnostics.Debug; namespace Anatawa12.AvatarOptimizer.Processors { + internal class MeshInfo2Context : IExtensionContext + { + [CanBeNull] public MeshInfo2Holder Holder { get; private set; } + public void OnActivate(BuildContext context) + { + Holder = new MeshInfo2Holder(context.AvatarRootObject); + } + + public void OnDeactivate(BuildContext context) + { + Debug.Assert(Holder != null, nameof(Holder) + " != null"); + Holder.SaveToMesh(); + Holder = null; + } + } + internal class MeshInfo2Holder { private readonly Dictionary _skinnedCache = @@ -18,10 +38,18 @@ public MeshInfo2Holder(GameObject rootObject) var avatarTagComponent = rootObject.GetComponentInChildren(true); if (avatarTagComponent == null) return; foreach (var renderer in rootObject.GetComponentsInChildren(true)) + { + Profiler.BeginSample($"Read Skinned Mesh"); GetMeshInfoFor(renderer); + Profiler.EndSample(); + } foreach (var renderer in rootObject.GetComponentsInChildren(true)) + { + Profiler.BeginSample($"Read Static Mesh"); GetMeshInfoFor(renderer); + Profiler.EndSample(); + } } public MeshInfo2 GetMeshInfoFor(SkinnedMeshRenderer renderer) => @@ -35,14 +63,16 @@ public MeshInfo2 GetMeshInfoFor(MeshRenderer renderer) => ? cached : _staticCache[renderer] = new MeshInfo2(renderer); - public void SaveToMesh(OptimizerSession session) + public void SaveToMesh() { foreach (var keyValuePair in _skinnedCache) { var targetRenderer = keyValuePair.Key; if (!targetRenderer) continue; - keyValuePair.Value.WriteToSkinnedMeshRenderer(targetRenderer, session); + Profiler.BeginSample($"Save Skinned Mesh {targetRenderer.name}"); + keyValuePair.Value.WriteToSkinnedMeshRenderer(targetRenderer); + Profiler.EndSample(); } foreach (var keyValuePair in _staticCache) @@ -52,6 +82,7 @@ public void SaveToMesh(OptimizerSession session) var meshInfo = keyValuePair.Value; var meshFilter = targetRenderer.GetComponent(); + Profiler.BeginSample($"Save Static Mesh {targetRenderer.name}"); BuildReport.ReportingObject(targetRenderer, () => { var mesh = new Mesh { name = $"AAOGeneratedMesh{targetRenderer.name}" }; @@ -59,6 +90,7 @@ public void SaveToMesh(OptimizerSession session) meshFilter.sharedMesh = mesh; targetRenderer.sharedMaterials = meshInfo.SubMeshes.Select(x => x.SharedMaterial).ToArray(); }); + Profiler.EndSample(); } } } diff --git a/Editor/Processors/OptimizerPlugin.cs b/Editor/Processors/OptimizerPlugin.cs deleted file mode 100644 index 4d7094578..000000000 --- a/Editor/Processors/OptimizerPlugin.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using Anatawa12.AvatarOptimizer.ErrorReporting; -using Anatawa12.AvatarOptimizer.ndmf; -using Anatawa12.AvatarOptimizer.Processors; -using nadena.dev.ndmf; -using nadena.dev.ndmf.builtin; - -[assembly: ExportsPlugin(typeof(OptimizerPlugin))] - -namespace Anatawa12.AvatarOptimizer.ndmf -{ - internal class OptimizerContext : IExtensionContext - { - internal OptimizerSession session; - - public void OnActivate(BuildContext context) - { - session = new OptimizerSession(context.AvatarRootObject, false); - } - - public void OnDeactivate(BuildContext context) - { - session.SaveMeshInfo2(); - } - } - - internal class OptimizerPlugin : Plugin - { - public override string DisplayName => "Anatawa12's Avatar Optimizer"; - - public override string QualifiedName => "com.anatawa12.avatar-optimizer"; - - protected override void Configure() - { - // Run early steps before EditorOnly objects are purged - InPhase(BuildPhase.Resolving) - .WithRequiredExtensions(new [] {typeof(OptimizerContext), typeof(BuildReportContext)}, seq => - { - seq.Run("Info if AAO is Out of Date", ctx => - { - // we skip check for update - var components = ctx.AvatarRootObject.GetComponentInChildren(true); - if (components && CheckForUpdate.OutOfDate) - BuildReport.LogInfo("CheckForUpdate:out-of-date", - CheckForUpdate.LatestVersionName, CheckForUpdate.CurrentVersionName); - }) - .Then.Run("Early: UnusedBonesByReference", - ctx => new Processors.UnusedBonesByReferencesToolEarlyProcessor().Process(ctx) - ) - .Then.Run("Early: MakeChildren", - ctx => new Processors.MakeChildrenProcessor(early: true).Process(ctx) - ) - .BeforePass(RemoveEditorOnlyPass.Instance); - }); - - // Run everything else in the optimize phase - InPhase(BuildPhase.Optimizing) - .WithRequiredExtensions(new [] {typeof(OptimizerContext), typeof(BuildReportContext)}, seq => - { - seq.Run("TraceAndOptimize", - ctx => - { - ctx.GetState().Process(ctx); - }) - .Then.Run("ClearEndpointPosition", - ctx => new Processors.ClearEndpointPositionProcessor().Process(ctx) - ) - .Then.Run("MergePhysBone", - ctx => new Processors.MergePhysBoneProcessor().Process(ctx) - ) - .Then.Run("EditSkinnedMeshComponent", - ctx => new Processors.EditSkinnedMeshComponentProcessor().Process(ctx) - ) - .Then.Run("MakeChildrenProcessor", - ctx => new Processors.MakeChildrenProcessor(early: false).Process(ctx) - ) - .Then.Run("TraceAndOptimize:ProcessLater", - ctx => ctx.GetState().ProcessLater(ctx)) - .Then.Run("MergeBoneProcessor", ctx => new Processors.MergeBoneProcessor().Process(ctx)) - .Then.Run("ApplyObjectMapping", - ctx => new Processors.ApplyObjectMapping().Apply(ctx) - ); - }); - } - - protected override void OnUnhandledException(Exception e) - { - BuildReport.ReportInternalError(e); - } - } -} diff --git a/Editor/Processors/SkinnedMeshes/EditSkinnedMeshProcessor.cs b/Editor/Processors/SkinnedMeshes/EditSkinnedMeshProcessor.cs index 4557db1fc..6e166d9b8 100644 --- a/Editor/Processors/SkinnedMeshes/EditSkinnedMeshProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/EditSkinnedMeshProcessor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using nadena.dev.ndmf; using UnityEngine; namespace Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes @@ -22,7 +23,7 @@ protected EditSkinnedMeshProcessor(TComponent component) Target = component.GetComponent(); } - public abstract void Process(OptimizerSession session, MeshInfo2 target); + public abstract void Process(BuildContext context, MeshInfo2 target); public abstract IMeshInfoComputer GetComputer(IMeshInfoComputer upstream); @@ -42,7 +43,7 @@ internal interface IEditSkinnedMeshProcessor IEnumerable Dependencies { get; } SkinnedMeshRenderer Target { get; } EditSkinnedMeshComponent Component { get; } - void Process(OptimizerSession session, MeshInfo2 target); + void Process(BuildContext context, MeshInfo2 target); [NotNull] IMeshInfoComputer GetComputer([NotNull] IMeshInfoComputer upstream); } diff --git a/Editor/Processors/SkinnedMeshes/FreezeBlendShapeProcessor.cs b/Editor/Processors/SkinnedMeshes/FreezeBlendShapeProcessor.cs index 6642f68bb..b6f963fe6 100644 --- a/Editor/Processors/SkinnedMeshes/FreezeBlendShapeProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/FreezeBlendShapeProcessor.cs @@ -1,7 +1,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using nadena.dev.ndmf; using UnityEngine; +using UnityEngine.Profiling; namespace Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes { @@ -13,14 +15,14 @@ public FreezeBlendShapeProcessor(FreezeBlendShape component) : base(component) public override EditSkinnedMeshProcessorOrder ProcessOrder => EditSkinnedMeshProcessorOrder.AfterRemoveMesh; - public override void Process(OptimizerSession session, MeshInfo2 target) + public override void Process(BuildContext context, MeshInfo2 target) { - FreezeBlendShapes(Target, session, target, Component.FreezingShapeKeys); + FreezeBlendShapes(Target, context, target, Component.FreezingShapeKeys); } public static void FreezeBlendShapes( SkinnedMeshRenderer targetSMR, - OptimizerSession session, + BuildContext context, MeshInfo2 target, HashSet freezeNames ) @@ -29,22 +31,31 @@ HashSet freezeNames for (var i = 0; i < target.BlendShapes.Count; i++) freezes[i] = freezeNames.Contains(target.BlendShapes[i].name); + Profiler.BeginSample("DoFreezeBlendShape"); foreach (var vertex in target.Vertices) { for (var i = 0; i < target.BlendShapes.Count; i++) { if (!freezes[i]) continue; var (name, weight) = target.BlendShapes[i]; - if (!vertex.TryGetBlendShape(name, weight, out var position, out var normal, out var tangent)) continue; + Profiler.BeginSample("TryGetBlendShape"); + var result = + vertex.TryGetBlendShape(name, weight, out var position, out var normal, out var tangent); + Profiler.EndSample(); + if (!result) continue; + Profiler.BeginSample("Apply offsets"); vertex.Position += position; vertex.Normal += normal; tangent += (Vector3)vertex.Tangent; vertex.Tangent = new Vector4(tangent.x, tangent.y, tangent.z, vertex.Tangent.w); vertex.BlendShapes.Remove(name); + Profiler.EndSample(); } } + Profiler.EndSample(); + Profiler.BeginSample("MoveProperties"); { int srcI = 0, dstI = 0; for (; srcI < target.BlendShapes.Count; srcI++) @@ -52,21 +63,20 @@ HashSet freezeNames if (!freezes[srcI]) { // for keep prop: move the BlendShape index. name is not changed. - session.MappingBuilder.RecordMoveProperty(targetSMR, - VProp.BlendShapeIndex(srcI), - VProp.BlendShapeIndex(dstI)); + context.RecordMoveProperty(targetSMR, VProp.BlendShapeIndex(srcI), VProp.BlendShapeIndex(dstI)); target.BlendShapes[dstI++] = target.BlendShapes[srcI]; } else { // for frozen prop: remove that BlendShape - session.MappingBuilder.RecordRemoveProperty(targetSMR, VProp.BlendShapeIndex(srcI)); - session.MappingBuilder.RecordRemoveProperty(targetSMR, $"blendShape.{target.BlendShapes[srcI].name}"); + context.RecordRemoveProperty(targetSMR, VProp.BlendShapeIndex(srcI)); + context.RecordRemoveProperty(targetSMR, $"blendShape.{target.BlendShapes[srcI].name}"); } } target.BlendShapes.RemoveRange(dstI, target.BlendShapes.Count - dstI); } + Profiler.EndSample(); } public override IMeshInfoComputer GetComputer(IMeshInfoComputer upstream) => new MeshInfoComputer(this, upstream); diff --git a/Editor/Processors/SkinnedMeshes/InternalAutoFreezeMeaninglessBlendShapeProcessor.cs b/Editor/Processors/SkinnedMeshes/InternalAutoFreezeMeaninglessBlendShapeProcessor.cs index f97399402..2e472580b 100644 --- a/Editor/Processors/SkinnedMeshes/InternalAutoFreezeMeaninglessBlendShapeProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/InternalAutoFreezeMeaninglessBlendShapeProcessor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using nadena.dev.ndmf; using UnityEditor; namespace Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes @@ -12,19 +13,19 @@ public InternalAutoFreezeMeaninglessBlendShapeProcessor(InternalAutoFreezeMeanin public override EditSkinnedMeshProcessorOrder ProcessOrder => EditSkinnedMeshProcessorOrder.AutoConfigureFreezeBlendShape; - public override void Process(OptimizerSession session, MeshInfo2 target) + public override void Process(BuildContext context, MeshInfo2 target) { - var meaningfulBlendShapes = new HashSet(); - if (session.PreserveBlendShapes.TryGetValue(Target, out var preserve)) - meaningfulBlendShapes.UnionWith(preserve); + var meaninglessBlendShapes = new HashSet(target.BlendShapes.Select(x => x.name)); + var state = context.GetState(); + if (state.PreserveBlendShapes.TryGetValue(Target, out var preserve)) + meaninglessBlendShapes.ExceptWith(preserve); foreach (var vertex in target.Vertices) - meaningfulBlendShapes.UnionWith(vertex.BlendShapes.Keys); + meaninglessBlendShapes.ExceptWith(vertex.BlendShapes.Keys); var freezeBlendShape = Target.GetComponent(); var set = freezeBlendShape.shapeKeysSet.GetAsSet(); - set.UnionWith(target.BlendShapes.Where(x => !meaningfulBlendShapes.Contains(x.name)) - .Select(x => x.name)); + set.UnionWith(meaninglessBlendShapes); freezeBlendShape.shapeKeysSet.SetValueNonPrefab(set); } diff --git a/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs b/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs index 55d9aeada..c4628c12f 100644 --- a/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Linq; using Anatawa12.AvatarOptimizer.ErrorReporting; +using nadena.dev.ndmf; using UnityEditor; using UnityEditor.Animations; using UnityEngine; +using UnityEngine.Profiling; using Object = UnityEngine.Object; namespace Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes @@ -25,27 +27,32 @@ public MergeSkinnedMeshProcessor(MergeSkinnedMesh component) : base(component) public override EditSkinnedMeshProcessorOrder ProcessOrder => EditSkinnedMeshProcessorOrder.Generation; - public override void Process(OptimizerSession session, MeshInfo2 target) + public override void Process(BuildContext context, MeshInfo2 target) { var skinnedMeshRenderers = SkinnedMeshRenderers.ToList(); var staticMeshRenderers = StaticMeshRenderers.ToList(); + Profiler.BeginSample("Merge PreserveBlendShapes"); { + var state = context.GetState(); HashSet thisPreserve = null; foreach (var skinnedRenderer in skinnedMeshRenderers) { - if (!session.PreserveBlendShapes.TryGetValue(skinnedRenderer, out var preserve)) continue; + if (!state.PreserveBlendShapes.TryGetValue(skinnedRenderer, out var preserve)) continue; - if (thisPreserve == null && !session.PreserveBlendShapes.TryGetValue(Target, out thisPreserve)) - session.PreserveBlendShapes.Add(Target, thisPreserve = new HashSet()); + if (thisPreserve == null && !state.PreserveBlendShapes.TryGetValue(Target, out thisPreserve)) + state.PreserveBlendShapes.Add(Target, thisPreserve = new HashSet()); thisPreserve.UnionWith(preserve); } } - - var meshInfos = skinnedMeshRenderers.Select(session.MeshInfo2Holder.GetMeshInfoFor) - .Concat(staticMeshRenderers.Select(session.MeshInfo2Holder.GetMeshInfoFor)) + Profiler.EndSample(); + Profiler.BeginSample("Collect MeshInfos"); + var meshInfos = skinnedMeshRenderers.Select(context.GetMeshInfoFor) + .Concat(staticMeshRenderers.Select(context.GetMeshInfoFor)) .ToArray(); var sourceMaterials = meshInfos.Select(x => x.SubMeshes.Select(y => y.SharedMaterial).ToArray()).ToArray(); + Profiler.EndSample(); + Profiler.BeginSample("Material Normal Configuration Check"); // check normal information. int hasNormal = 0; foreach (var meshInfo2 in meshInfos) @@ -75,8 +82,11 @@ public override void Process(OptimizerSession session, MeshInfo2 target) BuildReport.LogFatal("MergeSkinnedMesh:error:mix-normal-existence") ?.WithContext((object[])meshesWithoutNormals.ToArray()); } + Profiler.EndSample(); + Profiler.BeginSample("Merge Material Indices"); var (subMeshIndexMap, materials) = CreateMergedMaterialsAndSubMeshIndexMapping(sourceMaterials); + Profiler.EndSample(); var sourceRootBone = target.RootBone; var updateBounds = sourceRootBone && target.Bounds == default; @@ -97,6 +107,7 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => for (var i = 0; i < meshInfos.Length; i++) { + Profiler.BeginSample($"Process MeshInfo#{i}"); var meshInfo = meshInfos[i]; mappings.Clear(); @@ -153,7 +164,7 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => } - session.MappingBuilder.RecordMoveProperties(meshInfo.SourceRenderer, mappings.ToArray()); + context.RecordMoveProperties(meshInfo.SourceRenderer, mappings.ToArray()); target.RootBone = sourceRootBone; target.Bones.AddRange(meshInfo.Bones); @@ -163,13 +174,16 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => target.HasTangent |= meshInfo.HasTangent; target.AssertInvariantContract($"processing meshInfo {Target.gameObject.name}"); + Profiler.EndSample(); } #if !UNITY_2021_2_OR_NEWER + Profiler.BeginSample("ShiftIndex Check"); // material slot #4 should not be animated to avoid Unity bug // https://issuetracker.unity3d.com/issues/material-is-applied-to-two-slots-when-applying-material-to-a-single-slot-while-recording-animation const int SubMeshIndexToShiftIfAnimated = 4; - bool shouldShiftSubMeshIndex = CheckAnimateSubMeshIndex(session, meshInfos, subMeshIndexMap, SubMeshIndexToShiftIfAnimated); + bool shouldShiftSubMeshIndex = CheckAnimateSubMeshIndex(context, meshInfos, subMeshIndexMap, SubMeshIndexToShiftIfAnimated); + Profiler.EndSample(); #endif foreach (var weightMismatchBlendShape in weightMismatchBlendShapes) @@ -182,6 +196,7 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => var boneTransforms = new HashSet(target.Bones.Select(x => x.Transform)); + Profiler.BeginSample("Postprocess Source Renderers"); foreach (var renderer in SkinnedMeshRenderers) { // Avatars can have animation to hide source meshes. @@ -190,8 +205,8 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => // This often be a unexpected behavior so we invalidate changing m_Enabled // property for original mesh in animation. // This invalidation doesn't affect to m_Enabled property of merged mesh. - session.MappingBuilder.RecordRemoveProperty(renderer, "m_Enabled"); - session.MappingBuilder.RecordMergeComponent(renderer, Target); + context.RecordRemoveProperty(renderer, "m_Enabled"); + context.RecordMergeComponent(renderer, Target); var rendererGameObject = renderer.gameObject; Object.DestroyImmediate(renderer); @@ -212,21 +227,24 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => Object.DestroyImmediate(renderer.GetComponent()); Object.DestroyImmediate(renderer); } + Profiler.EndSample(); #if !UNITY_2021_2_OR_NEWER if (shouldShiftSubMeshIndex) { + Profiler.BeginSample("ShiftIndex"); mappings.Clear(); for (var i = SubMeshIndexToShiftIfAnimated; i < target.SubMeshes.Count; i++) { mappings.Add(($"m_Materials.Array.data[{i}]", $"m_Materials.Array.data[{i + 1}]")); } - session.MappingBuilder.RecordMoveProperties(target.SourceRenderer, mappings.ToArray()); + context.RecordMoveProperties(target.SourceRenderer, mappings.ToArray()); target.SubMeshes.Insert(SubMeshIndexToShiftIfAnimated, new SubMesh()); target.AssertInvariantContract($"shifting meshInfo.SubMeshes {Target.gameObject.name}"); + Profiler.EndSample(); } #endif } @@ -263,30 +281,27 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => } #if !UNITY_2021_2_OR_NEWER - private bool CheckAnimateSubMeshIndex(OptimizerSession session, MeshInfo2[] meshInfos, int[][] subMeshIndexMap, int targetSubMeshIndex) + private bool CheckAnimateSubMeshIndex(BuildContext context, MeshInfo2[] meshInfos, int[][] subMeshIndexMap, int targetSubMeshIndex) { var targetProperties = new HashSet<(Object, string)>(subMeshIndexMap .SelectMany((x, i) => x.Select((y, j) => (renderer: meshInfos[i].SourceRenderer, srcSubMeshIndex: j, dstSubMeshIndex: y))) .Where(x => x.dstSubMeshIndex == targetSubMeshIndex) .Select(x => (x.renderer as Object, $"m_Materials.Array.data[{x.srcSubMeshIndex}]"))); - foreach (var component in session.GetComponents()) + foreach (var component in context.GetComponents()) { if (component is Transform) continue; - var serialized = new SerializedObject(component); - var prop = serialized.GetIterator(); - var enterChildren = true; - while (prop.Next(enterChildren)) + using (var serialized = new SerializedObject(component)) { - enterChildren = prop.propertyType == SerializedPropertyType.Generic; - - if (prop.propertyType == SerializedPropertyType.ObjectReference && - prop.objectReferenceValue is AnimatorController controller && - controller.animationClips + foreach (var prop in serialized.ObjectReferenceProperties()) + { + if (!(prop.objectReferenceValue is AnimatorController controller)) continue; + if (controller.animationClips .SelectMany(x => AnimationUtility.GetObjectReferenceCurveBindings(x)) .Select(x => (AnimationUtility.GetAnimatedObject(component.gameObject, x), x.propertyName)) .Any(targetProperties.Contains)) - return true; + return true; + } } } return false; diff --git a/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs b/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs index d57db9058..6b4cdd2f3 100644 --- a/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using nadena.dev.ndmf; using UnityEditor; using UnityEngine; using UnityEngine.Experimental.Rendering; @@ -26,7 +27,7 @@ public MergeToonLitMaterialProcessor(MergeToonLitMaterial component) : base(comp public override EditSkinnedMeshProcessorOrder ProcessOrder => EditSkinnedMeshProcessorOrder.AfterRemoveMesh; - public override void Process(OptimizerSession session, MeshInfo2 target) + public override void Process(BuildContext context, MeshInfo2 target) { // compute usages. AdditionalTemporal is usage count for now. // if #usages is not zero for merging triangles @@ -87,7 +88,7 @@ public override void Process(OptimizerSession session, MeshInfo2 target) var materials = target.SubMeshes.Select(x => x.SharedMaterial).ToArray(); var merged = Component.merges.Select(x => new SubMesh( x.source.SelectMany(src => target.SubMeshes[src.materialIndex].Triangles).ToList(), - CreateMaterial(GenerateTexture(x, materials, !session.IsTest)))); + CreateMaterial(GenerateTexture(x, materials, true)))); var subMeshes = copied.Concat(merged).ToList(); target.SubMeshes.Clear(); target.SubMeshes.AddRange(subMeshes); diff --git a/Editor/Processors/SkinnedMeshes/MeshInfo2.cs b/Editor/Processors/SkinnedMeshes/MeshInfo2.cs index a1c898118..bcd687b71 100644 --- a/Editor/Processors/SkinnedMeshes/MeshInfo2.cs +++ b/Editor/Processors/SkinnedMeshes/MeshInfo2.cs @@ -5,9 +5,12 @@ using System.Linq; using Anatawa12.AvatarOptimizer.ErrorReporting; using JetBrains.Annotations; +using Unity.Burst; using Unity.Collections; +using Unity.Jobs; using UnityEngine; using UnityEngine.Assertions; +using UnityEngine.Profiling; using UnityEngine.Rendering; using Debug = System.Diagnostics.Debug; @@ -127,6 +130,17 @@ public void ReadSkinnedMesh([NotNull] Mesh mesh) { ReadStaticMesh(mesh); + Profiler.BeginSample("Read Skinned Mesh Part"); + Profiler.BeginSample("Read Bones"); + ReadBones(mesh); + Profiler.EndSample(); + Profiler.BeginSample("Read BlendShapes"); + ReadBlendShapes(mesh); + Profiler.EndSample(); + } + + private void ReadBones([NotNull] Mesh mesh) + { Bones.Clear(); Bones.Capacity = Math.Max(Bones.Capacity, mesh.bindposes.Length); Bones.AddRange(mesh.bindposes.Select(x => new Bone(x))); @@ -142,46 +156,109 @@ public void ReadSkinnedMesh([NotNull] Mesh mesh) Vertices[i].BoneWeights.Add((Bones[boneWeight1.boneIndex], boneWeight1.weight)); bonesBase += count; } + } + private void ReadBlendShapes([NotNull] Mesh mesh) + { BlendShapes.Clear(); - var deltaVertices = new Vector3[Vertices.Count]; - var deltaNormals = new Vector3[Vertices.Count]; - var deltaTangents = new Vector3[Vertices.Count]; + Profiler.BeginSample("Prepare shared buffers"); + var maxFrames = 0; + var frameCounts = new NativeArray(mesh.blendShapeCount, Allocator.TempJob); + var shapeNames = new string[mesh.blendShapeCount]; for (var i = 0; i < mesh.blendShapeCount; i++) { - var shapeName = mesh.GetBlendShapeName(i); - - BlendShapes.Add((shapeName, 0.0f)); + var frames = mesh.GetBlendShapeFrameCount(i); + shapeNames[i] = mesh.GetBlendShapeName(i); + maxFrames = Math.Max(frames, maxFrames); + frameCounts[i] = frames; + } - var frameCount = mesh.GetBlendShapeFrameCount(i); + var deltaVertices = new Vector3[Vertices.Count]; + var deltaNormals = new Vector3[Vertices.Count]; + var deltaTangents = new Vector3[Vertices.Count]; + var allFramesBuffer = new NativeArray3(mesh.blendShapeCount, Vertices.Count, + maxFrames, Allocator.TempJob); + var meaningfuls = new NativeArray2(mesh.blendShapeCount, Vertices.Count, Allocator.TempJob); + Profiler.EndSample(); - var shapes = new Vertex.BlendShapeFrame[Vertices.Count][]; - for (var vertex = 0; vertex < shapes.Length; vertex++) - shapes[vertex] = new Vertex.BlendShapeFrame[frameCount]; + for (var blendShape = 0; blendShape < mesh.blendShapeCount; blendShape++) + { + BlendShapes.Add((shapeNames[blendShape], 0.0f)); - for (var frame = 0; frame < frameCount; frame++) + for (var frame = 0; frame < frameCounts[blendShape]; frame++) { - mesh.GetBlendShapeFrameVertices(i, frame, deltaVertices, deltaNormals, deltaTangents); - var weight = mesh.GetBlendShapeFrameWeight(i, frame); + Profiler.BeginSample("GetFrameInfo"); + mesh.GetBlendShapeFrameVertices(blendShape, frame, deltaVertices, deltaNormals, deltaTangents); + var weight = mesh.GetBlendShapeFrameWeight(blendShape, frame); + Profiler.EndSample(); + Profiler.BeginSample("Copy to buffer"); for (var vertex = 0; vertex < deltaNormals.Length; vertex++) { var deltaVertex = deltaVertices[vertex]; var deltaNormal = deltaNormals[vertex]; var deltaTangent = deltaTangents[vertex]; - shapes[vertex][frame] = - new Vertex.BlendShapeFrame(weight, deltaVertex, deltaNormal, deltaTangent); + allFramesBuffer[blendShape, vertex, frame] = new Vertex.BlendShapeFrame(weight, deltaVertex, deltaNormal, deltaTangent); } + Profiler.EndSample(); } + } - for (var vertex = 0; vertex < shapes.Length; vertex++) + Profiler.BeginSample("Compute Meaningful with Job"); + new ComputeMeaningfulJob + { + vertexCount = Vertices.Count, + allFramesBuffer = allFramesBuffer, + frameCounts = frameCounts, + meaningfuls = meaningfuls, + }.Schedule(Vertices.Count * mesh.blendShapeCount, 1).Complete(); + Profiler.EndSample(); + + for (var blendShape = 0; blendShape < mesh.blendShapeCount; blendShape++) + { + Profiler.BeginSample("Save to Vertices"); + for (var vertex = 0; vertex < Vertices.Count; vertex++) { - if (IsMeaningful(shapes[vertex])) - Vertices[vertex].BlendShapes[shapeName] = shapes[vertex]; + if (meaningfuls[blendShape, vertex]) + { + Profiler.BeginSample("Clone BlendShapes"); + var slice = allFramesBuffer[blendShape, vertex].Slice(0, frameCounts[blendShape]); + Vertices[vertex].BlendShapes[shapeNames[blendShape]] = slice.ToArray(); + Profiler.EndSample(); + } } + Profiler.EndSample(); + } + + meaningfuls.Dispose(); + frameCounts.Dispose(); + allFramesBuffer.Dispose(); + Profiler.EndSample(); + } + + [BurstCompile] + struct ComputeMeaningfulJob : IJobParallelFor + { + public int vertexCount; + + // allFramesBuffer[blendShape][vertex][frame] + [ReadOnly] + public NativeArray3 allFramesBuffer; + [ReadOnly] + public NativeArray frameCounts; + // allFramesBuffer[blendShape][vertex] + [WriteOnly] + public NativeArray2 meaningfuls; + + public void Execute(int index) + { + var blendShape = index / vertexCount; + var vertex = index % vertexCount; + var slice = allFramesBuffer[blendShape, vertex].Slice(0, frameCounts[blendShape]); + meaningfuls[blendShape, vertex] = IsMeaningful(slice); } - bool IsMeaningful(Vertex.BlendShapeFrame[] frames) + bool IsMeaningful(NativeSlice frames) { foreach (var (_, position, normal, tangent) in frames) { @@ -196,6 +273,7 @@ bool IsMeaningful(Vertex.BlendShapeFrame[] frames) public void ReadStaticMesh([NotNull] Mesh mesh) { + Profiler.BeginSample($"Read Static Mesh Part"); Vertices.Capacity = Math.Max(Vertices.Capacity, mesh.vertexCount); Vertices.Clear(); for (var i = 0; i < mesh.vertexCount; i++) Vertices.Add(new Vertex()); @@ -250,6 +328,7 @@ public void ReadStaticMesh([NotNull] Mesh mesh) SubMeshes.Capacity = Math.Max(SubMeshes.Capacity, mesh.subMeshCount); for (var i = 0; i < mesh.subMeshCount; i++) SubMeshes.Add(new SubMesh(Vertices, triangles, mesh.GetSubMesh(i))); + Profiler.EndSample(); } void CopyVertexAttr(T[] attributes, Action assign) @@ -312,6 +391,9 @@ public void WriteToMesh(Mesh destMesh) Optimize(); destMesh.Clear(); + Profiler.BeginSample("Write to Mesh"); + + Profiler.BeginSample("Vertices and Normals"); // Basic Vertex Attributes: vertices, normals { var vertices = new Vector3[Vertices.Count]; @@ -328,14 +410,17 @@ public void WriteToMesh(Mesh destMesh) normals[i] = Vertices[i].Normal; destMesh.normals = normals; } + Profiler.EndSample(); // tangents if (HasTangent) { + Profiler.BeginSample("Tangents"); var tangents = new Vector4[Vertices.Count]; for (var i = 0; i < Vertices.Count; i++) tangents[i] = Vertices[i].Tangent; destMesh.tangents = tangents; + Profiler.EndSample(); } // UVs @@ -345,6 +430,7 @@ public void WriteToMesh(Mesh destMesh) var uv4 = new Vector4[Vertices.Count]; for (var uvIndex = 0; uvIndex < 8; uvIndex++) { + Profiler.BeginSample($"UV#{uvIndex}"); switch (GetTexCoordStatus(uvIndex)) { case TexCoordStatus.NotDefined: @@ -368,22 +454,26 @@ public void WriteToMesh(Mesh destMesh) default: throw new ArgumentOutOfRangeException(); } + Profiler.EndSample(); } } // color if (HasColor) { + Profiler.BeginSample($"Vertex Color"); var colors = new Color32[Vertices.Count]; for (var i = 0; i < Vertices.Count; i++) colors[i] = Vertices[i].Color; destMesh.colors32 = colors; + Profiler.EndSample(); } // bones destMesh.bindposes = Bones.Select(x => x.Bindpose.ToUnity()).ToArray(); // triangles and SubMeshes + Profiler.BeginSample("Triangles"); { var vertexIndices = new Dictionary(); // first, set vertex indices @@ -419,9 +509,11 @@ public void WriteToMesh(Mesh destMesh) for (var i = 0; i < SubMeshes.Count; i++) destMesh.SetSubMesh(i, subMeshDescriptors[i]); } + Profiler.EndSample(); // BoneWeights if (Vertices.Any(x => x.BoneWeights.Count != 0)){ + Profiler.BeginSample("BoneWeights"); var boneIndices = new Dictionary(); for (var i = 0; i < Bones.Count; i++) boneIndices.Add(Bones[i], i); @@ -440,11 +532,13 @@ public void WriteToMesh(Mesh destMesh) } destMesh.SetBoneWeights(bonesPerVertex, allBoneWeights); + Profiler.EndSample(); } // BlendShapes if (BlendShapes.Count != 0) { + Profiler.BeginSample("BlendShapes"); for (var i = 0; i < BlendShapes.Count; i++) { Debug.Assert(destMesh.blendShapeCount == i, "Unexpected state: blend shape count"); @@ -484,10 +578,12 @@ public void WriteToMesh(Mesh destMesh) destMesh.AddBlendShapeFrame(shapeName, weight, positions, normals, tangents); } } + Profiler.EndSample(); } + Profiler.EndSample(); } - public void WriteToSkinnedMeshRenderer(SkinnedMeshRenderer targetRenderer, OptimizerSession session) + public void WriteToSkinnedMeshRenderer(SkinnedMeshRenderer targetRenderer) { BuildReport.ReportingObject(targetRenderer, () => { diff --git a/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs b/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs index aef705724..95ae1d4e0 100644 --- a/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using nadena.dev.ndmf; namespace Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes { @@ -12,7 +13,7 @@ public RemoveMeshByBlendShapeProcessor(RemoveMeshByBlendShape component) : base( // This needs to be less than FreezeBlendshapeProcessor.ProcessOrder. public override EditSkinnedMeshProcessorOrder ProcessOrder => EditSkinnedMeshProcessorOrder.RemovingMesh; - public override void Process(OptimizerSession session, MeshInfo2 target) + public override void Process(BuildContext context, MeshInfo2 target) { var byBlendShapeVertices = new HashSet(); var sqrTolerance = Component.tolerance * Component.tolerance; @@ -51,7 +52,7 @@ public override void Process(OptimizerSession session, MeshInfo2 target) target.Vertices.RemoveAll(x => byBlendShapeVertices.Contains(x)); // remove the blend shapes - FreezeBlendShapeProcessor.FreezeBlendShapes(Target, session, target, Component.RemovingShapeKeys); + FreezeBlendShapeProcessor.FreezeBlendShapes(Target, context, target, Component.RemovingShapeKeys); } public override IMeshInfoComputer GetComputer(IMeshInfoComputer upstream) => new MeshInfoComputer(this, upstream); diff --git a/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs b/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs index 1465a2e8b..fef449231 100644 --- a/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using nadena.dev.ndmf; using UnityEngine; namespace Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes @@ -12,7 +13,7 @@ public RemoveMeshInBoxProcessor(RemoveMeshInBox component) : base(component) public override EditSkinnedMeshProcessorOrder ProcessOrder => EditSkinnedMeshProcessorOrder.RemovingMesh; - public override void Process(OptimizerSession session, MeshInfo2 target) + public override void Process(BuildContext context, MeshInfo2 target) { var inBoxVertices = new HashSet(); // Vertex.AdditionalTemporal: 0 if in box, 1 if out of box diff --git a/Editor/Processors/TraceAndOptimize/AnimatorParser.cs b/Editor/Processors/TraceAndOptimize/AnimatorParser.cs index 75804cd88..b25adce55 100644 --- a/Editor/Processors/TraceAndOptimize/AnimatorParser.cs +++ b/Editor/Processors/TraceAndOptimize/AnimatorParser.cs @@ -1,45 +1,49 @@ using System; using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.API; +using Anatawa12.AvatarOptimizer.APIInternal; using static Anatawa12.AvatarOptimizer.ErrorReporting.BuildReport; using JetBrains.Annotations; +using nadena.dev.ndmf; using UnityEditor.Animations; using UnityEngine; using UnityEngine.Animations; + +#if AAO_VRCSDK3_AVATARS using VRC.Dynamics; using VRC.SDK3.Avatars.Components; using VRC.SDKBase; +#endif namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes { class AnimatorParser { private bool mmdWorldCompatibility; - private bool advancedAnimatorParser; private AnimationParser _animationParser = new AnimationParser(); - public AnimatorParser(bool mmdWorldCompatibility, bool advancedAnimatorParser) + public AnimatorParser(bool mmdWorldCompatibility) { this.mmdWorldCompatibility = mmdWorldCompatibility; - this.advancedAnimatorParser = advancedAnimatorParser; } - public AnimatorParser(TraceAndOptimize config) + public AnimatorParser(TraceAndOptimizeState config) { - mmdWorldCompatibility = config.mmdWorldCompatibility; - advancedAnimatorParser = config.advancedAnimatorParser; + mmdWorldCompatibility = config.MmdWorldCompatibility; } - public ImmutableModificationsContainer GatherAnimationModifications(OptimizerSession session) + public ImmutableModificationsContainer GatherAnimationModifications(BuildContext context) { var modificationsContainer = new ModificationsContainer(); - modificationsContainer.MergeAsNewLayer(CollectAvatarRootAnimatorModifications(session), + + modificationsContainer.MergeAsNewLayer(CollectAvatarRootAnimatorModifications(context), weightState: AnimatorWeightState.AlwaysOne); - foreach (var child in session.GetRootComponent().DirectChildrenEnumerable()) + foreach (var child in context.AvatarRootTransform.DirectChildrenEnumerable()) WalkForAnimator(child, true, modificationsContainer); - OtherMutateComponents(modificationsContainer, session); + OtherMutateComponents(modificationsContainer, context); return modificationsContainer.ToImmutable(); } @@ -127,117 +131,57 @@ private void ParseAnimationOrAnimator( #region OtherComponents + private class Collector : ComponentMutationsCollector + { + private readonly ModificationsContainer _modifications; + + public Collector(ModificationsContainer modifications) => _modifications = modifications; + + public override void ModifyProperties(Component component, IEnumerable properties) + { + var updater = _modifications.ModifyObject(component); + foreach (var prop in properties) + updater.AddModificationAsNewLayer(prop, AnimationProperty.Variable()); + } + } + /// /// Collect modifications by non-animation changes. For example, constraints, PhysBones, and else /// - private void OtherMutateComponents(ModificationsContainer mod, OptimizerSession session) + private static void OtherMutateComponents(ModificationsContainer mod, BuildContext context) { - ReportingObjects(session.GetComponents(), component => + var collector = new Collector(mod); + ReportingObjects(context.GetComponents(), component => { - switch (component) - { - case VRCPhysBoneBase pb: - foreach (var transform in pb.GetAffectedTransforms()) - { - var updater = mod.ModifyObject(transform); - foreach (var prop in TransformPositionAnimationKeys.Concat(TransformRotationAnimationKeys)) - updater.AddModificationAsNewLayer(prop, AnimationProperty.Variable()); - } - - break; - case Rigidbody _: - case ParentConstraint _: - { - var updater = mod.ModifyObject(component.transform); - foreach (var prop in TransformPositionAnimationKeys.Concat(TransformRotationAnimationKeys)) - updater.AddModificationAsNewLayer(prop, AnimationProperty.Variable()); - break; - } - case AimConstraint _: - case LookAtConstraint _: - case RotationConstraint _: - { - var updater = mod.ModifyObject(component.transform); - foreach (var prop in TransformRotationAnimationKeys) - updater.AddModificationAsNewLayer(prop, AnimationProperty.Variable()); - break; - } - case PositionConstraint _: - { - var updater = mod.ModifyObject(component.transform); - foreach (var prop in TransformPositionAnimationKeys) - updater.AddModificationAsNewLayer(prop, AnimationProperty.Variable()); - break; - } - case ScaleConstraint _: - { - var updater = mod.ModifyObject(component.transform); - foreach (var prop in TransformScaleAnimationKeys) - updater.AddModificationAsNewLayer(prop, AnimationProperty.Variable()); - break; - } - case RemoveMeshByBlendShape removeMesh: - { - var blendShapes = removeMesh.RemovingShapeKeys; - { - var updater = mod.ModifyObject(removeMesh.GetComponent()); - foreach (var blendShape in blendShapes) - updater.AddModificationAsNewLayer($"blendShape.{blendShape}", - AnimationProperty.Variable()); - } - - DeriveMergeSkinnedMeshProperties(removeMesh.GetComponent()); - - void DeriveMergeSkinnedMeshProperties(MergeSkinnedMesh mergeSkinnedMesh) - { - if (mergeSkinnedMesh == null) return; - - foreach (var renderer in mergeSkinnedMesh.renderersSet.GetAsSet()) - { - var updater = mod.ModifyObject(renderer); - foreach (var blendShape in blendShapes) - updater.AddModificationAsNewLayer($"blendShape.{blendShape}", - AnimationProperty.Variable()); - - DeriveMergeSkinnedMeshProperties(renderer.GetComponent()); - } - } - - break; - } - default: - { - if (DynamicBone.TryCast(component, out var dynamicBone)) - { - // DynamicBone : similar to PhysBone - foreach (var transform in dynamicBone.GetAffectedTransforms()) - { - var updater = mod.ModifyObject(transform); - foreach (var prop in TransformRotationAnimationKeys) - updater.AddModificationAsNewLayer(prop, AnimationProperty.Variable()); - } - } - break; - } - // TODO: FinalIK - } + if (ComponentInfoRegistry.TryGetInformation(component.GetType(), out var info)) + info.CollectMutationsInternal(component, collector); }); } #endregion - #region AvatarDescriptor + #region Avatar Root Animator - private IModificationsContainer CollectAvatarRootAnimatorModifications(OptimizerSession session) + private IModificationsContainer CollectAvatarRootAnimatorModifications(BuildContext session) { - var animator = session.GetRootComponent(); - var descriptor = session.GetRootComponent(); - var modificationsContainer = new ModificationsContainer(); + var animator = session.AvatarRootObject.GetComponent(); if (animator) modificationsContainer = AddHumanoidModifications(modificationsContainer, animator).ToMutable(); + +#if AAO_VRCSDK3_AVATARS + var descriptor = session.AvatarRootObject.GetComponent(); + if (descriptor) + CollectAvatarDescriptorModifications(modificationsContainer, descriptor); +#endif + return modificationsContainer; + } + +#if AAO_VRCSDK3_AVATARS + private void CollectAvatarDescriptorModifications(ModificationsContainer modificationsContainer, VRCAvatarDescriptor descriptor) + { // process playable layers // see https://misskey.niri.la/notes/9ioemawdit // see https://creators.vrchat.com/avatars/playable-layers @@ -386,8 +330,6 @@ select mesh.GetBlendShapeName(index)) foreach (var shape in MmdBlendShapeNames) updater.AddModificationAsNewLayer($"blendShape.{shape}", AnimationProperty.Variable()); } - - return modificationsContainer; } private void CollectWeightChangesInController(RuntimeAnimatorController runtimeController, @@ -491,6 +433,7 @@ private static RuntimeAnimatorController GetPlayableLayerController(VRCAvatarDes throw new InvalidOperationException($"default controller for {layer.type} not found"); return controller; } +#endif #endregion @@ -522,30 +465,12 @@ public IModificationsContainer ParseAnimatorController(GameObject root, RuntimeA { return ReportingObject(controller, () => { - if (advancedAnimatorParser) - { - var (animatorController, mapping) = GetControllerAndOverrides(controller); - return AdvancedParseAnimatorController(root, animatorController, mapping, - externallyWeightChanged); - } - else - { - return FallbackParseAnimatorController(root, controller); - } + var (animatorController, mapping) = GetControllerAndOverrides(controller); + return AdvancedParseAnimatorController(root, animatorController, mapping, + externallyWeightChanged); }); } - /// - /// Fallback AnimatorController Parser but always assumed as partially applied. - /// This process assumes everything is applied as non-additive state motion. - /// This parsing MAY not correct with direct blendtree or additive layer - /// but it's extremely rare case so ignoring such case. - /// - private IModificationsContainer FallbackParseAnimatorController(GameObject root, RuntimeAnimatorController controller) - { - return controller.animationClips.Select(clip => _animationParser.GetParsedAnimation(root, clip)).MergeContainersSideBySide(); - } - internal IModificationsContainer AdvancedParseAnimatorController(GameObject root, AnimatorController controller, IReadOnlyDictionary mapping, [CanBeNull] AnimatorLayerWeightMap externallyWeightChanged) @@ -673,6 +598,7 @@ private IEnumerable CollectStates(AnimatorStateMachine stateMachi private static readonly string[] TransformScaleAnimationKeys = { "m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z" }; +#if AAO_VRCSDK3_AVATARS private static readonly AnimatorLayerMap> DefaultLayers = new AnimatorLayerMap> { @@ -693,6 +619,7 @@ private IEnumerable CollectStates(AnimatorStateMachine stateMachi // vrc_AvatarV3UtilityIKPose [VRCAvatarDescriptor.AnimLayerType.IKPose] = "a9b90a833b3486e4b82834c9d1f7c4ee" }; +#endif private static readonly string[] MmdBlendShapeNames = new [] { // New EN by Yi MMD World diff --git a/Editor/Processors/TraceAndOptimize/AnimatorParserDebugWindow.cs b/Editor/Processors/TraceAndOptimize/AnimatorParserDebugWindow.cs index 844289888..e70956092 100644 --- a/Editor/Processors/TraceAndOptimize/AnimatorParserDebugWindow.cs +++ b/Editor/Processors/TraceAndOptimize/AnimatorParserDebugWindow.cs @@ -1,9 +1,9 @@ using System; using System.Text; using JetBrains.Annotations; +using nadena.dev.ndmf; using UnityEditor; using UnityEngine; -using VRC.SDK3.Avatars.Components; using Object = UnityEngine.Object; namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes @@ -14,8 +14,8 @@ internal class AnimatorParserDebugWindow : EditorWindow private static void Open() => GetWindow("AnimatorParser Debug Window"); public ParserSource parserSource; - public VRCAvatarDescriptor avatar; public RuntimeAnimatorController animatorController; + public GameObject avatar; public GameObject rootGameObject; public Motion motion; @@ -75,7 +75,7 @@ private string CreateText() foreach (var (obj, properties) in Container.ModifiedProperties) { - var gameObject = obj.SelfOrAttachedGameObject.transform; + var gameObject = obj.gameObject.transform; resultText.Append(Utils.RelativePath(root, gameObject)).Append(": ") .Append(((Object)obj).GetType().FullName).Append('\n'); @@ -132,9 +132,9 @@ void OnParserSourceGUI() { if (GUILayout.Button("Parse") && avatar) { - parsedRootObject = avatar.gameObject; - Container = new AnimatorParser(true, true).GatherAnimationModifications( - new OptimizerSession(avatar.gameObject, true)); + parsedRootObject = avatar; + Container = new AnimatorParser(true).GatherAnimationModifications( + new BuildContext(avatar, null)); } } @@ -148,7 +148,7 @@ void OnParserSourceGUI() if (GUILayout.Button("Parse") && animatorController && rootGameObject) { parsedRootObject = rootGameObject; - Container = new AnimatorParser(true, true) + Container = new AnimatorParser(true) .ParseAnimatorController(rootGameObject, animatorController) .ToImmutable(); } diff --git a/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs b/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs index bfed5d0eb..411ffa485 100644 --- a/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs +++ b/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs @@ -1,45 +1,42 @@ using System; using System.Collections.Generic; using System.Linq; +using nadena.dev.ndmf; using UnityEditor; using UnityEngine; + +#if AAO_VRCSDK3_AVATARS using VRC.SDK3.Avatars.Components; using VRC.SDKBase; +#endif namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes { - class AutoFreezeBlendShape + internal class AutoFreezeBlendShape : Pass { - private readonly ImmutableModificationsContainer _modifications; - private readonly OptimizerSession _session; - private readonly HashSet _exclusions; + public override string DisplayName => "T&O: AutoFreezeBlendShape"; - public AutoFreezeBlendShape(ImmutableModificationsContainer modifications, OptimizerSession session, - HashSet exclusions) + protected override void Execute(BuildContext context) { - _modifications = modifications; - _session = session; - _exclusions = exclusions; - } + var state = context.GetState(); + if (!state.FreezeBlendShape) return; - public void Process(bool skipFreezingNonAnimatedBlendShape, bool skipFreezingMeaningless) - { - if (!skipFreezingNonAnimatedBlendShape) - FreezeNonAnimatedBlendShapes(); - if (!skipFreezingMeaningless) - FreezeMeaninglessBlendShapes(); + if (!state.SkipFreezingNonAnimatedBlendShape) + FreezeNonAnimatedBlendShapes(context, state); + if (!state.SkipFreezingMeaninglessBlendShape) + FreezeMeaninglessBlendShapes(context, state); } - void FreezeNonAnimatedBlendShapes() + void FreezeNonAnimatedBlendShapes(BuildContext context, TraceAndOptimizeState state) { // first optimization: unused blend shapes - foreach (var skinnedMeshRenderer in _session.GetComponents()) + foreach (var skinnedMeshRenderer in context.GetComponents()) { - if (_exclusions.Contains(skinnedMeshRenderer.gameObject)) continue; // manual exclusiton + if (state.Exclusions.Contains(skinnedMeshRenderer.gameObject)) continue; // manual exclusiton - var meshInfo = _session.MeshInfo2Holder.GetMeshInfoFor(skinnedMeshRenderer); + var meshInfo = context.GetMeshInfoFor(skinnedMeshRenderer); - var modifies = _modifications.GetModifiedProperties(skinnedMeshRenderer); + var modifies = state.Modifications.GetModifiedProperties(skinnedMeshRenderer); var unchanged = new HashSet(); @@ -75,32 +72,29 @@ bool IsUnchangedBlendShape(string name, float weight, out float newWeight) if (unchanged.Count == 0) continue; var freeze = skinnedMeshRenderer.gameObject.GetOrAddComponent(); - var serialized = new SerializedObject(freeze); - var editorUtil = PrefabSafeSet.EditorUtil.Create( - serialized.FindProperty(nameof(FreezeBlendShape.shapeKeysSet)), - 0, p => p.stringValue, (p, v) => p.stringValue = v); - foreach (var shape in unchanged) - editorUtil.GetElementOf(shape).EnsureAdded(); - serialized.ApplyModifiedPropertiesWithoutUndo(); + var shapeKeys = freeze.shapeKeysSet.GetAsSet(); + shapeKeys.UnionWith(unchanged); + freeze.shapeKeysSet.SetValueNonPrefab(shapeKeys); } } - void FreezeMeaninglessBlendShapes() { - ComputePreserveBlendShapes(_session.PreserveBlendShapes); + void FreezeMeaninglessBlendShapes(BuildContext context, TraceAndOptimizeState state) { + ComputePreserveBlendShapes(context, state.PreserveBlendShapes); // second optimization: remove meaningless blendShapes - foreach (var skinnedMeshRenderer in _session.GetComponents()) + foreach (var skinnedMeshRenderer in context.GetComponents()) { - if (_exclusions.Contains(skinnedMeshRenderer.gameObject)) continue; // manual exclusion + if (state.Exclusions.Contains(skinnedMeshRenderer.gameObject)) continue; // manual exclusion skinnedMeshRenderer.gameObject.GetOrAddComponent(); skinnedMeshRenderer.gameObject.GetOrAddComponent(); } } - private void ComputePreserveBlendShapes(Dictionary> preserveBlendShapes) + private void ComputePreserveBlendShapes(BuildContext context, Dictionary> preserveBlendShapes) { +#if AAO_VRCSDK3_AVATARS // some BlendShapes manipulated by VRC Avatar Descriptor must exists - var descriptor = _session.GetRootComponent(); + var descriptor = context.AvatarDescriptor; switch (descriptor.lipSync) { case VRC_AvatarDescriptor.LipSyncStyle.VisemeBlendShape when descriptor.VisemeSkinnedMesh != null: @@ -146,6 +140,7 @@ select mesh.GetBlendShapeName(index) break; } } +#endif } } } diff --git a/Editor/Processors/TraceAndOptimize/ComponentDependencyCollector.cs b/Editor/Processors/TraceAndOptimize/ComponentDependencyCollector.cs index f8a964b25..26712034f 100644 --- a/Editor/Processors/TraceAndOptimize/ComponentDependencyCollector.cs +++ b/Editor/Processors/TraceAndOptimize/ComponentDependencyCollector.cs @@ -1,639 +1,200 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; +using Anatawa12.AvatarOptimizer.APIInternal; using Anatawa12.AvatarOptimizer.ErrorReporting; +using Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes; using JetBrains.Annotations; +using nadena.dev.ndmf; using UnityEditor; using UnityEngine; -using UnityEngine.Animations; -using UnityEngine.Rendering; -using VRC.Core; -using VRC.Dynamics; -using VRC.SDK3; -using VRC.SDK3.Avatars.Components; -using VRC.SDK3.Dynamics.Contact.Components; -using VRC.SDK3.Dynamics.PhysBone.Components; -using VRC.SDKBase; +using Debug = System.Diagnostics.Debug; namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes { /// /// This class collects ALL dependencies of each component /// - class ComponentDependencyCollector + readonly struct ComponentDependencyCollector { - static ComponentDependencyCollector() - { - InitByTypeParsers(); - } - private readonly bool _preserveEndBone; - private readonly OptimizerSession _session; + private readonly BuildContext _session; + private readonly GCComponentInfoHolder _componentInfos; - public ComponentDependencyCollector(OptimizerSession session, bool preserveEndBone) + public ComponentDependencyCollector(BuildContext session, bool preserveEndBone, + GCComponentInfoHolder componentInfos) { _preserveEndBone = preserveEndBone; _session = session; + _componentInfos = componentInfos; } - private readonly Dictionary _dependencies = - new Dictionary(); - - public class ComponentDependencies - { - /// - /// True if this component has Active Meaning on the Avatar. - /// - public bool EntrypointComponent = false; - - /// - /// Dependencies of this component - /// - [NotNull] - public IReadOnlyDictionary Dependencies => _dependencies; - - [NotNull] private readonly Dictionary _dependencies = - new Dictionary(); - - public void AddActiveDependency(Component component, bool onlyIfTargetCanBeEnabled = false, - DependencyType kind = DependencyType.Normal) - { - if (!component) return; - _dependencies.TryGetValue(component, out var pair); - var (flags, kindFlags) = pair; - if (!onlyIfTargetCanBeEnabled) flags |= DependencyFlags.EvenIfTargetIsDisabled; - _dependencies[component] = (flags, kindFlags | kind); - } - - public void AddAlwaysDependency(Component component, bool onlyIfTargetCanBeEnabled = false, - DependencyType kind = DependencyType.Normal) - { - if (!component) return; - _dependencies.TryGetValue(component, out var pair); - var (flags, kindFlags) = pair; - flags |= DependencyFlags.EvenIfThisIsDisabled; - if (!onlyIfTargetCanBeEnabled) flags |= DependencyFlags.EvenIfTargetIsDisabled; - _dependencies[component] = (flags, kindFlags | kind); - } - } - - [Flags] - public enum DependencyFlags : byte - { - // dependency flags - EvenIfTargetIsDisabled = 1 << 0, - EvenIfThisIsDisabled = 1 << 1, - } - - [Flags] - public enum DependencyType : byte - { - Normal = 1 << 0, - Parent = 1 << 1, - ComponentToTransform = 1 << 2, - Bone = 1 << 3, - } - - [CanBeNull] - public ComponentDependencies TryGetDependencies(Component dependent) => - _dependencies.TryGetValue(dependent, out var dependencies) ? dependencies : null; - - [NotNull] - public ComponentDependencies GetDependencies(Component dependent) => _dependencies[dependent]; public void CollectAllUsages() { - var components = _session.GetComponents().ToArray(); - // first iteration: create mapping - foreach (var component in components) _dependencies.Add(component, new ComponentDependencies()); - + var collector = new Collector(this, _componentInfos); // second iteration: process parsers - BuildReport.ReportingObjects(components, component => + foreach (var componentInfo in _componentInfos.AllInformation) { - // component requires GameObject. - GetDependencies(component).AddAlwaysDependency(component.gameObject.transform, - kind: DependencyType.ComponentToTransform); - - if (_byTypeParser.TryGetValue(component.GetType(), out var parser)) - { - var deps = GetDependencies(component); - parser(this, deps, component); - } - else + var component = componentInfo.Component; + BuildReport.ReportingObject(component, () => { - BuildReport.LogWarning("TraceAndOptimize:warn:unknown-type", component.GetType().Name); + // component requires GameObject. + collector.Init(componentInfo); + if (ComponentInfoRegistry.TryGetInformation(component.GetType(), out var information)) + { + information.CollectDependencyInternal(component, collector); + } + else + { + BuildReport.LogWarning("TraceAndOptimize:warn:unknown-type", component.GetType().Name); - FallbackDependenciesParser(component); - } - }); + FallbackDependenciesParser(component, collector); + } + + collector.FinalizeForComponent(); + }); + } } - private void FallbackDependenciesParser(Component component) + private static void FallbackDependenciesParser(Component component, API.ComponentDependencyCollector collector) { // fallback dependencies: All References are Always Dependencies. - var dependencies = GetDependencies(component); - dependencies.EntrypointComponent = true; + collector.MarkEntrypoint(); using (var serialized = new SerializedObject(component)) { - var iterator = serialized.GetIterator(); - var enterChildren = true; - while (iterator.Next(enterChildren)) + foreach (var property in serialized.ObjectReferenceProperties()) { - if (iterator.propertyType == SerializedPropertyType.ObjectReference) - { - if (iterator.objectReferenceValue is GameObject go) - dependencies.AddAlwaysDependency(go.transform); - else if (iterator.objectReferenceValue is Component com) - dependencies.AddAlwaysDependency(com); - } - - switch (iterator.propertyType) - { - case SerializedPropertyType.String: - case SerializedPropertyType.Integer: - case SerializedPropertyType.Boolean: - case SerializedPropertyType.Float: - case SerializedPropertyType.Color: - case SerializedPropertyType.ObjectReference: - case SerializedPropertyType.LayerMask: - case SerializedPropertyType.Enum: - case SerializedPropertyType.Vector2: - case SerializedPropertyType.Vector3: - case SerializedPropertyType.Vector4: - case SerializedPropertyType.Rect: - case SerializedPropertyType.ArraySize: - case SerializedPropertyType.Character: - case SerializedPropertyType.AnimationCurve: - case SerializedPropertyType.Bounds: - case SerializedPropertyType.Gradient: - case SerializedPropertyType.Quaternion: - case SerializedPropertyType.FixedBufferSize: - case SerializedPropertyType.Vector2Int: - case SerializedPropertyType.Vector3Int: - case SerializedPropertyType.RectInt: - case SerializedPropertyType.BoundsInt: - enterChildren = false; - break; - case SerializedPropertyType.Generic: - case SerializedPropertyType.ExposedReference: - case SerializedPropertyType.ManagedReference: - default: - enterChildren = true; - break; - } + if (property.objectReferenceValue is GameObject go) + collector.AddDependency(go.transform).EvenIfDependantDisabled(); + else if (property.objectReferenceValue is Component com) + collector.AddDependency(com).EvenIfDependantDisabled(); } } } - #region ByComponentMappingGeneration + internal class Collector : API.ComponentDependencyCollector + { + private readonly ComponentDependencyCollector _collector; + private GCComponentInfo _info; + [NotNull] private readonly ComponentDependencyInfo _dependencyInfoSharedInstance; - delegate void ComponentParser(ComponentDependencyCollector collector, ComponentDependencies deps, - TComponent component); + public Collector(ComponentDependencyCollector collector, GCComponentInfoHolder componentInfos) + { + _collector = collector; + _dependencyInfoSharedInstance = new ComponentDependencyInfo(componentInfos); + } + + public void Init(GCComponentInfo info) + { + Debug.Assert(_info == null, "Init on not finished"); + _info = info; + } - private static readonly Dictionary> _byTypeParser = - new Dictionary>(); + public bool PreserveEndBone => _collector._preserveEndBone; - private static void AddParser(ComponentParser parser) where T : Component - { - _byTypeParser.Add(typeof(T), (collector, deps, component) => parser(collector, deps, (T)component)); - } + public MeshInfo2 GetMeshInfoFor(SkinnedMeshRenderer renderer) => + _collector._session.GetMeshInfoFor(renderer); - private static void AddParserWithExtends(ComponentParser parser) - where TParent : Component - where TChild : TParent - { - var parentParser = _byTypeParser[typeof(TParent)]; - _byTypeParser.Add(typeof(TChild), (collector, deps, component) => - { - parentParser(collector, deps, component); - parser(collector, deps, (TChild)component); - }); - } + public override void MarkEntrypoint() => _info.EntrypointComponent = true; - private static void AddNopParser() where T : Component - { - _byTypeParser.Add(typeof(T), (collector, deps, component) => { }); - } + public override void MarkBehaviour() => _info.BehaviourComponent = true; - private static void AddEntryPointParser() where T : Component - { - _byTypeParser.Add(typeof(T), (collector, deps, component) => deps.EntrypointComponent = true); - } + private API.ComponentDependencyInfo AddDependencyInternal( + [NotNull] GCComponentInfo info, + [CanBeNull] Component dependency, + GCComponentInfo.DependencyType type = GCComponentInfo.DependencyType.Normal) + { + _dependencyInfoSharedInstance.Finish(); + _dependencyInfoSharedInstance.Init(info, dependency, type); + return _dependencyInfoSharedInstance; + } - private static void AddParserWithExtends() - where TParent : Component - where TChild : TParent - { - _byTypeParser.Add(typeof(TChild), _byTypeParser[typeof(TParent)]); - } + public override API.ComponentDependencyInfo AddDependency(Component dependant, Component dependency) => + AddDependencyInternal(_collector._componentInfos.GetInfo(dependant), dependency); - #endregion + public override API.ComponentDependencyInfo AddDependency(Component dependency) => + AddDependencyInternal(_info, dependency); - #region ByType Parser + public void AddParentDependency(Transform component) => + AddDependencyInternal(_info, component.parent, GCComponentInfo.DependencyType.Parent) + .EvenIfDependantDisabled(); - /// - /// Initializes _byTypeParser. This includes huge amount of definition for components. - /// - private static void InitByTypeParsers() - { - // unity generic - AddParser((collector, deps, transform) => - { - deps.AddAlwaysDependency(transform.parent, kind: DependencyType.Parent); + public void AddBoneDependency(Transform bone) => + AddDependencyInternal(_info, bone, GCComponentInfo.DependencyType.Bone); - // For compatibility with UnusedBonesByReferenceTool - // https://github.com/anatawa12/AvatarOptimizer/issues/429 - if (collector._preserveEndBone && - transform.name.EndsWith("end", StringComparison.OrdinalIgnoreCase)) - { - collector.GetDependencies(transform.parent) - .AddAlwaysDependency(transform); - } - }); - // Animator does not do much for motion, just changes states of other components. - // All State Changes are collected separately - AddParser((collector, deps, component) => + public void FinalizeForComponent() { - // We can have some - deps.EntrypointComponent = true; + _dependencyInfoSharedInstance.Finish(); + _info = null; + } - // we need bone between Armature..Humanoid - for (var bone = HumanBodyBones.Hips; bone < HumanBodyBones.LastBone; bone++) - { - var boneTransform = component.GetBoneTransform(bone); - deps.AddActiveDependency(boneTransform); - foreach (var transform in boneTransform.ParentEnumerable()) - { - if (transform == component.transform) break; - deps.AddActiveDependency(transform); - } - } - }); - AddEntryPointParser(); - AddParser((collector, deps, renderer) => + private class ComponentDependencyInfo : API.ComponentDependencyInfo { - // GameObject => Renderer dependency ship - deps.EntrypointComponent = true; - // anchor proves - if (renderer.reflectionProbeUsage != ReflectionProbeUsage.Off || - renderer.lightProbeUsage != LightProbeUsage.Off) - deps.AddActiveDependency(renderer.probeAnchor); - if (renderer.lightProbeUsage == LightProbeUsage.UseProxyVolume) - deps.AddActiveDependency(renderer.lightProbeProxyVolumeOverride.transform); - }); - AddParserWithExtends((collector, deps, skinnedMeshRenderer) => - { - var meshInfo2 = collector._session.MeshInfo2Holder.GetMeshInfoFor(skinnedMeshRenderer); - foreach (var bone in meshInfo2.Bones) - deps.AddActiveDependency(bone.Transform, kind: DependencyType.Bone); - deps.AddActiveDependency(meshInfo2.RootBone); - }); - AddParserWithExtends((collector, deps, component) => - { - deps.AddActiveDependency(component.GetComponent()); - }); - AddNopParser(); - AddParser((collector, deps, particleSystem) => - { - if (particleSystem.main.simulationSpace == ParticleSystemSimulationSpace.Custom) - deps.AddActiveDependency(particleSystem.main.customSimulationSpace); - if (particleSystem.shape.enabled) - { - switch (particleSystem.shape.shapeType) - { - case ParticleSystemShapeType.MeshRenderer: - deps.AddActiveDependency(particleSystem.shape.meshRenderer); - break; - case ParticleSystemShapeType.SkinnedMeshRenderer: - deps.AddActiveDependency(particleSystem.shape.skinnedMeshRenderer); - break; - case ParticleSystemShapeType.SpriteRenderer: - deps.AddActiveDependency(particleSystem.shape.spriteRenderer); - break; -#pragma warning disable CS0618 - case ParticleSystemShapeType.Sphere: - case ParticleSystemShapeType.SphereShell: - case ParticleSystemShapeType.Hemisphere: - case ParticleSystemShapeType.HemisphereShell: - case ParticleSystemShapeType.Cone: - case ParticleSystemShapeType.Box: - case ParticleSystemShapeType.Mesh: - case ParticleSystemShapeType.ConeShell: - case ParticleSystemShapeType.ConeVolume: - case ParticleSystemShapeType.ConeVolumeShell: - case ParticleSystemShapeType.Circle: - case ParticleSystemShapeType.CircleEdge: - case ParticleSystemShapeType.SingleSidedEdge: - case ParticleSystemShapeType.BoxShell: - case ParticleSystemShapeType.BoxEdge: - case ParticleSystemShapeType.Donut: - case ParticleSystemShapeType.Rectangle: - case ParticleSystemShapeType.Sprite: - default: -#pragma warning restore CS0618 - break; - } - } + private readonly GCComponentInfoHolder _componentInfos; - if (particleSystem.collision.enabled) - { - switch (particleSystem.collision.type) - { - case ParticleSystemCollisionType.Planes: - for (var i = 0; i < particleSystem.collision.maxPlaneCount; i++) - deps.AddActiveDependency(particleSystem.collision.GetPlane(i)); - break; - case ParticleSystemCollisionType.World: - default: - break; - } - } + [CanBeNull] private Component _dependency; + private GCComponentInfo _dependantInformation; + private GCComponentInfo.DependencyType _type; - if (particleSystem.trigger.enabled) - { - for (var i = 0; i < particleSystem.trigger.maxColliderCount; i++) - deps.AddActiveDependency(particleSystem.trigger.GetCollider(i)); - } + private bool _evenIfTargetIsDisabled; + private bool _evenIfThisIsDisabled; - if (particleSystem.subEmitters.enabled) + // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized + public ComponentDependencyInfo(GCComponentInfoHolder componentInfos) { - for (var i = 0; i < particleSystem.subEmitters.subEmittersCount; i++) - deps.AddActiveDependency(particleSystem.subEmitters.GetSubEmitterSystem(i)); + _componentInfos = componentInfos; } - if (particleSystem.lights.enabled) + internal void Init(GCComponentInfo dependantInformation, + [CanBeNull] Component component, + GCComponentInfo.DependencyType type = GCComponentInfo.DependencyType.Normal) { - deps.AddActiveDependency(particleSystem.lights.light); + Debug.Assert(_dependency == null, "Init on not finished"); + _dependency = component; + _dependantInformation = dependantInformation; + _evenIfTargetIsDisabled = true; + _evenIfThisIsDisabled = false; + _type = type; } - deps.AddAlwaysDependency(particleSystem.GetComponent()); - deps.EntrypointComponent = true; - }); - AddParserWithExtends((collector, deps, component) => - { - deps.AddAlwaysDependency(component.GetComponent()); - }); - AddParserWithExtends(); - AddParserWithExtends(); - AddParser((collector, deps, component) => - { - // If Cloth is disabled, SMR work as SMR without Cloth - // If Cloth is enabled and SMR is disabled, SMR draw nothing. - var skinnedMesh = component.GetComponent(); - collector.GetDependencies(skinnedMesh).AddActiveDependency(component, true); - foreach (var collider in component.capsuleColliders) - deps.AddActiveDependency(collider); - foreach (var collider in component.sphereColliders) + internal void Finish() { - deps.AddActiveDependency(collider.first); - deps.AddActiveDependency(collider.second); + if (_dependency == null) return; + SetToDictionary(); + _dependency = null; } - }); - AddEntryPointParser(); - AddParser((collector, deps, component) => - { - deps.EntrypointComponent = true; - var rigidbody = component.GetComponentInParent(); - if (rigidbody) collector.GetDependencies(rigidbody) - .AddActiveDependency(component, true); - }); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParser((collector, deps, component) => - { - collector.GetDependencies(component.GetComponent()).AddActiveDependency(component); - deps.AddActiveDependency(component.connectedBody); - }); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParser((collector, deps, component) => - { - collector.GetDependencies(component.transform) - .AddAlwaysDependency(component, true); - }); - // affects RenderTexture - AddEntryPointParser(); - AddParser((collector, deps, component) => - { - collector.GetDependencies(component.GetComponent()).AddActiveDependency(component); - }); - // plays sound - AddEntryPointParser(); - AddParser((collector, deps, component) => - { - ConstraintParser(collector, deps, component); - deps.AddActiveDependency(component.worldUpObject); - }); - AddParser((collector, deps, component) => - { - ConstraintParser(collector, deps, component); - deps.AddActiveDependency(component.worldUpObject); - }); - AddParser(ConstraintParser); - AddParser(ConstraintParser); - AddParser(ConstraintParser); - AddParser(ConstraintParser); - void ConstraintParser(ComponentDependencyCollector collector, ComponentDependencies deps, - TConstraint constraint) - where TConstraint : Component, IConstraint - { - collector.GetDependencies(constraint.transform) - .AddAlwaysDependency(constraint, true); - for (var i = 0; i < constraint.sourceCount; i++) - deps.AddActiveDependency(constraint.GetSource(i).sourceTransform); - } - - // VRChat specific - AddParser((collector, deps, component) => - { - // to avoid unexpected deletion - deps.EntrypointComponent = true; - deps.AddAlwaysDependency(component.GetComponent()); - deps.AddAlwaysDependency(component.GetComponent()); - }); - AddParserWithExtends((collector, deps, component) => - { - AddCollider(component.collider_head); - AddCollider(component.collider_torso); - AddCollider(component.collider_footR); - AddCollider(component.collider_footL); - AddCollider(component.collider_handR); - AddCollider(component.collider_handL); - AddCollider(component.collider_fingerIndexL); - AddCollider(component.collider_fingerMiddleL); - AddCollider(component.collider_fingerRingL); - AddCollider(component.collider_fingerLittleL); - AddCollider(component.collider_fingerIndexR); - AddCollider(component.collider_fingerMiddleR); - AddCollider(component.collider_fingerRingR); - AddCollider(component.collider_fingerLittleR); - - void AddCollider(VRCAvatarDescriptor.ColliderConfig collider) + private void SetToDictionary() { - switch (collider.state) - { - case VRCAvatarDescriptor.ColliderConfig.State.Automatic: - case VRCAvatarDescriptor.ColliderConfig.State.Custom: - deps.AddAlwaysDependency(collider.transform); - break; - case VRCAvatarDescriptor.ColliderConfig.State.Disabled: - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - }); - AddEntryPointParser(); - AddEntryPointParser(); -#pragma warning disable CS0618 - AddEntryPointParser(); -#pragma warning restore CS0618 - AddParser((collector, deps, component) => - { - deps.AddActiveDependency(component.stationEnterPlayerLocation); - deps.AddActiveDependency(component.stationExitPlayerLocation); - deps.EntrypointComponent = true; - deps.AddActiveDependency(component.GetComponentInChildren()); - }); - AddParserWithExtends(); - AddParser((collector, deps, physBone) => - { - // first, Transform <=> PhysBone - // Transform is used even if the bone is inactive so Transform => PB is always dependency - // PhysBone works only if enabled so PB => Transform is active dependency - var ignoreTransforms = new HashSet(physBone.ignoreTransforms); - CollectTransforms(physBone.GetTarget()); + Debug.Assert(_dependency != null, nameof(_dependency) + " != null"); - void CollectTransforms(Transform bone) - { - collector.GetDependencies(bone) - .AddAlwaysDependency(physBone, true); - deps.AddActiveDependency(bone); - foreach (var child in bone.DirectChildrenEnumerable()) + if (!_evenIfThisIsDisabled) { - if (!ignoreTransforms.Contains(child)) - CollectTransforms(child); + // dependant must can be able to be enable + if (_dependantInformation.Activeness == false) return; } - } - - // then, PB => Collider - // in PB, PB Colliders work only if Colliders are enabled - foreach (var physBoneCollider in physBone.colliders) - deps.AddActiveDependency(physBoneCollider, true); - - // If parameter is not empty, the PB can be required for Animator Parameter so it's Entrypoint Component - // https://github.com/anatawa12/AvatarOptimizer/issues/450 - if (!string.IsNullOrEmpty(physBone.parameter)) - deps.EntrypointComponent = true; - }); - AddParser((collector, deps, component) => - { - deps.AddActiveDependency(component.rootTransform); - }); - AddParserWithExtends(); - AddParserWithExtends(); - - AddParser((collector, deps, component) => - { - deps.EntrypointComponent = true; - deps.AddActiveDependency(component.rootTransform); - }); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - AddParserWithExtends(); - - AddEntryPointParser(); - AddParserWithExtends(); - - // VRC_IKFollower is not available in SDK 3 - - // External library: Dynamic Bone - if (DynamicBone.Type is Type dynamicBoneType) - { - _byTypeParser.Add(dynamicBoneType, (collector, deps, component) => - { - DynamicBone.TryCast(component, out var dynamicBone); - foreach (var transform in dynamicBone.GetAffectedTransforms()) + + if (!_evenIfTargetIsDisabled) { - collector.GetDependencies(transform) - .AddAlwaysDependency(component, true); - deps.AddActiveDependency(transform); + // dependency must can be able to be enable + if (_componentInfos.GetInfo(_dependency).Activeness == false) return; } - foreach (var collider in dynamicBone.Colliders) - { - // DynamicBone ignores enabled/disabled of Collider Component AFAIK - deps.AddActiveDependency(collider, false); - } - }); - - // ReSharper disable once PossibleNullReferenceException - _byTypeParser.Add(ExternalLibraryAccessor.DynamicBone.ColliderType, - (collector, deps, component) => {}); - } + _dependantInformation.Dependencies.TryGetValue(_dependency, out var type); + _dependantInformation.Dependencies[_dependency] = type | _type; + } - // TODOL External Library: FinalIK + public override API.ComponentDependencyInfo EvenIfDependantDisabled() + { + _evenIfThisIsDisabled = true; + return this; + } - // NDMF - AddEntryPointParser(); - var contextHolder = typeof(nadena.dev.ndmf.BuildContext).Assembly - .GetType("nadena.dev.ndmf.VRChat.ContextHolder"); - if (contextHolder != null) - { - // nadena.dev.ndmf.VRChat.ContextHolder is internal so I use reflection - _byTypeParser.Add(contextHolder, (collector, deps, component) => deps.EntrypointComponent = true); + public override API.ComponentDependencyInfo OnlyIfTargetCanBeEnable() + { + _evenIfTargetIsDisabled = false; + return this; + } } - - #region Satania's Kisetene Components - - // KiseteneComponent holds information about which cloth the bone came from, which is not important on build - var kiseteneComponent = GetTypeByGuidFileId("e78466b6bcd24e5409dca557eb81d45b", 11500000); - if (kiseteneComponent != null) - _byTypeParser.Add(kiseteneComponent, (collector, deps, component) => deps.EntrypointComponent = true); - - // FlyAvatarSetupTool is on-inspector tool which is not important on build - var flyAvatarSetupTool = GetTypeByGuidFileId("7f9c3fe1cfb9d1843a9dc7da26352ce2", 11500000); - if (flyAvatarSetupTool != null) - _byTypeParser.Add(flyAvatarSetupTool, (collector, deps, component) => deps.EntrypointComponent = true); - - // BlendShapeOverrider is on-inspector tool which is not important on build - var blendShapeOverrider = GetTypeByGuidFileId("95f6e1368d803614f8a351322ab09bac", 11500000); - if (blendShapeOverrider != null) - _byTypeParser.Add(blendShapeOverrider, (collector, deps, component) => deps.EntrypointComponent = true); - - #endregion - - #region VRCQuestTools - - var vertexColorRemover = GetTypeByGuidFileId("f055e14e1beba894ea68aedffde8ada6", 11500000); - if (vertexColorRemover != null) - _byTypeParser.Add(vertexColorRemover, (collector, deps, component) => deps.EntrypointComponent = true); - - #endregion - - // Components Proceed after T&O later - AddEntryPointParser(); } - - private static Type GetTypeByGuidFileId(string guid, long fileId) - { - if (!GlobalObjectId.TryParse($"GlobalObjectId_V1-1-{guid}-{fileId}-0", out var id)) return null; - var script = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(id) as MonoScript; - if (!script) return null; - return script.GetClass(); - } - - #endregion } } diff --git a/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs b/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs index 72e4c078a..d0f0f8458 100644 --- a/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs +++ b/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs @@ -1,360 +1,280 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using UnityEditor; +using nadena.dev.ndmf; using UnityEngine; -using VRC.Dynamics; -using Debug = System.Diagnostics.Debug; -using Object = UnityEngine.Object; namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes { - class FindUnusedObjectsProcessor + internal class FindUnusedObjects : Pass { - private readonly ImmutableModificationsContainer _modifications; - private readonly OptimizerSession _session; - private readonly HashSet _exclusions; - private readonly bool _preserveEndBone; - private readonly bool _useLegacyGC; - private readonly bool _noConfigureMergeBone; - private readonly bool _gcDebug; + public override string DisplayName => "T&O: FindUnusedObjects"; - public FindUnusedObjectsProcessor(ImmutableModificationsContainer modifications, OptimizerSession session, - bool preserveEndBone, - bool useLegacyGC, - bool noConfigureMergeBone, - bool gcDebug, - HashSet exclusions) + protected override void Execute(BuildContext context) { - _modifications = modifications; - _session = session; - _preserveEndBone = preserveEndBone; - _useLegacyGC = useLegacyGC; - _noConfigureMergeBone = noConfigureMergeBone; - _gcDebug = gcDebug; - _exclusions = exclusions; - } + var state = context.GetState(); + if (!state.RemoveUnusedObjects) return; - public void Process() - { - if (_useLegacyGC) - ProcessLegacy(); - else if (_gcDebug) - CollectDataForGc(); - else - ProcessNew(); + var processor = new FindUnusedObjectsProcessor(context, state); + processor.ProcessNew(); } + } + + internal readonly struct MarkObjectContext { + private readonly GCComponentInfoHolder _componentInfos; - // Mark & Sweep Variables - private readonly Dictionary _marked = - new Dictionary(); - private readonly Queue<(Component, bool)> _processPending = new Queue<(Component, bool)>(); - private readonly Dictionary _activeNessCache = new Dictionary(); + private readonly Queue _processPending; + private readonly Component _entrypoint; - private bool? GetActiveness(Component component) + public MarkObjectContext(GCComponentInfoHolder componentInfos, Component entrypoint) { - if (_activeNessCache.TryGetValue(component, out var activeness)) - return activeness; - activeness = ComputeActiveness(component); - _activeNessCache.Add(component, activeness); - return activeness; + _componentInfos = componentInfos; + _processPending = new Queue(); + _entrypoint = entrypoint; } - private bool? ComputeActiveness(Component component) + public void MarkComponent(Component component, + GCComponentInfo.DependencyType type) { - if (_session.GetRootComponent() == component) return true; - bool? parentActiveness; - if (component is Transform t) - parentActiveness = t.parent == null ? true : GetActiveness(t.parent); - else - parentActiveness = GetActiveness(component.transform); - if (parentActiveness == false) return false; + var dependencies = _componentInfos.TryGetInfo(component); + if (dependencies == null) return; - bool? activeness; - switch (component) + if (dependencies.DependantEntrypoint.TryGetValue(_entrypoint, out var existingFlags)) { - case Transform transform: - var gameObject = transform.gameObject; - activeness = _modifications.GetConstantValue(gameObject, "m_IsActive", gameObject.activeSelf); - break; - case Behaviour behaviour: - activeness = _modifications.GetConstantValue(behaviour, "m_Enabled", behaviour.enabled); - break; - case Cloth cloth: - activeness = _modifications.GetConstantValue(cloth, "m_Enabled", cloth.enabled); - break; - case Collider collider: - activeness = _modifications.GetConstantValue(collider, "m_Enabled", collider.enabled); - break; - case LODGroup lodGroup: - activeness = _modifications.GetConstantValue(lodGroup, "m_Enabled", lodGroup.enabled); - break; - case Renderer renderer: - activeness = _modifications.GetConstantValue(renderer, "m_Enabled", renderer.enabled); - break; - // components without isEnable - case CanvasRenderer _: - case Joint _: - case MeshFilter _: - case OcclusionArea _: - case OcclusionPortal _: - case ParticleSystem _: - case ParticleSystemForceField _: - case Rigidbody _: - case Rigidbody2D _: - case TextMesh _: - case Tree _: - case WindZone _: - case UnityEngine.XR.WSA.WorldAnchor _: - activeness = true; - break; - case Component _: - case null: - // fallback: all components type should be proceed with above switch - activeness = null; - break; + dependencies.DependantEntrypoint[_entrypoint] = existingFlags | type; + } + else + { + _processPending.Enqueue(component); + dependencies.DependantEntrypoint.Add(_entrypoint, type); } - - if (activeness == false) return false; - if (parentActiveness == true && activeness == true) return true; - - return null; } - private void MarkComponent(Component component, - bool ifTargetCanBeEnabled, - ComponentDependencyCollector.DependencyType type) + public void MarkRecursively() { - bool? activeness = GetActiveness(component); - - if (ifTargetCanBeEnabled && activeness == false) - return; // The Target is not active so not dependency - - if (_marked.TryGetValue(component, out var existingFlags)) - { - _marked[component] = existingFlags | type; - } - else + while (_processPending.Count != 0) { - _processPending.Enqueue((component, activeness != false)); - _marked.Add(component, type); + var component = _processPending.Dequeue(); + var dependencies = _componentInfos.TryGetInfo(component); + if (dependencies == null) continue; // not part of this Hierarchy Tree + + foreach (var (dependency, type) in dependencies.Dependencies) + MarkComponent(dependency, type); } } + } - private void ProcessNew() + internal readonly struct FindUnusedObjectsProcessor + { + private readonly ImmutableModificationsContainer _modifications; + private readonly BuildContext _context; + private readonly HashSet _exclusions; + private readonly bool _preserveEndBone; + private readonly bool _noConfigureMergeBone; + private readonly bool _noActivenessAnimation; + private readonly bool _gcDebug; + + public FindUnusedObjectsProcessor(BuildContext context, TraceAndOptimizeState state) { - MarkAndSweep(); - if (!_noConfigureMergeBone) ConfigureMergeBone(); + _context = context; + + _modifications = state.Modifications; + _preserveEndBone = state.PreserveEndBone; + _noConfigureMergeBone = state.NoConfigureMergeBone; + _noActivenessAnimation = state.NoActivenessAnimation; + _gcDebug = state.GCDebug; + _exclusions = state.Exclusions; } - private void MarkAndSweep() + public void ProcessNew() { - // first, collect usages - var collector = new ComponentDependencyCollector(_session, _preserveEndBone); - collector.CollectAllUsages(); + var componentInfos = new GCComponentInfoHolder(_modifications, _context.AvatarRootObject); + Mark(componentInfos); + if (_gcDebug) + { + GCDebug.AddGCDebugInfo(componentInfos, _context.AvatarRootObject); + return; + } + Sweep(componentInfos); + if (!_noConfigureMergeBone) + MergeBone(componentInfos); + if (!_noActivenessAnimation) + ActivenessAnimation(componentInfos); + } - // then, mark and sweep. + private void ActivenessAnimation(GCComponentInfoHolder componentInfos) + { + // entrypoint -> affected activeness animated components / GameObjects + Dictionary> entryPointActiveness = + new Dictionary>(); + + foreach (var componentInfo in componentInfos.AllInformation) + { + if (!componentInfo.Component) continue; // swept + if (componentInfo.IsEntrypoint) continue; + if (!componentInfo.BehaviourComponent) continue; + if (_modifications.GetModifiedProperties(componentInfo.Component).ContainsKey("m_Enabled")) + continue; // enabled is animated so we will not generate activeness animation + + HashSet resultSet; + using (var enumerator = componentInfo.DependantEntrypoint.Keys.GetEnumerator()) + { + System.Diagnostics.Debug.Assert(enumerator.MoveNext()); + resultSet = GetEntrypointActiveness(enumerator.Current, _modifications, _context.AvatarRootTransform); - // entrypoint for mark & sweep is active-able GameObjects - foreach (var gameObject in CollectAllActiveAbleGameObjects()) - foreach (var component in gameObject.GetComponents()) - if (collector.GetDependencies(component).EntrypointComponent) - MarkComponent(component, true, ComponentDependencyCollector.DependencyType.Normal); + // resultSet.Count == 0 => no longer meaning + if (enumerator.MoveNext() && resultSet.Count != 0) + { + resultSet = new HashSet(resultSet); + + do + { + var current = GetEntrypointActiveness(enumerator.Current, _modifications, + _context.AvatarRootTransform); + resultSet.IntersectWith(current); + } while (enumerator.MoveNext() && resultSet.Count != 0); + } + } - // excluded GameObjects must be exists - foreach (var gameObject in _exclusions) - foreach (var component in gameObject.GetComponents()) - MarkComponent(component, true, ComponentDependencyCollector.DependencyType.Normal); + if (resultSet.Count == 0) + continue; // there are no common activeness animation - while (_processPending.Count != 0) - { - var (component, canBeActive) = _processPending.Dequeue(); - var dependencies = collector.TryGetDependencies(component); - if (dependencies == null) continue; // not part of this Hierarchy Tree + resultSet.Remove(componentInfo.Component.transform); + resultSet.ExceptWith(componentInfo.Component.transform.ParentEnumerable()); - foreach (var (dependency, flags) in dependencies.Dependencies) + Component commonActiveness; + // TODO: we may use all activeness with nested identity transform + // if activeness animation is not changed + if (resultSet.Count == 0) { - var ifActive = - (flags.flags & ComponentDependencyCollector.DependencyFlags.EvenIfThisIsDisabled) == 0; - if (ifActive && !canBeActive) continue; - var ifTargetCanBeEnabled = - (flags.flags & ComponentDependencyCollector.DependencyFlags.EvenIfTargetIsDisabled) == 0; - MarkComponent(dependency, ifTargetCanBeEnabled, flags.type); + // the only activeness is parent of this component so adding animation is not required + continue; } - } + if (resultSet.Count == 1) + { + commonActiveness = resultSet.First(); + } + else + { + // TODO: currently this is using most-child component but I don't know is this the best. + var nonTransform = resultSet.FirstOrDefault(x => !(x is Transform)); + if (nonTransform != null) + { + // unlikely: deepest common activeness is the entrypoint component. + commonActiveness = nonTransform; + } + else + { + // likely: deepest common activeness is parent + commonActiveness = null; + foreach (var component in resultSet) + { + var transform = (Transform)component; + if (commonActiveness == null) commonActiveness = transform; + else + { + // if commonActiveness is parent of transform, transform is children of commonActiveness + if (transform.ParentEnumerable().Contains(commonActiveness)) + commonActiveness = transform; + } + } - foreach (var component in _session.GetComponents()) - { - // null values are ignored - if (!component) continue; + System.Diagnostics.Debug.Assert(commonActiveness != null); + } + } - if (component is Transform) + if (commonActiveness is Transform) { - // Treat Transform Component as GameObject because they are two sides of the same coin - if (!_marked.ContainsKey(component)) - Object.DestroyImmediate(component.gameObject); + _context.Extension().MappingBuilder + .RecordCopyProperty(commonActiveness.gameObject, "m_IsActive", + componentInfo.Component, "m_Enabled"); } else { - if (!_marked.ContainsKey(component)) - Object.DestroyImmediate(component); + _context.Extension().MappingBuilder + .RecordCopyProperty(commonActiveness, "m_Enabled", + componentInfo.Component, "m_Enabled"); } } + + return; + + HashSet GetEntrypointActiveness(Component entryPoint, + ImmutableModificationsContainer modifications, + Transform avatarRoot) + { + if (entryPointActiveness.TryGetValue(entryPoint, out var found)) + return found; + var set = new HashSet(); + + if (modifications.GetModifiedProperties(entryPoint).ContainsKey("m_Enabled")) + set.Add(entryPoint); + + for (var transform = entryPoint.transform; transform != avatarRoot; transform = transform.parent) + if (modifications.GetModifiedProperties(transform.gameObject).ContainsKey("m_IsActive")) + set.Add(transform); + + entryPointActiveness.Add(entryPoint, set); + return set; + } } - private void CollectDataForGc() + private void Mark(GCComponentInfoHolder componentInfos) { // first, collect usages - var collector = new ComponentDependencyCollector(_session, _preserveEndBone); - collector.CollectAllUsages(); + new ComponentDependencyCollector(_context, _preserveEndBone, componentInfos).CollectAllUsages(); - var componentDataMap = new Dictionary(); + // then, mark and sweep. - foreach (var component in _session.GetComponents()) + // entrypoint for mark & sweep is active-able GameObjects + foreach (var componentInfo in componentInfos.AllInformation) { - var componentData = new GCData.ComponentData { component = component }; - componentDataMap.Add(component, componentData); - - switch (ComputeActiveness(component)) + if (componentInfo.IsEntrypoint) { - case false: - componentData.activeness = GCData.ActiveNess.False; - break; - case true: - componentData.activeness = GCData.ActiveNess.True; - break; - case null: - componentData.activeness = GCData.ActiveNess.Variable; - break; + var component = componentInfo.Component; + var markContext = new MarkObjectContext(componentInfos, component); + markContext.MarkComponent(component, GCComponentInfo.DependencyType.Normal); + markContext.MarkRecursively(); } - - var dependencies = collector.GetDependencies(component); - foreach (var (key, (flags, type)) in dependencies.Dependencies) - componentData.dependencies.Add(new GCData.DependencyInfo(key, flags, type)); } - foreach (var gameObject in CollectAllActiveAbleGameObjects()) - foreach (var component in gameObject.GetComponents()) - if (collector.GetDependencies(component).EntrypointComponent) - componentDataMap[component].entrypoint = true; + if (_exclusions.Count != 0) { + // excluded GameObjects must be exists + var markContext = new MarkObjectContext(componentInfos, _context.AvatarRootTransform); - foreach (var gameObject in _exclusions) - foreach (var component in gameObject.GetComponents()) - componentDataMap[component].entrypoint = true; + foreach (var gameObject in _exclusions) + foreach (var component in gameObject.GetComponents()) + markContext.MarkComponent(component, GCComponentInfo.DependencyType.Normal); - foreach (var component in _session.GetComponents()) - { - var dependencies = collector.GetDependencies(component); - foreach (var (key, (flags, type)) in dependencies.Dependencies) - if (componentDataMap.TryGetValue(key, out var info)) - info.dependants.Add(new GCData.DependencyInfo(component, flags, type)); + markContext.MarkRecursively(); } - - foreach (var component in _session.GetComponents()) - component.gameObject.GetOrAddComponent().data.Add(componentDataMap[component]); - _session.GetRootComponent().gameObject.AddComponent(); } - class GCDataRoot : MonoBehaviour + private void Sweep(GCComponentInfoHolder componentInfos) { - } - - [CustomEditor(typeof(GCDataRoot))] - class GCDataRootEditor : Editor - { - public override void OnInspectorGUI() + foreach (var componentInfo in componentInfos.AllInformation) { - if (GUILayout.Button("Copy All Data")) - { - GUIUtility.systemCopyBuffer = CreateData(); - } + // null values are ignored + if (!componentInfo.Component) continue; - if (GUILayout.Button("Save All Data")) + if (componentInfo.DependantEntrypoint.Count == 0) { - var path = EditorUtility.SaveFilePanel("DebugGCData", "", "data.txt", "txt"); - if (!string.IsNullOrEmpty(path)) + if (componentInfo.Component is Transform) { - System.IO.File.WriteAllText(path, CreateData()); + // Treat Transform Component as GameObject because they are two sides of the same coin + Object.DestroyImmediate(componentInfo.Component.gameObject); } - } - - string CreateData() - { - var root = ((Component)target).gameObject; - var collect = new StringBuilder(); - foreach (var gcData in root.GetComponentsInChildren(true)) + else { - collect.Append(RuntimeUtil.RelativePath(root, gcData.gameObject)).Append(":\n"); - - foreach (var componentData in gcData.data.Where(componentData => componentData.component)) - { - collect.Append(" ").Append(componentData.component.GetType().Name).Append(":\n"); - collect.Append(" ActiveNess: ").Append(componentData.activeness).Append('\n'); - collect.Append(" Dependencies:\n"); - var list = new List(); - foreach (var dependencyInfo in componentData.dependencies.Where(x => x.component)) - { - var path = RuntimeUtil.RelativePath(root, dependencyInfo.component.gameObject); - var types = dependencyInfo.component.GetType().Name; - list.Add($"{path}({types})({dependencyInfo.type},{dependencyInfo.flags})"); - } - - list.Sort(); - foreach (var line in list) - collect.Append(" ").Append(line).Append("\n"); - } - - collect.Append("\n"); + Object.DestroyImmediate(componentInfo.Component); } - - return collect.ToString(); } } } - class GCData : MonoBehaviour + private void MergeBone(GCComponentInfoHolder componentInfos) { - public List data = new List(); - - [Serializable] - public class ComponentData - { - public Component component; - public ActiveNess activeness; - public bool entrypoint; - public List dependencies = new List(); - public List dependants = new List(); - } - - [Serializable] - public class DependencyInfo - { - public Component component; - public ComponentDependencyCollector.DependencyFlags flags; - public ComponentDependencyCollector.DependencyType type; - - public DependencyInfo(Component component, ComponentDependencyCollector.DependencyFlags flags, - ComponentDependencyCollector.DependencyType type) - { - this.component = component; - this.flags = flags; - this.type = type; - } - } - - public enum ActiveNess - { - False, - True, - Variable - } - } - - private void ConfigureMergeBone() - { - ConfigureRecursive(_session.GetRootComponent(), _modifications); + ConfigureRecursive(_context.AvatarRootTransform, _modifications); // returns (original mergedChildren, list of merged children if merged, and null if not merged) //[CanBeNull] @@ -377,10 +297,10 @@ private void ConfigureMergeBone() } } - const ComponentDependencyCollector.DependencyType AllowedUsages = - ComponentDependencyCollector.DependencyType.Bone - | ComponentDependencyCollector.DependencyType.Parent - | ComponentDependencyCollector.DependencyType.ComponentToTransform; + const GCComponentInfo.DependencyType AllowedUsages = + GCComponentInfo.DependencyType.Bone + | GCComponentInfo.DependencyType.Parent + | GCComponentInfo.DependencyType.ComponentToTransform; // functions for make it easier to know meaning of result (bool, List) YesMerge() => (mergedChildren, afterChildren); @@ -391,7 +311,7 @@ private void ConfigureMergeBone() // Components must be Transform Only if (transform.GetComponents().Length != 1) return NotMerged(); // The bone cannot be used generally - if ((_marked[transform] & ~AllowedUsages) != 0) return NotMerged(); + if ((componentInfos.GetInfo(transform).AllUsages & ~AllowedUsages) != 0) return NotMerged(); // must not be animated if (TransformAnimated(transform, modifications)) return NotMerged(); @@ -454,141 +374,5 @@ bool GameObjectAnimated(Transform transform, ImmutableModificationsContainer mod "m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z", "localEulerAnglesRaw.x", "localEulerAnglesRaw.y", "localEulerAnglesRaw.z" }; - - private IEnumerable CollectAllActiveAbleGameObjects() - { - var queue = new Queue(); - queue.Enqueue(_session.GetRootComponent().gameObject); - - while (queue.Count != 0) - { - var gameObject = queue.Dequeue(); - var activeNess = _modifications.GetConstantValue(gameObject, "m_IsActive", gameObject.activeSelf); - switch (activeNess) - { - case null: - case true: - // This GameObject can be active - yield return gameObject; - foreach (var transform in gameObject.transform.DirectChildrenEnumerable()) - queue.Enqueue(transform.gameObject); - break; - case false: - // This GameObject and their children will never be active - break; - } - } - } - - private void ProcessLegacy() { - // mark & sweep - var gameObjects = new HashSet(_session.GetComponents().Select(x => x.gameObject)); - var referenced = new HashSet(); - var newReferenced = new Queue(); - - void AddGameObject(GameObject gameObject) - { - if (gameObject && gameObjects.Contains(gameObject) && referenced.Add(gameObject)) - newReferenced.Enqueue(gameObject); - } - - // entry points: active GameObjects - foreach (var component in gameObjects.Where(x => x.activeInHierarchy)) - AddGameObject(component); - - // entry points: modified enable/disable - foreach (var keyValuePair in _modifications.ModifiedProperties) - { - // TODO: if the any of parent is inactive and kept, it should not be assumed as - if (!keyValuePair.Key.AsGameObject(out var gameObject)) continue; - if (!keyValuePair.Value.TryGetValue("m_IsActive", out _)) continue; - - // TODO: if the child is not activeSelf, it should not be assumed as entry point. - foreach (var transform in gameObject.GetComponentsInChildren()) - AddGameObject(transform.gameObject); - } - - // entry points: active GameObjects - foreach (var gameObject in _exclusions) - AddGameObject(gameObject); - - while (newReferenced.Count != 0) - { - var gameObject = newReferenced.Dequeue(); - - foreach (var component in gameObject.GetComponents()) - { - if (component is Transform transform) - { - if (transform.parent) - AddGameObject(transform.parent.gameObject); - continue; - } - - if (component is VRCPhysBoneBase) - { - foreach (var child in component.GetComponentsInChildren(true)) - AddGameObject(child.gameObject); - } - - using (var serialized = new SerializedObject(component)) - { - var iter = serialized.GetIterator(); - var enterChildren = true; - while (iter.Next(enterChildren)) - { - if (iter.propertyType == SerializedPropertyType.ObjectReference) - { - var value = iter.objectReferenceValue; - if (value is Component c && !EditorUtility.IsPersistent(value)) - AddGameObject(c.gameObject); - } - - switch (iter.propertyType) - { - case SerializedPropertyType.Integer: - case SerializedPropertyType.Boolean: - case SerializedPropertyType.Float: - case SerializedPropertyType.String: - case SerializedPropertyType.Color: - case SerializedPropertyType.ObjectReference: - case SerializedPropertyType.Enum: - case SerializedPropertyType.Vector2: - case SerializedPropertyType.Vector3: - case SerializedPropertyType.Vector4: - case SerializedPropertyType.Rect: - case SerializedPropertyType.ArraySize: - case SerializedPropertyType.Character: - case SerializedPropertyType.Bounds: - case SerializedPropertyType.Quaternion: - case SerializedPropertyType.FixedBufferSize: - case SerializedPropertyType.Vector2Int: - case SerializedPropertyType.Vector3Int: - case SerializedPropertyType.RectInt: - case SerializedPropertyType.BoundsInt: - enterChildren = false; - break; - case SerializedPropertyType.Generic: - case SerializedPropertyType.LayerMask: - case SerializedPropertyType.AnimationCurve: - case SerializedPropertyType.Gradient: - case SerializedPropertyType.ExposedReference: - case SerializedPropertyType.ManagedReference: - default: - enterChildren = true; - break; - } - } - } - } - } - - // sweep - foreach (var gameObject in gameObjects.Where(x => !referenced.Contains(x))) - { - if (gameObject) - Object.DestroyImmediate(gameObject); - } - } } } diff --git a/Editor/Processors/TraceAndOptimize/GCComponentInfo.cs b/Editor/Processors/TraceAndOptimize/GCComponentInfo.cs new file mode 100644 index 000000000..5f5c6c760 --- /dev/null +++ b/Editor/Processors/TraceAndOptimize/GCComponentInfo.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes +{ + internal readonly partial struct GCComponentInfoHolder + { + private readonly ImmutableModificationsContainer _modifications; + private readonly Dictionary _dependencies; + + public GCComponentInfoHolder(ImmutableModificationsContainer modifications, GameObject rootGameObject) + { + _modifications = modifications; + // initialize _dependencies + _dependencies = new Dictionary(); + InitializeDependencies(rootGameObject.transform, true); + } + + private void InitializeDependencies(Transform transform, bool? parentActiveness) + { + // add & process the GameObject itself + var transformActiveness = ComputeActiveness(transform, parentActiveness); + _dependencies.Add(transform, new GCComponentInfo(transform, transformActiveness)); + + // process components on the GameObject + foreach (var component in transform.GetComponents()) + { + if (component is Transform) continue; + var activeness = ComputeActiveness(component, transformActiveness); + _dependencies.Add(component, new GCComponentInfo(component, activeness)); + } + + // process children + foreach (var child in transform.DirectChildrenEnumerable()) + { + InitializeDependencies(child, transformActiveness); + } + } + + public IEnumerable AllInformation => _dependencies.Values; + + [CanBeNull] + public GCComponentInfo TryGetInfo(Component dependent) => + _dependencies.TryGetValue(dependent, out var dependencies) ? dependencies : null; + + [NotNull] + public GCComponentInfo GetInfo(Component dependent) => _dependencies[dependent]; + } + + internal class GCComponentInfo + { + /// + /// True if this component has Active side-effect Meaning on the Avatar. + /// + public bool EntrypointComponent = false; + + /// + /// True if activeness of this component has meaning and inactive is lighter + /// + public bool BehaviourComponent = false; + + /// + /// Dependencies of this component + /// + [NotNull] internal readonly Dictionary Dependencies = + new Dictionary(); + + /// + /// Dependants entrypoint components + /// + [NotNull] internal readonly Dictionary DependantEntrypoint = + new Dictionary(); + + internal readonly Component Component; + + public DependencyType AllUsages + { + get + { + DependencyType type = default; + foreach (var usage in DependantEntrypoint.Values) + type |= usage; + return type; + } + } + + public bool IsEntrypoint => EntrypointComponent && Activeness != false; + + public readonly bool? Activeness; + + public GCComponentInfo(Component component, bool? activeness) + { + Component = component; + Dependencies[component.gameObject.transform] = DependencyType.ComponentToTransform; + Activeness = activeness; + } + + [Flags] + public enum DependencyType : byte + { + Normal = 1 << 0, + Parent = 1 << 1, + ComponentToTransform = 1 << 2, + Bone = 1 << 3, + } + } + + internal readonly partial struct GCComponentInfoHolder + { + private bool? ComputeActiveness(Component component, bool? parentActiveness) + { + if (parentActiveness == false) return false; + + bool? activeness; + switch (component) + { + case Transform transform: + var gameObject = transform.gameObject; + activeness = _modifications.GetConstantValue(gameObject, "m_IsActive", gameObject.activeSelf); + break; + case Behaviour behaviour: + activeness = _modifications.GetConstantValue(behaviour, "m_Enabled", behaviour.enabled); + break; + case Cloth cloth: + activeness = _modifications.GetConstantValue(cloth, "m_Enabled", cloth.enabled); + break; + case Collider collider: + activeness = _modifications.GetConstantValue(collider, "m_Enabled", collider.enabled); + break; + case LODGroup lodGroup: + activeness = _modifications.GetConstantValue(lodGroup, "m_Enabled", lodGroup.enabled); + break; + case Renderer renderer: + activeness = _modifications.GetConstantValue(renderer, "m_Enabled", renderer.enabled); + break; + // components without isEnable + case CanvasRenderer _: + case Joint _: + case MeshFilter _: + case OcclusionArea _: + case OcclusionPortal _: + case ParticleSystem _: +#if !UNITY_2021_3_OR_NEWER + case ParticleSystemForceField _: +#endif + case Rigidbody _: + case Rigidbody2D _: + case TextMesh _: + case Tree _: + case WindZone _: +#if !UNITY_2020_2_OR_NEWER + case UnityEngine.XR.WSA.WorldAnchor _: +#endif + activeness = true; + break; + case Component _: + case null: + // fallback: all components type should be proceed with above switch + activeness = null; + break; + } + + if (activeness == false) return false; + if (parentActiveness == true && activeness == true) return true; + + return null; + } + } +} \ No newline at end of file diff --git a/Editor/Processors/TraceAndOptimize/GCComponentInfo.cs.meta b/Editor/Processors/TraceAndOptimize/GCComponentInfo.cs.meta new file mode 100644 index 000000000..e898161d5 --- /dev/null +++ b/Editor/Processors/TraceAndOptimize/GCComponentInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8050285e19a24712b94c510a4b257f12 +timeCreated: 1697885657 \ No newline at end of file diff --git a/Editor/Processors/TraceAndOptimize/GCDebug.cs b/Editor/Processors/TraceAndOptimize/GCDebug.cs new file mode 100644 index 000000000..7497924e7 --- /dev/null +++ b/Editor/Processors/TraceAndOptimize/GCDebug.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes +{ + internal static class GCDebug + { + public static void AddGCDebugInfo(GCComponentInfoHolder componentInfos, GameObject avatarRootObject) + { + foreach (var componentInfo in componentInfos.AllInformation) + { + var gcDebugInfo = componentInfo.Component.gameObject.AddComponent(); + gcDebugInfo.component = componentInfo.Component; + gcDebugInfo.activeness = GCDebugInfo.ActivenessFromBool(componentInfo.Activeness); + gcDebugInfo.isEntryPoint = componentInfo.EntrypointComponent; + gcDebugInfo.entryPoints = componentInfo.DependantEntrypoint.Select(GCDebugInfo.ComponentTypePair.From).ToArray(); + gcDebugInfo.dependencies = componentInfo.Dependencies.Select(GCDebugInfo.ComponentTypePair.From).ToArray(); + } + + avatarRootObject.AddComponent(); + } + + class GCDebugRoot : MonoBehaviour { } + + [CustomEditor(typeof(GCDebugRoot))] + class GCDebugRootEditor : Editor + { + public override void OnInspectorGUI() + { + if (GUILayout.Button("Copy All Data")) + { + GUIUtility.systemCopyBuffer = CreateData(); + } + + if (GUILayout.Button("Save All Data")) + { + var path = EditorUtility.SaveFilePanel("DebugGCData", "", "data.txt", "txt"); + if (!string.IsNullOrEmpty(path)) + { + System.IO.File.WriteAllText(path, CreateData()); + } + } + + string CreateData() + { + var root = ((Component)target).gameObject; + var collect = new StringBuilder(); + foreach (var gcData in root.GetComponentsInChildren(true)) + { + collect.Append(RuntimeUtil.RelativePath(root, gcData.gameObject)) + .Append("(").Append(gcData.component.GetType().Name).Append("):\n"); + collect.Append(" IsEntryPoint: ").Append(gcData.isEntryPoint).Append('\n'); + collect.Append(" ActiveNess: ").Append(gcData.activeness).Append('\n'); + collect.Append(" Dependencies:\n"); + foreach (var line in PairsToStrings(root, gcData.dependencies)) + collect.Append(" ").Append(line).Append("\n"); + collect.Append(" EntryPoints:\n"); + foreach (var line in PairsToStrings(root, gcData.entryPoints)) + collect.Append(" ").Append(line).Append("\n"); + collect.Append("\n"); + } + + return collect.ToString(); + } + + IEnumerable PairsToStrings(GameObject root, GCDebugInfo.ComponentTypePair[] pairs) => + from pair in pairs + where pair.component != null + let path = RuntimeUtil.RelativePath(root, pair.component.gameObject) + let type = pair.component.GetType().Name + let processing = $"{path}({type}): {pair.type}" + orderby processing + select processing; + } + } + + class GCDebugInfo : MonoBehaviour + { + public Component component; + public Activeness activeness; + public bool isEntryPoint; + public ComponentTypePair[] dependencies; + public ComponentTypePair[] entryPoints; + + public static Activeness ActivenessFromBool(bool? activeness) + { + if (activeness == true) + return Activeness.Active; + if (activeness == false) + return Activeness.Inactive; + return Activeness.Variable; + } + + public enum Activeness + { + Active, + Inactive, + Variable + } + + [Serializable] + public struct ComponentTypePair + { + public Component component; + public GCComponentInfo.DependencyType type; + + public static ComponentTypePair From(KeyValuePair arg) + { + return new ComponentTypePair + { + component = arg.Key, + type = arg.Value, + }; + } + } + + [CustomPropertyDrawer(typeof(ComponentTypePair))] + private class ComponentTypePairEditor : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + var indent = EditorGUI.indentLevel * 15f; + const float spacing = 2.0f; + var labelPosition = new Rect(position.x + indent, position.y, EditorGUIUtility.labelWidth - indent, position.height); + var rect = new Rect(position.x + EditorGUIUtility.labelWidth + spacing, position.y, + position.width - EditorGUIUtility.labelWidth - spacing, position.height); + + EditorGUI.ObjectField(labelPosition, property.FindPropertyRelative("component")); + GUI.Label(rect, ((GCComponentInfo.DependencyType)property.FindPropertyRelative("type").intValue).ToString()); + } + } + } + } +} \ No newline at end of file diff --git a/Editor/Processors/TraceAndOptimize/GCDebug.cs.meta b/Editor/Processors/TraceAndOptimize/GCDebug.cs.meta new file mode 100644 index 000000000..2398c6c64 --- /dev/null +++ b/Editor/Processors/TraceAndOptimize/GCDebug.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3f5e1a78bdb74a0e9ca0458ac90ed247 +timeCreated: 1698029331 \ No newline at end of file diff --git a/Editor/Processors/TraceAndOptimize/ModificationsContainers.cs b/Editor/Processors/TraceAndOptimize/ModificationsContainers.cs index 8007c4b12..2f1459cd3 100644 --- a/Editor/Processors/TraceAndOptimize/ModificationsContainers.cs +++ b/Editor/Processors/TraceAndOptimize/ModificationsContainers.cs @@ -262,41 +262,6 @@ public void MakeAllVariable() } } - public readonly struct ComponentOrGameObject : IEquatable - { - private readonly Object _object; - - public ComponentOrGameObject(Object o) => _object = o; - - public static implicit operator ComponentOrGameObject(GameObject gameObject) => - new ComponentOrGameObject(gameObject); - - public static implicit operator ComponentOrGameObject(Component component) => - new ComponentOrGameObject(component); - - public static implicit operator Object(ComponentOrGameObject componentOrGameObject) => - componentOrGameObject._object; - - public GameObject SelfOrAttachedGameObject => _object as GameObject ?? ((Component)_object).gameObject; - public Object Value => _object; - - public bool AsGameObject(out GameObject gameObject) - { - gameObject = _object as GameObject; - return gameObject; - } - - public bool AsComponent(out T gameObject) where T : Component - { - gameObject = _object as T; - return gameObject; - } - - public bool Equals(ComponentOrGameObject other) => Equals(_object, other._object); - public override bool Equals(object obj) => obj is ComponentOrGameObject other && Equals(other); - public override int GetHashCode() => _object != null ? _object.GetHashCode() : 0; - } - readonly struct AnimationProperty : IEquatable { public readonly PropertyState State; diff --git a/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs b/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs index 562009f5d..e04d91d01 100644 --- a/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs +++ b/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs @@ -1,42 +1,74 @@ using System.Collections.Generic; -using Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes; -using JetBrains.Annotations; +using nadena.dev.ndmf; using UnityEngine; -namespace Anatawa12.AvatarOptimizer.Processors +namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes { - internal class TraceAndOptimizeProcessor + class TraceAndOptimizeState { - private ImmutableModificationsContainer _modifications; - [CanBeNull] private TraceAndOptimize _config; - private HashSet _exclusions; + public bool Enabled; + public bool FreezeBlendShape; + public bool RemoveUnusedObjects; + public bool MmdWorldCompatibility; - public void Process(OptimizerSession session) + public bool PreserveEndBone; + public HashSet Exclusions = new HashSet(); + public bool GCDebug; + public bool NoConfigureMergeBone; + public bool NoActivenessAnimation; + public bool SkipFreezingNonAnimatedBlendShape; + public bool SkipFreezingMeaninglessBlendShape; + + public Dictionary> PreserveBlendShapes = + new Dictionary>(); + + public ImmutableModificationsContainer Modifications; + + public TraceAndOptimizeState() + { + } + + public void Initialize(TraceAndOptimize config) { - _config = session.GetRootComponent(); - if (_config is null) return; - Object.DestroyImmediate(_config); - _exclusions = new HashSet(_config.advancedSettings.exclusions); - - _modifications = new AnimatorParser(_config).GatherAnimationModifications(session); - if (_config.freezeBlendShape) - new AutoFreezeBlendShape(_modifications, session, _exclusions).Process( - _config.advancedSettings.skipFreezingNonAnimatedBlendShape, - _config.advancedSettings.skipFreezingMeaninglessBlendShape); + FreezeBlendShape = config.freezeBlendShape; + RemoveUnusedObjects = config.removeUnusedObjects; + MmdWorldCompatibility = config.mmdWorldCompatibility; + + PreserveEndBone = config.preserveEndBone; + + Exclusions = new HashSet(config.advancedSettings.exclusions); + GCDebug = config.advancedSettings.gcDebug; + NoConfigureMergeBone = config.advancedSettings.noConfigureMergeBone; + NoActivenessAnimation = config.advancedSettings.noActivenessAnimation; + SkipFreezingNonAnimatedBlendShape = config.advancedSettings.skipFreezingNonAnimatedBlendShape; + SkipFreezingMeaninglessBlendShape = config.advancedSettings.skipFreezingMeaninglessBlendShape; + + Enabled = true; } + } + + internal class LoadTraceAndOptimizeConfiguration : Pass + { + public override string DisplayName => "T&O: Load Configuration"; - public void ProcessLater(OptimizerSession session) + protected override void Execute(BuildContext context) { - if (_config is null) return; + var config = context.AvatarRootObject.GetComponent(); + if (config) + context.GetState().Initialize(config); + Object.DestroyImmediate(config); + } + } - if (_config.removeUnusedObjects) - new FindUnusedObjectsProcessor(_modifications, session, - preserveEndBone: _config.preserveEndBone, - useLegacyGC: _config.advancedSettings.useLegacyGc, - noConfigureMergeBone: _config.advancedSettings.noConfigureMergeBone, - gcDebug: _config.advancedSettings.gcDebug, - exclusions: _exclusions).Process(); + internal class ParseAnimator : Pass + { + public override string DisplayName => "T&O: Parse Animator"; + protected override void Execute(BuildContext context) + { + var state = context.GetState(); + if (state.Enabled) + state.Modifications = new AnimatorParser(state).GatherAnimationModifications(context); } } } diff --git a/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs b/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs index 588d6c098..3460fd617 100644 --- a/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs +++ b/Editor/Processors/UnusedBonesByReferencesToolEarlyProcessor.cs @@ -1,18 +1,20 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Anatawa12.AvatarOptimizer.ErrorReporting; +using nadena.dev.ndmf; using UnityEngine; namespace Anatawa12.AvatarOptimizer.Processors { #pragma warning disable CS0618 - class UnusedBonesByReferencesToolEarlyProcessor + class UnusedBonesByReferencesToolEarlyProcessor : Pass { - public void Process(OptimizerSession session) + public override string DisplayName => "Early: UnusedBonesByReference"; + + protected override void Execute(BuildContext context) { - var configuration = session.GetRootComponent(); + var configuration = context.AvatarRootObject.GetComponent(); if (!configuration) return; BuildReport.ReportingObject(configuration, () => diff --git a/Editor/UnusedBonesByReferencesToolEditor.cs b/Editor/UnusedBonesByReferencesToolEditor.cs index 6f44be52c..4db583fdf 100644 --- a/Editor/UnusedBonesByReferencesToolEditor.cs +++ b/Editor/UnusedBonesByReferencesToolEditor.cs @@ -38,7 +38,6 @@ protected override void OnInspectorGUIInner() Undo.RecordObject(traceAndOptimize, CONTENT_UNDO_NAME); traceAndOptimize.removeUnusedObjects = true; - traceAndOptimize.advancedSettings.useLegacyGc = false; traceAndOptimize.preserveEndBone = component.preserveEndBone; PrefabUtility.RecordPrefabInstancePropertyModifications(traceAndOptimize); diff --git a/Editor/Utils.cs b/Editor/Utils.cs deleted file mode 100644 index 3fa4e65e4..000000000 --- a/Editor/Utils.cs +++ /dev/null @@ -1,697 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using JetBrains.Annotations; -using UnityEditor; -using UnityEngine; -using UnityEngine.Assertions; -using VRC.Dynamics; -using VRC.SDK3.Avatars.Components; -using Object = UnityEngine.Object; - -namespace Anatawa12.AvatarOptimizer -{ - internal static class Utils - { - private static CachedGuidLoader _toonLitShader = "affc81f3d164d734d8f13053effb1c5c"; - public static Shader ToonLitShader => _toonLitShader.Value; - - private static CachedGuidLoader _mergeTextureHelper = "2d4f01f29e91494bb5eafd4c99153ab0"; - public static Shader MergeTextureHelper => _mergeTextureHelper.Value; - - private static CachedGuidLoader _previewHereTex = "617775211fe634657ae06fc9f81b6ceb"; - public static Texture2D PreviewHereTex => _previewHereTex.Value; - - public static void HorizontalLine(bool marginTop = true, bool marginBottom = true) - { - const float margin = 17f / 2; - var maxHeight = 1f; - if (marginTop) maxHeight += margin; - if (marginBottom) maxHeight += margin; - - var rect = GUILayoutUtility.GetRect( - EditorGUIUtility.fieldWidth, float.MaxValue, - 1, maxHeight, GUIStyle.none); - if (marginTop && marginBottom) - rect.y += rect.height / 2 - 0.5f; - else if (marginTop) - rect.y += rect.height - 1f; - else if (marginBottom) - rect.y += 0; - rect.height = 1; - EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 1)); - } - - public static ArraySerializedPropertyEnumerable AsEnumerable(this SerializedProperty property) - { - Assert.IsTrue(property.isArray); - return new ArraySerializedPropertyEnumerable(property); - } - - public static TransformParentEnumerable ParentEnumerable(this Transform transform) => - new TransformParentEnumerable(transform); - - public static TransformDirectChildrenEnumerable DirectChildrenEnumerable(this Transform transform) => - new TransformDirectChildrenEnumerable(transform); - - public static void FlattenMapping(this Dictionary self) - { - foreach (var key in self.Keys.ToArray()) - { - var value = self[key]; - while (value != null && self.TryGetValue(value, out var mapped)) - value = mapped; - self[key] = value; - } - } - - [ContractAnnotation("root:null => notnull")] - [ContractAnnotation("root:notnull => canbenull")] - public static string RelativePath(Transform root, Transform child) - { - if (root == child) return ""; - - var pathSegments = new List(); - while (child != root) - { - if (child == null) return null; - pathSegments.Add(child.name); - child = child.transform.parent; - } - - pathSegments.Reverse(); - return string.Join("/", pathSegments); - } - - - // func should returns false if nothing to return - public static void WalkChildren(this Transform root, Func func) - { - var queue = new Queue(); - var head = root.DirectChildrenEnumerable().GetEnumerator(); - for (;;) - { - while (!head.MoveNext()) - { - if (queue.Count == 0) return; - head = queue.Dequeue(); - } - - if (func(head.Current)) - { - queue.Enqueue(head); - head = head.Current.DirectChildrenEnumerable().GetEnumerator(); - } - } - } - - // this is same as .Distinct().Count() however Optimized for int with range 0-X with BitArray - public static int DistinctCountIntWithUpperLimit(this IEnumerable self, int upperLimit) - { - var exists = new BitArray(upperLimit); - var count = 0; - foreach (var i in self) - { - if (exists[i]) continue; - exists[i] = true; - count++; - } - - return count; - } - - public static int CountFalse(this BitArray self) => self.Count - self.CountTrue(); - - public static int CountTrue(this BitArray self) - { - var ints = new int[(self.Count >> 5) + 1]; - self.CopyTo(ints, 0); - var count = 0; - - // fix for not truncated bits in last integer that may have been set to true with SetAll() - ints[ints.Length - 1] &= ~(-1 << (self.Count % 32)); - - foreach (var t in ints) - { - uint c = (uint)t; - - // magic (http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel) - unchecked - { - c = ((c >> 0) & 0xFFFFFFFF) - ((c >> 1) & 0x55555555); - c = ((c >> 2) & 0x33333333) + (c & 0x33333333); - c = ((c >> 4) + c) & 0x0F0F0F0F; - c = ((c >> 8) + c) & 0x00FF00FF; - c = ((c >> 16) + c) & 0x0000FFFF; - } - - count += (int) c; - } - - return count; - } - - public static void FillArray(T[] array, T value) - { - for (var i = 0; i < array.Length; i++) - array[i] = value; - } - - public static T GetOrAddComponent(this GameObject go) where T : Component - { - var component = go.GetComponent(); - if (!component) component = go.AddComponent(); - return component; - } - - public static Transform GetTarget(this VRCPhysBoneBase physBoneBase) => - physBoneBase.rootTransform ? physBoneBase.rootTransform : physBoneBase.transform; - - public static IEnumerable GetAffectedTransforms(this VRCPhysBoneBase physBoneBase) - { - var ignores = new HashSet(physBoneBase.ignoreTransforms); - var queue = new Queue(); - queue.Enqueue(physBoneBase.GetTarget()); - - while (queue.Count != 0) - { - var transform = queue.Dequeue(); - yield return transform; - - foreach (var child in transform.DirectChildrenEnumerable()) - if (!ignores.Contains(child)) - queue.Enqueue(child); - } - } - - // based on https://gist.github.com/anatawa12/16fbf529c8da4a0fb993c866b1d86512 - public static void CopyDataFrom(this SerializedProperty dest, SerializedProperty source) - { - if (dest.propertyType == SerializedPropertyType.Generic) - CopyBetweenTwoRecursively(source, dest); - else - CopyBetweenTwoValue(source, dest); - - - void CopyBetweenTwoRecursively(SerializedProperty src, SerializedProperty dst) - { - var srcIter = src.Copy(); - var dstIter = dst.Copy(); - var srcEnd = src.GetEndProperty(); - var dstEnd = dst.GetEndProperty(); - var enterChildren = true; - while (srcIter.Next(enterChildren) && !SerializedProperty.EqualContents(srcIter, srcEnd)) - { - var destCheck = dstIter.Next(enterChildren) && !SerializedProperty.EqualContents(dstIter, dstEnd); - Assert.IsTrue(destCheck); - - //Debug.Log($"prop: {dstIter.propertyPath}: {dstIter.propertyType}"); - - switch (dstIter.propertyType) - { - case SerializedPropertyType.FixedBufferSize: - Assert.AreEqual(srcIter.fixedBufferSize, dstIter.fixedBufferSize); - break; - case SerializedPropertyType.Generic: - break; - default: - CopyBetweenTwoValue(srcIter, dstIter); - break; - } - - enterChildren = dstIter.propertyType == SerializedPropertyType.Generic; - } - - { - var destCheck = dstIter.NextVisible(enterChildren) && - !SerializedProperty.EqualContents(dstIter, dstEnd); - Assert.IsFalse(destCheck); - } - } - - void CopyBetweenTwoValue(SerializedProperty src, SerializedProperty dst) - { - switch (dst.propertyType) - { - case SerializedPropertyType.Generic: - throw new InvalidOperationException("for generic, use CopyBetweenTwoRecursively"); - case SerializedPropertyType.Integer: - dst.intValue = src.intValue; - break; - case SerializedPropertyType.Boolean: - dst.boolValue = src.boolValue; - break; - case SerializedPropertyType.Float: - dst.floatValue = src.floatValue; - break; - case SerializedPropertyType.String: - dst.stringValue = src.stringValue; - break; - case SerializedPropertyType.Color: - dst.colorValue = src.colorValue; - break; - case SerializedPropertyType.ObjectReference: - dst.objectReferenceValue = src.objectReferenceValue; - break; - case SerializedPropertyType.LayerMask: - dst.intValue = src.intValue; - break; - case SerializedPropertyType.Enum: - dst.intValue = src.intValue; - break; - case SerializedPropertyType.Vector2: - dst.vector2Value = src.vector2Value; - break; - case SerializedPropertyType.Vector3: - dst.vector3Value = src.vector3Value; - break; - case SerializedPropertyType.Vector4: - dst.vector4Value = src.vector4Value; - break; - case SerializedPropertyType.Rect: - dst.rectValue = src.rectValue; - break; - case SerializedPropertyType.ArraySize: - dst.intValue = src.intValue; - break; - case SerializedPropertyType.Character: - dst.intValue = src.intValue; - break; - case SerializedPropertyType.AnimationCurve: - dst.animationCurveValue = src.animationCurveValue; - break; - case SerializedPropertyType.Bounds: - dst.boundsValue = src.boundsValue; - break; - case SerializedPropertyType.Gradient: - //dst.gradientValue = src.gradientValue; - //break; - throw new InvalidOperationException("unsupported type: Gradient"); - case SerializedPropertyType.Quaternion: - dst.quaternionValue = src.quaternionValue; - break; - case SerializedPropertyType.ExposedReference: - dst.exposedReferenceValue = src.exposedReferenceValue; - break; - case SerializedPropertyType.FixedBufferSize: - throw new InvalidOperationException("unsupported type: FixedBufferSize"); - case SerializedPropertyType.Vector2Int: - dst.vector2IntValue = src.vector2IntValue; - break; - case SerializedPropertyType.Vector3Int: - dst.vector3IntValue = src.vector3IntValue; - break; - case SerializedPropertyType.RectInt: - dst.rectIntValue = src.rectIntValue; - break; - case SerializedPropertyType.BoundsInt: - dst.boundsIntValue = src.boundsIntValue; - break; - case SerializedPropertyType.ManagedReference: - throw new InvalidOperationException("unsupported type: ManagedReference"); - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - public static GameObject NewGameObject(string name, Transform parent) - { - var rootObject = new GameObject(name); - rootObject.transform.parent = parent; - rootObject.transform.localPosition = Vector3.zero; - rootObject.transform.localRotation = Quaternion.identity; - rootObject.transform.localScale = Vector3.one; - return rootObject; - } - - public static IEnumerable<(T, T)> ZipWithNext(this IEnumerable enumerable) - { - using (var enumerator = enumerable.GetEnumerator()) - { - if (!enumerator.MoveNext()) yield break; - var prev = enumerator.Current; - while (enumerator.MoveNext()) - { - var current = enumerator.Current; - yield return (prev, current); - prev = current; - } - } - } - - // Properties detailed first and nothing last - public static IEnumerable<(string prop, string rest)> FindSubPaths(string prop, char sep) - { - var rest = ""; - for (;;) - { - yield return (prop, rest); - - var index = prop.LastIndexOf(sep); - if (index == -1) yield break; - - rest = prop.Substring(index) + rest; - prop = prop.Substring(0, index); - } - } - - public static BoundsCornersEnumerable Corners(this Bounds bounds) - { - return new BoundsCornersEnumerable(bounds); - } - - public struct DicCaster - { - public IReadOnlyDictionary CastedDic(IReadOnlyDictionary self) - where TValue : class, TValueCasted - => new CastedDictionary(self); - } - - public static DicCaster CastDic() - => new DicCaster(); - - class CastedDictionary : IReadOnlyDictionary - where TValue : class, TValueCasted - { - private readonly IReadOnlyDictionary _base; - public CastedDictionary(IReadOnlyDictionary @base) => _base = @base; - - public IEnumerator> GetEnumerator() => - new Enumerator(_base.GetEnumerator()); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public int Count => _base.Count; - public bool ContainsKey(TKey key) => _base.ContainsKey(key); - public TValueCasted this[TKey key] => _base[key]; - public IEnumerable Keys => _base.Keys; - public IEnumerable Values => _base.Values; - - public bool TryGetValue(TKey key, out TValueCasted value) - { - var result = _base.TryGetValue(key, out var original); - value = original; - return result; - } - - private class Enumerator : IEnumerator> - { - private IEnumerator> _base; - - public Enumerator(IEnumerator> @base) => _base = @base; - public bool MoveNext() => _base.MoveNext(); - public void Reset() => _base.Reset(); - object IEnumerator.Current => Current; - public void Dispose() => _base.Dispose(); - - public KeyValuePair Current - { - get - { - var (key, value) = _base.Current; - return new KeyValuePair(key, value); - } - } - } - } - - private class EmptyDictionaryHolder - { - public static readonly IReadOnlyDictionary Empty = - new ReadOnlyDictionary(new Dictionary()); - } - - public static IReadOnlyDictionary EmptyDictionary() => - EmptyDictionaryHolder.Empty; - - public static void Deconstruct(this KeyValuePair keyValuePair, out TKey key, - out TValue value) - { - key = keyValuePair.Key; - value = keyValuePair.Value; - } - - public static IEnumerable> ZipByKey( - this IReadOnlyDictionary first, IReadOnlyDictionary second) - { - foreach (var key in first.Keys.ToArray()) - { - if (!second.TryGetValue(key, out var secondValue)) secondValue = default; - - yield return new KeyValuePair(key, (first[key], secondValue)); - } - - foreach (var key in second.Keys.ToArray()) - if (!first.ContainsKey(key)) - yield return new KeyValuePair(key, (default, second[key])); - } - - [CanBeNull] - public static Type GetTypeFromName(string name) => - AppDomain.CurrentDomain.GetAssemblies().Select(assembly => assembly.GetType(name)) - .FirstOrDefault(type => !(type == null)); - - public static T DistinctSingleOrDefaultIfNoneOrMultiple(this IEnumerable enumerable) - { - using (var enumerator = enumerable.GetEnumerator()) - { - if (!enumerator.MoveNext()) return default; - var found = enumerator.Current; - var eqOperator = EqualityComparer.Default; - - while (enumerator.MoveNext()) - { - var nextValue = enumerator.Current; - if (!eqOperator.Equals(found, nextValue)) - return default; - } - - return found; - } - } - } - - internal readonly struct ArraySerializedPropertyEnumerable : IEnumerable - { - private readonly SerializedProperty _property; - - public ArraySerializedPropertyEnumerable(SerializedProperty property) - { - this._property = property; - } - - Enumerator GetEnumerator() => new Enumerator(_property); - - private struct Enumerator : IEnumerator - { - private int _index; - private readonly SerializedProperty _property; - - public Enumerator(SerializedProperty property) - { - _index = -1; - _property = property; - } - - public bool MoveNext() => ++_index < _property.arraySize; - - public void Reset() => _index = -1; - - public SerializedProperty Current => _property.GetArrayElementAtIndex(_index); - - object IEnumerator.Current => Current; - - public void Dispose() - { - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - internal readonly struct TransformParentEnumerable : IEnumerable - { - private readonly Transform _transform; - - public TransformParentEnumerable(Transform transform) - { - - _transform = transform; - } - - public Enumerator GetEnumerator() => new Enumerator(_transform); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public struct Enumerator: IEnumerator - { - object IEnumerator.Current => Current; - public Transform Current { get; private set; } - private readonly Transform _initial; - - public Enumerator(Transform transform) => _initial = Current = transform; - - public bool MoveNext() - { - Current = Current != null ? Current.parent : null; - return Current; - } - - public void Reset() => Current = _initial; - - public void Dispose() {} - } - } - - internal readonly struct TransformDirectChildrenEnumerable : IEnumerable - { - private readonly Transform _parent; - - public TransformDirectChildrenEnumerable(Transform parent) - { - - _parent = parent; - } - - public Enumerator GetEnumerator() => new Enumerator(_parent); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public struct Enumerator: IEnumerator - { - private int _index; - private readonly Transform _parent; - object IEnumerator.Current => Current; - public Transform Current => _parent.GetChild(_index); - - public Enumerator(Transform parent) => (_index, _parent) = (-1, parent); - - public bool MoveNext() => ++_index < _parent.childCount; - public void Reset() => _index = -1; - public void Dispose() {} - } - } - - internal struct CachedGuidLoader where T : Object - { - private readonly string _guid; - private T _cached; - - public CachedGuidLoader(string guid) - { - _guid = guid; - _cached = null; - } - - public T Value => - _cached - ? _cached - : _cached = - AssetDatabase.LoadAssetAtPath( - AssetDatabase.GUIDToAssetPath(_guid)); - - public bool IsValid => _guid != null; - - public static implicit operator CachedGuidLoader(string guid) => - new CachedGuidLoader(guid); - } - - internal readonly struct BoundsCornersEnumerable : IEnumerable - { - private readonly Bounds _bounds; - - public BoundsCornersEnumerable(Bounds bounds) => _bounds = bounds; - - public Enumerator GetEnumerator() => new Enumerator(_bounds); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - internal struct Enumerator : IEnumerator - { - private readonly Bounds _bounds; - // 0: before first - // 1..=8: corner - // 9: end - private int _index; - - public Enumerator(Bounds bounds) => (_bounds, _index) = (bounds, 0); - - public bool MoveNext() => ++_index <= 8; - - public void Reset() => _index = 0; - - public Vector3 Current - { - get - { - switch (_index) - { - case 1: - return new Vector3(_bounds.min.x, _bounds.min.y, _bounds.min.z); - case 2: - return new Vector3(_bounds.min.x, _bounds.min.y, _bounds.max.z); - case 3: - return new Vector3(_bounds.min.x, _bounds.max.y, _bounds.min.z); - case 4: - return new Vector3(_bounds.min.x, _bounds.max.y, _bounds.max.z); - case 5: - return new Vector3(_bounds.max.x, _bounds.min.y, _bounds.min.z); - case 6: - return new Vector3(_bounds.max.x, _bounds.min.y, _bounds.max.z); - case 7: - return new Vector3(_bounds.max.x, _bounds.max.y, _bounds.min.z); - case 8: - return new Vector3(_bounds.max.x, _bounds.max.y, _bounds.max.z); - default: - throw new InvalidOperationException(); - } - } - } - - object IEnumerator.Current => Current; - - public void Dispose() - { - } - } - } - - class AnimatorLayerMap - { - private T[] _values = new T[(int)(VRCAvatarDescriptor.AnimLayerType.IKPose + 1)]; - - public static bool IsValid(VRCAvatarDescriptor.AnimLayerType type) - { - switch (type) - { - case VRCAvatarDescriptor.AnimLayerType.Base: - case VRCAvatarDescriptor.AnimLayerType.Additive: - case VRCAvatarDescriptor.AnimLayerType.Gesture: - case VRCAvatarDescriptor.AnimLayerType.Action: - case VRCAvatarDescriptor.AnimLayerType.FX: - case VRCAvatarDescriptor.AnimLayerType.Sitting: - case VRCAvatarDescriptor.AnimLayerType.TPose: - case VRCAvatarDescriptor.AnimLayerType.IKPose: - return true; - case VRCAvatarDescriptor.AnimLayerType.Deprecated0: - default: - return false; - } - } - - public ref T this[VRCAvatarDescriptor.AnimLayerType type] - { - get - { - if (!IsValid(type)) - throw new ArgumentOutOfRangeException(nameof(type), type, null); - - return ref _values[(int)type]; - } - } - } -} diff --git a/Editor/Utils.meta b/Editor/Utils.meta new file mode 100644 index 000000000..1e4a19afe --- /dev/null +++ b/Editor/Utils.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 466c3751f84646bea1307b23d840d885 +timeCreated: 1695885614 \ No newline at end of file diff --git a/Editor/Utils/AnimatorLayerMap.cs b/Editor/Utils/AnimatorLayerMap.cs new file mode 100644 index 000000000..f14d8024d --- /dev/null +++ b/Editor/Utils/AnimatorLayerMap.cs @@ -0,0 +1,44 @@ +#if AAO_VRCSDK3_AVATARS + +using System; +using VRC.SDK3.Avatars.Components; + +namespace Anatawa12.AvatarOptimizer +{ + class AnimatorLayerMap + { + private T[] _values = new T[(int)(VRCAvatarDescriptor.AnimLayerType.IKPose + 1)]; + + public static bool IsValid(VRCAvatarDescriptor.AnimLayerType type) + { + switch (type) + { + case VRCAvatarDescriptor.AnimLayerType.Base: + case VRCAvatarDescriptor.AnimLayerType.Additive: + case VRCAvatarDescriptor.AnimLayerType.Gesture: + case VRCAvatarDescriptor.AnimLayerType.Action: + case VRCAvatarDescriptor.AnimLayerType.FX: + case VRCAvatarDescriptor.AnimLayerType.Sitting: + case VRCAvatarDescriptor.AnimLayerType.TPose: + case VRCAvatarDescriptor.AnimLayerType.IKPose: + return true; + case VRCAvatarDescriptor.AnimLayerType.Deprecated0: + default: + return false; + } + } + + public ref T this[VRCAvatarDescriptor.AnimLayerType type] + { + get + { + if (!IsValid(type)) + throw new ArgumentOutOfRangeException(nameof(type), type, null); + + return ref _values[(int)type]; + } + } + } +} + +#endif \ No newline at end of file diff --git a/Editor/Utils/AnimatorLayerMap.cs.meta b/Editor/Utils/AnimatorLayerMap.cs.meta new file mode 100644 index 000000000..194f92823 --- /dev/null +++ b/Editor/Utils/AnimatorLayerMap.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b80a226112554551ae0b390223bd05e7 +timeCreated: 1695888385 \ No newline at end of file diff --git a/Editor/Utils/CachedGuidLoader.cs b/Editor/Utils/CachedGuidLoader.cs new file mode 100644 index 000000000..01bfb4e6b --- /dev/null +++ b/Editor/Utils/CachedGuidLoader.cs @@ -0,0 +1,29 @@ +using UnityEditor; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer +{ + internal struct CachedGuidLoader where T : Object + { + private readonly string _guid; + private T _cached; + + public CachedGuidLoader(string guid) + { + _guid = guid; + _cached = null; + } + + public T Value => + _cached + ? _cached + : _cached = + AssetDatabase.LoadAssetAtPath( + AssetDatabase.GUIDToAssetPath(_guid)); + + public bool IsValid => _guid != null; + + public static implicit operator CachedGuidLoader(string guid) => + new CachedGuidLoader(guid); + } +} \ No newline at end of file diff --git a/Editor/Utils/CachedGuidLoader.cs.meta b/Editor/Utils/CachedGuidLoader.cs.meta new file mode 100644 index 000000000..5dc58132b --- /dev/null +++ b/Editor/Utils/CachedGuidLoader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b42f06521f054689b74aae5313c72990 +timeCreated: 1695888401 \ No newline at end of file diff --git a/Editor/Utils/ComponentOrGameObject.cs b/Editor/Utils/ComponentOrGameObject.cs new file mode 100644 index 000000000..427ff15de --- /dev/null +++ b/Editor/Utils/ComponentOrGameObject.cs @@ -0,0 +1,50 @@ +using System; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Anatawa12.AvatarOptimizer +{ + public readonly struct ComponentOrGameObject : IEquatable + { + private readonly Object _object; + + public ComponentOrGameObject(Object o) + { + if (!(o is null || o is Component || o is GameObject)) + throw new ArgumentException(); + _object = o; + } + + private ComponentOrGameObject(Object o, int marker) => _object = o; + + public static implicit operator ComponentOrGameObject(GameObject gameObject) => + new ComponentOrGameObject(gameObject, 0); + + public static implicit operator ComponentOrGameObject(Component component) => + new ComponentOrGameObject(component, 0); + + public static implicit operator Object(ComponentOrGameObject componentOrGameObject) => + componentOrGameObject._object; + + public static implicit operator bool(ComponentOrGameObject componentOrGameObject) => + (Object)componentOrGameObject; + + // ReSharper disable InconsistentNaming + // GameObject-like properties + public GameObject gameObject => _object as GameObject ?? ((Component)_object).gameObject; + public Transform transform => gameObject.transform; + public int GetInstanceID() => ((Object)this).GetInstanceID(); + // ReSharper restore InconsistentNaming + public Object Value => _object; + + public bool TryAs(out T gameObject) where T : Object + { + gameObject = _object as T; + return gameObject; + } + + public bool Equals(ComponentOrGameObject other) => Equals(_object, other._object); + public override bool Equals(object obj) => obj is ComponentOrGameObject other && Equals(other); + public override int GetHashCode() => _object != null ? _object.GetHashCode() : 0; + } +} \ No newline at end of file diff --git a/Editor/Utils/ComponentOrGameObject.cs.meta b/Editor/Utils/ComponentOrGameObject.cs.meta new file mode 100644 index 000000000..be3ee54ea --- /dev/null +++ b/Editor/Utils/ComponentOrGameObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e243c57aebee409794351ba8a385b53f +timeCreated: 1698203270 \ No newline at end of file diff --git a/Editor/Utils/MultiDimensionalNativeArray.cs b/Editor/Utils/MultiDimensionalNativeArray.cs new file mode 100644 index 000000000..803771d10 --- /dev/null +++ b/Editor/Utils/MultiDimensionalNativeArray.cs @@ -0,0 +1,57 @@ +using System; +using Unity.Burst; +using Unity.Collections; + +namespace Anatawa12.AvatarOptimizer +{ + [BurstCompile] + struct NativeArray2 : IDisposable where T : unmanaged + { + private NativeArray _array; + private readonly int _firstDimension; + private readonly int _secondDimension; + + public NativeArray2(int firstDimension, int secondDimension, Allocator allocator) + { + _array = new NativeArray(firstDimension * secondDimension, allocator); + _firstDimension = firstDimension; + _secondDimension = secondDimension; + } + + public T this[int first, int second] + { + get => _array[first * _secondDimension + second]; + set => _array[first * _secondDimension + second] = value; + } + + public void Dispose() => _array.Dispose(); + } + + [BurstCompile] + struct NativeArray3 : IDisposable where T : unmanaged + { + private NativeArray _array; + private readonly int _firstDimension; + private readonly int _secondDimension; + private readonly int _thirdDimension; + + public NativeArray3(int firstDimension, int secondDimension, int thirdDimension, Allocator allocator) + { + _array = new NativeArray(firstDimension * secondDimension * thirdDimension, allocator); + _firstDimension = firstDimension; + _secondDimension = secondDimension; + _thirdDimension = thirdDimension; + } + + public T this[int first, int second, int third] + { + get => _array[(first * _secondDimension + second) * _thirdDimension + third]; + set => _array[(first * _secondDimension + second) * _thirdDimension + third] = value; + } + + public NativeSlice this[int first, int second] => + _array.Slice((first * _secondDimension + second) * _thirdDimension, _thirdDimension); + + public void Dispose() => _array.Dispose(); + } +} \ No newline at end of file diff --git a/Editor/Utils/MultiDimensionalNativeArray.cs.meta b/Editor/Utils/MultiDimensionalNativeArray.cs.meta new file mode 100644 index 000000000..8bc5b7749 --- /dev/null +++ b/Editor/Utils/MultiDimensionalNativeArray.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 52baf36f64ba4445aded9e3020c48bf8 +timeCreated: 1698288858 \ No newline at end of file diff --git a/Editor/Utils/Utils.BoundsCornersEnumerable.cs b/Editor/Utils/Utils.BoundsCornersEnumerable.cs new file mode 100644 index 000000000..34f494957 --- /dev/null +++ b/Editor/Utils/Utils.BoundsCornersEnumerable.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer +{ + internal partial class Utils + { + public static BoundsCornersEnumerable Corners(this Bounds bounds) + { + return new BoundsCornersEnumerable(bounds); + } + + public readonly struct BoundsCornersEnumerable : IEnumerable + { + private readonly Bounds _bounds; + + public BoundsCornersEnumerable(Bounds bounds) => _bounds = bounds; + + public Enumerator GetEnumerator() => new Enumerator(_bounds); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private readonly Bounds _bounds; + + // 0: before first + // 1..=8: corner + // 9: end + private int _index; + + public Enumerator(Bounds bounds) => (_bounds, _index) = (bounds, 0); + + public bool MoveNext() => ++_index <= 8; + + public void Reset() => _index = 0; + + public Vector3 Current + { + get + { + switch (_index) + { + case 1: + return new Vector3(_bounds.min.x, _bounds.min.y, _bounds.min.z); + case 2: + return new Vector3(_bounds.min.x, _bounds.min.y, _bounds.max.z); + case 3: + return new Vector3(_bounds.min.x, _bounds.max.y, _bounds.min.z); + case 4: + return new Vector3(_bounds.min.x, _bounds.max.y, _bounds.max.z); + case 5: + return new Vector3(_bounds.max.x, _bounds.min.y, _bounds.min.z); + case 6: + return new Vector3(_bounds.max.x, _bounds.min.y, _bounds.max.z); + case 7: + return new Vector3(_bounds.max.x, _bounds.max.y, _bounds.min.z); + case 8: + return new Vector3(_bounds.max.x, _bounds.max.y, _bounds.max.z); + default: + throw new InvalidOperationException(); + } + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + } + } + } +} diff --git a/Editor/Utils/Utils.BoundsCornersEnumerable.cs.meta b/Editor/Utils/Utils.BoundsCornersEnumerable.cs.meta new file mode 100644 index 000000000..b8c59035e --- /dev/null +++ b/Editor/Utils/Utils.BoundsCornersEnumerable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b0febf7aea4842cbb0ee2b3cac8a47ab +timeCreated: 1695886812 \ No newline at end of file diff --git a/Editor/Utils/Utils.CastDic.cs b/Editor/Utils/Utils.CastDic.cs new file mode 100644 index 000000000..d8ca1def0 --- /dev/null +++ b/Editor/Utils/Utils.CastDic.cs @@ -0,0 +1,45 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Anatawa12.AvatarOptimizer +{ + internal partial class Utils + { + public static DicCaster CastDic() + => new DicCaster(); + + public struct DicCaster + { + public IReadOnlyDictionary CastedDic(IReadOnlyDictionary self) + where TValue : class, TValueCasted + => new CastedDictionary(self); + } + + class CastedDictionary : IReadOnlyDictionary + where TValue : class, TValueCasted + { + private readonly IReadOnlyDictionary _base; + public CastedDictionary(IReadOnlyDictionary @base) => _base = @base; + + public IEnumerator> GetEnumerator() + { + foreach (var (key, value) in _base) + yield return new KeyValuePair(key, value); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public int Count => _base.Count; + public bool ContainsKey(TKey key) => _base.ContainsKey(key); + public TValueCasted this[TKey key] => _base[key]; + public IEnumerable Keys => _base.Keys; + public IEnumerable Values => _base.Values; + + public bool TryGetValue(TKey key, out TValueCasted value) + { + var result = _base.TryGetValue(key, out var original); + value = original; + return result; + } + } + } +} \ No newline at end of file diff --git a/Editor/Utils/Utils.CastDic.cs.meta b/Editor/Utils/Utils.CastDic.cs.meta new file mode 100644 index 000000000..3a15f7cfc --- /dev/null +++ b/Editor/Utils/Utils.CastDic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d147892288754bf9a6721a942cf244b8 +timeCreated: 1695886696 \ No newline at end of file diff --git a/Editor/Utils/Utils.CopyDataFrom.cs b/Editor/Utils/Utils.CopyDataFrom.cs new file mode 100644 index 000000000..0b44a83dd --- /dev/null +++ b/Editor/Utils/Utils.CopyDataFrom.cs @@ -0,0 +1,140 @@ +using System; +using UnityEditor; +using UnityEngine.Assertions; + +namespace Anatawa12.AvatarOptimizer +{ + internal partial class Utils + { + // based on https://gist.github.com/anatawa12/16fbf529c8da4a0fb993c866b1d86512 + public static void CopyDataFrom(this SerializedProperty dest, SerializedProperty source) + { + if (dest.propertyType == SerializedPropertyType.Generic) + CopyBetweenTwoRecursively(source, dest); + else + CopyBetweenTwoValue(source, dest); + + + void CopyBetweenTwoRecursively(SerializedProperty src, SerializedProperty dst) + { + var srcIter = src.Copy(); + var dstIter = dst.Copy(); + var srcEnd = src.GetEndProperty(); + var dstEnd = dst.GetEndProperty(); + var enterChildren = true; + while (srcIter.Next(enterChildren) && !SerializedProperty.EqualContents(srcIter, srcEnd)) + { + var destCheck = dstIter.Next(enterChildren) && !SerializedProperty.EqualContents(dstIter, dstEnd); + Assert.IsTrue(destCheck); + + //Debug.Log($"prop: {dstIter.propertyPath}: {dstIter.propertyType}"); + + switch (dstIter.propertyType) + { + case SerializedPropertyType.FixedBufferSize: + Assert.AreEqual(srcIter.fixedBufferSize, dstIter.fixedBufferSize); + break; + case SerializedPropertyType.Generic: + break; + default: + CopyBetweenTwoValue(srcIter, dstIter); + break; + } + + enterChildren = dstIter.propertyType == SerializedPropertyType.Generic; + } + + { + var destCheck = dstIter.NextVisible(enterChildren) && + !SerializedProperty.EqualContents(dstIter, dstEnd); + Assert.IsFalse(destCheck); + } + } + + void CopyBetweenTwoValue(SerializedProperty src, SerializedProperty dst) + { + switch (dst.propertyType) + { + case SerializedPropertyType.Generic: + throw new InvalidOperationException("for generic, use CopyBetweenTwoRecursively"); + case SerializedPropertyType.Integer: + dst.intValue = src.intValue; + break; + case SerializedPropertyType.Boolean: + dst.boolValue = src.boolValue; + break; + case SerializedPropertyType.Float: + dst.floatValue = src.floatValue; + break; + case SerializedPropertyType.String: + dst.stringValue = src.stringValue; + break; + case SerializedPropertyType.Color: + dst.colorValue = src.colorValue; + break; + case SerializedPropertyType.ObjectReference: + dst.objectReferenceValue = src.objectReferenceValue; + break; + case SerializedPropertyType.LayerMask: + dst.intValue = src.intValue; + break; + case SerializedPropertyType.Enum: + dst.intValue = src.intValue; + break; + case SerializedPropertyType.Vector2: + dst.vector2Value = src.vector2Value; + break; + case SerializedPropertyType.Vector3: + dst.vector3Value = src.vector3Value; + break; + case SerializedPropertyType.Vector4: + dst.vector4Value = src.vector4Value; + break; + case SerializedPropertyType.Rect: + dst.rectValue = src.rectValue; + break; + case SerializedPropertyType.ArraySize: + dst.intValue = src.intValue; + break; + case SerializedPropertyType.Character: + dst.intValue = src.intValue; + break; + case SerializedPropertyType.AnimationCurve: + dst.animationCurveValue = src.animationCurveValue; + break; + case SerializedPropertyType.Bounds: + dst.boundsValue = src.boundsValue; + break; + case SerializedPropertyType.Gradient: + //dst.gradientValue = src.gradientValue; + //break; + throw new InvalidOperationException("unsupported type: Gradient"); + case SerializedPropertyType.Quaternion: + dst.quaternionValue = src.quaternionValue; + break; + case SerializedPropertyType.ExposedReference: + dst.exposedReferenceValue = src.exposedReferenceValue; + break; + case SerializedPropertyType.FixedBufferSize: + throw new InvalidOperationException("unsupported type: FixedBufferSize"); + case SerializedPropertyType.Vector2Int: + dst.vector2IntValue = src.vector2IntValue; + break; + case SerializedPropertyType.Vector3Int: + dst.vector3IntValue = src.vector3IntValue; + break; + case SerializedPropertyType.RectInt: + dst.rectIntValue = src.rectIntValue; + break; + case SerializedPropertyType.BoundsInt: + dst.boundsIntValue = src.boundsIntValue; + break; + case SerializedPropertyType.ManagedReference: + throw new InvalidOperationException("unsupported type: ManagedReference"); + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } +} \ No newline at end of file diff --git a/Editor/Utils/Utils.CopyDataFrom.cs.meta b/Editor/Utils/Utils.CopyDataFrom.cs.meta new file mode 100644 index 000000000..9761c0436 --- /dev/null +++ b/Editor/Utils/Utils.CopyDataFrom.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8e3f1ae810904cd8ad322243c8fd9703 +timeCreated: 1695886587 \ No newline at end of file diff --git a/Editor/Utils/Utils.ObjectReferencePropertiesEnumerable.cs b/Editor/Utils/Utils.ObjectReferencePropertiesEnumerable.cs new file mode 100644 index 000000000..b4c025a05 --- /dev/null +++ b/Editor/Utils/Utils.ObjectReferencePropertiesEnumerable.cs @@ -0,0 +1,90 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEditor; + +namespace Anatawa12.AvatarOptimizer +{ + internal partial class Utils + { + public static ObjectReferencePropertiesEnumerable ObjectReferenceProperties(this SerializedObject obj) + => new ObjectReferencePropertiesEnumerable(obj); + + public readonly struct ObjectReferencePropertiesEnumerable : IEnumerable + { + private readonly SerializedObject _obj; + + public ObjectReferencePropertiesEnumerable(SerializedObject obj) => _obj = obj; + + public Enumerator GetEnumerator() => new Enumerator(_obj); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private readonly SerializedProperty _iterator; + + public Enumerator(SerializedObject obj) => _iterator = obj.GetIterator(); + + public bool MoveNext() + { + while (true) + { + bool enterChildren; + switch (_iterator.propertyType) + { + case SerializedPropertyType.String: + case SerializedPropertyType.Integer: + case SerializedPropertyType.Boolean: + case SerializedPropertyType.Float: + case SerializedPropertyType.Color: + case SerializedPropertyType.ObjectReference: + case SerializedPropertyType.LayerMask: + case SerializedPropertyType.Enum: + case SerializedPropertyType.Vector2: + case SerializedPropertyType.Vector3: + case SerializedPropertyType.Vector4: + case SerializedPropertyType.Rect: + case SerializedPropertyType.ArraySize: + case SerializedPropertyType.Character: + case SerializedPropertyType.AnimationCurve: + case SerializedPropertyType.Bounds: + case SerializedPropertyType.Gradient: + case SerializedPropertyType.Quaternion: + case SerializedPropertyType.FixedBufferSize: + case SerializedPropertyType.Vector2Int: + case SerializedPropertyType.Vector3Int: + case SerializedPropertyType.RectInt: + case SerializedPropertyType.BoundsInt: + enterChildren = false; + break; + case SerializedPropertyType.Generic: + case SerializedPropertyType.ExposedReference: + case SerializedPropertyType.ManagedReference: + default: + enterChildren = true; + break; + } + + if (!_iterator.Next(enterChildren)) return false; + if (_iterator.propertyType == SerializedPropertyType.ObjectReference) + return true; + } + } + + public void Reset() + { + var obj = _iterator.serializedObject; + Dispose(); + this = new Enumerator(obj); + } + + public SerializedProperty Current => _iterator; + object IEnumerator.Current => Current; + + public void Dispose() + { + } + } + } + } +} \ No newline at end of file diff --git a/Editor/Utils/Utils.ObjectReferencePropertiesEnumerable.cs.meta b/Editor/Utils/Utils.ObjectReferencePropertiesEnumerable.cs.meta new file mode 100644 index 000000000..2df0decfc --- /dev/null +++ b/Editor/Utils/Utils.ObjectReferencePropertiesEnumerable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3afedfb6fd1b45b28d978c5f63de68b6 +timeCreated: 1695888802 \ No newline at end of file diff --git a/Editor/Utils/Utils.TransformDirectChildrenEnumerable.cs b/Editor/Utils/Utils.TransformDirectChildrenEnumerable.cs new file mode 100644 index 000000000..23ff8481a --- /dev/null +++ b/Editor/Utils/Utils.TransformDirectChildrenEnumerable.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer +{ + internal static partial class Utils + { + public static TransformDirectChildrenEnumerable DirectChildrenEnumerable(this Transform transform) => + new TransformDirectChildrenEnumerable(transform); + + internal readonly struct TransformDirectChildrenEnumerable : IEnumerable + { + private readonly Transform _parent; + + public TransformDirectChildrenEnumerable(Transform parent) + { + + _parent = parent; + } + + public Enumerator GetEnumerator() => new Enumerator(_parent); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private int _index; + private readonly Transform _parent; + object IEnumerator.Current => Current; + public Transform Current => _parent.GetChild(_index); + + public Enumerator(Transform parent) => (_index, _parent) = (-1, parent); + + public bool MoveNext() => ++_index < _parent.childCount; + public void Reset() => _index = -1; + + public void Dispose() + { + } + } + } + } +} \ No newline at end of file diff --git a/Editor/Utils/Utils.TransformDirectChildrenEnumerable.cs.meta b/Editor/Utils/Utils.TransformDirectChildrenEnumerable.cs.meta new file mode 100644 index 000000000..b50dbe6a2 --- /dev/null +++ b/Editor/Utils/Utils.TransformDirectChildrenEnumerable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6a7d6d6e9c5847b69df179e1f4f6717e +timeCreated: 1695886361 \ No newline at end of file diff --git a/Editor/Utils/Utils.TransformParentEnumerable.cs b/Editor/Utils/Utils.TransformParentEnumerable.cs new file mode 100644 index 000000000..32762e95b --- /dev/null +++ b/Editor/Utils/Utils.TransformParentEnumerable.cs @@ -0,0 +1,43 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer +{ + internal partial class Utils + { + public static TransformParentEnumerable ParentEnumerable(this Transform transform) => + new TransformParentEnumerable(transform); + + public readonly struct TransformParentEnumerable : IEnumerable + { + private readonly Transform _transform; + + public TransformParentEnumerable(Transform transform) => _transform = transform; + public Enumerator GetEnumerator() => new Enumerator(_transform); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + object IEnumerator.Current => Current; + public Transform Current { get; private set; } + private readonly Transform _initial; + + public Enumerator(Transform transform) => _initial = Current = transform; + + public bool MoveNext() + { + Current = Current != null ? Current.parent : null; + return Current != null; + } + + public void Reset() => Current = _initial; + + public void Dispose() + { + } + } + } + } +} \ No newline at end of file diff --git a/Editor/Utils/Utils.TransformParentEnumerable.cs.meta b/Editor/Utils/Utils.TransformParentEnumerable.cs.meta new file mode 100644 index 000000000..2c34e89fb --- /dev/null +++ b/Editor/Utils/Utils.TransformParentEnumerable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 49ee7a99e9814981b535d47c922256e3 +timeCreated: 1695886300 \ No newline at end of file diff --git a/Editor/Utils/Utils.cs b/Editor/Utils/Utils.cs new file mode 100644 index 000000000..db5f345a0 --- /dev/null +++ b/Editor/Utils/Utils.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using JetBrains.Annotations; +using UnityEditor; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer +{ + internal static partial class Utils + { + private static CachedGuidLoader _toonLitShader = "affc81f3d164d734d8f13053effb1c5c"; + public static Shader ToonLitShader => _toonLitShader.Value; + + private static CachedGuidLoader _mergeTextureHelper = "2d4f01f29e91494bb5eafd4c99153ab0"; + public static Shader MergeTextureHelper => _mergeTextureHelper.Value; + + private static CachedGuidLoader _previewHereTex = "617775211fe634657ae06fc9f81b6ceb"; + public static Texture2D PreviewHereTex => _previewHereTex.Value; + + public static void HorizontalLine(bool marginTop = true, bool marginBottom = true) + { + const float margin = 17f / 2; + var maxHeight = 1f; + if (marginTop) maxHeight += margin; + if (marginBottom) maxHeight += margin; + + var rect = GUILayoutUtility.GetRect( + EditorGUIUtility.fieldWidth, float.MaxValue, + 1, maxHeight, GUIStyle.none); + if (marginTop && marginBottom) + rect.y += rect.height / 2 - 0.5f; + else if (marginTop) + rect.y += rect.height - 1f; + else if (marginBottom) + rect.y += 0; + rect.height = 1; + EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 1)); + } + + public static void FlattenMapping(this Dictionary self) + { + foreach (var key in self.Keys.ToArray()) + { + var value = self[key]; + while (value != null && self.TryGetValue(value, out var mapped)) + value = mapped; + self[key] = value; + } + } + + [ContractAnnotation("root:null => notnull")] + [ContractAnnotation("root:notnull => canbenull")] + public static string RelativePath(Transform root, Transform child) + { + if (root == child) return ""; + + var pathSegments = new List(); + while (child != root) + { + if (child == null) return null; + pathSegments.Add(child.name); + child = child.transform.parent; + } + + pathSegments.Reverse(); + return string.Join("/", pathSegments); + } + + public static T GetOrAddComponent(this GameObject go) where T : Component + { + var component = go.GetComponent(); + if (!component) component = go.AddComponent(); + return component; + } + +#if AAO_VRCSDK3_AVATARS + public static Transform GetTarget(this VRC.Dynamics.VRCPhysBoneBase physBoneBase) => + physBoneBase.rootTransform ? physBoneBase.rootTransform : physBoneBase.transform; + + public static IEnumerable GetAffectedTransforms(this VRC.Dynamics.VRCPhysBoneBase physBoneBase) + { + var ignores = new HashSet(physBoneBase.ignoreTransforms); + var queue = new Queue(); + queue.Enqueue(physBoneBase.GetTarget()); + + while (queue.Count != 0) + { + var transform = queue.Dequeue(); + yield return transform; + + foreach (var child in transform.DirectChildrenEnumerable()) + if (!ignores.Contains(child)) + queue.Enqueue(child); + } + } +#endif + + public static GameObject NewGameObject(string name, Transform parent) + { + var rootObject = new GameObject(name); + rootObject.transform.parent = parent; + rootObject.transform.localPosition = Vector3.zero; + rootObject.transform.localRotation = Quaternion.identity; + rootObject.transform.localScale = Vector3.one; + return rootObject; + } + + public static IEnumerable<(T, T)> ZipWithNext(this IEnumerable enumerable) + { + using (var enumerator = enumerable.GetEnumerator()) + { + if (!enumerator.MoveNext()) yield break; + var prev = enumerator.Current; + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + yield return (prev, current); + prev = current; + } + } + } + + // Properties detailed first and nothing last + public static IEnumerable<(string prop, string rest)> FindSubPaths(string prop, char sep) + { + var rest = ""; + for (;;) + { + yield return (prop, rest); + + var index = prop.LastIndexOf(sep); + if (index == -1) yield break; + + rest = prop.Substring(index) + rest; + prop = prop.Substring(0, index); + } + } + + private class EmptyDictionaryHolder + { + public static readonly IReadOnlyDictionary Empty = + new ReadOnlyDictionary(new Dictionary()); + } + + public static IReadOnlyDictionary EmptyDictionary() => + EmptyDictionaryHolder.Empty; + + public static void Deconstruct(this KeyValuePair keyValuePair, out TKey key, + out TValue value) + { + key = keyValuePair.Key; + value = keyValuePair.Value; + } + + public static IEnumerable> ZipByKey( + this IReadOnlyDictionary first, IReadOnlyDictionary second) + { + foreach (var key in first.Keys.ToArray()) + { + if (!second.TryGetValue(key, out var secondValue)) secondValue = default; + + yield return new KeyValuePair(key, (first[key], secondValue)); + } + + foreach (var key in second.Keys.ToArray()) + if (!first.ContainsKey(key)) + yield return new KeyValuePair(key, (default, second[key])); + } + + [CanBeNull] + public static Type GetTypeFromName(string name) => + AppDomain.CurrentDomain.GetAssemblies().Select(assembly => assembly.GetType(name)) + .FirstOrDefault(type => !(type == null)); + + public static T DistinctSingleOrDefaultIfNoneOrMultiple(this IEnumerable enumerable) + { + using (var enumerator = enumerable.GetEnumerator()) + { + if (!enumerator.MoveNext()) return default; + var found = enumerator.Current; + var eqOperator = EqualityComparer.Default; + + while (enumerator.MoveNext()) + { + var nextValue = enumerator.Current; + if (!eqOperator.Equals(found, nextValue)) + return default; + } + + return found; + } + } + } +} diff --git a/Editor/Utils.cs.meta b/Editor/Utils/Utils.cs.meta similarity index 100% rename from Editor/Utils.cs.meta rename to Editor/Utils/Utils.cs.meta diff --git a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef index 8ecfa0215..10cc54940 100644 --- a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef +++ b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef @@ -1,18 +1,18 @@ { "name": "com.anatawa12.avatar-optimizer.editor", "references": [ - "GUID:8d9ff9f975de4bf0b86c6cc50332d028", - "GUID:b23bdfe8d18741a29e869995d47026d0", - "GUID:c03011c168164e1a954438f640cbd728", - "GUID:295ffbe0b63504ae3a7879cf089501ba", - "GUID:8264e72376854221affe9980c91c2fff", - "GUID:f69eeb3e25674f4a9bd20e6d7e69e0e6", - "GUID:2633ab9fa94544a69517fc9a1bc143c9", - "GUID:b9880ca0b6584453a2627bd3c038759f", - "GUID:8542dbf824204440a818dbc2377cb4d6", - "GUID:2665a8d13d1b3f18800f46e256720795", - "GUID:62ced99b048af7f4d8dfe4bed8373d76", - "GUID:fe747755f7b44e048820525b07f9b956" + "com.anatawa12.avatar-optimizer.internal.error-reporter.runtime", + "com.anatawa12.avatar-optimizer.internal.error-reporter.editor", + "com.anatawa12.custom-localization-for-editor-extension", + "com.anatawa12.custom-localization-for-editor-extension.runtime", + "com.anatawa12.avatar-optimizer.runtime", + "com.anatawa12.avatar-optimizer.internal.unsafe", + "com.anatawa12.avatar-optimizer.internal.prefab-safe-set.editor", + "com.anatawa12.avatar-optimizer.internal.prefab-safe-set", + "Unity.Burst", + "nadena.dev.ndmf", + "nadena.dev.ndmf.runtime", + "com.anatawa12.avatar-optimizer.api.editor" ], "includePlatforms": [ "Editor" @@ -38,6 +38,11 @@ "name": "nadena.dev.modular-avatar", "expression": "(,1.8-a)", "define": "LEGACY_MODULAR_AVATAR" + }, + { + "name": "com.vrchat.avatars", + "expression": "", + "define": "AAO_VRCSDK3_AVATARS" } ], "noEngineReferences": false diff --git a/Internal/ErrorReporter/Editor/BuildReport.cs b/Internal/ErrorReporter/Editor/BuildReport.cs index 5238c073c..1a90c1ebe 100644 --- a/Internal/ErrorReporter/Editor/BuildReport.cs +++ b/Internal/ErrorReporter/Editor/BuildReport.cs @@ -4,10 +4,10 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using JetBrains.Annotations; using UnityEditor; using UnityEngine; -using VRC.SDK3.Avatars.Components; using Object = UnityEngine.Object; namespace Anatawa12.AvatarOptimizer.ErrorReporting @@ -31,8 +31,8 @@ public class BuildReport [SerializeField] internal List avatars = new List(); - internal ConditionalWeakTable AvatarsByObject = - new ConditionalWeakTable(); + internal ConditionalWeakTable AvatarsByObject = + new ConditionalWeakTable(); internal AvatarReport CurrentAvatar { get; set; } internal static BuildReport CurrentReport @@ -81,18 +81,18 @@ internal static void SaveReport() ErrorReportUI.ReloadErrorReport(); } - internal AvatarReport Initialize([NotNull] VRCAvatarDescriptor descriptor) + internal AvatarReport Initialize([NotNull] GameObject avatarGameObject) { - if (descriptor == null) throw new ArgumentNullException(nameof(descriptor)); + if (avatarGameObject == null) throw new ArgumentNullException(nameof(avatarGameObject)); AvatarReport report = new AvatarReport(); - report.objectRef = new ObjectRef(descriptor.gameObject); + report.objectRef = new ObjectRef(avatarGameObject); avatars.Add(report); report.successful = true; - report.logs.AddRange(ComponentValidation.ValidateAll(descriptor.gameObject)); + report.logs.AddRange(ComponentValidation.ValidateAll(avatarGameObject)); - AvatarsByObject.Add(descriptor, report); + AvatarsByObject.Add(avatarGameObject, report); return report; } @@ -103,6 +103,27 @@ internal static ErrorLog Log(ReportLevel level, string code, string[] strings, A strings[i] = strings[i] ?? ""; var errorLog = new ErrorLog(level, code, strings, assembly); + var builder = new StringBuilder("BuildReport: "); + builder.Append(code); + foreach (var s in strings) + builder.Append(", '").Append(s).Append("'"); + switch (level) + { + case ReportLevel.Validation: + case ReportLevel.Error: + case ReportLevel.InternalError: + Debug.LogError(builder.ToString()); + break; + case ReportLevel.Info: + Debug.Log(builder.ToString()); + break; + case ReportLevel.Warning: + Debug.LogWarning(builder.ToString()); + break; + default: + throw new ArgumentOutOfRangeException(nameof(level), level, null); + } + var avatarReport = CurrentReport.CurrentAvatar; if (avatarReport == null) { diff --git a/Internal/ErrorReporter/Editor/BuildReportContext.cs b/Internal/ErrorReporter/Editor/BuildReportContext.cs index d1add9d31..d2d8ee340 100644 --- a/Internal/ErrorReporter/Editor/BuildReportContext.cs +++ b/Internal/ErrorReporter/Editor/BuildReportContext.cs @@ -17,15 +17,15 @@ public class BuildReportContext : IExtensionContext public void OnActivate(BuildContext context) { var state = context.GetState(); - var descriptor = context.AvatarDescriptor; - if (descriptor == null) throw new Exception(); + var avatarGameObject = context.AvatarRootObject; + if (avatarGameObject == null) throw new Exception(); var report = state.Report; if (state.Report == null) { // If it's in unity editor, I assume building avatar. if (!EditorApplication.isPlayingOrWillChangePlaymode) BuildReport.Clear(); - state.Report = report = BuildReport.CurrentReport.Initialize(descriptor); + state.Report = report = BuildReport.CurrentReport.Initialize(avatarGameObject); } BuildReport.CurrentReport.CurrentAvatar = report; @@ -39,8 +39,6 @@ public void OnDeactivate(BuildContext context) BuildReport.SaveReport(); if (avatar.logs.Any()) ErrorReportUI.OpenErrorReportUIFor(avatar); - else - ErrorReportUI.MaybeOpenErrorReportUI(); if (!successful) throw new Exception("Avatar processing failed"); } } diff --git a/Internal/ErrorReporter/Editor/ErrorReportUI.cs b/Internal/ErrorReporter/Editor/ErrorReportUI.cs index 1fb84cd7b..856556147 100644 --- a/Internal/ErrorReporter/Editor/ErrorReportUI.cs +++ b/Internal/ErrorReporter/Editor/ErrorReportUI.cs @@ -139,7 +139,7 @@ private void CreateGUI() if (Selection.gameObjects.Length == 1) { - activeAvatarObject = Utils.FindAvatarInParents(Selection.activeGameObject.transform)?.gameObject; + activeAvatarObject = Utils.FindAvatarTransformInParents(Selection.activeGameObject.transform)?.gameObject; if (activeAvatarObject != null) { var foundAvatarPath = Utils.RelativePath(null, activeAvatarObject); diff --git a/Internal/ErrorReporter/Editor/Utils.cs b/Internal/ErrorReporter/Editor/Utils.cs index fb0d0279b..ea4e8bcca 100644 --- a/Internal/ErrorReporter/Editor/Utils.cs +++ b/Internal/ErrorReporter/Editor/Utils.cs @@ -1,24 +1,17 @@ using System; using System.Collections.Generic; using JetBrains.Annotations; +using nadena.dev.ndmf.runtime; using UnityEngine; -using VRC.SDK3.Avatars.Components; namespace Anatawa12.AvatarOptimizer.ErrorReporting { internal static class Utils { [CanBeNull] - public static VRCAvatarDescriptor FindAvatarInParents([CanBeNull] Transform transform) + public static Transform FindAvatarTransformInParents([CanBeNull] Transform transform) { - while (transform != null) - { - if (transform.GetComponent() is VRCAvatarDescriptor descriptor) - return descriptor; - transform = transform.parent; - } - - return null; + return RuntimeUtil.FindAvatarInParents(transform); } /// diff --git a/Internal/ErrorReporter/Editor/com.anatawa12.avatar-optimizer.internal.error-reporter.editor.asmdef b/Internal/ErrorReporter/Editor/com.anatawa12.avatar-optimizer.internal.error-reporter.editor.asmdef index 2a1d1ffa6..03dc40fc9 100644 --- a/Internal/ErrorReporter/Editor/com.anatawa12.avatar-optimizer.internal.error-reporter.editor.asmdef +++ b/Internal/ErrorReporter/Editor/com.anatawa12.avatar-optimizer.internal.error-reporter.editor.asmdef @@ -1,11 +1,11 @@ { "name": "com.anatawa12.avatar-optimizer.internal.error-reporter.editor", "references": [ - "GUID:8d9ff9f975de4bf0b86c6cc50332d028", - "GUID:295ffbe0b63504ae3a7879cf089501ba", - "GUID:8264e72376854221affe9980c91c2fff", - "GUID:b23bdfe8d18741a29e869995d47026d0", - "GUID:62ced99b048af7f4d8dfe4bed8373d76" + "com.anatawa12.custom-localization-for-editor-extension", + "com.anatawa12.custom-localization-for-editor-extension.runtime", + "com.anatawa12.avatar-optimizer.internal.error-reporter.runtime", + "nadena.dev.ndmf", + "nadena.dev.ndmf.runtime" ], "includePlatforms": [ "Editor" @@ -20,6 +20,12 @@ ], "autoReferenced": false, "defineConstraints": [], - "versionDefines": [], + "versionDefines": [ + { + "name": "com.vrchat.avatars", + "expression": "", + "define": "AAO_VRCSDK3_AVATARS" + } + ], "noEngineReferences": false } \ No newline at end of file diff --git a/Internal/ErrorReporter/Runtime/com.anatawa12.avatar-optimizer.internal.error-reporter.runtime.asmdef b/Internal/ErrorReporter/Runtime/com.anatawa12.avatar-optimizer.internal.error-reporter.runtime.asmdef index 8756d6034..4bd879898 100644 --- a/Internal/ErrorReporter/Runtime/com.anatawa12.avatar-optimizer.internal.error-reporter.runtime.asmdef +++ b/Internal/ErrorReporter/Runtime/com.anatawa12.avatar-optimizer.internal.error-reporter.runtime.asmdef @@ -1,7 +1,7 @@ { "name": "com.anatawa12.avatar-optimizer.internal.error-reporter.runtime", "references": [ - "GUID:8264e72376854221affe9980c91c2fff" + "com.anatawa12.custom-localization-for-editor-extension.runtime" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Internal/PrefabSafeSet/Editor/BasicTypeEditors.cs b/Internal/PrefabSafeSet/Editor/BasicTypeEditors.cs new file mode 100644 index 000000000..43b7cb974 --- /dev/null +++ b/Internal/PrefabSafeSet/Editor/BasicTypeEditors.cs @@ -0,0 +1,171 @@ +using System; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Anatawa12.AvatarOptimizer.PrefabSafeSet +{ + abstract class BasicEditorBase : EditorBase + { + private protected override float GetAddRegionSize() => EditorGUIUtility.singleLineHeight; + + private protected override void OnGUIAddRegion(Rect position) => + EditorGUI.LabelField(position, EditorStatics.ToAdd, EditorStatics.AddNotSupported); + + protected BasicEditorBase(SerializedProperty property, int nestCount) : base(property, nestCount) + { + } + } + + class IntegerEditorImpl : BasicEditorBase + { + public IntegerEditorImpl(SerializedProperty property, int nestCount) : base(property, nestCount) + { + } + + private protected override long GetValue(SerializedProperty prop) => prop.longValue; + private protected override void SetValue(SerializedProperty prop, long value) => prop.longValue = value; + + protected override float FieldHeight(GUIContent label) => + EditorGUI.GetPropertyHeight(SerializedPropertyType.Integer, label); + + protected override long Field(Rect position, GUIContent label, long value) => + EditorGUI.LongField(position, label, value); + } + + class StringEditorImpl : BasicEditorBase + { + public StringEditorImpl(SerializedProperty property, int nestCount) : base(property, nestCount) + { + } + + private protected override string GetValue(SerializedProperty prop) => prop.stringValue; + private protected override void SetValue(SerializedProperty prop, string value) => prop.stringValue = value; + + protected override float FieldHeight(GUIContent label) => + EditorGUI.GetPropertyHeight(SerializedPropertyType.String, label); + + protected override string Field(Rect position, GUIContent label, string value) => + EditorGUI.TextField(position, label, value); + } + + class ObjectEditorImpl : EditorBase + { + public ObjectEditorImpl(SerializedProperty property, int nestCount) : base(property, nestCount) + { + } + + private protected override Object GetValue(SerializedProperty prop) => prop.objectReferenceValue; + private protected override void SetValue(SerializedProperty prop, Object value) => + prop.objectReferenceValue = value; + + private protected override float GetAddRegionSize() => EditorGUIUtility.singleLineHeight; + + private protected override void OnGUIAddRegion(Rect position) + { + var current = Event.current; + var eventType = current.type; + switch (eventType) + { + case EventType.DragUpdated: + case EventType.DragPerform: + case EventType.DragExited: + { + var controlId = + GUIUtility.GetControlID("s_PPtrHash".GetHashCode(), FocusType.Keyboard, position); + position = EditorGUI.PrefixLabel(position, controlId, EditorStatics.ToAdd); + HandleDragEvent(position, controlId, current); + break; + } + default: + { + var addValue = Field(position, EditorStatics.ToAdd, null); + if (addValue != null) EditorUtil.GetElementOf(addValue).Add(); + break; + } + } + } + + protected override float FieldHeight(GUIContent label) => + EditorGUI.GetPropertyHeight(SerializedPropertyType.ObjectReference, label); + + protected override Object Field(Rect position, GUIContent label, Object value) + { + bool allowSceneObjects = false; + var targetObject = FakeSlot.serializedObject.targetObject; + if (targetObject != null && !EditorUtility.IsPersistent(targetObject)) + allowSceneObjects = true; + return EditorGUI.ObjectField(position, label, value, null, allowSceneObjects); + } + + public void HandleDragEvent(Rect position, int lastControlId, Event @event) + { + switch (@event.type) + { + case EventType.DragUpdated: + case EventType.DragPerform: + if (position.Contains(@event.mousePosition) && GUI.enabled) + { + var objectReferences = DragAndDrop.objectReferences; + var referencesCache = new Object[1]; + var flag3 = false; + foreach (var object1 in objectReferences) + { + referencesCache[0] = object1; + var object2 = ValidateObjectFieldAssignment(referencesCache, FakeSlot); + if (object2 == null) continue; + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + if (@event.type == EventType.DragPerform) + { + EditorUtil.GetElementOf(object2).EnsureAdded(); + flag3 = true; + DragAndDrop.activeControlID = 0; + } + else + DragAndDrop.activeControlID = lastControlId; + } + + if (flag3) + { + GUI.changed = true; + DragAndDrop.AcceptDrag(); + } + } + + break; + case EventType.DragExited: + if (GUI.enabled) + HandleUtility.Repaint(); + break; + } + } + + private static readonly MethodInfo ValidateObjectFieldAssignmentMethod = + typeof(EditorGUI).GetMethod("ValidateObjectFieldAssignment", BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly Type ObjectFieldValidatorOptionsType = + typeof(EditorGUI).Assembly.GetType("UnityEditor.EditorGUI+ObjectFieldValidatorOptions"); + + private static Object ValidateObjectFieldAssignment(Object[] references, + SerializedProperty property) + { + if (ValidateObjectFieldAssignmentMethod == null) + { + Debug.LogError( + "Compatibility with Unity broke: can't find ValidateObjectFieldAssignment method in EditorGUI"); + return null; + } + + if (ObjectFieldValidatorOptionsType == null) + { + Debug.LogError( + "Compatibility with Unity broke: can't find ObjectFieldValidatorOptions type in EditorGUI"); + return null; + } + + return ValidateObjectFieldAssignmentMethod.Invoke(null, + new[] { references, null, property, Enum.ToObject(ObjectFieldValidatorOptionsType, 0) }) as Object; + } + } +} \ No newline at end of file diff --git a/Internal/PrefabSafeSet/Editor/BasicTypeEditors.cs.meta b/Internal/PrefabSafeSet/Editor/BasicTypeEditors.cs.meta new file mode 100644 index 000000000..f12968778 --- /dev/null +++ b/Internal/PrefabSafeSet/Editor/BasicTypeEditors.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d0ed527003804d7a8c3c6f60adacdb0d +timeCreated: 1698310231 \ No newline at end of file diff --git a/Internal/PrefabSafeSet/Editor/PrefabSafeSetEditor.cs b/Internal/PrefabSafeSet/Editor/PrefabSafeSetEditor.cs index abd78cde5..dff5c05d9 100644 --- a/Internal/PrefabSafeSet/Editor/PrefabSafeSetEditor.cs +++ b/Internal/PrefabSafeSet/Editor/PrefabSafeSetEditor.cs @@ -49,39 +49,35 @@ internal class ObjectsEditor : PropertyDrawer private int GetNestCount(Object obj) => _nestCountCache != -1 ? _nestCountCache : _nestCountCache = PrefabSafeSetUtil.PrefabNestCount(obj); - private readonly Dictionary _caches = - new Dictionary(); + private readonly Dictionary _caches = + new Dictionary(); [CanBeNull] - private Editor GetCache(SerializedProperty property) + private EditorBase GetCache(SerializedProperty property) { if (!_caches.TryGetValue(property.propertyPath, out var cached)) { var prop = property.FindPropertyRelative(Names.FakeSlot); - if (IsSupportedPropType(prop.propertyType)) - { - _caches[property.propertyPath] = cached = - new Editor(property, GetNestCount(property.serializedObject.targetObject)); - } - else - { - _caches[property.propertyPath] = cached = null; - } + _caches[property.propertyPath] = + cached = GetEditorImpl(prop.propertyType, property, GetNestCount(property.serializedObject.targetObject)); } return cached; } - private static bool IsSupportedPropType(SerializedPropertyType type) + private static EditorBase GetEditorImpl(SerializedPropertyType type, SerializedProperty property, int nestCount) { switch (type) { case SerializedPropertyType.Integer: + return new IntegerEditorImpl(property, nestCount); + case SerializedPropertyType.String: + return new StringEditorImpl(property, nestCount); + case SerializedPropertyType.ObjectReference: + return new ObjectEditorImpl(property, nestCount); case SerializedPropertyType.Boolean: case SerializedPropertyType.Float: - case SerializedPropertyType.String: case SerializedPropertyType.Color: - case SerializedPropertyType.ObjectReference: case SerializedPropertyType.LayerMask: case SerializedPropertyType.Enum: case SerializedPropertyType.Vector2: @@ -97,177 +93,8 @@ private static bool IsSupportedPropType(SerializedPropertyType type) case SerializedPropertyType.Vector3Int: case SerializedPropertyType.RectInt: case SerializedPropertyType.BoundsInt: - return true; default: - return false; - } - } - - private class Editor : EditorBase - { - public Editor(SerializedProperty property, int nestCount) : base(property, nestCount) - { - } - - public EditorUtil GetEditorUtil() => EditorUtil; - public SerializedProperty GetFakeSlot() => FakeSlot; - - private protected override object GetValue(SerializedProperty prop) - { - switch (prop.propertyType) - { - case SerializedPropertyType.Integer: - return prop.intValue; - case SerializedPropertyType.Boolean: - return prop.boolValue; - case SerializedPropertyType.Float: - return prop.floatValue; - case SerializedPropertyType.String: - return prop.stringValue; - case SerializedPropertyType.Color: - return prop.colorValue; - case SerializedPropertyType.ObjectReference: - return prop.objectReferenceValue; - case SerializedPropertyType.LayerMask: - return (LayerMask) prop.intValue; - case SerializedPropertyType.Enum: - return prop.enumValueIndex; - case SerializedPropertyType.Vector2: - return prop.vector2Value; - case SerializedPropertyType.Vector3: - return prop.vector3Value; - case SerializedPropertyType.Vector4: - return prop.vector4Value; - case SerializedPropertyType.Rect: - return prop.rectValue; - case SerializedPropertyType.ArraySize: - return prop.intValue; - case SerializedPropertyType.Character: - return (char) prop.intValue; - case SerializedPropertyType.AnimationCurve: - return prop.animationCurveValue; - case SerializedPropertyType.Bounds: - return prop.boundsValue; - case SerializedPropertyType.ExposedReference: - return prop.exposedReferenceValue; - case SerializedPropertyType.Vector2Int: - return prop.vector2IntValue; - case SerializedPropertyType.Vector3Int: - return prop.vector3IntValue; - case SerializedPropertyType.RectInt: - return prop.rectIntValue; - case SerializedPropertyType.BoundsInt: - return prop.boundsIntValue; - default: - throw new InvalidOperationException(); - } - } - - private protected override void SetValue(SerializedProperty prop, object value) - { - switch (prop.propertyType) - { - case SerializedPropertyType.Integer: - prop.intValue = (int)value; - break; - case SerializedPropertyType.Boolean: - prop.boolValue = (bool)value; - break; - case SerializedPropertyType.Float: - prop.floatValue = (float)value; - break; - case SerializedPropertyType.String: - prop.stringValue = (string)value; - break; - case SerializedPropertyType.Color: - prop.colorValue = (Color)value; - break; - case SerializedPropertyType.ObjectReference: - prop.objectReferenceValue = (Object)value; - break; - case SerializedPropertyType.LayerMask: - prop.intValue = (LayerMask)value; - break; - case SerializedPropertyType.Enum: - prop.enumValueIndex = (int)value; - break; - case SerializedPropertyType.Vector2: - prop.vector2Value = (Vector2)value; - break; - case SerializedPropertyType.Vector3: - prop.vector3Value = (Vector3)value; - break; - case SerializedPropertyType.Vector4: - prop.vector4Value = (Vector4)value; - break; - case SerializedPropertyType.Rect: - prop.rectValue = (Rect)value; - break; - case SerializedPropertyType.ArraySize: - prop.intValue = (int)value; - break; - case SerializedPropertyType.Character: - prop.intValue = (char)value; - break; - case SerializedPropertyType.AnimationCurve: - prop.animationCurveValue = (AnimationCurve)value; - break; - case SerializedPropertyType.Bounds: - prop.boundsValue = (Bounds)value; - break; - case SerializedPropertyType.ExposedReference: - prop.exposedReferenceValue = (Object)value; - break; - case SerializedPropertyType.Vector2Int: - prop.vector2IntValue = (Vector2Int)value; - break; - case SerializedPropertyType.Vector3Int: - prop.vector3IntValue = (Vector3Int)value; - break; - case SerializedPropertyType.RectInt: - prop.rectIntValue = (RectInt)value; - break; - case SerializedPropertyType.BoundsInt: - prop.boundsIntValue = (BoundsInt)value; - break; - default: - throw new InvalidOperationException(); - } - } - - private protected override float GetAddRegionSize() => EditorGUIUtility.singleLineHeight; - - private protected override void OnGUIAddRegion(Rect position) - { - if (FakeSlot.propertyType == SerializedPropertyType.ObjectReference) - { - var current = Event.current; - var eventType = current.type; - switch (eventType) - { - case EventType.DragUpdated: - case EventType.DragPerform: - case EventType.DragExited: - { - var controlId = GUIUtility.GetControlID("s_PPtrHash".GetHashCode(), FocusType.Keyboard, position); - position = EditorGUI.PrefixLabel(position, controlId, EditorStatics.ToAdd); - HandleDragEvent(position, controlId, current, this); - break; - } - default: - { - SetValue(FakeSlot, default); - EditorGUI.PropertyField(position, FakeSlot, EditorStatics.ToAdd); - var addValue = FakeSlot.objectReferenceValue; - if (addValue != null) EditorUtil.GetElementOf(addValue).Add(); - break; - } - } - } - else - { - EditorGUI.LabelField(position, EditorStatics.ToAdd, EditorStatics.AddNotSupported); - } + return null; } } @@ -311,9 +138,9 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten } } - private bool PropertyGUI(Rect position, SerializedProperty property, GUIContent label, Editor editor) + private bool PropertyGUI(Rect position, SerializedProperty property, GUIContent label, EditorBase editor) { - var hasOverride = editor.GetEditorUtil().HasPrefabOverride(); + var hasOverride = editor.HasPrefabOverride(); if (hasOverride) EditorGUI.BeginProperty(position, label, property); @@ -329,55 +156,13 @@ private bool PropertyGUI(Rect position, SerializedProperty property, GUIContent int lastControlId = GetLastControlId(); - HandleDragEvent(position, lastControlId, @event, editor); + (editor as ObjectEditorImpl)?.HandleDragEvent(position, lastControlId, @event); if (hasOverride) EditorGUI.EndProperty(); return isExpanded; } - private static void HandleDragEvent(Rect position, int lastControlId, Event @event, Editor editor) - { - switch (@event.type) - { - case EventType.DragUpdated: - case EventType.DragPerform: - if (position.Contains(@event.mousePosition) && GUI.enabled) - { - var objectReferences = DragAndDrop.objectReferences; - var referencesCache = new Object[1]; - var flag3 = false; - foreach (var object1 in objectReferences) - { - referencesCache[0] = object1; - var object2 = ValidateObjectFieldAssignment(referencesCache, editor.GetFakeSlot()); - if (object2 == null) continue; - DragAndDrop.visualMode = DragAndDropVisualMode.Copy; - if (@event.type == EventType.DragPerform) - { - editor.GetEditorUtil().GetElementOf(object2).EnsureAdded(); - flag3 = true; - DragAndDrop.activeControlID = 0; - } - else - DragAndDrop.activeControlID = lastControlId; - } - - if (flag3) - { - GUI.changed = true; - DragAndDrop.AcceptDrag(); - } - } - - break; - case EventType.DragExited: - if (GUI.enabled) - HandleUtility.Repaint(); - break; - } - } - private static readonly FieldInfo LastControlIdField = typeof(EditorGUIUtility).GetField("s_LastControlID", BindingFlags.Static | BindingFlags.NonPublic); @@ -391,34 +176,6 @@ private static int GetLastControlId() return (int)LastControlIdField.GetValue(null); } - - private static readonly MethodInfo ValidateObjectFieldAssignmentMethod = - typeof(EditorGUI).GetMethod("ValidateObjectFieldAssignment", BindingFlags.Static | BindingFlags.NonPublic); - - private static readonly Type ObjectFieldValidatorOptionsType = - typeof(EditorGUI).Assembly.GetType("UnityEditor.EditorGUI+ObjectFieldValidatorOptions"); - - private static Object ValidateObjectFieldAssignment(Object[] references, - SerializedProperty property) - { - if (ValidateObjectFieldAssignmentMethod == null) - { - Debug.LogError( - "Compatibility with Unity broke: can't find ValidateObjectFieldAssignment method in EditorGUI"); - return null; - } - - if (ObjectFieldValidatorOptionsType == null) - { - Debug.LogError( - "Compatibility with Unity broke: can't find ObjectFieldValidatorOptions type in EditorGUI"); - return null; - } - - return ValidateObjectFieldAssignmentMethod.Invoke(null, - new[] { references, null, property, Enum.ToObject(ObjectFieldValidatorOptionsType, 0) }) as Object; - } - } /// @@ -433,10 +190,17 @@ internal static class Names public const string Removes = nameof(PrefabLayer.removes); } - internal abstract class EditorBase + internal abstract class EditorBase + { + public abstract float GetPropertyHeight(); + public abstract void OnGUI(Rect position); + public abstract bool HasPrefabOverride(); + } + + internal abstract class EditorBase : EditorBase { [NotNull] protected readonly SerializedProperty FakeSlot; - protected readonly EditorUtil EditorUtil; + internal readonly EditorUtil EditorUtil; public EditorBase(SerializedProperty property, int nestCount) { @@ -452,12 +216,16 @@ public EditorBase(SerializedProperty property, int nestCount) private protected abstract float GetAddRegionSize(); private protected abstract void OnGUIAddRegion(Rect position); - public float GetPropertyHeight() => - EditorUtil.ElementsCount * (FieldHeight() + EditorGUIUtility.standardVerticalSpacing) + public override bool HasPrefabOverride() => EditorUtil.HasPrefabOverride(); + + private static readonly GUIContent content = new GUIContent("Element 0"); + + public override float GetPropertyHeight() => + EditorUtil.ElementsCount * (FieldHeight(content) + EditorGUIUtility.standardVerticalSpacing) + GetAddRegionSize(); // position is - public void OnGUI(Rect position) + public override void OnGUI(Rect position) { var elementI = 0; var newLabel = new GUIContent(""); @@ -511,7 +279,7 @@ public void OnGUI(Rect position) break; } - position.y += FieldHeight() + EditorGUIUtility.standardVerticalSpacing; + position.y += FieldHeight(newLabel) + EditorGUIUtility.standardVerticalSpacing; } action?.Invoke(); @@ -557,14 +325,7 @@ private ModificationKind OnePrefabElement(Rect position, GUIContent label, IElem return result; } - protected virtual float FieldHeight() => EditorGUI.GetPropertyHeight(FakeSlot); - - protected virtual T Field(Rect position, GUIContent label, T value) - { - SetValue(FakeSlot, value); - EditorGUI.PropertyField(position, FakeSlot, label); - value = GetValue(FakeSlot); - return value; - } + protected abstract float FieldHeight(GUIContent label); + protected abstract T Field(Rect position, GUIContent label, T value); } } diff --git a/Internal/PrefabSafeSet/Editor/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.editor.asmdef b/Internal/PrefabSafeSet/Editor/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.editor.asmdef index 14cb96343..ba54ca0a9 100644 --- a/Internal/PrefabSafeSet/Editor/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.editor.asmdef +++ b/Internal/PrefabSafeSet/Editor/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.editor.asmdef @@ -1,9 +1,9 @@ { "name": "com.anatawa12.avatar-optimizer.internal.prefab-safe-set.editor", "references": [ - "GUID:295ffbe0b63504ae3a7879cf089501ba", - "GUID:8264e72376854221affe9980c91c2fff", - "GUID:8542dbf824204440a818dbc2377cb4d6" + "com.anatawa12.custom-localization-for-editor-extension", + "com.anatawa12.custom-localization-for-editor-extension.runtime", + "com.anatawa12.avatar-optimizer.internal.prefab-safe-set" ], "includePlatforms": [ "Editor" diff --git a/Internal/PrefabSafeSet/Runtime/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.asmdef b/Internal/PrefabSafeSet/Runtime/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.asmdef index e810254fb..d54199533 100644 --- a/Internal/PrefabSafeSet/Runtime/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.asmdef +++ b/Internal/PrefabSafeSet/Runtime/com.anatawa12.avatar-optimizer.internal.prefab-safe-set.asmdef @@ -1,7 +1,7 @@ { "name": "com.anatawa12.avatar-optimizer.internal.prefab-safe-set", "references": [ - "GUID:8264e72376854221affe9980c91c2fff" + "com.anatawa12.custom-localization-for-editor-extension.runtime" ], "includePlatforms": [], "excludePlatforms": [], @@ -12,6 +12,12 @@ ], "autoReferenced": false, "defineConstraints": [], - "versionDefines": [], + "versionDefines": [ + { + "name": "com.vrchat.avatars", + "expression": "", + "define": "AAO_VRCSDK3_AVATARS" + } + ], "noEngineReferences": false } \ No newline at end of file diff --git a/Internal/PrefabSafeSet/Runtime/generate.ts b/Internal/PrefabSafeSet/Runtime/generate.ts index 65574a91e..c79b4cf6d 100644 --- a/Internal/PrefabSafeSet/Runtime/generate.ts +++ b/Internal/PrefabSafeSet/Runtime/generate.ts @@ -1,4 +1,11 @@ +const AAO_VRCSDK3_AVATARS = "AAO_VRCSDK3_AVATARS"; +function conditional(flag: string, inner: () => void) { + console.log(`#if ${flag}`) + inner(); + console.log(`#endif`) +} + function generate(type: string) { console.log(` [Serializable]`) console.log(` public class ${type}Set : PrefabSafeSet<${type}, ${type}Set.Layer>`) @@ -18,7 +25,9 @@ console.log(`using System.Collections.Generic;`) console.log(`using System.Reflection;`) console.log(`using JetBrains.Annotations;`) console.log(`using UnityEngine;`) -console.log(`using VRC.Dynamics;`) +conditional(AAO_VRCSDK3_AVATARS, () => { + console.log(`using VRC.Dynamics;`) +}); console.log(`using Object = UnityEngine.Object;`) console.log(``) console.log(`namespace Anatawa12.AvatarOptimizer.PrefabSafeSet`) @@ -27,6 +36,8 @@ generate("SkinnedMeshRenderer") generate("MeshRenderer") generate("Material") generate("String") -generate("VRCPhysBoneBase") +conditional(AAO_VRCSDK3_AVATARS, () => { + generate("VRCPhysBoneBase") +}); generate("Transform") console.log(`}`) diff --git a/Internal/PrefabSafeSet/Runtime/generated.cs b/Internal/PrefabSafeSet/Runtime/generated.cs index ed385f383..80edae605 100644 --- a/Internal/PrefabSafeSet/Runtime/generated.cs +++ b/Internal/PrefabSafeSet/Runtime/generated.cs @@ -5,7 +5,9 @@ using System.Reflection; using JetBrains.Annotations; using UnityEngine; +#if AAO_VRCSDK3_AVATARS using VRC.Dynamics; +#endif using Object = UnityEngine.Object; namespace Anatawa12.AvatarOptimizer.PrefabSafeSet @@ -46,6 +48,7 @@ public StringSet(Object outerObject) : base(outerObject) [Serializable] public class Layer : PrefabLayer{} } +#if AAO_VRCSDK3_AVATARS [Serializable] public class VRCPhysBoneBaseSet : PrefabSafeSet { @@ -55,6 +58,7 @@ public VRCPhysBoneBaseSet(Object outerObject) : base(outerObject) [Serializable] public class Layer : PrefabLayer{} } +#endif [Serializable] public class TransformSet : PrefabSafeSet { diff --git a/Localization/en.po b/Localization/en.po index 6a4e6dc25..881394c91 100644 --- a/Localization/en.po +++ b/Localization/en.po @@ -352,6 +352,9 @@ msgstr "Invert All" msgid "DeleteEditorOnlyGameObjects:NotOnAvatarDescriptor" msgstr "This component must be set on root of Avatar (GameObject with AvatarDescriptor)" +msgid "DeleteEditorOnlyGameObjects:NotOnAvatarRoot" +msgstr "This component must be set on root of Avatar (For VRChat avatars, GameObject with AvatarDescriptor)" + # endregion # region UnusedBonesByReferencesTool diff --git a/Localization/ja.po b/Localization/ja.po index d998d4970..928e82bdf 100644 --- a/Localization/ja.po +++ b/Localization/ja.po @@ -289,6 +289,9 @@ msgstr "すべての有効/無効を入れ替える" msgid "DeleteEditorOnlyGameObjects:NotOnAvatarDescriptor" msgstr "このコンポーネントはアバターの最上位に付ける必要があります。(AvatarDescriptorのあるGameObject)" +msgid "DeleteEditorOnlyGameObjects:NotOnAvatarRoot" +msgstr "このコンポーネントはアバターの最上位に付ける必要があります。(VRChatのアバターでは、AvatarDescriptorのあるGameObject)" + # endregion # region UnusedBonesByReferencesTool diff --git a/Runtime/AvatarGlobalComponent.cs b/Runtime/AvatarGlobalComponent.cs index b013719fc..4dbf08b31 100644 --- a/Runtime/AvatarGlobalComponent.cs +++ b/Runtime/AvatarGlobalComponent.cs @@ -1,6 +1,4 @@ using Anatawa12.AvatarOptimizer.ErrorReporting; -using UnityEngine; -using VRC.SDKBase; namespace Anatawa12.AvatarOptimizer { diff --git a/Runtime/AvatarTagComponent.cs b/Runtime/AvatarTagComponent.cs index 0239b354d..b554cde87 100644 --- a/Runtime/AvatarTagComponent.cs +++ b/Runtime/AvatarTagComponent.cs @@ -1,8 +1,6 @@ using System; using Anatawa12.AvatarOptimizer.ErrorReporting; using UnityEngine; -using UnityEngine.Animations; -using VRC.SDKBase; namespace Anatawa12.AvatarOptimizer { @@ -14,7 +12,10 @@ namespace Anatawa12.AvatarOptimizer */ [DefaultExecutionOrder(-9990)] // run before av3emu [ExecuteAlways] - internal abstract class AvatarTagComponent : MonoBehaviour, IEditorOnly + internal abstract class AvatarTagComponent : MonoBehaviour +#if AAO_VRCSDK3_AVATARS + , VRC.SDKBase.IEditorOnly +#endif { private void OnValidate() { @@ -27,4 +28,4 @@ private void OnDestroy() ErrorReporterRuntime.TriggerChange(); } } -} +} \ No newline at end of file diff --git a/Runtime/ClearEndpointPosition.cs b/Runtime/ClearEndpointPosition.cs index c9598bda9..aa32f9099 100644 --- a/Runtime/ClearEndpointPosition.cs +++ b/Runtime/ClearEndpointPosition.cs @@ -1,3 +1,5 @@ +#if AAO_VRCSDK3_AVATARS + using UnityEngine; using UnityEngine.Animations; using VRC.Dynamics; @@ -13,3 +15,5 @@ internal class ClearEndpointPosition : AvatarTagComponent { } } + +#endif \ No newline at end of file diff --git a/Runtime/MergePhysBone.cs b/Runtime/MergePhysBone.cs index 810efe974..2fce4eb19 100644 --- a/Runtime/MergePhysBone.cs +++ b/Runtime/MergePhysBone.cs @@ -1,3 +1,5 @@ +#if AAO_VRCSDK3_AVATARS + using System; using System.Collections.Generic; using Anatawa12.AvatarOptimizer.ErrorReporting; @@ -244,3 +246,5 @@ internal enum CollidersSettings Override, } } + +#endif \ No newline at end of file diff --git a/Runtime/TraceAndOptimize.cs b/Runtime/TraceAndOptimize.cs index 356738ab1..87382af52 100644 --- a/Runtime/TraceAndOptimize.cs +++ b/Runtime/TraceAndOptimize.cs @@ -34,13 +34,6 @@ internal class TraceAndOptimize : AvatarGlobalComponent [ToggleLeft] public bool mmdWorldCompatibility = true; - // for compatibility, this is not inside AdvancedSettings but this is part of Advanced Settings - [NotKeyable] - [InspectorName("Use Advanced Animator Parser")] - [Tooltip("Advanced Animator Parser will parse your AnimatorController, including layer structure.")] - [ToggleLeft] - public bool advancedAnimatorParser = true; - [NotKeyable] public AdvancedSettings advancedSettings; @@ -49,9 +42,6 @@ public struct AdvancedSettings { [Tooltip("Exclude some GameObjects from Trace and Optimize")] public GameObject[] exclusions; - [Tooltip("Use Legacy algorithm for Remove Unused Objects")] - [ToggleLeft] - public bool useLegacyGc; [Tooltip("Add GC Debug Components instead of setting GC components")] [ToggleLeft] public bool gcDebug; @@ -59,6 +49,8 @@ public struct AdvancedSettings [ToggleLeft] public bool noConfigureMergeBone; [ToggleLeft] + public bool noActivenessAnimation; + [ToggleLeft] public bool skipFreezingNonAnimatedBlendShape; [ToggleLeft] public bool skipFreezingMeaninglessBlendShape; diff --git a/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef b/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef index 05092ff79..32e8ead24 100644 --- a/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef +++ b/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef @@ -1,10 +1,10 @@ { "name": "com.anatawa12.avatar-optimizer.runtime", "references": [ - "GUID:b23bdfe8d18741a29e869995d47026d0", - "GUID:8264e72376854221affe9980c91c2fff", - "GUID:8542dbf824204440a818dbc2377cb4d6", - "GUID:2665a8d13d1b3f18800f46e256720795" + "com.anatawa12.avatar-optimizer.internal.error-reporter.runtime", + "com.anatawa12.custom-localization-for-editor-extension.runtime", + "com.anatawa12.avatar-optimizer.internal.prefab-safe-set", + "Unity.Burst" ], "includePlatforms": [], "excludePlatforms": [], @@ -18,6 +18,12 @@ ], "autoReferenced": false, "defineConstraints": [], - "versionDefines": [], + "versionDefines": [ + { + "name": "com.vrchat.avatars", + "expression": "", + "define": "AAO_VRCSDK3_AVATARS" + } + ], "noEngineReferences": false } \ No newline at end of file diff --git a/Test~/AnimatorParser/AnimatorTest.cs b/Test~/AnimatorParser/AnimatorTest.cs index 8fa8c885a..587a7d5bd 100644 --- a/Test~/AnimatorParser/AnimatorTest.cs +++ b/Test~/AnimatorParser/AnimatorTest.cs @@ -141,7 +141,7 @@ public void TestLayer21_Animate16ToConst100Weight0() => [Test] public void TestParseWhole() { - var parser = new AnimatorParser(true, true); + var parser = new AnimatorParser(true); // execute var parsed = parser.AdvancedParseAnimatorController(_prefab, _controller, @@ -197,7 +197,7 @@ public void TestParseWhole() [Test] public void TestParseWholeWithExternalWeightChanges() { - var parser = new AnimatorParser(true, true); + var parser = new AnimatorParser(true); var externallyWeightChanged = new AnimatorLayerWeightMap { @@ -252,7 +252,7 @@ public void TestParseWholeWithExternalWeightChanges() [Test] public void TestOneLayerOverrides() { - var parser = new AnimatorParser(true, true); + var parser = new AnimatorParser(true); var controller = TestUtils.GetAssetAt("AnimatorParser/OneLayerOverrideController.overrideController"); var animate0To100 = TestUtils.GetAssetAt("AnimatorParser/Animate0To100.anim"); var animate1To100 = TestUtils.GetAssetAt("AnimatorParser/Animate1To100.anim"); @@ -268,7 +268,7 @@ public void TestOneLayerOverrides() [Test] public void TestTwoLayerOverrides() { - var parser = new AnimatorParser(true, true); + var parser = new AnimatorParser(true); var controller = TestUtils.GetAssetAt("AnimatorParser/TwoLayerOverrideController.overrideController"); var animate0To100 = TestUtils.GetAssetAt("AnimatorParser/Animate0To100.anim"); var animate1To100 = TestUtils.GetAssetAt("AnimatorParser/Animate1To100.anim"); @@ -289,7 +289,7 @@ private void LayerTest(int layerIndex, string layerName, string propertyName, AnimationProperty property, AnimatorLayerBlendingMode blendingMode = AnimatorLayerBlendingMode.Override) { - var parser = new AnimatorParser(true, true); + var parser = new AnimatorParser(true); // preconditions Assert.That(_controller.layers[layerIndex].name, Is.EqualTo(layerName)); diff --git a/Test~/BuildAssetBundle.cs b/Test~/BuildAssetBundle.cs index e9a13c9c8..cfa0c7ffb 100644 --- a/Test~/BuildAssetBundle.cs +++ b/Test~/BuildAssetBundle.cs @@ -26,3 +26,4 @@ public void Build() } } } + diff --git a/Test~/MergePhysBoneTest.cs b/Test~/MergePhysBoneTest.cs index 895f09d82..cff4abd20 100644 --- a/Test~/MergePhysBoneTest.cs +++ b/Test~/MergePhysBoneTest.cs @@ -1,3 +1,5 @@ +#if AAO_VRCSDK3_AVATARS + using Anatawa12.AvatarOptimizer.PrefabSafeSet; using Anatawa12.AvatarOptimizer.Processors; using NUnit.Framework; @@ -35,9 +37,9 @@ public void CopyTest() var child2 = Utils.NewGameObject("child2", root.transform); var child2Component = AddConfigure(child2); var merged = Utils.NewGameObject("merged", root.transform); - CreateMergePhysBone(merged, child1Component, child2Component); + var mergePhysBone = CreateMergePhysBone(merged, child1Component, child2Component); - new MergePhysBoneProcessor().Process(new OptimizerSession(root, false)); + MergePhysBoneProcessor.DoMerge(mergePhysBone); var mergedPhysBone = merged.GetComponent(); Assert.That(mergedPhysBone.pull, Is.EqualTo(0.4f)); @@ -88,7 +90,7 @@ public void OverrideTest() mergePhysBone.allowCollisionConfig.filter.allowOthers = false; mergePhysBone.allowCollisionConfig.filter.allowSelf = true; - new MergePhysBoneProcessor().Process(new OptimizerSession(root, false)); + MergePhysBoneProcessor.DoMerge(mergePhysBone); var mergedPhysBone = merged.GetComponent(); Assert.That(mergedPhysBone.pull, Is.EqualTo(0.4f)); @@ -120,3 +122,5 @@ VRCPhysBone AddConfigureRandom(GameObject go) } } } + +#endif \ No newline at end of file diff --git a/Test~/ObjectMappingTest.cs b/Test~/ObjectMappingTest.cs index f268e037d..46b748aa7 100644 --- a/Test~/ObjectMappingTest.cs +++ b/Test~/ObjectMappingTest.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using NUnit.Framework; using UnityEditor; using UnityEngine; @@ -26,15 +27,13 @@ public void MoveObjectTest() Assert.That( rootMapper.MapBinding(B("child1/child11", typeof(GameObject), "m_Enabled")), Is.EqualTo(B("child2/child11", typeof(GameObject), "m_Enabled"))); - + Assert.That( rootMapper.MapBinding(B("child1/child11/child111", typeof(GameObject), "m_Enabled")), Is.EqualTo(B("child2/child11/child111", typeof(GameObject), "m_Enabled"))); var child1Mapper = built.CreateAnimationMapper(child1); - Assert.That( - child1Mapper.MapBinding(B("child11", typeof(GameObject), "m_Enabled")), - Is.EqualTo(Default)); + ExAsset.MapBindingRemoved(child1Mapper, B("child11", typeof(GameObject), "m_Enabled")); } [Test] @@ -51,22 +50,14 @@ public void RecordRemoveGameObject() var built = builder.BuildObjectMapping(); var rootMapper = built.CreateAnimationMapper(root); - Assert.That( - rootMapper.MapBinding(B("child1/child11", typeof(GameObject), "m_Enabled")), - Is.EqualTo(Default)); + ExAsset.MapBindingRemoved(rootMapper, B("child1/child11", typeof(GameObject), "m_Enabled")); - Assert.That( - rootMapper.MapBinding(B("child1", typeof(GameObject), "m_Enabled")), - Is.EqualTo(B("child1", typeof(GameObject), "m_Enabled"))); + ExAsset.MapBindingUnchanged(rootMapper, B("child1", typeof(GameObject), "m_Enabled")); - Assert.That( - rootMapper.MapBinding(B("child1/child11/child111", typeof(GameObject), "m_Enabled")), - Is.EqualTo(Default)); + ExAsset.MapBindingRemoved(rootMapper, B("child1/child11/child111", typeof(GameObject), "m_Enabled")); var child1Mapper = built.CreateAnimationMapper(child1); - Assert.That( - child1Mapper.MapBinding(B("child11", typeof(GameObject), "m_Enabled")), - Is.EqualTo(Default)); + ExAsset.MapBindingRemoved(child1Mapper, B("child11", typeof(GameObject), "m_Enabled")); } [Test] @@ -87,9 +78,7 @@ public void RecordMoveComponentTest() var rootMapper = built.CreateAnimationMapper(root); // should not affect to GameObject - Assert.That( - rootMapper.MapBinding(B("child1", typeof(GameObject), "m_Enabled")), - Is.EqualTo(B("child1", typeof(GameObject), "m_Enabled"))); + ExAsset.MapBindingUnchanged(rootMapper, B("child1", typeof(GameObject), "m_Enabled")); // but should affect to component Assert.That( @@ -117,14 +106,10 @@ public void RecordRemoveComponentTest() var rootMapper = built.CreateAnimationMapper(root); // should not affect to GameObject itself - Assert.That( - rootMapper.MapBinding(B("child1", typeof(GameObject), "m_Enabled")), - Is.EqualTo(B("child1", typeof(GameObject), "m_Enabled"))); + ExAsset.MapBindingUnchanged(rootMapper, B("child1", typeof(GameObject), "m_Enabled")); // but should affect to component - Assert.That( - rootMapper.MapBinding(B("child1", typeof(SkinnedMeshRenderer), "blendShapes.test")), - Is.EqualTo(Default)); + ExAsset.MapBindingRemoved(rootMapper, B("child1", typeof(SkinnedMeshRenderer), "blendShapes.test")); // check for component replication Assert.That(built.MapComponentInstance(child1ComponentId, out var component), Is.True); @@ -146,9 +131,7 @@ public void RecordMovePropertyTest() var rootMapper = built.CreateAnimationMapper(root); // should not affect to other component - Assert.That( - rootMapper.MapBinding(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.test")), - Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.test"))); + ExAsset.MapBindingUnchanged(rootMapper, B("child2", typeof(SkinnedMeshRenderer), "blendShapes.test")); // but should affect to component Assert.That( @@ -220,14 +203,10 @@ public void RecordRemovePropertyTest() var rootMapper = built.CreateAnimationMapper(root); // should not affect to other component - Assert.That( - rootMapper.MapBinding(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.test")), - Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.test"))); + ExAsset.MapBindingUnchanged(rootMapper, B("child2", typeof(SkinnedMeshRenderer), "blendShapes.test")); // but should affect to component - Assert.That( - rootMapper.MapBinding(B("child1", typeof(SkinnedMeshRenderer), "blendShapes.test")), - Is.EqualTo(Default)); + ExAsset.MapBindingRemoved(rootMapper, B("child1", typeof(SkinnedMeshRenderer), "blendShapes.test")); } [Test] @@ -262,15 +241,11 @@ public void RecordMovePropertyThenComponentThenPropertyTest() rootMapper.MapBinding(B("child1", typeof(SkinnedMeshRenderer), "blendShapes.moved")), Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.movedChanged"))); - Assert.That( - rootMapper.MapBinding(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child1")), - Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child1"))); + ExAsset.MapBindingUnchanged(rootMapper, B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child1")); Assert.That( rootMapper.MapBinding(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child2")), Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child2Changed"))); - Assert.That( - rootMapper.MapBinding(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child2Other")), - Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child2Other"))); + ExAsset.MapBindingUnchanged(rootMapper, B("child2", typeof(SkinnedMeshRenderer), "blendShapes.child2Other")); Assert.That( rootMapper.MapBinding(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.moved")), Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "blendShapes.movedChanged"))); @@ -323,30 +298,137 @@ public void RecordRemovePropertyThenMergeComponent() var rootMapper = built.CreateAnimationMapper(root); - Assert.That( - rootMapper.MapBinding(B("child1", typeof(SkinnedMeshRenderer), "m_Enabled")), - Is.EqualTo(Default)); - Assert.That( - rootMapper.MapBinding(B("child2", typeof(SkinnedMeshRenderer), "m_Enabled")), - Is.EqualTo(B("child2", typeof(SkinnedMeshRenderer), "m_Enabled"))); + ExAsset.MapBindingRemoved(rootMapper, B("child1", typeof(SkinnedMeshRenderer), "m_Enabled")); + ExAsset.MapBindingUnchanged(rootMapper, B("child2", typeof(SkinnedMeshRenderer), "m_Enabled")); // check for component replication Assert.That(built.MapComponentInstance(child1ComponentId, out var component), Is.True); Assert.That(component, Is.SameAs(child2Component)); } + [Test] + public void RecordMoveProperty() + { + var root = new GameObject(); + var child1 = Utils.NewGameObject("child1", root.transform); + var child11 = Utils.NewGameObject("child11", child1.transform); + var child11Component = child11.AddComponent(); + var child2 = Utils.NewGameObject("child2", root.transform); + + var builder = new ObjectMappingBuilder(root); + builder.RecordMoveProperty(child11Component, "blendShapes.child11", "blendShapes.child11Changed"); + child11.transform.parent = child2.transform; + builder.RecordMoveProperty(child11Component, "blendShapes.moved", "blendShapes.movedChanged"); + + var built = builder.BuildObjectMapping(); + + var rootMapper = built.CreateAnimationMapper(root); + + Assert.That( + rootMapper.MapBinding(B("child1/child11", typeof(SkinnedMeshRenderer), "blendShapes.child11")), + Is.EqualTo(B("child2/child11", typeof(SkinnedMeshRenderer), "blendShapes.child11Changed"))); + Assert.That( + rootMapper.MapBinding(B("child1/child11", typeof(SkinnedMeshRenderer), "blendShapes.moved")), + Is.EqualTo(B("child2/child11", typeof(SkinnedMeshRenderer), "blendShapes.movedChanged"))); + + Assert.That(built.MapComponentInstance(child11Component.GetInstanceID(), out var component), Is.False); + } + + [Test] + public void MovePropertyOfGameObject() + { + var root = new GameObject(); + var child1 = Utils.NewGameObject("child1", root.transform); + var child11 = Utils.NewGameObject("child11", child1.transform); + + var builder = new ObjectMappingBuilder(root); + builder.RecordMoveProperty(child11, "m_IsActive", child1, "m_IsActive"); + + var built = builder.BuildObjectMapping(); + + var rootMapper = built.CreateAnimationMapper(root); + + Assert.That( + rootMapper.MapBinding(B("child1/child11", typeof(GameObject), "m_IsActive")), + Is.EqualTo(B("child1", typeof(GameObject), "m_IsActive"))); + } + + + [Test] + public void CopyProperty() + { + var root = new GameObject(); + var child1 = Utils.NewGameObject("child1", root.transform); + var child11 = Utils.NewGameObject("child11", child1.transform); + var child12 = Utils.NewGameObject("child12", child1.transform); + var child13 = Utils.NewGameObject("child13", child1.transform); + var child14 = Utils.NewGameObject("child14", child1.transform); + + var builder = new ObjectMappingBuilder(root); + builder.RecordCopyProperty(child11, "m_IsActive", + child12, "m_IsActive"); + builder.RecordCopyProperty(child11, "m_IsActive", + child13, "m_IsActive"); + builder.RecordCopyProperty(child12, "m_IsActive", + child14, "m_IsActive"); + + var built = builder.BuildObjectMapping(); + + var rootMapper = built.CreateAnimationMapper(root); + + Assert.That( + rootMapper.MapBinding(Curve("child1/child11", typeof(GameObject), "m_IsActive"))?.Select(ToTuple), + Is.Not.Null.And.EquivalentTo(new[] + { + B("child1/child11", typeof(GameObject), "m_IsActive"), + B("child1/child12", typeof(GameObject), "m_IsActive"), + B("child1/child13", typeof(GameObject), "m_IsActive"), + B("child1/child14", typeof(GameObject), "m_IsActive"), + })); + + Assert.That( + rootMapper.MapBinding(Curve("child1/child12", typeof(GameObject), "m_IsActive"))?.Select(ToTuple), + Is.Not.Null.And.EquivalentTo(new[] + { + B("child1/child12", typeof(GameObject), "m_IsActive"), + B("child1/child14", typeof(GameObject), "m_IsActive"), + })); + } private static (string, Type, string) B(string path, Type type, string prop) => (path, type, prop); - private static (string, Type, string) Default = default; + + private static (string, Type, string) ToTuple(EditorCurveBinding binding) => + (binding.path, binding.type, binding.propertyName); + + private static EditorCurveBinding Curve(string path, Type type, string prop) + => EditorCurveBinding.PPtrCurve(path, type, prop); } - static class ObjectMappingTestUtils + static class ExAsset { + public static void MapBindingRemoved(AnimationObjectMapper mapping, (string, Type, string) binding) + { + var result = mapping.MapBinding(EditorCurveBinding.PPtrCurve(binding.Item1, binding.Item2, binding.Item3)); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.EqualTo(0)); + } + + public static void MapBindingUnchanged(AnimationObjectMapper mapping, (string, Type, string) binding) + { + var result = mapping.MapBinding(EditorCurveBinding.PPtrCurve(binding.Item1, binding.Item2, binding.Item3)); + + Assert.That(result, Is.Null); + } + public static (string, Type, string) MapBinding(this AnimationObjectMapper mapping, (string, Type, string) binding) { var result = mapping.MapBinding(EditorCurveBinding.PPtrCurve(binding.Item1, binding.Item2, binding.Item3)); - return (result.path, result.type, result.propertyName); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.EqualTo(1)); + + return (result[0].path, result[0].type, result[0].propertyName); } } } diff --git a/Test~/Runtime/com.anatawa12.avatar-optimizer.test.runtime.asmdef b/Test~/Runtime/com.anatawa12.avatar-optimizer.test.runtime.asmdef index 5d014b4cd..ff2ef0052 100644 --- a/Test~/Runtime/com.anatawa12.avatar-optimizer.test.runtime.asmdef +++ b/Test~/Runtime/com.anatawa12.avatar-optimizer.test.runtime.asmdef @@ -1,8 +1,8 @@ { "name": "com.anatawa12.avatar-optimizer.test.runtime", "references": [ - "GUID:f69eeb3e25674f4a9bd20e6d7e69e0e6", - "GUID:8542dbf824204440a818dbc2377cb4d6" + "com.anatawa12.avatar-optimizer.runtime", + "com.anatawa12.avatar-optimizer.internal.prefab-safe-set" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Test~/TestUtils.cs b/Test~/TestUtils.cs index 9b08b1a22..f14f1ab0d 100644 --- a/Test~/TestUtils.cs +++ b/Test~/TestUtils.cs @@ -1,7 +1,6 @@ using System.IO; using UnityEditor; using UnityEngine; -using VRC.SDK3.Avatars.Components; namespace Anatawa12.AvatarOptimizer.Test { @@ -13,7 +12,9 @@ public static GameObject NewAvatar(string name = null) root.name = name ?? "Test Avatar"; var animator = root.AddComponent(); animator.avatar = AvatarBuilder.BuildGenericAvatar(root, ""); - var descriptor = root.AddComponent(); +#if AAO_VRCSDK3_AVATARS + var descriptor = root.AddComponent(); +#endif return root; } diff --git a/Test~/com.anatawa12.avatar-optimizer.test.asmdef b/Test~/com.anatawa12.avatar-optimizer.test.asmdef index c4e5b070a..739364e38 100644 --- a/Test~/com.anatawa12.avatar-optimizer.test.asmdef +++ b/Test~/com.anatawa12.avatar-optimizer.test.asmdef @@ -1,15 +1,16 @@ { "name": "com.anatawa12.avatar-optimizer.test", "references": [ - "GUID:5718fb738711cd34ea54e9553040911d", - "GUID:3456780c4fb2d324ab9c633d6f1b0ddb", - "GUID:aeaad65d10ca4136b1d0ee8d9bef1709", - "GUID:7e6b618db39145b8a5eb4790d78c2972", - "GUID:f69eeb3e25674f4a9bd20e6d7e69e0e6", - "GUID:8542dbf824204440a818dbc2377cb4d6", - "GUID:b9880ca0b6584453a2627bd3c038759f", - "GUID:27619889b8ba8c24980f49ee34dbb44a", - "GUID:0acc523941302664db1f4e527237feb3" + "VRC.SDK3A", + "VRC.SDKBase", + "com.anatawa12.avatar-optimizer.test.runtime", + "com.anatawa12.avatar-optimizer.editor", + "com.anatawa12.avatar-optimizer.runtime", + "com.anatawa12.avatar-optimizer.internal.prefab-safe-set", + "com.anatawa12.avatar-optimizer.internal.prefab-safe-set.editor", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "nadena.dev.ndmf" ], "includePlatforms": [ "Editor" @@ -25,6 +26,12 @@ ], "autoReferenced": false, "defineConstraints": [], - "versionDefines": [], + "versionDefines": [ + { + "name": "com.vrchat.avatars", + "expression": "", + "define": "AAO_VRCSDK3_AVATARS" + } + ], "noEngineReferences": false } \ No newline at end of file diff --git a/package.json b/package.json index bbe92f72b..48ca778b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.anatawa12.avatar-optimizer", - "version": "1.5.10-beta.0", + "version": "1.6.0-beta.1", "unity": "2019.4", "description": "Set of Anatawa12's Small Avatar Optimization Utilities", "displayName": "Anatawa12's AvatarOptimizer",