Unity: Screen Space Gizmos

Unity’s default gizmos are essential for visual debugging but can only be drawn in world space. This restriction can be rather annoying if you try to debug custom UI or viewport-based camera movement. A simple line at a specific pixel coordinate would solve the problem most of the time. There are two solutions to this problem. 

 

Solution A

Add a canvas with a half-transparent image at the screen position. This is easy to do but comes with the following disadvantages:

 

  • Must be created/deleted each time you want to debug
  • Sets the canvas and scene dirty -> causes version control noise
  • Not suitable if the screen position is not static

 

Solution B

Draw gizmos at the Camera.NearClipPlane. You can’t draw the gizmos exactly on the near clip plane because they would be invisible. Offset the gizmos a bit (0.001f) in the camera view direction. The gizmos will move with the camera and behave like screen space gizmos. Be aware that the canvas scale must be taken into account when dealing with pixel coordinates. If something is slightly off chances are high that the canvas.scaleFactor is missing in a calculation.

 

Below is an example implementation that works with all camera projections and canvas modes except world-space. In world-space just use the default gizmos.

 

Code at my GitHub: https://github.com/AuriMoogle/Unity-ScreenSpaceGizmos

 

// Author: Anja Haumann 2022 - MIT License
// Explanation and more content at my blog: https://anja-haumann.de

using UnityEngine;

public static class ScreenGizmos
{
  private const float offset = 0.001f;

  /// <summary>
  /// Draws a line in screen space between the 
  /// <paramref name="startPixelPos"/> and the 
  /// <paramref name="endPixelPos"/>. 
  /// </summary>
  public static void DrawLine(
    Canvas canvas, 
    Camera camera, 
    Vector3 startPixelPos, 
    Vector3 endPixelPos)
  {
    if (camera == null || canvas == null)
      return;

    Vector3 startWorld = PixelToCameraClipPlane(
      camera, 
      canvas, 
      startPixelPos);

    Vector3 endWorld = PixelToCameraClipPlane(
      camera, 
      canvas, 
      endPixelPos);

    Gizmos.DrawLine(startWorld, endWorld);
  }

  /// <summary>
  /// Converts the <paramref name="screenPos"/> to world space 
  /// near the <paramref name="camera"/> near clip plane. The 
  /// z component of the <paramref name="screenPos"/> 
  /// will be overriden.
  /// </summary>
  private static Vector3 PixelToCameraClipPlane(
    Camera camera, 
    Canvas canvas,
    Vector3 screenPos)
  {
    // The canvas scale factor affects the
    // screen position of all UI elements.
    screenPos *= canvas.scaleFactor;

    // The z-position defines the distance to the camera
    // when using Camera.ScreenToWorldPoint.
    screenPos.z = camera.nearClipPlane + offset;
    return camera.ScreenToWorldPoint(screenPos);
  }
}