Today I am going to walk you through a bug I encountered while creating my most recent project Project Stardust. If a camera in Unity moves too far away from the origin of the play space, things start to break down mathematically. Visual artifacts become very apparent, most often seen in the form of vibrating or flickering meshes/textures.
The Cause: Floating point rounding error
Since the Transform’s x, y, and z coordinates are of C# float data type there are limitations to ranges in which rounding errors do not occur. For a full write-up of why this happens you must understand the IEEE standard for floating point numbers which you can read about here. A rudimentary calculation based on established ranges listed here show that a play area would be confined to around 10km or ~16k unity units in any direction before things start to deteriorate fairly quickly.
What if you want to allow the player to build an infinitely large play space for your world?
The fix: Move the world
I very recently found out about a technique called Floating Origin which is used to keep the origin centered on the player. This continuously offsets the origin on a fixed interval which will recenter the world’s x,y,z coordinates about the player, then take every object in the scene and move it by the distance delta.
The result is the movement of the world around the player, rather than moving the player around the world. If the environment around the player to be shifted around is not optimized well enough then each “movement” of the world will be a computationally intensive thing to compute, resulting in bad CPU performance.
The script shown here is an example of such a movement of the world about the player. Attach it to the player’s camera [source]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
// FloatingOrigin.cs // Written by Peter Stirling // 11 November 2010 // Uploaded to Unify Community Wiki on 11 November 2010 // Updated to Unity 5.x particle system by Tony Lovell 14 January, 2016 // fix to ensure ALL particles get moved by Tony Lovell 8 September, 2016 // URL: http://wiki.unity3d.com/index.php/Floating_Origin using UnityEngine; using System.Collections; [RequireComponent(typeof(Camera))] public class FloatingOrigin : MonoBehaviour { public float threshold = 100.0f; public float physicsThreshold = 1000.0f; // Set to zero to disable #if OLD_PHYSICS public float defaultSleepVelocity = 0.14f; public float defaultAngularVelocity = 0.14f; #else public float defaultSleepThreshold = 0.14f; #endif ParticleSystem.Particle[] parts = null; void LateUpdate() { Vector3 cameraPosition = gameObject.transform.position; cameraPosition.y = 0f; if (cameraPosition.magnitude > threshold) { Object[] objects = FindObjectsOfType(typeof(Transform)); foreach(Object o in objects) { Transform t = (Transform)o; if (t.parent == null) { t.position -= cameraPosition; } } #if SUPPORT_OLD_PARTICLE_SYSTEM // move active particles from old Unity particle system that are active in world space objects = FindObjectsOfType(typeof(ParticleEmitter)); foreach (Object o in objects) { ParticleEmitter pe = (ParticleEmitter)o; // if the particle is not in world space, the logic above should have moved them already if (!pe.useWorldSpace) continue; Particle[] emitterParticles = pe.particles; for(int i = 0; i < emitterParticles.Length; ++i) { emitterParticles[i].position -= cameraPosition; } pe.particles = emitterParticles; } #endif // new particles... very similar to old version above objects = FindObjectsOfType(typeof(ParticleSystem)); foreach (UnityEngine.Object o in objects) { ParticleSystem sys = (ParticleSystem)o; if (sys.simulationSpace != ParticleSystemSimulationSpace.World) continue; int particlesNeeded = sys.maxParticles; if (particlesNeeded <= 0) continue; bool wasPaused = sys.isPaused; bool wasPlaying = sys.isPlaying; if (!wasPaused) sys.Pause (); // ensure a sufficiently large array in which to store the particles if (parts == null || parts.Length < particlesNeeded) { parts = new ParticleSystem.Particle[particlesNeeded]; } // now get the particles int num = sys.GetParticles(parts); for (int i = 0; i < num; i++) { parts[i].position -= cameraPosition; } sys.SetParticles(parts, num); if (wasPlaying) sys.Play (); } if (physicsThreshold > 0f) { float physicsThreshold2 = physicsThreshold * physicsThreshold; // simplify check on threshold objects = FindObjectsOfType(typeof(Rigidbody)); foreach (UnityEngine.Object o in objects) { Rigidbody r = (Rigidbody)o; if (r.gameObject.transform.position.sqrMagnitude > physicsThreshold2) { #if OLD_PHYSICS r.sleepAngularVelocity = float.MaxValue; r.sleepVelocity = float.MaxValue; #else r.sleepThreshold = float.MaxValue; #endif } else { #if OLD_PHYSICS r.sleepAngularVelocity = defaultSleepVelocity; r.sleepVelocity = defaultAngularVelocity; #else r.sleepThreshold = defaultSleepThreshold; #endif } } } } } } /* Addendum from DulcetTone on 22 April 2018: a user named Marcos-Elias sent me a message with an optimization he found helpful on recent versions of Unity which include the new "SceneManager" functionality. He suggests replacing this fragment of my code: Object[] objects = FindObjectsOfType(typeof(Transform)); foreach(Object o in objects) { Transform t = (Transform)o; if (t.parent == null) { t.position -= cameraPosition; } } with the following code, to avoid having to process ALL objects to find the root objects for (int z=0; z < SceneManager.sceneCount; z++) { foreach (GameObject g in SceneManager.GetSceneAt(z).GetRootGameObjects()) { g.transform.position -= cameraPosition; } } I have not use this myself, as yet, but I wonder if an additional optimation would be the following, to update only the active scene: foreach (GameObject g in SceneManager.GetActiveScene().GetRootGameObjects()) { g.transform.position -= cameraPosition; } */ |