namespace PixelCamera { using UnityEngine; /// /// Tooltips for Unity editor /// public static class Tooltips { public const string TT_FOLLOWED_TRANSFORM = "Transform that this camera follow while doing pixel perfect corrections. Very useful for camera controllers. Allows for full control of a transform."; public const string TT_GRID_MOVEMENT = "Stationary objects in 3D world look stationary without jittering colors or outlines. Camera moves on a voxel grid."; public const string TT_SUB_PIXEL = "Subpixel adjustments counter the blocky nature of moving along a grid."; public const string TT_FOLLOW_ROTATION = "Should the camera transform follow the followed transforms rotation as well as position."; public const string TT_GAME_RESOLUTION = "The resolution of the game render texture. Lower values look more pixelated."; public const string TT_RESOLUTION_SYNCHRONIZATION_MODE = "How 'GameResolution' should be calculated. Synchronization ensures consistent pixelation for different device resolutions and artstyles."; public const string TT_CONTROL_GAME_ZOOM = "Should this script control the game cameras orthographic size. This can be turned off if another controller is already controlling it to avoid unexpected behaviour."; public const string TT_GAME_ZOOM = "The resolution of the game appears to stay the same but everything becomes more detailed while zooming in. The game cameras orhtographic size."; public const string TT_VIEW_ZOOM = "Zoom where pixels stay a constant size and the view camera zooms in. The view cameras orhtographic size."; } /// /// The main script that manges the pixel camera system. /// [ExecuteInEditMode] public class PixelCameraManager : MonoBehaviour { [Tooltip(Tooltips.TT_FOLLOWED_TRANSFORM)] public Transform FollowedTransform; [Header("Settings")] [Tooltip(Tooltips.TT_GRID_MOVEMENT)] public bool VoxelGridMovement = true; [Tooltip(Tooltips.TT_SUB_PIXEL)] public bool SubpixelAdjustments = true; [Tooltip(Tooltips.TT_FOLLOW_ROTATION)] public bool FollowRotation = true; [Header("Resolution")] [Tooltip(Tooltips.TT_RESOLUTION_SYNCHRONIZATION_MODE)] public ResolutionSynchronizationMode resolutionSynchronizationMode = ResolutionSynchronizationMode.SetHeight; public Vector2Int GameResolution = new Vector2Int(640, 360); [Header("Zoom")] [Tooltip(Tooltips.TT_CONTROL_GAME_ZOOM)] public bool ControlGameZoom = true; [Tooltip(Tooltips.TT_GAME_ZOOM)] public float GameCameraZoom = 5f; [Tooltip(Tooltips.TT_VIEW_ZOOM)][Range(-1f, 1f)] public float ViewCameraZoom = 1f; Camera gameCamera; CanvasViewCamera viewCamera; UpscaledCanvas upscaledCanvas; float renderTextureAspect; void OnEnable() { this.Initialize(); } void LateUpdate() { this.UpdateCameraSystem(); } /// /// How tall and wide a pixel on the camera is in world units. /// float PixelWorldSize => 2f * this.gameCamera.orthographicSize / this.gameCamera.pixelHeight; /// /// Gets the target texture resolution and Vector2Int.left if none is defined. /// Vector2Int TargetTextureResolution => this.gameCamera.targetTexture == null ? Vector2Int.left : new Vector2Int(this.gameCamera.targetTexture.width, this.gameCamera.targetTexture.height); /// /// Rounds a vector3 to the closest camera pixel sized voxel. /// public Vector3 PositionToGrid(Vector3 worldPosition) { var localPosition = this.transform.InverseTransformDirection(worldPosition); var localPositionInPixels = localPosition / this.PixelWorldSize; var integerMovement = (Vector3)Vector3Int.RoundToInt(localPositionInPixels); var movement = integerMovement * this.PixelWorldSize; return (movement.x * this.transform.right) + (movement.y * this.transform.up) + (movement.z * this.transform.forward); } /// /// Sets the game camera zoom with some checks as orthographic cameras can not tolerate size = 0. /// float SetGameZoom(float zoom) { var checkedZoom = Mathf.Approximately(zoom, 0f) ? 0.01f : zoom; this.gameCamera.orthographicSize = checkedZoom; return checkedZoom; } /// /// Synchronizes the clip planes. The view camera offset should be taken into account. /// void SynchronizeClipPlanes() { this.viewCamera.SetClipPlanes(0f, this.gameCamera.farClipPlane - this.viewCamera.transform.localPosition.z); } /// /// Initializes the camera system. /// private void Initialize() { if (this.gameCamera == null) { if (!this.TryGetComponent(out this.gameCamera)) { Debug.LogError("Camera is null. Attach required component!"); } } if (this.viewCamera == null) { this.viewCamera = FindAnyObjectByType(typeof(CanvasViewCamera)) as CanvasViewCamera; if (this.viewCamera == null) { Debug.LogError("viewCamera is null. Set it in editor!"); } } if (this.upscaledCanvas == null) { this.upscaledCanvas = FindAnyObjectByType(typeof(UpscaledCanvas)) as UpscaledCanvas; if (this.upscaledCanvas == null) { Debug.LogError("upscaledCanvas is null. Set it in editor!"); } } if (this.transform.parent == null) { Debug.LogError("There is no parent object! Are you sure this is setup correctly. Check the prefab!"); } if (this.transform.parent.childCount > 2) { Debug.LogWarning("Pixel Camera Managers parent should only have 2 child objects. This and a transform that it follows."); } if (this.FollowedTransform == null) { Debug.LogError("Followed Transform is null. Create a empty sibling object and set in editor."); } this.SynchronizeClipPlanes(); } /// /// Sets the render texture of the upscaled canvas and game camera. /// void SetRenderTexture(float aspect, RenderTexture newRenderTexture) { this.upscaledCanvas.SetCanvasRenderTexture(newRenderTexture); this.gameCamera.targetTexture = newRenderTexture; this.renderTextureAspect = aspect; } /// /// Updates the whole pixel camera system. /// void UpdateCameraSystem() { // On aspect change var aspectRatioChanged = this.renderTextureAspect != this.viewCamera.Aspect; var pixelResolutionChanged = this.GameResolution != this.TargetTextureResolution; var resizeCanvas = false; if (aspectRatioChanged || pixelResolutionChanged || this.gameCamera.targetTexture == null) { // Change fitting aspect ratio this.GameResolution = RenderTextureUtilities.TextureResultion(this.viewCamera.Aspect, this.GameResolution, this.resolutionSynchronizationMode); if (this.gameCamera.targetTexture != null) { this.gameCamera.targetTexture.Release(); } var newRenderTexture = RenderTextureUtilities.CreateRenderTexture(this.GameResolution); this.SetRenderTexture(this.viewCamera.Aspect, newRenderTexture); resizeCanvas = true; } else if (Application.isEditor && this.upscaledCanvas.MaterialHasRenderTexture) { this.SetRenderTexture(this.renderTextureAspect, this.gameCamera.targetTexture); resizeCanvas = true; } // Zooming var orthographicSizeChanged = this.gameCamera.orthographicSize != this.GameCameraZoom; if (!this.ControlGameZoom) { this.GameCameraZoom = this.gameCamera.orthographicSize; resizeCanvas = true; } if (this.ControlGameZoom && orthographicSizeChanged) { this.GameCameraZoom = this.SetGameZoom(this.GameCameraZoom); resizeCanvas = true; } if (orthographicSizeChanged || pixelResolutionChanged || this.ViewCameraZoom != this.viewCamera.Zoom) { var canvasOnScreenLimit = 1 - (2f / this.GameResolution.y); // This ensures that the view camera doesn't move over the canvas edge if (this.GameResolution.y < 3) { canvasOnScreenLimit = 1f; Debug.LogWarning("The resolution is too small, this might lead to unexpected behaviour"); } this.ViewCameraZoom = Mathf.Approximately(this.ViewCameraZoom, 0f) ? 0.01f : Mathf.Clamp(this.ViewCameraZoom, -1, 1f); this.viewCamera.SetZoom(this.ViewCameraZoom * canvasOnScreenLimit, this.GameCameraZoom); } // Resize canvas if aspect changed or camera orthographic size changed if (resizeCanvas) { var gameResolutionAspect = (float)this.GameResolution.x / this.GameResolution.y; this.upscaledCanvas.ResizeCanvas(gameResolutionAspect, this.GameCameraZoom * 2f); } // Transform updates if (this.FollowRotation) { this.transform.rotation = this.FollowedTransform.rotation; } this.transform.position = this.VoxelGridMovement ? this.PositionToGrid(this.FollowedTransform.position) : this.FollowedTransform.position; if (this.SubpixelAdjustments) { var targetViewPosition = this.gameCamera.WorldToViewportPoint(this.FollowedTransform.position); this.viewCamera.AdjustSubPixelPosition(targetViewPosition, this.upscaledCanvas.transform.localScale); } } } }