Before I started making games I used to do the opposite whenever I got some spare time. Which means, I picked one of my favorite games and started to take it apart, see how it works and if there is something to learn from it (and in rare cases, if it is possible to exploit the game). However, I don't reverse engineer games that often anymore, they became more complex and therefore tedious to understand and I have too little time.
However, recently I watched some speedruns, including a live speed run of the The Legend Of Zelda: Wind Waker. It was quite funny to watch as there are some curios glitches in this game. You might want to take a look at the current non-TAS world record speedrun to see for yourself. This combined with the fact that I really like the game resulted in the idea that I could take a peek at the game's internals to understand the mechanics of the glitch and maybe be able to exploit it even further or discover entirely new glitches. That's exactly what I did for the past two weeks.
One small note beforehand, as you can already tell from the title of this post, I will mostly focus on the storage glitch. I will also go into very technical details, up to code offsets and structures. But nevertheless, you don't need to be a programmer to understand the concept. In addition it's worthwhile to mention that while I own the game I used the Dolphin Emulator to test most stuff because it allowed me to fast save&load and it has a basic but decent debugger. All glitches mentioned should work 1:1 on both, the emulator and the real console (GC/Wii) and also in any regional version (EUR, USA, JP). I used the European version of TWW, which is only relevant if you want to make use of the offsets and C/C++ structures I reverse engineered.
So here it goes. The storage glitch (click link for explanation) is a very interesting bug, with lots of videos on YouTube demonstrating it if you haven't seen it yet. Basically what happens is that you interrupt or cancel a cutscene into a state of "partially finished". The next cutscene/event will then go weird, freeze or "get stored for later" because the game still thinks the previous scene is running. One thing that I found out is that there is no general formula to predict behavior, because most logic that causes this is programmed into the game separately for each object. E.g. not every NPC will react to "storage" the same way, because they were programmed by different people who make different mistakes. The famous "dry storage" exploit uses a bug in the "Wind Waker" item (the conductor's baton item) that is very specific to only this item. Bottles seem to have a very similar behavior, you can take them into Link's hand which is a requirement for dry storage. Yet, bottles cannot be used because they do not have the same faulty programming code. This however does not exclude the chance that there might be another way to cause storage with an empty or full bottle, except for the already known Forest Water storage glitch.
The question is, what does actually go wrong inside the Wind Waker logic that makes the game go crazy? To be honest, I can not give you an 100% accurate answer to this, as I have spent most time analyzing the symptoms of the glitch rather than what's wrong with the Wind Waker item logic. I still know a few things: It's not about a single state that is incorrectly updated. There is no "in_cutscene" variable that is set to 1 but never to 0. Instead the game uses various variables to check if the current event can progress, is finished or has to wait. For example some events check if there is a camera lock active only, others check if there is a textbox or the tact window open. Now that might have sounded a bit naive and not really like revolutionary TWW glitch research results, but it's really a complex issue in the game. The objects seems to be programmed from a viewpoint that only one event at the same time is supported, although the engine does indeed support an infinite number of parallel events.
To better understand the event/cutscene system I also started to examine how the game stores and processes cutscenes or events. After some inspection of the game data files I found out that "event_list.dat" contains a list of all events. It is present for every stage. In TWW, a level is defined as a "stage" which can contain subparts called "rooms". E.g. the stage "sea" represents the whole overworld while subfiles "room0" to "room49" are the individual islands. A fade out effect usually indicates that a new stage is loaded. Only one stage can be loaded at a time, but multiple rooms of the same stage can be active simultaneously, with the exception being the overworld where the limit is only one loaded room (alias island) at a time. Since there is only one event_list.dat file per stage there is a lot of redundancy between the various event_list.dat files. For example each of them contains the events for using the tingle tuner.
To keep things short, I reverse engineered the whole file format in hope to understand and be able to better exploit the storage glitch. It definitively helped, but it made me realize even more how complex the event system in this Zelda game is. I was able to dump all events from the event list data of the overworld stage into a semi-readable text file. You can find it here.
Finally, how about an detailed example? I took a look on "door storage" on Outset Island:
First lets start explaining how the doors on Outset Island actually work internally. Every object and npc in TWW is represented as an actor. The actor object that represents the doors in this case is "knob00". Like every actor in TWW it has it's own dynamic executable which is loaded whenever the actor type is present on the current map. Very technical sidenote: If you have a disassembler ready and know ppc assembly you can open "./RELS.arc/rels/mmem/d_a_knob00.rel" using IDA. Although you first need to extract the files from the gamecube image, unpack the rels.arc and then decompress d_a_knoboo.rel to even get the file in it's raw form. Since its hard to scavenge the web for the necessary tools I put them into a archive and uploaded it here
The knob00 door knows the following actions:
WAIT, STOP_OPEN, STOP_CLOSE, OPEN, CLOSE, SMOKE, SMOKE_END, SETGOAL, UNLOCK, SETSTART, SETANGLE, ADJUSTMENT, OPEN_PUSH, OPEN_PULL, OPEN_PUSH2, OPEN_PULL2, TALK, SETSTART_PUSH, SETSTART_PULL, END_CHECK, DROP_BF, DROP_AF.
Quite a nice list, however most of them are not used on Outset Island. For most actors there is a default interaction event name that is defined somewhere inside the main executable. For this door type it is DEFAULT_KNOB_DOOR_F_OPEN. So what happens when you use the door? First the game will look up the event with the name DEFAULT_KNOB_DOOR_F_OPEN inside the event_list.dat. This event defines, besides camera effects and Link's animations, the actions for the door: 1) SETANGLE 2) ADJUSTMENT 3) OPEN_PULL 4) WAIT. If you think about it, it makes sense. When you open the door, Link is turned towards the door (set angle), then his position is corrected to be exactly in front of the door (adjustment), then the door plays the open animation (open pull) and finally the event waits until the other actors, like Link and the Camera object, have finished their actions too (wait). You can see a more detailed version of this event here.
When you use storage, what usually happens is that the event will immediately finish without going through the remaining actions. For example, if you get dry storage and then try to open a door right away, nothing will happen and you cannot open the door anymore. The reason is that the door is stuck in the SETANGLE action. Which, because it is programmed this way, only sets Link's angle once. It is stuck in this action until you reload the stage. Btw. when you can't interact with an object after you used storage, that usually means it's still stuck in some action. Another thing I should mention is that while an object is inside an action, it executes the code for the current action every single frame. Sometimes it has checks to not do things more than once, like the SetAngle action.
There is also a different version of the "storage glitch" with doors. Talk to the King of Red Lions first, pull out the Wind Waker (which will cause the stored talk text to play). Cancel the Wind Waker again and wait until the last text message is up. Then, turn towards a door so a press of the A button will open the door and at the same time will close the textbox. This will cause the door get stuck in the ADJUSTMENT action. Which has the interesting effect of resettings Link position whenever the camera is locked. You can easily warp to the door as long as you remain in the same stage. It also seems that only the textbox of the KoRL works to cause this effect and according to others, the pirate's password door works too.
Then there is something called 'scene cancel' and it seems to be far more powerful than I originally thought. Whenever you close the KoRL's last textbox, the event that is currently active is canceled and causes the related actors to get stuck in their current action. If you apply this to Outset Island doors, you can freeze them in any of the four actions. We already know what ADJUSTMENT does, but how about OPEN_PULL? Well it has the nice effect of allowing you to clip through walls but sadly it fades the screen to black. But it is an known effect and already used by speedrunners.
Another interesting effect is if you store a door, then enter another door, in which room will you end up? Sometimes in the first room, sometimes in the second one. While it seems random at first, it is actually not. The door that was loaded later will always win. There is a great tool, Wind Viewer, which allows you to load and view stages and rooms. It also displays actors, player spawn points and much more. You can see doors in the 'TGDR' node. The order of the doors in this list, and therefore also in the game data files, is the same as the order of which door overrides the destination room when using storage to open multiple rooms. The later one will always win. I can technically explain it but I think I went through enough door specific talk already.
Now this was just one example for how storage works internally. Don't forget that there are a few different types of door and all of them are separately programmed and therefore behave different when storage is used. In total there are over 400 different objects that have their own actions and logic. It gets even funnier when you combine the storage of multiple objects, remember Door Teleport (the ADJUSTMENT lock), where not every NPC's textbox has the same effect.
Obviously, I also had to check if wrong warp is possible. Therefore I have analyzed a lot of the stage and room warp code. There are three ways Link can be warped into a new stage: By entering a warp zone, which is mostly used by doors or pits. Then there are events that cause a warp, for example the tag game on Windfall Island. The third type is warp by object logic, an example for this would be the Wind Waker item and the Ballad of Gales. I looked at warp zones extensively and have not found any exploitable flaws so far. That doesn't mean it is impossible, but from what I understand it could be very tricky and maybe only possible in a useless way.
That's it for now, this post doesn't cover everything I have found so far, but I think I already flooded you with enough text. Maybe I will write a second one if I can find something more concrete.
Last but not least, you can find parts of my notes and other data I gathered here. It includes the event_list.dat file format if anyone needs it.
Still going strong with the development of my game and I wanted to show you two new screenshots:
As you can see, graphics wise, a lot of things have changed. The terrain is now smooth and there is also water. I replaced almost all textures with more real looking ones. I definitively prefer the new look over the old pixelart style. There are still some things that need to be done to complete the whole look, like getting rid of the placeholder art and implementing proper transitions between different terrain types. Currently there is no transition at all which just looks strange. In the screenshots you can see it best at where it just goes sharp from grass to rock texture.
There have been introduced a lot of new gameplay features too, but it doesn't make much sense to explain them as they are out of context. I am generally holding back with giving out gameplay details for now, because it's too easily possible major game mechanics get changed. On the good side, I am working hard to complete all basic features so I can push out a alpha release. Whats a better way to explain a game than a hands-on demo? Additionally, I think user feedback is helpful to shape an game in the early stages of development. Let's see how it goes, maybe I can finally reveal more info at the end of month.
Recently I have been putting a lot of effort into a new game and I think it has progressed far enough to post some super early pre-alpha screenshots and information. Although I have been hesitating a bit to publish this post, because I do well know that I have a certain tendency to lose motivation and interest in a personal project fast. This affects about 95% of the projects I start.
The game is aimed to be an open-world, sandbox RPG. I try to focus on ingame economy and survival and less about the typical RPG aspects like leveling and combat, although it will probably end up being an allrounder. Very essential to the gameplay is the fact that both, players and NPCs have to eat, drink and sleep to survive. This is the basic force that drives the economy. The NPCs will, if left unharmed, try to build a save settlement with farms and houses and later structures to protect from outside dangers. I should also mention that items do not spawn out of nowhere but have to be created individually (with the exception being the raw resources like wood or stone that are scattered across the map when a new game is started).
Enough talk, how about some screenshots?
The screenshots only show basic game mechanics, the more exciting features are still under heavy development, they look too unfinished or are simply not working yet so I don't want to show them yet.
One more thing, while the graphics may change in future, the general graphic-style will probably stay the same. It's art I can make myself without having to rely on someone else and it also allows me to spent more time with the actual gameplay. Although I noticed some parts look a bit too Minecrafty in my opinion, so those will definitively change.
What can you expect next? I will continue to work on the game and I plan to write another blog post about the AI and the principles of economy in the game soon.
I find it really hard to find useful information about textures and their respective performance. Because of this I wrote a little tool to measure the performance of various texture formats and texture environment settings.
Since MipMapping has a high impact on the performance and results in highly view-dependent differences I disabled it for most of the tests.
My intial test of rendering a 4096x4096 texture in various formats with magnification set to GL_LINEAR and wrapping set to GL_REPEAT I got the following results on my GTX 560:
From the gathered data we learn a few things:
- The more bits per pixel used, the slower it is
- It doesn't matter if the type is integer or float
- There is no performance difference between 24bit (RGB) and 32bit (RGBA) formats
- Compressed textures are much faster even though they should add a lot of decoding overhead
While it is good to know these rules, it is actually better to know the 'why'. I don't have 100% accurate explanations on everything but here are my assumptions:
Texture cache and bandwidth is very important, if the data is smaller the pixels will be loaded faster into the cache. Modern GPUs have the decoding logic of the texture formats hardwired, including compressed formats, which means that there is practically no additional decoding cost when using one format over the other. Therefore using GL_COMPRESSED_RGB is theoretically as fast as GL_RGB and the speed difference comes only from the size of the pixel data. That also explains why it doesn't matter if the format is signed or unsigned, or if it is integer or float.
But if bandwidth is so important, why is GL_RGB not faster than GL_RGBA? That's because GPUs like 4 byte aligned access. The 24bit RGB data therefore gets padded to 32 bit.
I also tested the speed difference when using certain texture parameters, the results are as following:
When using (bi-)linear texture filtering the performance is about 5% to 10% worse compared to just GL_NEAREST.
The various wrap modes (GL_CLAMP, GL_CLAMP_TO_EDGE, GL_REPEAT, GL_MIRROR_REPEAT) hardly have any impact on performance. When trying out the different modes the difference was less than 1%.
Using Mipmaps can give a huge performance boost. It often ranges from 10 to 100 times faster. But it goes down fast if you start touching states like GL_TEXTURE_MIN_LOD, GL_TEXTURE_MAX_LOD and GL_TEXTURE_MAX_LEVEL.
In addition, if you want to dive in deeper I can recommend you this article. Page 10 covers texture cache more detailed, if you don't want to skim through all the text. It explains a lot of my findings.
Until now everything was mostly NVIDIA specific, as I tested only on NVIDIA hardware. But what about AMD GPUs or even mobile hardware? For recent GPUs that support at least OpenGL 4.0 or OpenGL ES 2.0 it will presumably be very similar. When using uncompressed formats, smaller pixel component data will always be faster. When using compressed formats it can differ. For example I have an 4 year old Netbook with OpenGL 2.0 support (Intel GMA 950), when I tried using compressed textures there, the performance dropped by at least 80%. It is slow as hell and is just not worth the memory saving.
The solution: If unsure it is probably better to use an uncompressed format with the smallest number of bits per pixel possible. If targeting OpenGL 4.0+ hardware you can safely assume that compressed textures will be fine too.
If you really want to go for the highest speed in any case, then it is probably a good idea to write a small performance test that evaluates the speed and compatibility of each format and later dynamically decides which formats to use. In this case it also becomes important to consider the quality and intended use of the textures, for example normalmaps should never be compressed using standard DXT1.
I might implement this into my engine soon and if I do, I will provide more info and code about it.
Man, I am really behind with posting news. I have been writing on the lengthy second post of my update-about-my-RPG series and in the meanwhile there are so many new interesting things happening that I want to post about, but I also don't want to write about stuff in a non-chronological order. Today I tough about the situation and came to the conclusion that I will just sum up everything of the past in a few words rather than a whole detailed blog post. So here it goes:
After my terrain renderer was complete I ran into trouble. It required to calculate LODs after every change of the terrain. Real-time editing of the terrain was hardly possible anymore. Because of this I started to work on an improved editor. The new editor would still use the old terrain render system when editing the landscape, but it featured an 'generate LODs' button. The advantage was that I could edit the terrain in real-time (but with decreased visibility range) and once I was done, I just pressed the magic button and it would calculate all the data for the improved terrain render. That step however could take up to a few minutes but that wasn't much of an issue.
The new editor was also separated from the game itself, I stayed away from hotkeys and integrated everything into an humble window interface. So instead of the earlier hotkeys there were now toolbars.
As time passed I started to realize that even while there was some progress, I had barely implemented any of the real gameplay elements. There was an inventory and a simple crafting system, but that was about it. The reason was that of the 10 months I have been working on this game, I spent about 90% of the time just on the engine. That is definitively not what I want to continue do to in the feature. I don't want to say an 3d engine is too much work for a single guy, but it definitively is a really big challenge if you have never written a complete engine before. In the last 10 months I probably re-wrote every single part of the engine two or three times. I learned a lot by doing that and I am happy I did go through the effort, but it is not going to help the overall progress of the game. To sum it up, at the current development speed I would have an playable beta version in maybe 5-10 years. That is clearly too much. I sure do know of the old KISS (keep it simple stupid) rule and I know how important it is. But on the other side I get bored fast if the thing I am working on is not a challenge to me.
There is yet another problem, one that existed since the beginning and that I cannot nullify myself. I can't do 2d or 3d art. If you take a look at my previous Ludum Dare games (one, two, three) you see there is some kind of progress regarding the graphics. But that is because I discovered I can still make good looking stuff by going abstract. I also have a good sense for aesthetics, so I usually just change colors and details until I think it looks good. Kind of brute-forcing the art. But thats about it. I could never do a decent 3d model or an animated 2d character. I tried to learn it, but it is useless. Therefore, for the RPG I would rely on other people to make the models and maybe the level design. This is a big problem. Because I don't have much money available I cannot hire professional artists. While there are indeed people on this world who would work for free, or a promised revenue share, they are hard to find and most importantly, you never know if they will suddenly change their mind and leave the project.
The final conclusion of all this is simple: I stopped working on the RPG. Probably not forever, but at least for the next years until I have the possibility to make it happen within an realistic amount of time. For me, this is not a bad thing, as I learned a lot about OpenGL and engines. But in the long term I would just waste my time working on an game that might not even be successful.
What next? I don't know. Currently I am prototyping games, the recent Ludum Dare showed me that I can indeed complete an game with decent art and music. I just need to stay realistic.
I have finished my game 'cuboidAI' for Ludum Dare 24. The theme was evolution. My originally simple idea became too complex fast and I had to drop a lot of features that would have improved the understanding of what is going on.
You can find the game here. Don't forget to check the other games too!
Being the lazy guy I am, I have not finished the second post of my 'update on the RPG game'-series yet. However, I wanted to drop a note that I will participate in the 24th Ludum Dare competition this weekend. It's my fourth time trying to make a game in just 48 hours. I already made a post summing up the details of Ludum Dare for the previous competition in April. My record is overall rank 15 and rank 8 in the category mood in Ludum Dare #22. This time I want to make something really risky and innovative and not focus on getting a good rating. But so far I don't know what the game will be about, I will come up with an idea just in time since it highly depends on the given theme.
If you want to track my progress during the competition you can check my journal.
Of course it is also interesting to watch other people post their progress, screenshots and ideas. Just check the main page of the Ludum Dare compo every now and then.
During the last two months I have written lots and lots of code and yet there is not really anything to show. Previously I always tried to implement feature after feature and avoided to look on the project as a whole, because then I would realize how much work still lies ahead and how slowly I am actually progressing. Despite the slow progress from a player's perspective, there has been a lot of changes both in 'behind-the-scene' terms and in plans regarding the future of the game. This is why this update will be split up into three parts. The next part will follow soon.
The reason that there is not much new stuff I can present is because I wasn't happy with the terrain renderer and spent most of the time trying to improve it. I was able to increase the amount of visible terrain from about 300 meters to 10km in any direction while still having the same performance. Since static world objects (like foliage, houses, furniture, just anything that can be placed in the world and never changes it's location) were tightly coupled with terrain nodes I had to work on that part too. All in all the new solution is good and I am happy with it.
Technically it isn't too different from my previous terrain rendering method. But before I start to explain it I want to elaborate on the old renderer first.
To render the terrain I simply split up my heightmap into 32x32 metre cells. For each cell there are 6 LOD variants (32x32 vertices, 26x26 vertices, 20x20 vertices ...) The distance of the cell center to the camera would determine the LOD used for rendering that specific cell. Since the resolution difference between each LOD variant wasn't big there were hardly any geometry detail based popping artifacts noticeable when moving the camera. The only visible effects where sudden changes in the texturing because I used blend weights stored in the vertex data to interpolate between various textures. To solve this issue I moved the blend weight data from the vertices to textures. By using two RGBA textures that only stored blendweights I could have up to 9 different textures per cell. Since the blend weight lookup and interpolation was done in the fragment shader it was not influenced by the resolution of the vertex grid. The only downside of this approach is the massive bandwidth required. In the worst case there could be up to 29 texture lookups. 2 blend weight textures + 9 diffuse textures + 9 normal maps + 9 specular maps, maybe even more if I would start to encode normals in textures too. Anyway, I could live with that since in 90% of cases only 3-4 textures were used and I could spare one blend weight texture and the 4 related diffuse/normal/specular texture lookups. Furthermore I limited the implementation to monochrome specular colors and thus could store the specular intensity in the alpha component of the diffuse texture. This way I ended up with about 5-10 texture lookups in most cases. That is quite acceptable. Another common problem is that between cells rendered with different LODs there could be holes because the vertex density at the borders did not match. I solved that by simply moving all vertices at the border of a cell down a little bit. In some rare cases the holes were still visible but I could cover it with foliage.
All in all, the solution was far from ideal but still good enough. It had only two downsides: High number of drawcalls and high memory usage since I need to have 6 LOD variants of each visible cell in memory. The memory usage could be optimized by doing more streaming, i.e. only loading LODs that are required and unloading them when they are no more needed. But ultimately, the number of drawcalls kept me to a limit of 400 meters for the view distance.
If it were not for the fact that I am always out to make everything 100% perfect I would probably have never touched the terrain renderer again. It was good, but I wanted it better. I already knew the basics of the most common terrain rendering techniques but never took the effort to read up the details. (btw. useful info can be found here) That had to be changed. So I read a lot of papers and made a simple demo to start experimenting with my new knowledge. I ended up with a solution that is very similar to CDLOD and also wasn't too different from my previous approach. Instead of having fixed size cells with various LODs, each cell has the same amount of information but differs in size. I use simple quadtrees where each node contains the same terrain data than it's four subnodes, just with lower detail. Basically, there are still the plain old 32x32m cells, but now there are also 64x64m, 128x128m, 256x256m, 512x512m ... 2048x2048m cells with the same amount of vertices but less detail which keeps the amount of rendered vertices low. Another main advantage is that I only need to load the cells, or actually quadtree nodes, that are currently visible, beginning from the coarsest level. For terrain that is far away I only need to load the 2048x2048 cell (quadtree root node) which has exactly the same amount of information like the finest node but is 64 times bigger in world size.
However, there is one problem that arises regarding the way I did texturing previously. Since each 32x32 node can have up to 9 different textures independently of it's neighbors, the parent node thus can have 4*9 textures, the next parent node can have up to 4*4*9 textures and so on. The root node technically could have thousands of different textures, although most of the time some nodes will share textures. Think of a desert, pick two points anywhere and you can assume that you will find the same textures used in both regions. Nevertheless it gets dramatically worse if the terrain transitions from desert to a jungle biome and a completely different set of textures is used.
The major issue here was that using the old method I was already at the limit, 9 different textures with each a specular and a normal map is already very inefficient. Using the quadruple for each parent node makes it just unsuitable. The way I solved this problem was by using virtual textures for all but the smallest cells. Specifically this means that the 32x32 nodes are still textured in the same way, using blend weight textures, but all of the larger nodes use a pre-rendered texture. The textures would be generated by simply rendering the four subnodes of a node to a virtual texture. It is important to not include height data when rendering to the virtual texture to avoid distortions. Since this task can consume a lot of time and RAM I generated the virtual textures offline rather than on the fly.
There remain a few small problems like terrain holes or GPU memory consumption, but that's something I will deal with at a later time.
The first half of May I practically did not make any progress. The reason is that I had only very little spare time and of the little time I had, I spent most playing Dwarf Fortress. Shame on me. Anyway, to compensate for that the last two weeks were very productive. I added a few new features, fixed a ton of bugs and improved the performance of the engine by at least 20-40%.
The most visible change is the new main menu screen. Before that, the game would immediately launch into editor mode. It was not exactly a high-priority feature, but I wanted some diversity from the usual engine-only-work. Here is how it currently looks:
As a side note, the background is not a static image. The scene is dynamically rendered and then warped/shattered using a tiled lookup texture. The main menu is not final in any way and may not even relate to the later game, so don't try to interpret anything into it. Since I also don't have a name for my game yet, this part is also missing in the main menu.
The next feature is something I already mentioned some time ago. I finally added collisions with game objects. Although I am not using a fancy physics engine. I decided to go for the do-it-yourself method because I figured that I don't need very realistic physics and it is also helpful to know what's going on behind the scenes when trying to combine physics with multiplayer. (Again, it is not guaranteed that the final game will include a multi-player mode, but for now, my plan is to have one)
The third change is about font rendering. I tried to implement Valve's Signed Distance Field technique for rendering of texture mapped fonts. The results are ok, but not as good as I expected them to be. Although it is possible that I did something wrong. I will look into this at a later time again. The quality of the rendered text is about the same, but I could reduce the font texture size from 1024x1024 to 512x512. I guess even 256x256 would still look good. Thus I at least saved some texture memory.
So far, this were the most visible updates, but there have also been a lot of changes under the hood. Most of them were about increasing the engine stability and performance. This was done in foresight of the first big gameplay feature. More about that once it is done.
This time less words but more images.
Today I tweaked a lot of small details to make the world appear more believable. For example, only the currently loaded chunks (32x32 meter rectangles) were visible. This resulted in oddly cut off terrain. Especially when moving the player would also see new chunks appear out of nowhere. To make it less obvious that the sight range is limited, I added terrain fog as a post processing effect. The further away a pixel is, the more does it's color fade into the sky color behind it.
I made a small test world in about 10 minutes, here is how it appears to the player:
(Ignore the green bars in the corner, they are a relic)