-
Notifications
You must be signed in to change notification settings - Fork 0
Understanding The Simulator
The foundations of the simulator can be broken down into 4 main blocks:
- Units & Scaling > This takes into consideration how celestial objects and the physics being applied are rescaled appropriately for the simulated model.
- Physics > This looks into what is needed to correctly model physics and how to implement it taking into consideration Units & Scaling.
- Building The Scene > This involves assembling GameObjects, PreFabs and other assets to construct the scene in the Unity Engine that is to be simulated.
- User Experience > This details what was done to give the user control of the simulation whilst in a VR environment e.g. constructing UI and tools.
Once these are in place, they can be added to to evolve the simulator into a multi-featured tool to demonstrate gravity between masses.
CASE-R's BSc report for the original project is also linked later for additional information if needed. It can also be found here.
Just like normal unit systems, building this simulator required definition of a new unit system that was comparable to Astronomical Units (AU) and the SI Unit system. One reason for this is due to the 6-9 digit precision of float variable, which are used throughout most script components like in the Rigidbody component used in Celestial PreFabs. Another is that the AU system reduces the very large values in the SI system by too much, and so we have to reduce this scaling factor so that the Unity Engine can use reasonable float values when calculating something with length or mass for example. Thus, the most reasonable unit definitions would best be based off a variation of the AU system where mass is measured in Earth Masses (M), length is measured in Astronomical Units (AU), and time is measured in Mean Solar Days (D) which approximates to 24 hours.
| Unit System | Length | Mass | Time |
|---|---|---|---|
| SI | (1.49598e+11)m | (1.98850e+30)kg | (86400)s |
| AU | (1)AU | (1)M_Solar | (1)D |
| Unity | (2500)x | ~(333,000)m | (1)t |
From the table above it can be inferred that 2500x=1AU, 1m=1M and 1t=1D, which whilst poses a problem for rendering the sizes of Celestial bodies and simulating orbits to scale it is a good enough choice to shrink the physical size of the Solar System into Unity. A new Excel Datasheet should be accessible to all to show how properties were calculated and converted.
The need to define a new unit system could potentially be avoided with the use of double-precision data types and more complex methods, but this was beyond the scope of the project. This may have a domino effect, requiring a new physics system as the
Rigidbodycomponent may become obsolete.
Once a choice of units is made and scaling of values can be done, the next step is to also redefine the units of any physical constants that will be used in our calculations. An example of this is the gravitational constant G which can be done using Kepler's Third Law for a known case. With this change, the gravitational force between two GameObjects can correctly be calculated for the simulation space and applied to them through a custom script component. This is what Gravity.cs does every FixedUpdate() (see here):
public void GravityCalc()
{
for (int i = 0; i <= celestialList.Length - 1; i++) // Links index i to a celestial object in the array
{
for (int j = i + 1; j <= celestialList.Length - 1; j++) // Pairs index i with the index j that corresponds to the next celestial object
{
float mass_i = celestialList[i].GetComponent<Rigidbody>().mass; // Assigns mass values that are entered via the editor
float mass_j = celestialList[j].GetComponent<Rigidbody>().mass;
Vector3 direction = (celestialList[j].transform.position - celestialList[i].transform.position).normalized; // Finds a unit vector to describe the direction from one object to the other
Vector3 gravForce = ((simSettings.gravitationalConstant * mass_i * mass_j) / (Mathf.Pow(Vector3.Distance(celestialList[j].transform.position, celestialList[i].transform.position), 2))) * direction; // Calculates the gravitational force vector
celestialList[i].GetComponent<Rigidbody>().AddForce(gravForce); // Applies the force vectors to each celestial object, simulating gravitation towards each other at that moment
celestialList[j].GetComponent<Rigidbody>().AddForce(-gravForce);
}
}
}The code snippet shows an optimised method of calculating the required parameters between two Celestial objects from a GameObject array called celestialList, which essentially iterates once through all possible pairs and apply Newton's Third Law at the end to half the number of calculations made per call/update. This does have a slight advantage over the gravity script used in Coderious's tutorial, allowing better performance with a higher n-body simulation (n<40) but is nothing like what is achievable with the use of compute shaders in N-Body simulations i.e. this blog.
This could perhaps be a future task to replace the gravity system with this, freeing up a large percent of resources. This was not implemented at first as it seemed like it was beyond what was necessary.
In order to induce correct orbits with physics, we will need to apply velocity to each Celestial body based on their starting positions relative to their host/the nearest body. The most convenient way would be to create a similar loop found in Gravity.cs and apply the Vis Viva Equation for elliptical orbits to every celestial. Not only could this require many calculations on startup, but it could also cause an imbalance between the resultant (gravitational) force and resultant velocity vector acting on one body at any moment in time. The latter actually posed a problem to the simulation, causing inaccuracies in the elliptical orbits of a many-body simulation.
To work around this issue, a tier system was introduced to the GameObject Hierarchy where Celestial objects would be parented to others based on what we already knew about the system. For example, the general tier system would go Star > Planet > Moon which would mean the Sun would parent all planets and where applicable some planets i.e. Earth would parent a moon.
This is put into practice using InitialiseVelocity(), which contains a series of checks within a for loop for a "Celestial" child object. Hence this executes throughout the celestialList and only executes completely for Star > Planet and Planet > Moon pairs. During this all orbital parameters are taken from an external script CelestialProperties.cs attached to the Celestial PreFab where these values are entered in the inspector beforehand.
Pseudo-code for InitialiseVelocity()
- Begin
foreachloop for the "parent" GameObject. - Find child count of the current parent object.
- Begin
forloop through each child object usingtransform.GetChild(i)and check ifchild[i]has the tag "Celestial". - If the above is true, apply Vis Viva equation to velocity of the Rigidbody component of the child object.
- Once velocities are calculated and applied, the parent of each celestial must be stored in a
public List<GameObject> celestialParentsto be used later on when restarting the scene. - Finally, invoke a new method to reparent all celestials to be direct children of the
SystemGameObject used to contain all Solar System objects a couple of seconds after the above steps are complete. The reason for this is explained later in the VR section
It is important to note that to get correctly modelled elliptical orbits, the shape, orientation and position must also be defined for each Celestial so that we get a non-flat Solar System with eccentric orbits like Mercury's. For detailed information on this, see the Fundamentals of Astrophysics textbook which was used to understand how these work but to summarise we take an starting reference orbit (i.e. Earth's orbit) and rotate and resize it in 3D space to mirror what we would see for other planets using quaternion rotations.
See
CelestialProperties.csto see where and how these are done.
This avoids the need for complex setup by using a combination of "Heliocentric-Ecliptic" and "Perifocal" coordinate systems which lets us treat the Sun as the origin of our system and the Earth's orbit as the ecliptic plane. This is different to positioning the System GameObject at the origin of worldspace, which itself is not used in physics calculations and only to hold scripting components for the simulation in general.
Full detail can by found in CASE-R's BSc report by clicking the below diagram.
Each Celestial object in the simulator is based off 1 of 2 PreFab models, these models mostly differ by the inclusion of a script that generates a ring mesh to model planetary rings from Board To Bits' tutorials. These are dragged into the scene hierarchy and edited from the inspector within the scene meaning each Celestial becomes unique across each scene
Within each PreFab, there is a structured list of GameObjects with unique purposes given by the scripting components they hold. These are summarised in the table below, where it is expected that you look at the linked documentation to better understand its function. Only when it is unclear, will further details be given.
Celestial PF Overview
Items marked with an asterisk* are specific to the CelestialRings PF image.
| GameObject | Components | Component Documentation | Overview |
|---|---|---|---|
| CelestialPF |
Transform, Rigidbody, Celestial Properties, Rigidbody Shift Controller, XR Grab Interactable, Starlight, Sphere Collider
|
Transform, Rigidbody, CelestialProperties.cs, RigidbodyShiftController.cs, XRGrabInteractable.cs, Starlight.cs, Sphere Collider, | This is the main object for a Celestial, all physics and simulator mechanics are applied to this as it parents all other listed GameObjects. It is here where properties, VR player interactions and light positioning are managed. This is also given the Celestial tag and this is placed on the Grab layer (see below). |
| Sphere |
Transform, Sphere (Mesh Filter), Mesh Renderer, Sphere Collider, Planet Ring Mesh Generator*, Material
|
Primitive Sphere, Planet Ring Mesh Generator* | This manages the main rendering of a Celestial object, and in some cases its planetary rings. This is placed on the Collisionless layer to avoid any collisions/interactions with anything in the active scene. |
| Sphere Ring* |
Transform, (Mesh Filter), Mesh Renderer, Material
|
Planet Ring Mesh Generator* | The above applies here also, except this is purely generated as a result of the above and custom materials are applied for unique rings. |
| Spot Light |
Transform, Light
|
Light | This works as part of the lighting system workaround implemented due to the drawbacks of using a single point light source for the Sun. This is positioned by Starlight.cs to recreate the incoming light rays from the direction of the Sun. The layer this is on should not matter. |
| Trail |
Transform, Trail Renderer
|
Trail Renderer | This allows a visual indicator of where a Celestial has most recently travelled, and is linked to the Floating Origin implementation. The layer this is on should not matter. |
| LatchDetector |
Transform, Sphere Collider
|
This is used for the aim-assist feature when trying to interact with Celestials using ray interactors. It is placed on its own LatchDetection layer for this purpose. |
The use of several colliders and layers allows you to have flexibility in triggering custom events and interactions for your own scripts. An example of this is using a collider to detect when a player enters a region so that you can make something appear like text. This is because colliders in Unity can be used for both physics collisions as well as non-physical detection. Examples of the different uses for colliders in the simulator are:
- VR Interactions; Allowing users to grab objects with VR input and have other actions
- Proximity Detection; Detecting what the closest Celestial is to a point
- Aim Assist; Detecting when users are trying to target a Celestial with a ray renderer without intention to affect it in anyway
- Floating Origin; Triggering an Event to shift the entire world and reset the user back to support large world sizes
Using the Collision Matrix, we can determine how layers collide with each other without directly impacting each other and even themselves. In practice this means we can have Celestial objects collide like marbles but UI objects that require colliders will not collide with anything but the rays used to grab and move UI panels around. It is also important to understand that whilst these can mostly be managed from within the Editor itself to create trigger custom events, it is sometimes necessary to understand layer masks which are particularly useful in proximity detection and using Physics.OverlapSphere() to check for colliders in a defined spherical region.
Layer Mask Table
| Layer Name | Layer # | Layer Mask | Purpose |
|---|---|---|---|
| Default | 0 | 1 << 0 | Default layer for collisions to take place. |
| TransparentFX | 1 | 1 << 1 | "Lens flares will show through the collider of an object on this layer etc." See here |
| Ignore Raycast | 2 | 1 << 2 | "An object on this layer will be ignored by raycasts (including OnMouseOver, etc.)." See here |
| Grab | 3 | 1 << 3 | Objects on this layer can be grabbed by the user when they aim with either controller and the ray, then pressing Axis1D.PrimaryHandTrigger. |
| Water | 4 | 1 << 4 | N/A |
| UI | 5 | 1 << 5 | Objects on this layer are considered as UI, and thus will not collide with anything but raycasters. This will allow the user to control the simulation via UI in a VR environment. |
| GrabUI | 6 | 1 << 6 | This is for specific UI GameObjects that can be grabbed by the player like with the Grab layer except it is independent of other GameObjects like Celestials. |
| Collisionless | 7 | 1 << 7 | Objects on this layer will not collide with anything, including rays. This is useful for render objects like the sphere and rings of any Celestial. |
| LatchDetection | 8 | 1 << 8 | Objects on this layer are used to help focus the targeting of Celestial objects by redirecting the ray renderers. These interact with both physics raycasting and VR raycasting. |
One of the major changes in this 2nd iteration of the VRSS Simulator is the addition of the "Floating Origin" technique which is a workaround Unity's limit on world size. Using this allows you to increase the world size by a considerable factor, giving a much better sense of scale when traversing the simulator during runtime. Although testing the limit of this, the unit system was redefined to scale distances by a 25x hence the inconsistency between the old unity units and the unity units in Units & Scaling.
The "Floating Origin" technique confines the movement of the player to a local region centered around the origin of your scene, and transforms the entire world by an offset and reset the player to the origin to give the illusion that they are traversing the world around them. This maintains a consistent level of resolution of transform data between objects as this is the effect of single-point float variables in most engines. This is best shown in Bound Fox Studios' Tutorial and/or the accompanying blog tutorial by Manuel Reuber. These tutorials and some tips from the creators actually helped guide the implementation of a Floating Origin here.
Due to time constraints, a full walkthrough of what was done will not be written here especially as the above tutorials mostly apply. The main difference is how the TrailRendererOriginShiftController.cs script is setup and used in this simulator. Given that each Celestial object consists of different components that are shifted upon the OriginShiftEvent i.e. Rigidbody, Transform and Trail Renderers it became a problem trying to shift the trail renderer positions every time for each trail object. To work around this, the script was rewritten to iterate through a list of trail renderers and shift these positions after collating them via the celestialList upon startup.