├── .gitignore ├── LICENSE.txt ├── Moments Recorder ├── Demo.meta ├── Demo │ ├── Demo.unity │ ├── Demo.unity.meta │ ├── Materials.meta │ ├── Materials │ │ ├── Cube.mat │ │ ├── Cube.mat.meta │ │ ├── Floor.mat │ │ └── Floor.mat.meta │ ├── Prefabs.meta │ ├── Prefabs │ │ ├── Cube.prefab │ │ └── Cube.prefab.meta │ ├── Scripts.meta │ └── Scripts │ │ ├── CubeKiller.cs │ │ ├── CubeKiller.cs.meta │ │ ├── CubeSpawner.cs │ │ ├── CubeSpawner.cs.meta │ │ ├── Record.cs │ │ └── Record.cs.meta ├── Scripts.meta └── Scripts │ ├── Editor.meta │ ├── Editor │ ├── MinDrawer.cs │ ├── MinDrawer.cs.meta │ ├── RecorderEditor.cs │ └── RecorderEditor.cs.meta │ ├── Gif.meta │ ├── Gif │ ├── GifEncoder.cs │ ├── GifEncoder.cs.meta │ ├── GifFrame.cs │ ├── GifFrame.cs.meta │ ├── LzwEncoder.cs │ ├── LzwEncoder.cs.meta │ ├── NeuQuant.cs │ └── NeuQuant.cs.meta │ ├── MinAttribute.cs │ ├── MinAttribute.cs.meta │ ├── Recorder.cs │ ├── Recorder.cs.meta │ ├── ReflectionUtils.cs │ ├── ReflectionUtils.cs.meta │ ├── Worker.cs │ └── Worker.cs.meta └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # =============== # 2 | # Unity generated # 3 | # =============== # 4 | [Tt]emp/ 5 | [Oo]bj/ 6 | [Bb]uild 7 | [Ll]ibrary/ 8 | sysinfo.txt 9 | LICENSE.txt.meta 10 | README.md.meta 11 | Moments Recorder.meta 12 | 13 | # ===================================== # 14 | # Visual Studio / MonoDevelop generated # 15 | # ===================================== # 16 | [Ee]xported[Oo]bj/ 17 | /*.userprefs 18 | /*.csproj 19 | /*.pidb 20 | /*.suo 21 | /*.sln* 22 | /*.user 23 | /*.unityproj 24 | /*.booproj 25 | 26 | # ============ # 27 | # OS generated # 28 | # ============ # 29 | .DS_Store* 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | Icon? 34 | ehthumbs.db 35 | [Tt]humbs.db -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Thomas Hourdel 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source 20 | distribution. 21 | -------------------------------------------------------------------------------- /Moments Recorder/Demo.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6a663f1db9e844643bf190330d309caf 3 | folderAsset: yes 4 | timeCreated: 1429374477 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Demo.unity: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chman/Moments/3b8a876bfb6edec9ea260349693252818914ca92/Moments Recorder/Demo/Demo.unity -------------------------------------------------------------------------------- /Moments Recorder/Demo/Demo.unity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cbac2e477c5f6b49b1577b6aa323b57 3 | timeCreated: 1429374657 4 | licenseType: Free 5 | DefaultImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Materials.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7d648e0117f099f48ae5ade1a8f93770 3 | folderAsset: yes 4 | timeCreated: 1429374860 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Materials/Cube.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chman/Moments/3b8a876bfb6edec9ea260349693252818914ca92/Moments Recorder/Demo/Materials/Cube.mat -------------------------------------------------------------------------------- /Moments Recorder/Demo/Materials/Cube.mat.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2603c640b645e534996bfbc75a1f3999 3 | timeCreated: 1429374797 4 | licenseType: Free 5 | NativeFormatImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Materials/Floor.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chman/Moments/3b8a876bfb6edec9ea260349693252818914ca92/Moments Recorder/Demo/Materials/Floor.mat -------------------------------------------------------------------------------- /Moments Recorder/Demo/Materials/Floor.mat.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8ea628181e9be7e4f80fda8ea1aa7721 3 | timeCreated: 1429374801 4 | licenseType: Free 5 | NativeFormatImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Prefabs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 630e07d25695d4748bd66dd90788cdf5 3 | folderAsset: yes 4 | timeCreated: 1429374866 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Prefabs/Cube.prefab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chman/Moments/3b8a876bfb6edec9ea260349693252818914ca92/Moments Recorder/Demo/Prefabs/Cube.prefab -------------------------------------------------------------------------------- /Moments Recorder/Demo/Prefabs/Cube.prefab.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cc044cb3cafa06f4999e18d253e31e9c 3 | timeCreated: 1429374831 4 | licenseType: Free 5 | NativeFormatImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5ca951baf8eb4fb4296620b042096350 3 | folderAsset: yes 4 | timeCreated: 1429374870 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Scripts/CubeKiller.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | [AddComponentMenu("")] 4 | public class CubeKiller : MonoBehaviour 5 | { 6 | void OnTriggerEnter(Collider other) 7 | { 8 | Destroy(other.gameObject); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Scripts/CubeKiller.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0584be45a828514448cff5acaeff6540 3 | timeCreated: 1429374683 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Scripts/CubeSpawner.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | 4 | [AddComponentMenu("")] 5 | public class CubeSpawner : MonoBehaviour 6 | { 7 | public Transform CubePrefab; 8 | public float Interval = 0.5f; 9 | 10 | void Start() 11 | { 12 | if (CubePrefab == null) 13 | return; 14 | 15 | StartCoroutine(SpawnCube()); 16 | } 17 | 18 | IEnumerator SpawnCube() 19 | { 20 | float timer = 0f; 21 | 22 | while (true) 23 | { 24 | timer += Time.deltaTime; 25 | 26 | if (timer > Interval) 27 | { 28 | timer = 0f; 29 | Instantiate(CubePrefab, transform.position, Random.rotationUniform); 30 | } 31 | 32 | yield return null; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Scripts/CubeSpawner.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 300789b6350254b4cbc13f8fbbcb783d 3 | timeCreated: 1429374755 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Scripts/Record.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using Moments; 3 | 4 | [RequireComponent(typeof(Recorder)), AddComponentMenu("")] 5 | public class Record : MonoBehaviour 6 | { 7 | Recorder m_Recorder; 8 | float m_Progress = 0f; 9 | string m_LastFile = ""; 10 | bool m_IsSaving = false; 11 | 12 | void Start() 13 | { 14 | // Get our Recorder instance (there can be only one per camera). 15 | m_Recorder = GetComponent(); 16 | 17 | // If you want to change Recorder settings at runtime, use : 18 | //m_Recorder.Setup(autoAspect, width, height, fps, bufferSize, repeat, quality); 19 | 20 | // The Recorder starts paused for performance reasons, call Record() to start 21 | // saving frames to memory. You can pause it at any time by calling Pause(). 22 | m_Recorder.Record(); 23 | 24 | // Optional callbacks (see each function for more info). 25 | m_Recorder.OnPreProcessingDone = OnProcessingDone; 26 | m_Recorder.OnFileSaveProgress = OnFileSaveProgress; 27 | m_Recorder.OnFileSaved = OnFileSaved; 28 | } 29 | 30 | void OnProcessingDone() 31 | { 32 | // All frames have been extracted and sent to a worker thread for compression ! 33 | // The Recorder is ready to record again, you can call Record() here if you don't 34 | // want to wait for the file to be compresse and saved. 35 | // Pre-processing is done in the main thread, but frame compression & file saving 36 | // has its own thread, so you can save multiple gif at once. 37 | 38 | m_IsSaving = true; 39 | } 40 | 41 | void OnFileSaveProgress(int id, float percent) 42 | { 43 | // This callback is probably not thread safe so use it at your own risks. 44 | // Percent is in range [0;1] (0 being 0%, 1 being 100%). 45 | m_Progress = percent * 100f; 46 | } 47 | 48 | void OnFileSaved(int id, string filepath) 49 | { 50 | // Our file has successfully been compressed & written to disk ! 51 | m_LastFile = filepath; 52 | 53 | m_IsSaving = false; 54 | 55 | // Let's start recording again (note that we could do that as soon as pre-processing 56 | // is done and actually save multiple gifs at once, see OnProcessingDone(). 57 | m_Recorder.Record(); 58 | } 59 | 60 | void OnDestroy() 61 | { 62 | // Memory is automatically flushed when the Recorder is destroyed or (re)setup, 63 | // but if for some reason you want to do it manually, just call FlushMemory(). 64 | //m_Recorder.FlushMemory(); 65 | } 66 | 67 | void Update() 68 | { 69 | if (Input.GetKeyDown(KeyCode.Space)) 70 | { 71 | // Compress & save the buffered frames to a gif file. We should check the State 72 | // of the Recorder before saving, but for the sake of this example we won't, so 73 | // you'll see a warning in the console if you try saving while the Recorder is 74 | // processing another gif. 75 | m_Recorder.Save(); 76 | m_Progress = 0f; 77 | } 78 | } 79 | 80 | void OnGUI() 81 | { 82 | GUILayout.BeginHorizontal(); 83 | GUILayout.Space(10f); 84 | GUILayout.BeginVertical(); 85 | 86 | GUILayout.Space(10f); 87 | GUILayout.Label("Press [SPACE] to export the buffered frames to a gif file."); 88 | GUILayout.Label("Recorder State : " + m_Recorder.State.ToString()); 89 | 90 | if (m_IsSaving) 91 | GUILayout.Label("Progress Report : " + m_Progress.ToString("F2") + "%"); 92 | 93 | if (!string.IsNullOrEmpty(m_LastFile)) 94 | GUILayout.Label("Last File Saved : " + m_LastFile); 95 | 96 | GUILayout.EndVertical(); 97 | GUILayout.EndHorizontal(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Moments Recorder/Demo/Scripts/Record.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3729fda5a6290194ba951a6cf75459e5 3 | timeCreated: 1429375769 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2f7b85b2af9f9e0438b022d1469371a6 3 | folderAsset: yes 4 | timeCreated: 1429343823 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 725f96481eb874b43a223152132ce12b 3 | folderAsset: yes 4 | timeCreated: 1429343902 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Editor/MinDrawer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Thomas Hourdel 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | using UnityEngine; 25 | using UnityEditor; 26 | using Moments; 27 | 28 | namespace MomentsEditor 29 | { 30 | [CustomPropertyDrawer(typeof(MinAttribute))] 31 | internal sealed class MinDrawer : PropertyDrawer 32 | { 33 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 34 | { 35 | MinAttribute attribute = (MinAttribute)base.attribute; 36 | 37 | if (property.propertyType == SerializedPropertyType.Integer) 38 | { 39 | int v = EditorGUI.IntField(position, label, property.intValue); 40 | property.intValue = (int)Mathf.Max(v, attribute.min); 41 | } 42 | else if (property.propertyType == SerializedPropertyType.Float) 43 | { 44 | float v = EditorGUI.FloatField(position, label, property.floatValue); 45 | property.floatValue = Mathf.Max(v, attribute.min); 46 | } 47 | else 48 | { 49 | EditorGUI.LabelField(position, label.text, "Use Min with float or int."); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Editor/MinDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aed1c286be7441e4fb1e81c0058fcc3f 3 | timeCreated: 1429351220 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Editor/RecorderEditor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Thomas Hourdel 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | using UnityEngine; 25 | using UnityEditor; 26 | using Moments; 27 | 28 | namespace MomentsEditor 29 | { 30 | [CustomEditor(typeof(Recorder))] 31 | public sealed class RecorderEditor : Editor 32 | { 33 | SerializedProperty m_AutoAspect; 34 | SerializedProperty m_Width; 35 | SerializedProperty m_Height; 36 | SerializedProperty m_FramePerSecond; 37 | SerializedProperty m_Repeat; 38 | SerializedProperty m_Quality; 39 | SerializedProperty m_BufferSize; 40 | SerializedProperty m_WorkerPriority; 41 | 42 | void OnEnable() 43 | { 44 | m_AutoAspect = serializedObject.FindProperty("m_AutoAspect"); 45 | m_Width = serializedObject.FindProperty("m_Width"); 46 | m_Height = serializedObject.FindProperty("m_Height"); 47 | m_FramePerSecond = serializedObject.FindProperty("m_FramePerSecond"); 48 | m_Repeat = serializedObject.FindProperty("m_Repeat"); 49 | m_Quality = serializedObject.FindProperty("m_Quality"); 50 | m_BufferSize = serializedObject.FindProperty("m_BufferSize"); 51 | m_WorkerPriority = serializedObject.FindProperty("WorkerPriority"); 52 | } 53 | 54 | public override void OnInspectorGUI() 55 | { 56 | Recorder recorder = (Recorder)target; 57 | 58 | EditorGUILayout.HelpBox("This inspector is only used to tweak default values for the component. To change values at runtime, use the Setup() method.", MessageType.Info); 59 | 60 | // Don't let the user tweak settings while playing as it may break everything 61 | if (Application.isEditor && Application.isPlaying) 62 | GUI.enabled = false; 63 | 64 | serializedObject.Update(); 65 | 66 | // Hooray for propertie drawers ! 67 | EditorGUILayout.PropertyField(m_AutoAspect, new GUIContent("Automatic Height", "Automatically compute height from the current aspect ratio.")); 68 | EditorGUILayout.PropertyField(m_Width, new GUIContent("Width", "Output gif width in pixels.")); 69 | 70 | if (!m_AutoAspect.boolValue) 71 | EditorGUILayout.PropertyField(m_Height, new GUIContent("Height", "Output gif height in pixels.")); 72 | else 73 | EditorGUILayout.LabelField(new GUIContent("Height", "Output gif height in pixels."), new GUIContent(m_Height.intValue.ToString())); 74 | 75 | EditorGUILayout.PropertyField(m_WorkerPriority, new GUIContent("Worker Thread Priority", "Thread priority to use when processing frames to a gif file.")); 76 | 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.")); 77 | EditorGUILayout.PropertyField(m_Repeat, new GUIContent("Repeat", "-1 to disable, 0 to loop indefinitely, >0 to loop a set number of time.")); 78 | EditorGUILayout.PropertyField(m_FramePerSecond, new GUIContent("Frames Per Second", "The number of frames per second the gif will run at.")); 79 | EditorGUILayout.PropertyField(m_BufferSize, new GUIContent("Record Time", "The amount of time (in seconds) to record to memory.")); 80 | 81 | serializedObject.ApplyModifiedProperties(); 82 | 83 | GUI.enabled = true; 84 | 85 | recorder.ComputeHeight(); 86 | EditorGUILayout.LabelField("Estimated VRam Usage", recorder.EstimatedMemoryUse.ToString("F3") + " MB"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Editor/RecorderEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 068b3a813633f6142900d10b6fc24cc7 3 | timeCreated: 1429343908 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7a755c66a6dbb5441b5e17b95a7b805b 3 | folderAsset: yes 4 | timeCreated: 1429343831 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/GifEncoder.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * No copyright asserted on the source code of this class. May be used 3 | * for any purpose. 4 | * 5 | * Original code by Kevin Weiner, FM Software. 6 | * Adapted by Thomas Hourdel. 7 | */ 8 | 9 | using System; 10 | using System.IO; 11 | using UnityEngine; 12 | 13 | namespace Moments.Encoder 14 | { 15 | public class GifEncoder 16 | { 17 | protected int m_Width; 18 | protected int m_Height; 19 | protected int m_Repeat = -1; // -1: no repeat, 0: infinite, >0: repeat count 20 | protected int m_FrameDelay = 0; // Frame delay (milliseconds) 21 | protected bool m_HasStarted = false; // Ready to output frames 22 | protected FileStream m_FileStream; 23 | 24 | protected GifFrame m_CurrentFrame; 25 | protected byte[] m_Pixels; // BGR byte array from frame 26 | protected byte[] m_IndexedPixels; // Converted frame indexed to palette 27 | protected int m_ColorDepth; // Number of bit planes 28 | protected byte[] m_ColorTab; // RGB palette 29 | protected bool[] m_UsedEntry = new bool[256]; // Active palette entries 30 | protected int m_PaletteSize = 7; // Color table size (bits-1) 31 | protected int m_DisposalCode = -1; // Disposal code (-1 = use default) 32 | protected bool m_ShouldCloseStream = false; // Close stream when finished 33 | protected bool m_IsFirstFrame = true; 34 | protected bool m_IsSizeSet = false; // If false, get size from first frame 35 | protected int m_SampleInterval = 10; // Default sample interval for quantizer 36 | 37 | /// 38 | /// Default constructor. Repeat will be set to -1 and Quality to 10. 39 | /// 40 | public GifEncoder() : this(-1, 10) 41 | { 42 | } 43 | 44 | /// 45 | /// Constructor with the number of times the set of GIF frames should be played. 46 | /// 47 | /// Default is -1 (no repeat); 0 means play indefinitely 48 | /// Sets quality of color quantization (conversion of images to 49 | /// the maximum 256 colors allowed by the GIF specification). Lower values (minimum = 1) 50 | /// produce better colors, but slow processing significantly. Higher values will speed 51 | /// up the quantization pass at the cost of lower image quality (maximum = 100). 52 | public GifEncoder(int repeat, int quality) 53 | { 54 | if (repeat >= 0) 55 | m_Repeat = repeat; 56 | 57 | m_SampleInterval = (int)Mathf.Clamp(quality, 1, 100); 58 | } 59 | 60 | /// 61 | /// Sets the delay time between each frame, or changes it for subsequent frames (applies 62 | /// to last frame added). 63 | /// 64 | /// Delay time in milliseconds 65 | public void SetDelay(int ms) 66 | { 67 | m_FrameDelay = Mathf.RoundToInt(ms / 10f); 68 | } 69 | 70 | /// 71 | /// Sets frame rate in frames per second. Equivalent to SetDelay(1000/fps). 72 | /// 73 | /// Frame rate 74 | public void SetFrameRate(float fps) 75 | { 76 | if (fps > 0f) 77 | m_FrameDelay = Mathf.RoundToInt(100f / fps); 78 | } 79 | 80 | /// 81 | /// Adds next GIF frame. The frame is not written immediately, but is actually deferred 82 | /// until the next frame is received so that timing data can be inserted. Invoking 83 | /// Finish() flushes all frames. 84 | /// 85 | /// GifFrame containing frame to write. 86 | public void AddFrame(GifFrame frame) 87 | { 88 | if ((frame == null)) 89 | throw new ArgumentNullException("Can't add a null frame to the gif."); 90 | 91 | if (!m_HasStarted) 92 | throw new InvalidOperationException("Call Start() before adding frames to the gif."); 93 | 94 | // Use first frame's size 95 | if (!m_IsSizeSet) 96 | SetSize(frame.Width, frame.Height); 97 | 98 | m_CurrentFrame = frame; 99 | GetImagePixels(); 100 | AnalyzePixels(); 101 | 102 | if (m_IsFirstFrame) 103 | { 104 | WriteLSD(); 105 | WritePalette(); 106 | 107 | if (m_Repeat >= 0) 108 | WriteNetscapeExt(); 109 | } 110 | 111 | WriteGraphicCtrlExt(); 112 | WriteImageDesc(); 113 | 114 | if (!m_IsFirstFrame) 115 | WritePalette(); 116 | 117 | WritePixels(); 118 | m_IsFirstFrame = false; 119 | } 120 | 121 | /// 122 | /// Initiates GIF file creation on the given stream. The stream is not closed automatically. 123 | /// 124 | /// OutputStream on which GIF images are written 125 | public void Start(FileStream os) 126 | { 127 | if (os == null) 128 | throw new ArgumentNullException("Stream is null."); 129 | 130 | m_ShouldCloseStream = false; 131 | m_FileStream = os; 132 | 133 | try 134 | { 135 | WriteString("GIF89a"); // header 136 | } 137 | catch (IOException e) 138 | { 139 | throw e; 140 | } 141 | 142 | m_HasStarted = true; 143 | } 144 | 145 | /// 146 | /// Initiates writing of a GIF file with the specified name. The stream will be handled for you. 147 | /// 148 | /// String containing output file name 149 | public void Start(String file) 150 | { 151 | try 152 | { 153 | m_FileStream = new FileStream(file, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); 154 | Start(m_FileStream); 155 | m_ShouldCloseStream = true; 156 | } 157 | catch (IOException e) 158 | { 159 | throw e; 160 | } 161 | } 162 | 163 | /// 164 | /// Flushes any pending data and closes output file. 165 | /// If writing to an OutputStream, the stream is not closed. 166 | /// 167 | public void Finish() 168 | { 169 | if (!m_HasStarted) 170 | throw new InvalidOperationException("Can't finish a non-started gif."); 171 | 172 | m_HasStarted = false; 173 | 174 | try 175 | { 176 | m_FileStream.WriteByte(0x3b); // Gif trailer 177 | m_FileStream.Flush(); 178 | 179 | if (m_ShouldCloseStream) 180 | m_FileStream.Close(); 181 | } 182 | catch (IOException e) 183 | { 184 | throw e; 185 | } 186 | 187 | // Reset for subsequent use 188 | m_FileStream = null; 189 | m_CurrentFrame = null; 190 | m_Pixels = null; 191 | m_IndexedPixels = null; 192 | m_ColorTab = null; 193 | m_ShouldCloseStream = false; 194 | m_IsFirstFrame = true; 195 | } 196 | 197 | // Sets the GIF frame size. 198 | protected void SetSize(int w, int h) 199 | { 200 | m_Width = w; 201 | m_Height = h; 202 | m_IsSizeSet = true; 203 | } 204 | 205 | // Extracts image pixels into byte array "pixels". 206 | protected void GetImagePixels() 207 | { 208 | m_Pixels = new Byte[3 * m_CurrentFrame.Width * m_CurrentFrame.Height]; 209 | Color32[] p = m_CurrentFrame.Data; 210 | int count = 0; 211 | 212 | // Texture data is layered down-top, so flip it 213 | for (int th = m_CurrentFrame.Height - 1; th >= 0; th--) 214 | { 215 | for (int tw = 0; tw < m_CurrentFrame.Width; tw++) 216 | { 217 | Color32 color = p[th * m_CurrentFrame.Width + tw]; 218 | m_Pixels[count] = color.r; count++; 219 | m_Pixels[count] = color.g; count++; 220 | m_Pixels[count] = color.b; count++; 221 | } 222 | } 223 | } 224 | 225 | // Analyzes image colors and creates color map. 226 | protected void AnalyzePixels() 227 | { 228 | int len = m_Pixels.Length; 229 | int nPix = len / 3; 230 | m_IndexedPixels = new byte[nPix]; 231 | NeuQuant nq = new NeuQuant(m_Pixels, len, (int)m_SampleInterval); 232 | m_ColorTab = nq.Process(); // Create reduced palette 233 | 234 | // Map image pixels to new palette 235 | int k = 0; 236 | for (int i = 0; i < nPix; i++) 237 | { 238 | int index = nq.Map(m_Pixels[k++] & 0xff, m_Pixels[k++] & 0xff, m_Pixels[k++] & 0xff); 239 | m_UsedEntry[index] = true; 240 | m_IndexedPixels[i] = (byte)index; 241 | } 242 | 243 | m_Pixels = null; 244 | m_ColorDepth = 8; 245 | m_PaletteSize = 7; 246 | } 247 | 248 | // Writes Graphic Control Extension. 249 | protected void WriteGraphicCtrlExt() 250 | { 251 | m_FileStream.WriteByte(0x21); // Extension introducer 252 | m_FileStream.WriteByte(0xf9); // GCE label 253 | m_FileStream.WriteByte(4); // Data block size 254 | 255 | // Packed fields 256 | m_FileStream.WriteByte(Convert.ToByte(0 | // 1:3 reserved 257 | 0 | // 4:6 disposal 258 | 0 | // 7 user input - 0 = none 259 | 0)); // 8 transparency flag 260 | 261 | WriteShort(m_FrameDelay); // Delay x 1/100 sec 262 | m_FileStream.WriteByte(Convert.ToByte(0)); // Transparent color index 263 | m_FileStream.WriteByte(0); // Block terminator 264 | } 265 | 266 | // Writes Image Descriptor. 267 | protected void WriteImageDesc() 268 | { 269 | m_FileStream.WriteByte(0x2c); // Image separator 270 | WriteShort(0); // Image position x,y = 0,0 271 | WriteShort(0); 272 | WriteShort(m_Width); // image size 273 | WriteShort(m_Height); 274 | 275 | // Packed fields 276 | if (m_IsFirstFrame) 277 | { 278 | m_FileStream.WriteByte(0); // No LCT - GCT is used for first (or only) frame 279 | } 280 | else 281 | { 282 | // Specify normal LCT 283 | m_FileStream.WriteByte(Convert.ToByte(0x80 | // 1 local color table 1=yes 284 | 0 | // 2 interlace - 0=no 285 | 0 | // 3 sorted - 0=no 286 | 0 | // 4-5 reserved 287 | m_PaletteSize)); // 6-8 size of color table 288 | } 289 | } 290 | 291 | // Writes Logical Screen Descriptor. 292 | protected void WriteLSD() 293 | { 294 | // Logical screen size 295 | WriteShort(m_Width); 296 | WriteShort(m_Height); 297 | 298 | // Packed fields 299 | m_FileStream.WriteByte(Convert.ToByte(0x80 | // 1 : global color table flag = 1 (gct used) 300 | 0x70 | // 2-4 : color resolution = 7 301 | 0x00 | // 5 : gct sort flag = 0 302 | m_PaletteSize)); // 6-8 : gct size 303 | 304 | m_FileStream.WriteByte(0); // Background color index 305 | m_FileStream.WriteByte(0); // Pixel aspect ratio - assume 1:1 306 | } 307 | 308 | // Writes Netscape application extension to define repeat count. 309 | protected void WriteNetscapeExt() 310 | { 311 | m_FileStream.WriteByte(0x21); // Extension introducer 312 | m_FileStream.WriteByte(0xff); // App extension label 313 | m_FileStream.WriteByte(11); // Block size 314 | WriteString("NETSCAPE" + "2.0"); // App id + auth code 315 | m_FileStream.WriteByte(3); // Sub-block size 316 | m_FileStream.WriteByte(1); // Loop sub-block id 317 | WriteShort(m_Repeat); // Loop count (extra iterations, 0=repeat forever) 318 | m_FileStream.WriteByte(0); // Block terminator 319 | } 320 | 321 | // Write color table. 322 | protected void WritePalette() 323 | { 324 | m_FileStream.Write(m_ColorTab, 0, m_ColorTab.Length); 325 | int n = (3 * 256) - m_ColorTab.Length; 326 | 327 | for (int i = 0; i < n; i++) 328 | m_FileStream.WriteByte(0); 329 | } 330 | 331 | // Encodes and writes pixel data. 332 | protected void WritePixels() 333 | { 334 | LzwEncoder encoder = new LzwEncoder(m_Width, m_Height, m_IndexedPixels, m_ColorDepth); 335 | encoder.Encode(m_FileStream); 336 | } 337 | 338 | // Write 16-bit value to output stream, LSB first. 339 | protected void WriteShort(int value) 340 | { 341 | m_FileStream.WriteByte(Convert.ToByte(value & 0xff)); 342 | m_FileStream.WriteByte(Convert.ToByte((value >> 8) & 0xff)); 343 | } 344 | 345 | // Writes string to output stream. 346 | protected void WriteString(String s) 347 | { 348 | char[] chars = s.ToCharArray(); 349 | 350 | for (int i = 0; i < chars.Length; i++) 351 | m_FileStream.WriteByte((byte)chars[i]); 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/GifEncoder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a7303ea6ac5f84c418cc4f429d0d5363 3 | timeCreated: 1429343857 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/GifFrame.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Thomas Hourdel 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | using UnityEngine; 25 | 26 | namespace Moments.Encoder 27 | { 28 | public class GifFrame 29 | { 30 | public int Width; 31 | public int Height; 32 | public Color32[] Data; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/GifFrame.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 73c6f009af1d68a49803bc21d2820a81 3 | timeCreated: 1429343874 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/LzwEncoder.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * No copyright asserted on the source code of this class. May be used 3 | * for any purpose, however, refer to the Unisys LZW patent for restrictions 4 | * on use of the associated LZWEncoder class : 5 | * 6 | * The Unisys patent expired on 20 June 2003 in the USA, in Europe it expired 7 | * on 18 June 2004, in Japan the patent expired on 20 June 2004 and in Canada 8 | * it expired on 7 July 2004. The U.S. IBM patent expired 11 August 2006, The 9 | * Software Freedom Law Center says that after 1 October 2006, there will be 10 | * no significant patent claims interfering with employment of the GIF format. 11 | * 12 | * Original code by Kevin Weiner, FM Software. 13 | * Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. 14 | */ 15 | 16 | using System; 17 | using System.IO; 18 | 19 | namespace Moments.Encoder 20 | { 21 | public class LzwEncoder 22 | { 23 | private static readonly int EOF = -1; 24 | 25 | private byte[] pixAry; 26 | private int initCodeSize; 27 | private int curPixel; 28 | 29 | // GIFCOMPR.C - GIF Image compression routines 30 | // 31 | // Lempel-Ziv compression based on 'compress'. GIF modifications by 32 | // David Rowley (mgardi@watdcsu.waterloo.edu) 33 | 34 | // General DEFINEs 35 | 36 | static readonly int BITS = 12; 37 | 38 | static readonly int HSIZE = 5003; // 80% occupancy 39 | 40 | // GIF Image compression - modified 'compress' 41 | // 42 | // Based on: compress.c - File compression ala IEEE Computer, June 1984. 43 | // 44 | // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) 45 | // Jim McKie (decvax!mcvax!jim) 46 | // Steve Davies (decvax!vax135!petsd!peora!srd) 47 | // Ken Turkowski (decvax!decwrl!turtlevax!ken) 48 | // James A. Woods (decvax!ihnp4!ames!jaw) 49 | // Joe Orost (decvax!vax135!petsd!joe) 50 | 51 | int n_bits; // number of bits/code 52 | int maxbits = BITS; // user settable max # bits/code 53 | int maxcode; // maximum code, given n_bits 54 | int maxmaxcode = 1 << BITS; // should NEVER generate this code 55 | 56 | int[] htab = new int[HSIZE]; 57 | int[] codetab = new int[HSIZE]; 58 | 59 | int hsize = HSIZE; // for dynamic table sizing 60 | 61 | int free_ent = 0; // first unused entry 62 | 63 | // block compression parameters -- after all codes are used up, 64 | // and compression rate changes, start over. 65 | bool clear_flg = false; 66 | 67 | // Algorithm: use open addressing double hashing (no chaining) on the 68 | // prefix code / next character combination. We do a variant of Knuth's 69 | // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime 70 | // secondary probe. Here, the modular division first probe is gives way 71 | // to a faster exclusive-or manipulation. Also do block compression with 72 | // an adaptive reset, whereby the code table is cleared when the compression 73 | // ratio decreases, but after the table fills. The variable-length output 74 | // codes are re-sized at this point, and a special CLEAR code is generated 75 | // for the decompressor. Late addition: construct the table according to 76 | // file size for noticeable speed improvement on small files. Please direct 77 | // questions about this implementation to ames!jaw. 78 | 79 | int g_init_bits; 80 | 81 | int ClearCode; 82 | int EOFCode; 83 | 84 | // output 85 | // 86 | // Output the given code. 87 | // Inputs: 88 | // code: A n_bits-bit integer. If == -1, then EOF. This assumes 89 | // that n_bits =< wordsize - 1. 90 | // Outputs: 91 | // Outputs code to the file. 92 | // Assumptions: 93 | // Chars are 8 bits long. 94 | // Algorithm: 95 | // Maintain a BITS character long buffer (so that 8 codes will 96 | // fit in it exactly). Use the VAX insv instruction to insert each 97 | // code in turn. When the buffer fills up empty it and start over. 98 | 99 | int cur_accum = 0; 100 | int cur_bits = 0; 101 | 102 | int [] masks = 103 | { 104 | 0x0000, 105 | 0x0001, 106 | 0x0003, 107 | 0x0007, 108 | 0x000F, 109 | 0x001F, 110 | 0x003F, 111 | 0x007F, 112 | 0x00FF, 113 | 0x01FF, 114 | 0x03FF, 115 | 0x07FF, 116 | 0x0FFF, 117 | 0x1FFF, 118 | 0x3FFF, 119 | 0x7FFF, 120 | 0xFFFF }; 121 | 122 | // Number of characters so far in this 'packet' 123 | int a_count; 124 | 125 | // Define the storage for the packet accumulator 126 | byte[] accum = new byte[256]; 127 | 128 | //---------------------------------------------------------------------------- 129 | public LzwEncoder(int width, int height, byte[] pixels, int color_depth) 130 | { 131 | pixAry = pixels; 132 | initCodeSize = Math.Max(2, color_depth); 133 | } 134 | 135 | // Add a character to the end of the current packet, and if it is 254 136 | // characters, flush the packet to disk. 137 | void Add(byte c, Stream outs) 138 | { 139 | accum[a_count++] = c; 140 | if (a_count >= 254) 141 | Flush(outs); 142 | } 143 | 144 | // Clear out the hash table 145 | 146 | // table clear for block compress 147 | void ClearTable(Stream outs) 148 | { 149 | ResetCodeTable(hsize); 150 | free_ent = ClearCode + 2; 151 | clear_flg = true; 152 | 153 | Output(ClearCode, outs); 154 | } 155 | 156 | // reset code table 157 | void ResetCodeTable(int hsize) 158 | { 159 | for (int i = 0; i < hsize; ++i) 160 | htab[i] = -1; 161 | } 162 | 163 | void Compress(int init_bits, Stream outs) 164 | { 165 | int fcode; 166 | int i /* = 0 */; 167 | int c; 168 | int ent; 169 | int disp; 170 | int hsize_reg; 171 | int hshift; 172 | 173 | // Set up the globals: g_init_bits - initial number of bits 174 | g_init_bits = init_bits; 175 | 176 | // Set up the necessary values 177 | clear_flg = false; 178 | n_bits = g_init_bits; 179 | maxcode = MaxCode(n_bits); 180 | 181 | ClearCode = 1 << (init_bits - 1); 182 | EOFCode = ClearCode + 1; 183 | free_ent = ClearCode + 2; 184 | 185 | a_count = 0; // clear packet 186 | 187 | ent = NextPixel(); 188 | 189 | hshift = 0; 190 | for (fcode = hsize; fcode < 65536; fcode *= 2) 191 | ++hshift; 192 | hshift = 8 - hshift; // set hash code range bound 193 | 194 | hsize_reg = hsize; 195 | ResetCodeTable(hsize_reg); // clear hash table 196 | 197 | Output(ClearCode, outs); 198 | 199 | outer_loop : while ((c = NextPixel()) != EOF) 200 | { 201 | fcode = (c << maxbits) + ent; 202 | i = (c << hshift) ^ ent; // xor hashing 203 | 204 | if (htab[i] == fcode) 205 | { 206 | ent = codetab[i]; 207 | continue; 208 | } 209 | else if (htab[i] >= 0) // non-empty slot 210 | { 211 | disp = hsize_reg - i; // secondary hash (after G. Knott) 212 | if (i == 0) 213 | disp = 1; 214 | do 215 | { 216 | if ((i -= disp) < 0) 217 | i += hsize_reg; 218 | 219 | if (htab[i] == fcode) 220 | { 221 | ent = codetab[i]; 222 | goto outer_loop; 223 | } 224 | } while (htab[i] >= 0); 225 | } 226 | Output(ent, outs); 227 | ent = c; 228 | if (free_ent < maxmaxcode) 229 | { 230 | codetab[i] = free_ent++; // code -> hashtable 231 | htab[i] = fcode; 232 | } 233 | else 234 | ClearTable(outs); 235 | } 236 | // Put out the final code. 237 | Output(ent, outs); 238 | Output(EOFCode, outs); 239 | } 240 | 241 | //---------------------------------------------------------------------------- 242 | public void Encode( Stream os) 243 | { 244 | os.WriteByte( Convert.ToByte( initCodeSize) ); // write "initial code size" byte 245 | curPixel = 0; 246 | Compress(initCodeSize + 1, os); // compress and write the pixel data 247 | os.WriteByte(0); // write block terminator 248 | } 249 | 250 | // Flush the packet to disk, and reset the accumulator 251 | void Flush(Stream outs) 252 | { 253 | if (a_count > 0) 254 | { 255 | outs.WriteByte( Convert.ToByte( a_count )); 256 | outs.Write(accum, 0, a_count); 257 | a_count = 0; 258 | } 259 | } 260 | 261 | int MaxCode(int n_bits) 262 | { 263 | return (1 << n_bits) - 1; 264 | } 265 | 266 | //---------------------------------------------------------------------------- 267 | // Return the next pixel from the image 268 | //---------------------------------------------------------------------------- 269 | private int NextPixel() 270 | { 271 | if (curPixel == pixAry.Length) 272 | return EOF; 273 | 274 | curPixel++; 275 | return pixAry[curPixel - 1] & 0xff; 276 | } 277 | 278 | void Output(int code, Stream outs) 279 | { 280 | cur_accum &= masks[cur_bits]; 281 | 282 | if (cur_bits > 0) 283 | cur_accum |= (code << cur_bits); 284 | else 285 | cur_accum = code; 286 | 287 | cur_bits += n_bits; 288 | 289 | while (cur_bits >= 8) 290 | { 291 | Add((byte) (cur_accum & 0xff), outs); 292 | cur_accum >>= 8; 293 | cur_bits -= 8; 294 | } 295 | 296 | // If the next entry is going to be too big for the code size, 297 | // then increase it, if possible. 298 | if (free_ent > maxcode || clear_flg) 299 | { 300 | if (clear_flg) 301 | { 302 | maxcode = MaxCode(n_bits = g_init_bits); 303 | clear_flg = false; 304 | } 305 | else 306 | { 307 | ++n_bits; 308 | if (n_bits == maxbits) 309 | maxcode = maxmaxcode; 310 | else 311 | maxcode = MaxCode(n_bits); 312 | } 313 | } 314 | 315 | if (code == EOFCode) 316 | { 317 | // At EOF, write the rest of the buffer. 318 | while (cur_bits > 0) 319 | { 320 | Add((byte) (cur_accum & 0xff), outs); 321 | cur_accum >>= 8; 322 | cur_bits -= 8; 323 | } 324 | 325 | Flush(outs); 326 | } 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/LzwEncoder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2f366e2fe812b6a40a427e15cf5bf727 3 | timeCreated: 1429343885 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/NeuQuant.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 1994 Anthony Dekker 3 | * Ported to Java by Kevin Weiner, FM Software 4 | * 5 | * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. 6 | * See "Kohonen neural networks for optimal colour quantization" 7 | * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 8 | * for a discussion of the algorithm. 9 | * 10 | * Any party obtaining a copy of these files from the author, directly or 11 | * indirectly, is granted, free of charge, a full and unrestricted irrevocable, 12 | * world-wide, paid up, royalty-free, nonexclusive right and license to deal 13 | * in this software and documentation files (the "Software"), including without 14 | * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons who receive 16 | * copies from any such party to do so, with the only requirement being 17 | * that this copyright notice remain intact. 18 | */ 19 | 20 | using System; 21 | 22 | namespace Moments.Encoder 23 | { 24 | public class NeuQuant 25 | { 26 | protected static readonly int netsize = 256; // Number of colours used 27 | 28 | // Four primes near 500 - assume no image has a length so large that it is divisible by all four primes 29 | protected static readonly int prime1 = 499; 30 | protected static readonly int prime2 = 491; 31 | protected static readonly int prime3 = 487; 32 | protected static readonly int prime4 = 503; 33 | 34 | protected static readonly int minpicturebytes = (3 * prime4); // Minimum size for input image 35 | 36 | // Network Definitions 37 | protected static readonly int maxnetpos = (netsize - 1); 38 | protected static readonly int netbiasshift = 4; // Bias for colour values 39 | protected static readonly int ncycles = 100; // No. of learning cycles 40 | 41 | // Defs for freq and bias 42 | protected static readonly int intbiasshift = 16; // Bias for fractions 43 | protected static readonly int intbias = (((int)1) << intbiasshift); 44 | protected static readonly int gammashift = 10; // Gamma = 1024 45 | protected static readonly int gamma = (((int)1) << gammashift); 46 | protected static readonly int betashift = 10; 47 | protected static readonly int beta = (intbias >> betashift); // Beta = 1/1024 48 | protected static readonly int betagamma = (intbias << (gammashift - betashift)); 49 | 50 | // Defs for decreasing radius factor 51 | protected static readonly int initrad = (netsize >> 3); // For 256 cols, radius starts 52 | protected static readonly int radiusbiasshift = 6; // At 32.0 biased by 6 bits 53 | protected static readonly int radiusbias = (((int)1) << radiusbiasshift); 54 | protected static readonly int initradius = (initrad * radiusbias); // And decreases by a 55 | protected static readonly int radiusdec = 30; // Factor of 1/30 each cycle 56 | 57 | // Defs for decreasing alpha factor 58 | protected static readonly int alphabiasshift = 10; /* alpha starts at 1.0 */ 59 | protected static readonly int initalpha = (((int)1) << alphabiasshift); 60 | 61 | protected int alphadec; // Biased by 10 bits 62 | 63 | // Radbias and alpharadbias used for radpower calculation 64 | protected static readonly int radbiasshift = 8; 65 | protected static readonly int radbias = (((int)1) << radbiasshift); 66 | protected static readonly int alpharadbshift = (alphabiasshift + radbiasshift); 67 | protected static readonly int alpharadbias = (((int)1) << alpharadbshift); 68 | 69 | // Types and Global Variables 70 | protected byte[] thepicture; // The input image itself 71 | protected int lengthcount; // Lengthcount = H*W*3 72 | protected int samplefac; // Sampling factor 1..30 73 | protected int[][] network; // The network itself - [netsize][4] 74 | protected int[] netindex = new int[256]; // For network lookup - really 256 75 | protected int[] bias = new int[netsize]; // Bias and freq arrays for learning 76 | protected int[] freq = new int[netsize]; 77 | protected int[] radpower = new int[initrad]; // Radpower for precomputation 78 | 79 | // Initialize network in range (0,0,0) to (255,255,255) and set parameters 80 | public NeuQuant(byte[] thepic, int len, int sample) 81 | { 82 | int i; 83 | int[] p; 84 | 85 | thepicture = thepic; 86 | lengthcount = len; 87 | samplefac = sample; 88 | 89 | network = new int[netsize][]; 90 | for (i = 0; i < netsize; i++) 91 | { 92 | network[i] = new int[4]; 93 | p = network[i]; 94 | p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize; 95 | freq[i] = intbias / netsize; // 1 / netsize 96 | bias[i] = 0; 97 | } 98 | } 99 | 100 | public byte[] ColorMap() 101 | { 102 | byte[] map = new byte[3 * netsize]; 103 | int[] index = new int[netsize]; 104 | 105 | for (int i = 0; i < netsize; i++) 106 | index[network[i][3]] = i; 107 | 108 | int k = 0; 109 | for (int i = 0; i < netsize; i++) 110 | { 111 | int j = index[i]; 112 | map[k++] = (byte)(network[j][0]); 113 | map[k++] = (byte)(network[j][1]); 114 | map[k++] = (byte)(network[j][2]); 115 | } 116 | 117 | return map; 118 | } 119 | 120 | // Insertion sort of network and building of netindex[0..255] (to do after unbias) 121 | public void Inxbuild() 122 | { 123 | int i, j, smallpos, smallval; 124 | int[] p; 125 | int[] q; 126 | int previouscol, startpos; 127 | 128 | previouscol = 0; 129 | startpos = 0; 130 | 131 | for (i = 0; i < netsize; i++) 132 | { 133 | p = network[i]; 134 | smallpos = i; 135 | smallval = p[1]; // Index on g 136 | 137 | // Find smallest in i..netsize-1 138 | for (j = i + 1; j < netsize; j++) 139 | { 140 | q = network[j]; 141 | if (q[1] < smallval) 142 | { 143 | smallpos = j; 144 | smallval = q[1]; // Index on g 145 | } 146 | } 147 | 148 | q = network[smallpos]; 149 | 150 | // Swap p (i) and q (smallpos) entries 151 | if (i != smallpos) 152 | { 153 | j = q[0]; 154 | q[0] = p[0]; 155 | p[0] = j; 156 | j = q[1]; 157 | q[1] = p[1]; 158 | p[1] = j; 159 | j = q[2]; 160 | q[2] = p[2]; 161 | p[2] = j; 162 | j = q[3]; 163 | q[3] = p[3]; 164 | p[3] = j; 165 | } 166 | 167 | // Smallval entry is now in position i 168 | if (smallval != previouscol) 169 | { 170 | netindex[previouscol] = (startpos + i) >> 1; 171 | 172 | for (j = previouscol + 1; j < smallval; j++) 173 | netindex[j] = i; 174 | 175 | previouscol = smallval; 176 | startpos = i; 177 | } 178 | } 179 | 180 | netindex[previouscol] = (startpos + maxnetpos) >> 1; 181 | 182 | for (j = previouscol + 1; j < 256; j++) 183 | netindex[j] = maxnetpos; 184 | } 185 | 186 | // Main Learning Loop 187 | public void Learn() 188 | { 189 | int i, j, b, g, r; 190 | int radius, rad, alpha, step, delta, samplepixels; 191 | byte[] p; 192 | int pix, lim; 193 | 194 | if (lengthcount < minpicturebytes) 195 | samplefac = 1; 196 | 197 | alphadec = 30 + ((samplefac - 1) / 3); 198 | p = thepicture; 199 | pix = 0; 200 | lim = lengthcount; 201 | samplepixels = lengthcount / (3 * samplefac); 202 | delta = samplepixels / ncycles; 203 | alpha = initalpha; 204 | radius = initradius; 205 | 206 | rad = radius >> radiusbiasshift; 207 | 208 | if (rad <= 1) 209 | rad = 0; 210 | 211 | for (i = 0; i < rad; i++) 212 | radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); 213 | 214 | if (lengthcount < minpicturebytes) 215 | { 216 | step = 3; 217 | } 218 | else if ((lengthcount % prime1) != 0) 219 | { 220 | step = 3 * prime1; 221 | } 222 | else 223 | { 224 | if ((lengthcount % prime2) != 0) 225 | { 226 | step = 3 * prime2; 227 | } 228 | else 229 | { 230 | if ((lengthcount % prime3) != 0) 231 | step = 3 * prime3; 232 | else 233 | step = 3 * prime4; 234 | } 235 | } 236 | 237 | i = 0; 238 | while (i < samplepixels) 239 | { 240 | b = (p[pix + 0] & 0xff) << netbiasshift; 241 | g = (p[pix + 1] & 0xff) << netbiasshift; 242 | r = (p[pix + 2] & 0xff) << netbiasshift; 243 | j = Contest(b, g, r); 244 | 245 | Altersingle(alpha, j, b, g, r); 246 | 247 | if (rad != 0) 248 | Alterneigh(rad, j, b, g, r); // Alter neighbours 249 | 250 | pix += step; 251 | 252 | if (pix >= lim) 253 | pix -= lengthcount; 254 | 255 | i++; 256 | 257 | if (delta == 0) 258 | delta = 1; 259 | 260 | if (i % delta == 0) 261 | { 262 | alpha -= alpha / alphadec; 263 | radius -= radius / radiusdec; 264 | rad = radius >> radiusbiasshift; 265 | 266 | if (rad <= 1) 267 | rad = 0; 268 | 269 | for (j = 0; j < rad; j++) 270 | radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); 271 | } 272 | } 273 | } 274 | 275 | // Search for BGR values 0..255 (after net is unbiased) and return colour index 276 | public int Map(int b, int g, int r) 277 | { 278 | int i, j, dist, a, bestd; 279 | int[] p; 280 | int best; 281 | 282 | bestd = 1000; // Biggest possible dist is 256*3 283 | best = -1; 284 | i = netindex[g]; // Index on g 285 | j = i - 1; // Start at netindex[g] and work outwards 286 | 287 | while ((i < netsize) || (j >= 0)) 288 | { 289 | if (i < netsize) 290 | { 291 | p = network[i]; 292 | dist = p[1] - g; // Inx key 293 | 294 | if (dist >= bestd) 295 | { 296 | i = netsize; // Stop iter 297 | } 298 | else 299 | { 300 | i++; 301 | 302 | if (dist < 0) 303 | dist = -dist; 304 | 305 | a = p[0] - b; 306 | 307 | if (a < 0) 308 | a = -a; 309 | 310 | dist += a; 311 | 312 | if (dist < bestd) 313 | { 314 | a = p[2] - r; 315 | 316 | if (a < 0) 317 | a = -a; 318 | 319 | dist += a; 320 | 321 | if (dist < bestd) 322 | { 323 | bestd = dist; 324 | best = p[3]; 325 | } 326 | } 327 | } 328 | } 329 | 330 | if (j >= 0) 331 | { 332 | p = network[j]; 333 | dist = g - p[1]; // Inx key - reverse dif 334 | 335 | if (dist >= bestd) 336 | { 337 | j = -1; // Stop iter 338 | } 339 | else 340 | { 341 | j--; 342 | 343 | if (dist < 0) 344 | dist = -dist; 345 | 346 | a = p[0] - b; 347 | 348 | if (a < 0) 349 | a = -a; 350 | 351 | dist += a; 352 | 353 | if (dist < bestd) 354 | { 355 | a = p[2] - r; 356 | 357 | if (a < 0) 358 | a = -a; 359 | 360 | dist += a; 361 | 362 | if (dist < bestd) 363 | { 364 | bestd = dist; 365 | best = p[3]; 366 | } 367 | } 368 | } 369 | } 370 | } 371 | 372 | return best; 373 | } 374 | 375 | public byte[] Process() 376 | { 377 | Learn(); 378 | Unbiasnet(); 379 | Inxbuild(); 380 | return ColorMap(); 381 | } 382 | 383 | // Unbias network to give byte values 0..255 and record position i to prepare for sort 384 | public void Unbiasnet() 385 | { 386 | int i; 387 | 388 | for (i = 0; i < netsize; i++) 389 | { 390 | network[i][0] >>= netbiasshift; 391 | network[i][1] >>= netbiasshift; 392 | network[i][2] >>= netbiasshift; 393 | network[i][3] = i; // Record colour no 394 | } 395 | } 396 | 397 | // Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] 398 | protected void Alterneigh(int rad, int i, int b, int g, int r) 399 | { 400 | int j, k, lo, hi, a, m; 401 | int[] p; 402 | 403 | lo = i - rad; 404 | 405 | if (lo < -1) 406 | lo = -1; 407 | 408 | hi = i + rad; 409 | 410 | if (hi > netsize) 411 | hi = netsize; 412 | 413 | j = i + 1; 414 | k = i - 1; 415 | m = 1; 416 | 417 | while ((j < hi) || (k > lo)) 418 | { 419 | a = radpower[m++]; 420 | 421 | if (j < hi) 422 | { 423 | p = network[j++]; 424 | p[0] -= (a * (p[0] - b)) / alpharadbias; 425 | p[1] -= (a * (p[1] - g)) / alpharadbias; 426 | p[2] -= (a * (p[2] - r)) / alpharadbias; 427 | } 428 | 429 | if (k > lo) 430 | { 431 | p = network[k--]; 432 | p[0] -= (a * (p[0] - b)) / alpharadbias; 433 | p[1] -= (a * (p[1] - g)) / alpharadbias; 434 | p[2] -= (a * (p[2] - r)) / alpharadbias; 435 | } 436 | } 437 | } 438 | 439 | // Move neuron i towards biased (b,g,r) by factor alpha 440 | protected void Altersingle(int alpha, int i, int b, int g, int r) 441 | { 442 | /* Alter hit neuron */ 443 | int[] n = network[i]; 444 | n[0] -= (alpha * (n[0] - b)) / initalpha; 445 | n[1] -= (alpha * (n[1] - g)) / initalpha; 446 | n[2] -= (alpha * (n[2] - r)) / initalpha; 447 | } 448 | 449 | // Search for biased BGR values 450 | protected int Contest(int b, int g, int r) 451 | { 452 | // Finds closest neuron (min dist) and updates freq 453 | // Finds best neuron (min dist-bias) and returns position 454 | // For frequently chosen neurons, freq[i] is high and bias[i] is negative 455 | // bias[i] = gamma*((1/netsize)-freq[i]) 456 | 457 | int i, dist, a, biasdist, betafreq; 458 | int bestpos, bestbiaspos, bestd, bestbiasd; 459 | int[] n; 460 | 461 | bestd = ~(((int)1) << 31); 462 | bestbiasd = bestd; 463 | bestpos = -1; 464 | bestbiaspos = bestpos; 465 | 466 | for (i = 0; i < netsize; i++) 467 | { 468 | n = network[i]; 469 | dist = n[0] - b; 470 | 471 | if (dist < 0) 472 | dist = -dist; 473 | 474 | a = n[1] - g; 475 | 476 | if (a < 0) 477 | a = -a; 478 | 479 | dist += a; 480 | a = n[2] - r; 481 | 482 | if (a < 0) 483 | a = -a; 484 | 485 | dist += a; 486 | 487 | if (dist < bestd) 488 | { 489 | bestd = dist; 490 | bestpos = i; 491 | } 492 | 493 | biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); 494 | 495 | if (biasdist < bestbiasd) 496 | { 497 | bestbiasd = biasdist; 498 | bestbiaspos = i; 499 | } 500 | 501 | betafreq = (freq[i] >> betashift); 502 | freq[i] -= betafreq; 503 | bias[i] += (betafreq << gammashift); 504 | } 505 | 506 | freq[bestpos] += beta; 507 | bias[bestpos] -= betagamma; 508 | return bestbiaspos; 509 | } 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Gif/NeuQuant.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fb60b36fa02b1144fa89d6a543c60a68 3 | timeCreated: 1429343891 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/MinAttribute.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Thomas Hourdel 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | using UnityEngine; 25 | 26 | namespace Moments 27 | { 28 | public sealed class MinAttribute : PropertyAttribute 29 | { 30 | public readonly float min; 31 | 32 | public MinAttribute(float min) 33 | { 34 | this.min = min; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/MinAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ad7f56bd499cb747b6e9e27fc13dd1f 3 | timeCreated: 1429349100 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Recorder.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Thomas Hourdel 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | using UnityEngine; 25 | using System; 26 | using System.Collections; 27 | using System.Collections.Generic; 28 | using Moments.Encoder; 29 | using ThreadPriority = System.Threading.ThreadPriority; 30 | 31 | namespace Moments 32 | { 33 | using UnityObject = UnityEngine.Object; 34 | 35 | public enum RecorderState 36 | { 37 | Recording, 38 | Paused, 39 | PreProcessing 40 | } 41 | 42 | [AddComponentMenu("Miscellaneous/Moments Recorder")] 43 | [RequireComponent(typeof(Camera)), DisallowMultipleComponent] 44 | public sealed class Recorder : MonoBehaviour 45 | { 46 | #region Exposed fields 47 | 48 | // These fields aren't public, the user shouldn't modify them directly as they can't break 49 | // everything if not used correctly. Use Setup() instead. 50 | 51 | [SerializeField, Min(8)] 52 | int m_Width = 320; 53 | 54 | [SerializeField, Min(8)] 55 | int m_Height = 200; 56 | 57 | [SerializeField] 58 | bool m_AutoAspect = true; 59 | 60 | [SerializeField, Range(1, 30)] 61 | int m_FramePerSecond = 15; 62 | 63 | [SerializeField, Min(-1)] 64 | int m_Repeat = 0; 65 | 66 | [SerializeField, Range(1, 100)] 67 | int m_Quality = 15; 68 | 69 | [SerializeField, Min(0.1f)] 70 | float m_BufferSize = 3f; 71 | 72 | #endregion 73 | 74 | #region Public fields 75 | 76 | /// 77 | /// Current state of the recorder. 78 | /// 79 | public RecorderState State { get; private set; } 80 | 81 | /// 82 | /// The folder to save the gif to. No trailing slash. 83 | /// 84 | public string SaveFolder { get; set; } 85 | 86 | /// 87 | /// Sets the worker threads priority. This will only affect newly created threads (on save). 88 | /// 89 | public ThreadPriority WorkerPriority = ThreadPriority.BelowNormal; 90 | 91 | /// 92 | /// Returns the estimated VRam used (in MB) for recording. 93 | /// 94 | public float EstimatedMemoryUse 95 | { 96 | get 97 | { 98 | float mem = m_FramePerSecond * m_BufferSize; 99 | mem *= m_Width * m_Height * 4; 100 | mem /= 1024 * 1024; 101 | return mem; 102 | } 103 | } 104 | 105 | #endregion 106 | 107 | #region Delegates 108 | 109 | /// 110 | /// Called when the pre-processing step has finished. 111 | /// 112 | public Action OnPreProcessingDone; 113 | 114 | /// 115 | /// Called by each worker thread every time a frame is processed during the save process. 116 | /// The first parameter holds the worker ID and the second one a value in range [0;1] for 117 | /// the actual progress. This callback is probably not thread-safe, use at your own risks. 118 | /// 119 | public Action OnFileSaveProgress; 120 | 121 | /// 122 | /// Called once a gif file has been saved. The first parameter will hold the worker ID and 123 | /// the second one the absolute file path. 124 | /// 125 | public Action OnFileSaved; 126 | 127 | #endregion 128 | 129 | #region Internal fields 130 | 131 | int m_MaxFrameCount; 132 | float m_Time; 133 | float m_TimePerFrame; 134 | Queue m_Frames; 135 | RenderTexture m_RecycledRenderTexture; 136 | ReflectionUtils m_ReflectionUtils; 137 | 138 | #endregion 139 | 140 | #region Public API 141 | 142 | /// 143 | /// Initializes the component. Use this if you need to change the recorder settings in a script. 144 | /// This will flush the previously saved frames as settings can't be changed while recording. 145 | /// 146 | /// Automatically compute height from the current aspect ratio 147 | /// Width in pixels 148 | /// Height in pixels 149 | /// Recording FPS 150 | /// Maximum amount of seconds to record to memory 151 | /// -1: no repeat, 0: infinite, >0: repeat count 152 | /// Quality of color quantization (conversion of images to the maximum 153 | /// 256 colors allowed by the GIF specification). Lower values (minimum = 1) produce better 154 | /// colors, but slow processing significantly. Higher values will speed up the quantization 155 | /// pass at the cost of lower image quality (maximum = 100). 156 | public void Setup(bool autoAspect, int width, int height, int fps, float bufferSize, int repeat, int quality) 157 | { 158 | if (State == RecorderState.PreProcessing) 159 | { 160 | Debug.LogWarning("Attempting to setup the component during the pre-processing step."); 161 | return; 162 | } 163 | 164 | // Start fresh 165 | FlushMemory(); 166 | 167 | // Set values and validate them 168 | m_AutoAspect = autoAspect; 169 | m_ReflectionUtils.ConstrainMin(x => x.m_Width, width); 170 | 171 | if (!autoAspect) 172 | m_ReflectionUtils.ConstrainMin(x => x.m_Height, height); 173 | 174 | m_ReflectionUtils.ConstrainRange(x => x.m_FramePerSecond, fps); 175 | m_ReflectionUtils.ConstrainMin(x => x.m_BufferSize, bufferSize); 176 | m_ReflectionUtils.ConstrainMin(x => x.m_Repeat, repeat); 177 | m_ReflectionUtils.ConstrainRange(x => x.m_Quality, quality); 178 | 179 | // Ready to go 180 | Init(); 181 | } 182 | 183 | /// 184 | /// Pauses recording. 185 | /// 186 | public void Pause() 187 | { 188 | if (State == RecorderState.PreProcessing) 189 | { 190 | Debug.LogWarning("Attempting to pause recording during the pre-processing step. The recorder is automatically paused when pre-processing."); 191 | return; 192 | } 193 | 194 | State = RecorderState.Paused; 195 | } 196 | 197 | /// 198 | /// Starts or resumes recording. You can't resume while it's pre-processing data to be saved. 199 | /// 200 | public void Record() 201 | { 202 | if (State == RecorderState.PreProcessing) 203 | { 204 | Debug.LogWarning("Attempting to resume recording during the pre-processing step."); 205 | return; 206 | } 207 | 208 | State = RecorderState.Recording; 209 | } 210 | 211 | /// 212 | /// Clears all saved frames from memory and starts fresh. 213 | /// 214 | public void FlushMemory() 215 | { 216 | if (State == RecorderState.PreProcessing) 217 | { 218 | Debug.LogWarning("Attempting to flush memory during the pre-processing step."); 219 | return; 220 | } 221 | 222 | Init(); 223 | 224 | if (m_RecycledRenderTexture != null) 225 | Flush(m_RecycledRenderTexture); 226 | 227 | if (m_Frames == null) 228 | return; 229 | 230 | foreach (RenderTexture rt in m_Frames) 231 | Flush(rt); 232 | 233 | m_Frames.Clear(); 234 | } 235 | 236 | /// 237 | /// Saves the stored frames to a gif file. The filename will automatically be generated. 238 | /// Recording will be paused and won't resume automatically. You can use the 239 | /// OnPreProcessingDone callback to be notified when the pre-processing 240 | /// step has finished. 241 | /// 242 | public void Save() 243 | { 244 | Save(GenerateFileName()); 245 | } 246 | 247 | /// 248 | /// Saves the stored frames to a gif file. If the filename is null or empty, an unique one 249 | /// will be generated. You don't need to add the .gif extension to the name. Recording will 250 | /// be paused and won't resume automatically. You can use the OnPreProcessingDone 251 | /// callback to be notified when the pre-processing step has finished. 252 | /// 253 | /// File name without extension 254 | public void Save(string filename) 255 | { 256 | if (State == RecorderState.PreProcessing) 257 | { 258 | Debug.LogWarning("Attempting to save during the pre-processing step."); 259 | return; 260 | } 261 | 262 | if (m_Frames.Count == 0) 263 | { 264 | Debug.LogWarning("Nothing to save. Maybe you forgot to start the recorder ?"); 265 | return; 266 | } 267 | 268 | State = RecorderState.PreProcessing; 269 | 270 | if (string.IsNullOrEmpty(filename)) 271 | filename = GenerateFileName(); 272 | 273 | StartCoroutine(PreProcess(filename)); 274 | } 275 | 276 | #endregion 277 | 278 | #region Unity events 279 | 280 | void Awake() 281 | { 282 | m_ReflectionUtils = new ReflectionUtils(this); 283 | m_Frames = new Queue(); 284 | Init(); 285 | } 286 | 287 | void OnDestroy() 288 | { 289 | FlushMemory(); 290 | } 291 | 292 | void OnRenderImage(RenderTexture source, RenderTexture destination) 293 | { 294 | if (State != RecorderState.Recording) 295 | { 296 | Graphics.Blit(source, destination); 297 | return; 298 | } 299 | 300 | m_Time += Time.unscaledDeltaTime; 301 | 302 | if (m_Time >= m_TimePerFrame) 303 | { 304 | // Limit the amount of frames stored in memory 305 | if (m_Frames.Count >= m_MaxFrameCount) 306 | m_RecycledRenderTexture = m_Frames.Dequeue(); 307 | 308 | m_Time -= m_TimePerFrame; 309 | 310 | // Frame data 311 | RenderTexture rt = m_RecycledRenderTexture; 312 | m_RecycledRenderTexture = null; 313 | 314 | if (rt == null) 315 | { 316 | rt = new RenderTexture(m_Width, m_Height, 0, RenderTextureFormat.ARGB32); 317 | rt.wrapMode = TextureWrapMode.Clamp; 318 | rt.filterMode = FilterMode.Bilinear; 319 | rt.anisoLevel = 0; 320 | } 321 | 322 | Graphics.Blit(source, rt); 323 | m_Frames.Enqueue(rt); 324 | } 325 | 326 | Graphics.Blit(source, destination); 327 | } 328 | 329 | #endregion 330 | 331 | #region Methods 332 | 333 | // Used to reset internal values, called on Start(), Setup() and FlushMemory() 334 | void Init() 335 | { 336 | State = RecorderState.Paused; 337 | ComputeHeight(); 338 | m_MaxFrameCount = Mathf.RoundToInt(m_BufferSize * m_FramePerSecond); 339 | m_TimePerFrame = 1f / m_FramePerSecond; 340 | m_Time = 0f; 341 | 342 | // Make sure the output folder is set or use the default one 343 | if (string.IsNullOrEmpty(SaveFolder)) 344 | { 345 | #if UNITY_EDITOR 346 | SaveFolder = Application.dataPath; // Defaults to the asset folder in the editor for faster access to the gif file 347 | #else 348 | SaveFolder = Application.persistentDataPath; 349 | #endif 350 | } 351 | } 352 | 353 | // Automatically computes height from the current aspect ratio if auto aspect is set to true 354 | public void ComputeHeight() 355 | { 356 | if (!m_AutoAspect) 357 | return; 358 | 359 | m_Height = Mathf.RoundToInt(m_Width / GetComponent().aspect); 360 | } 361 | 362 | void Flush(UnityObject obj) 363 | { 364 | #if UNITY_EDITOR 365 | if (Application.isPlaying) 366 | Destroy(obj); 367 | else 368 | DestroyImmediate(obj); 369 | #else 370 | UnityObject.Destroy(obj); 371 | #endif 372 | } 373 | 374 | // Gets a filename : GifCapture-yyyyMMddHHmmssffff 375 | string GenerateFileName() 376 | { 377 | string timestamp = DateTime.Now.ToString("yyyyMMddHHmmssffff"); 378 | return "GifCapture-" + timestamp; 379 | } 380 | 381 | // Pre-processing coroutine to extract frame data and send everything to a separate worker thread 382 | IEnumerator PreProcess(string filename) 383 | { 384 | string filepath = SaveFolder + "/" + filename + ".gif"; 385 | List frames = new List(m_Frames.Count); 386 | 387 | // Get a temporary texture to read RenderTexture data 388 | Texture2D temp = new Texture2D(m_Width, m_Height, TextureFormat.RGB24, false); 389 | temp.hideFlags = HideFlags.HideAndDontSave; 390 | temp.wrapMode = TextureWrapMode.Clamp; 391 | temp.filterMode = FilterMode.Bilinear; 392 | temp.anisoLevel = 0; 393 | 394 | // Process the frame queue 395 | while (m_Frames.Count > 0) 396 | { 397 | GifFrame frame = ToGifFrame(m_Frames.Dequeue(), temp); 398 | frames.Add(frame); 399 | yield return null; 400 | } 401 | 402 | // Dispose the temporary texture 403 | Flush(temp); 404 | 405 | // Switch the state to pause, let the user choose to keep recording or not 406 | State = RecorderState.Paused; 407 | 408 | // Callback 409 | if (OnPreProcessingDone != null) 410 | OnPreProcessingDone(); 411 | 412 | // Setup a worker thread and let it do its magic 413 | GifEncoder encoder = new GifEncoder(m_Repeat, m_Quality); 414 | encoder.SetDelay(Mathf.RoundToInt(m_TimePerFrame * 1000f)); 415 | Worker worker = new Worker(WorkerPriority) 416 | { 417 | m_Encoder = encoder, 418 | m_Frames = frames, 419 | m_FilePath = filepath, 420 | m_OnFileSaved = OnFileSaved, 421 | m_OnFileSaveProgress = OnFileSaveProgress 422 | }; 423 | worker.Start(); 424 | } 425 | 426 | // Converts a RenderTexture to a GifFrame 427 | // Should be fast enough for low-res textures but will tank the framerate at higher res 428 | GifFrame ToGifFrame(RenderTexture source, Texture2D target) 429 | { 430 | RenderTexture.active = source; 431 | target.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0); 432 | target.Apply(); 433 | RenderTexture.active = null; 434 | 435 | return new GifFrame() { Width = target.width, Height = target.height, Data = target.GetPixels32() }; 436 | } 437 | 438 | #endregion 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Recorder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dd5378998db5ba9488d08a4e3b80821a 3 | timeCreated: 1429343841 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/ReflectionUtils.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Thomas Hourdel 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | using UnityEngine; 25 | using System; 26 | using System.Linq.Expressions; 27 | using System.Reflection; 28 | 29 | namespace Moments 30 | { 31 | public class ReflectionUtils where T : class, new() 32 | { 33 | readonly T _Instance; 34 | 35 | public ReflectionUtils(T instance) 36 | { 37 | _Instance = instance; 38 | } 39 | 40 | public string GetFieldName(Expression> fieldAccess) 41 | { 42 | MemberExpression memberExpression = fieldAccess.Body as MemberExpression; 43 | 44 | if (memberExpression != null) 45 | return memberExpression.Member.Name; 46 | 47 | throw new InvalidOperationException("Member expression expected"); 48 | } 49 | 50 | public FieldInfo GetField(string fieldName) 51 | { 52 | return typeof(T).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); 53 | } 54 | 55 | public A GetAttribute(FieldInfo field) where A : Attribute 56 | { 57 | return (A)Attribute.GetCustomAttribute(field, typeof(A)); 58 | } 59 | 60 | // MinAttribute 61 | public void ConstrainMin(Expression> fieldAccess, float value) 62 | { 63 | FieldInfo fieldInfo = GetField(GetFieldName(fieldAccess)); 64 | fieldInfo.SetValue(_Instance, Mathf.Max(value, GetAttribute(fieldInfo).min)); 65 | } 66 | 67 | public void ConstrainMin(Expression> fieldAccess, int value) 68 | { 69 | FieldInfo fieldInfo = GetField(GetFieldName(fieldAccess)); 70 | fieldInfo.SetValue(_Instance, (int)Mathf.Max(value, GetAttribute(fieldInfo).min)); 71 | } 72 | 73 | // RangeAttribute 74 | public void ConstrainRange(Expression> fieldAccess, float value) 75 | { 76 | FieldInfo fieldInfo = GetField(GetFieldName(fieldAccess)); 77 | RangeAttribute attr = GetAttribute(fieldInfo); 78 | fieldInfo.SetValue(_Instance, Mathf.Clamp(value, attr.min, attr.max)); 79 | } 80 | 81 | public void ConstrainRange(Expression> fieldAccess, int value) 82 | { 83 | FieldInfo fieldInfo = GetField(GetFieldName(fieldAccess)); 84 | RangeAttribute attr = GetAttribute(fieldInfo); 85 | fieldInfo.SetValue(_Instance, (int)Mathf.Clamp(value, attr.min, attr.max)); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/ReflectionUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 080ed41b115406c4c8ae6e2460d6078f 3 | timeCreated: 1429353452 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Worker.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Thomas Hourdel 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not be 18 | * misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | using UnityEngine; 25 | using System; 26 | using System.Collections.Generic; 27 | using System.Threading; 28 | using Moments.Encoder; 29 | using ThreadPriority = System.Threading.ThreadPriority; 30 | 31 | namespace Moments 32 | { 33 | internal sealed class Worker 34 | { 35 | static int workerId = 1; 36 | 37 | Thread m_Thread; 38 | int m_Id; 39 | 40 | internal List m_Frames; 41 | internal GifEncoder m_Encoder; 42 | internal string m_FilePath; 43 | internal Action m_OnFileSaved; 44 | internal Action m_OnFileSaveProgress; 45 | 46 | internal Worker(ThreadPriority priority) 47 | { 48 | m_Id = workerId++; 49 | m_Thread = new Thread(Run); 50 | m_Thread.Priority = priority; 51 | } 52 | 53 | internal void Start() 54 | { 55 | m_Thread.Start(); 56 | } 57 | 58 | void Run() 59 | { 60 | m_Encoder.Start(m_FilePath); 61 | 62 | for (int i = 0; i < m_Frames.Count; i++) 63 | { 64 | GifFrame frame = m_Frames[i]; 65 | m_Encoder.AddFrame(frame); 66 | 67 | if (m_OnFileSaveProgress != null) 68 | { 69 | float percent = (float)i / (float)m_Frames.Count; 70 | m_OnFileSaveProgress(m_Id, percent); 71 | } 72 | } 73 | 74 | m_Encoder.Finish(); 75 | 76 | if (m_OnFileSaved != null) 77 | m_OnFileSaved(m_Id, m_FilePath); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Moments Recorder/Scripts/Worker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 88f498cb1380c074aaff4631614e0509 3 | timeCreated: 1429349100 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moments 2 | 3 | **Moments** is a quick GIF replay recorder for Unity3D. It automatically records the last few seconds of gameplay and lets you save to a GIF file on demand, like the game [TowerFall Ascension](http://www.towerfall-game.com/) does. 4 | 5 | Tested with Unity 4.6. The demo requires Unity 5+ (Personal or Pro). 6 | 7 | ## Instructions 8 | 9 | Drop the `Moments Recorder` folder in your project and add the `Recorder` script to your camera (or select your camera and use `Component -> Miscellaneous -> Moments Recorder`). 10 | 11 | The included demo should get you started. For more advanced features, browse the `Moments.Recorder` source code, it's heavily commented. 12 | 13 | [Here's a preview](http://i.imgur.com/K4R8UZ0.gifv) of the output quality. 14 | 15 | Pull requests are welcomed ! 16 | 17 | ## License 18 | 19 | Zlib (see [License.txt](LICENSE.txt)) 20 | --------------------------------------------------------------------------------