Porting the UnionBytes Realistic Water Shader to Godot 4

As part of the transition from the 3.x branch of the Godot Game Engine to the new 4.0 release, engine developers made a number of breaking changes. These changes should make it easier to develop new games, and provide a solid foundation for the future of Godot. That being said, the sheer number of these changes makes it a bit challenging to migrate existing projects to use the new version of the engine.

One of these pain points is the updated shader language. Complex 3D games tend to use custom shaders for all kinds of effects in game. Grass and trees blowing in the wind? Shader. Healing spells? Explosions? Shaders. The cell shading effect in Breath of the Wild and Tears of the Kingdom? Guess. Even the standard PBR material type in Godot is nothing more than a shader, since you can convert it to a Shader Material with the push of a button. And, as the title of this article should make clear, water is also a shader.

Upgrading to 4.0

So, the obvious and naïve approach is to simply create a Godot 4 project, and copy the files over. To be fair, I made this a bit more difficult for myself since I prefer to keep my reusable assets organized differently than what's in the original project. In this case, I have a folder for assets, which has a materials folder, which, in turn, has subfolders for shaders and textures. That's definitely overkill for a project of this scale, but it's a little easier to integrate into larger projects with more assets. I won't argue whether one way or the other is the "right" way to do it, but either one is definitely a way.

Once I had the textures copied to the texture folder, the material in the root of the material folder, and the shader file in the shader folder, I ran into my first hiccup. Apparently, Godot 4 doesn't recognize .shader files, and it won't even show up in the project explorer. Changing the file extension to .gdshader fixes this problem.

At this point, we may want to open the Material file to see what we've got. Well, in this case, what we've got is broken dependencies:

Fortunately, Godot makes it relatively easy to fix the issue. Clicking 'Fix Dependencies' opens another dialog box with a list of the missing files, and a folder icon that we can click to track down the files I moved:

Locating each of these resources allows us to open what appears to be a blank Shader Material:

At this point, everything has been applied the right way, but our shader code has run into some of those breaking changes I alluded to earlier. Opening the shader editor reveals that we're trying to use invalid type hints at various points in our code:

Sorry, it's supposed to say, "hint_anisotropy"

Here, I had to use some Google-Fu to figure out what the new type hints are called. The Shader Editor also supports code suggestions, so when I was too lazy to open a browser tab, I just cleared out the invalid type hint, and started typing the old one again until I got a suggestion that looked like it could be right. Whoever says that programming is art, or science clearly hasn't been watching how I do it.

Regardless, I managed to muddle through the various errors that came up, until I got something that actually looked more or less like the original shader:

A sample of the ported shader running under the Forward+, Mobile, and OpenGL renderers in Godot 4.0.2

Obviously, there are some issues compared to the original version:

  • The Seaweed shader hasn't been ported (sorry)
  • I haven't figured out how to apply the Caustics texture yet

There are also a few issues with the foam which only seem to affect the two Vulkan renderers:

  • The foam effect is way too aggressive around objects close to the water surface (this is especially noticeable when moving higher above the water, when the entire ground plane starts to trigger the foam – no idea why)
  • The foam also creates artifacts at the edges of screen space, which are visible when water extends past the top, left, and right edges of the screen (again, no idea why that's happening, or why it's not showing up on the bottom)

For reasons I won't even pretend to understand, the OpenGL renderer produces something much more similar to the original Godot 3 version of the shader. The OpenGL renderer is still Godot 4, so it's running the same shader I hacked together for Vulkan. If I had to guess, I'd assume that the compatibility mode reuses a lot from the Godot 3.x branch, but patched to work with (a limited set of) the new features, while the Vulkan renderers are all but written from scratch. At the same time, the OpenGL renderer in 4.0 seems to run a lot faster than it did in 3.x, so I really don't know what's going on here.

Takeaways

While the process of getting this to (mostly) work in Godot 4 has been relatively straightforward and almost easy, it also shows why the Godot team was right to caution developers against migrating projects from 3.x to 4.0, once a project is past the early stages of development. One shader is easy enough to port over, but doing dozens of them just to get a project running would be beyond tedious. On top of that, there are similar breaking changes in GDScript, as well as the C# integration for projects that use Mono. In a lot of ways, Godot 4 might as well be a completely new engine, albeit one that looks and works a lot like the old one, once you get used to the differences.

I'd also like to thank Achim "AiYori" Menzel and UnionBytes for creating this demo, and releasing it as open source. The ported shader code, sample assets, and animation are available at this GitHub repository. Like the original, it's available under the MIT license.