Shader Experimentation Results - They're Good!
Added 2021-05-07 09:07:38 +0000 UTCtl;dr - The native Ren'py shader support is much faster than the current system used. LR2 v0.41 will move to Ren'py 7.4 to make use of it. v0.40.1, releasing tomorrow, will still be using the old system.
Two days ago I noticed that Ren'py 7.4 included native shader support and decided to spend some time experimenting with it. The goal was to see if the new version would have better results than the current shader implementation running in Lab Rats 2. If I could get the basic functionality working, then I wanted to evaluate how much work would be needed to move LR2 from it's current Ren'py version of 7.3.5 to the newest version 7.4.4.
My initial experimentation attempts hit a snag early on. All of the animation effects in LR2 are achieved by combining three things: First, the static image to be manipulated. Second, a "weight" image that highlights part of the image that should be effected, and to what strength. Third, the shader itself that describes what should be done to the affected areas. The new shader system makes the first and third part easy, but didn't seem to provide a way to provide more than one texture to a shader.
Solving this took about a day, and in the end it was really just a problem of limited documentation (not surprising considering how new and technical the shader support is). To anyone who has stumbled upon this post looking to pass two or three textures to a Ren'py shader, this next paragraph is for you. Otherwise, feel free to skip over the technical details.
To hand multiple textures to a Ren'py shader you will need to construct a creator defined displayable. Ren'py doesn't (at the time of writing) document this, but each render that is blit'd is handed to the shader as a texture, stored in the order they are blit'd to the final render. If you're reading this some time after May 2021 then the most recent version of LR2 contains a functional example. I hope I've saved you some time and frustration!
With that solved I had the shader working in a basic test case, animating a single image with a given weight map. Everything looked like it would be feasible to bring into LR2, so it was time to figure out what sort of performance gain there would be, if any. I thought about it for a while, and the simplest way to get a useful answer to that turned out to be to just take a copy of LR2, update it to Ren'py 7.4, and replace the old shader system. This was relatively quick to do, since I only needed it to be functional for a single test case to get a comparative benchmark.
I tested performance by running the draw_group integration test on both versions and checking sustained framerates when three animated characters were displayed on screen. On my machine the old system ran at a steady 39-41 FPS with three characters. The new system ran at a flat 100 FPS, which is the framerate cap of Ren'py.
The new shader implementation has two other major victories over the old system. First, characters are displayed more quickly to the screen, especially when multiple characters are being manipulated. Second, it doesn't require any sort of multi-threading to achieve this responsiveness. Multithreading the display code in LR2 had resulted in smoother performance, but was a frequent source of game breaking and difficult to diagnose bugs.
A final consideration for the new shader are the new stylistic options it opens up. The new system allows me to apply shaders to displayable that are partially transparent. This means I can eliminate the background frame that was necessary for the old system to function. It is also possible to apply multiple shaders at once to a single displayable, making it easy to have an "idle bounce" shader and then add more dramatic movement on top of that.
All of this makes it clear that updating to Ren'py 7.4 will be worth the time and effort. This will be my first task for v0.41, which I start development of in a couple of days. Tomorrow's release will still be using Ren'py 7.3.5 and the old shader system.