├── Documentation~ ├── performance.png └── visualizer.gif ├── Editor.meta ├── Editor ├── Thammin.UnitySpring.Editor.asmdef ├── Thammin.UnitySpring.Editor.asmdef.meta ├── Visualizer.cs └── Visualizer.cs.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── ClosedForm.cs ├── ClosedForm.cs.meta ├── ExplicitRK4.cs ├── ExplicitRK4.cs.meta ├── SemiImplicitEuler.cs ├── SemiImplicitEuler.cs.meta ├── SpringBase.cs ├── SpringBase.cs.meta ├── Thammin.UnitySpring.asmdef ├── Thammin.UnitySpring.asmdef.meta ├── VerletIntegration.cs └── VerletIntegration.cs.meta ├── Tests.meta ├── Tests ├── Editor.meta └── Editor │ ├── PerformanceTest.cs │ ├── PerformanceTest.cs.meta │ ├── Thammin.UnitySpring.Tests.asmdef │ └── Thammin.UnitySpring.Tests.asmdef.meta ├── package.json └── package.json.meta /Documentation~/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thammin/unity-spring/6bf3aed946ac670331c5337893f965645911538e/Documentation~/performance.png -------------------------------------------------------------------------------- /Documentation~/visualizer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thammin/unity-spring/6bf3aed946ac670331c5337893f965645911538e/Documentation~/visualizer.gif -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d131a29b5cf6249b98b250cdc8062771 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Thammin.UnitySpring.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Thammin.UnitySpring.Editor", 3 | "references": [ 4 | "Thammin.UnitySpring" 5 | ], 6 | "includePlatforms": [ 7 | "Editor" 8 | ], 9 | "excludePlatforms": [] 10 | } -------------------------------------------------------------------------------- /Editor/Thammin.UnitySpring.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 749c26ff8beca40b896199bafbde8046 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Visualizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using static UnityEditor.EditorGUI; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace UnitySpring.Editor 9 | { 10 | public class Visualizer : EditorWindow 11 | { 12 | // Graph 13 | static readonly Rect Box = new Rect(100, 100, 1020, 500); 14 | int graphOffsetY = 65; 15 | int cellSize = 20; 16 | int offset = 10; 17 | Color gridColor = new Color(0.5f, 0.5f, 0.5f, 0.1f); 18 | Color axisColor = Color.gray; 19 | float gridSizeX => Visualizer.Box.width - offset * 2; 20 | float gridSizeY => Visualizer.Box.height - offset * 2 - graphOffsetY; 21 | int gridCountX => Mathf.CeilToInt(gridSizeX / cellSize); 22 | int gridCountY => Mathf.CeilToInt(gridSizeY / cellSize); 23 | int axisY => gridCountY / 2; 24 | 25 | // Spring Types 26 | Type[] springTypes = new Type[] 27 | { 28 | typeof(ClosedForm.Spring), 29 | typeof(SemiImplicitEuler.Spring), 30 | typeof(ExplicitRK4.Spring), 31 | typeof(VerletIntegration.Spring) 32 | }; 33 | String[] springTypeOptions => springTypes 34 | .Select(t => t.FullName) 35 | .Select(s => s.Split('.')[1]) 36 | .ToArray(); 37 | 38 | Type currentType = typeof(ClosedForm.Spring); 39 | 40 | (float damping, float startValue, float endValue, float initialVelocity)[] dataset 41 | = new (float damping, float startValue, float endValue, float initialVelocity)[] 42 | { 43 | (26 , 10, 0, 0 ), // critically damped 44 | (5 , 10, 0, 0 ), // under damped 45 | (100, 10, 0, 0 ), // over damped 46 | (5 , 0, 0, 100) // under damped with initial velocity 47 | }; 48 | 49 | Color[] colors = new Color[] 50 | { 51 | Color.red, // critically damped 52 | Color.cyan, // under damped 53 | Color.yellow, // over damped 54 | Color.magenta // under damped with initial velocity 55 | }; 56 | 57 | // caches 58 | SpringBase[] springs; 59 | FieldInfo stepSizeField; 60 | int stepSizeFps; 61 | int fps = 60; 62 | float graphTime = 2f; 63 | float damping; 64 | float mass; 65 | float stiffness; 66 | float startValue; 67 | float endValue; 68 | float initialVelocity; 69 | 70 | string[] graphModes = new string[] { "Presets", "Custom" }; 71 | int graphModeIndex = 0; 72 | 73 | [MenuItem("Tools/UnitySpring/Visualizer")] 74 | static void ShowWindow() 75 | { 76 | GetWindowWithRect(Box, true, "Unity Spring Visualizer", true); 77 | } 78 | 79 | void OnEnable() => SetupPresetSprings(); 80 | 81 | void SetupPresetSprings() 82 | { 83 | springs = dataset.Select(d => 84 | { 85 | var spring = Activator.CreateInstance(currentType) as SpringBase; 86 | spring.damping = d.damping; 87 | spring.startValue = d.startValue; 88 | spring.endValue = d.endValue; 89 | spring.initialVelocity = d.initialVelocity; 90 | return spring; 91 | }).ToArray(); 92 | 93 | SetupStepSize(springs[0]); 94 | } 95 | 96 | void SetupCustomSpring() 97 | { 98 | var spring = Activator.CreateInstance(currentType) as SpringBase; 99 | damping = spring.damping; 100 | mass = spring.mass; 101 | stiffness = spring.stiffness; 102 | startValue = spring.startValue = 10; 103 | endValue = spring.endValue = 0; 104 | initialVelocity = spring.initialVelocity = 0; 105 | springs = new SpringBase[] { spring }; 106 | 107 | SetupStepSize(spring); 108 | } 109 | 110 | void SetupStepSize(SpringBase spring) 111 | { 112 | stepSizeField = currentType.GetField("stepSize", BindingFlags.NonPublic | BindingFlags.Instance); 113 | if (stepSizeField != null) 114 | { 115 | stepSizeFps = Mathf.CeilToInt(1f / (float)stepSizeField.GetValue(spring)); 116 | } 117 | } 118 | 119 | void UpdateCustomSpring() 120 | { 121 | var spring = springs[0]; 122 | spring.damping = damping; 123 | spring.mass = mass; 124 | spring.stiffness = stiffness; 125 | spring.startValue = startValue; 126 | spring.endValue = endValue; 127 | spring.initialVelocity = initialVelocity; 128 | } 129 | 130 | void OnGUI() 131 | { 132 | DrawController(); 133 | DrawGrid(); 134 | PlotGraph(); 135 | } 136 | 137 | void DrawGrid() 138 | { 139 | for (var x = 0; x < gridCountX + 1; x++) 140 | { 141 | var color = x == 0 ? axisColor : gridColor; 142 | drawLine(x, 0, 1, gridCountY * cellSize, color); 143 | } 144 | 145 | for (var y = 0; y < gridCountY + 1; y++) 146 | { 147 | var color = y == axisY ? axisColor : gridColor; 148 | drawLine(0, y, gridCountX * cellSize, 1, color); 149 | } 150 | 151 | for (var t = 0f; t < graphTime; t++) 152 | { 153 | var x = t / (graphTime / gridSizeX) / cellSize; 154 | drawHatchMark(x, axisY, t); 155 | } 156 | 157 | void drawLine(float x, float y, float w, float h, Color c) 158 | { 159 | x = x * cellSize + offset - 0.5f; 160 | y = y * cellSize + offset - 0.5f + graphOffsetY; 161 | DrawRect(new Rect(x, y, w, h), c); 162 | } 163 | 164 | void drawHatchMark(float x, float y, float unit) 165 | { 166 | x = x * cellSize + offset - 0.5f; 167 | y = y * cellSize + offset + -0.5f + graphOffsetY; 168 | DrawRect(new Rect(x, y - 10, 1, 21), axisColor); 169 | GUI.Label(new Rect(x + 2, y + 6, 10, 10), unit.ToString()); 170 | } 171 | } 172 | 173 | void PlotGraph() 174 | { 175 | var step = 1f / fps; 176 | var dt = step / (graphTime / gridSizeX); 177 | 178 | foreach (var s in springs) s.Reset(); 179 | 180 | // start values 181 | for (var i = 0; i < springs.Length; i++) 182 | { 183 | drawPoint(0, springs[i].startValue, colors[i]); 184 | } 185 | 186 | // draw until end of axis x 187 | var t = dt; 188 | while (t < gridSizeX) 189 | { 190 | for (var i = 0; i < springs.Length; i++) 191 | { 192 | drawPoint(t, springs[i].Evaluate(step), colors[i]); 193 | } 194 | t += dt; 195 | } 196 | 197 | void drawPoint(float x, float y, Color c) 198 | { 199 | var n = Mathf.FloorToInt(dt / 2); 200 | n = Mathf.Clamp(n, 1, 5); 201 | DrawRect( 202 | new Rect( 203 | x + offset - 0.5f * n, 204 | (axisY - y) * cellSize + offset - 0.5f * n + graphOffsetY, 205 | n, 206 | n 207 | ), 208 | c 209 | ); 210 | } 211 | } 212 | 213 | void DrawController() 214 | { 215 | EditorGUIUtility.labelWidth = 70; 216 | var sliderWidth = GUILayout.Width(230); 217 | 218 | GUILayout.Space(10); 219 | EditorGUILayout.BeginHorizontal(); 220 | { 221 | GUILayout.Space(10); 222 | 223 | // spring types 224 | BeginChangeCheck(); 225 | { 226 | var index = Array.IndexOf(springTypes, currentType); 227 | index = EditorGUILayout.Popup("Spring Type:", index, springTypeOptions); 228 | currentType = springTypes[index]; 229 | } 230 | if (EndChangeCheck()) 231 | { 232 | if (graphModeIndex == 0) 233 | { 234 | SetupPresetSprings(); 235 | } 236 | else 237 | { 238 | SetupCustomSpring(); 239 | } 240 | } 241 | 242 | GUILayout.Space(10); 243 | 244 | // fps 245 | fps = EditorGUILayout.IntSlider("FPS:", fps, 10, 120, sliderWidth); 246 | 247 | GUILayout.Space(10); 248 | 249 | // step size 250 | if (stepSizeField != null) 251 | { 252 | BeginChangeCheck(); 253 | { 254 | stepSizeFps = EditorGUILayout.IntSlider("Step FPS:", stepSizeFps, fps, 120, sliderWidth); 255 | } 256 | if (EndChangeCheck()) 257 | { 258 | foreach (var s in springs) stepSizeField.SetValue(s, 1f / stepSizeFps); 259 | } 260 | } 261 | 262 | GUILayout.Space(10); 263 | 264 | // graph time 265 | graphTime = EditorGUILayout.Slider("Graph Time:", graphTime, 0.1f, 5f, sliderWidth); 266 | 267 | GUILayout.Space(10); 268 | } 269 | EditorGUILayout.EndHorizontal(); 270 | 271 | EditorGUILayout.BeginHorizontal(); 272 | { 273 | GUILayout.Space(10); 274 | 275 | // graph mode 276 | BeginChangeCheck(); 277 | { 278 | graphModeIndex = EditorGUILayout.Popup("Modes:", graphModeIndex, graphModes); 279 | } 280 | if (EndChangeCheck()) 281 | { 282 | if (graphModeIndex == 0) 283 | { 284 | SetupPresetSprings(); 285 | } 286 | else 287 | { 288 | SetupCustomSpring(); 289 | } 290 | } 291 | 292 | // custom spring parameters 293 | BeginDisabledGroup(graphModeIndex == 0); 294 | BeginChangeCheck(); 295 | { 296 | GUILayout.Space(10); 297 | EditorGUIUtility.labelWidth = 100; 298 | startValue = EditorGUILayout.FloatField("Start Value:", startValue); 299 | GUILayout.Space(10); 300 | endValue = EditorGUILayout.FloatField("End Value:", endValue); 301 | GUILayout.Space(10); 302 | initialVelocity = EditorGUILayout.FloatField("Initial Velocity:", initialVelocity); 303 | GUILayout.Space(10); 304 | } 305 | if (EndChangeCheck()) UpdateCustomSpring(); 306 | EndDisabledGroup(); 307 | } 308 | EditorGUILayout.EndHorizontal(); 309 | 310 | EditorGUILayout.BeginHorizontal(); 311 | { 312 | // custom spring parameters 313 | BeginDisabledGroup(graphModeIndex == 0); 314 | BeginChangeCheck(); 315 | { 316 | EditorGUIUtility.labelWidth = 70; 317 | GUILayout.Space(10); 318 | damping = EditorGUILayout.Slider("Damping:", damping, 0f, 100f); 319 | GUILayout.Space(10); 320 | mass = EditorGUILayout.Slider("Mass:", mass, 0f, 100f); 321 | GUILayout.Space(10); 322 | stiffness = EditorGUILayout.Slider("Stiffness:", stiffness, 0f, 200f); 323 | GUILayout.Space(10); 324 | } 325 | if (EndChangeCheck()) UpdateCustomSpring(); 326 | EndDisabledGroup(); 327 | } 328 | EditorGUILayout.EndHorizontal(); 329 | 330 | EditorGUIUtility.labelWidth = 0; 331 | } 332 | } 333 | } -------------------------------------------------------------------------------- /Editor/Visualizer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2dd8d05748faa4a12b1f60482a072c5c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Young 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 317d30e326bc54bc79a5ebc7fa7f7e2c 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A minimal spring physics library for Unity 2 | 3 | Implement multiple solvers for damped harmonic oscillator. 4 | 5 | Solvers: 6 | 7 | - [x] [Closed-form solution for the ODE](http://www.ryanjuckett.com/programming/damped-springs/) 8 | - [x] [Semi-implicit Euler method](https://en.wikipedia.org/wiki/Semi-implicit_Euler_method) 9 | - [x] [Explicit Runge-Kutta 4th order aka RK4](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods) 10 | - [x] [Verlet Integration](https://en.wikipedia.org/wiki/Verlet_integration) 11 | 12 | Maybe not: 13 | 14 | - [ ] [Explicit Euler aka Forward Euler](https://en.wikipedia.org/wiki/Euler_method) 15 | - [ ] [Implicit Euler aka Backward Euler](https://en.wikipedia.org/wiki/Backward_Euler_method) 16 | - [ ] [Mid-point method](https://en.wikipedia.org/wiki/Midpoint_method) 17 | - [ ] [Implicit Runge-Kutta 4th order aka RK4](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#Implicit_Runge%E2%80%93Kutta_methods) 18 | 19 | Performance rough check with 2.6 GHz Intel Core i7: 20 | 21 | 22 | 23 | # Install 24 | 25 | via Package Manager UI 26 | 27 | ``` 28 | ssh://git@github.com/thammin/unity-spring.git 29 | ``` 30 | 31 | via [OpenUPM](https://openupm.com/packages/com.thammin.unity-spring/) 32 | 33 | ``` 34 | openupm add com.thammin.unity-spring 35 | ``` 36 | 37 | # Usage 38 | 39 | Every solver is just a [simple class](https://github.com/thammin/unity-spring/blob/master/Runtime/SpringBase.cs) with few fields. 40 | 41 | ```cs 42 | using UnityEngine; 43 | using Spring = UnitySpring.ClosedForm.Spring; 44 | 45 | public class Ball : MonoBehaviour 46 | { 47 | Spring spring; 48 | 49 | void Start() 50 | { 51 | // interpolate from -10f to 10f 52 | spring = new Spring() 53 | { 54 | startValue = -10f, 55 | endValue = 10f 56 | }; 57 | } 58 | 59 | void Update() 60 | { 61 | var x = spring.Evaluate(Time.deltaTime); 62 | transform.position = new Vector3(x, 0f, 0f); 63 | } 64 | } 65 | ``` 66 | 67 | # Screenshot or demo 68 | 69 | Visualizer: 70 | 71 | ![](./Documentation~/visualizer.gif) 72 | 73 | # FAQ 74 | 75 | ### Unity SmoothDamp 76 | 77 | Source code: [link](https://github.com/Unity-Technologies/UnityCsReference/blob/2019.3/Runtime/Export/Math/Mathf.cs#L302-L331) 78 | 79 | Based on closed-form solution, but only modeling critically damped spring. Using tweaked Exponential approximation (up to Taylor 3rd order) which claims as roughly 80 times faster and approximate less than 0.1% error than `exp` function. 80 | 81 | 82 | 83 | ```cs 84 | // tweaked coefficients 85 | float exp = 1F / (1F + x + 0.48F * x * x + 0.235F * x * x * x); 86 | ``` 87 | 88 | # References 89 | 90 | Analytical: 91 | 92 | - http://www.entropy.energy/scholar/node/damped-harmonic-oscillator 93 | - https://doc.lagout.org/Others/Game%20Development/Programming/Game%20Programming%20Gems%204.pdf 94 | 95 | Numerical: 96 | 97 | - http://box2d.org/files/GDC2015/ErinCatto_NumericalMethods.pdf 98 | 99 | General: 100 | 101 | - https://hplgit.github.io/num-methods-for-PDEs/doc/pub/vib/pdf/vib-4print-A4-2up.pdf 102 | 103 | # License 104 | 105 | MIT 106 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 419b6c056a58d4d86bfe078532e271df 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 99b5fbba3100f4b75bc28d63ad481064 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/ClosedForm.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnitySpring.ClosedForm 4 | { 5 | /// 6 | /// Closed-form solution for the ODE of damped harmonic oscillator. 7 | /// https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator 8 | /// 9 | /// Proof and derived from http://www.ryanjuckett.com/programming/damped-springs/ 10 | /// 11 | public class Spring : SpringBase 12 | { 13 | protected float springTime; 14 | 15 | public override void Reset() 16 | { 17 | springTime = 0f; 18 | currentValue = 0f; 19 | currentVelocity = 0f; 20 | } 21 | 22 | public override void UpdateEndValue(float value, float velocity) 23 | { 24 | startValue = currentValue; 25 | endValue = value; 26 | initialVelocity = velocity; 27 | springTime = 0f; 28 | } 29 | 30 | public override float Evaluate(float deltaTime) 31 | { 32 | springTime += deltaTime; 33 | 34 | var c = damping; 35 | var m = mass; 36 | var k = stiffness; 37 | var v0 = -initialVelocity; 38 | var t = springTime; 39 | 40 | var zeta = c / (2 * Mathf.Sqrt(k * m)); // damping ratio 41 | var omega0 = Mathf.Sqrt(k / m); // undamped angular frequency of the oscillator (rad/s) 42 | var x0 = endValue - startValue; 43 | 44 | var omegaZeta = omega0 * zeta; 45 | var x = 0f; 46 | var v = 0f; 47 | 48 | if (zeta < 1) // Under damped 49 | { 50 | var omega1 = omega0 * Mathf.Sqrt(1.0f - zeta * zeta); // exponential decay 51 | var e = Mathf.Exp(-omegaZeta * t); 52 | var c1 = x0; 53 | var c2 = (v0 + omegaZeta * x0) / omega1; 54 | var cos = Mathf.Cos(omega1 * t); 55 | var sin = Mathf.Sin(omega1 * t); 56 | x = e * (c1 * cos + c2 * sin); 57 | v = -e * ((x0 * omegaZeta - c2 * omega1) * cos + (x0 * omega1 + c2 * omegaZeta) * sin); 58 | } 59 | else if (zeta > 1) // Over damped 60 | { 61 | var omega2 = omega0 * Mathf.Sqrt(zeta * zeta - 1.0f); // frequency of damped oscillation 62 | var z1 = -omegaZeta - omega2; 63 | var z2 = -omegaZeta + omega2; 64 | var e1 = Mathf.Exp(z1 * t); 65 | var e2 = Mathf.Exp(z2 * t); 66 | var c1 = (v0 - x0 * z2) / (-2 * omega2); 67 | var c2 = x0 - c1; 68 | x = c1 * e1 + c2 * e2; 69 | v = c1 * z1 * e1 + c2 * z2 * e2; 70 | } 71 | else // Critically damped 72 | { 73 | var e = Mathf.Exp(-omega0 * t); 74 | x = e * (x0 + (v0 + omega0 * x0) * t); 75 | v = e * (v0 * (1 - t * omega0) + t * x0 * (omega0 * omega0)); 76 | } 77 | 78 | currentValue = endValue - x; 79 | currentVelocity = v; 80 | 81 | return currentValue; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Runtime/ClosedForm.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 78a0200ae1eb54fbda066797eff434b6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/ExplicitRK4.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnitySpring.ExplicitRK4 4 | { 5 | /// 6 | /// Explicit Runge-Kutta 4th order aka RK4 7 | /// https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods 8 | /// 9 | public class Spring : SpringBase 10 | { 11 | float stepSize = 1f / 60f; // stable if < 1/36 12 | bool isFirstEvaluate = true; 13 | 14 | public override void Reset() 15 | { 16 | currentValue = startValue; 17 | currentVelocity = initialVelocity; 18 | } 19 | 20 | public override void UpdateEndValue(float value, float velocity) 21 | { 22 | endValue = value; 23 | currentVelocity = velocity; 24 | } 25 | 26 | public override float Evaluate(float deltaTime) 27 | { 28 | if (isFirstEvaluate) 29 | { 30 | Reset(); 31 | isFirstEvaluate = false; 32 | } 33 | 34 | var c = damping; 35 | var m = mass; 36 | var k = stiffness; 37 | 38 | var x = currentValue; 39 | var v = currentVelocity; 40 | var _x = currentValue; 41 | var _v = currentVelocity; 42 | 43 | var steps = Mathf.Ceil(deltaTime / stepSize); 44 | for (var i = 0; i < steps; i++) 45 | { 46 | var dt = i == steps - 1 ? deltaTime - i * stepSize : stepSize; 47 | 48 | // springForce = -k * (x - endValue) 49 | // dampingForce = -c * v 50 | var a_v = _v; 51 | var a_a = (-k * (_x - endValue) - c * _v) / m; 52 | _x = x + a_v * dt / 2; 53 | _v = v + a_a * dt / 2; 54 | 55 | var b_v = _v; 56 | var b_a = (-k * (_x - endValue) - c * _v) / m; 57 | _x = x + b_v * dt / 2; 58 | _v = v + b_a * dt / 2; 59 | 60 | var c_v = _v; 61 | var c_a = (-k * (_x - endValue) - c * _v) / m; 62 | _x = x + c_v * dt / 2; 63 | _v = v + c_a * dt / 2; 64 | 65 | var d_v = _v; 66 | var d_a = (-k * (_x - endValue) - c * _v) / m; 67 | _x = x + c_v * dt / 2; 68 | _v = v + c_a * dt / 2; 69 | 70 | var dxdt = (a_v + 2 * (b_v + c_v) + d_v) / 6; 71 | var dvdt = (a_a + 2 * (b_a + c_a) + d_a) / 6; 72 | 73 | x += dxdt * dt; 74 | v += dvdt * dt; 75 | } 76 | 77 | currentValue = x; 78 | currentVelocity = v; 79 | 80 | return currentValue; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /Runtime/ExplicitRK4.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 449e686c620494f18a322f82c4650af8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/SemiImplicitEuler.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnitySpring.SemiImplicitEuler 4 | { 5 | /// 6 | /// Semi-implicit Euler method 7 | /// https://en.wikipedia.org/wiki/Semi-implicit_Euler_method 8 | /// 9 | /// Proof and derived from http://box2d.org/files/GDC2015/ErinCatto_NumericalMethods.pdf 10 | /// 11 | public class Spring : SpringBase 12 | { 13 | float stepSize = 1f / 60f; // stable if < 1/51 14 | bool isFirstEvaluate = true; 15 | 16 | public override void Reset() 17 | { 18 | currentValue = startValue; 19 | currentVelocity = initialVelocity; 20 | } 21 | 22 | public override void UpdateEndValue(float value, float velocity) 23 | { 24 | endValue = value; 25 | currentVelocity = velocity; 26 | } 27 | 28 | public override float Evaluate(float deltaTime) 29 | { 30 | if (isFirstEvaluate) 31 | { 32 | Reset(); 33 | isFirstEvaluate = false; 34 | } 35 | 36 | var c = damping; 37 | var m = mass; 38 | var k = stiffness; 39 | 40 | var x = currentValue; 41 | var v = currentVelocity; 42 | 43 | var steps = Mathf.Ceil(deltaTime / stepSize); 44 | for (var i = 0; i < steps; i++) 45 | { 46 | var dt = i == steps - 1 ? deltaTime - i * stepSize : stepSize; 47 | 48 | // springForce = -k * (x - endValue) 49 | // dampingForce = -c * v 50 | var a = (-k * (x - endValue) + -c * v) / m; 51 | v += a * dt; 52 | x += v * dt; 53 | } 54 | 55 | currentValue = x; 56 | currentVelocity = v; 57 | 58 | return currentValue; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Runtime/SemiImplicitEuler.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b1e513599b2d34c3ea862297aff02eea 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/SpringBase.cs: -------------------------------------------------------------------------------- 1 | namespace UnitySpring 2 | { 3 | public abstract class SpringBase 4 | { 5 | // Default to critically damped 6 | public float damping = 26f; 7 | public float mass = 1f; 8 | public float stiffness = 169f; 9 | public float startValue; 10 | public float endValue; 11 | public float initialVelocity; 12 | 13 | protected float currentValue; 14 | protected float currentVelocity; 15 | 16 | /// 17 | /// Reset all values to initial states. 18 | /// 19 | public abstract void Reset(); 20 | 21 | /// 22 | /// Update the end value in the middle of motion. 23 | /// This reuse the current velocity and interpolate the value smoothly afterwards. 24 | /// 25 | /// End value 26 | public virtual void UpdateEndValue(float value) => UpdateEndValue(value, currentVelocity); 27 | 28 | /// 29 | /// Update the end value in the middle of motion but using a new velocity. 30 | /// 31 | /// End value 32 | /// New velocity 33 | public abstract void UpdateEndValue(float value, float velocity); 34 | 35 | /// 36 | /// Advance a step by deltaTime(seconds). 37 | /// 38 | /// Delta time since previous frame 39 | /// Evaluated value 40 | public abstract float Evaluate(float deltaTime); 41 | } 42 | } -------------------------------------------------------------------------------- /Runtime/SpringBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b2fe12d2a3a9748dd973875f66140dfc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Thammin.UnitySpring.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Thammin.UnitySpring", 3 | "references": [], 4 | "includePlatforms": [], 5 | "excludePlatforms": [] 6 | } -------------------------------------------------------------------------------- /Runtime/Thammin.UnitySpring.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c28d16b56f0464b4ebe472985a9a60d1 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/VerletIntegration.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnitySpring.VerletIntegration 4 | { 5 | /// 6 | /// Verlet Integration 7 | /// https://en.wikipedia.org/wiki/Verlet_integration 8 | /// 9 | public class Spring : SpringBase 10 | { 11 | float stepSize = 1f / 61f; // stable if < deltaTime && < 1/60 12 | bool isFirstEvaluate = true; 13 | float currentAcceleration = 0.0f; 14 | 15 | public override void Reset() 16 | { 17 | currentValue = startValue; 18 | currentVelocity = initialVelocity; 19 | currentAcceleration = 0.0f; 20 | } 21 | 22 | public override void UpdateEndValue(float value, float velocity) 23 | { 24 | endValue = value; 25 | currentVelocity = velocity; 26 | } 27 | 28 | public override float Evaluate(float deltaTime) 29 | { 30 | if (isFirstEvaluate) 31 | { 32 | Reset(); 33 | isFirstEvaluate = false; 34 | } 35 | 36 | var c = damping; 37 | var m = mass; 38 | var k = stiffness; 39 | 40 | var x = currentValue; 41 | var v = currentVelocity; 42 | var a = currentAcceleration; 43 | 44 | var _stepSize = deltaTime > stepSize ? stepSize : deltaTime - 0.001f; 45 | var steps = Mathf.Ceil(deltaTime / _stepSize); 46 | for (var i = 0; i < steps; i++) 47 | { 48 | var dt = i == steps - 1 ? deltaTime - i * _stepSize : _stepSize; 49 | 50 | x += v * dt + a * (dt * dt * 0.5f); 51 | // springForce = -k * (x - endValue) 52 | // dampingForce = -c * v 53 | var _a = (-k * (x - endValue) + -c * v) / m; 54 | v += (a + _a) * (dt * 0.5f); 55 | a = _a; 56 | } 57 | 58 | currentValue = x; 59 | currentVelocity = v; 60 | currentAcceleration = a; 61 | 62 | return currentValue; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /Runtime/VerletIntegration.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 023115d0478d64bb7bed63325dc94ecf 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5aeb2a1a7c5b84d8aa0e40a99071fee4 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c08df7f66eed74363b493647db164aee 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/PerformanceTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Diagnostics; 3 | using UnityEngine; 4 | using Debug = UnityEngine.Debug; 5 | 6 | namespace UnitySpring.Tests 7 | { 8 | public class PerformanceTest 9 | { 10 | const int Count = 100000; 11 | 12 | [Test] 13 | public static void _ClosedForm() 14 | => Benchmark(CreateSprings(), "ClosedForm"); 15 | 16 | [Test] 17 | public static void _SemiImplicitEuler() 18 | => Benchmark(CreateSprings(), "SemiImplicitEuler"); 19 | 20 | [Test] 21 | public static void _ExplicitRK4() 22 | => Benchmark(CreateSprings(), "ExplicitRK4"); 23 | 24 | [Test] 25 | public static void _VerletIntegration() 26 | => Benchmark(CreateSprings(), "VerletIntegration"); 27 | 28 | [Test] 29 | public static void UnitySmoothDamp() 30 | => Benchmark(CreateSprings(), "UnitySmoothDamp"); 31 | 32 | static void Benchmark(SpringBase[] springs, string name) 33 | { 34 | var sw = new Stopwatch(); 35 | var count = 0; 36 | var sum = 0.0; 37 | 38 | while (count < 120) 39 | { 40 | sum += Step(); 41 | count++; 42 | } 43 | 44 | Debug.Log($"[{name}] Average time for {Count} spring per frame : {(sum / count).ToString("0.000")} ms"); 45 | 46 | double Step() 47 | { 48 | sw.Reset(); 49 | sw.Start(); 50 | foreach (var s in springs) s.Evaluate(1 / 60f); 51 | sw.Stop(); 52 | return GetPreciseTime(sw); 53 | } 54 | } 55 | 56 | static T[] CreateSprings() where T : SpringBase, new() 57 | { 58 | var springs = new T[Count]; 59 | for (var i = 0; i < springs.Length; i++) 60 | { 61 | springs[i] = new T(); 62 | } 63 | return springs; 64 | } 65 | 66 | static double GetPreciseTime(Stopwatch sw) 67 | { 68 | return 1000.0 * (double) sw.ElapsedTicks / Stopwatch.Frequency; 69 | } 70 | } 71 | 72 | class UnitySmoothDamp : SpringBase 73 | { 74 | float smoothTime = 0.1f; 75 | float maxSpeed = Mathf.Infinity; 76 | 77 | public override void Reset() 78 | { 79 | currentValue = startValue; 80 | currentVelocity = initialVelocity; 81 | } 82 | 83 | public override void UpdateEndValue(float value, float velocity) 84 | { 85 | endValue = value; 86 | currentVelocity = velocity; 87 | } 88 | 89 | public override float Evaluate(float deltaTime) 90 | { 91 | currentValue = Mathf.SmoothDamp(currentValue, endValue, ref currentVelocity, smoothTime, maxSpeed, deltaTime); 92 | return currentValue; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/Editor/PerformanceTest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bcbce0b3cfd3a4f5c81485d3f5377e31 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/Thammin.UnitySpring.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Thammin.UnitySpring.Tests", 3 | "references": [ 4 | "Thammin.UnitySpring" 5 | ], 6 | "optionalUnityReferences": [ 7 | "TestAssemblies" 8 | ], 9 | "includePlatforms": [ 10 | "Editor" 11 | ] 12 | } -------------------------------------------------------------------------------- /Tests/Editor/Thammin.UnitySpring.Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9e949fb209bcb471da9050adda8da354 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.thammin.unity-spring", 3 | "version": "0.0.1", 4 | "displayName": "Unity Spring", 5 | "description": "Spring physics that models a damped harmonic oscillator.", 6 | "unity": "2019.1", 7 | "unityRelease": "0b5", 8 | "dependencies": {}, 9 | "keywords": [ 10 | "spring", 11 | "physic", 12 | "damped", 13 | "harmonic", 14 | "oscillator", 15 | "tween", 16 | "animation", 17 | "dynamic" 18 | ], 19 | "author": { 20 | "name": "Paul Young", 21 | "email": "thammin@live.co.uk", 22 | "url": "https://github.com/thammin" 23 | } 24 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c6663946781f04017a8121079ceb540e 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------