├── FrameCapture ├── FrameCaptureTemporal.cs ├── FrameCaptureTemporal.cs.meta ├── Resolve.shader └── Resolve.shader.meta ├── LICENSE └── README.md /FrameCapture/FrameCaptureTemporal.cs: -------------------------------------------------------------------------------- 1 | namespace Tools 2 | { 3 | using System; 4 | using System.IO; 5 | using UnityEngine; 6 | 7 | [RequireComponent(typeof(Camera))] 8 | public sealed class FrameCaptureTemporal : MonoBehaviour 9 | { 10 | [Range(1, 120)] 11 | public int frameRate = 30; 12 | 13 | [Range(1, 16)] 14 | public int samples = 8; 15 | 16 | public bool supersample = false; 17 | 18 | [SerializeField, HideInInspector] 19 | Shader resolveShader; 20 | 21 | string m_Folder; 22 | Camera m_Camera; 23 | Material m_Material; 24 | Texture2D m_Output; 25 | int m_FrameCount; 26 | 27 | void OnEnable() 28 | { 29 | var dataPath = Application.dataPath; 30 | dataPath = dataPath.Substring(0, dataPath.Length - 6); // Remove 'Assets' 31 | var date = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss-ffff"); 32 | m_Folder = Path.Combine(dataPath, date); 33 | 34 | try 35 | { 36 | Directory.CreateDirectory(m_Folder); 37 | } 38 | catch (Exception e) 39 | { 40 | Debug.LogException(e); 41 | enabled = false; 42 | } 43 | 44 | m_Camera = GetComponent(); 45 | m_Material = new Material(resolveShader); 46 | m_FrameCount = 0; 47 | Time.captureFramerate = Mathf.Clamp(frameRate, 1, 120); 48 | } 49 | 50 | void OnDisable() 51 | { 52 | Time.captureFramerate = 0; 53 | m_FrameCount = 0; 54 | 55 | Destroy(m_Material); 56 | Destroy(m_Output); 57 | 58 | m_Material = null; 59 | m_Output = null; 60 | } 61 | 62 | void LateUpdate() 63 | { 64 | var cam = m_Camera; 65 | int ss = supersample ? 2 : 1; 66 | int w = cam.pixelWidth; 67 | int h = cam.pixelHeight; 68 | int sw = w * ss; 69 | int sh = h * ss; 70 | 71 | var kOutFormat = RenderTextureFormat.ARGB32; 72 | var kRenderFormat = cam.allowHDR 73 | ? RenderTextureFormat.ARGBHalf 74 | : RenderTextureFormat.ARGB32; 75 | 76 | var targets = new [] 77 | { 78 | RenderTexture.GetTemporary(sw, sh, 24, kRenderFormat), 79 | RenderTexture.GetTemporary(sw, sh, 0, kOutFormat), 80 | RenderTexture.GetTemporary(sw, sh, 0, kOutFormat), 81 | RenderTexture.GetTemporary(w, h, 0, kOutFormat) 82 | }; 83 | 84 | var oldActive = RenderTexture.active; 85 | var oldTarget = cam.targetTexture; 86 | 87 | samples = Mathf.Clamp(samples, 1, 16); 88 | m_Material.SetFloat("_Samples", samples); 89 | 90 | for (int s = 0; s < samples; s++) 91 | { 92 | cam.targetTexture = targets[0]; 93 | 94 | if (samples > 1) // Only jitters if we're actually using the temporal filter 95 | SetProjectionMatrix(cam, s); 96 | 97 | cam.Render(); 98 | 99 | if (s == 0) 100 | { 101 | Graphics.Blit(targets[0], targets[1]); 102 | } 103 | else 104 | { 105 | m_Material.SetTexture("_HistoryTex", targets[1]); 106 | Graphics.Blit(targets[0], targets[2], m_Material, 0); 107 | 108 | // Swap history targets 109 | var t = targets[1]; 110 | targets[1] = targets[2]; 111 | targets[2] = t; 112 | } 113 | } 114 | 115 | var finalTarget = targets[1]; 116 | if (supersample) 117 | { 118 | Graphics.Blit(targets[1], targets[3], m_Material, 1); 119 | finalTarget = targets[3]; 120 | } 121 | 122 | cam.ResetProjectionMatrix(); 123 | cam.targetTexture = oldTarget; 124 | 125 | RenderTexture.active = finalTarget; 126 | CheckOutput(ref m_Output, w, h); 127 | m_Output.ReadPixels(new Rect(0, 0, w, h), 0, 0); 128 | m_Output.Apply(); 129 | 130 | RenderTexture.active = oldActive; 131 | 132 | foreach (var target in targets) 133 | RenderTexture.ReleaseTemporary(target); 134 | 135 | SaveOutput(); 136 | m_FrameCount++; 137 | } 138 | 139 | void SetProjectionMatrix(Camera cam, int sample) 140 | { 141 | const float kJitterScale = 1f; 142 | var jitter = new Vector2( 143 | HaltonSeq(sample & 1023, 2), 144 | HaltonSeq(sample & 1023, 3) 145 | ) * kJitterScale; 146 | 147 | cam.nonJitteredProjectionMatrix = cam.projectionMatrix; 148 | 149 | if (cam.orthographic) 150 | cam.projectionMatrix = GetOrthoProjectionMatrix(cam, jitter); 151 | else 152 | cam.projectionMatrix = GetPerspProjectionMatrix(cam, jitter); 153 | 154 | cam.useJitteredProjectionMatrixForTransparentRendering = false; 155 | } 156 | 157 | float HaltonSeq(int index, int radix) 158 | { 159 | float result = 0f; 160 | float fraction = 1f / (float)radix; 161 | 162 | while (index > 0) 163 | { 164 | result += (float)(index % radix) * fraction; 165 | 166 | index /= radix; 167 | fraction /= (float)radix; 168 | } 169 | 170 | return result; 171 | } 172 | 173 | Matrix4x4 GetPerspProjectionMatrix(Camera cam, Vector2 offset) 174 | { 175 | float vertical = Mathf.Tan(0.5f * Mathf.Deg2Rad * cam.fieldOfView); 176 | float horizontal = vertical * cam.aspect; 177 | float near = cam.nearClipPlane; 178 | float far = cam.farClipPlane; 179 | 180 | offset.x *= horizontal / (0.5f * cam.pixelWidth); 181 | offset.y *= vertical / (0.5f * cam.pixelHeight); 182 | 183 | float left = (offset.x - horizontal) * near; 184 | float right = (offset.x + horizontal) * near; 185 | float top = (offset.y + vertical) * near; 186 | float bottom = (offset.y - vertical) * near; 187 | 188 | var matrix = new Matrix4x4(); 189 | 190 | matrix[0, 0] = (2f * near) / (right - left); 191 | matrix[0, 1] = 0f; 192 | matrix[0, 2] = (right + left) / (right - left); 193 | matrix[0, 3] = 0f; 194 | 195 | matrix[1, 0] = 0f; 196 | matrix[1, 1] = (2f * near) / (top - bottom); 197 | matrix[1, 2] = (top + bottom) / (top - bottom); 198 | matrix[1, 3] = 0f; 199 | 200 | matrix[2, 0] = 0f; 201 | matrix[2, 1] = 0f; 202 | matrix[2, 2] = -(far + near) / (far - near); 203 | matrix[2, 3] = -(2f * far * near) / (far - near); 204 | 205 | matrix[3, 0] = 0f; 206 | matrix[3, 1] = 0f; 207 | matrix[3, 2] = -1f; 208 | matrix[3, 3] = 0f; 209 | 210 | return matrix; 211 | } 212 | 213 | Matrix4x4 GetOrthoProjectionMatrix(Camera cam, Vector2 offset) 214 | { 215 | float vertical = cam.orthographicSize; 216 | float horizontal = vertical * cam.aspect; 217 | 218 | offset.x *= horizontal / (0.5f * cam.pixelWidth); 219 | offset.y *= vertical / (0.5f * cam.pixelHeight); 220 | 221 | float left = offset.x - horizontal; 222 | float right = offset.x + horizontal; 223 | float top = offset.y + vertical; 224 | float bottom = offset.y - vertical; 225 | 226 | return Matrix4x4.Ortho(left, right, bottom, top, cam.nearClipPlane, cam.farClipPlane); 227 | } 228 | 229 | void CheckOutput(ref Texture2D texture, int width, int height) 230 | { 231 | if (texture != null && texture.width == width && texture.height == height) 232 | return; 233 | 234 | Destroy(texture); 235 | texture = new Texture2D(width, height, TextureFormat.ARGB32, false); 236 | } 237 | 238 | void SaveOutput() 239 | { 240 | try 241 | { 242 | var bytes = m_Output.EncodeToPNG(); 243 | var path = Path.Combine(m_Folder, string.Format("{0:D06}.png", m_FrameCount)); 244 | File.WriteAllBytes(path, bytes); 245 | } 246 | catch (Exception e) 247 | { 248 | Debug.LogException(e); 249 | enabled = false; 250 | } 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /FrameCapture/FrameCaptureTemporal.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5424d59a5d28dfc4b917f706e17955e7 3 | timeCreated: 1496487258 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: 8 | - resolveShader: {fileID: 4800000, guid: 000a2f3bd9ef6c14f8a0ab3e05ff2376, type: 3} 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /FrameCapture/Resolve.shader: -------------------------------------------------------------------------------- 1 | Shader "Hidden/Tools/Resolve" 2 | { 3 | Properties 4 | { 5 | _MainTex ("", 2D) = "white" {} 6 | _HistoryTex ("", 2D) = "white" {} 7 | } 8 | 9 | CGINCLUDE 10 | 11 | #pragma target 3.0 12 | #include "UnityCG.cginc" 13 | 14 | struct Attributes 15 | { 16 | float4 vertex : POSITION; 17 | float2 texcoord : TEXCOORD0; 18 | }; 19 | 20 | struct Varyings 21 | { 22 | float4 vertex : SV_POSITION; 23 | float2 texcoord : TEXCOORD0; 24 | }; 25 | 26 | Varyings Vert(Attributes v) 27 | { 28 | Varyings o; 29 | o.vertex = UnityObjectToClipPos(v.vertex); 30 | o.texcoord = v.texcoord; 31 | return o; 32 | } 33 | 34 | sampler2D _MainTex; 35 | float4 _MainTex_TexelSize; 36 | sampler2D _HistoryTex; 37 | float _Samples; 38 | 39 | // Very basic temporal resolve filter using moving averages, we don't have to deal with 40 | // ghosting so who cares 41 | float4 Frag(Varyings i) : SV_Target 42 | { 43 | float4 color = tex2D(_MainTex, i.texcoord); 44 | float4 history = tex2D(_HistoryTex, i.texcoord); 45 | return ((history * _Samples) + color) / (_Samples + 1); 46 | } 47 | 48 | float4 FragDownscaling(Varyings i) : SV_Target 49 | { 50 | // Standard bilinear (using hardware filtering) 51 | return tex2D(_MainTex, i.texcoord); 52 | 53 | // Standard bilinear (manual) 54 | //float4 a = tex2D(_MainTex, i.texcoord + _MainTex_TexelSize.xy * float2(-0.5, -0.5)); 55 | //float4 b = tex2D(_MainTex, i.texcoord + _MainTex_TexelSize.xy * float2(-0.5, 0.5)); 56 | //float4 c = tex2D(_MainTex, i.texcoord + _MainTex_TexelSize.xy * float2( 0.5, 0.5)); 57 | //float4 d = tex2D(_MainTex, i.texcoord + _MainTex_TexelSize.xy * float2( 0.5, -0.5)); 58 | //return (a + b + c + d) / 4; 59 | } 60 | 61 | ENDCG 62 | 63 | SubShader 64 | { 65 | Cull Off ZWrite Off ZTest Always 66 | 67 | Pass 68 | { 69 | CGPROGRAM 70 | 71 | #pragma vertex Vert 72 | #pragma fragment Frag 73 | 74 | ENDCG 75 | } 76 | 77 | Pass 78 | { 79 | CGPROGRAM 80 | 81 | #pragma vertex Vert 82 | #pragma fragment FragDownscaling 83 | 84 | ENDCG 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /FrameCapture/Resolve.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 000a2f3bd9ef6c14f8a0ab3e05ff2376 3 | timeCreated: 1496486917 4 | licenseType: Pro 5 | ShaderImporter: 6 | defaultTextures: [] 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thomas Hourdel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FrameCapture 2 | ============ 3 | 4 | A simple frame-by-frame capture tool for Unity to record perfectly smooth, supersampled replays or cinematics. Best used in the editor. 5 | 6 | Tested with Unity 5.6+. 7 | 8 | Instructions 9 | ------------ 10 | 11 | Copy the `FrameCapture` folder into your project and add the component to the camera you wish to capture. The component will start recording as soon as it's enabled and will stop once disabled. Frames will be saved in your project folder (next to `Assets` and `ProjectSettings`) and will be numbered properly (a new folder will be created for each capture session). 12 | 13 | Settings: 14 | 15 | - **Frame Rate**: Sets a desired framerate for the capture (the game timestep will be fixed to `1.0 / frameRate` for the duration of the recording, regardless of real time and the time required to render a frame). 16 | - **Samples**: The number of samples to use for a relatively-cheap, temporal-like anti-aliasing filter. Higher means better quality. Set to `1` to disable the filter. 17 | - **Supersample**: Renders each frame at twice the original resolution and downscale them back. Very expensive, but also very high quality. 18 | 19 | Maximum quality can be achieved by pushing `Samples` to `16` and enabling `Supersample`. 20 | 21 | License 22 | ------- 23 | 24 | MIT (see [LICENSE.txt](LICENSE.txt)) --------------------------------------------------------------------------------