Skip to content

Commit

Permalink
Add option to run stitch command after each frame
Browse files Browse the repository at this point in the history
  • Loading branch information
mminer committed May 22, 2021
1 parent 7318257 commit 3d45c09
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 20 deletions.
56 changes: 49 additions & 7 deletions Editor/BigImageRecorder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using UnityEditor.Recorder;
using UnityEngine;

Expand All @@ -10,9 +12,11 @@ namespace UnityEditor.BigImageRecorder
/// </summary>
class BigImageRecorder : GenericRecorder<BigImageRecorderSettings>
{
public BigCameraInput Input => m_Inputs[0] as BigCameraInput;

// These are exposed so that the filename generator in the settings can read what tile we're currently writing.
public int columnBeingWritten { get; private set; }
public int rowBeingWritten { get; private set; }
public int ColumnBeingWritten { get; private set; }
public int RowBeingWritten { get; private set; }

protected override bool BeginRecording(RecordingSession session)
{
Expand All @@ -27,22 +31,50 @@ protected override bool BeginRecording(RecordingSession session)

protected override void RecordFrame(RecordingSession session)
{
WriteImageTiles(session);
var paths = WriteImageTiles(session);
RunStitchCommand(session, paths);
}

void RunStitchCommand(RecordingSession session, IReadOnlyList<string> paths)
{
if (string.IsNullOrWhiteSpace(Settings.StitchCommand))
{
return;
}

var arguments = ApplyWildcards(Settings, session, Settings.StitchCommandArguments);

var processInfo = new ProcessStartInfo(Settings.StitchCommand, arguments)
{
CreateNoWindow = true,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(paths[0]) ?? string.Empty,
};

using var process = Process.Start(processInfo);
process?.WaitForExit();

if (Settings.DeleteAfterStitching)
{
foreach (var path in paths)
{
File.Delete(path);
}
}
}

IReadOnlyList<string> WriteImageTiles(RecordingSession session)
{
var paths = new List<string>();
var input = m_Inputs[0] as BigCameraInput;

for (rowBeingWritten = 0; rowBeingWritten < input.InputSettings.RowCount; rowBeingWritten++)
for (RowBeingWritten = 0; RowBeingWritten < Input.InputSettings.RowCount; RowBeingWritten++)
{
for (columnBeingWritten = 0; columnBeingWritten < input.InputSettings.ColumnCount; columnBeingWritten++)
for (ColumnBeingWritten = 0; ColumnBeingWritten < Input.InputSettings.ColumnCount; ColumnBeingWritten++)
{
var path = Settings.FileNameGenerator.BuildAbsolutePath(session);
paths.Add(path);

var renderTexture = input.OutputRenderTextures[rowBeingWritten, columnBeingWritten];
var renderTexture = Input.OutputRenderTextures[RowBeingWritten, ColumnBeingWritten];
var texture = ConvertToTexture2D(renderTexture);
var bytes = texture.EncodeToPNG();

Expand All @@ -53,6 +85,16 @@ IReadOnlyList<string> WriteImageTiles(RecordingSession session)
return paths;
}

static string ApplyWildcards(RecorderSettings settings, RecordingSession session, string str)
{
// Danger zone: we use reflection to call a private API, which could break with a new version of Recorder.
var applyWildcardsMethod = settings.FileNameGenerator
.GetType()
.GetMethod("ApplyWildcards", BindingFlags.Instance | BindingFlags.NonPublic);

return applyWildcardsMethod.Invoke(settings.FileNameGenerator, new object[] {str, session}) as string;
}

static Texture2D ConvertToTexture2D(RenderTexture renderTexture)
{
var texture = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBA32, false);
Expand Down
25 changes: 25 additions & 0 deletions Editor/BigImageRecorderEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,35 @@ namespace UnityEditor.BigImageRecorder
[CustomEditor(typeof(BigImageRecorderSettings))]
class BigImageRecorderEditor : RecorderEditor
{
static class Styles
{
public static readonly GUIContent ArgumentsLabel = new GUIContent("Arguments");
}

protected override void FileTypeAndFormatGUI()
{
base.FileTypeAndFormatGUI();
GUILayout.Label("PNG is the only supported image format.");
}

protected override void NameAndPathGUI()
{
base.NameAndPathGUI();
var stitchCommand = serializedObject.FindProperty("stitchCommand");
var stitchCommandArguments = serializedObject.FindProperty("stitchCommandArguments");
var deleteAfterStitching = serializedObject.FindProperty("deleteAfterStitching");

EditorGUILayout.PropertyField(stitchCommand);

using (new EditorGUI.DisabledScope(string.IsNullOrWhiteSpace(stitchCommand.stringValue)))
{
using (new EditorGUI.IndentLevelScope())
{
EditorGUILayout.PropertyField(stitchCommandArguments, Styles.ArgumentsLabel);
}

EditorGUILayout.PropertyField(deleteAfterStitching);
}
}
}
}
33 changes: 29 additions & 4 deletions Editor/BigImageRecorderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,47 @@ namespace UnityEditor.BigImageRecorder
[RecorderSettings(typeof(BigImageRecorder), "Big Image Sequence", "imagesequence_16")]
class BigImageRecorderSettings : RecorderSettings
{
[SerializeField] BigImageInputSelector imageInputSelector = new BigImageInputSelector();
static readonly string columnCountWildcard = DefaultWildcard.GeneratePattern("Column Count");
static readonly string columnWildcard = DefaultWildcard.GeneratePattern("Column");
static readonly string rowCountWildcard = DefaultWildcard.GeneratePattern("Row Count");
static readonly string rowWildcard = DefaultWildcard.GeneratePattern("Row");

public bool DeleteAfterStitching => deleteAfterStitching;

[Tooltip("Whether to trash the images after running the stitch command.")]
[SerializeField] bool deleteAfterStitching = true;

protected override string Extension => "png";

[SerializeField] BigImageInputSelector imageInputSelector = new BigImageInputSelector();
public string StitchCommand => stitchCommand;

[Tooltip("Command to run after each frame is written to image tiles.")]
[SerializeField] string stitchCommand = "";

public string StitchCommandArguments => stitchCommandArguments;

[Tooltip("Arguments to pass to the stitch command.")]
[SerializeField] string stitchCommandArguments = "";

public override IEnumerable<RecorderInputSettings> InputsSettings
{
get { yield return imageInputSelector.Selected; }
}

public BigImageRecorderSettings()
{
FileNameGenerator.AddWildcard(columnWildcard, session =>
(session?.recorder as BigImageRecorder)?.columnBeingWritten.ToString() ?? "0");
FileNameGenerator.AddWildcard(rowCountWildcard, session =>
(session?.recorder as BigImageRecorder)?.Input.InputSettings.RowCount.ToString() ?? "0");

FileNameGenerator.AddWildcard(columnCountWildcard, session =>
(session?.recorder as BigImageRecorder)?.Input.InputSettings.ColumnCount.ToString() ?? "0");

FileNameGenerator.AddWildcard(rowWildcard, session =>
(session?.recorder as BigImageRecorder)?.rowBeingWritten.ToString() ?? "0");
(session?.recorder as BigImageRecorder)?.RowBeingWritten.ToString() ?? "0");

FileNameGenerator.AddWildcard(columnWildcard, session =>
(session?.recorder as BigImageRecorder)?.ColumnBeingWritten.ToString() ?? "0");

FileNameGenerator.FileName = $"image_{DefaultWildcard.Frame}_{rowWildcard}-{rowWildcard}";
}
Expand Down
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ plugin allows you to capture an image sequence at a higher resolution than the
maximum texture size. Want to render your scene at 100,000 x 100,000 pixels? You
got it friend.

It accomplishes this by dividing the camera's projection matrix and saving the
result as individual tile images for you to stitch together. At present this
stitching operation is left up to you (but see below for recommendations).
It accomplishes this by dividing the camera's projection matrix into tiles and
saving the renders as individual images to stitch together. At present this
stitching operation is left up to you (but see "Image Stitching" below for
recommendations).


## Installing
Expand All @@ -19,7 +20,8 @@ Add the package to your project via the Unity Package Manager (UPM).
3. Click "+" in the top-left corner and choose "Add package from git URL..."
4. Enter https://github.com/mminer/big-image-recorder.git

You can also clone this repository and point UPM to your local copy.
You can also clone this repository and point UPM to your local copy. You need to
go this route if you want to modify the source code.


## Using
Expand All @@ -29,11 +31,22 @@ You can also clone this repository and point UPM to your local copy.
3. Enter the tag of your target camera (or keep the default to use your main camera)
4. Enter your desired output size, number of rows and columns, and start recording

Recorder spits out multiple images per frame, one for each "tile". By default
these are named *image_FRAME_ROW-COLUMN.png*, e.g. *image_0003_1-1.png*.

### Image Stitching

The recorder plugin spits out multiple images per frame, one for each "tile". By
default these are named *image_FRAME_ROW-COLUMN.png*, e.g. *image_0003_1-1.png*.
One option to stitch these together into a final image is
[ImageMagick](https://imagemagick.org).
[ImageMagick](https://imagemagick.org) offers one option to stitch the image
tiles together into a final image.

# Stitches together a frame of two rows and two columns.
montage -mode concatenate -tile 2x2 *.png out.png

To run this automatically, enter the absolute path to the executable in the
"Stitch Command" field and its arguments (i.e. `-mode ...`) in "Arguments". For
example, if you install ImageMagick on macOS using Homebrew, enter
`/usr/local/bin/montage` as the command.

montage -mode concatenate -tile 2x2 image_0000_*.png out.png
The arguments can contain the same wildcards as the file name. Use these to
specify the frame, row count, and column count, e.g. `-mode concatenate -tile
<Row Count>x<Column Count> image_<Frame>*.png out_<Frame>.png`.

0 comments on commit 3d45c09

Please sign in to comment.