namespace ColliderVisualizer { using System; using System.Collections.Generic; using UnityEngine; /// /// Collider visualizer. /// [ExecuteAlways] [RequireComponent(typeof(Collider))] public class ColliderVisualizer : MonoBehaviour { private static readonly int ColorPropertyID = Shader.PropertyToID("_Color"); private static readonly int[,] BoxEdges = { { 0, 1 }, { 1, 3 }, { 3, 2 }, { 2, 0 }, { 4, 5 }, { 5, 7 }, { 7, 6 }, { 6, 4 }, { 0, 4 }, { 1, 5 }, { 2, 6 }, { 3, 7 } }; /// /// The need to draw the collider fill has changed. /// public event Action onNeedDrawSolidChanged = delegate { }; /// /// The need to draw collider boundaries has changed. /// public event Action onNeedDrawWireChanged = delegate { }; /// /// The fill color has changed. /// public event Action onSolidColorChanged = delegate { }; /// /// The border color has changed. /// public event Action onWireColorChanged = delegate { }; /// /// Need to draw a fill. /// public bool NeedDrawSolid { get => drawSolid; set { if (drawSolid != value) { drawSolid = value; onNeedDrawSolidChanged(); } } } /// /// You need to draw boundaries. /// public bool NeedDrawWire { get => drawWire; set { if (drawWire != value) { drawWire = value; onNeedDrawWireChanged(); } } } /// /// Fill color. /// public Color SolidColor { get => solidColor; set { if (solidColor != value) { solidColor = value; onSolidColorChanged(); } } } /// /// Border color. /// public Color WireColor { get => wireColor; set { if (wireColor != value) { wireColor = value; onWireColorChanged(); } } } [Header("Rendering")] [SerializeField] private bool drawSolid = true; [SerializeField] private bool drawWire = true; [SerializeField] private Color solidColor = new Color(0f, 1f, 0f, 0.15f); [SerializeField] private Color wireColor = Color.green; [Header("Wire Quality")] [SerializeField, Range(3, 64)] private int wireSegments = 24; [SerializeField, HideInInspector, Range(0, 100000)] private int maxMeshTriangles = 0; // 0 = without limit [SerializeField, Range(2, 32)] private int capsuleArcSegments = 8; [SerializeField, HideInInspector] private Mesh meshBox; [SerializeField, HideInInspector] private Mesh meshSphere; [SerializeField, HideInInspector] private Mesh meshCylinder; [SerializeField, HideInInspector] private Mesh upperHalfSphere; [SerializeField, HideInInspector] private Mesh lowerHalfSphere; [SerializeField, HideInInspector] private Shader coloredShader; private Material solidMat; private Material wireMat; private Collider col; private Transform _transform; private void Start() => Initialize(); /// /// Initializes the component. /// public void Initialize() { col = GetComponent(); if (col == null) return; _transform = transform; RemoveMaterials(); solidMat = new Material(coloredShader); wireMat = new Material(coloredShader); } private void OnRenderObject() { if (!enabled || col == null) return; if (drawSolid) { solidMat.SetColor(ColorPropertyID, solidColor); solidMat.SetPass(0); DrawSolid(col); } if (drawWire) { wireMat.SetColor(ColorPropertyID, wireColor); wireMat.SetPass(0); if (col is SphereCollider or CapsuleCollider) { GL.Begin(GL.LINES); GL.Color(wireColor); DrawWire(col); GL.End(); } else { GL.PushMatrix(); GL.MultMatrix(transform.localToWorldMatrix); GL.Begin(GL.LINES); GL.Color(wireColor); DrawWire(col); GL.End(); GL.PopMatrix(); } } } private void OnDestroy() { if (Application.isPlaying) { RemoveMaterials(); } } private void RemoveMaterials() { #if UNITY_EDITOR if (solidMat != null) { DestroyImmediate(solidMat); } if (wireMat != null) { DestroyImmediate(wireMat); } #else if (solidMat != null) { Destroy(solidMat); } if (wireMat != null) { Destroy(wireMat); } #endif } private void DrawSolid(Collider collider) { switch (collider) { case BoxCollider box: Graphics.DrawMeshNow(meshBox, Matrix4x4.TRS(_transform.position + _transform.rotation * Vector3.Scale(box.center, _transform.lossyScale), _transform.rotation, Vector3.Scale(box.size, _transform.lossyScale))); break; case SphereCollider sphere: float scaledRadius = sphere.radius * MaxAbsAxis(_transform.lossyScale); Graphics.DrawMeshNow(meshSphere, Matrix4x4.TRS(_transform.position + _transform.rotation * Vector3.Scale(sphere.center, _transform.lossyScale), _transform.rotation, Vector3.one * scaledRadius * 2f)); break; case CapsuleCollider capsule: DrawCapsuleSolid(capsule); break; case MeshCollider meshCol: if (meshCol.sharedMesh != null) Graphics.DrawMeshNow(meshCol.sharedMesh, _transform.localToWorldMatrix); break; } } private void DrawWire(Collider collider) { switch (collider) { case BoxCollider box: DrawWireCube(box.center, box.size); break; case SphereCollider sphere: DrawWireSphere(sphere); break; case CapsuleCollider capsule: DrawWireCapsule(capsule); break; case MeshCollider meshCol: DrawWireMesh(meshCol); break; } } private float MaxAbsAxis(Vector3 v) => Mathf.Max(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); #region Box private void DrawWireCube(Vector3 center, Vector3 size) { Vector3 half = size * 0.5f; Vector3[] pts = new Vector3[8]; for (int i = 0; i < 8; i++) { pts[i] = new Vector3( ((i & 1) == 0 ? -1 : 1) * half.x, ((i & 2) == 0 ? -1 : 1) * half.y, ((i & 4) == 0 ? -1 : 1) * half.z ) + center; } for (int i = 0; i < BoxEdges.GetLength(0); i++) { GL.Vertex(pts[BoxEdges[i, 0]]); GL.Vertex(pts[BoxEdges[i, 1]]); } } #endregion #region Sphere private void DrawWireSphere(SphereCollider sphere) { float scaledRadius = sphere.radius * MaxAbsAxis(transform.lossyScale); float angleStep = 360f / wireSegments; for (int i = 0; i < wireSegments; i++) { float a0 = Mathf.Deg2Rad * i * angleStep; float a1 = Mathf.Deg2Rad * (i + 1) * angleStep; Vector3 c = _transform.TransformPoint(sphere.center); // XY Vector3 p0 = c + _transform.rotation * new Vector3(Mathf.Cos(a0), Mathf.Sin(a0), 0) * scaledRadius; Vector3 p1 = c + _transform.rotation * new Vector3(Mathf.Cos(a1), Mathf.Sin(a1), 0) * scaledRadius; GL.Vertex(p0); GL.Vertex(p1); // XZ p0 = c + _transform.rotation * new Vector3(Mathf.Cos(a0), 0, Mathf.Sin(a0)) * scaledRadius; p1 = c + _transform.rotation * new Vector3(Mathf.Cos(a1), 0, Mathf.Sin(a1)) * scaledRadius; GL.Vertex(p0); GL.Vertex(p1); // YZ p0 = c + _transform.rotation * new Vector3(0, Mathf.Cos(a0), Mathf.Sin(a0)) * scaledRadius; p1 = c + _transform.rotation * new Vector3(0, Mathf.Cos(a1), Mathf.Sin(a1)) * scaledRadius; GL.Vertex(p0); GL.Vertex(p1); } } #endregion #region Capsule private void DrawCapsuleSolid(CapsuleCollider capsule) { float radius = capsule.radius; float height = capsule.height; Vector3 center = capsule.center; float cylinderHeight = Mathf.Max(0f, height - 2f * radius); Quaternion rotation = transform.rotation; Vector3 up = Vector3.up; Vector3 lossyScale = transform.lossyScale; float heightScale = 1f; float radiusScale = 1f; switch (capsule.direction) { case 0: // X-axis up = Vector3.right; rotation *= Quaternion.Euler(0, 0, -90); heightScale = lossyScale.x; radiusScale = Mathf.Max(lossyScale.y, lossyScale.z); break; case 1: // Y-axis up = Vector3.up; heightScale = lossyScale.y; radiusScale = Mathf.Max(lossyScale.x, lossyScale.z); break; case 2: // Z-axis up = Vector3.forward; rotation *= Quaternion.Euler(90, 0, 0); heightScale = lossyScale.z; radiusScale = Mathf.Max(lossyScale.x, lossyScale.y); break; } Vector3 worldCenter = transform.TransformPoint(center); Vector3 worldUp = transform.TransformDirection(up); float scaledRadius = radius * radiusScale; float scaledHeight = (height * heightScale) - 2f * scaledRadius; scaledHeight = Mathf.Max(0f, scaledHeight); Vector3 top = worldCenter + worldUp * (scaledHeight * 0.5f); Vector3 bottom = worldCenter - worldUp * (scaledHeight * 0.5f); Vector3 cylinderScale = new Vector3(scaledRadius, scaledHeight, scaledRadius); Vector3 sphereScale = Vector3.one * scaledRadius; Graphics.DrawMeshNow(meshCylinder, Matrix4x4.TRS(worldCenter, rotation, cylinderScale)); Graphics.DrawMeshNow(upperHalfSphere, Matrix4x4.TRS(top, rotation, sphereScale)); Graphics.DrawMeshNow(lowerHalfSphere, Matrix4x4.TRS(bottom, rotation, sphereScale)); } private void DrawWireCapsule(CapsuleCollider capsule) { Vector3 center = capsule.center; float radius = capsule.radius; float height = capsule.height; Vector3 up = Vector3.up; Vector3 right = Vector3.right; Vector3 forward = Vector3.forward; Quaternion rotation = transform.rotation; Vector3 scale = transform.lossyScale; Vector3 position = transform.position; switch (capsule.direction) { case 0: up = Vector3.right; right = Vector3.up; forward = Vector3.forward; break; case 1: up = Vector3.up; right = Vector3.right; forward = Vector3.forward; break; case 2: up = Vector3.forward; right = Vector3.right; forward = Vector3.up; break; } float heightScale = Mathf.Abs(Vector3.Dot(scale, up)); float radiusScale = Mathf.Max( Mathf.Abs(Vector3.Dot(scale, right)), Mathf.Abs(Vector3.Dot(scale, forward)) ); float scaledRadius = radius * radiusScale; float scaledHeight = height * heightScale; float cylinderHeight = Mathf.Max(0f, scaledHeight - 2f * scaledRadius); Vector3 worldCenter = position + rotation * Vector3.Scale(center, scale); Vector3 worldUp = rotation * up; Vector3 worldRight = rotation * right; Vector3 worldForward = rotation * forward; Vector3 top = worldCenter + worldUp * (cylinderHeight * 0.5f); Vector3 bottom = worldCenter - worldUp * (cylinderHeight * 0.5f); DrawWireCapsuleBody(top, bottom, worldUp, worldRight, worldForward, scaledRadius); DrawWireCapsuleCap(top, worldUp, worldRight, worldForward, scaledRadius, true); DrawWireCapsuleCap(bottom, worldUp, worldRight, worldForward, scaledRadius, false); } private void DrawWireCapsuleBody(Vector3 top, Vector3 bottom, Vector3 up, Vector3 right, Vector3 forward, float radius) { float angleStep = 360f / wireSegments; for (int i = 0; i < wireSegments; i++) { float angle0 = Mathf.Deg2Rad * i * angleStep; float angle1 = Mathf.Deg2Rad * (i + 1) * angleStep; Vector3 dir0 = (right * Mathf.Cos(angle0) + forward * Mathf.Sin(angle0)) * radius; Vector3 dir1 = (right * Mathf.Cos(angle1) + forward * Mathf.Sin(angle1)) * radius; GL.Vertex(top + dir0); GL.Vertex(bottom + dir0); GL.Vertex(top + dir0); GL.Vertex(top + dir1); GL.Vertex(bottom + dir0); GL.Vertex(bottom + dir1); } } private void DrawWireCapsuleCap(Vector3 center, Vector3 up, Vector3 right, Vector3 forward, float radius, bool isTop) { int arcSegments = capsuleArcSegments; float arcStep = 90f / arcSegments; for (int i = 0; i < 360; i += 360 / wireSegments) { float rad = Mathf.Deg2Rad * i; Vector3 baseDir = (right * Mathf.Cos(rad) + forward * Mathf.Sin(rad)).normalized; Vector3 from = baseDir * radius; Vector3 axis = Vector3.Cross(baseDir, up).normalized; if (!isTop) axis = -axis; Quaternion rot = Quaternion.AngleAxis(arcStep, axis); for (int j = 0; j < arcSegments; j++) { Vector3 to = rot * from; GL.Vertex(center + from); GL.Vertex(center + to); from = to; } } } private Mesh GenerateHalfSphereMesh(bool upper = true, int longitude = 24, int latitude = 12) { Mesh mesh = new Mesh(); mesh.name = upper ? "UpperHalfSphere" : "LowerHalfSphere"; List vertices = new(); List triangles = new(); int latStart = upper ? 0 : latitude / 2; int latEnd = upper ? latitude / 2 : latitude; for (int lat = latStart; lat <= latEnd; lat++) { float a1 = Mathf.PI * lat / latitude; float sin1 = Mathf.Sin(a1); float cos1 = Mathf.Cos(a1); for (int lon = 0; lon <= longitude; lon++) { float a2 = 2f * Mathf.PI * lon / longitude; float sin2 = Mathf.Sin(a2); float cos2 = Mathf.Cos(a2); Vector3 vertex = new Vector3(sin1 * cos2, cos1, sin1 * sin2); vertices.Add(vertex); } } int vertsPerRow = longitude + 1; for (int lat = 0; lat < (latEnd - latStart); lat++) { for (int lon = 0; lon < longitude; lon++) { int current = lat * vertsPerRow + lon; int next = current + vertsPerRow; triangles.Add(current); triangles.Add(next); triangles.Add(current + 1); triangles.Add(current + 1); triangles.Add(next); triangles.Add(next + 1); } } mesh.SetVertices(vertices); mesh.SetTriangles(triangles, 0); mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } private Mesh GenerateOpenCylinder(int segments = 24) { Mesh mesh = new Mesh(); mesh.name = "OpenCylinder"; List vertices = new(); List triangles = new(); for (int i = 0; i <= segments; i++) { float angle = 2f * Mathf.PI * i / segments; float x = Mathf.Cos(angle); float z = Mathf.Sin(angle); vertices.Add(new Vector3(x, -0.5f, z)); vertices.Add(new Vector3(x, 0.5f, z)); } for (int i = 0; i < segments; i++) { int baseIndex = i * 2; triangles.Add(baseIndex); triangles.Add(baseIndex + 3); triangles.Add(baseIndex + 1); triangles.Add(baseIndex); triangles.Add(baseIndex + 2); triangles.Add(baseIndex + 3); } mesh.SetVertices(vertices); mesh.SetTriangles(triangles, 0); mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } #endregion #region Mesh private void DrawWireMesh(MeshCollider meshCollider) { var mesh = meshCollider.sharedMesh; if (mesh == null) return; var vertices = mesh.vertices; var triangles = mesh.triangles; int triangleCount = triangles.Length / 3; if (maxMeshTriangles > 0 && triangleCount > maxMeshTriangles) triangleCount = maxMeshTriangles; for (int i = 0; i < triangleCount * 3; i += 3) { int i0 = triangles[i]; int i1 = triangles[i + 1]; int i2 = triangles[i + 2]; GL.Vertex(vertices[i0]); GL.Vertex(vertices[i1]); GL.Vertex(vertices[i1]); GL.Vertex(vertices[i2]); GL.Vertex(vertices[i2]); GL.Vertex(vertices[i0]); } } #endregion } }