diff --git a/.docs/content/docs/reference/remove-zero-sized-polygon/component.png b/.docs/content/docs/reference/remove-zero-sized-polygon/component.png new file mode 100644 index 000000000..760d2ba5e Binary files /dev/null and b/.docs/content/docs/reference/remove-zero-sized-polygon/component.png differ diff --git a/.docs/content/docs/reference/remove-zero-sized-polygon/index.ja.md b/.docs/content/docs/reference/remove-zero-sized-polygon/index.ja.md new file mode 100644 index 000000000..61f6e2c56 --- /dev/null +++ b/.docs/content/docs/reference/remove-zero-sized-polygon/index.ja.md @@ -0,0 +1,38 @@ +--- +title: Remove Zero Sized Polygon +weight: 100 +--- + +# Remove Zero Sized Polygon + +面積がゼロのポリゴンを削除します。 + +このコンポーネントは、SkinnedMeshRendererコンポーネントのあるGameObjectに追加してください。 + +{{< hint warning >}} + +このコンポーネントはビルドの最後の方で実行されるため、[Modifying Edit Skinned Mesh Component](../../component-kind/edit-skinned-mesh-components#modifying-component) では**ありません**。 + +このコンポーネントを[Merge Skinned Mesh](../merge-skinned-mesh)の統合対象となるSkinnedMeshRendererに追加しても効果がありません。 + +{{< /hint >}} + +## 利点 {#benefits} + +面積がゼロのポリゴンを削除することで、描画負荷を減らすことができます。 +見た目に影響を与えることはほとんどありません。 + +## 備考 {#notes} + +モデルファイルでのポリゴンの大きさがゼロであっても、シェーダーによって何かを描画していることがあるため、見た目に影響が出ることがあります。 + +## 設定 {#settings} + +今のところ、このコンポーネントに設定項目はありません。 + +![component.png](component.png) + +## 備考 {#notes} + +このコンポーネントは[Trace and Optimize](../trace-and-optimize)コンポーネントによって自動的に追加されます。 +このコンポーネントを手動で追加するよりも、Trace and Optimizeを使うことをお勧めします。 diff --git a/.docs/content/docs/reference/remove-zero-sized-polygon/index.md b/.docs/content/docs/reference/remove-zero-sized-polygon/index.md new file mode 100644 index 000000000..72fda2586 --- /dev/null +++ b/.docs/content/docs/reference/remove-zero-sized-polygon/index.md @@ -0,0 +1,39 @@ +--- +title: Remove Zero Sized Polygon +weight: 100 +--- + +# Remove Zero Sized Polygon + +Remove polygons whose area are zero. + +This component should be added to a GameObject which has a SkinnedMeshRenderer component. + +{{< hint warning >}} + +Since this component works very late in the build process, this component is **NOT** [Modifying Edit Skinned Mesh Component](../../component-kind/edit-skinned-mesh-components#modifying-component). + +Adding this component to the SkinnedMeshRenderers to be merged by [Merge Skinned Mesh](../merge-skinned-mesh) component has no effect. + +{{< /hint >}} + +## Benefits + +By removing polygons whose area are zero, you can reduce rendering cost. +This will have almost zero effect on the appearance. + +## Notes + +In some shaders, even if size of polygon in model file is zero, something can be rendered so +there may be effect on the appearance. + +## Settings + +This Component doesn't have any configuration for now. + +![component.png](component.png) + +## Notes + +This component will be added by [Trace and Optimize](../trace-and-optimize) component. +I recommend you to use Trace and Optimize instead of adding this component manually. diff --git a/.docs/content/docs/reference/trace-and-optimize/component.png b/.docs/content/docs/reference/trace-and-optimize/component.png index 3ed90a1cd..cf656f785 100644 Binary files a/.docs/content/docs/reference/trace-and-optimize/component.png and b/.docs/content/docs/reference/trace-and-optimize/component.png differ diff --git a/.docs/content/docs/reference/trace-and-optimize/index.ja.md b/.docs/content/docs/reference/trace-and-optimize/index.ja.md index bf9346acf..e25afae91 100644 --- a/.docs/content/docs/reference/trace-and-optimize/index.ja.md +++ b/.docs/content/docs/reference/trace-and-optimize/index.ja.md @@ -21,6 +21,8 @@ aliases: アニメーションなどを走査して、使われていないObject(GameObjectやコンポーネントなど)を自動的に削除します。 - `endボーンを残す` 親が削除されていないendボーン[^endbone]を削除しないようにします。 +- `面積がゼロのポリゴンを自動的に削除する` + 面積がゼロのポリゴンを削除します。 また、以下の設定で自動設定を調節できます。 - `MMDワールドとの互換性` diff --git a/.docs/content/docs/reference/trace-and-optimize/index.md b/.docs/content/docs/reference/trace-and-optimize/index.md index f2ac51c63..a55ff55d3 100644 --- a/.docs/content/docs/reference/trace-and-optimize/index.md +++ b/.docs/content/docs/reference/trace-and-optimize/index.md @@ -21,6 +21,8 @@ Currently the following optimizations are applied automatically. By scanning animation etc., automatically removes unused Objects (e.g. GameObjects, Components). - `Preserve EndBone` Prevents removing end bones[^endbone] whose parent is not removed. +- `Automatically Remove Zero Sized Polygons` + Removes polygons whose area are zero. Also, You can adjust optimization with the following settings - `MMD World Compatibility` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40f1b1a09..5019306d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,14 @@ jobs: hugo-version: '0.111.3' extended: true + - name: Check release is public + if: github.event.inputs.release_kind == 'stable' + run: + if [[ "$(jq '.private == true' < package.json)" == "true" ]]; then + echo "package.json is private" + exit 255 + fi + - name: Update Version Name id: update-version run: | diff --git a/API-Editor/ComponentInformation.cs b/API-Editor/ComponentInformation.cs index 3b1f2ad99..41099d7c9 100644 --- a/API-Editor/ComponentInformation.cs +++ b/API-Editor/ComponentInformation.cs @@ -48,7 +48,7 @@ internal sealed override void CollectMutationsInternal(Component component, ComponentMutationsCollector collector) => CollectMutations((TComponent)component, collector); - internal override void ApplySpecialMappingInternal(Component component, MappingSource collector) => + internal sealed override void ApplySpecialMappingInternal(Component component, MappingSource collector) => ApplySpecialMapping((TComponent)component, collector); /// @@ -207,23 +207,51 @@ internal ComponentMutationsCollector() { } + /// + /// Registers of the will be changed by current component. + /// + /// The component current component will modifies + /// The list of properties current component will modifies [PublicAPI] - public abstract void ModifyProperties([NotNull] Component component, [NotNull] IEnumerable properties); + public abstract void ModifyProperties([NotNull] Component component, + [NotNull] [ItemNotNull] IEnumerable properties); + /// [PublicAPI] - public void ModifyProperties([NotNull] Component component, [NotNull] string[] properties) => - ModifyProperties(component, (IEnumerable) properties); + public void ModifyProperties([NotNull] Component component, + [NotNull] [ItemNotNull] params string[] properties) => + ModifyProperties(component, (IEnumerable)properties); } + /// + /// The class provides object and property replaced by Avatar Optimizer. + /// + /// Avatar Optimizer may replaces or merges component to another component. + /// This class provide the information about the replacement. + /// In addition, Avatar Optimizer may replace or merges some properties of the component. + /// This class also provide the information about the property replacement. + /// public abstract class MappingSource { internal MappingSource() { } + /// + /// Returns about the component instance. + /// The instance can be a missing component. + /// + /// The component to get information about + /// The type of component [PublicAPI] public abstract MappedComponentInfo GetMappedComponent(T component) where T : Component; + /// + /// Returns about the GameObject instance. + /// The instance can be a missing component. + /// + /// The component to get information about + /// The type of component [PublicAPI] public abstract MappedComponentInfo GetMappedGameObject(GameObject component); } @@ -237,10 +265,11 @@ internal MappedComponentInfo() /// /// The mapped component (or GameObject). /// The component may be removed without mapped component. - /// If there are not mapped component, this will be null. + /// If there are no mapped component, this will be null. /// - /// Even if the component is removed without mapped component, - /// each animation property can be mapped to another component. + /// Even if the component is removed without mapped component, some animation property can be mapped + /// to a property on another component so you should use if the component is highly related + /// to animation property, for example, blendShape related SkinnedMeshRenderer. /// [PublicAPI] public abstract T MappedComponent { get; } @@ -249,6 +278,10 @@ internal MappedComponentInfo() /// Maps animation property name to component and MappedPropertyInfo. /// If the property is not removed, returns true and is set. /// If the property is removed, returns false and will be default. + /// + /// To get mapped property probably, you must register the property as modified property by + /// . + /// Unless that, renaming or moving the property may not be tracked by Avatar Optimizer. /// /// The name of property will be mapped /// The result parameter @@ -259,9 +292,15 @@ internal MappedComponentInfo() public readonly struct MappedPropertyInfo { + /// + /// The Component or GameObject the property is on. + /// [PublicAPI] public Object Component { get; } + /// + /// The name of the mapped property. + /// [PublicAPI] public string Property { get; } diff --git a/CHANGELOG-PRERELEASE.md b/CHANGELOG-PRERELEASE.md index 87fe4466a..7dc9f6ac3 100644 --- a/CHANGELOG-PRERELEASE.md +++ b/CHANGELOG-PRERELEASE.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog]. ## [Unreleased] ### Added +- Remove Zero Sized Polygons `#659` +- Add support for UniVRM components `#653` +- Support for Mesh Topologies other than Triangles `#692` ### Changed - When you're animating activeness/enablement of source renderers, warning is shown since this release `#675` @@ -17,6 +20,7 @@ The format is based on [Keep a Changelog]. ### Removed ### Fixed +- proxy animation can be modified `#678` ### Security @@ -40,6 +44,10 @@ The format is based on [Keep a Changelog]. - Prefab blinks when we see editor of PrefabSafeSet of prefab asset [`#645`](https://github.com/anatawa12/AvatarOptimizer/pull/645) [`#664`](https://github.com/anatawa12/AvatarOptimizer/pull/664) - Fixes in 1.5.9 [`#654`](https://github.com/anatawa12/AvatarOptimizer/pull/654) +## [1.5.10] - 2023-11-04 +### Fixed +- RigidBody Joint can be broken [`#683`](https://github.com/anatawa12/AvatarOptimizer/pull/683) + ## [1.5.9] - 2023-10-29 ## [1.5.9-rc.1] - 2023-10-28 ### Fixed @@ -976,7 +984,8 @@ This release is mistake. [Unreleased]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.6.0-beta.2...HEAD [1.6.0-beta.2]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.6.0-beta.1...v1.6.0-beta.2 -[1.6.0-beta.1]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.9...v1.6.0-beta.1 +[1.6.0-beta.1]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.10...v1.6.0-beta.1 +[1.5.10]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.9...v1.5.10 [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 9d16f64bd..3408d616d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,20 @@ The format is based on [Keep a Changelog]. ## [Unreleased] ### Added -- Public API for registering component information `#632` +- Public API for registering component information `#632` `#668` - 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` - Ability to prevent changing enablement of component `#668` +- Remove Zero Sized Polygons `#659` +- Add support for UniVRM components `#653` +- Support for Mesh Topologies other than Triangles `#692` ### Changed - All logs passed to ErrorReport is now shown on the console log `#643` - Improved Behaviour with multi-material multi pass rendering `#662` - Previously, multi-material multi pass rendering are flattened. - Since 1.6, flattened if component doesn't support that. -- BREAKING API CHANGES: Behaviour components are renamed to HeavyBehaviour `#668` - When you're animating activeness/enablement of source renderers, warning is shown since this release `#675` ### Deprecated @@ -36,6 +38,10 @@ The format is based on [Keep a Changelog]. ### Security +## [1.5.10] - 2023-11-04 +### Fixed +- RigidBody Joint can be broken [`#683`](https://github.com/anatawa12/AvatarOptimizer/pull/683) + ## [1.5.9] - 2023-10-29 ### Fixed - Animation clip length can be changed [`#647`](https://github.com/anatawa12/AvatarOptimizer/pull/647) @@ -641,7 +647,8 @@ The format is based on [Keep a Changelog]. - Merge Bone - Clear Endpoint Position -[Unreleased]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.9...HEAD +[Unreleased]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.10...HEAD +[1.5.10]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.9...v1.5.10 [1.5.9]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.8...v1.5.9 [1.5.8]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.7...v1.5.8 [1.5.7]: https://github.com/anatawa12/AvatarOptimizer/compare/v1.5.6...v1.5.7 diff --git a/Editor/APIInternal/ComponentInfos.VRCSDK.cs b/Editor/APIInternal/ComponentInfos.VRCSDK.cs index 85a1c5f27..fbb0724af 100644 --- a/Editor/APIInternal/ComponentInfos.VRCSDK.cs +++ b/Editor/APIInternal/ComponentInfos.VRCSDK.cs @@ -56,7 +56,7 @@ protected override void CollectMutations(T component, ComponentMutationsCollecto case VRC_AvatarDescriptor.LipSyncStyle.JawFlapBlendShape when component.VisemeSkinnedMesh != null: { collector.ModifyProperties(component.VisemeSkinnedMesh, - new[] { $"blendShape.{component.MouthOpenBlendShapeName}" }); + $"blendShape.{component.MouthOpenBlendShapeName}"); break; } case VRC_AvatarDescriptor.LipSyncStyle.VisemeBlendShape when component.VisemeSkinnedMesh != null: diff --git a/Editor/APIInternal/ComponentInfos.VRM0.cs b/Editor/APIInternal/ComponentInfos.VRM0.cs new file mode 100644 index 000000000..eda2e3945 --- /dev/null +++ b/Editor/APIInternal/ComponentInfos.VRM0.cs @@ -0,0 +1,159 @@ +#if AAO_VRM0 + +using System.Linq; +using Anatawa12.AvatarOptimizer.API; +using Anatawa12.AvatarOptimizer.ErrorReporting; +using UnityEngine; +using VRM; + +namespace Anatawa12.AvatarOptimizer.APIInternal +{ + + // NOTE: VRM0 bones are not animated, therefore no need to configure ComponentDependencyInfo + + [ComponentInformation(typeof(VRMMeta))] + internal class VRMMetaInformation : ComponentInformation + { + protected override void CollectDependency(VRMMeta component, ComponentDependencyCollector collector) + { + collector.MarkEntrypoint(); + } + } + + [ComponentInformation(typeof(VRMSpringBone))] + internal class VRMSpringBoneInformation : ComponentInformation + { + protected override void CollectDependency(VRMSpringBone component, ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + foreach (var transform in component.GetComponentsInChildren()) collector.AddDependency(transform); + foreach (var collider in component.ColliderGroups) collector.AddDependency(collider); + } + + protected override void CollectMutations(VRMSpringBone component, ComponentMutationsCollector collector) + { + foreach (var transform in component.GetComponentsInChildren()) + collector.TransformPositionAndRotation(transform); + } + } + + [ComponentInformation(typeof(VRMSpringBoneColliderGroup))] + internal class VRMSpringBoneColliderGroupInformation : ComponentInformation + { + protected override void CollectDependency(VRMSpringBoneColliderGroup component, + ComponentDependencyCollector collector) + { + } + } + + [ComponentInformation(typeof(VRMBlendShapeProxy))] + internal class VRMBlendShapeProxyInformation : ComponentInformation + { + protected override void CollectDependency(VRMBlendShapeProxy component, ComponentDependencyCollector collector) + { + var avatarRootTransform = component.transform; + + collector.MarkHeavyBehaviour(); + foreach (var clip in component.BlendShapeAvatar.Clips) + { + foreach (var binding in clip.Values) + { + var target = avatarRootTransform.Find(binding.RelativePath); + collector.AddDependency(target, component); + collector.AddDependency(target); + } + foreach (var materialBinding in clip.MaterialValues) + { + // TODO: I don't know what to do with BlendShape materials, so I pretend material names does not change (ex. MergeToonLitMaterial) + } + } + } + } + + [ComponentInformation(typeof(VRMLookAtHead))] + internal class VRMLookAtHeadInformation : ComponentInformation + { + protected override void CollectDependency(VRMLookAtHead component, ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + collector.AddDependency(component.Head, component); + collector.AddDependency(component.Head); + } + } + + [ComponentInformation(typeof(VRMLookAtBoneApplyer))] + internal class VRMLookAtBoneApplyerInformation : ComponentInformation + { + protected override void CollectDependency(VRMLookAtBoneApplyer component, ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + collector.AddDependency(component.GetComponent()); + collector.AddDependency(component.LeftEye.Transform); + collector.AddDependency(component.RightEye.Transform); + } + + protected override void CollectMutations(VRMLookAtBoneApplyer component, ComponentMutationsCollector collector) + { + collector.TransformRotation(component.LeftEye.Transform); + collector.TransformRotation(component.RightEye.Transform); + } + } + + + [ComponentInformation(typeof(VRMLookAtBlendShapeApplyer))] + internal class VRMLookAtBlendShapeApplyerInformation : ComponentInformation + { + protected override void CollectDependency(VRMLookAtBlendShapeApplyer component, ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + collector.AddDependency(component.GetComponent()); + } + + } + + + [ComponentInformation(typeof(VRMFirstPerson))] + internal class VRMFirstPersonInformation : ComponentInformation + { + protected override void CollectDependency(VRMFirstPerson component, ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + collector.AddDependency(component.FirstPersonBone, component); + collector.AddDependency(component.FirstPersonBone); + } + + protected override void ApplySpecialMapping(VRMFirstPerson component, MappingSource mappingSource) + { + component.Renderers = component.Renderers + .Select(r => new VRMFirstPerson.RendererFirstPersonFlags + { + Renderer = mappingSource.GetMappedComponent(r.Renderer).MappedComponent, + FirstPersonFlag = r.FirstPersonFlag + }) + .Where(r => r.Renderer) + .GroupBy(r => r.Renderer, r => r.FirstPersonFlag) + .Select(grouping => + { + FirstPersonFlag mergedFirstPersonFlag; + var firstPersonFlags = grouping.Distinct().ToArray(); + if (firstPersonFlags.Length == 1) + { + mergedFirstPersonFlag = firstPersonFlags[0]; + } + else + { + mergedFirstPersonFlag = firstPersonFlags.Contains(FirstPersonFlag.Both) ? FirstPersonFlag.Both : FirstPersonFlag.Auto; + BuildReport.LogWarning("MergeSkinnedMesh:warning:VRM:FirstPersonFlagsMismatch", mergedFirstPersonFlag.ToString()); + } + + return new VRMFirstPerson.RendererFirstPersonFlags + { + Renderer = grouping.Key, + FirstPersonFlag = mergedFirstPersonFlag + }; + }).ToList(); + } + } +} + +#endif \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInfos.VRM0.cs.meta b/Editor/APIInternal/ComponentInfos.VRM0.cs.meta new file mode 100644 index 000000000..8f4538747 --- /dev/null +++ b/Editor/APIInternal/ComponentInfos.VRM0.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 82842faa704e42cca084ea181df31a3e +timeCreated: 1698426374 \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInfos.VRM1.cs b/Editor/APIInternal/ComponentInfos.VRM1.cs new file mode 100644 index 000000000..df82e6a59 --- /dev/null +++ b/Editor/APIInternal/ComponentInfos.VRM1.cs @@ -0,0 +1,199 @@ +#if AAO_VRM1 + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Anatawa12.AvatarOptimizer.API; +using UniGLTF.Extensions.VRMC_vrm; +using UnityEngine; +using UniVRM10; +using Humanoid = UniHumanoid.Humanoid; + +namespace Anatawa12.AvatarOptimizer.APIInternal +{ + + // NOTE: VRM1 bones are not animated their enabled states, therefore no need to configure ComponentDependencyInfo + + [ComponentInformation(typeof(Vrm10Instance))] + internal class Vrm10InstanceInformation : ComponentInformation + { + protected override void CollectDependency(Vrm10Instance component, ComponentDependencyCollector collector) + { + var avatarRootTransform = component.transform; + + collector.MarkEntrypoint(); + + // SpringBones + + foreach (var spring in component.SpringBone.Springs) + { + foreach (var joint in spring.Joints) collector.AddDependency(joint); + foreach (var collider in spring.ColliderGroups) collector.AddDependency(collider); + } + + // Expressions + + foreach (var clip in component.Vrm.Expression.Clips.Select(c => c.Clip)) + { + foreach (var binding in clip.MorphTargetBindings) + { + var target = avatarRootTransform.Find(binding.RelativePath); + collector.AddDependency(target, component); + collector.AddDependency(target); + } + foreach (var materialUVBinding in clip.MaterialUVBindings) + { + // TODO: I don't know what to do with BlendShape materials, so I pretend material names does not change (ex. MergeToonLitMaterial) + } + foreach (var materialColorBinding in clip.MaterialColorBindings) + { + // TODO: I don't know what to do with BlendShape materials, so I pretend material names does not change (ex. MergeToonLitMaterial) + } + } + + // First Person and LookAt + // NOTE: these dependencies are satisfied by either Animator or Humanoid + // collector.AddDependency(GetBoneTransformForVrm10(component, HumanBodyBones.Head)); + + // if (component.Vrm.LookAt.LookAtType == LookAtType.bone) + // { + // if (GetBoneTransformForVrm10(component, HumanBodyBones.LeftEye) is Transform leftEye) + // { + // collector.AddDependency(leftEye); + // } + // if (GetBoneTransformForVrm10(component, HumanBodyBones.RightEye) is Transform rightEye) + // { + // collector.AddDependency(rightEye); + // } + // } + } + + protected override void CollectMutations(Vrm10Instance component, ComponentMutationsCollector collector) + { + // SpringBones + foreach (var joint in component.SpringBone.Springs.SelectMany(spring => spring.Joints)) + { + collector.TransformPositionAndRotation(joint.transform); + } + + // Expressions + + // First Person and LookAt + if (component.Vrm.LookAt.LookAtType == LookAtType.bone) + { + if (GetBoneTransformForVrm10(component, HumanBodyBones.LeftEye) is Transform leftEye) + { + collector.TransformRotation(leftEye); + } + if (GetBoneTransformForVrm10(component, HumanBodyBones.RightEye) is Transform rightEye) + { + collector.TransformRotation(rightEye); + } + } + } + + Transform GetBoneTransformForVrm10(Vrm10Instance component, HumanBodyBones bones) + { + if (component.GetComponent() is Humanoid avatarHumanoid) + { + return avatarHumanoid.GetBoneTransform(bones); + } + + if (component.GetComponent() is Animator avatarAnimator) + { + return avatarAnimator.GetBoneTransform(bones); + } + + return null; + } + } + + [ComponentInformation(typeof(VRM10SpringBoneColliderGroup))] + internal class Vrm10SpringBoneColliderGroupInformation : ComponentInformation + { + protected override void CollectDependency(VRM10SpringBoneColliderGroup component, + ComponentDependencyCollector collector) + { + foreach (var collider in component.Colliders) collector.AddDependency(collider); + } + } + + [ComponentInformation(typeof(VRM10SpringBoneJoint))] + [ComponentInformation(typeof(VRM10SpringBoneCollider))] + internal class Vrm10ReferenceHolderInformation : ComponentInformation + { + protected override void CollectDependency(Component component, + ComponentDependencyCollector collector) + { + } + } + + [ComponentInformation(typeof(Vrm10AimConstraint))] + internal class Vrm10AimConstraintInformation : ComponentInformation + { + protected override void CollectDependency(Vrm10AimConstraint component, + ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + collector.AddDependency(component.transform, component.Source); + } + + protected override void CollectMutations(Vrm10AimConstraint component, ComponentMutationsCollector collector) + { + collector.TransformRotation(component.transform); + } + } + + [ComponentInformation(typeof(Vrm10RollConstraint))] + internal class Vrm10RollConstraintInformation : ComponentInformation + { + protected override void CollectDependency(Vrm10RollConstraint component, + ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + collector.AddDependency(component.transform, component.Source); + } + + protected override void CollectMutations(Vrm10RollConstraint component, ComponentMutationsCollector collector) + { + collector.TransformRotation(component.transform); + } + } + + [ComponentInformation(typeof(Vrm10RotationConstraint))] + internal class Vrm10RotationConstraintInformation : ComponentInformation + { + protected override void CollectDependency(Vrm10RotationConstraint component, + ComponentDependencyCollector collector) + { + collector.MarkHeavyBehaviour(); + collector.AddDependency(component.transform, component.Source); + } + + protected override void CollectMutations(Vrm10RotationConstraint component, ComponentMutationsCollector collector) + { + collector.TransformRotation(component.transform); + } + } + + [ComponentInformation(typeof(Humanoid))] + internal class HumanoidInformation : ComponentInformation + { + protected override void CollectDependency(Humanoid component, ComponentDependencyCollector collector) + { + // VRM1 Humanoid has side effect because it overwrites Animator's Avatar on VRM1 export + collector.MarkEntrypoint(); + + // Use reflection to support UniVRM 0.99.4 + var boneMapProperty = typeof(Humanoid).GetProperty("BoneMap", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var boneMap = (IEnumerable<(Transform, HumanBodyBones)>)boneMapProperty.GetValue(component); + foreach ((Transform transform, HumanBodyBones) bone in boneMap) + { + if (bone.transform) collector.AddDependency(bone.transform); + } + } + } + +} + +#endif \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInfos.VRM1.cs.meta b/Editor/APIInternal/ComponentInfos.VRM1.cs.meta new file mode 100644 index 000000000..ff06b75c8 --- /dev/null +++ b/Editor/APIInternal/ComponentInfos.VRM1.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cda357a894554e8bb67cb0fd3cd02659 +timeCreated: 1698673843 \ No newline at end of file diff --git a/Editor/APIInternal/ComponentInfos.cs b/Editor/APIInternal/ComponentInfos.cs index 6151e8b59..c8f387b27 100644 --- a/Editor/APIInternal/ComponentInfos.cs +++ b/Editor/APIInternal/ComponentInfos.cs @@ -266,8 +266,17 @@ internal class JointInformation : ComponentInformation { protected override void CollectDependency(Joint component, ComponentDependencyCollector collector) { - collector.AddDependency(component.GetComponent(), component); - collector.AddDependency(component.connectedBody); + var rigidBody = component.GetComponent(); + if (rigidBody) + { + collector.AddDependency(rigidBody, component); + collector.AddDependency(rigidBody); + } + if (component.connectedBody) + { + collector.AddDependency(component.connectedBody, component); + collector.AddDependency(component.connectedBody); + } } } @@ -404,6 +413,19 @@ void DeriveMergeSkinnedMeshProperties(MergeSkinnedMesh mergeSkinnedMesh) } } + [ComponentInformation(typeof(RemoveZeroSizedPolygon))] + internal class RemoveZeroSizedPolygonInformation : ComponentInformation + { + protected override void CollectDependency(RemoveZeroSizedPolygon component, ComponentDependencyCollector collector) + { + collector.AddDependency(component.GetComponent(), component); + } + + protected override void CollectMutations(RemoveZeroSizedPolygon component, ComponentMutationsCollector collector) + { + } + } + [ComponentInformation(typeof(MergeBone))] internal class MergeBoneInformation : ComponentInformation { diff --git a/Editor/AnimatorParsers/AnimatorParser.cs b/Editor/AnimatorParsers/AnimatorParser.cs index abef9721d..579632c08 100644 --- a/Editor/AnimatorParsers/AnimatorParser.cs +++ b/Editor/AnimatorParsers/AnimatorParser.cs @@ -174,6 +174,18 @@ private IModificationsContainer CollectAvatarRootAnimatorModifications(BuildCont if (descriptor) CollectAvatarDescriptorModifications(modificationsContainer, descriptor); #endif + +#if AAO_VRM0 + var blendShapeProxy = session.AvatarRootObject.GetComponent(); + if (blendShapeProxy) + CollectBlendShapeProxyModifications(session, modificationsContainer, blendShapeProxy); +#endif + +#if AAO_VRM1 + var vrm10Instance = session.AvatarRootObject.GetComponent(); + if (vrm10Instance) + CollectVrm10InstanceModifications(session, modificationsContainer, vrm10Instance); +#endif return modificationsContainer; } @@ -356,6 +368,35 @@ private static RuntimeAnimatorController GetPlayableLayerController(VRCAvatarDes } #endif +#if AAO_VRM0 + private void CollectBlendShapeProxyModifications(BuildContext context, ModificationsContainer modificationsContainer, VRM.VRMBlendShapeProxy vrmBlendShapeProxy) + { + + var bindings = vrmBlendShapeProxy.BlendShapeAvatar.Clips.SelectMany(clip => clip.Values); + foreach (var binding in bindings) + { + var skinnedMeshRenderer = context.AvatarRootTransform.Find(binding.RelativePath).GetComponent(); + var blendShapePropName = $"blendShape.{skinnedMeshRenderer.sharedMesh.GetBlendShapeName(binding.Index)}"; + modificationsContainer.ModifyObject(skinnedMeshRenderer) + .AddModificationAsNewLayer(blendShapePropName, AnimationFloatProperty.Variable(vrmBlendShapeProxy)); + } + } +#endif + +#if AAO_VRM1 + private void CollectVrm10InstanceModifications(BuildContext context, ModificationsContainer modificationsContainer, UniVRM10.Vrm10Instance vrm10Instance) + { + var bindings = vrm10Instance.Vrm.Expression.Clips.SelectMany(clip => clip.Clip.MorphTargetBindings); + foreach (var binding in bindings) + { + var skinnedMeshRenderer = context.AvatarRootTransform.Find(binding.RelativePath).GetComponent(); + var blendShapePropName = $"blendShape.{skinnedMeshRenderer.sharedMesh.GetBlendShapeName(binding.Index)}"; + modificationsContainer.ModifyObject(skinnedMeshRenderer) + .AddModificationAsNewLayer(blendShapePropName, AnimationFloatProperty.Variable(vrm10Instance)); + } + } +#endif + #endregion #region Animator diff --git a/Editor/AutomaticConfiguration.cs b/Editor/AutomaticConfiguration.cs index c95e69941..c4d35b5db 100644 --- a/Editor/AutomaticConfiguration.cs +++ b/Editor/AutomaticConfiguration.cs @@ -10,6 +10,7 @@ internal class TraceAndOptimizeEditor : AvatarGlobalComponentEditorBase private SerializedProperty _freezeBlendShape; private SerializedProperty _removeUnusedObjects; private SerializedProperty _preserveEndBone; + private SerializedProperty _removeZeroSizedPolygons; private SerializedProperty _mmdWorldCompatibility; private SerializedProperty _advancedSettings; private GUIContent _advancedSettingsLabel = new GUIContent(); @@ -19,6 +20,7 @@ private void OnEnable() _freezeBlendShape = serializedObject.FindProperty(nameof(TraceAndOptimize.freezeBlendShape)); _removeUnusedObjects = serializedObject.FindProperty(nameof(TraceAndOptimize.removeUnusedObjects)); _preserveEndBone = serializedObject.FindProperty(nameof(TraceAndOptimize.preserveEndBone)); + _removeZeroSizedPolygons = serializedObject.FindProperty(nameof(TraceAndOptimize.removeZeroSizedPolygons)); _mmdWorldCompatibility = serializedObject.FindProperty(nameof(TraceAndOptimize.mmdWorldCompatibility)); _advancedSettings = serializedObject.FindProperty(nameof(TraceAndOptimize.advancedSettings)); } @@ -39,6 +41,7 @@ protected override void OnInspectorGUIInner() EditorGUILayout.PropertyField(_preserveEndBone); EditorGUI.indentLevel--; } + EditorGUILayout.PropertyField(_removeZeroSizedPolygons); _advancedSettingsLabel.text = CL4EE.Tr("TraceAndOptimize:prop:advancedSettings"); if (EditorGUILayout.PropertyField(_advancedSettings, _advancedSettingsLabel, false)) diff --git a/Editor/ObjectMapping/AnimationObjectMapper.cs b/Editor/ObjectMapping/AnimationObjectMapper.cs index 0a1002652..db3c2e776 100644 --- a/Editor/ObjectMapping/AnimationObjectMapper.cs +++ b/Editor/ObjectMapping/AnimationObjectMapper.cs @@ -116,22 +116,28 @@ public string MapPath(string srcPath, Type type) } [CanBeNull] - public EditorCurveBinding[] MapBinding(EditorCurveBinding binding) + public (string path, Type type, string propertyName)[] MapBinding(string path, Type type, string propertyName) { - var gameObjectInfo = GetGameObjectInfo(binding.path); + var gameObjectInfo = GetGameObjectInfo(path); if (gameObjectInfo == null) return null; - var (instanceId, componentInfo) = gameObjectInfo.GetComponentByType(binding.type); + var (instanceId, componentInfo) = gameObjectInfo.GetComponentByType(type); if (componentInfo != null) { // there's mapping about component. // this means the component is merged or some prop has mapping - if (componentInfo.PropertyMapping.TryGetValue(binding.propertyName, out var newProp)) + if (componentInfo.PropertyMapping.TryGetValue(propertyName, out var newProp)) { + // if mapped one is exactly same as original, return null + if (newProp.AllCopiedTo.Length == 1 + && newProp.AllCopiedTo[0].InstanceId == instanceId + && newProp.AllCopiedTo[0].Name == propertyName) + return null; + // there are mapping for property - var curveBindings = new EditorCurveBinding[newProp.AllCopiedTo.Length]; + var mappedBindings = new (string path, Type type, string propertyName)[newProp.AllCopiedTo.Length]; var copiedToIndex = 0; for (var i = 0; i < newProp.AllCopiedTo.Length; i++) { @@ -148,28 +154,24 @@ public EditorCurveBinding[] MapBinding(EditorCurveBinding binding) // this means moved to out of the animator scope // TODO: add warning - if (newPath == null) return Array.Empty(); + if (newPath == null) return Array.Empty<(string path, Type type, string propertyName)>(); - binding.path = newPath; - binding.type = descriptor.Type; - binding.propertyName = descriptor.Name; - curveBindings[i] = binding; // copy + mappedBindings[i] = (newPath, descriptor.Type, descriptor.Name); } - if (copiedToIndex != curveBindings.Length) - return curveBindings.AsSpan().Slice(0, copiedToIndex).ToArray(); - return curveBindings; + if (copiedToIndex != mappedBindings.Length) + return mappedBindings.AsSpan().Slice(0, copiedToIndex).ToArray(); + return mappedBindings; } else { var component = new ComponentOrGameObject(EditorUtility.InstanceIDToObject(componentInfo.MergedInto)); - if (!component) return Array.Empty(); // this means removed. + if (!component) return Array.Empty<(string path, Type type, string propertyName)>(); // 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 }; + if (newPath == null) return Array.Empty<(string path, Type type, string propertyName)>(); // this means moved to out of the animator scope + if (path == newPath) return null; + return new []{ (newPath, type, propertyName) }; } } else @@ -177,13 +179,32 @@ public EditorCurveBinding[] MapBinding(EditorCurveBinding binding) // The component is not merged & no prop mapping so process GameObject mapping var component = EditorUtility.InstanceIDToObject(instanceId); - if (!component) return Array.Empty(); // this means removed + if (!component) return Array.Empty<(string path, Type type, string propertyName)>(); // this means removed - if (gameObjectInfo.NewPath == null) return Array.Empty(); - if (binding.path == gameObjectInfo.NewPath) return null; - binding.path = gameObjectInfo.NewPath; - return new[] { binding }; + if (gameObjectInfo.NewPath == null) return Array.Empty<(string path, Type type, string propertyName)>(); + if (path == gameObjectInfo.NewPath) return null; + return new[] { (gameObjectInfo.NewPath, type, propertyName) }; + } + } + + [CanBeNull] + public EditorCurveBinding[] MapBinding(EditorCurveBinding binding) + { + var mappedBindings = MapBinding(binding.path, binding.type, binding.propertyName); + if (mappedBindings == null) + { + return null; + } + + var curveBindings = new EditorCurveBinding[mappedBindings.Length]; + for (var i = 0; i < mappedBindings.Length; i++) + { + binding.path = mappedBindings[i].path; + binding.type = mappedBindings[i].type; + binding.propertyName = mappedBindings[i].propertyName; + curveBindings[i] = binding; // copy everything else } + return curveBindings; } } } diff --git a/Editor/ObjectMapping/ObjectMappingContext.cs b/Editor/ObjectMapping/ObjectMappingContext.cs index b5d0736ba..71a4d95fb 100644 --- a/Editor/ObjectMapping/ObjectMappingContext.cs +++ b/Editor/ObjectMapping/ObjectMappingContext.cs @@ -44,16 +44,25 @@ public void OnDeactivate(BuildContext context) if (mapping.MapComponentInstance(p.objectReferenceInstanceIDValue, out var mappedComponent)) p.objectReferenceValue = mappedComponent; - if (p.objectReferenceValue is RuntimeAnimatorController controller) + var objectReferenceValue = p.objectReferenceValue; + switch (objectReferenceValue) { - 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; + case RuntimeAnimatorController _: +#if AAO_VRM0 + case VRM.BlendShapeAvatar _: +#endif +#if AAO_VRM1 + case UniVRM10.VRM10Object _: +#endif + if (mapper == null) + mapper = new AnimatorControllerMapper(mapping.CreateAnimationMapper(component.gameObject)); + + // ReSharper disable once AccessToModifiedClosure + var mapped = BuildReport.ReportingObject(objectReferenceValue, + () => mapper.MapObject(objectReferenceValue)); + if (mapped != objectReferenceValue) + p.objectReferenceValue = mapped; + break; } } @@ -135,6 +144,9 @@ public AnimatorControllerMapper(AnimationObjectMapper mapping) public T MapAnimatorController(T controller) where T : RuntimeAnimatorController => DeepClone(controller, CustomClone); + public T MapObject(T obj) where T : Object => + DeepClone(obj, CustomClone); + // https://github.com/bdunderscore/modular-avatar/blob/db49e2e210bc070671af963ff89df853ae4514a5/Packages/nadena.dev.modular-avatar/Editor/AnimatorMerger.cs#L199-L241 // Originally under MIT License // Copyright (c) 2022 bd_ @@ -142,6 +154,10 @@ private Object CustomClone(Object o) { if (o is AnimationClip clip) { +#if AAO_VRCSDK3_AVATARS + // TODO: when BuildContext have property to check if it is for VRCSDK3, additionally use it. + if (clip.IsProxy()) return clip; +#endif var newClip = new AnimationClip(); newClip.name = "rebased " + clip.name; @@ -228,11 +244,62 @@ private Object CustomClone(Object o) newMask.SetTransformActive(dstI, mask.GetTransformActive(srcI)); dstI++; } + if (path != newPath) _mapped = true; } newMask.transformCount = dstI; return newMask; } +#if AAO_VRM0 + else if (o is VRM.BlendShapeClip blendShapeClip) + { + var newBlendShapeClip = DefaultDeepClone(blendShapeClip, CustomClone); + newBlendShapeClip.Prefab = null; // This likely to point prefab before mapping, which is invalid by now + newBlendShapeClip.name = "rebased " + blendShapeClip.name; + newBlendShapeClip.Values = newBlendShapeClip.Values.SelectMany(binding => + { + var mappedBindings = _mapping.MapBinding(binding.RelativePath, typeof(SkinnedMeshRenderer), VProp.BlendShapeIndex(binding.Index)); + if (mappedBindings == null) + { + return new[] { binding }; + } + _mapped = true; + return mappedBindings + .Select(mapped => new VRM.BlendShapeBinding + { + RelativePath = _mapping.MapPath(mapped.path, typeof(SkinnedMeshRenderer)), + Index = VProp.ParseBlendShapeIndex(mapped.propertyName), + Weight = binding.Weight + }); + }).ToArray(); + return newBlendShapeClip; + } +#endif +#if AAO_VRM1 + else if (o is UniVRM10.VRM10Expression vrm10Expression) + { + var newVrm10Expression = DefaultDeepClone(vrm10Expression, CustomClone); + newVrm10Expression.Prefab = null; // This likely to point prefab before mapping, which is invalid by now + newVrm10Expression.name = "rebased " + vrm10Expression.name; + newVrm10Expression.MorphTargetBindings = newVrm10Expression.MorphTargetBindings.SelectMany(binding => + { + var mappedBindings = _mapping.MapBinding(binding.RelativePath, typeof(SkinnedMeshRenderer), VProp.BlendShapeIndex(binding.Index)); + if (mappedBindings == null) + { + return new[] { binding }; + } + _mapped = true; + return mappedBindings + .Select(mapped => new UniVRM10.MorphTargetBinding + { + RelativePath = _mapping.MapPath(mapped.path, typeof(SkinnedMeshRenderer)), + Index = VProp.ParseBlendShapeIndex(mapped.propertyName), + Weight = binding.Weight + }); + }).ToArray(); + return newVrm10Expression; + } +#endif else if (o is RuntimeAnimatorController controller) { using (new MappedScope(this)) @@ -244,6 +311,62 @@ private Object CustomClone(Object o) return newController; } } +#if AAO_VRM0 + else if (o is VRM.BlendShapeAvatar blendShapeAvatar) + { + using (new MappedScope(this)) + { + var newBlendShapeAvatar = DefaultDeepClone(blendShapeAvatar, CustomClone); + newBlendShapeAvatar.name = blendShapeAvatar.name + " (rebased)"; + if (!_mapped) newBlendShapeAvatar = blendShapeAvatar; + _cache[blendShapeAvatar] = newBlendShapeAvatar; + return newBlendShapeAvatar; + } + } +#endif +#if AAO_VRM1 + else if (o is UniVRM10.VRM10Object vrm10Object) + { + using (new MappedScope(this)) + { + var newVrm10Object = DefaultDeepClone(vrm10Object, CustomClone); + newVrm10Object.name = vrm10Object.name + " (rebased)"; + if (!_mapped) newVrm10Object = vrm10Object; + _cache[vrm10Object] = newVrm10Object; + + newVrm10Object.FirstPerson.Renderers = newVrm10Object.FirstPerson.Renderers + .Select(r => new UniVRM10.RendererFirstPersonFlags + { + Renderer = _mapping.MapPath(r.Renderer, typeof(Renderer)), + FirstPersonFlag = r.FirstPersonFlag + }) + .Where(r => r.Renderer != null) + .GroupBy(r => r.Renderer, r => r.FirstPersonFlag) + .Select(grouping => + { + UniGLTF.Extensions.VRMC_vrm.FirstPersonType mergedFirstPersonFlag; + var firstPersonFlags = grouping.Distinct().ToArray(); + if (firstPersonFlags.Length == 1) + { + mergedFirstPersonFlag = firstPersonFlags[0]; + } + else + { + mergedFirstPersonFlag = firstPersonFlags.Contains(UniGLTF.Extensions.VRMC_vrm.FirstPersonType.both) ? UniGLTF.Extensions.VRMC_vrm.FirstPersonType.both : UniGLTF.Extensions.VRMC_vrm.FirstPersonType.auto; + BuildReport.LogWarning("MergeSkinnedMesh:warning:VRM:FirstPersonFlagsMismatch", mergedFirstPersonFlag.ToString()); + } + + return new UniVRM10.RendererFirstPersonFlags + { + Renderer = grouping.Key, + FirstPersonFlag = mergedFirstPersonFlag + }; + }).ToList(); + + return newVrm10Object; + } + } +#endif else { return null; @@ -290,10 +413,24 @@ private T DeepClone(T original, Func visitor) where T : Objec case AvatarMask _: break; // We want to clone these types + // also handle VRM objects here +#if AAO_VRM0 + case VRM.BlendShapeAvatar _: + case VRM.BlendShapeClip _: + break; // We want to clone these types +#endif + +#if AAO_VRM1 + case UniVRM10.VRM10Object _: + case UniVRM10.VRM10Expression _: + break; // We want to clone these types +#endif + // Leave textures, materials, and script definitions alone case Texture _: case MonoScript _: case Material _: + case GameObject _: return original; // Also avoid copying unknown scriptable objects. diff --git a/Editor/OptimizerPlugin.cs b/Editor/OptimizerPlugin.cs index c721d52a2..e3ddcaad8 100644 --- a/Editor/OptimizerPlugin.cs +++ b/Editor/OptimizerPlugin.cs @@ -58,7 +58,10 @@ protected override void Configure() ctx => new Processors.MakeChildrenProcessor(early: false).Process(ctx) ) .Then.Run(Processors.TraceAndOptimizes.FindUnusedObjects.Instance) - .Then.Run(Processors.MergeBoneProcessor.Instance); + .Then.Run(Processors.TraceAndOptimizes.ConfigureRemoveZeroSizedPolygon.Instance) + .Then.Run(Processors.MergeBoneProcessor.Instance) + .Then.Run(Processors.RemoveZeroSizedPolygonProcessor.Instance) + ; }); seq.Run("EmptyPass for Context Ordering", _ => {}); }); diff --git a/Editor/Processors/MergeBoneProcessor.cs b/Editor/Processors/MergeBoneProcessor.cs index f4b8b22b3..cef25a221 100644 --- a/Editor/Processors/MergeBoneProcessor.cs +++ b/Editor/Processors/MergeBoneProcessor.cs @@ -264,7 +264,7 @@ public override int GetHashCode() => } - struct MergeBoneTransParentInfo + public struct MergeBoneTransParentInfo { public Quaternion ParentRotation; public Matrix4x4 ParentMatrix; diff --git a/Editor/Processors/RemoveZeroSizedPolygonProcessor.cs b/Editor/Processors/RemoveZeroSizedPolygonProcessor.cs new file mode 100644 index 000000000..c044a2667 --- /dev/null +++ b/Editor/Processors/RemoveZeroSizedPolygonProcessor.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes; +using nadena.dev.ndmf; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.Processors +{ + public class RemoveZeroSizedPolygonProcessor : Pass + { + public override string DisplayName => "RemoveZeroSizedPolygonProcessor"; + + protected override void Execute(BuildContext context) + { + foreach (var removeZeroSizedPolygon in context.GetComponents()) + { + var mesh = removeZeroSizedPolygon.GetComponent(); + if (!mesh) continue; + Process(context.GetMeshInfoFor(mesh), removeZeroSizedPolygon); + Object.DestroyImmediate(removeZeroSizedPolygon); + } + } + + private static void Process(MeshInfo2 meshInfo2, RemoveZeroSizedPolygon _) + { + foreach (var subMesh in meshInfo2.SubMeshes) + { + var dstI = 0; + for (var srcI = 0; srcI < subMesh.Triangles.Count; srcI += 3) + { + if (!IsPolygonEmpty(subMesh.Triangles[srcI], subMesh.Triangles[srcI + 1], subMesh.Triangles[srcI + 2])) + { + subMesh.Triangles[dstI] = subMesh.Triangles[srcI]; + subMesh.Triangles[dstI + 1] = subMesh.Triangles[srcI + 1]; + subMesh.Triangles[dstI + 2] = subMesh.Triangles[srcI + 2]; + dstI += 3; + } + } + + subMesh.Triangles.RemoveRange(dstI, subMesh.Triangles.Count - dstI); + } + } + + private static bool IsPolygonEmpty(Vertex a, Vertex b, Vertex c) + { + // BlendShapes are hard to check so disallow it + // TODO: check BlendShape delta is same + if (a.BlendShapes.Count != 0) return false; + if (b.BlendShapes.Count != 0) return false; + if (c.BlendShapes.Count != 0) return false; + + // check three points are at same position + // TODO: should we use cross product instead? + if (a.Position != b.Position) return false; + if (a.Position != c.Position) return false; + + // check bone and bone weights are same + var aWeights = new HashSet<(Bone bone, float weight)>(a.BoneWeights); + if (!aWeights.SetEquals(b.BoneWeights)) return false; + if (!aWeights.SetEquals(c.BoneWeights)) return false; + return true; + } + } +} \ No newline at end of file diff --git a/Editor/Processors/RemoveZeroSizedPolygonProcessor.cs.meta b/Editor/Processors/RemoveZeroSizedPolygonProcessor.cs.meta new file mode 100644 index 000000000..88d6e7560 --- /dev/null +++ b/Editor/Processors/RemoveZeroSizedPolygonProcessor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 37bd4776160b4b74a31637d91faf35e3 +timeCreated: 1698635819 \ No newline at end of file diff --git a/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs b/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs index c64472880..39cad276e 100644 --- a/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/MergeSkinnedMeshProcessor.cs @@ -52,7 +52,7 @@ public override void Process(BuildContext context, MeshInfo2 target) foreach (var meshInfo2 in meshInfos) meshInfo2.FlattenMultiPassRendering("Merge Skinned Mesh"); - var sourceMaterials = meshInfos.Select(x => x.SubMeshes.Select(y => y.SharedMaterial).ToArray()).ToArray(); + var sourceMaterials = meshInfos.Select(x => x.SubMeshes.Select(y => (y.Topology, y.SharedMaterial)).ToArray()).ToArray(); Profiler.EndSample(); Profiler.BeginSample("Material Normal Configuration Check"); @@ -88,7 +88,7 @@ from meshInfo2 in meshInfos target.Clear(); target.SubMeshes.Capacity = Math.Max(target.SubMeshes.Capacity, materials.Count); foreach (var material in materials) - target.SubMeshes.Add(new SubMesh(material)); + target.SubMeshes.Add(new SubMesh(material.material, material.topology)); TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => (TexCoordStatus)Math.Max((int)x, (int)y); @@ -115,7 +115,10 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => for (var j = 0; j < meshInfo.SubMeshes.Count; j++) { var targetSubMeshIndex = subMeshIndexMap[i][j]; - target.SubMeshes[targetSubMeshIndex].Triangles.AddRange(meshInfo.SubMeshes[j].Triangles); + var targetSubMesh = target.SubMeshes[targetSubMeshIndex]; + var sourceSubMesh = meshInfo.SubMeshes[j]; + System.Diagnostics.Debug.Assert(targetSubMesh.Topology == sourceSubMesh.Topology); + targetSubMesh.Vertices.AddRange(sourceSubMesh.Vertices); mappings.Add(($"m_Materials.Array.data[{j}]", $"m_Materials.Array.data[{targetSubMeshIndex}]")); } @@ -207,6 +210,13 @@ TexCoordStatus TexCoordStatusMax(TexCoordStatus x, TexCoordStatus y) => context.RecordMergeComponent(renderer, Target); var rendererGameObject = renderer.gameObject; + var toDestroy = renderer.GetComponent(); + if (toDestroy) + { + BuildReport.LogWarning("MergeSkinnedMesh:warning:removeZeroSizedPolygonOnSources") + ?.WithContext(toDestroy); + Object.DestroyImmediate(toDestroy); + } Object.DestroyImmediate(renderer); // process removeEmptyRendererObject @@ -275,11 +285,12 @@ private void ActivenessAnimationWarning(Renderer renderer, BuildContext context, } } - private (int[][] mapping, List materials) CreateMergedMaterialsAndSubMeshIndexMapping( - Material[][] sourceMaterials) + private (int[][] mapping, List<(MeshTopology topology, Material material)> materials) + CreateMergedMaterialsAndSubMeshIndexMapping( + (MeshTopology topology, Material material)[][] sourceMaterials) { var doNotMerges = Component.doNotMergeMaterials.GetAsSet(); - var resultMaterials = new List(); + var resultMaterials = new List<(MeshTopology, Material)>(); var resultIndices = new int[sourceMaterials.Length][]; for (var i = 0; i < sourceMaterials.Length; i++) @@ -291,7 +302,7 @@ private void ActivenessAnimationWarning(Renderer renderer, BuildContext context, { var material = materials[j]; var foundIndex = resultMaterials.IndexOf(material); - if (doNotMerges.Contains(material) || foundIndex == -1) + if (doNotMerges.Contains(material.material) || foundIndex == -1) { indices[j] = resultMaterials.Count; resultMaterials.Add(material); @@ -352,10 +363,12 @@ public Material[] Materials(bool fast = true) { var sourceMaterials = _processor.SkinnedMeshRenderers.Select(EditSkinnedMeshComponentUtil.GetMaterials) .Concat(_processor.StaticMeshRenderers.Select(x => x.sharedMaterials)) + .Select(a => a.Select(b => (MeshTopology.Triangles, b)).ToArray()) .ToArray(); return _processor.CreateMergedMaterialsAndSubMeshIndexMapping(sourceMaterials) .materials + .Select(x => x.material) .ToArray(); } diff --git a/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs b/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs index 4fc5296a6..2175a0d50 100644 --- a/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/MergeToonLitMaterialProcessor.cs @@ -36,7 +36,7 @@ public override void Process(BuildContext context, MeshInfo2 target) foreach (var v in target.Vertices) users[v] = 0; foreach (var targetSubMesh in target.SubMeshes) - foreach (var v in targetSubMesh.Triangles.Distinct()) + foreach (var v in targetSubMesh.Vertices.Distinct()) users[v]++; // compute per-material data @@ -55,30 +55,30 @@ public override void Process(BuildContext context, MeshInfo2 target) var subMesh = target.SubMeshes[subMeshI]; var targetRect = targetRectForMaterial[subMeshI]; var vertexCache = new Dictionary(); - for (var i = 0; i < subMesh.Triangles.Count; i++) + for (var i = 0; i < subMesh.Vertices.Count; i++) { - if (vertexCache.TryGetValue(subMesh.Triangles[i], out var cached)) + if (vertexCache.TryGetValue(subMesh.Vertices[i], out var cached)) { - subMesh.Triangles[i] = cached; + subMesh.Vertices[i] = cached; continue; } - if (users[subMesh.Triangles[i]] != 1) + if (users[subMesh.Vertices[i]] != 1) { // if there are multiple users for the vertex: duplicate it - var cloned = subMesh.Triangles[i].Clone(); + var cloned = subMesh.Vertices[i].Clone(); target.Vertices.Add(cloned); - users[subMesh.Triangles[i]]--; + users[subMesh.Vertices[i]]--; - vertexCache[subMesh.Triangles[i]] = cloned; - subMesh.Triangles[i] = cloned; + vertexCache[subMesh.Vertices[i]] = cloned; + subMesh.Vertices[i] = cloned; } else { - vertexCache[subMesh.Triangles[i]] = subMesh.Triangles[i]; + vertexCache[subMesh.Vertices[i]] = subMesh.Vertices[i]; } - subMesh.Triangles[i].TexCoord0 = MapUV(subMesh.Triangles[i].TexCoord0, targetRect); + subMesh.Vertices[i].TexCoord0 = MapUV(subMesh.Vertices[i].TexCoord0, targetRect); } } } diff --git a/Editor/Processors/SkinnedMeshes/MeshInfo2.cs b/Editor/Processors/SkinnedMeshes/MeshInfo2.cs index 55051b66d..b86c0a52b 100644 --- a/Editor/Processors/SkinnedMeshes/MeshInfo2.cs +++ b/Editor/Processors/SkinnedMeshes/MeshInfo2.cs @@ -124,7 +124,7 @@ private void SetMaterials(Renderer renderer) public void AssertInvariantContract(string context) { var vertices = new HashSet(Vertices); - Debug.Assert(SubMeshes.SelectMany(x => x.Triangles).All(vertices.Contains), + Debug.Assert(SubMeshes.SelectMany(x => x.Vertices).All(vertices.Contains), $"{context}: some SubMesh has invalid triangles"); var bones = new HashSet(Bones); Debug.Assert(Vertices.SelectMany(x => x.BoneWeights).Select(x => x.bone).All(bones.Contains), @@ -336,11 +336,15 @@ public void ReadStaticMesh([NotNull] Mesh mesh) // ReSharper restore AccessToModifiedClosure } - var triangles = mesh.triangles; SubMeshes.Clear(); SubMeshes.Capacity = Math.Max(SubMeshes.Capacity, mesh.subMeshCount); + + var triangles = new List(); for (var i = 0; i < mesh.subMeshCount; i++) + { + mesh.GetIndices(triangles, i); SubMeshes.Add(new SubMesh(Vertices, triangles, mesh.GetSubMesh(i))); + } Profiler.EndSample(); } @@ -412,7 +416,7 @@ public void FlattenMultiPassRendering(string reasonComponent) SubMeshes.Clear(); foreach (var subMesh in subMeshes) foreach (var material in subMesh.SharedMaterials) - SubMeshes.Add(new SubMesh(subMesh.Triangles, material)); + SubMeshes.Add(new SubMesh(subMesh, material)); } public void WriteToMesh(Mesh destMesh) @@ -512,59 +516,50 @@ public void WriteToMesh(Mesh destMesh) for (var i = 0; i < Vertices.Count; i++) vertexIndices.Add(Vertices[i], i); - var totalTriangles = 0; + var maxIndices = 0; var totalSubMeshes = 0; for (var i = 0; i < SubMeshes.Count - 1; i++) { + maxIndices = Mathf.Max(maxIndices, SubMeshes[i].Vertices.Count); // for non-last submesh, we have to duplicate submesh for multi pass rendering for (var j = 0; j < SubMeshes[i].SharedMaterials.Length; j++) - { - totalTriangles += SubMeshes[i].Triangles.Count; totalSubMeshes++; - } } { + maxIndices = Mathf.Max(maxIndices, SubMeshes[SubMeshes.Count - 1].Vertices.Count); // for last submesh, we can use single submesh for multi pass reendering - totalTriangles += SubMeshes[SubMeshes.Count - 1].Triangles.Count; totalSubMeshes++; } - var triangles = new int[totalTriangles]; - var subMeshDescriptors = new SubMeshDescriptor[totalSubMeshes]; - var trianglesIndex = 0; + var indices = new int[maxIndices]; var submeshIndex = 0; + destMesh.indexFormat = Vertices.Count <= ushort.MaxValue ? IndexFormat.UInt16 : IndexFormat.UInt32; + destMesh.subMeshCount = totalSubMeshes; + for (var i = 0; i < SubMeshes.Count - 1; i++) { var subMesh = SubMeshes[i]; - var descriptor = new SubMeshDescriptor(trianglesIndex, subMesh.Triangles.Count); - foreach (var triangle in subMesh.Triangles) - triangles[trianglesIndex++] = vertexIndices[triangle]; + + for (var index = 0; index < subMesh.Vertices.Count; index++) + indices[index] = vertexIndices[subMesh.Vertices[index]]; // general case: for non-last submesh, we have to duplicate submesh for multi pass rendering for (var j = 0; j < subMesh.SharedMaterials.Length; j++) - subMeshDescriptors[submeshIndex++] = descriptor; + destMesh.SetIndices(indices, 0, subMesh.Vertices.Count, subMesh.Topology, submeshIndex++); } { var subMesh = SubMeshes[SubMeshes.Count - 1]; - var descriptor = new SubMeshDescriptor(trianglesIndex, subMesh.Triangles.Count); - foreach (var triangle in subMesh.Triangles) - triangles[trianglesIndex++] = vertexIndices[triangle]; + for (var index = 0; index < subMesh.Vertices.Count; index++) + indices[index] = vertexIndices[subMesh.Vertices[index]]; // for last submesh, we can use single submesh for multi pass reendering - subMeshDescriptors[submeshIndex++] = descriptor; + destMesh.SetIndices(indices, 0, subMesh.Vertices.Count, subMesh.Topology, submeshIndex++); } - Debug.Assert(subMeshDescriptors.Length == submeshIndex); - Debug.Assert(triangles.Length == trianglesIndex); - - destMesh.indexFormat = Vertices.Count <= ushort.MaxValue ? IndexFormat.UInt16 : IndexFormat.UInt32; - destMesh.triangles = triangles; - destMesh.subMeshCount = submeshIndex; - for (var i = 0; i < subMeshDescriptors.Length; i++) - destMesh.SetSubMesh(i, subMeshDescriptors[i]); + Debug.Assert(totalSubMeshes == submeshIndex); } Profiler.EndSample(); @@ -674,8 +669,19 @@ public void WriteToMeshRenderer(MeshRenderer targetRenderer) internal class SubMesh { + public readonly MeshTopology Topology = MeshTopology.Triangles; + // size of this must be 3 * n - public readonly List Triangles = new List(); + public List Triangles + { + get + { + Debug.Assert(Topology == MeshTopology.Triangles); + return Vertices; + } + } + + public List Vertices { get; } = new List(); public Material SharedMaterial { @@ -689,18 +695,73 @@ public SubMesh() { } - public SubMesh(List vertices) => Triangles = vertices; + public SubMesh(List vertices) => Vertices = vertices; public SubMesh(List vertices, Material sharedMaterial) => - (Triangles, SharedMaterial) = (vertices, sharedMaterial); - public SubMesh(Material sharedMaterial) => - SharedMaterial = sharedMaterial; + (Vertices, SharedMaterial) = (vertices, sharedMaterial); + public SubMesh(Material sharedMaterial) => SharedMaterial = sharedMaterial; + public SubMesh(Material sharedMaterial, MeshTopology topology) => + (SharedMaterial, Topology) = (sharedMaterial, topology); + + public SubMesh(SubMesh subMesh, Material triangles) + { + Topology = subMesh.Topology; + Vertices = new List(subMesh.Vertices); + SharedMaterial = triangles; + } - public SubMesh(List vertices, ReadOnlySpan triangles, SubMeshDescriptor descriptor) + public SubMesh(List vertices, List triangles, SubMeshDescriptor descriptor) { - Assert.AreEqual(MeshTopology.Triangles, descriptor.topology); - Triangles.Capacity = descriptor.indexCount; - foreach (var i in triangles.Slice(descriptor.indexStart, descriptor.indexCount)) - Triangles.Add(vertices[i]); + Topology = descriptor.topology; + Vertices.Capacity = descriptor.indexCount; + foreach (var i in triangles) + Vertices.Add(vertices[i]); + } + + public bool TryGetPrimitiveSize(string component, out int primitiveSize) + { + switch (Topology) + { + case MeshTopology.Triangles: + primitiveSize = 3; + return true; + case MeshTopology.Quads: + primitiveSize = 4; + return true; + case MeshTopology.Lines: + primitiveSize = 2; + return true; + case MeshTopology.Points: + primitiveSize = 1; + return true; + case MeshTopology.LineStrip: + BuildReport.LogWarning("MeshInfo2:warning:lineStrip", component); + primitiveSize = default; + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public void RemovePrimitives(string component, Func condition) + { + if (!TryGetPrimitiveSize(component, out var primitiveSize)) + return; + var primitiveBuffer = new Vertex[primitiveSize]; + int srcI = 0, dstI = 0; + for (; srcI < Vertices.Count; srcI += primitiveSize) + { + for (var i = 0; i < primitiveSize; i++) + primitiveBuffer[i] = Vertices[srcI + i]; + + if (condition(primitiveBuffer)) + continue; + + // no vertex is in box: + for (var i = 0; i < primitiveSize; i++) + Vertices[dstI + i] = primitiveBuffer[i]; + dstI += primitiveSize; + } + Vertices.RemoveRange(dstI, Vertices.Count - dstI); } } diff --git a/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs b/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs index 95ae1d4e0..96a571f45 100644 --- a/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/RemoveMeshByBlendShapeProcessor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using nadena.dev.ndmf; @@ -26,27 +27,9 @@ public override void Process(BuildContext context, MeshInfo2 target) byBlendShapeVertices.Add(vertex); } + Func condition = primitive => primitive.Any(byBlendShapeVertices.Contains); foreach (var subMesh in target.SubMeshes) - { - int srcI = 0, dstI = 0; - for (; srcI < subMesh.Triangles.Count; srcI += 3) - { - // process 3 vertex in sub mesh at once to process one polygon - var v0 = subMesh.Triangles[srcI + 0]; - var v1 = subMesh.Triangles[srcI + 1]; - var v2 = subMesh.Triangles[srcI + 2]; - - if (byBlendShapeVertices.Contains(v0) || byBlendShapeVertices.Contains(v1) || byBlendShapeVertices.Contains(v2)) - continue; - - // no vertex is affected by the blend shape: - subMesh.Triangles[dstI + 0] = v0; - subMesh.Triangles[dstI + 1] = v1; - subMesh.Triangles[dstI + 2] = v2; - dstI += 3; - } - subMesh.Triangles.RemoveRange(dstI, subMesh.Triangles.Count - dstI); - } + subMesh.RemovePrimitives("RemoveMeshByBlendShape", condition); // remove unused vertices target.Vertices.RemoveAll(x => byBlendShapeVertices.Contains(x)); diff --git a/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs b/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs index fef449231..9125830d0 100644 --- a/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs +++ b/Editor/Processors/SkinnedMeshes/RemoveMeshInBoxProcessor.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Linq; +using Anatawa12.AvatarOptimizer.ErrorReporting; using nadena.dev.ndmf; using UnityEngine; @@ -24,27 +26,9 @@ public override void Process(BuildContext context, MeshInfo2 target) inBoxVertices.Add(vertex); } + Func condition = primitive => primitive.All(inBoxVertices.Contains); foreach (var subMesh in target.SubMeshes) - { - int srcI = 0, dstI = 0; - for (; srcI < subMesh.Triangles.Count; srcI += 3) - { - // process 3 vertex in sub mesh at once to process one polygon - var v0 = subMesh.Triangles[srcI + 0]; - var v1 = subMesh.Triangles[srcI + 1]; - var v2 = subMesh.Triangles[srcI + 2]; - - if (inBoxVertices.Contains(v0) && inBoxVertices.Contains(v1) && inBoxVertices.Contains(v2)) - continue; - - // some vertex is not in box: - subMesh.Triangles[dstI + 0] = v0; - subMesh.Triangles[dstI + 1] = v1; - subMesh.Triangles[dstI + 2] = v2; - dstI += 3; - } - subMesh.Triangles.RemoveRange(dstI, subMesh.Triangles.Count - dstI); - } + subMesh.RemovePrimitives("RemoveMeshInBox", condition); // We don't need to reset AdditionalTemporal because if out of box, it always be used. // Vertex.AdditionalTemporal: 0 if unused, 1 if used @@ -52,7 +36,7 @@ public override void Process(BuildContext context, MeshInfo2 target) inBoxVertices.Clear(); var usingVertices = inBoxVertices; foreach (var subMesh in target.SubMeshes) - foreach (var vertex in subMesh.Triangles) + foreach (var vertex in subMesh.Vertices) usingVertices.Add(vertex); // remove unused vertices diff --git a/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs b/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs index 07dba60af..b9cd30f5b 100644 --- a/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs +++ b/Editor/Processors/TraceAndOptimize/AutoFreezeBlendShape.cs @@ -96,49 +96,52 @@ private void ComputePreserveBlendShapes(BuildContext context, Dictionary()); - set.UnionWith(descriptor.VisemeBlendShapes); - break; - } - case VRC_AvatarDescriptor.LipSyncStyle.JawFlapBlendShape when descriptor.VisemeSkinnedMesh != null: - { - var skinnedMeshRenderer = descriptor.VisemeSkinnedMesh; - if (!preserveBlendShapes.TryGetValue(skinnedMeshRenderer, out var set)) - preserveBlendShapes.Add(skinnedMeshRenderer, set = new HashSet()); - set.Add(descriptor.MouthOpenBlendShapeName); - break; - } - } - - if (descriptor.enableEyeLook) - { - switch (descriptor.customEyeLookSettings.eyelidType) - { - case VRCAvatarDescriptor.EyelidType.None: - break; - case VRCAvatarDescriptor.EyelidType.Bones: + case VRC_AvatarDescriptor.LipSyncStyle.VisemeBlendShape when descriptor.VisemeSkinnedMesh != null: + { + var skinnedMeshRenderer = descriptor.VisemeSkinnedMesh; + if (!preserveBlendShapes.TryGetValue(skinnedMeshRenderer, out var set)) + preserveBlendShapes.Add(skinnedMeshRenderer, set = new HashSet()); + set.UnionWith(descriptor.VisemeBlendShapes); break; - case VRCAvatarDescriptor.EyelidType.Blendshapes - when descriptor.customEyeLookSettings.eyelidsSkinnedMesh != null: + } + case VRC_AvatarDescriptor.LipSyncStyle.JawFlapBlendShape when descriptor.VisemeSkinnedMesh != null: { - var skinnedMeshRenderer = descriptor.customEyeLookSettings.eyelidsSkinnedMesh; + var skinnedMeshRenderer = descriptor.VisemeSkinnedMesh; if (!preserveBlendShapes.TryGetValue(skinnedMeshRenderer, out var set)) preserveBlendShapes.Add(skinnedMeshRenderer, set = new HashSet()); + set.Add(descriptor.MouthOpenBlendShapeName); + break; + } + } - var mesh = skinnedMeshRenderer.sharedMesh; - set.UnionWith( - from index in descriptor.customEyeLookSettings.eyelidsBlendshapes - where 0 <= index && index < mesh.blendShapeCount - select mesh.GetBlendShapeName(index) - ); + if (descriptor.enableEyeLook) + { + switch (descriptor.customEyeLookSettings.eyelidType) + { + case VRCAvatarDescriptor.EyelidType.None: + break; + case VRCAvatarDescriptor.EyelidType.Bones: + break; + case VRCAvatarDescriptor.EyelidType.Blendshapes + when descriptor.customEyeLookSettings.eyelidsSkinnedMesh != null: + { + var skinnedMeshRenderer = descriptor.customEyeLookSettings.eyelidsSkinnedMesh; + if (!preserveBlendShapes.TryGetValue(skinnedMeshRenderer, out var set)) + preserveBlendShapes.Add(skinnedMeshRenderer, set = new HashSet()); + + var mesh = skinnedMeshRenderer.sharedMesh; + set.UnionWith( + from index in descriptor.customEyeLookSettings.eyelidsBlendshapes + where 0 <= index && index < mesh.blendShapeCount + select mesh.GetBlendShapeName(index) + ); + } + break; } - break; } } #endif diff --git a/Editor/Processors/TraceAndOptimize/ConfigureRemoveZeroSizedPolygon.cs b/Editor/Processors/TraceAndOptimize/ConfigureRemoveZeroSizedPolygon.cs new file mode 100644 index 000000000..ee7064810 --- /dev/null +++ b/Editor/Processors/TraceAndOptimize/ConfigureRemoveZeroSizedPolygon.cs @@ -0,0 +1,19 @@ +using nadena.dev.ndmf; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes +{ + public class ConfigureRemoveZeroSizedPolygon : Pass + { + public override string DisplayName => "T&O: ConfigureRemoveZeroSizedPolygon"; + + protected override void Execute(BuildContext context) + { + var state = context.GetState(); + if (!state.RemoveZeroSizedPolygon) return; + + foreach (var renderer in context.GetComponents()) + renderer.gameObject.GetOrAddComponent(); + } + } +} \ No newline at end of file diff --git a/Editor/Processors/TraceAndOptimize/ConfigureRemoveZeroSizedPolygon.cs.meta b/Editor/Processors/TraceAndOptimize/ConfigureRemoveZeroSizedPolygon.cs.meta new file mode 100644 index 000000000..f2454e1e9 --- /dev/null +++ b/Editor/Processors/TraceAndOptimize/ConfigureRemoveZeroSizedPolygon.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: becbc671d0aa43ecaf0e48ec3cf5d7fe +timeCreated: 1698643078 \ No newline at end of file diff --git a/Editor/Processors/TraceAndOptimize/GCDebug.cs b/Editor/Processors/TraceAndOptimize/GCDebug.cs index 7497924e7..f84507ec4 100644 --- a/Editor/Processors/TraceAndOptimize/GCDebug.cs +++ b/Editor/Processors/TraceAndOptimize/GCDebug.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using nadena.dev.ndmf.runtime; using UnityEditor; using UnityEngine; diff --git a/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs b/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs index 3de2099cc..dbfd195c0 100644 --- a/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs +++ b/Editor/Processors/TraceAndOptimize/TraceAndOptimizeProcessor.cs @@ -10,6 +10,7 @@ class TraceAndOptimizeState public bool Enabled; public bool FreezeBlendShape; public bool RemoveUnusedObjects; + public bool RemoveZeroSizedPolygon; public bool MmdWorldCompatibility = true; public bool PreserveEndBone; @@ -31,6 +32,7 @@ public void Initialize(TraceAndOptimize config) { FreezeBlendShape = config.freezeBlendShape; RemoveUnusedObjects = config.removeUnusedObjects; + RemoveZeroSizedPolygon = config.removeZeroSizedPolygons; MmdWorldCompatibility = config.mmdWorldCompatibility; PreserveEndBone = config.preserveEndBone; diff --git a/Editor/Utils/Utils.VRCSDK.cs b/Editor/Utils/Utils.VRCSDK.cs index bc4a82af1..1c133a4ac 100644 --- a/Editor/Utils/Utils.VRCSDK.cs +++ b/Editor/Utils/Utils.VRCSDK.cs @@ -1,3 +1,6 @@ +#if AAO_VRCSDK3_AVATARS + +using System; using System.Collections.Generic; using UnityEngine; using VRC.Dynamics; @@ -25,5 +28,10 @@ public static IEnumerable GetAffectedTransforms(this VRCPhysBoneBase queue.Enqueue(child); } } + + // https://creators.vrchat.com/avatars/#proxy-animations + public static bool IsProxy(this AnimationClip clip) => clip.name.StartsWith("proxy_", StringComparison.Ordinal); } -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef index 10cc54940..c7d9e51af 100644 --- a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef +++ b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef @@ -12,7 +12,10 @@ "Unity.Burst", "nadena.dev.ndmf", "nadena.dev.ndmf.runtime", - "com.anatawa12.avatar-optimizer.api.editor" + "com.anatawa12.avatar-optimizer.api.editor", + "UniHumanoid", + "VRM", + "VRM10" ], "includePlatforms": [ "Editor" @@ -43,6 +46,16 @@ "name": "com.vrchat.avatars", "expression": "", "define": "AAO_VRCSDK3_AVATARS" + }, + { + "name": "com.vrmc.univrm", + "expression": "", + "define": "AAO_VRM0" + }, + { + "name": "com.vrmc.vrm", + "expression": "", + "define": "AAO_VRM1" } ], "noEngineReferences": false 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 4f14499ce..4ebbd7970 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 @@ -21,12 +21,6 @@ ], "autoReferenced": false, "defineConstraints": [], - "versionDefines": [ - { - "name": "com.vrchat.avatars", - "expression": "", - "define": "AAO_VRCSDK3_AVATARS" - } - ], + "versionDefines": [], "noEngineReferences": false } \ No newline at end of file diff --git a/Localization/en.po b/Localization/en.po index bf283e039..6c84030c6 100644 --- a/Localization/en.po +++ b/Localization/en.po @@ -246,6 +246,11 @@ msgstr "" "Some weights of BlendShape '{0}' of some source SkinnedMeshRenderer are not same value.\n" "In this case, the weight of final SkinnedMeshRenderer is not defined so please make uniform weight or freeze BlendShape." +msgid "MergeSkinnedMesh:warning:VRM:FirstPersonFlagsMismatch" +msgstr "" +"Source Renderers had specified mixed FirstPersonFlags values, so fallbacked into '{0}'.\n" +"It is recommended to set same FirstPersonFlags values for Renderers to be merged by MergeSkinnedMesh.\n" + msgid "MergeSkinnedMesh:warning:MeshIsNotNone" msgstr "" "Mesh of SkinnedMeshRenderer is not None!\n" @@ -277,6 +282,9 @@ msgstr "" "Merging both meshes with and without normal is not supported." "Please change import setting of models to include normals!" +msgid "MergeSkinnedMesh:warning:removeZeroSizedPolygonOnSources" +msgstr "Since Remove Zero Sized Polygons are processed later, it has no effects if it is added with the source Skinned Mesh Renderers." + msgid "MergeSkinnedMesh:warning:animation-mesh-hide" msgstr "" "You're merging meshes that visibility animated differently than the merged mesh." @@ -352,6 +360,13 @@ msgstr "Invert All" # endregion +# region RemoveZeroSizedPolygon + +msgid "RemoveZeroSizedPolygon:description" +msgstr "Removes polygons whose area are zero" + +# endregion + # region AvatarGlobalComponent msgid "DeleteEditorOnlyGameObjects:NotOnAvatarDescriptor" @@ -436,6 +451,9 @@ msgstr "Preserve EndBone" msgid "TraceAndOptimize:tooltip:preserveEndBone" msgstr "Prevents removing end bones whose parent is not removed." +msgid "TraceAndOptimize:prop:removeZeroSizedPolygons" +msgstr "Automatically Remove Zero Sized Polygons" + # endregion #region ApplyObjectMapping @@ -456,6 +474,9 @@ msgstr "" "There's no big difference in actual performance, but the number of polygons in the performance rank will increase.\n" "Using multi pass rendering often not be intended. Please check if you intended to use multi pass rendering." +msgid "MeshInfo2:warning:lineStrip" +msgstr "{0} Component does not process SubMeshes with LineStrip." + #endregion # region ErrorReporter diff --git a/Localization/ja.po b/Localization/ja.po index a1f0a909b..8a313a3e6 100644 --- a/Localization/ja.po +++ b/Localization/ja.po @@ -183,6 +183,11 @@ msgstr "" "統合対象のSkinnedMeshRenderer間でBlendShape '{0}'の値が揃っていません。\n" "どの値を適用するかが不定になってしまうため、統合対象の同名BlendShapeの値は揃えるか固定・除去してください。" +msgid "MergeSkinnedMesh:warning:VRM:FirstPersonFlagsMismatch" +msgstr "" +"統合対象のRenderer間でFirstPersonFlagsの値が揃っていなかったため、'{0}'に統一されました。\n" +"MergeSkinnedMeshで統合されるRendererのFirstPersonFlagsの値は揃えることを推奨します。" + msgid "MergeSkinnedMesh:warning:MeshIsNotNone" msgstr "" "SkinnedMeshRendererのMeshがNoneではありません!\n" @@ -214,6 +219,9 @@ msgstr "" "法線があるメッシュとないメッシュの両方を1つに統合する操作には対応していません。" "法線が含まれるようにモデルのインポート設定を変更してください!" +msgid "MergeSkinnedMesh:warning:removeZeroSizedPolygonOnSources" +msgstr "Remove Zero Sized Polygonsは遅めに処理されるため、統合対象のメッシュにつけても効果がありません" + # endregion # region MergeToonLitMaterial @@ -284,6 +292,13 @@ msgstr "すべての有効/無効を入れ替える" # endregion +# region RemoveZeroSizedPolygon + +msgid "RemoveZeroSizedPolygon:description" +msgstr "面積がゼロのポリゴンを削除します。" + +# endregion + # region AvatarGlobalComponent msgid "DeleteEditorOnlyGameObjects:NotOnAvatarDescriptor" @@ -367,6 +382,9 @@ msgstr "endボーンを残す" msgid "TraceAndOptimize:tooltip:preserveEndBone" msgstr "親が削除されていないendボーンを削除しないようにします。" +msgid "TraceAndOptimize:prop:removeZeroSizedPolygons" +msgstr "面積がゼロのポリゴンを自動的に削除する" + # endregion #region ApplyObjectMapping @@ -392,6 +410,9 @@ msgstr "" "実際の負荷に大きな差はありませんが、パフォーマンスランクにおけるポリゴン数が増えるなどの影響があります。\n" "マルチパスレンダリングの使用が意図したものであるかを確認してください。" +msgid "MeshInfo2:warning:lineStrip" +msgstr "{0}コンポーネントはLineStripが使用されているSubMeshを処理しません。" + #endregion # region ErrorReporter diff --git a/Runtime/AvatarTagComponent.cs b/Runtime/AvatarTagComponent.cs index b554cde87..75056544f 100644 --- a/Runtime/AvatarTagComponent.cs +++ b/Runtime/AvatarTagComponent.cs @@ -1,5 +1,5 @@ -using System; using Anatawa12.AvatarOptimizer.ErrorReporting; +using nadena.dev.ndmf.runtime; using UnityEngine; namespace Anatawa12.AvatarOptimizer @@ -19,7 +19,7 @@ internal abstract class AvatarTagComponent : MonoBehaviour { private void OnValidate() { - if (RuntimeUtil.isPlaying) return; + if (RuntimeUtil.IsPlaying) return; ErrorReporterRuntime.TriggerChange(); } diff --git a/Runtime/RemoveZeroSizedPolygon.cs b/Runtime/RemoveZeroSizedPolygon.cs new file mode 100644 index 000000000..11bd888c6 --- /dev/null +++ b/Runtime/RemoveZeroSizedPolygon.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer +{ + [AddComponentMenu("Avatar Optimizer/AAO Remove Zero Sized Polygon")] + [DisallowMultipleComponent] + [RequireComponent(typeof(SkinnedMeshRenderer))] + [HelpURL("https://vpm.anatawa12.com/avatar-optimizer/ja/docs/reference/remove-zero-sized-polygon/")] + internal class RemoveZeroSizedPolygon : AvatarTagComponent + { + } +} diff --git a/Runtime/RemoveZeroSizedPolygon.cs.meta b/Runtime/RemoveZeroSizedPolygon.cs.meta new file mode 100644 index 000000000..35d63a690 --- /dev/null +++ b/Runtime/RemoveZeroSizedPolygon.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b87714a042f7485184e185edd88fec6c +timeCreated: 1698635337 \ No newline at end of file diff --git a/Runtime/RuntimeUtil.cs b/Runtime/RuntimeUtil.cs deleted file mode 100644 index 2ef7cb776..000000000 --- a/Runtime/RuntimeUtil.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using UnityEngine; - -namespace Anatawa12.AvatarOptimizer -{ - // https://github.com/bdunderscore/modular-avatar/blob/db49e2e210bc070671af963ff89df853ae4514a5/Packages/nadena.dev.modular-avatar/Runtime/RuntimeUtil.cs - // Originally under MIT License - // Copyright (c) 2022 bd_ - internal static class RuntimeUtil - { - [CanBeNull] - public static string RelativePath(GameObject root, GameObject child) - { - if (root == child) return ""; - - List pathSegments = new List(); - while (child != root && child != null) - { - pathSegments.Add(child.name); - child = child.transform.parent != null ? child.transform.parent.gameObject : null; - } - - if (child == null) return null; - - pathSegments.Reverse(); - return String.Join("/", pathSegments); - } - -#if UNITY_EDITOR - public static bool isPlaying => UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode; -#else - public static bool isPlaying => true; -#endif - } -} diff --git a/Runtime/RuntimeUtil.cs.meta b/Runtime/RuntimeUtil.cs.meta deleted file mode 100644 index a0d03ce79..000000000 --- a/Runtime/RuntimeUtil.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 3a3212f462e5430a81a74acb08e41adb -timeCreated: 1672489040 \ No newline at end of file diff --git a/Runtime/TraceAndOptimize.cs b/Runtime/TraceAndOptimize.cs index 87382af52..d7626fa58 100644 --- a/Runtime/TraceAndOptimize.cs +++ b/Runtime/TraceAndOptimize.cs @@ -27,6 +27,11 @@ internal class TraceAndOptimize : AvatarGlobalComponent [ToggleLeft] public bool preserveEndBone; + [NotKeyable] + [CL4EELocalized("TraceAndOptimize:prop:removeZeroSizedPolygons")] + [ToggleLeft] + public bool removeZeroSizedPolygons = false; + // common parsing configuration [NotKeyable] [CL4EELocalized("TraceAndOptimize:prop:mmdWorldCompatibility", diff --git a/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef b/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef index 32e8ead24..c3414c1f5 100644 --- a/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef +++ b/Runtime/com.anatawa12.avatar-optimizer.runtime.asmdef @@ -4,7 +4,8 @@ "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" + "Unity.Burst", + "nadena.dev.ndmf.runtime" ], "includePlatforms": [], "excludePlatforms": [], @@ -23,6 +24,16 @@ "name": "com.vrchat.avatars", "expression": "", "define": "AAO_VRCSDK3_AVATARS" + }, + { + "name": "com.vrmc.univrm", + "expression": "", + "define": "AAO_VRM0" + }, + { + "name": "com.vrmc.vrm", + "expression": "", + "define": "AAO_VRM1" } ], "noEngineReferences": false diff --git a/Test~/ApplyObjectMappingTest.cs b/Test~/ApplyObjectMappingTest.cs new file mode 100644 index 000000000..e9830c23d --- /dev/null +++ b/Test~/ApplyObjectMappingTest.cs @@ -0,0 +1,126 @@ +using NUnit.Framework; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.Test +{ + public class ApplyObjectMappingTest + { + [Test] + public void AvatarMask() + { + var root = new GameObject(); + var child1 = Utils.NewGameObject("child1", root.transform); + var child11 = Utils.NewGameObject("child11", child1.transform); + var builder = new ObjectMappingBuilder(root); + + child11.name = "child12"; + + var built = builder.BuildObjectMapping(); + + var rootMapper = new AnimatorControllerMapper(built.CreateAnimationMapper(root)); + + var avatarMask = new AvatarMask(); + avatarMask.transformCount = 1; + avatarMask.SetHumanoidBodyPartActive(AvatarMaskBodyPart.Head, true); + avatarMask.SetHumanoidBodyPartActive(AvatarMaskBodyPart.LeftLeg, false); + avatarMask.SetTransformPath(0, "child1/child11"); + + var animatorController = new AnimatorController(); + animatorController.AddLayer(new AnimatorControllerLayer() + { + name = "layer", + avatarMask = avatarMask, + stateMachine = new AnimatorStateMachine() { name = "layer" }, + }); + + var mappedController = rootMapper.MapAnimatorController(animatorController); + Assert.That(mappedController, Is.Not.EqualTo(animatorController)); + Assert.That(mappedController.layers[0].avatarMask.GetTransformPath(0), + Is.EqualTo("child1/child12")); + Assert.That(avatarMask.GetHumanoidBodyPartActive(AvatarMaskBodyPart.Head), Is.True); + Assert.That(avatarMask.GetHumanoidBodyPartActive(AvatarMaskBodyPart.LeftLeg), Is.False); + } + + [Test] + public void PreserveAnimationLength() + { + var root = new GameObject(); + var child1 = Utils.NewGameObject("child1", root.transform); + var child11 = Utils.NewGameObject("child11", child1.transform); + var builder = new ObjectMappingBuilder(root); + + Object.DestroyImmediate(child11); + + var built = builder.BuildObjectMapping(); + + var rootMapper = new AnimatorControllerMapper(built.CreateAnimationMapper(root)); + + var animatorController = new AnimatorController(); + var layer = new AnimatorControllerLayer() + { + name = "layer", + stateMachine = new AnimatorStateMachine() { name = "layer" }, + }; + var state = layer.stateMachine.AddState("theState"); + var clip = new AnimationClip(); + clip.SetCurve("child1/child11", typeof(GameObject), "m_IsActive", AnimationCurve.Constant(0, 0.3f, 1)); + state.motion = clip; + animatorController.AddLayer(layer); + + var mappedController = rootMapper.MapAnimatorController(animatorController); + Assert.That(mappedController, Is.Not.EqualTo(animatorController)); + var mappedClip = mappedController.layers[0].stateMachine.states[0].state.motion as AnimationClip; + Assert.That(mappedClip, Is.Not.Null); + + Assert.That(mappedClip.length, Is.EqualTo(0.3f)); + Assert.That(AnimationUtility.GetCurveBindings(mappedClip)[0].path, + Contains.Substring("AvatarOptimizerClipLengthDummy")); + } + + [Test] + public void PreserveProxyAnimation() + { + var root = new GameObject(); + var child1 = Utils.NewGameObject("child1", root.transform); + var child11 = Utils.NewGameObject("child11", child1.transform); + var builder = new ObjectMappingBuilder(root); + + Object.DestroyImmediate(child11); + + var built = builder.BuildObjectMapping(); + + var rootMapper = new AnimatorControllerMapper(built.CreateAnimationMapper(root)); + + var animatorController = new AnimatorController(); + var layer = new AnimatorControllerLayer() + { + name = "layer", + stateMachine = new AnimatorStateMachine() { name = "layer" }, + }; + var state = layer.stateMachine.AddState("theState"); + var clip = new AnimationClip(); + clip.SetCurve("child1/child11", typeof(GameObject), "m_IsActive", AnimationCurve.Constant(0, 0.3f, 1)); + state.motion = clip; + + var proxyMotion = + AssetDatabase.LoadAssetAtPath( + AssetDatabase.GUIDToAssetPath("806c242c97b686d4bac4ad50defd1fdb")); + state = layer.stateMachine.AddState("afk"); + state.motion = proxyMotion; + + animatorController.AddLayer(layer); + + var mappedController = rootMapper.MapAnimatorController(animatorController); + Assert.That(mappedController, Is.Not.EqualTo(animatorController)); + + // ensure non-proxy mapped + var mappedClip = mappedController.layers[0].stateMachine.states[0].state.motion as AnimationClip; + Assert.That(mappedClip, Is.Not.EqualTo(clip)); + + mappedClip = mappedController.layers[0].stateMachine.states[1].state.motion as AnimationClip; + Assert.That(mappedClip, Is.EqualTo(proxyMotion)); + } + } +} \ No newline at end of file diff --git a/Test~/ApplyObjectMappingTest.cs.meta b/Test~/ApplyObjectMappingTest.cs.meta new file mode 100644 index 000000000..5c0a23352 --- /dev/null +++ b/Test~/ApplyObjectMappingTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 17ff08d02c7e45adac0adee86f7137d6 +timeCreated: 1698298544 \ No newline at end of file diff --git a/Test~/MergeBoneTest.cs b/Test~/MergeBoneTest.cs new file mode 100644 index 000000000..907538b33 --- /dev/null +++ b/Test~/MergeBoneTest.cs @@ -0,0 +1,34 @@ +using Anatawa12.AvatarOptimizer.Processors; +using NUnit.Framework; +using UnityEngine; + +namespace Anatawa12.AvatarOptimizer.Test +{ + public class MergeBoneTest + { + [Test] + public void ExtremeSmall() + { + var epsilonVector3 = new Vector3(float.Epsilon, float.Epsilon, float.Epsilon); + var root = TestUtils.NewAvatar(); + var merged = Utils.NewGameObject("merged", root.transform); + merged.transform.localScale = epsilonVector3; + + var identity = Utils.NewGameObject("identity", merged.transform); + var moved = Utils.NewGameObject("moved", merged.transform); + moved.transform.localPosition = Vector3.one; + + var transInfo = MergeBoneProcessor.MergeBoneTransParentInfo.Compute(merged.transform, root.transform); + + var identityAfter = transInfo.ComputeInfoFor(identity.transform); + Assert.That(identityAfter.scale, Is.EqualTo(epsilonVector3)); + Assert.That(identityAfter.position, Is.EqualTo(Vector3.zero)); + Assert.That(identityAfter.rotation, Is.EqualTo(Quaternion.identity)); + + var movedAfter = transInfo.ComputeInfoFor(moved.transform); + Assert.That(movedAfter.scale, Is.EqualTo(epsilonVector3)); + Assert.That(movedAfter.position, Is.EqualTo(epsilonVector3)); + Assert.That(movedAfter.rotation, Is.EqualTo(Quaternion.identity)); + } + } +} \ No newline at end of file diff --git a/Test~/MergeBoneTest.cs.meta b/Test~/MergeBoneTest.cs.meta new file mode 100644 index 000000000..b6b4b7191 --- /dev/null +++ b/Test~/MergeBoneTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d513e5d706854597b6990b5ad7dfc559 +timeCreated: 1698296645 \ No newline at end of file diff --git a/Test~/MeshInfo2/MeshInfo2Test.cs b/Test~/MeshInfo2/MeshInfo2Test.cs index 471958e0e..ce6d21d5f 100644 --- a/Test~/MeshInfo2/MeshInfo2Test.cs +++ b/Test~/MeshInfo2/MeshInfo2Test.cs @@ -1,6 +1,8 @@ +using System; using Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes; using NUnit.Framework; using UnityEngine; +using UnityEngine.Rendering; namespace Anatawa12.AvatarOptimizer.Test { @@ -95,5 +97,101 @@ public void RootBoneWithNoneMeshSkinnedMeshRenderer() var meshInfo2 = new MeshInfo2(smr); Assert.That(meshInfo2.RootBone, Is.EqualTo(secondGo.transform)); } + + [Test] + public void MultiFrameBlendShapeWithPartiallyIdentity() + { + var mesh = BoxMesh(); + var deltas = new Vector3[8]; + deltas.AsSpan().Fill(new Vector3(1, 2, 3)); + mesh.AddBlendShapeFrame("shape", 0, new Vector3[8], null, null); + mesh.AddBlendShapeFrame("shape", 1, new Vector3[8], null, null); + mesh.AddBlendShapeFrame("shape", 2, new Vector3[8], null, null); + mesh.AddBlendShapeFrame("shape", 3, deltas, null, null); + mesh.AddBlendShapeFrame("shape", 4, new Vector3[8], null, null); + + var go = new GameObject(); + var smr = go.AddComponent(); + smr.sharedMesh = mesh; + + var meshInfo2 = new MeshInfo2(smr); + + foreach (var vertex in meshInfo2.Vertices) + { + var frames = vertex.BlendShapes["shape"]; + Assert.That(frames.Length, Is.EqualTo(5)); + for (var i = 0; i < frames.Length; i++) + { + Assert.That(frames[i].Weight, Is.EqualTo((float)i)); + Assert.That(frames[i].Position, Is.EqualTo(i == 3 ? new Vector3(1, 2, 3) : new Vector3())); + } + } + } + + [Test] + public void BlendShapeWithFrameAtZero() + { + var mesh = BoxMesh(); + var deltas = new Vector3[8]; + deltas.AsSpan().Fill(new Vector3(1, 2, 3)); + mesh.AddBlendShapeFrame("shape", 0, deltas, null, null); + mesh.AddBlendShapeFrame("shape", 1, deltas, null, null); + + var go = new GameObject(); + var smr = go.AddComponent(); + smr.sharedMesh = mesh; + + var meshInfo2 = new MeshInfo2(smr); + + Vector3 position; + var vertex = meshInfo2.Vertices[0]; + Assert.That(vertex.TryGetBlendShape("shape", 0, out position, out _, out _), Is.True); + Assert.That(position, Is.EqualTo(new Vector3(0, 0, 0))); + + Assert.That(vertex.TryGetBlendShape("shape", 0, out position, out _, out _, getDefined: true), Is.True); + Assert.That(position, Is.EqualTo(new Vector3(1, 2, 3))); + } + + private Mesh BoxMesh() + { + var mesh = new Mesh + { + vertices = new[] + { + new Vector3(-1, -1, -1), + new Vector3(+1, -1, -1), + new Vector3(-1, +1, -1), + new Vector3(+1, +1, -1), + new Vector3(-1, -1, +1), + new Vector3(+1, -1, +1), + new Vector3(-1, +1, +1), + new Vector3(+1, +1, +1), + }, + triangles = new[] + { + 0, 1, 2, + 1, 3, 2, + + 4, 6, 5, + 5, 6, 7, + + 0, 4, 1, + + 1, 4, 5, + 1, 5, 3, + + 3, 5, 7, + 3, 7, 2, + + 2, 7, 6, + 2, 6, 0, + }, + }; + + mesh.subMeshCount = 1; + mesh.SetSubMesh(0, new SubMeshDescriptor(0, mesh.triangles.Length)); + + return mesh; + } } } diff --git a/Test~/com.anatawa12.avatar-optimizer.test.asmdef b/Test~/com.anatawa12.avatar-optimizer.test.asmdef index 739364e38..023d6db02 100644 --- a/Test~/com.anatawa12.avatar-optimizer.test.asmdef +++ b/Test~/com.anatawa12.avatar-optimizer.test.asmdef @@ -22,7 +22,8 @@ "VRC.SDK3.Dynamics.PhysBone.dll", "VRC.Dynamics.dll", "VRCSDK3A.dll", - "VRCSDKBase.dll" + "VRCSDKBase.dll", + "System.Memory.dll" ], "autoReferenced": false, "defineConstraints": [], @@ -31,6 +32,16 @@ "name": "com.vrchat.avatars", "expression": "", "define": "AAO_VRCSDK3_AVATARS" + }, + { + "name": "com.vrmc.univrm", + "expression": "", + "define": "AAO_VRM0" + }, + { + "name": "com.vrmc.vrm", + "expression": "", + "define": "AAO_VRM1" } ], "noEngineReferences": false diff --git a/package.json b/package.json index c99029650..e849ff02e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "com.anatawa12.avatar-optimizer", "version": "1.6.0-beta.2", + "private": true, "unity": "2019.4", "description": "Set of Anatawa12's Small Avatar Optimization Utilities", "displayName": "Anatawa12's AvatarOptimizer",