Last active
June 10, 2022 13:54
-
-
Save TobiasPott/bf918c451ef5d21750c86fe0892dbabb to your computer and use it in GitHub Desktop.
Unity3D ScreenGrab class for non-blocking camera capturing
This file contains 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 System; | |
using System.Collections; | |
using UnityEngine; | |
namespace NoXP | |
{ | |
/// <summary> | |
/// list of texture format encodings currently available in Unity3D (at runtime) | |
/// </summary> | |
public enum TextureEncodings | |
{ | |
JPG = 0, | |
PNG = 1, | |
EXR = 2, | |
#if UNITY_2018_3_OR_NEWER | |
TGA = 3 | |
#endif | |
} | |
/// <summary> | |
/// list of texture format encodings currently available in Unity3D (at runtime) | |
/// </summary> | |
public enum OutputDimensions | |
{ | |
HD_3840x2160 = 0, | |
HD_1920x1080 = 1, | |
HD_1280x720 = 2, | |
SD_640x480 = 20, | |
SD_720x576 = 21, // PAL | |
SD_PAL = 21, // PAL | |
SD_720x486 = 22, // NTSC | |
SD_NTSC = 22, // NTSC | |
} | |
/// <summary> | |
/// a runtime component used during the screen grab process | |
/// </summary> | |
public class CoroutineProxy : MonoBehaviour | |
{ } | |
/// <summary> | |
/// provides information for the screen grab process | |
/// </summary> | |
public class CameraGrabParameter : IDisposable | |
{ | |
private CoroutineProxy _coroutineProxy; | |
private bool _isTargetOwner = false; | |
private bool _isTemporary = false; | |
/// <summary> | |
/// gets the camera instance used for screen capturing | |
/// </summary> | |
public Camera Camera | |
{ get; private set; } = null; | |
/// <summary> | |
/// gets the texture instance the screen is captured into | |
/// </summary> | |
public Texture2D Target | |
{ get; private set; } = null; | |
/// <summary> | |
/// gets the optional callback which is called when the process has finished | |
/// </summary> | |
public Action<CameraGrabParameter> Callback | |
{ get; private set; } = null; | |
/// <summary> | |
/// gets the width of the grabbed screen | |
/// </summary> | |
public int Width | |
{ get; private set; } = -1; | |
/// <summary> | |
/// gets the height of the grabbed screen | |
/// </summary> | |
public int Height | |
{ get; private set; } = -1; | |
/// <summary> | |
/// gets the number of horizontal slices created when grabbing the screen inside the coroutine to avoid application stalling | |
/// </summary> | |
/// <remarks>The higher the slices the longer the grabbing process will take but less application stalling is noticable</remarks> | |
public int HorizontalSlices | |
{ get; private set; } = 4; | |
/// <summary> | |
/// gets the number of vertical slices created when grabbing the screen inside the coroutine to avoid application stalling | |
/// </summary> | |
/// <remarks>The higher the slices the longer the grabbing process will take but less application stalling is noticable</remarks> | |
public int VerticalSlices | |
{ get; private set; } = 4; | |
/// <summary> | |
/// gets or sets the quality level used when writing the screen grab encoded as jpg file to disk | |
/// </summary> | |
public int JPEGQuality | |
{ get; set; } = 100; | |
/// <summary> | |
/// gets or sets the texture encoding used to save the screen grabb to disk | |
/// </summary> | |
public TextureEncodings Encoding | |
{ get; set; } = TextureEncodings.JPG; | |
internal CoroutineProxy Proxy | |
{ get { return GetCoroutineProxy(); } } | |
public bool IsTemporary | |
{ get { return _isTemporary; } } | |
/// <summary> | |
/// gets or sets whether or not a deactivated camera should be activated before execution and the active-state restored afterwards. | |
/// </summary> | |
/// <remarks>This might be useful for cameras using PostFX or other components which do not work when the GameObject is inactive.</remarks> | |
public bool ExecuteWithActivateCamera | |
{ get; set; } = false; | |
public CameraGrabParameter(Camera camera, Texture2D target, Action<CameraGrabParameter> callback, int width = -1, int height = -1, int horizontalSlices = 4, int verticalSlices = 4) | |
{ | |
this.AssignFields(camera, target, callback, width, height, horizontalSlices, verticalSlices); | |
} | |
public CameraGrabParameter(Camera camera, Texture2D target, Action<CameraGrabParameter> callback, OutputDimensions dimensions, int horizontalSlices = 4, int verticalSlices = 4) | |
{ | |
int width = -1, height = -1; | |
switch (dimensions) | |
{ | |
case OutputDimensions.HD_3840x2160: | |
width = 3840; height = 2160; | |
break; | |
case OutputDimensions.HD_1920x1080: | |
width = 1920; height = 1080; | |
break; | |
case OutputDimensions.HD_1280x720: | |
width = 1280; height = 720; | |
break; | |
case OutputDimensions.SD_640x480: | |
width = 640; height = 480; | |
break; | |
//case OutputDimensions.SD_720x486: | |
case OutputDimensions.SD_NTSC: | |
width = 720; height = 486; | |
break; | |
//case OutputDimensions.SD_720x576: | |
case OutputDimensions.SD_PAL: | |
width = 720; height = 576; | |
break; | |
} | |
this.AssignFields(camera, target, callback, width, height, horizontalSlices, verticalSlices); | |
} | |
private CameraGrabParameter(CameraGrabParameter parameter, bool temporary = false) | |
{ | |
_isTemporary = temporary; | |
this.AssignFields(parameter.Camera, null, parameter.Callback, parameter.Width, parameter.Height, parameter.HorizontalSlices, parameter.VerticalSlices); | |
this.JPEGQuality = parameter.JPEGQuality; | |
this.Encoding = parameter.Encoding; | |
this.ExecuteWithActivateCamera = parameter.ExecuteWithActivateCamera; | |
} | |
public static CameraGrabParameter CreateCopy(CameraGrabParameter parameter) | |
{ | |
return new CameraGrabParameter(parameter, false); | |
} | |
public static CameraGrabParameter CreateTemporary(CameraGrabParameter parameter) | |
{ | |
return new CameraGrabParameter(parameter, true); | |
} | |
private void AssignFields(Camera camera, Texture2D target, Action<CameraGrabParameter> callback, int width = -1, int height = -1, int horizontalSlices = 4, int verticalSlices = 4) | |
{ | |
// assign width and height parameter (any value lesser or equal to zero will cause the screen dimension to be used | |
this.Width = width > 0 ? width : Screen.width; | |
this.Height = height > 0 ? height : Screen.height; | |
this.Camera = camera; | |
// assign parameter or initialize new texture object with appropiate size | |
this.Target = target != null ? target : new Texture2D(this.Width, this.Height, TextureFormat.RGB24, false); | |
_isTargetOwner = (target == null); | |
this.Callback = callback; | |
this.HorizontalSlices = horizontalSlices; | |
this.VerticalSlices = verticalSlices; | |
} | |
private CoroutineProxy GetCoroutineProxy() | |
{ | |
if (_coroutineProxy == null) | |
_coroutineProxy = new GameObject("ProxyForScreenGrabOn" + this.Camera.name, typeof(CoroutineProxy)).GetComponent<CoroutineProxy>(); | |
return _coroutineProxy; | |
} | |
public void WriteToDisk(string path) | |
{ | |
this.WriteToDiskWithEncoding(path, this.Encoding); | |
} | |
public void WriteToDiskWithEncoding(string path, TextureEncodings encoding) | |
{ | |
string pathLI = path.ToLowerInvariant(); | |
byte[] encodedData = null; | |
switch (encoding) | |
{ | |
case TextureEncodings.PNG: | |
encodedData = this.Target.EncodeToPNG(); | |
if (!pathLI.EndsWith(".png")) path += ".png"; | |
break; | |
#if UNITY_2018_3_OR_NEWER | |
case TextureEncodings.TGA: | |
encodedData = this.Target.EncodeToTGA(); | |
if (!pathLI.EndsWith(".tga")) path += ".tga"; | |
break; | |
#endif | |
case TextureEncodings.EXR: | |
encodedData = this.Target.EncodeToEXR(); | |
if (!pathLI.EndsWith(".exr")) path += ".exr"; | |
break; | |
case TextureEncodings.JPG: | |
default: | |
encodedData = this.Target.EncodeToJPG(this.JPEGQuality); | |
if (!pathLI.EndsWith(".jpg")) path += ".jpg"; | |
break; | |
} | |
System.IO.File.WriteAllBytes(path, encodedData); | |
} | |
public void OnCallback() | |
{ | |
if (this.Callback != null) | |
this.Callback.Invoke(this); | |
} | |
public void Dispose() | |
{ | |
if (_coroutineProxy != null) | |
GameObject.Destroy(_coroutineProxy.gameObject); | |
if (_isTargetOwner && this.Target != null) | |
UnityEngine.Object.Destroy(this.Target); | |
} | |
} | |
public class CameraGrab | |
{ | |
public static void Execute(CameraGrabParameter parameter) | |
{ | |
parameter.Proxy.StartCoroutine(CameraGrab.COR_ScreenGrabInternal(parameter)); | |
} | |
public static void Execute(CameraGrabParameter parameter, string path) | |
{ | |
parameter.Proxy.StartCoroutine(CameraGrab.COR_ScreenGrabToDisk(parameter, path)); | |
} | |
private static IEnumerator COR_ScreenGrab(CameraGrabParameter parameter, string path) | |
{ | |
yield return COR_ScreenGrabInternal(parameter); | |
// dispose parameter if it was created as a temporary one | |
if (parameter.IsTemporary) | |
parameter.Dispose(); | |
} | |
private static IEnumerator COR_ScreenGrabToDisk(CameraGrabParameter parameter, string path) | |
{ | |
yield return COR_ScreenGrabInternal(parameter); | |
parameter.WriteToDisk(path); | |
// dispose parameter if it was created as a temporary one | |
if (parameter.IsTemporary) | |
parameter.Dispose(); | |
} | |
private static IEnumerator COR_ScreenGrabInternal(CameraGrabParameter parameter) | |
{ | |
int sW = parameter.Width; | |
int sH = parameter.Height; | |
RenderTexture temp = new RenderTexture(sW, sH, 16); | |
CameraGrab.RenderCameraToTexture(parameter, temp); | |
yield return new WaitForEndOfFrame(); | |
int blockW = (sW / parameter.HorizontalSlices); | |
int blockH = (sH / parameter.VerticalSlices); | |
Debug.Log(string.Format("Do {0} x {1} iterations with {2} x {3} in size.", parameter.HorizontalSlices, parameter.VerticalSlices, blockW, blockH)); | |
Debug.Log("ScreenGrab.Started: " + Time.time); | |
int x = 0, y = 0; | |
for (int iX = 0; iX < parameter.HorizontalSlices; iX++) | |
{ | |
for (int iY = 0; iY < parameter.VerticalSlices; iY++) | |
{ | |
yield return null; | |
RenderTexture prevActive = RenderTexture.active; | |
RenderTexture.active = temp; | |
x = iX * blockW; | |
y = iY * blockH; | |
#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS) | |
int yDest = y; | |
#else | |
int yDest = (sH - y) - blockH; | |
#endif | |
parameter.Target.ReadPixels(new Rect(x, y, blockW, blockH), x, yDest); | |
RenderTexture.active = prevActive; | |
} | |
} | |
//parameter.Target.Apply(); | |
Debug.Log("ScreenGrab.Ended: " + Time.time); | |
temp.Release(); | |
parameter.OnCallback(); | |
} | |
private static void RenderCameraToTexture(CameraGrabParameter parameter, RenderTexture renderTexture) | |
{ | |
// get active state of camera | |
bool previousActiveState = parameter.Camera.gameObject.activeSelf; | |
// ensure camera is active before call to Render() | |
if (!previousActiveState && parameter.ExecuteWithActivateCamera) | |
parameter.Camera.gameObject.SetActive(true); | |
parameter.Camera.targetTexture = renderTexture; | |
parameter.Camera.Render(); | |
parameter.Camera.targetTexture = null; | |
// restore the cameras active state after call to Render() | |
if (!previousActiveState && parameter.ExecuteWithActivateCamera) | |
parameter.Camera.gameObject.SetActive(false); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment