unity/Assets/Scripts/Recorder.cs (304 lines of code) (raw):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.IO;
using UnityEngine.Rendering.PostProcessing;
using UnityEngine.Rendering;
// The Recorder works in server / offline mode. It is responsible for
// preparing offscreen buffers that are used for batch rendering, issuing
// the actual render call and finally pulling the images from the GPU.
// To avoid GPU/CPU stalls and unnecessary data dependencies, there is
// a long pipeline of images in flight. The Recorder keeps a large number
// of render targets and destination textures, that are accessed in
// a round robin fashion.
//
// Configurable flags:
// int capture_width - default width for the captured image,
// int capture_height - default height for the captured image,
// bool capture_alpha - should capture transparency by default,
// int capture_fps - deprecated,
// float speedup - deprecated,
// int batch_size - default batch size.
public class Recorder : MonoBehaviour {
[SerializeField]
[Flag]
public int capture_width_ = 512;
[SerializeField]
[Flag]
public int capture_height_ = 512;
[SerializeField]
[Flag]
public bool capture_alpha_ = false;
[SerializeField]
[Flag]
public int capture_fps_ = 30;
[SerializeField]
[Flag]
public float speedup_ = 10.0f;
[SerializeField]
[Flag]
public int batch_size_ = 200;
[SerializeField]
public IImageBatchConsumer batch_consumer_ = null;
public RenderTexture last_render_ = null;
private int render_texture_rgb_index_ = -1;
private int render_texture_depth_index_ = -1;
private int capture_count_ = 0;
private int capture_total_count_ = 0;
private bool capture_next_frame_ = false;
private int batch_count_ = 0;
private bool capture_depth_ = false;
private bool capture_normals_ = false;
// This structure holds the batch textures for a single camera that
// is capturing.
[System.Serializable]
public class CameraSetup {
public enum CameraType {
RGB = 0, DEPTH_NORMALS, SEGMENTATION,
}
public Camera camera_ = null;
public List<Texture2D> captured_images_ = new List<Texture2D>();
public CameraType camera_type_ = CameraType.RGB;
public string camera_name_ = null;
private int width_ = 0;
private int height_ = 0;
private int batch_size_ = 0;
private bool alpha_ = false;
public CameraSetup(Camera camera, int batch_size, int width, int height,
bool alpha, CameraType camera_type) {
width_ = width;
height_ = height;
batch_size_ = batch_size;
alpha_ = alpha;
camera_type_ = camera_type;
camera_name_ = camera.name;
camera_ = PrepareCamera(camera);
ResetBatch(camera, batch_size, width, height, alpha, camera_type);
}
// Set the current render texture.
public void UpdateRenderTexture(RenderTexture next_render_texture) {
camera_.targetTexture = next_render_texture;
}
// Issue framebuffer to texture asynchronous blit.
public RenderTexture CaptureRenderTexture(int index) {
Texture2D next_capture = captured_images_[index];
RenderTexture.active = camera_.targetTexture;
next_capture.ReadPixels(new Rect(0, 0, next_capture.width, next_capture.height), 0, 0);
return camera_.targetTexture;
}
// Commit and apply any not commited textures, this might be blocking,
// so do it at the end, when the whole batch is ready and data stalls
// would be least significant.
public List<Texture2D> ApplyAndGetImages() {
foreach (Texture2D texture in captured_images_) {
texture.Apply();
}
List<Texture2D> images = new List<Texture2D>();
for (int i = 0; i < batch_size_; ++i) {
images.Add(captured_images_[i]);
}
return images;
}
// Make sure we have enough of big enough render textures for new
// batch parameters.
public void ResetBatch(Camera camera, int batch_size, int width, int height,
bool alpha, CameraSetup.CameraType camera_type) {
if (width != width_ || height != height_ || alpha != alpha_) {
captured_images_.Clear();
width_ = width;
height_ = height;
alpha_ = alpha;
}
if (camera_type_ != camera_type || camera.name != camera_name_) {
camera_type_ = camera_type;
camera_name_ = camera.name;
camera_ = PrepareCamera(camera);
}
batch_size_ = batch_size;
if (captured_images_.Count > 2 * batch_size && batch_size >= 32) {
captured_images_.RemoveRange(batch_size, captured_images_.Count - batch_size);
return;
}
while (captured_images_.Count < batch_size) {
TextureFormat format;
if (camera_type_ == CameraType.DEPTH_NORMALS) {
format = TextureFormat.RGBAFloat;
} else {
format = alpha ? TextureFormat.RGBA32 : TextureFormat.RGB24;
}
Texture2D capture_image = new Texture2D(width, height, format, false);
captured_images_.Add(capture_image);
}
}
public void CleanUp() {
captured_images_.Clear();
batch_size_ = 0;
}
private Camera PrepareCamera(Camera camera) {
// Save original camera name since it is modified for segmentation
if (camera_type_ != CameraType.RGB) {
return CreateSegmentationCamera(camera);
} else {
return camera;
}
}
private Camera CreateSegmentationCamera(Camera camera_template) {
Camera cam = Instantiate(camera_template);
cam.name = camera_template.name + " (Segmentation)";
cam.transform.parent = camera_template.transform;
cam.transform.localPosition = Vector3.zero;
cam.transform.localRotation = Quaternion.identity;
cam.transform.localScale = Vector3.one;
Destroy(cam.GetComponent<PostProcessVolume>());
Destroy(cam.GetComponent<PostProcessLayer>());
var cb = new CommandBuffer();
switch (camera_type_) {
case CameraType.SEGMENTATION:
cb.SetGlobalFloat("_OutputMode", 0);
break;
case CameraType.DEPTH_NORMALS:
cb.SetGlobalFloat("_OutputMode", 1);
break;
default:
break;
}
cam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, cb);
cam.AddCommandBuffer(CameraEvent.BeforeFinalPass, cb);
cam.backgroundColor = Color.black;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.renderingPath = RenderingPath.Forward;
cam.SetReplacementShader(Shader.Find("Hidden/LabelShader"), "");
return cam;
}
};
public Dictionary<CameraSetup.CameraType, List<CameraSetup>> camera_setups_ = new Dictionary<CameraSetup.CameraType, List<CameraSetup>>();
// RGB (and optionally Alpha, so RGBA) textures used for appearance
public List<RenderTexture> render_textures_rgb_ = new List<RenderTexture>();
// Depth textures used for depth and surface normals
public List<RenderTexture> render_textures_depth_ = new List<RenderTexture>();
// Use this for initialization
void Start() {
Flags.InitFlags(this, "recorder");
}
public bool Initialize(IImageBatchConsumer batch_consumer) {
Screen.SetResolution(capture_width_, capture_height_, false);
Time.captureFramerate = (int)(speedup_ * capture_fps_);
Time.timeScale = speedup_;
batch_consumer_ = batch_consumer;
PrepareRenderTextures();
ResetBatch(new List<Camera>(), batch_size_, capture_width_, capture_height_, capture_alpha_,
false, false, false);
StartCoroutine(RenderCaptureHook());
Logger.Info("Recorder::Initialize::Capture ready.");
return true;
}
public void ResetBatch(IList<Camera> capture_cameras, int batch_size, int width, int height, bool alpha,
bool depth, bool normals, bool segmentation) {
HashSet<Recorder.CameraSetup.CameraType> camera_types = new HashSet<Recorder.CameraSetup.CameraType>();
camera_types.Add(Recorder.CameraSetup.CameraType.RGB);
if (depth || normals) {
camera_types.Add(Recorder.CameraSetup.CameraType.DEPTH_NORMALS);
}
if (segmentation) {
camera_types.Add(Recorder.CameraSetup.CameraType.SEGMENTATION);
}
foreach (CameraSetup.CameraType camera_type in System.Enum.GetValues(typeof(CameraSetup.CameraType))) {
if (!camera_types.Contains(camera_type)) {
camera_setups_.Remove(camera_type);
continue;
}
if (!camera_setups_.ContainsKey(camera_type)) {
camera_setups_.Add(camera_type, new List<CameraSetup>());
}
for (int i = 0; i < capture_cameras.Count; ++i) {
if (camera_setups_[camera_type].Count < i + 1) {
camera_setups_[camera_type].Add(new CameraSetup(capture_cameras[i], batch_size,
width, height, alpha, camera_type));
} else {
camera_setups_[camera_type][i].ResetBatch(capture_cameras[i], batch_size,
width, height, alpha, camera_type);
}
}
}
if (capture_width_ != width || capture_height_ != height || capture_alpha_ != alpha) {
capture_width_ = width;
capture_height_ = height;
capture_alpha_ = alpha;
PrepareRenderTextures();
}
capture_depth_ = depth;
capture_normals_ = normals;
batch_size_ = batch_size;
RoundRobinRenderTextures();
capture_count_ = 0;
capture_next_frame_ = false;
}
private void PrepareRenderTextures() {
PrepareRenderTextures(30, render_textures_rgb_, RenderTextureFormat.Default);
PrepareRenderTextures(30, render_textures_depth_, RenderTextureFormat.ARGBFloat);
}
// Prepare a pool of render textures.
private void PrepareRenderTextures(int rt_count, List<RenderTexture> texture_list, RenderTextureFormat format) {
texture_list.Clear();
for (int i = 0; i < rt_count; ++i) {
RenderTexture texture = new RenderTexture(capture_width_, capture_height_, 24,
format, RenderTextureReadWrite.sRGB);
texture.Create();
texture_list.Add(texture);
}
}
// Cycle through the cameras and the pool of render textures in a round
// robin fashion (to avoid data stalls).
private void RoundRobinRenderTextures() {
foreach (List<CameraSetup> camera_setups in camera_setups_.Values) {
foreach (CameraSetup camera_setup in camera_setups) {
camera_setup.UpdateRenderTexture(NextRenderTexture(camera_setup.camera_type_));
}
}
}
private RenderTexture NextRenderTexture(CameraSetup.CameraType camera_type) {
if (camera_type == CameraSetup.CameraType.DEPTH_NORMALS) {
render_texture_depth_index_ = (render_texture_depth_index_ + 1) % render_textures_depth_.Count;
return render_textures_depth_[render_texture_depth_index_];
} else {
render_texture_rgb_index_ = (render_texture_rgb_index_ + 1) % render_textures_rgb_.Count;
return render_textures_rgb_[render_texture_rgb_index_];
}
}
// Issue render request to all capturing cameras.
public void Capture() {
// We will be capturing this frame. Flip a flag to let the coroutine know.
capture_next_frame_ = true;
foreach (List<CameraSetup> camera_setups in camera_setups_.Values) {
foreach (CameraSetup camera_setup in camera_setups) {
camera_setup.camera_.Render();
}
}
}
// This coroutine is hooked to the event that notifies when the rendering
// (which is asynchronous) is done.
public IEnumerator RenderCaptureHook() {
while (true) {
yield return new WaitForEndOfFrame();
// Are we capturing this frame?
if (!capture_next_frame_) {
continue;
}
capture_next_frame_ = false;
CaptureRenderTextures();
RoundRobinRenderTextures();
// The whole batch is done, apply, DMA and inform the consumers.
if (capture_count_ == batch_size_) {
ProcessBatch();
}
}
}
private void CaptureRenderTextures() {
foreach (List<CameraSetup> camera_setups in camera_setups_.Values) {
foreach (CameraSetup camera_setup in camera_setups) {
last_render_ = camera_setup.CaptureRenderTexture(capture_count_);
}
}
capture_count_++;
capture_total_count_++;
}
private void ProcessBatch() {
RenderBatch batch = new RenderBatch();
foreach (List<CameraSetup> camera_setups in camera_setups_.Values) {
foreach (CameraSetup camera_setup in camera_setups) {
RenderBatch.CameraBatch camera_batch;
if (batch.camera_batches_.ContainsKey(camera_setup.camera_name_)) {
camera_batch = batch.camera_batches_[camera_setup.camera_name_];
}
else {
camera_batch = new RenderBatch.CameraBatch();
batch.camera_batches_.Add(camera_setup.camera_name_, camera_batch);
}
switch (camera_setup.camera_type_) {
case CameraSetup.CameraType.RGB:
camera_batch.images_.Add(RenderBatch.CameraBatch.RenderType.RGB, camera_setup.ApplyAndGetImages());
break;
case CameraSetup.CameraType.SEGMENTATION:
camera_batch.images_.Add(RenderBatch.CameraBatch.RenderType.SEGMENTATION, camera_setup.ApplyAndGetImages());
break;
case CameraSetup.CameraType.DEPTH_NORMALS:
List<Texture2D> images = camera_setup.ApplyAndGetImages();
if (capture_depth_) {
camera_batch.images_.Add(RenderBatch.CameraBatch.RenderType.DEPTH, images);
}
if (capture_normals_) {
camera_batch.images_.Add(RenderBatch.CameraBatch.RenderType.NORMALS, images);
}
break;
}
}
}
batch_consumer_.ConsumeImageBatch(batch);
capture_count_ = 0;
batch_count_++;
}
}