You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
473 lines
17 KiB
473 lines
17 KiB
using System.Collections.Generic; |
|
using UnityEngine.Rendering; |
|
using UnityEngine.Rendering.Universal; |
|
using UnityEngine.Scripting.APIUpdating; |
|
|
|
namespace UnityEngine.Experimental.Rendering.Universal |
|
{ |
|
/// <summary> |
|
/// The Pixel Perfect Camera component ensures your pixel art remains crisp and clear at different resolutions, and stable in motion. |
|
/// </summary> |
|
[ExecuteInEditMode] |
|
[DisallowMultipleComponent] |
|
[AddComponentMenu("Rendering/2D/Pixel Perfect Camera")] |
|
[RequireComponent(typeof(Camera))] |
|
[MovedFrom("UnityEngine.Experimental.Rendering")] |
|
[HelpURL("https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@latest/index.html?subfolder=/manual/2d-pixelperfect.html%23properties")] |
|
public class PixelPerfectCamera : MonoBehaviour, IPixelPerfectCamera, ISerializationCallbackReceiver |
|
{ |
|
/// <summary> |
|
/// An enumeration of the types of display cropping. |
|
/// </summary> |
|
public enum CropFrame |
|
{ |
|
/// <summary> |
|
/// No cropping. |
|
/// </summary> |
|
None, |
|
/// <summary> |
|
/// Black borders added to the left and right of viewport to match Reference Resolution. |
|
/// </summary> |
|
Pillarbox, |
|
/// <summary> |
|
/// Black borders added to the top and bottom of viewport to match Reference Resolution. |
|
/// </summary> |
|
Letterbox, |
|
/// <summary> |
|
/// Black borders added to all sides of viewport to match Reference Resolution. |
|
/// </summary> |
|
Windowbox, |
|
/// <summary> |
|
/// Expands the viewport to fit the screen resolution while maintaining the viewport's aspect ratio. |
|
/// </summary> |
|
StretchFill |
|
} |
|
|
|
/// <summary> |
|
/// Determines how pixels are snapped to the grid. |
|
/// </summary> |
|
public enum GridSnapping |
|
{ |
|
/// <summary> |
|
/// No snapping. |
|
/// </summary> |
|
None, |
|
/// <summary> |
|
/// Prevent subpixel movement and make Sprites appear to move in pixel-by-pixel increments. |
|
/// </summary> |
|
PixelSnapping, |
|
/// <summary> |
|
/// The scene is rendered to a temporary texture set as close as possible to the Reference Resolution, while maintaining the full screen aspect ratio. This temporary texture is then upscaled to fit the full screen. |
|
/// </summary> |
|
UpscaleRenderTexture |
|
} |
|
|
|
private enum ComponentVersions |
|
{ |
|
Version_Unserialized = 0, |
|
Version_1 = 1 |
|
} |
|
|
|
|
|
#if UNITY_EDITOR |
|
const ComponentVersions k_CurrentComponentVersion = ComponentVersions.Version_1; |
|
[SerializeField] ComponentVersions m_ComponentVersion = ComponentVersions.Version_Unserialized; |
|
#endif |
|
|
|
/// <summary> |
|
/// Defines how the output display will be cropped. |
|
/// </summary> |
|
public CropFrame cropFrame { get { return m_CropFrame; } set { m_CropFrame = value; } } |
|
|
|
/// <summary> |
|
/// Defines if pixels will be locked to a grid determined by assetsPPU. |
|
/// </summary> |
|
public GridSnapping gridSnapping { get { return m_GridSnapping; } set { m_GridSnapping = value; } } |
|
|
|
/// <summary> |
|
/// The target orthographic size of the camera. |
|
/// </summary> |
|
public float orthographicSize { get { return m_Internal.orthoSize; } } |
|
|
|
/// <summary> |
|
/// Match this value to to the Pixels Per Unit values of all Sprites within the Scene. |
|
/// </summary> |
|
public int assetsPPU { get { return m_AssetsPPU; } set { m_AssetsPPU = value > 0 ? value : 1; } } |
|
|
|
/// <summary> |
|
/// The original horizontal resolution your Assets are designed for. |
|
/// </summary> |
|
public int refResolutionX { get { return m_RefResolutionX; } set { m_RefResolutionX = value > 0 ? value : 1; } } |
|
|
|
/// <summary> |
|
/// Original vertical resolution your Assets are designed for. |
|
/// </summary> |
|
public int refResolutionY { get { return m_RefResolutionY; } set { m_RefResolutionY = value > 0 ? value : 1; } } |
|
|
|
/// <summary> |
|
/// Set to true to have the Scene rendered to a temporary texture set as close as possible to the Reference Resolution, |
|
/// while maintaining the full screen aspect ratio. This temporary texture is then upscaled to fit the full screen. |
|
/// </summary> |
|
[System.Obsolete("Use gridSnapping instead", false)] |
|
public bool upscaleRT |
|
{ |
|
get |
|
{ |
|
return m_GridSnapping == GridSnapping.UpscaleRenderTexture; |
|
} |
|
set |
|
{ |
|
m_GridSnapping = value ? GridSnapping.UpscaleRenderTexture : GridSnapping.None; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Set to true to prevent subpixel movement and make Sprites appear to move in pixel-by-pixel increments. |
|
/// Only applicable when upscaleRT is false. |
|
/// </summary> |
|
[System.Obsolete("Use gridSnapping instead", false)] |
|
public bool pixelSnapping |
|
{ |
|
get |
|
{ |
|
return m_GridSnapping == GridSnapping.PixelSnapping; |
|
} |
|
set |
|
{ |
|
m_GridSnapping = value ? GridSnapping.PixelSnapping : GridSnapping.None; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Set to true to crop the viewport with black bars to match refResolutionX in the horizontal direction. |
|
/// </summary> |
|
[System.Obsolete("Use cropFrame instead", false)] |
|
public bool cropFrameX |
|
{ |
|
get |
|
{ |
|
return m_CropFrame == CropFrame.StretchFill || m_CropFrame == CropFrame.Windowbox || m_CropFrame == CropFrame.Pillarbox; |
|
} |
|
set |
|
{ |
|
if (value) |
|
{ |
|
if (m_CropFrame == CropFrame.None) |
|
m_CropFrame = CropFrame.Pillarbox; |
|
else if (m_CropFrame == CropFrame.Letterbox) |
|
m_CropFrame = CropFrame.Windowbox; |
|
} |
|
else |
|
{ |
|
if (m_CropFrame == CropFrame.Pillarbox) |
|
m_CropFrame = CropFrame.None; |
|
else if (m_CropFrame == CropFrame.Windowbox || m_CropFrame == CropFrame.StretchFill) |
|
m_CropFrame = CropFrame.Letterbox; |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Set to true to crop the viewport with black bars to match refResolutionY in the vertical direction. |
|
/// </summary> |
|
[System.Obsolete("Use cropFrame instead", false)] |
|
public bool cropFrameY |
|
{ |
|
get |
|
{ |
|
return m_CropFrame == CropFrame.StretchFill || m_CropFrame == CropFrame.Windowbox || m_CropFrame == CropFrame.Letterbox; |
|
} |
|
set |
|
{ |
|
if (value) |
|
{ |
|
if (m_CropFrame == CropFrame.None) |
|
m_CropFrame = CropFrame.Letterbox; |
|
else if (m_CropFrame == CropFrame.Pillarbox) |
|
m_CropFrame = CropFrame.Windowbox; |
|
} |
|
else |
|
{ |
|
if (m_CropFrame == CropFrame.Letterbox) |
|
m_CropFrame = CropFrame.None; |
|
else if (m_CropFrame == CropFrame.Windowbox || m_CropFrame == CropFrame.StretchFill) |
|
m_CropFrame = CropFrame.Pillarbox; |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Set to true to expand the viewport to fit the screen resolution while maintaining the viewport's aspect ratio. |
|
/// Only applicable when both cropFrameX and cropFrameY are true. |
|
/// </summary> |
|
[System.Obsolete("Use cropFrame instead", false)] |
|
public bool stretchFill |
|
{ |
|
get |
|
{ |
|
return m_CropFrame == CropFrame.StretchFill; |
|
} |
|
set |
|
{ |
|
if (value) |
|
m_CropFrame = CropFrame.StretchFill; |
|
else |
|
m_CropFrame = CropFrame.Windowbox; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Ratio of the rendered Sprites compared to their original size (readonly). |
|
/// </summary> |
|
public int pixelRatio |
|
{ |
|
get |
|
{ |
|
if (m_CinemachineCompatibilityMode) |
|
{ |
|
if (m_GridSnapping == GridSnapping.UpscaleRenderTexture) |
|
return m_Internal.zoom * m_Internal.cinemachineVCamZoom; |
|
else |
|
return m_Internal.cinemachineVCamZoom; |
|
} |
|
else |
|
{ |
|
return m_Internal.zoom; |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Round a arbitrary position to an integer pixel position. Works in world space. |
|
/// </summary> |
|
/// <param name="position"> The position you want to round.</param> |
|
/// <returns> |
|
/// The rounded pixel position. |
|
/// Depending on the values of upscaleRT and pixelSnapping, it could be a screen pixel position or an art pixel position. |
|
/// </returns> |
|
public Vector3 RoundToPixel(Vector3 position) |
|
{ |
|
float unitsPerPixel = m_Internal.unitsPerPixel; |
|
if (unitsPerPixel == 0.0f) |
|
return position; |
|
|
|
Vector3 result; |
|
result.x = Mathf.Round(position.x / unitsPerPixel) * unitsPerPixel; |
|
result.y = Mathf.Round(position.y / unitsPerPixel) * unitsPerPixel; |
|
result.z = Mathf.Round(position.z / unitsPerPixel) * unitsPerPixel; |
|
|
|
return result; |
|
} |
|
|
|
/// <summary> |
|
/// Find a pixel-perfect orthographic size as close to targetOrthoSize as possible. Used by Cinemachine to solve compatibility issues with Pixel Perfect Camera. |
|
/// </summary> |
|
/// <param name="targetOrthoSize">Orthographic size from the live Cinemachine Virtual Camera.</param> |
|
/// <returns>The corrected orthographic size.</returns> |
|
public float CorrectCinemachineOrthoSize(float targetOrthoSize) |
|
{ |
|
m_CinemachineCompatibilityMode = true; |
|
|
|
if (m_Internal == null) |
|
return targetOrthoSize; |
|
else |
|
return m_Internal.CorrectCinemachineOrthoSize(targetOrthoSize); |
|
} |
|
|
|
[SerializeField] int m_AssetsPPU = 100; |
|
[SerializeField] int m_RefResolutionX = 320; |
|
[SerializeField] int m_RefResolutionY = 180; |
|
|
|
[SerializeField] CropFrame m_CropFrame; |
|
[SerializeField] GridSnapping m_GridSnapping; |
|
|
|
// These are obsolete. They are here only for migration. |
|
#if UNITY_EDITOR |
|
[SerializeField] bool m_UpscaleRT; |
|
[SerializeField] bool m_PixelSnapping; |
|
[SerializeField] bool m_CropFrameX; |
|
[SerializeField] bool m_CropFrameY; |
|
[SerializeField] bool m_StretchFill; |
|
#endif |
|
|
|
Camera m_Camera; |
|
PixelPerfectCameraInternal m_Internal; |
|
bool m_CinemachineCompatibilityMode; |
|
|
|
internal FilterMode finalBlitFilterMode |
|
{ |
|
get |
|
{ |
|
return m_Internal.useStretchFill ? FilterMode.Bilinear : FilterMode.Point; |
|
} |
|
} |
|
|
|
internal Vector2Int offscreenRTSize |
|
{ |
|
get |
|
{ |
|
return new Vector2Int(m_Internal.offscreenRTWidth, m_Internal.offscreenRTHeight); |
|
} |
|
} |
|
|
|
Vector2Int cameraRTSize |
|
{ |
|
get |
|
{ |
|
var targetTexture = m_Camera.targetTexture; |
|
return targetTexture == null ? new Vector2Int(Screen.width, Screen.height) : new Vector2Int(targetTexture.width, targetTexture.height); |
|
} |
|
} |
|
|
|
// Snap camera position to pixels using Camera.worldToCameraMatrix. |
|
void PixelSnap() |
|
{ |
|
Vector3 cameraPosition = m_Camera.transform.position; |
|
Vector3 roundedCameraPosition = RoundToPixel(cameraPosition); |
|
Vector3 offset = roundedCameraPosition - cameraPosition; |
|
offset.z = -offset.z; |
|
Matrix4x4 offsetMatrix = Matrix4x4.TRS(-offset, Quaternion.identity, new Vector3(1.0f, 1.0f, -1.0f)); |
|
|
|
m_Camera.worldToCameraMatrix = offsetMatrix * m_Camera.transform.worldToLocalMatrix; |
|
} |
|
|
|
void Awake() |
|
{ |
|
m_Camera = GetComponent<Camera>(); |
|
m_Internal = new PixelPerfectCameraInternal(this); |
|
|
|
// Case 1249076: Initialize internals immediately after the scene is loaded, |
|
// as the Cinemachine extension may need them before OnBeginContextRendering is called. |
|
UpdateCameraProperties(); |
|
} |
|
|
|
void UpdateCameraProperties() |
|
{ |
|
var rtSize = cameraRTSize; |
|
m_Internal.CalculateCameraProperties(rtSize.x, rtSize.y); |
|
|
|
if (m_Internal.useOffscreenRT) |
|
m_Camera.pixelRect = m_Internal.CalculateFinalBlitPixelRect(rtSize.x, rtSize.y); |
|
else |
|
m_Camera.rect = new Rect(0.0f, 0.0f, 1.0f, 1.0f); |
|
} |
|
|
|
void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) |
|
{ |
|
if (camera == m_Camera) |
|
{ |
|
UpdateCameraProperties(); |
|
PixelSnap(); |
|
|
|
if (!m_CinemachineCompatibilityMode) |
|
{ |
|
m_Camera.orthographicSize = m_Internal.orthoSize; |
|
} |
|
|
|
UnityEngine.U2D.PixelPerfectRendering.pixelSnapSpacing = m_Internal.unitsPerPixel; |
|
} |
|
} |
|
|
|
void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) |
|
{ |
|
if (camera == m_Camera) |
|
UnityEngine.U2D.PixelPerfectRendering.pixelSnapSpacing = 0.0f; |
|
} |
|
|
|
void OnEnable() |
|
{ |
|
m_CinemachineCompatibilityMode = false; |
|
|
|
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; |
|
RenderPipelineManager.endCameraRendering += OnEndCameraRendering; |
|
} |
|
|
|
internal void OnDisable() |
|
{ |
|
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; |
|
RenderPipelineManager.endCameraRendering -= OnEndCameraRendering; |
|
|
|
m_Camera.rect = new Rect(0.0f, 0.0f, 1.0f, 1.0f); |
|
m_Camera.ResetWorldToCameraMatrix(); |
|
} |
|
|
|
#if DEVELOPMENT_BUILD || UNITY_EDITOR |
|
// Show on-screen warning about invalid render resolutions. |
|
void OnGUI() |
|
{ |
|
Color oldColor = GUI.color; |
|
GUI.color = Color.red; |
|
|
|
Vector2Int renderResolution = Vector2Int.zero; |
|
renderResolution.x = m_Internal.useOffscreenRT ? m_Internal.offscreenRTWidth : m_Camera.pixelWidth; |
|
renderResolution.y = m_Internal.useOffscreenRT ? m_Internal.offscreenRTHeight : m_Camera.pixelHeight; |
|
|
|
if (renderResolution.x % 2 != 0 || renderResolution.y % 2 != 0) |
|
{ |
|
string warning = string.Format("Rendering at an odd-numbered resolution ({0} * {1}). Pixel Perfect Camera may not work properly in this situation.", renderResolution.x, renderResolution.y); |
|
GUILayout.Box(warning); |
|
} |
|
|
|
var targetTexture = m_Camera.targetTexture; |
|
Vector2Int rtSize = targetTexture == null ? new Vector2Int(Screen.width, Screen.height) : new Vector2Int(targetTexture.width, targetTexture.height); |
|
|
|
if (rtSize.x < refResolutionX || rtSize.y < refResolutionY) |
|
{ |
|
GUILayout.Box("Target resolution is smaller than the reference resolution. Image may appear stretched or cropped."); |
|
} |
|
|
|
GUI.color = oldColor; |
|
} |
|
|
|
#endif |
|
|
|
/// <summary> |
|
/// OnBeforeSerialize implementation. |
|
/// </summary> |
|
public void OnBeforeSerialize() |
|
{ |
|
#if UNITY_EDITOR |
|
m_ComponentVersion = k_CurrentComponentVersion; |
|
#endif |
|
} |
|
|
|
/// <summary> |
|
/// OnAfterSerialize implementation. |
|
/// </summary> |
|
public void OnAfterDeserialize() |
|
{ |
|
#if UNITY_EDITOR |
|
// Upgrade from no serialized version |
|
if (m_ComponentVersion == ComponentVersions.Version_Unserialized) |
|
{ |
|
if (m_UpscaleRT) |
|
m_GridSnapping = GridSnapping.UpscaleRenderTexture; |
|
else if (m_PixelSnapping) |
|
m_GridSnapping = GridSnapping.PixelSnapping; |
|
|
|
if (m_CropFrameX && m_CropFrameY) |
|
{ |
|
if (m_StretchFill) |
|
m_CropFrame = CropFrame.StretchFill; |
|
else |
|
m_CropFrame = CropFrame.Windowbox; |
|
} |
|
else if (m_CropFrameX) |
|
{ |
|
m_CropFrame = CropFrame.Pillarbox; |
|
} |
|
else if (m_CropFrameY) |
|
{ |
|
m_CropFrame = CropFrame.Letterbox; |
|
} |
|
else |
|
{ |
|
m_CropFrame = CropFrame.None; |
|
} |
|
|
|
m_ComponentVersion = ComponentVersions.Version_1; |
|
} |
|
#endif |
|
} |
|
} |
|
}
|
|
|