Skip to content

Instantly share code, notes, and snippets.

@kurtdekker
Created September 29, 2025 18:15
Show Gist options
  • Save kurtdekker/444fa291e3a7c2cdf060905b55c40c6a to your computer and use it in GitHub Desktop.
Save kurtdekker/444fa291e3a7c2cdf060905b55c40c6a to your computer and use it in GitHub Desktop.
Camera frustum plane-projecting gizmo
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// @kurtdekker
//
// Projects a Camera frustum onto an arbitrary Transform plane
//
// You can (probably) drop this script on any GameObject.
//
// It will try and find the Main Camera if present, or you can specify a Camera.
//
// You must specify a ProjectionPlane transform as well.
//
[ExecuteAlways]
public class CameraFrustumPlaneGizmo : MonoBehaviour
{
public enum ProjAxis
{
X,
Y,
Z,
}
[Header("Projects on the Z == 0 of this transform.", order = 1)]
public Transform ProjectionPlane;
[Header("Will try to find Main Camera if empty")]
public Camera CameraToUse;
[Header("Axis orientation.")]
public ProjAxis ProjectionAxis = ProjAxis.Z;
private void Start()
{
if (!CameraToUse)
{
CameraToUse = Camera.main;
}
// TODO: you might want to destroy this when not in editor...
//if (!Application.isEditor)
//{
// Destroy(this);
//}
}
bool valid;
Vector3[] ProjectedPoints;
Vector2[] ViewportCorners = new Vector2[]
{
new Vector2( 0, 0),
new Vector2( 0, 1),
new Vector2( 1, 1),
new Vector2( 1, 0),
};
private void Update()
{
// presume invalidity
valid = false;
// print out some warnings if you like...
if (!CameraToUse)
{
return;
}
if (!ProjectionPlane)
{
return;
}
// malloc
if (ProjectedPoints == null || ProjectedPoints.Length != ViewportCorners.Length)
{
ProjectedPoints = new Vector3[ ViewportCorners.Length];
}
// all four corners must hit
for (int cornerIndex = 0; cornerIndex < ProjectedPoints.Length; cornerIndex++)
{
Vector2 viewportPosition = ViewportCorners[cornerIndex];
Ray ray = CameraToUse.ViewportPointToRay(viewportPosition);
// by default: this projects on back of the Z == 0
Plane plane = new Plane(inNormal: ProjectionPlane.forward, inPoint: ProjectionPlane.position);
// choose orientation
switch( ProjectionAxis)
{
case ProjAxis.X:
plane = new Plane(inNormal: ProjectionPlane.right, inPoint: ProjectionPlane.position);
break;
case ProjAxis.Y:
plane = new Plane(inNormal: ProjectionPlane.up, inPoint: ProjectionPlane.position);
break;
case ProjAxis.Z: // defaulted above
break;
}
// cast
float enter;
if (plane.Raycast( ray, out enter))
{
ProjectedPoints[cornerIndex] = ray.GetPoint(enter);
}
else
{
// can't hit the plane from this camera viewport; give up fully
return;
}
}
// only executed if we hit all four corners
valid = true;
}
private void OnDrawGizmos()
{
if (valid)
{
for (int cornerIndex = 0; cornerIndex < ProjectedPoints.Length; cornerIndex++)
{
// main bounding rect / trapezoid
int nextCorner = (cornerIndex + 1) % ProjectedPoints.Length;
var point1 = ProjectedPoints[cornerIndex];
var point2 = ProjectedPoints[nextCorner];
Gizmos.DrawLine(point1, point2);
// crossing bars (X down the middle)
if (cornerIndex < 2)
{
nextCorner = (cornerIndex + 2) % ProjectedPoints.Length;
point1 = ProjectedPoints[cornerIndex];
point2 = ProjectedPoints[nextCorner];
Gizmos.DrawLine(point1, point2);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment