Search code examples
c#unity-game-enginecamerarenderingheadless

Run Unity standalone in Headless Mode whilst capturing screenshots


I need to create a unity project which runs in Headless mode (using the -batchmode command), but it must capture screenshots e.g. every one second and write them out to a file.

I understand that in headless mode you need to forcefully call Camera.Render() in order for anything to be rendered.

It seems that time freezes after the first screenshot is captured. The first screenshot looks absolutely correct, but all subsequent screenshots are identical to the first, implying that time has frozen.

What do I need to do to ensure that the scene updates correctly over time and that the camera is able to render every second?

using System;
using System.Collections;
using System.IO;
using UnityEngine;

namespace Assets
{
    public class Particles : MonoBehaviour
    {
        private const float screenshotEvery = 1.0f;
        private float timerCount = 0f;
        private float elapsedSecond;
        private const float maxElapsedSecond = 20;
        private string screenshotsDirectory = "UnityHeadlessRenderingScreenshots";
        public Camera camOV;
        public RenderTexture currentRT;

        // Use this for initialization
        public void Start ()
        {
            Application.targetFrameRate = 60;
            if (Directory.Exists(screenshotsDirectory))
            {
                Directory.Delete(screenshotsDirectory, true);
            }
            if (!Application.isEditor)
            {
                Directory.CreateDirectory(screenshotsDirectory);
                camOV.targetTexture = currentRT;
            }

        }

        // Update is called once per frame
        public void Update ()
        {
            elapsedSecond += Time.deltaTime;
            timerCount += Time.deltaTime;
            if (Application.isEditor)
            {
                return;
            }
            if (elapsedSecond <= maxElapsedSecond)
            {
                if (timerCount >= screenshotEvery)
                {
                    TakeScreenShot();
                    timerCount = 0f;
                }
            }
            else
            {
                Application.Quit();
            }
        }

        public void TakeScreenShot()
        {
            RenderTexture.active = camOV.targetTexture;
            camOV.Render();
            Texture2D imageOverview = new Texture2D(camOV.targetTexture.width, camOV.targetTexture.height, TextureFormat.RGB24, false);
            imageOverview.ReadPixels(new Rect(0, 0, camOV.targetTexture.width, camOV.targetTexture.height), 0, 0);
            imageOverview.Apply();
            RenderTexture.active = currentRT;

            // Encode texture into PNG
            byte[] bytes = imageOverview.EncodeToPNG();

            // save in memory
            string filename = elapsedSecond + ".png";
            var path = screenshotsDirectory + "/" + filename;
            File.WriteAllBytes(path, bytes);

        }
    }
}

Many thanks in advance!


Solution

  • One advice I would give you about taking screenshots is to wait for WaitForEndOfFrame before taking the screenshot.

    You said that you want to capture screenshot every second and save this. This should be done in coroutine not in the Update function.

    Something like this in a coroutine function:

    WaitForEndOfFrame waitForFrame = new WaitForEndOfFrame();
    WaitForSeconds waitForTime = new WaitForSeconds(1);//1 second
    
    while (true)
    {
        //Wait for frame
        yield return waitForFrame;
    
        Increment timer
    
        Capture and save Screenshot
    
        Save the screenshot
    
        //Wait for one second 
        yield return waitForTime;
    }
    

    I did not encounter any problems by doing it this. Just start the coroutine and let it run forever in a while loop. It won't freeze because it is yielding every 1 second and also every frame. Below is the complete code:

    public Camera camOV;
    public RenderTexture currentRT;
    float elapsedSecond = 0;
    string screenshotsDirectory = "UnityHeadlessRenderingScreenshots";
    float beginTime;
    
    void Start()
    {
        beginTime = Time.time;
        StartCoroutine(TakeScreenShot());
    }
    
    public IEnumerator TakeScreenShot()
    {
        beginTime = Time.time;
    
        WaitForEndOfFrame waitForFrame = new WaitForEndOfFrame();
        WaitForSeconds waitForTime = new WaitForSeconds(1);//1 second
    
        while (true)
        {
            //Wait for frame
            yield return waitForFrame;
    
            //Increment timer
            elapsedSecond = Time.time - beginTime;
    
            RenderTexture.active = camOV.targetTexture;
            camOV.Render();
            Debug.Log(camOV);
            Texture2D imageOverview = new Texture2D(camOV.targetTexture.width, camOV.targetTexture.height,
                TextureFormat.RGB24, false);
            imageOverview.ReadPixels(new Rect(0, 0, camOV.targetTexture.width, camOV.targetTexture.height), 0, 0);
            imageOverview.Apply();
            RenderTexture.active = currentRT;
    
            // Encode texture into PNG
            byte[] bytes = imageOverview.EncodeToPNG();
    
            // save in memory
            string filename = elapsedSecond + ".png";
            var path = screenshotsDirectory + "/" + filename;
            File.WriteAllBytes(path, bytes);
    
            //Wait for one second 
            yield return waitForTime;
    
            Debug.Log(elapsedSecond);
        }
    }