(This was originally written on ludumdare.com in the aftermath of Ludum Dare 34, December 21, 2015.)
After writing up my post-mortem for Xtreme Crop Duster Simulator ’82, I had a comment from pkenney asking about how the two-camera setup I created in Unity worked, and how I used Unity’s built-in shaders to achieve the graphical style of the game. A lot of the positive feedback I’ve received about the game makes reference to the graphical style, so I was already mulling the idea of a post about exactly that – the comment spurred me to actually write it up.
Lots of text and graphics to follow, which likely isn’t be applicable outside of Unity and may only be of interest to people keen on this sort of graphical style, so the real meat of the article follows the break. But as a teaser:
The challenge, which I had run into in previous Ludum Dares, is that square pixels are a relatively recent innovation. The Commodore 64’s multicolor low-resolution mode which I emulated in this game had a resolution of 160×200, displayed on a 4:3 television. This means that the pixels, once rendered, are 1.6 times as wide as they are tall – not a nice ratio to deal with. In my LD32 entry, Red Threat, I handwaved the problem away by drawing the sprites with 2:1 pixels, and scaling the whole thing up 2x to 640×400. It worked, but the effect was graphics that were noticeably stretched if you’re familiar with the real hardware.
This time around, I wanted to do better.
Stating the problem
I had learned, from my previous attempts, that the way to keep things pixel-perfect without going insane involved a couple of strategies:
- In the sprites, use a Pixels Per Unit of 1, so one unit in world space is a pixel at your in-game “native” resolution. For my purposes, this is the native resolution of a historical platform that I’m emulating, but it could be anything.
- Similarly, fix the camera and all game objects to integer positions in X and Y space.
- Force the game’s resolution to an integer multiple of the “native” resolution.
This works just fine, except that it doesn’t play nice with displays of different sizes, makes full-screen modes a non-starter, and doesn’t get around the aspect-ratio issue. So, I quickly realized that, while strategies #1 and #2 were still going to serve me well, strategy #3 had to go.
Instead, I needed an approach that allowed me to keep all the advantages of using sprite pixels as my unit in world space, but get the aspect-ratio correction in place and keep things looking pretty at arbitrary resolutions. Using shaders to achieve a CRT-style effect was a bit of an afterthought, but it worked really well. After trying some simpler ideas out involving just putting scaling factors on the one camera, I was able to meet these requirements with…
A two-camera approach
A picture says a thousand words, so here’s a picture of how the main scene looks in Unity, showing both cameras:
The more distant one, or the top preview, is the one that I called the WorldCamera. This is the one that actually has all sprites and text drawn to it, and so it exists at the 1:1 pixel-to-unit scale. Since my target “native” resolution is 160×200, it’s size in Unity is set to 100 units – more generally, the size needs to be 1/2 of whichever dimension is larger in the target resolution.
Rather than having this camera render to the screen, it renders to a RenderTexture. According to the Unity docs, RenderTextures need to be a square size that’s a power of two on each dimension – so much for being pixel-perfect using my strategies above. (As an aside, I’m not actually sure this is true – the editor lets you assign any arbitrary sizes to each dimension of a RenderTexture. I just chose to follow what the docs said to eliminate a possible bug source.)
The RenderTexture is, therefore, square – so it’s actually rendering an area of 200 pixels x 200 pixels in world space. While that’s not what I’m after, it means that the WorldCamera is completely independent of the aspect ratio and resolution of the display – which is what makes the rest of this approach possible.
The second camera, which actually renders to the screen, is aptly named the RenderCamera. This camera is set way back from the rest of the scene (-100 Z), with a close enough clipping plane that it won’t catch any of the action going on in the background. All that’s within the clipping planes of this camera is a single Quad, with an X scale of 1.33 – meaning it has a 4:3 aspect ratio. All that’s left to do to get things looking spot on, then, is to scale and offset the RenderTexture so it trims off the surplus 20 pixels captured on the left and right sides of the WorldCamera.
Thankfully, Unity gives a very simple way to do this: to map the RenderTexture to the Quad, it needs to be a Material, not a RenderTexture, and a Material using the “Unlit/Texture” shader has handy scale and offset properties. With that done, the RenderCamera displays the perfectly aspect-ratio-adjusted version of exactly the part of the WorldCamera’s viewport that matches our target “native” resolution.
The Commodore 64, like many computers of it’s era, always had solid-colour borders around the graphics. I simulated this by setting the RenderCamera’s size to 0.6 units, while the Quad was only 1 unit high – this meant that there’s 0.1 units of the RenderCamera’s background colour peeking through around the edges. On a 4:3 window, this is a nice even border – at other resolutions, it may not be so authentic to the C64 experience, but it means that the full quad is rendered, with a nice chunky border, at any resolution and aspect ratios all the way from 5:4 to 16:9.
One final aside about this two-camera setup: the calculations for the size of the WorldCamera, and offset and scale values of the render material, are very straight-forward to implement in code. I did so in lines 71-84 of my game controller. With that approach, it would theoretically be possible to change the “native” resolution on the fly at runtime, if you found a use-case for that.
Shaders for CRT effects
There’s certainly must more sophisticated ways to get a CRT effect than what I used this go-around: VHSPro in the Unity Asset Store looks amazing, but it costs $50 and using paid shaders isn’t really in the spirit of Ludum Dare, and the libretro project has an amazing collection of open-source shaders that could probably be ported to Unity if you know how shaders work – but I don’t.
Instead, I just used a handful of the ones in the Image Effects section of Unity’s standard assets. Applying these filters is as simple as adding the relevant scripts as components on whichever camera you’re looking to modify – in this case, they all went on the RenderCamera. Specifically, in order, I used the following:
- Noise and Grain
- Vignette and Chromatic Aberration
With some tweaking of the settings for these shaders, I pretty darn close to a good CRT effect. The only thing I’d possibly want beyond that is possibly some sort of scanline effect, but in my experience those often don’t look as good as you’d hope.
A final note on “authenticity”
I hate that word, so I feel it’s necessary to call out precisely all the ways that Xtreme Crop Duster Simulator ’82 cheats compared to what the real Commodore 64 could do.
- I mixed up text and graphics modes:
- The text mode is 320×200, so all the “regular size” text rendered in the game should properly be twice as wide as it appears in the finished game.
- While you could mix modes on screen at once, they couldn’t be on top of each other. It would be more realistic to have all the text at the top and bottom 8 or 16 pixels of the screen, with the sprites only appearing in between.
- I played fast-and-loose with the rendering of sprites and colours:
- “Sprites” on the Commodore 64 were properly one colour, while mine get free run of the 16 colours of the C64 palette.
- With appropriate trickery, you could of course render them in multiple colours. However, it’s not as simple as any pixel can be any colour. Each 8×8 block of pixels on the screen could be assigned 4 unique colours – so sprites either had to move 8 pixels at a time, risk colour artifacting as they crossed the block boundaries, or just use a limited palette across all sprites so it’s a non-issue.
- I only half did proper SID emulation:
- The SID is the famous and amazing 3-voice sound chip in the Commodore 64. While all the sound in the game is limited to three voices at once, the “sound effects” voice (engine sounds, crashes, etc) is playing sounds made in ChipTone so they’re not as accurate as they could be.
None of that probably matters to anyone but me, but I have a keen appreciation for the gaming platforms of yesteryear, so it keeps me humble to remain aware of all the ways I “cheated” even when staying within the constraints of a relatively “authentic” style.
That’s a whole lot than I thought I would write. Hopefully some of you find this interesting – since the graphical style is a defining feature of my game, and probably the place where I learned the most during this most recent Ludum Dare, I thought it may be valuable knowledge to share.
And hey, if you’ve made it this far without playing and rating the game – why not do that now?
Final bonus screenshot
I mentioned that this approach supports arbitrary “native” resolutions. For a larf I plopped in a screenshot of my very first Ludum Dare game, Smugglers!, which emulated the 4-colour 320×200 appearance of an early IBM PC with a CGA card. While the CRT effects are probably overdone here, the aspect ratio correction makes it a lot closer to my original intention: