2D Reflections in AkimBear

I’d like to share my approach for creating reflections for AkimBear in Unity since I think it turned out rather well and could give you some ideas for future projects. I am a technical one so there will be code, but I’ll try to keep it to a minimum.

Inspiration

If our reflections look at all familiar that’s because I based them on this post from Kingdom’s TIGsource. I wasn’t as strict on my pixel precision and didn’t do any of the perspective correction that Kingdom details (this being a game jam after all).

Overview

You can get an idea of the final result (if you haven’t played AkimBear yet) from the trailer:

https://youtu.be/avRx-U6SNHk

I felt that a flowchart of the rendering to attain the reflection would give the best sense for how this effect is achieved at a system level:

RenderFlow.png

In terms of components, the system is just a camera for capturing the area to be reflected and the reflection itself.

ReflectionSystem.PNG

Easy, right? Stick around for the alignment issues.

More perspectives on the world

Apparently, a camera in Unity cannot both display on the screen and render to a texture. Initially I found this disappointing as that’s what reflection is right? I think this may have actually been a blessing in disguise; AkimBear has a rather lively camera (both position and orthographic size are constantly changing) and I might have ended up with even more mess if I’d been trying to use that camera to generate reflections.

A second camera it was! This camera is offset up from the main camera and it has a render texture set as its target texture. Other than that, it’s identical to the main camera… or so I thought initially (see camera property matching script later). This was my first eureka moment, since I could see the screen in the inspector for the render texture:

ScreenTexture_inspector.PNG

The TIGsource thread from Kingdom had stated that this was not possible without the Pro version of Unity so I was very, very happy when this just worked without being pay-walled.

It’s basically done now right?

Sitting under a tree on a sunny day

The task of the shader is quite simple: take a sample from both the screen texture and the highlight texture and then combine them. The sample into the screen texture should ripple to give the sense of water and the highlight should show some flow. I’ve commented this snippet (shader source here) to give you an idea of what everything does:

fixed4 frag(v2f IN) : SV_Target { // Generate an offset value in both x and y directions float offx = noise(15.0 * IN.texcoord - 1.5 * _Time.yx); float offy = noise(0.05 * IN.texcoord); // Sample the screen texture offset in the x direction float2 uv = IN.texcoord + float2(0.02 * offx, 0.0); fixed4 c = tex2D(_ScreenTex, uv) * IN.color; // Sample the highlight texture uv.y += offy; fixed4 h = tex2D(_Highlights, 3.0 * uv + _Time.yx / 2.0) * IN.color; // This is not good shader code; should be no dynamic branching! // maybe o = lerp(c, h, h.a) would have worked as well? fixed4 o; if (h.a > 0.5) { o = lerp(c, h, 0.5); } else { o = c; } o.rgb *= o.a; return o; }

Why did I use a noise function seeded with a 2D vector? This is a legitimate question and I’m not sure (I’m sure it made more sense at the time). The same result could have been attained with a few trig functions and would likely have been more efficient.

Why all the magic numbers? If you take their product it spells game jam in ascii. Just take my word for that.

All done right?

alignment_bad.gif

Well, its kind of reflecting.

The last 10% is 90% of the work (but not of this post)

A few unacceptable issues are apparent. They all stem from using Cinemachine to control our camera for following Grizzle and creating screen shake. This means that it isn't as simple as just attaching our second camera and reflection surface to the main camera. This was solved with a few scripts to exactly follow the Cinemachine (main) camera on any axis and match the parameters for the two cameras. The parameter matching is incredibly hacky: it uses linear interpolation across empirical values to keep the reflection camera at the right offset while the orthographic size is varying.

After much hacking, both the reflection camera and surface were following the wild zooms and shakes. This took much longer than the actual reflection to get right.

Remaining bug

The last remaining (apparent) issue with the reflection system is quite awful. I really wished I’d had time to fix it (maybe a variable passed to the shader on a per-frame basis to compensate for it?). But I’ve yet to hear anyone notice it on their own, so if you see it keep it to yourself.

Find the bug yourself by playing AkimBear!

This was a pretty quick overview, so if you want to know more, feel free to reach out! If you want further detail than the comments allow, my email is ecmjohnson at gmail.com