Skip to content

Instantly share code, notes, and snippets.

@TobiasPott
Last active June 10, 2022 13:54
Show Gist options
  • Save TobiasPott/bf918c451ef5d21750c86fe0892dbabb to your computer and use it in GitHub Desktop.
Save TobiasPott/bf918c451ef5d21750c86fe0892dbabb to your computer and use it in GitHub Desktop.
Unity3D ScreenGrab class for non-blocking camera capturing
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