Skip to content

Instantly share code, notes, and snippets.

@SabinT
Created October 22, 2021 05:55
Show Gist options
  • Save SabinT/06cf2c744007fd120c0da719d53b1450 to your computer and use it in GitHub Desktop.
Save SabinT/06cf2c744007fd120c0da719d53b1450 to your computer and use it in GitHub Desktop.
Tiled camera rendering helper in Unity
using UnityEngine;
using EasyButtons;
using UnityEngine.Rendering;
namespace Lumic.Utils
{
/// <summary>
/// TODO support orthographic cameras
/// </summary>
[RequireComponent(typeof(Camera))]
[ExecuteInEditMode]
public class CameraGridSetter : MonoBehaviour
{
/// <summary>
/// The number of segments to "divide" the camera's view into.
/// </summary>
public Vector2Int GridSize;
public float Left = -0.2F;
public float Right = 0.2F;
public float Top = 0.2F;
public float Bottom = -0.2F;
/// <summary>
/// TODO needs more work: this is intended to add a margin on the render to allow blending seam artifacts.
/// </summary>
public uint PixelPadding = 0;
/// <summary>
/// Use this to set the camera to render successive grid cells on each frame.
/// Camera is reset after the last cell.
/// TODO make this work
/// </summary>
public bool AutoStepGridCellOnPlay = false;
private Camera cam;
private Matrix4x4 projectionMatrixBackup;
private int currentGridCellIndex = 0;
private bool needsUpdate = false;
private int lastFrame = -1;
#region Algebra
/// <summary>
/// Constructs an off-center projection matrix.
/// The math is from "glFrustum" - https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
/// </summary>
/// <param name="left">Camera-relative x-position of left edge of viewing rect at near plane</param>
/// <param name="right">Camera-relative x-position of right edge of viewing rect at near plane</param>
/// <param name="bottom">Camera-relative y-position of bottom edge of viewing rect at near plane</param>
/// <param name="top">Camera-relative y-position of top edge of viewing rect at near plane</param>
/// <param name="near">Distance from cam to near plane</param>
/// <param name="far">Distance from cam to far plane</param>
/// <returns>Off-center projection matrix with specified params</returns>
static Matrix4x4 PerspectiveOffCenter(float left, float right, float bottom, float top, float near, float far)
{
float x = 2.0F * near / (right - left);
float y = 2.0F * near / (top - bottom);
float a = (right + left) / (right - left);
float b = (top + bottom) / (top - bottom);
float c = -(far + near) / (far - near);
float d = -(2.0F * far * near) / (far - near);
float e = -1.0F;
Matrix4x4 m = new Matrix4x4();
m[0, 0] = x;
m[0, 1] = 0;
m[0, 2] = a;
m[0, 3] = 0;
m[1, 0] = 0;
m[1, 1] = y;
m[1, 2] = b;
m[1, 3] = 0;
m[2, 0] = 0;
m[2, 1] = 0;
m[2, 2] = c;
m[2, 3] = d;
m[3, 0] = 0;
m[3, 1] = 0;
m[3, 2] = e;
m[3, 3] = 0;
return m;
}
/// <summary>
/// Gets the bounds of viewing rect at near plane from camera's existing perspective projection matrix.
/// </summary>
[Button]
void GetFrustumCornersFromCameraMatrix()
{
// The frustum rect at near clip plane is the inverse projection of a rectangle
// with bounds [-1,-1,-1] to [1,1,-1] in normalized device coordinates (OpenGL-style)
var invProjection = cam.projectionMatrix.inverse;
var bottomLeft = invProjection * new Vector4(-1f, -1f, -1.0f, 1);
var topRight = invProjection * new Vector4(1f, 1f, -1.0f, 1);
bottomLeft /= bottomLeft.w;
topRight /= topRight.w;
this.Left = bottomLeft.x;
this.Right = topRight.x;
this.Bottom = bottomLeft.y;
this.Top = topRight.y;
}
/// <summary>
/// Gets the bounds of viewing rect at near plane from camera's settings (field of view, near, far, aspect ratio)
/// </summary>
[Button]
void GetFrustumCornersFromCamera()
{
Camera cam = this.GetComponent<Camera>();
float frustumHeight =
2.0f * cam.nearClipPlane
* Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad);
var frustumWidth = frustumHeight * cam.aspect;
this.Left = -frustumWidth * 0.5f;
this.Right = frustumWidth * 0.5f;
this.Bottom = -frustumHeight * 0.5f;
this.Top = frustumHeight * 0.5f;
}
/// <summary>
/// Split given extents into a grid of size <see cref="GridSize"/>, and return the extents for the supplied grid cell.
/// </summary>
(float left, float right, float top, float bottom) GetExtentsForGridCell(Vector2Int cell)
{
if (this.GridSize.x > 0 && this.GridSize.y > 0)
{
// Note that when you add padding to the frustum's extents, aspect ratio won't be the same as original.
// You'd have to modify the camera's pixel render size to account for that.
float overlapUnits = this.PixelPadding * (this.Right - this.Left) / this.cam.pixelWidth;
float stepX = (this.Right - this.Left) / this.GridSize.x;
float stepY = (this.Top - this.Bottom) / this.GridSize.y;
float l = this.Left + cell.x * stepX;
float r = this.Left + (cell.x + 1) * stepX + overlapUnits;
float b = this.Bottom + cell.y * stepY;
float t = this.Bottom + (cell.y + 1) * stepY + overlapUnits;
return (l, r, t, b);
}
else
{
return (this.Left, this.Right, this.Top, this.Bottom);
}
}
/// <summary>
/// Split the extents of near rect of viewing frustum, and set the camera to view only a portion of the grid
/// as specified by params. The size of the grid is <see cref="GridSize"/>.
/// </summary>
[Button]
public void SetNearClipRectExtentsForCell(int x, int y)
{
(float left, float right, float top, float bottom) = this.GetExtentsForGridCell(new Vector2Int(x, y));
Matrix4x4 m = PerspectiveOffCenter(left, right, bottom, top, this.cam.nearClipPlane, this.cam.farClipPlane);
this.cam.projectionMatrix = m;
}
/// <summary>
/// Set a view frustum based on whatever values <see cref="Left"/>, <see cref="Right"/>, <see cref="Top"/> and
/// <see cref="Bottom"/> currently have.
/// </summary>
[Button]
public void SetNearClipRectExtents()
{
Camera cam = this.GetComponent<Camera>();
Matrix4x4 m = PerspectiveOffCenter(Left, Right, Bottom, Top, cam.nearClipPlane, cam.farClipPlane);
cam.projectionMatrix = m;
}
#endregion
/// <summary>
/// Undo off-center projection, reset to whatever FOV/near/far is set in cam settings.
/// </summary>
[Button]
void ResetCameraMatrix()
{
this.cam.ResetProjectionMatrix();
}
#region Unity callbacks
private void Start()
{
this.cam = this.GetComponent<Camera>();
this.GetFrustumCornersFromCamera();
}
void OnEnable()
{
// TODO this doesn't seem to be working properly with the path tracer
RenderPipelineManager.endCameraRendering += RenderPipelineManager_endCameraRendering;
}
void OnDisable()
{
// TODO this doesn't seem to be working properly with the path tracer
RenderPipelineManager.endCameraRendering -= RenderPipelineManager_endCameraRendering;
}
private void RenderPipelineManager_endCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (camera == this.cam)
{
this.OnPostRender();
}
}
private void OnPostRender()
{
if (Application.isPlaying
&& Time.renderedFrameCount > this.lastFrame
&& this.currentGridCellIndex < this.GridSize.x * this.GridSize.y)
{
this.lastFrame = Time.renderedFrameCount;
this.needsUpdate = true;
}
if (this.needsUpdate && this.GridSize.x > 0 && this.GridSize.y > 0)
{
var x = this.currentGridCellIndex % this.GridSize.x;
var y = this.currentGridCellIndex / this.GridSize.x;
this.SetNearClipRectExtentsForCell(x, y);
this.currentGridCellIndex++;
this.needsUpdate = false;
}
}
#endregion
#region Debugging
[Button]
void PrintDebugInfo()
{
Debug.Log("Pixel width :" + this.cam.pixelWidth + " Pixel height : " + this.cam.pixelHeight);
}
/// <summary>
/// Visualize grid/frustum corners at near plane for debug.
/// </summary>
private void OnDrawGizmos()
{
var matbk = Gizmos.matrix;
float frustumCornerRadius = 0.02f;
float gridPointRadius = 0.01f;
Gizmos.matrix = this.transform.localToWorldMatrix;
Gizmos.color = Color.green;
DrawNearRectCorner(cam, frustumCornerRadius, this.Left, this.Right, this.Top, this.Bottom);
if (this.GridSize.x > 0 && this.GridSize.y > 0)
{
float stepX = (this.Right - this.Left) / this.GridSize.x;
float stepY = (this.Top - this.Bottom) / this.GridSize.y;
for (int i = 0; i < this.GridSize.x; i++)
{
for (int j = 0; j < this.GridSize.y; j++)
{
(float left, float right, float top, float bottom) =
this.GetExtentsForGridCell(new Vector2Int(i, j));
Gizmos.color = Color.cyan;
DrawNearRectCorner(cam, gridPointRadius, left, right, top, bottom);
}
}
}
Gizmos.matrix = matbk;
}
private static void DrawNearRectCorner(Camera cam, float radius, float left, float right, float top,
float bottom)
{
if (cam != null)
{
Gizmos.matrix = cam.transform.localToWorldMatrix;
Gizmos.DrawSphere(new Vector3(left, top, cam.nearClipPlane), radius);
Gizmos.DrawSphere(new Vector3(left, bottom, cam.nearClipPlane), radius);
Gizmos.DrawSphere(new Vector3(right, top, cam.nearClipPlane), radius);
Gizmos.DrawSphere(new Vector3(right, bottom, cam.nearClipPlane), radius);
}
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment