Created
October 22, 2021 05:55
-
-
Save SabinT/06cf2c744007fd120c0da719d53b1450 to your computer and use it in GitHub Desktop.
Tiled camera rendering helper in Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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