0
\$\begingroup\$

I have a very simple code that draws the shape of a pre-defined capsule. It handles everything well, except the case when the GameObject to which the CapsuleCollider2D is attached or any parent GameObject up the chain have scale set different than 1. The pre-computed matrix with lossyScale supposedly should take that into account but it does not:

Capsule diagram

var cap = GetComponent<CapsuleCollider2D>(); // -- basics var radius = cap.size.x * 0.5f; var halfH = cap.size.y * 0.5f; var center = cap.bounds.center; // -- hemispehre offsets var upperDiff = Mathf.Clamp(halfH - radius, 0, 999); var lowerDiff = upperDiff * -1; // -- matrix that supposedly hould take into account lossy scale var mtx = Matrix4x4.TRS(cap.bounds.center, cap.transform.rotation, cap.transform.lossyScale); // -- upper and lower hemisphere "centers" var upper = mtx.MultiplyPoint3x4(new Vector3(0, upperDiff, 0)); var lower = mtx.MultiplyPoint3x4(new Vector3(0, lowerDiff, 0)); // -- drawing setup int pts = 80; float step = 360f / pts; // -- central shape of the capsule essentially a box, so we find the "box" vertices var topLeft = mtx.MultiplyPoint3x4(new Vector3(-radius, upperDiff, 0)); var topRight = mtx.MultiplyPoint3x4(new Vector3(radius, upperDiff, 0)); var lowLeft = mtx.MultiplyPoint3x4(new Vector3(-radius, lowerDiff, 0)); var lowRight = mtx.MultiplyPoint3x4(new Vector3(radius, lowerDiff, 0)); // drawing for (int i = 0; i < pts / 2; i++) { // first point float cx = Mathf.Cos(Mathf.Deg2Rad * step * i) * radius; float cy = Mathf.Sin(Mathf.Deg2Rad * step * i) * radius; Vector3 cur = new Vector3(cx, cy); // next point float nx = Mathf.Cos(Mathf.Deg2Rad * step * (i + 1)) * radius; float ny = Mathf.Sin(Mathf.Deg2Rad * step * (i + 1)) * radius; Vector3 next = new Vector3(nx, ny); // upper hemisphere var curUP = mtx.MultiplyPoint3x4(cur + new Vector3(0, upperDiff, 0)); var nextUP = mtx.MultiplyPoint3x4(next + new Vector3(0, upperDiff, 0)); // lower hemisphere var curDOWN = mtx.MultiplyPoint3x4(new Vector3(0, lowerDiff, 0) - cur); var nextDOWN = mtx.MultiplyPoint3x4(new Vector3(0, lowerDiff, 0) - next); // triangle in upper hemisphere GL.Vertex3(curUP.x, curUP.y, 0); GL.Vertex3(nextUP.x, nextUP.y, 0); GL.Vertex3(center.x, center.y, 0); // triangle in lower GL.Vertex3(curDOWN.x, curDOWN.y, 0); GL.Vertex3(nextDOWN.x, nextDOWN.y, 0); GL.Vertex3(center.x, center.y, 0); } GL.Vertex3(topLeft.x, topLeft.y, 0); GL.Vertex3(lowLeft.x, lowLeft.y, 0); GL.Vertex3(center.x, center.y, 0); // -- GL.Vertex3(topRight.x, topRight.y, 0); GL.Vertex3(lowRight.x, lowRight.y, 0); GL.Vertex3(center.x, center.y, 0); 

Any ideas how can I account for scaling?

\$\endgroup\$
2
  • \$\begingroup\$ "lossyScale" is, as the name says, lossy: it does not and cannot account for all the effects of nested scaling. Have you considered using transform.localToWorldMatrix instead of building your own from parts? \$\endgroup\$ Commented Jul 18, 2024 at 1:23
  • \$\begingroup\$ I'm pretty sure the issue isnt with lossyScale, because even with perfect round values it still exhibits this issue. I'm not sure in which way I can use transform.localToWorldMatrix without complicating the code tenfolds (unless you mean https://docs.unity3d.com/ScriptReference/Transform-localToWorldMatrix.html then yes, TransformPoint exhibits the same issue). \$\endgroup\$ Commented Jul 18, 2024 at 1:40

1 Answer 1

0
\$\begingroup\$

If I understand you correctly, you want to match a set of vertices to the perimeter of a capsule collider, taking into account that the physics system doesn't handle non-uniform / non-axis-aligned scale and so it makes some compromises to get the collider to follow the object's transformation approximately.

First, we can define a data type that represents a capsule shape in world space. I've included a helper method for enumerating points on that capsule's perimeter. This keeps the code we write later nice and concise.

public readonly struct CapsuleShape { /// <summary> /// Center of the capsule. /// </summary> public readonly Vector2 center; /// <summary> /// Unit vector pointing along length of capsule. /// </summary> public readonly Vector2 direction; /// <summary> /// Radius of the capsule's cap semi-circles. /// </summary> public readonly float radius; /// <summary> /// Distance from center of capsule to its tip, including the cap. /// </summary> public readonly float halfLength; /// <summary> /// Constructs a capsule shape using its center, end offset, and radius. /// </summary> /// <param name="center">Center of the capsule.</param> /// <param name="endOffset">Vector from the center of the capsule to its tip.</param> /// <param name="radius">Radius of the capsule's semi-circles.</param> public CapsuleShape(Vector2 center, Vector2 endOffset, float radius) { this.center = center; this.radius = radius; halfLength = endOffset.magnitude; direction = endOffset/halfLength; } public IEnumerator<Vector2> GetPoints(int capVertices) { int capSegments = Mathf.Max(0, capVertices) + 1; var capCenterOffset = direction * Mathf.Max(0, halfLength - radius); var forward = radius * direction; var right = new Vector2(forward.y, -forward.x); float angleStep = Mathf.PI / capSegments; // Draw the "front" semi-circle. var capCenter = center + capCenterOffset; for(int i = 0; i <= capSegments; i++) { float angle = i * angleStep; var point = capCenter + forward * Mathf.Sin(angle) + right * Mathf.Cos(angle); yield return point; } // Iterate the "back" semi-circle. capCenter = center - capCenterOffset; for(int i = 0; i <= capSegments; i++) { float angle = i * angleStep; var point = capCenter - forward * Mathf.Sin(angle) - right * Mathf.Cos(angle); yield return point; } } } 

Then we can write a method that extracts this shape from a capsule's properties and transform:

public static CapsuleShape GetColliderShape(CapsuleCollider2D capsule) { var transform = capsule.transform; var halfExtents = capsule.size * 0.5f; Vector2 toTip = Vector2.zero, toSide = Vector2.zero; if (capsule.direction == CapsuleDirection2D.Horizontal) { toTip.x = halfExtents.x; toSide.y = halfExtents.y; } else { toTip.y = halfExtents.y; toSide.x = halfExtents.x; } return new CapsuleShape( transform.TransformPoint(capsule.offset), // Center transform.TransformVector(toTip), // End Offset transform.TransformVector(toSide).magnitude // Radius ); } 

Lastly, we can draw this capsule as a gizmo like so. I used this to verify that the computed shape correctly matches Unity's collider gizmo even in the presence of non-uniform / non-axis-aligned scale in the capsule's transformation hierarchy.

var shape = GetColliderShape(capsule); // Get an enumerator that walks over the points of the capsule, // at a resolution of our choosing. var pointEnumerator = shape.GetPoints(16); // Advance to the first point on the capsule's perimeter. pointEnumerator.MoveNext(); var first = pointEnumerator.Current; var previous = first; // Iterate over the rest of the points, joining them with lines. while(pointEnumerator.MoveNext()) { Gizmos.DrawLine(previous, pointEnumerator.Current); previous = pointEnumerator.Current; } // Close the shape by joining the last point we drew to the first. Gizmos.DrawLine(previous, first); 

I initially solved this for the 3D case, because the title said "hemispheres" instead of "semicircles". For completeness, here's how we can match the physics system's scaling behaviour for 3D capsules too:

/// Draws wire spheres matching the caps of a 3D capsule collider. void OnDrawGizmosSelected() { if (!TryGetComponent(out CapsuleCollider capsule)) return; var center = transform.TransformPoint(capsule.center); var localAxis = Vector3.zero; localAxis[capsule.direction] = 1; var worldAxis = transform.rotation * localAxis; var lossyScale = transform.lossyScale; var absScale = new Vector3(Mathf.Abs(lossyScale.x), Mathf.Abs(lossyScale.y), Mathf.Abs(lossyScale.z)); var radiusScale = Mathf.Max(absScale[(capsule.direction + 1) % 3], absScale[(capsule.direction + 2) % 3]); var toCapCenter = Mathf.Max(0f, absScale[capsule.direction] * capsule.height/2 - capsule.radius * radiusScale); Gizmos.DrawWireSphere(center + worldAxis * toCapCenter, capsule.radius * radiusScale); Gizmos.DrawWireSphere(center - worldAxis * toCapCenter, capsule.radius * radiusScale); } 
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.