Optional
This component is an optional feature in the metaverse_client crate. Projects that don't require this can compile with the feature disabled in Cargo.tomlTerrain
After completing login, the server will begin sending LayerData packets to the client. LayerData packets contain several patches, which are described by the initial Layer body header. Packet parsing specifics can be found on docs.rs.Unlike most other packets, the packet parsing only parses the packet header, and the body header of the LayerData object. and stops when it gets to the body data. This is because the body parsing is very complex, and should be handled asyncronously to avoid creating a bottleneck.
1.
The LayerData is received from the sever from the udp_read function
2.
The LayerData packet is parsed by the PacketData::from_bytes(), determining the stride, size, and type of the contained layers.
3.
The LayerData packet is received by the mailbox here, without fully parsing the body yet.
4.
parse_layer_data is called on the unstructured body data
LayerType
The server represents layer types as numbers. Some of them represent letters that make sense, but other seem to be picked at random. The representation goes as follows:| Land | 76 |
| LandExtended | 77 |
| Water | 87 |
| WaterExtended | 88 |
| Wind | 55 |
| WindExtended | 57 |
| Cloud | 56 |
| CloudExtended | 58 |
Land From Packet
Now that we have determined the type of the patch, we can begin work towards generating 3d models that our UI can display. The from_packet() function handles parsing the patch data from the unstructured data.At the beginning of each land patch is a a header. They all follow the same format, and are not compressed with the rest of the layer data. The header is parsed using the TerrainHeader::from_bytes() function.
Terrain Header
| Quantized world bits | Used to determine if we have reached the end of patches, along with determining the world_bits. |
| DC offset | Scales the decompressed data back to a real-world value |
| Range | a multiplier used for decompression |
| Patch IDs | 4 or 10 bytes that will be used to determine the XY location. |
Body:
Compressed layer data
Patch ID
The handling of the PatchID field is extremely confusing. For some reason, this field can be is 32 bits if it is an extended patch, and 10 bits if it is not. The binary is read into a buffer, with the first half of the bytes being used as the x value, and the second half of the bytes being used as the y value.However, the binary is laid out in big-endian format, which makes this very difficult to see. you might have raw data that looks like this after removing the headers from the packet
Bytes: 0 0 0 164 65 1 0 8 96 ...
| | | | | | | | |
| | | | | | | └────└─ [8, 96] patch ID bytes
| | | | | └────└─ [1,0] range (2 bytes)
| └────└────└───└─[0, 0, 164, 65] f32 DC offset (4 bytes)
└─[0] quantized_world_bits (1 byte)
Read as u8s, the values are
| 8 | 00001000 |
| 96 | 01100000 |
| 00001000 | 01 |
| 01 | 00001000 |
| 0100 | x:8 |
| 01000 | y:8 |
I have to be so for real with you right now, this is one of the most fucked up things I have ever seen in my life. All of this to save 3 bits in transport.
Parse Heightmap
Now that the TerrainHeader is taken care of, the next step is to handle the bytes that follow it.Parse_heightmap() is called, which runs an algorithm designed by secondlife to decompress terrain. Not all terrain patches are the same size, but they can be read if the algorithm is correct.
- Loop over every point in the patch
- If there is 0 at the first position checked, that means this point is zero height, and no heights need to be decoded.
- If there is a 0 at the second position checked, that means the following data is all zero, and exit the loop, writing the rest of the array to zero.
- If we have read up to the third bit, that means one of the first two is not zero. The third bit contains the sign information, to determine if it is a positive or negative height.
- Following this, we read as many bits as is specified by the terrain header's world_bits, and decode those in the same way we decoded the PatchID.
- apply the sign to the decoded height.
Decompress Heightmap
Now with the data parsed, we can decompress the heightmap data. The LayerData is encoded in a way that is similar to JPEG compression. Instead of storing data in rows, it stores it in a zig-zag pattern. build_copy_matrix stores where those values are in the zigzag. for example: I have unencoded data
a b c d e f g h i j k...
0 1 2 3 4 5 6 7 8 9 10...
this would be encoded as
a b f g n o aa bb rr ss ...
0 1 5 6 14 15 27 28 44 45 ...
the copy matrix contains the unencoded data's location.
encoded[3] would be g
copy_matrix[3] would be 6
by reading through the encoded data and the copy matrix at the same time, the data's original locations can be determined and reconstructed. This matrix is identical for each xy value it is calculated for, and is created once at compile time.
The first part of the function defines constants that will be usd to dequantize the data, returning it to its uncompressed form.
A similiar thing is done with the dequantize table. The LayerData packets are quantized and compressed. Before being sent from the server, they were divided by a certain factor in order to make the bytes small enough to send with the packet. sending floating point f32s would create an enormous packet, so the server divides each point by a factor defined by the quantize table, which is built identically on the client and server side. by multiplying the point by its corresponding factor, you can return the compressed data back to its f32 representation.
Stitch Patches
When patches are retrieved, there is a gap between them that has to be manually stitched by the client. This is to allow rendering two patches from different servers next to each other seamlessly.In order to fill this gap, the client needs to have retreived all of the tiles to the right, to the left, and to the upper top corner between them.
This is done in the core crate. When it parses a Land patch, it inserts it the patch cache. Unrendered land patckets are added to the patch queue and the total patch list. If the North, East and Top Corner exist in the total patch list, generate mesh retrieves them, and begins the stitching.
Generate_mesh_with_indices() follows a simple algorithm to convert the heightmap data into vertices with corresponding indices. Once the mesh has been created, stitch_patches_with_indices() is called, which adds the geometry between the gaps in the patches.
this is then sent to the UI as raw JSON vertices, allowing the UI to apply its own shaders to the land.