In this post I’ll go over some some shader basics, talk a bit about choosing an aesthetic for InnerSpace, and give a breakdown of our shader. I’m excited to show some of what we’ve been working on and I hope you like it as much as we do. Just one quick note: All assets, screenshots, and shaders are works in progress and may not be reflective of the final aesthetic.
What is a Shader?
Code that makes stuff a little less dark and a little more light
Put simply, a shader is a script that defines how an object will be drawn and shaded in a 3D scene. I like to think of shaders as being analogous to physical materials in the real-world. Light responds differently to glass than it does to stone or rubber. Their physical properties interact with light differently and ultimately inform the way said objects are shaded. Different materials create different shading, so in games we use shaders to mimic those types of materials.
The inherent advantage of games, and why I love them so much, is that they aren’t limited by the physics that limit the real-world. We can make an object reflect more light than it is being hit with, we can make an object invisible or visible depending on the camera direction, and we can even make an object change color depending on how close you are to it (and that’s only scratching the surface). By leveraging the math used in programming shaders, developers are able to create the stylized visuals that you see in games like Wind Waker, Killer is Dead, Katamari, and Journey.
Some examples of non-standard shaders:
The shaders we made and why they’re cool
Going in, we knew that our studio would be limited by time and manpower, so we sought to create shaders that would require less work on our parts, while still providing compelling visuals for the player. Because of this constraint, realism was clearly not an option (and not really our style, anyway). To me, the visuals of our game felt like they needed to be more abstract and expressive. Taking ideals from impressionism and merging them with stylistic cues from Wind Waker and Cucumber Quest, we created a shading style that combined a more realistic, almost other-worldly silhouette, with controlled but expressive bursts of color. There’s something really gratifying about this process; it’s one of the coolest points of development for a game. Making shaders is when we get to decide exactly what our game is going to look like, and it’s a major building block in defining a game’s atmosphere.
Beautiful, easy to read, and time saving. Those are the three goals for our aesthetic. With a small team like ours, time is of the utmost importance. Modeling, unwrapping, and texturing can take up a lot of that time, and the results of a mismanaged pipeline are extended deadlines and antsy programmers. Clearly we don’t want that, so we knew from early on that our shaders would need to save us some effort. We decided to go with a shader that doesn’t need a texture to look good. In other words, one that adds detail as you get closer and becomes an easy-to-read silhouette as you get farther away. Our shaders run the gamut from full detail to unlit color and, in addition to saving us asset development time, net an in-game efficiency boost because of it.
How We Made It
The slightly more jargon-y bit about the core elements of our shader
Element 1: View-Direction Based Shading
The entirety of our shader is built off of a little trick we discovered while experimenting with custom shading (custom shading refers to writing your own shader from scratch, instead of working off of a preset like Blinn-Phong). Normally, shaders use the Normal Direction vector and the Light Direction vector, along with a Dot-product function, to shade objects. The theory behind this is that the two vector directions are evaluated and assign a value ranging from black to white (0 to 1), based on how similar the vector orientations are. We found that if you replace the Normal Direction vector with a View Direction vector you get a result that smoothly transitions over the surface of an object without taking into account any surface detail of the object. The advantage to this method is that, unlike flat shading, we are able to achieve a flat shaded appearance while allowing objects to respond to scene-lighting.
View-Direction based shading with, perpendicular to, and away from light direction:
View-Direction basic node tree:
Element 2: Colored Shadows
Perhaps the most unique and flashy element of our shader, Colored shadows affords us unique control over one of the most primary elements of modern shading. This one definitely gave us some trouble. On a per-object basis, we are able to control the color, saturation, and brightness of the shadows in our scene. This means that we can really drive home a unique and colorful aesthetic, and can even have shadows that are brighter and of a different color than the light that is casting them. Much like the shading in Element 1, shadows are generally greyscale data applied to the surface of an object using a multiply function. We opted to use an overlay function and, in effect, replaced the greyscale data with color data thus allowing for shading that made colors more saturated instead of dimmer.
Regular shadows v. colored shadows:
Basic Colored Shadow node tree:
Element 3: Shadows Masking and Fresnels
The final element of our shader uses another fun and simple trick. We knew that we were using a lot of flat or low-fi shading and quickly realized that our objects would need more dimensionality; the flat look that we had created just didn’t give the player enough visual feedback. We kept flying into walls because we couldn’t tell how big an object was or how close we were to it. Pulling, once again, from our references we opted to use a fresnel effect in the shaded regions. We had learned from previous testing, and as a recent industry trend, that image-based lighting (IBL) is an excellent way to ground your objects in a scene and make them look like they really belong in that place. This effect is most commonly seen on mirrors or highly reflective surfaces, but has been used more recently on matte surfaces, too.
The basics of IBL are that an object is lit using the colors from objects and lights in the surrounding scene. This has a grounding effect on the object, as it makes it appear like it really fits into that space. While our aesthetic ultimately became too stylized to really use IBL, we were able to embrace it in spirit. By using the greyscale shadow as a mask over our colored fresnel, we established dimensionality in our shadows, grounded our object by matching the fresnel color to the ambient environment color, and retained the pseudo flat-shading in the brighter areas of our object.
Colored fresnel with vs. without Shadow Masking:
Basic Shadow Masking node tree:
The Results and Final Thoughts
After combining the above elements, and a few less ground-breaking ones, we established two primary shaders: one for highly specular/conductive surfaces like metal, and one for matte/insulating surfaces like wood. Almost every asset that we’ve made is comprised of pieces that fall into one of those categories. As a result, we established a wide variety of materials like cloth, stone, grass, wood, etc., that are based on the two master shaders. These various secondary shaders are applied to the appropriate surfaces of in-game objects and, with a few more additions, like vertex-driven ambient occlusion, create a large amount of visual diversity, while still maintaining a consistent aesthetic. By approaching our design in this manner, we drastically lightened the per-object workload, as our shaders offer a wide range of customizeability, and don’t require specific specular, normal, or diff maps.
No TL;DR this time, but maybe give the earlier screenshots another once-over with your new knowledge of our shaders and what it took to make them. We’re very proud of the work we’ve done and the style that we’ve established and we hope that you enjoy viewing it as much as we’ve enjoyed making it.