3D explorer first upgrade
Last changed on
The 3D explorer has enjoyed a couple of upgrades:
- Ability to navigate a galaxy itself, as opposed to seeing just its "macro structure"
- First data streaming version put online
Navigation
The explorer make uses of BabylonJS, one of the best WebGL frameworks out there along with ThreeJS. The camera used is a modified BABYLON.FlyCamera (more details about this below). As a result the following very standard WASD+ controls are used:
| Keyboard W | Move forward |
|---|---|
| Keyboard S | Move backward |
| Keyboard A | Skid left |
| Keyboard D | Skid right |
| Keyboard Q | Skid down |
| Keyboard E | Skid up |
| Left mouse hold + move | Free camera target move |
| Right mouse hold + move | Camera roll |
This is the closest I could get my hands on to reproduce the "ghost in space" navigation possibilities.
Camera movement speed is limited -at the moment- to an arbitrary value of that I judged to be both: 1) fast enough not to crawl and 2) not too quick so as not to encounter server/client communication lag issues as explained in this post.
Floating origin camera
As one might except, a galaxy is large; we are talking about scales the human mind can barely comprehend. But this is a digital "simulation" and so we should be free of such constraints, right?
We are, but only to some extent.
Ad lumens wants to try and simulate the huge, the reasonably small and anything in between. By "reasonably small" is meant an in-virtual-world resolution of about 1 km.
Now imagine a dwarf galaxy whose diameter is 15,000 light years; this is roughly equivalent to 141910500000000000 kilometres, perhaps more easily readable as 142 × 1015 kilometres. Such a number is easy for modern hardware to handle, whether for calculations or display. Where such large numbers potentially become a problem is when they must be potentially displayed alongside fractions. In other words, whichever the number of bits is available to store and compute floating point numbers (be it 16, 32 or 64), we must contend with the fact that this number of bits will have problems handling a huge number when that number is also supposed to contain an accurate fraction.
The engine tries to reach a resolution of 1 kilometre. For a wannabe game alternating between extreme strategic views and smallish tactical views, this seems like an appropriate value:
- In-space tactics probably will not be influenced too much by such a granularity, given the extreme distances of even interplanetary space. An exception to this might be the simulation of small vessels dogfighting, but we aren't there yet…
- For rendering of planetary surfaces, it will allow reasonably fine details to be displayed
- For tactical, management-related decisions, such as resource surveys, extraction and the like, it will ensure the importance of the right choice or series of choices.
- Interesting surface movement and combat become a possibility
Anyway, the way to solve this when rendering a seamless (almost) no-loading galaxy is to use a Floating origin camera. The secret behind this camera is that it never moves; instead, the 3D world moves around it.
This fixes the main problem of precision: with a normal camera standing at coordinates [141910400000000000.123456789, 141910400000000000.123456789, 141910400000000000.123456789], then the WebGL engine will have issues displaying the fractional part of objects sitting close to the camera (it will have run out of bits -or almost- by the time it needs to take fractions into account). The floating origin fixes it:
- objects close to the camera stand at coordinates that are "small enough" for their fractional part to be properly displayed;
- objects standing far away from the camera will lose that precision, but we don't care: they are so far away we will not notice them being displayed slightly off as a consequence of their coordinates being rounded/truncated.
Procedural data streaming
In a nutshell:
- the procedural data is computed server-side using the rust number-crunching part of the engine; it implements the principle of LOD bubbles briefly explained here.
- the data is pushed via websocket to the client
- … and is rendered client-side using BabylonJS
The LOD bubbles are defined as follows; these values/thresholds are not set in stone, so please do expect them to change. I do plan to enable dynamic change of the LOD bubble sizes/ranges as one navigates. Indeed, given that this version's main problem lies in performance, having the ability to vary the LOD bubble size will enable the explorer to cater to as much hardware variety as possible.
| Cube range | Passive LOD level | Explanation |
|---|---|---|
| 3 | LOD_STAR | In a ~30 light years bubble around the point of view, stars can be passively seen: color, size differences although reduced. |
| 6 | LOD_SYSTEM | In a ~60 light years bubble around the point of view can be seen the presence of stars. |
Please note that the above table is about passive LOD. It will not prevent clicking/picking a 3D object in the scene and getting more information about it - thus triggering active LOD.
A bit of Venn logic and sets
I thought I would try and explain how the rust part of the engine computes which data should be sent to the client. Not rocket science, but interesting nonetheless! So here are the steps, expressed as a table:
| Order | Name | Explanation | Details/pseudo code |
|---|---|---|---|
| 1 | Previous systems | The engine needs to know which systems have been seen previously, and at what level of detail: either LOD_SYSTEM or LOD_STAR. Note that on initial load, this step results in "no data", meaning that nothing was previously seen. | Previous systems IDs are received as two FxHashSet |
| 2 | Compute cubes | Cubes are used as system rendering proxies and result in the creation of two separate system groups: one at LOD_SYSTEM, and one at LOD_STAR. | One FxHashSet (system IDs) per LOD, and one FxHashMap (full system data) per LOD |
| 3 | Compute LOD_SYSTEM systems to remove | The engine compares the previous list of LOD_SYSTEM systems and the current one. And extracts those to be removed for the "current tick" | lod_sys_to_remove = lod_sys_prev |
| 4 | Compute LOD_STAR systems to remove | The engine compares the previous list of LOD_STAR systems and the current one. And extracts those to be removed for the "current tick" | lod_star_to_remove = lod_star_prev |
| 5 | Compute LOD_STAR systems to add | Those are the LOD_STAR systems that weren't present at all on the previous tick. This is done by comparing the current LOD_STAR systems to both previous list of systems | lod_star_to_add = lod_star_current |
| 6 | Compute LOD_SYSTEM systems to add | Those are the LOD_SYSTEM systems that weren't present at all on the previous tick. This is done by comparing the current LOD_SYSTEM systems to both previous list of systems | lod_sys_to_add = lod_sys_current |
| 7 | Compute LOD_SYSTEM systems to upgrade | Those systems go from LOD_SYSTEM to LOD_STAR; in other words, the camera got close enough to them so they resulted in a higher passive LOD | lod_sys_upgrade = lod_star_current |
| 8 | Compute LOD_STAR systems to downgrade | Those systems from LOD_STAR to LOD_SYSTEM; in other words, the camera moved far way enough from them to result in less visual information. But they remain close enough to be visible. | lod_star_downgrade = lod_sys_current |
The eagle-eyed will have noted that FxHashSet is mentioned above, as opposed to rust's native HashSet. Similarly, FxHashMap is used in lieu of HashMap. Why? Because of performance: even though the data involved never involves more a few tens of thousands of keys for each pass, I thought I would use the faster version. Especially since security/cryptography is not involved in any way and the relevant keys never reach the client anyway. To quote:
This hashing algorithm should not be used for cryptographic, or in scenarios where DOS attacks are a concern.
Some useful links about sets: Python; Rust.
Sending order matters (apparently)
Once the above is nice and **cough**tidy, the websocket pushes it all to the client, in the following order:
- lod_star_to_remove
- lod_sys_to_remove
- lod_star_downgrade
- lod_sys_upgrade
- lod_star_to_add
- lod_sys_to_add
Following the overall logic of "remove resources from the client before adding anything, thus favouring less load"
Needed improvements
I am not exactly satisfied with the client-side performance (whereas server-side multithreading does wonders, even on a virtualbox). I have not found a solution to this problem yet, but some ideas include:
- Client-side, the systems are displayed in a
SolidParticleSystem, which is given an allocated particles budget. Maybe splitting this into two smaller separate systems; the issue is not the faces/vertices count or the number of draw calls (the latter being at around 5, so pretty low), but the time takes for Javascript to loop/update on the particles when changes are required on them. Even when usingMapnative objects to speed up indexing. - Client-side again, I need to experiment with the order in which data is sent over the wire, to actually assess if the overall logic from above is indeed correct. Might not be.
- Using two
SolidParticleSystems will also help reduce the overall number of triangles: theLOD_SYSTEMsets, which are far larger than theLOD_STARones, are a perfect target: their systems are faraway; so reducing their sphere subdivisions will have a major impact, one that may even allow to increase theLOD_STARsphere subdivisions.
In the works
A few things are not available yet, but are (more or less) in progress:
- Star proximity trigger: causes the server to send detailed system data when getting close enough to a star. This results in orbits, planets, etc, being displayed along with the star ecliptic.
- Planet proximity trigger: same thing, with planets :-)
- Entity click: the ability to click on a star/planet/whatever and get details similar to the ones found in the data explorer.
And that's it for this post. Please, if you encounter problems, do drop me a line and tell me about it!
Please signin to add your comment.