Lately, I’ve been intrigued by shaders that emulate other visual effects and certain camera technologies. I’ve been considering doing a night vision kinda thing for my current project for quite some time, but I’ve been feeling kinda ambivalent about the whole thing. Recently, I got Zombie Gunship from the Humble Mobile Bundle, and they do the effect very impressively.
This got me thinking, what would it take to implement something like this? The primary requirement, other than looking badass, would be that I don’t have to re-author any graphics assets. I’d want all my geometry and textures to just work. Because I am lazy. The secondary requirement would be that the shader code should be as simple as I can make it, again because I am lazy (but also for efficiency’s sake).
Let’s throw together a quick and dirty implementation of a night vision-style shader. We’ll take it in stages, to make it easier for the kids at home to follow along. I write all my shaders in GLSL|es 1.0 (for OpenGL|es 2.0) for maximum compatibility, and they should be pretty trivial to port to other versions of GLSL. Disclaimer: I’m not really a graphics programmer, but I play one on TV.
You may have a simple frag shader like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #version 100 precision mediump float; uniform vec4 uColor; uniform sampler2D uTexture; varying vec4 vColor; varying vec2 vTexCoord; void main() { gl_FragColor = uColor * vColor * texture2D(uTexture, vTexCoord); } |
This is (almost) the simplest possible shader for textured geometry. All we’re doing is blending the texel color with the interpolated vertex color and a mesh color uniform, and writing the result directly to the fragment output. No lighting, no bump mapping, no special effects.
As a first step, we can change the output to that cool green color. A naïve programmer might simply try to write full green into the gl_FragColor
output. But we are not naïve, are we? We know this would just turn the whole screen into a solid block of green. It also wouldn’t be helpful to just use the current green value and throw away the red and blue channels; if the starting texel is red, we’ll just get black output because we have no green to start with. Instead, we want to capture the intensity of the fragment, and create a green value with that intensity.
One simple way to get this information and craft our results is to use the HSV color space. The formulae for converting between HSV and RGB are a little unwieldy, but we can greatly simplify the calculation because we only want to capture the value component. And likewise, that value is applied to only one color channel, because we know we want the output to be a shade of green.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #version 100 precision mediump float; uniform vec4 uColor; uniform sampler2D uTexture; varying vec4 vColor; varying vec2 vTexCoord; void main() { vec4 color = uColor * vColor * texture2D(uTexture, vTexCoord); float v = max(max(color.r, color.g), color.b); gl_FragColor = vec4(0.0, v, 0.0, color.a); } |
On line 14, we take the max of the red and green components, and then the max of that result and the blue component. We use this value for the green channel of our output on line 15, giving us an all-green result that’s approximately as bright as the normal, full-color render:
Next time, we’ll look at a cheap way to crank up the contrast, to help give us that lo-fi CRT look. Please do feel free to comment and share! <3
// Copyright © 2013-2014 Brigham Toskin // This software is meant to be educational only. It is distributable // under the terms of a modified MIT License. You can find a copy the license: // <http://code.google.com/p/rogue-op/wiki/LICENSE> |