diff --git a/Moments Recorder/Scripts/Editor/RecorderEditor.cs b/Moments Recorder/Scripts/Editor/RecorderEditor.cs index bb03c7d..cf07135 100644 --- a/Moments Recorder/Scripts/Editor/RecorderEditor.cs +++ b/Moments Recorder/Scripts/Editor/RecorderEditor.cs @@ -34,6 +34,7 @@ public sealed class RecorderEditor : Editor SerializedProperty m_Width; SerializedProperty m_Height; SerializedProperty m_FramePerSecond; + SerializedProperty m_FramesPerColorSample; SerializedProperty m_Repeat; SerializedProperty m_Quality; SerializedProperty m_BufferSize; @@ -45,6 +46,7 @@ void OnEnable() m_Width = serializedObject.FindProperty("m_Width"); m_Height = serializedObject.FindProperty("m_Height"); m_FramePerSecond = serializedObject.FindProperty("m_FramePerSecond"); + m_FramesPerColorSample = serializedObject.FindProperty("m_FramesPerColorSample"); m_Repeat = serializedObject.FindProperty("m_Repeat"); m_Quality = serializedObject.FindProperty("m_Quality"); m_BufferSize = serializedObject.FindProperty("m_BufferSize"); @@ -76,6 +78,7 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(m_Quality, new GUIContent("Compression Quality", "Lower values mean better quality but slightly longer processing time. 15 is generally a good middleground value.")); EditorGUILayout.PropertyField(m_Repeat, new GUIContent("Repeat", "-1 to disable, 0 to loop indefinitely, >0 to loop a set number of time.")); EditorGUILayout.PropertyField(m_FramePerSecond, new GUIContent("Frames Per Second", "The number of frames per second the gif will run at.")); + EditorGUILayout.PropertyField(m_FramesPerColorSample, new GUIContent("Frames Per Color Sample", "Create the gif's color palette by analysing every n-th frame. Lower values mean better color representation, but much longer processing time. Higher values process faster, but may miss important colors between samples. Zero means a new color palette is created per frame.")); EditorGUILayout.PropertyField(m_BufferSize, new GUIContent("Record Time", "The amount of time (in seconds) to record to memory.")); serializedObject.ApplyModifiedProperties(); diff --git a/Moments Recorder/Scripts/Gif/GifEncoder.cs b/Moments Recorder/Scripts/Gif/GifEncoder.cs index b7a9464..8c76eea 100644 --- a/Moments Recorder/Scripts/Gif/GifEncoder.cs +++ b/Moments Recorder/Scripts/Gif/GifEncoder.cs @@ -4,10 +4,12 @@ * * Original code by Kevin Weiner, FM Software. * Adapted by Thomas Hourdel. + * Extended slightly by Paul Kopetko */ using System; using System.IO; +using System.Collections.Generic; using UnityEngine; namespace Moments.Encoder @@ -33,6 +35,9 @@ public class GifEncoder protected bool m_IsFirstFrame = true; protected bool m_IsSizeSet = false; // If false, get size from first frame protected int m_SampleInterval = 10; // Default sample interval for quantizer + protected int m_FramesPerColorSample = 6; // Default sample rate (in frames per sample) of color palette + + protected NeuQuant nq; /// /// Default constructor. Repeat will be set to -1 and Quality to 10. @@ -77,6 +82,55 @@ public void SetFrameRate(float fps) m_FrameDelay = Mathf.RoundToInt(100f / fps); } + /// + /// Sets color palette sample rate in frames per sample. + /// + /// Frame rate + public void SetFramesPerColorSample(int frames) + { + m_FramesPerColorSample = frames; + } + + /// + /// Builds a colour map out of the combined colours from several frames. + /// + /// List of frames to sample colour palette from + public void BuildPalette(ref List frames) + { + + // Do not build the color palette here if user wants separate palettes created per frame + if (m_FramesPerColorSample == 0) + { + return; + } + + // Initialize a large image + Byte[] combinedPixels = new Byte[3 * frames[0].Width * frames[0].Height * (1 + frames.Count / m_FramesPerColorSample)]; + + int count = 0; + + // Stich the large image together out of pixels from several frames + for (int i = 0; i < frames.Count; i += m_FramesPerColorSample) + { + Color32[] p = frames[i].Data; + // Texture data is layered down-top, so flip it + for (int th = frames[i].Height - 1; th >= 0; th--) + { + for (int tw = 0; tw < frames[i].Width; tw++) + { + Color32 color = p[th * frames[i].Width + tw]; + combinedPixels[count] = color.r; count++; + combinedPixels[count] = color.g; count++; + combinedPixels[count] = color.b; count++; + } + } + } + + // Run the quantizer over our stitched together image and create reduced palette + nq = new NeuQuant(combinedPixels, combinedPixels.Length, (int)m_SampleInterval); + m_ColorTab = nq.Process(); + } + /// /// Adds next GIF frame. The frame is not written immediately, but is actually deferred /// until the next frame is received so that timing data can be inserted. Invoking @@ -222,14 +276,20 @@ protected void GetImagePixels() } } - // Analyzes image colors and creates color map. + // Maps image colors to the color map protected void AnalyzePixels() { + int len = m_Pixels.Length; int nPix = len / 3; m_IndexedPixels = new byte[nPix]; - NeuQuant nq = new NeuQuant(m_Pixels, len, (int)m_SampleInterval); - m_ColorTab = nq.Process(); // Create reduced palette + + // Analyze image colors and create color map (original, expensive, Moments Recorder behaviour) + if (m_FramesPerColorSample == 0) + { + nq = new NeuQuant(m_Pixels, len, (int)m_SampleInterval); + m_ColorTab = nq.Process(); // Create reduced palette + } // Map image pixels to new palette int k = 0; diff --git a/Moments Recorder/Scripts/Recorder.cs b/Moments Recorder/Scripts/Recorder.cs index aaa1735..1bdaee9 100644 --- a/Moments Recorder/Scripts/Recorder.cs +++ b/Moments Recorder/Scripts/Recorder.cs @@ -69,6 +69,9 @@ public sealed class Recorder : MonoBehaviour [SerializeField, Min(0.1f)] float m_BufferSize = 3f; + [SerializeField, Range(0, 60)] + int m_FramesPerColorSample = 6; + #endregion #region Public fields @@ -153,7 +156,10 @@ public float EstimatedMemoryUse /// 256 colors allowed by the GIF specification). Lower values (minimum = 1) produce better /// colors, but slow processing significantly. Higher values will speed up the quantization /// pass at the cost of lower image quality (maximum = 100). - public void Setup(bool autoAspect, int width, int height, int fps, float bufferSize, int repeat, int quality) + /// Sample every n-th frame in a recording for color + /// mapping purposes. If framesPerColorSample is set to 0, Moments uses its default behaviour + /// and creates a brand new color palette every frame. + public void Setup(bool autoAspect, int width, int height, int fps, float bufferSize, int repeat, int quality, int framesPerColorSample) { if (State == RecorderState.PreProcessing) { @@ -175,6 +181,7 @@ public void Setup(bool autoAspect, int width, int height, int fps, float bufferS m_ReflectionUtils.ConstrainMin(x => x.m_BufferSize, bufferSize); m_ReflectionUtils.ConstrainMin(x => x.m_Repeat, repeat); m_ReflectionUtils.ConstrainRange(x => x.m_Quality, quality); + m_ReflectionUtils.ConstrainRange(x => x.m_FramesPerColorSample, framesPerColorSample); // Ready to go Init(); diff --git a/Moments Recorder/Scripts/Worker.cs b/Moments Recorder/Scripts/Worker.cs index 7175d8a..3efd94c 100644 --- a/Moments Recorder/Scripts/Worker.cs +++ b/Moments Recorder/Scripts/Worker.cs @@ -59,11 +59,13 @@ void Run() { m_Encoder.Start(m_FilePath); + // pass all frames to encoder to build a palette out of a subset of them + m_Encoder.BuildPalette(ref m_Frames); + for (int i = 0; i < m_Frames.Count; i++) { GifFrame frame = m_Frames[i]; m_Encoder.AddFrame(frame); - if (m_OnFileSaveProgress != null) { float percent = (float)i / (float)m_Frames.Count;