diff --git a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/Anglelimit.cs b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/Anglelimit.cs index 52f916f6c..e3b47c205 100644 --- a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/Anglelimit.cs +++ b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/Anglelimit.cs @@ -1,3 +1,4 @@ +using System; using UniGLTF.SpringBoneJobs.Blittables; using Unity.Mathematics; @@ -5,62 +6,76 @@ namespace UniGLTF.SpringBoneJobs { public static class Anglelimit { + public static readonly float SINGULARITY_EPSILON = 1e-8f; + public static float3 Apply( - in BlittableJointImmutable logic, in BlittableJointMutable joint, - in quaternion parentRotation, in float3 head, in float3 nextTail) + in BlittableJointImmutable logic, + in BlittableJointMutable joint, + in quaternion parentRotation, + in float3 head, + in float3 nextTail + ) { + if (joint.anglelimitType == AnglelimitTypes.None) + { + // do nothing + return nextTail; + } + + var angleSpaceToWorld = anglelimitSpaceToWorld(logic, joint, parentRotation); + var tailDir = math.mul( + math.inverse(angleSpaceToWorld), + math.normalizesafe(nextTail - head) + ); + switch (joint.anglelimitType) { case AnglelimitTypes.None: - // do nothing - return nextTail; + throw new Exception("not reach here"); case AnglelimitTypes.Cone: - { - var angleSpaceToWorld = anglelimitSpaceToWorld(logic, joint, parentRotation); - var tailDir = math.mul(math.inverse(angleSpaceToWorld), math.normalizesafe(nextTail - head)); - tailDir = AnglelimitCone.Apply(tailDir, joint.anglelimit1); - return head + math.mul(angleSpaceToWorld, tailDir) * logic.length; - } + + tailDir = AnglelimitCone.Apply(tailDir, joint.anglelimit1); + break; case AnglelimitTypes.Hinge: - { - var angleSpaceToWorld = anglelimitSpaceToWorld(logic, joint, parentRotation); - var tailDir = math.mul(math.inverse(angleSpaceToWorld), math.normalizesafe(nextTail - head)); - tailDir = AnglelimitHinge.Apply(tailDir, joint.anglelimit1); - return head + math.mul(angleSpaceToWorld, tailDir) * logic.length; - } - + tailDir = AnglelimitHinge.Apply(tailDir, joint.anglelimit1); + break; case AnglelimitTypes.Spherical: - { - var angleSpaceToWorld = anglelimitSpaceToWorld(logic, joint, parentRotation); - var tailDir = math.mul(math.inverse(angleSpaceToWorld), math.normalizesafe(nextTail - head)); - tailDir = AnglelimitSpherical.Apply(tailDir, joint.anglelimit1, joint.anglelimit2); - return head + math.mul(angleSpaceToWorld, tailDir) * logic.length; - } + tailDir = AnglelimitSpherical.Apply( + tailDir, + joint.anglelimit1, + joint.anglelimit2 + ); + break; default: - throw new System.ArgumentException($"unknown joint.anglelimitType: {joint.anglelimitType}"); + throw new System.ArgumentException( + $"unknown joint.anglelimitType: {joint.anglelimitType}" + ); } + + return head + math.mul(angleSpaceToWorld, tailDir) * logic.length; } /// nextTail(position vector in world space) /// tailDir(directionay vector in angle space) /// - public static quaternion anglelimitSpaceToWorld(in BlittableJointImmutable logic, in BlittableJointMutable joint, - in quaternion parentRotation) + public static quaternion anglelimitSpaceToWorld( + in BlittableJointImmutable logic, + in BlittableJointMutable joint, + in quaternion parentRotation + ) { // Y+方向からjointのheadからtailに向かうベクトルへの最小回転 var axisRotation = getAxisRotation(logic.boneAxis); // limitのローカル空間をワールド空間に写像する回転 - return - math.mul(parentRotation, - math.mul(logic.localRotation, - math.mul(axisRotation, - joint.anglelimitOffset))) - ; + return math.mul( + parentRotation, + math.mul(logic.localRotation, math.mul(axisRotation, joint.anglelimitOffset)) + ); } /// @@ -71,20 +86,22 @@ namespace UniGLTF.SpringBoneJobs /// /// TODO: Replace with the appropriate link to the specification later /// - public static quaternion getAxisRotation(in float3 to) + public static quaternion getAxisRotation(in float3 boneAxis) { - // dot(from, to) + 1 - var dot1 = to.y + 1f; + // headからtailに向かうベクトルとY+方向との内積 + var dot = boneAxis.y; - // Handle the case where from and to are parallel and opposite - if (dot1 < 1e-8f) // dot is approximately -1 + if (dot <= -1f + SINGULARITY_EPSILON) { - return new quaternion(1f, 0f, 0f, 0f); + // headからtailに向かうベクトルがY-方向の場合、X軸周りに180度回転させた回転を設定する + return new quaternion(1, 0, 0, 0); + } + else + { + // それ以外の場合、Y+方向からjointのheadからtailに向かうベクトルへの最小回転を設定する + // quaternion(cross(from, to); dot(from, to) + 1).normalized + return math.normalizesafe(new quaternion(boneAxis.z, 0, -boneAxis.x, dot + 1)); } - - // General case - // quaternion(cross(from, to); dot(from, to) + 1).normalized - return math.normalizesafe(new quaternion(to.z, 0f, -to.x, dot1)); } } -} \ No newline at end of file +} diff --git a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitCone.cs b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitCone.cs index d8c159f86..bd530af4e 100644 --- a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitCone.cs +++ b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitCone.cs @@ -4,30 +4,41 @@ namespace UniGLTF.SpringBoneJobs { public static class AnglelimitCone { - /// AngleLimit空間の方向ベクトル - /// radius + /// AngleLimit空間の方向ベクトル + /// radius /// AngleLimit空間の方向ベクトル - public static float3 Apply(in float3 src, float angleLimit) + public static float3 Apply(float3 tailDir, float limitAngle) { - // tailDirのy要素をjointに設定されたangleの余弦と比較する - var cosAngle = math.cos(angleLimit); - if (src.y >= cosAngle) - { - return src; - } + // angleを0以上π以下に制限する + limitAngle = math.clamp(limitAngle, 0.0f, math.PI); - var tailDir = src; + // tailDirのy要素をlimitに設定されたangleの余弦と比較する + var cosLimitAngle = math.cos(limitAngle); + if (tailDir.y < cosLimitAngle) { // x・z要素を、tailDirの正弦とjointに設定されたangleの正弦の比を用いてスケールする - var ratio = math.sqrt((1.0f - cosAngle * cosAngle) / (1.0f - tailDir.y * tailDir.y)); - tailDir.x *= ratio; - tailDir.z *= ratio; + var horizontalLengthSquared = 1.0f - tailDir.y * tailDir.y; - // y要素を、jointに設定されたangleの余弦とする - tailDir.y = cosAngle; + if (horizontalLengthSquared <= Anglelimit.SINGULARITY_EPSILON) + { + // tailDirがy軸負方向の場合、z軸正方向側を選択する + tailDir.x = 0.0f; + tailDir.z = math.sqrt(1.0f - cosLimitAngle * cosLimitAngle); + } + else + { + var scale = math.sqrt( + (1.0f - cosLimitAngle * cosLimitAngle) / horizontalLengthSquared + ); + tailDir.x *= scale; + tailDir.z *= scale; + } + + // y要素をlimitに設定されたangleの余弦とする + tailDir.y = cosLimitAngle; } return tailDir; } } -} \ No newline at end of file +} diff --git a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitHinge.cs b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitHinge.cs index 45f291929..9e4fcf387 100644 --- a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitHinge.cs +++ b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitHinge.cs @@ -4,29 +4,39 @@ namespace UniGLTF.SpringBoneJobs { public static class AnglelimitHinge { - /// AngleLimit空間の方向ベクトル - /// radius + /// AngleLimit空間の方向ベクトル + /// radius /// AngleLimit空間の方向ベクトル - public static float3 Apply(in float3 src, float limitAngle) + public static float3 Apply(float3 tailDir, float limitAngle) { - // x要素を0にし、正規化する - float3 tailDir = src; - tailDir.x = 0.0f; - tailDir = math.normalizesafe(tailDir); + // angleを0以上π以下に制限する + limitAngle = math.clamp(limitAngle, 0.0f, math.PI); - // tailDirのy要素をjointに設定されたangleの余弦と比較する - var cosAngle = math.cos(limitAngle); - if (tailDir.y < cosAngle) + var projectedLengthSquared = tailDir.y * tailDir.y + tailDir.z * tailDir.z; + if (projectedLengthSquared <= Anglelimit.SINGULARITY_EPSILON) { - // z要素を、tailDirの正弦とjointに設定されたangleの正弦の比を用いてスケールする - var ratio = math.sqrt((1.0f - cosAngle * cosAngle) / (1.0f - tailDir.y * tailDir.y)); - tailDir.z *= ratio; - - // y要素を、jointに設定されたangleの余弦とする - tailDir.y = cosAngle; + // tailDirがx軸正方向または負方向の場合、Y軸正方向を選択する + tailDir = math.float3(0.0f, 1.0f, 0.0f); } + else + { + // tailDirをヒンジのYZ平面へ射影する + tailDir = + math.float3(0.0f, tailDir.y, tailDir.z) / math.sqrt(projectedLengthSquared); + // tailDirのy要素をlimitに設定されたangleの余弦と比較する + var cosLimitAngle = math.cos(limitAngle); + if (tailDir.y < cosLimitAngle) + { + var sinLimitAngle = math.sqrt(1.0f - cosLimitAngle * cosLimitAngle); + + // zの符号を維持し、z==0 の場合は z軸正方向側を選択する + var zSign = (tailDir.z < 0.0f) ? -1.0f : 1.0f; + tailDir.y = cosLimitAngle; + tailDir.z = sinLimitAngle * zSign; + } + } return tailDir; } } -} \ No newline at end of file +} diff --git a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitSpherical.cs b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitSpherical.cs index b15ce10fa..86dfe8264 100644 --- a/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitSpherical.cs +++ b/Packages/UniGLTF/Runtime/SpringBoneJobs/Anglelimit/AnglelimitSpherical.cs @@ -5,33 +5,55 @@ namespace UniGLTF.SpringBoneJobs public static class AnglelimitSpherical { /// AngleLimit空間の方向ベクトル - /// radius - /// radius + /// radius + /// radius /// AngleLimit空間の方向ベクトル - public static float3 Apply(in float3 tailDir, float limitAnglePhi, float limitAngleTheta) + public static float3 Apply(float3 tailDir, float limitPitch, float limitYaw) { - // tailDirのphi・thetaを計算する - var phi = math.atan2(tailDir.z, tailDir.y); - var theta = math.asin(tailDir.x); + // pitchを0以上π以下、yawを0以上π/2以下に制限する + limitPitch = math.clamp(limitPitch, 0.0f, math.PI); + limitYaw = math.clamp(limitYaw, 0.0f, math.PI / 2.0f); - // phi・thetaをjointに設定されたphi・thetaを用いて制限する - // var isLimited = false; - if (math.abs(phi) > limitAnglePhi) + // tailDirのpitch・yawを計算する + float pitch; + if (tailDir.y <= -1.0f + Anglelimit.SINGULARITY_EPSILON) + { + // tailDirがy軸負方向の場合、Z軸正方向側の境界を選択するため、pitchをπとする + pitch = math.PI; + } + else if (math.abs(tailDir.x) >= 1.0f - Anglelimit.SINGULARITY_EPSILON) + { + // tailDirがx軸正方向または負方向の場合、pitchを0とする + pitch = 0.0f; + } + else + { + pitch = math.atan2(tailDir.z, tailDir.y); + } + var yaw = math.asin(math.clamp(tailDir.x, -1f, 1f)); + + // pitchをlimitに設定されたpitchを用いて制限する + if (math.abs(pitch) > limitPitch) { // isLimited = true; - phi = limitAnglePhi * math.sign(phi); + pitch = limitPitch * math.sign(pitch); } - // thetaをjointに設定されたthetaを用いて制限する - if (math.abs(theta) > limitAngleTheta) + // yawをlimitに設定されたyawを用いて制限する + if (math.abs(yaw) > limitYaw) { - // isLimited = true; - theta = limitAngleTheta * math.sign(theta); + // isLimited = true; + yaw = limitYaw * math.sign(yaw); } - // tailDirをphi・thetaを用いて再計算する - var cos_theta = math.cos(theta); - return new float3(math.sin(theta), cos_theta * math.cos(phi), cos_theta * math.sin(phi)); + // tailDirをpitch・yawを用いて再計算する + tailDir = math.float3( + math.sin(yaw), + math.cos(yaw) * math.cos(pitch), + math.cos(yaw) * math.sin(pitch) + ); + + return tailDir; } } -} \ No newline at end of file +} diff --git a/Packages/UniGLTF/Runtime/SpringBoneJobs/Blittables/BlittableJointMutable.cs b/Packages/UniGLTF/Runtime/SpringBoneJobs/Blittables/BlittableJointMutable.cs index ccc69d2b6..1f63fae43 100644 --- a/Packages/UniGLTF/Runtime/SpringBoneJobs/Blittables/BlittableJointMutable.cs +++ b/Packages/UniGLTF/Runtime/SpringBoneJobs/Blittables/BlittableJointMutable.cs @@ -38,6 +38,8 @@ namespace UniGLTF.SpringBoneJobs.Blittables public float anglelimit2 => _data.c2.z; public quaternion anglelimitOffset => _data.c3.xyzw; + // angleLimit1: angle (Cone/Hinge), pitch (Spherical) + // angleLimit2: yaw (Spherical) public BlittableJointMutable(float stiffnessForce = 0, float gravityPower = 0, float3 gravityDir = default, diff --git a/Packages/VRM10/Editor/Components/SpringBone/VRM10SpringBoneJointEditor.cs b/Packages/VRM10/Editor/Components/SpringBone/VRM10SpringBoneJointEditor.cs index d2f44d272..77ac620e8 100644 --- a/Packages/VRM10/Editor/Components/SpringBone/VRM10SpringBoneJointEditor.cs +++ b/Packages/VRM10/Editor/Components/SpringBone/VRM10SpringBoneJointEditor.cs @@ -63,10 +63,11 @@ namespace UniVRM10 /// spring 情報。readonly。mouse click による hierarchy 参照補助 /// EditorGUI.BeginDisabledGroup(true); - var isLastTail = ShowSpringInfo(); + var info = Vrm10InstanceSpringBone.GetEditInfo(m_target, m_root); + ShowSpringInfo(info); EditorGUI.EndDisabledGroup(); - if (isLastTail) + if (info.IsLastTail) { EditorGUILayout.HelpBox("末端 joint です。下記の設定は使用されません。", MessageType.Info); } @@ -88,7 +89,7 @@ namespace UniVRM10 // // angle limit // - var fold = EditorGUILayout.Foldout(m_showAnglelimitSettings, "AngleLimit Settings (experimental)"); + var fold = EditorGUILayout.Foldout(m_showAnglelimitSettings, $"AngleLimit Settings (1.0-draft)"); if (m_showAnglelimitSettings != fold) { m_showAnglelimitSettings = fold; @@ -96,8 +97,13 @@ namespace UniVRM10 } if (m_showAnglelimitSettings) { - EditorGUILayout.HelpBox("SpringBoneの角度制限はまだdraft仕様です。将来的に仕様が変更される可能性があります。また、VRMファイルへのインポート・エクスポート機能はまだ実装されていません。\nThe angle limit feature for SpringBone is still in draft status. The specifications may change in the future. Also, the import/export of VRM files has not yet been implemented.", MessageType.Warning); + EditorGUILayout.HelpBox("SpringBoneの角度制限はまだdraft仕様です。将来的に仕様が変更される可能性があります。\nThe angle limit feature for SpringBone is still in draft status. The specifications may change in the future.", MessageType.Warning); + + if (info.IsLastTail) + { + EditorGUILayout.HelpBox("末端ノードではAngleLimitは無視されます。", MessageType.Info); + } EditorGUILayout.PropertyField(m_angleLimitType); switch ((UniGLTF.SpringBoneJobs.AnglelimitTypes)m_angleLimitType.enumValueIndex) { @@ -106,12 +112,12 @@ namespace UniVRM10 case UniGLTF.SpringBoneJobs.AnglelimitTypes.Cone: EditorGUILayout.PropertyField(m_angleLimitRotation); - EditorGUILayout.PropertyField(m_angleLimitPitch); + EditorGUILayout.PropertyField(m_angleLimitPitch, new GUIContent("Angle")); break; case UniGLTF.SpringBoneJobs.AnglelimitTypes.Hinge: EditorGUILayout.PropertyField(m_angleLimitRotation); - EditorGUILayout.PropertyField(m_angleLimitPitch); + EditorGUILayout.PropertyField(m_angleLimitPitch, new GUIContent("Angle")); break; case UniGLTF.SpringBoneJobs.AnglelimitTypes.Spherical: @@ -139,52 +145,51 @@ namespace UniVRM10 /// - Springに関連付けられたColliderの情報を表示する /// /// 末端 - bool ShowSpringInfo() + static void ShowSpringInfo(Vrm10InstanceSpringBone.EditInfo info) { - if (m_root == null) + if (info.Root == null) { EditorGUILayout.HelpBox("no vrm-1.0", MessageType.Warning); - return false; + return; } - EditorGUILayout.ObjectField("Vrm-1.0", m_root, typeof(Vrm10Instance), true, null); + EditorGUILayout.ObjectField("Vrm-1.0", info.Root, typeof(Vrm10Instance), true, null); - var found = m_root.SpringBone.FindJoint(m_target); - if (!found.HasValue) + if (info.Spring == null) { EditorGUILayout.HelpBox("This joint not belongs any spring", MessageType.Warning); - return false; + return; } - var (spring, i, _) = found.Value; - m_showJoints = EditorGUILayout.Foldout(m_showJoints, $"Springs[{i}]({spring.Name})"); + // var (spring, i, _) = found.Value; + m_showJoints = EditorGUILayout.Foldout(m_showJoints, $"Springs[{info.SpringIndex}]({info.Spring.Name})"); int? jointIndex = default; // joints - for (int j = 0; j < spring.Joints.Count; ++j) + for (int j = 0; j < info.Spring.Joints.Count; ++j) { - var joint = spring.Joints[j]; + var joint = info.Spring.Joints[j]; if (m_showJoints) { var label = $"Joints[{j}]"; - if (joint == m_target) + if (joint == info.Target) { label += "★"; } EditorGUILayout.ObjectField(label, joint, typeof(VRM10SpringBoneJoint), true, null); } - if (joint == m_target) + if (joint == info.Target) { jointIndex = j; } } m_showColliders = EditorGUILayout.Foldout(m_showColliders, "ColliderGroups"); - if (m_showColliders && found.HasValue) + if (m_showColliders) { // collider groups - for (int j = 0; j < spring.ColliderGroups.Count; ++j) + for (int j = 0; j < info.Spring.ColliderGroups.Count; ++j) { - var group = spring.ColliderGroups[j]; + var group = info.Spring.ColliderGroups[j]; EditorGUILayout.LabelField($"ColliderGroups[{j}]({group.Name})"); for (int k = 0; k < group.Colliders.Count; ++k) { @@ -194,8 +199,6 @@ namespace UniVRM10 } } } - - return jointIndex == (spring.Joints.Count - 1); } /// diff --git a/Packages/VRM10/Runtime/Components/SpringBone/VRM10SpringBoneJoint.cs b/Packages/VRM10/Runtime/Components/SpringBone/VRM10SpringBoneJoint.cs index d6a7949d7..3a4e9f4c2 100644 --- a/Packages/VRM10/Runtime/Components/SpringBone/VRM10SpringBoneJoint.cs +++ b/Packages/VRM10/Runtime/Components/SpringBone/VRM10SpringBoneJoint.cs @@ -26,15 +26,27 @@ namespace UniVRM10 [SerializeField] public float m_jointRadius = 0.02f; + /// + /// VRMC_springBone_limit + /// [SerializeField] public UniGLTF.SpringBoneJobs.AnglelimitTypes m_anglelimitType; + /// + /// VRMC_springBone_limit + /// [SerializeField] public Quaternion m_limitSpaceOffset = Quaternion.identity; + /// + /// VRMC_springBone_limit + /// [SerializeField, Range(0, Mathf.PI)] public float m_pitch = Mathf.PI; + /// + /// VRMC_springBone_limit + /// [SerializeField, Range(0, Mathf.PI / 2)] public float m_yaw = 0; @@ -59,7 +71,7 @@ namespace UniVRM10 } } - void AddJointRecursive(Transform t, VRM10SpringBoneJoint src) + void AddJointRecursive(Transform t, VRM10SpringBoneJoint src, bool copyLimit) { var joint = t.gameObject.GetOrAddComponent(); @@ -70,10 +82,19 @@ namespace UniVRM10 joint.m_dragForce = src.m_dragForce; joint.m_jointRadius = src.m_jointRadius; + if (copyLimit) + { + joint.m_anglelimitType = src.m_anglelimitType; + joint.m_limitSpaceOffset = src.m_limitSpaceOffset; + joint.m_yaw = src.m_yaw; + joint.m_pitch = src.m_pitch; + } + + if (t.childCount > 0) { // only first child - AddJointRecursive(t.GetChild(0), src); + AddJointRecursive(t.GetChild(0), src, copyLimit); } } @@ -106,7 +127,7 @@ namespace UniVRM10 return; } - AddJointRecursive(transform.GetChild(0), this); + AddJointRecursive(transform.GetChild(0), this, true); // updater root foreach (var spring in root.SpringBone.Springs) diff --git a/Packages/VRM10/Runtime/Components/Vrm10Instance/Vrm10InstanceSpringBone.cs b/Packages/VRM10/Runtime/Components/Vrm10Instance/Vrm10InstanceSpringBone.cs index f47a972c3..16ef83465 100644 --- a/Packages/VRM10/Runtime/Components/Vrm10Instance/Vrm10InstanceSpringBone.cs +++ b/Packages/VRM10/Runtime/Components/Vrm10Instance/Vrm10InstanceSpringBone.cs @@ -89,5 +89,58 @@ namespace UniVRM10 } return default; } + + public struct EditInfo + { + public VRM10SpringBoneJoint Target; + public Vrm10Instance Root; + public Spring Spring; + public int SpringIndex; + public int JointIndex; + public bool IsLastTail; + } + + /// + /// Return SpringBone EditInfo. + /// Do not call on runtime play for performance. + /// + /// On Editor may null. + /// GetComponentInParent cache. + /// + public static EditInfo GetEditInfo(VRM10SpringBoneJoint target, Vrm10Instance root) + { + EditInfo info = default; + if (target == null) + { + return info; + } + info.Target = target; + + if (root == null) + { + root = target.GetComponentInParent(); + } + if (root == null) + { + return info; + } + info.Root = root; + + var found = root.SpringBone.FindJoint(target); + if (!found.HasValue) + { + return info; + } + (info.Spring, info.SpringIndex, info.JointIndex) = found.Value; + + + // ヒエラルキーの末端ではなく、Springの末端である + if (info.Spring.Joints.Count > 0 && info.Spring.Joints[info.Spring.Joints.Count - 1] == target) + { + info.IsLastTail = true; + } + + return info; + } } } diff --git a/Packages/VRM10/Runtime/IO/Vrm10Exporter.cs b/Packages/VRM10/Runtime/IO/Vrm10Exporter.cs index fc6724f5d..e285ec744 100644 --- a/Packages/VRM10/Runtime/IO/Vrm10Exporter.cs +++ b/Packages/VRM10/Runtime/IO/Vrm10Exporter.cs @@ -14,6 +14,7 @@ namespace UniVRM10 public const string SPRINGBONE_EXTENDED_COLLIDER_SPEC_VERSION = "1.0"; public const string NODE_CONSTRAINT_SPEC_VERSION = "1.0"; public const string MTOON_SPEC_VERSION = "1.0"; + public const string VRMC_springBone_limit_SPEC_VERSION = "1.0-draft"; public const string LICENSE_URL_JA = "https://vrm.dev/licenses/1.0/"; public const string LICENSE_URL_EN = "https://vrm.dev/licenses/1.0/en/"; @@ -451,63 +452,70 @@ namespace UniVRM10 GravityPower = y.m_gravityPower, }; - switch (y.m_anglelimitType) + if (y.transform.childCount > 0) { - case UniGLTF.SpringBoneJobs.AnglelimitTypes.Cone: - { - var limit = new UniGLTF.Extensions.VRMC_springBone_limit.VRMC_springBone_limit + // childが存在する => 末端でない + switch (y.m_anglelimitType) + { + case UniGLTF.SpringBoneJobs.AnglelimitTypes.Cone: { - Limit = new UniGLTF.Extensions.VRMC_springBone_limit.Limit + var limit = new UniGLTF.Extensions.VRMC_springBone_limit.VRMC_springBone_limit { - Cone = new UniGLTF.Extensions.VRMC_springBone_limit.ConeLimit + SpecVersion = VRMC_springBone_limit_SPEC_VERSION, + Limit = new UniGLTF.Extensions.VRMC_springBone_limit.Limit { - Rotation = ReverseXToFloat4(y.m_limitSpaceOffset), - Angle = y.m_pitch, + Cone = new UniGLTF.Extensions.VRMC_springBone_limit.ConeLimit + { + Rotation = ReverseXToFloat4(y.m_limitSpaceOffset), + Angle = y.m_pitch, + } } - } - }; - glTFExtension extensions = default; - UniGLTF.Extensions.VRMC_springBone_limit.GltfSerializer.SerializeTo(ref extensions, limit); - joint.Extensions = extensions; - break; - } - case UniGLTF.SpringBoneJobs.AnglelimitTypes.Hinge: - { - var limit = new UniGLTF.Extensions.VRMC_springBone_limit.VRMC_springBone_limit + }; + glTFExtension extensions = default; + UniGLTF.Extensions.VRMC_springBone_limit.GltfSerializer.SerializeTo(ref extensions, limit); + joint.Extensions = extensions; + break; + } + case UniGLTF.SpringBoneJobs.AnglelimitTypes.Hinge: { - Limit = new UniGLTF.Extensions.VRMC_springBone_limit.Limit + var limit = new UniGLTF.Extensions.VRMC_springBone_limit.VRMC_springBone_limit { - Hinge = new UniGLTF.Extensions.VRMC_springBone_limit.HingeLimit + SpecVersion = VRMC_springBone_limit_SPEC_VERSION, + Limit = new UniGLTF.Extensions.VRMC_springBone_limit.Limit { - Rotation = ReverseXToFloat4(y.m_limitSpaceOffset), - Angle = y.m_pitch, + Hinge = new UniGLTF.Extensions.VRMC_springBone_limit.HingeLimit + { + Rotation = ReverseXToFloat4(y.m_limitSpaceOffset), + Angle = y.m_pitch, + } } - } - }; - glTFExtension extensions = default; - UniGLTF.Extensions.VRMC_springBone_limit.GltfSerializer.SerializeTo(ref extensions, limit); - joint.Extensions = extensions; - break; - } - case UniGLTF.SpringBoneJobs.AnglelimitTypes.Spherical: - { - var limit = new UniGLTF.Extensions.VRMC_springBone_limit.VRMC_springBone_limit + }; + glTFExtension extensions = default; + UniGLTF.Extensions.VRMC_springBone_limit.GltfSerializer.SerializeTo(ref extensions, limit); + joint.Extensions = extensions; + break; + } + case UniGLTF.SpringBoneJobs.AnglelimitTypes.Spherical: { - Limit = new UniGLTF.Extensions.VRMC_springBone_limit.Limit + var limit = new UniGLTF.Extensions.VRMC_springBone_limit.VRMC_springBone_limit { - Spherical = new UniGLTF.Extensions.VRMC_springBone_limit.SphericalLimit + SpecVersion = VRMC_springBone_limit_SPEC_VERSION, + Limit = new UniGLTF.Extensions.VRMC_springBone_limit.Limit { - Rotation = ReverseXToFloat4(y.m_limitSpaceOffset), - Pitch = y.m_pitch, - Yaw = y.m_yaw, + Spherical = new UniGLTF.Extensions.VRMC_springBone_limit.SphericalLimit + { + Rotation = ReverseXToFloat4(y.m_limitSpaceOffset), + Pitch = y.m_pitch, + Yaw = y.m_yaw, + } } - } - }; - glTFExtension extensions = default; - UniGLTF.Extensions.VRMC_springBone_limit.GltfSerializer.SerializeTo(ref extensions, limit); - joint.Extensions = extensions; - break; - } + }; + glTFExtension extensions = default; + UniGLTF.Extensions.VRMC_springBone_limit.GltfSerializer.SerializeTo(ref extensions, limit); + joint.Extensions = extensions; + break; + } + } } return joint; } diff --git a/Packages/VRM10/Runtime/IO/Vrm10Importer.cs b/Packages/VRM10/Runtime/IO/Vrm10Importer.cs index 557b51b7d..3293a901b 100644 --- a/Packages/VRM10/Runtime/IO/Vrm10Importer.cs +++ b/Packages/VRM10/Runtime/IO/Vrm10Importer.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using UniGLTF; -using UniGLTF.Extensions.VRMC_springBone_limit; using UniGLTF.Utils; +using Unity.Mathematics; using UnityEngine; namespace UniVRM10 @@ -651,24 +650,27 @@ namespace UniVRM10 if (UniGLTF.Extensions.VRMC_springBone_limit.GltfDeserializer.TryGet(gltfJoint.Extensions as glTFExtension, out var extensionSpringBoneLimit)) { - if (extensionSpringBoneLimit.Limit.Cone is UniGLTF.Extensions.VRMC_springBone_limit.ConeLimit cone) + if (VRMC_springBone_limit_Validate(go.transform, extensionSpringBoneLimit)) { - joint.m_anglelimitType = UniGLTF.SpringBoneJobs.AnglelimitTypes.Cone; - joint.m_limitSpaceOffset = QuaternionFromFloat4(cone.Rotation); - joint.m_pitch = cone.Angle.GetValueOrDefault(); - } - else if (extensionSpringBoneLimit.Limit.Hinge is UniGLTF.Extensions.VRMC_springBone_limit.HingeLimit hinge) - { - joint.m_anglelimitType = UniGLTF.SpringBoneJobs.AnglelimitTypes.Hinge; - joint.m_limitSpaceOffset = QuaternionFromFloat4(hinge.Rotation); - joint.m_pitch = hinge.Angle.GetValueOrDefault(); - } - else if (extensionSpringBoneLimit.Limit.Spherical is UniGLTF.Extensions.VRMC_springBone_limit.SphericalLimit spherical) - { - joint.m_anglelimitType = UniGLTF.SpringBoneJobs.AnglelimitTypes.Spherical; - joint.m_limitSpaceOffset = QuaternionFromFloat4(spherical.Rotation); - joint.m_pitch = spherical.Pitch.GetValueOrDefault(); - joint.m_yaw = spherical.Yaw.GetValueOrDefault(); + if (extensionSpringBoneLimit.Limit.Cone is UniGLTF.Extensions.VRMC_springBone_limit.ConeLimit cone) + { + joint.m_anglelimitType = UniGLTF.SpringBoneJobs.AnglelimitTypes.Cone; + joint.m_limitSpaceOffset = QuaternionFromFloat4(cone.Rotation); + joint.m_pitch = cone.Angle.GetValueOrDefault(); + } + else if (extensionSpringBoneLimit.Limit.Hinge is UniGLTF.Extensions.VRMC_springBone_limit.HingeLimit hinge) + { + joint.m_anglelimitType = UniGLTF.SpringBoneJobs.AnglelimitTypes.Hinge; + joint.m_limitSpaceOffset = QuaternionFromFloat4(hinge.Rotation); + joint.m_pitch = hinge.Angle.GetValueOrDefault(); + } + else if (extensionSpringBoneLimit.Limit.Spherical is UniGLTF.Extensions.VRMC_springBone_limit.SphericalLimit spherical) + { + joint.m_anglelimitType = UniGLTF.SpringBoneJobs.AnglelimitTypes.Spherical; + joint.m_limitSpaceOffset = QuaternionFromFloat4(spherical.Rotation); + joint.m_pitch = spherical.Pitch.GetValueOrDefault(); + joint.m_yaw = spherical.Yaw.GetValueOrDefault(); + } } } @@ -679,6 +681,60 @@ namespace UniVRM10 } } + private static bool VRMC_springBone_limit_Validate(Transform node, VRMC_springBone_limit extensionSpringBoneLimit) + { + var validate = true; + if (extensionSpringBoneLimit.SpecVersion != Vrm10Exporter.VRMC_springBone_limit_SPEC_VERSION) + { + validate = false; + UniGLTFLogger.Warning($"Unknown VRMC_springBone_limit specVersion: {extensionSpringBoneLimit.SpecVersion}"); + } + + var count = 0; + if (extensionSpringBoneLimit.Limit != null) + { + if (extensionSpringBoneLimit.Limit.Cone is UniGLTF.Extensions.VRMC_springBone_limit.ConeLimit cone) + { + ++count; + } + if (extensionSpringBoneLimit.Limit.Hinge is UniGLTF.Extensions.VRMC_springBone_limit.HingeLimit hinge) + { + ++count; + } + if (extensionSpringBoneLimit.Limit.Spherical is UniGLTF.Extensions.VRMC_springBone_limit.SphericalLimit spherical) + { + ++count; + } + } + + switch (count) + { + case 0: + // Limit 欠落 + validate = false; + UniGLTFLogger.Warning($"No VRMC_springBone_limit.Limit: skip"); + break; + + case 1: + // ok + break; + + default: + validate = false; + UniGLTFLogger.Warning($"Multiple VRMC_springBone_limit.Limit: skip"); + break; + } + + if (node.childCount == 0) + { + // leaf + UniGLTFLogger.Warning($"VRMC_springBone_limit: leaf node '{node.name}' must not have limit. skip"); + validate = false; + } + + return validate; + } + private static Quaternion QuaternionFromFloat4(float[] xyzw) { var q = (xyzw != null && xyzw.Length == 4)