In our engine’s implementation, while we track the camera movement on each frame, we only do terrain updates as the camera position crosses specific boundaries.
We split the terrain into two types of grids.
The top-level grid system splits the actual terrain files into parts that represent the world map in 533.3333 x 533.3333 world units. Each tile provides the engine with all the static map references such as the height-map, texture blend maps, static shadow maps, model references, etc.
The second-level grid system splits the actual individual tiles into sub-sections we refer to commonly as either cells or chunks. We do this as it allows us to provide a way to render chunks closest to the player in higher fidelity than those farther away so that we can keep memory budgets in check.
In terms of loading, we follow a similar practice to what JSandusky has described.
In our engine we define 3 values
- View distance (configured by the user)
- Persistence distance (a value slightly beyond the view distance for cache/pre-load)
- Low-polygon transition distance (a value between the player and view distance)
Our terrain exporter generates a file that the engine can use to draw the terrain with very low polygon counts, which allows us to draw very distant terrain with little cost. This terrain is drawn without texturing and is meant to provide mostly mountain-like silhouettes. Since this mesh data is very small compared to each of the tile’s vertex data, we can easily load this into memory at map load, so its never streamed.
The configured view distance paired with the persistence distance controls when tiles are streamed in/out. As the camera moves across a cell / chunk boundary in the smaller grid structure, we re-calculate the persistence distance based on the camera’s current position and trigger load / unload of tiles.
The loading is done in a set of worker threads that interact with the main thread to load the tile’s data across multiple frames to avoid any type of frame stutter.
Whenever a player teleports in the game, we obfuscate the rendering artifacts by showing a map load screen to the user while again the persistence and preparation happens across multiple frames. Once the tiles within the view distance have been loaded, we remove the load screen and render the world.