
Horizon Fix is an open-source SKSE plugin that improves the look of the horizon by adding additional water far past the vanilla map to give the illusion of an infinite horizon.
Note that when the term "horizon fix" has been thrown around in the Skyrim modding community it usually refers to the sky blending into the horizon. In real life, however, the sky usually does not blend into the horizon in clear weather. Most previous "horizon fix" attempts tried to blend it because if you didn't it looked like the above before screenshot which is, well, not good. Adding any sort of blending is out of scope of this mod. Weather mod authors, however, are free to continue to do so in conjunction with this mod if that is a desired aesthetic.
Horizon fix is feature complete and is ready for wider testing, so this release will be a beta. If you enjoy this mod please remember to endorse, and if you have a GitHub account I would appreciate a star on the repo!
Performance
While I have not done any formal benchmarks at this point, note that this mod will add somewhere on the order of hundreds of water draw calls which will affect your performance if you are CPU-bound (which often is the case with modded Skyrim). In my testing from a worst case scenario it jumped from ~40 water draw calls to ~600. You can reduce this exponentially by reducing the iWaterSkirtRimQuality option in the config. Each step reduced will roughly half the draw calls, just note that you will lose smoothness quality on the horizon line by doing this.
Compatibility
- Mostly Compatible with Community Shaders and Open Shaders - Incompatible with unified water (fix will be available in the next CS update)
- Compatible with ENBSeries - Make sure you have a version of ENBSeries downloaded on or after July 12th, 2026
- Weather mods are all compatible, HOWEVER, the mod author of your weather mod likely made it for the vanilla sky and horizon so there may be some visual weirdness especially if they ship their own sky mesh. In addition, a lot of weather mods used distant fog to hide the horizon which may look wrong with this mod. It is likely that some weather mods will have to be modified to look right.
- Compatible with all worldspaces (including modded ones)
- Should be fully compatible with everything else, but let me know if you find an incompatibility!
Technical Details
Contrary to popular belief, it is not as simple as adding more water LOD tiles, there were significant engine challenges to overcome. For the 6 people that are interested in how this works, thank you for being here! This section will be divided into certain aspects of this plugin. Note that while this section is pretty technical, it is abstracted somewhat for simplicity. My code should be well documented with comments if you want to dig deeper, or feel free to just ask me.
Building the Water Skirt
The water skirt is what you see as LOD water extending the horizon, this is the most important part of the mod. Building this takes several steps:
- Template Discovery - The plugin walks the scenegraph under TES::objLODWaterRoot to try to find a suitable "donor" tile from the vanilla map. Every BSTriShape is scored based on how many vertices it has. The one with the LEAST amount of vertices is chosen as the donor tile. This is to avoid cloning a tile that has extra geometry in it. In the case of a tie-breaker the one with the largest world bound is used.
- Layout and Cloning - Now, the skirt will start being build. Each tile is cloned from the donor tile selected in step 1. The tiles are attempted to be placed by radius selected from fWaterSkirtRadius in the config, skipping the immediate LOD32-sized tile the player is in. Each candidate is checked to see if it is fully outside the calculated circle. If it is, it is dropped. If it is halfway between the inside and the outside, it is split up into 4 smaller tiles, then this step is repeated recursively N times, where N is iWaterSkirtRimQuality from the config. This turns the skirt's silhouette into a smooth approximate circle that appears at a circle when looking at it from the map.
- Placement - Now that the layout is established and tiles clones, they must be placed. The height of each tile is by default the same height as the map's ocean (NAM4 water height). It can be offset using the config option fWaterSkirtZOffset. All tiles in the layout are placed under the NiNode HSF_WaterSkirtRoot. If bWaterSkirtDrawLast is 1 they are then attached to the scene-graph under TES::lodLandRoot which ensures they are drawn after the game's own LOD water which is important for some ENBSeries effects.
- Lifecycle - The skirt is rebuilt upon cell detatch events (when player moves or loads into a new cell), as well as when the game initially loads. There is also pathing to ensure the skirt stops rendering when indoors. When updating outside tiles are simply re-placed, the layout is not calculated again.
This diagram illustrates the above from a top-down view. It is not to scale.
add
Water Skirt Culling
Because of the game's default far clip, those placed tiles would ordinarily just be culled. Initial versions of this plugin modified the game's far clip to avoid this. However, this broke some vanilla effects and some effects in CS/ENB so a different approach was needed. Instead, each tile in the water skirt gets its model bound radius set to 1e9, which makes it intersect with every possible frustrum, in addition to the kAlwaysDraw flag. This results in the engine NEVER culling these tiles. But that is also a problem, because now we have to render every tile every frame.
To solve this, a custom culling algorithm was implemented. By hooking SkyObject::Update on Atmosphere, the 6 frustrum planes from NiCamera::world and viewFrustrum are reconstructed every event fire. In addition, each tile's bounding sphere is shrunk along the camera tile ray onto a sphere at half the distance to far plane. Because angular extent is preserved, the four side-planes still cull off-screen tiles correctly, while the ones in view remain.
Water Skirt Depth
Despite all of the above, the tiles STILL will not be rendered. This is because the rasterizer clips fragments with z height beyond the viewport depth range. Therefore, we also hook into BSWaterShader::SetupGeometry and BSWaterShader::RestoreGeometry. When these hooks come across a water skirt tile, they rebind the viewport with maxDepth = 1 - 8/2^24. That constant is 8 quanta of a 24-bit depth buffer below the far plane which is always closer than the 1.0 value. That allows these tiles to WIN the depth test in the empty horizon gap, but LOSE everywhere real geometry exists. This is why tiles can be drawn on the real map and it's not an issue.
Acknowledgements and Links
Thanks to po3 and the RE community for their work on commonlib!
Thanks to Community Shaders devs for working with me on some of this.
Thanks to Boris (ENBSeries Author) for working with me to resolve issues this mod had with ENB.









Comments (0)
Log in to post a comment.
This thread is open for discussion.
Be the first to leave a community review!