About Me

Hi. I'm Josh Ols. Lead Graphics Developer for RUST LTD.

Contact:
crunchy.bytes.blog[at]gmail[dot]com

View Joshua Ols's profile on LinkedIn

Meta

Entries in RNM (3)

Monday
Feb282011

Robot Butler - My first shipped title!

IMG_08022011_190739

Ladies and gentlemen...I give you Robot Butler! This game was made in 6 weeks for Kongregate's Unity 3D game contest. In it, you play LX-27, a robot butler just trying to attend to his human masters' needs while keeping them from being slaughtered by invading rogue robot butlers!

 

My role in this project was designing a set of custom enviroment and character shaders that would allow the game to look good and run smoothly on a wide range of hardware. Given that we had to use the basic Unity3D package, this was quite a tricky task.

 

Unity3D:

First, let me say that Unity is a capable, affordable game development platform. In particular, the most recent iteration has a rather convenient system for designing surface shaders. Sadly, its lighting system left us in a bit of a bind.

 

Now under ideal conditions, we could just use their new deferred lighting renderer and call it a day. Sadly, this dramatically ups the hardware requirements for anyone who would want to play our game. Plus, the project's artist would have had to scale back his lighting setups to get reasonable performance. Finally, it would have forced the whole team to buy Unity Pro licenses to work on the game.

 

Sadly, Unity's forward renderer wasn't quite right either, since its Single Lightmap system is rather lacking. Not to mention having no Irradiance Volumes to integrate dynamic objects with the static environment. Our artist needed a capable replacement that would scale well to older hardware, support the full suite of baked lighting effects, and handle highly detailed surfaces. So let's take a look at what I had to do to meet those needs.

 

Environments:

So the buzzwords here are: Radiosity Normal Mapping, Detail maps, & Fake specular cube maps.

 

To get the best kind of baked lighting for the least storage, we needed support for Radiosity Normal Mapping. This way, we could use low-res lightmaps, while still being able to support arbitrarily detailed normal maps. Plus, all normal-mapped surfaces could get the benefits of radiosity, ambient occlusion, and an arbitrary number of lights with soft-shadowing.

 

Now, while this system doesn't cover specular, we were able to do a convincing job using a cube map with fake highlights. Then it was a simple matter of multiplying it with the diffuse contribution to get color, shadowing, and ambient occlusion. Not perfect by any stretch, but adequate for this project.

 

For more details, please check out my official post on the Unity 3D forums. ;)


kitch_01 kitch_02 kitch_03 kitch_04 

Figure 1. 1, Flat RNM lighting; 2, Normal-mapped RNM; 3, Normal-mapped Fake specular; 4, final materials

 

Characters:

So the buzzwords here are: Rim lighting, Environment maps, AO maps, & Constant Ambient.

 

We used high-quality scene capture environment cube maps for reflections, which provides most of the enviroment's influence for dynamic object lighting. The rest comes from rim lighting and a constant ambient color with Ambient Occlusion. Despite its simplicity, this worked out well since all our dynamic objects occupy so little screen real-estate.

 

The only major annoyance we encountered was getting a decent system for setting zones for the varying colors/environment maps, and then smoothly interpolating/switching between them. Each dynamic object had to be aware of all the zones, and handle the transition itself. If our game world wasn't so small, and we didn't have so few parameters to change per zone, this would have become unmanageably cumbersome in a big hurry.

 

Despite its limitations, the results look great and are quite efficient.

 

Character shader:

RB01 RB02_env_occ RB03_addnormals RB04_complete

Figure 2. 1, Ambient Occlusion; 2, Environment map; 3, Normal mapped Env map; 4, final material

 

Lighting Environments:

RBinEnv01 RBinEnv02 RBinEnv03 RBinEnv04 RBinEnv05 RBinEnv06

Figure 3. 1,bedroom; 2, bathroom; 3, living room; 4, dining room; 5, kitchen; 6, shed

 

Conclusions:

Overall, I'd argue that despite their shortcomings, these custom shaders make Robot Butler one of the prettiest Unity3D games you will ever see. That is, until they really start taking advantage of their Beast license to provide more advanced static lighting options. ;)

 

In the meantime, go play the game people! :p

Wednesday
Dec222010

RNM Shaders + Unity 3D

Well, that happened a little sooner than I was expecting. Tonight, my artist friend managed to complete the example scene & documentation. So I decied to go ahead and post our RNM shaders, documentation, example scene, etc, on the Unity forums.

 

Hope you guys find them interesting/useful.

 

P.S.

Don't forget to check out my new IBL article below. ;)

Sunday
Feb072010

Self-Shadowed Bump maps

UPDATE:
Something I thought I should mention that I realized only recently. In the section "Usage tips", I realized I was in error by saying that you need to swap the green & blue channels to make it work with RH normals. It is actually much simpler to swap the bottom two vectors in the basis in the shader to get the same effect at no extra cost. This way, you can use readily available tools, without any need to modify their output and introduce another step in the content creation pipeline.

Other than that, forget to mention the bonus pictures I added to the end of the post. Check them out, if you haven't already. ;)

UPDATE2:
SashaRX was asking about reflection vectors from an SS Bump map, so I thought I'd add it up here where everyone can see. Basically, all you need to do is get a tangent-space normal back, and then apply standard techniques from there. This is achieved by applying an inverse transformation to the SS Bump value using the basis vectors. Using just the basis vectors, the result will be in tangent-space. If you concatenate the tangent2world matrix with the basis vectors, you can transform staight to world-space.

Hope that helps. ;)

Code:
tanNormal = bumpBasis[0] * bump.x + bumpBasis[1] * bump.y + bumpBasis[2] * bump.z;
tanNormal = normalize(tanNormal);

Original:

For those who don't know, Self-Shadowed Bump maps were originally developed by Valve for use with their Radiosity Normal Mapping technology. After I decided to make the switch to a renderer built around pre-computed data, this was one of the first things I decided to try out. If you wish to read more about the technique, the following links have provided me with most of my information on the subject.

Links:

[1] Efficient Self-Shadowed Radiosity Normal Mapping

[2] Surface Detail Maps with Soft Self-Shadowing

[3] Half-Life 2 / Source Shading

[4] Shading in Valve's Source Engine

[5] SSBump Generator (used to make the maps)

 

Overview:

During the course of my experiments, I have verified all the benefits that Valve described in their papers. However, I have also discovered a few drawbacks that they didn't mention. Keeping that in mind, I have pooled and summarized my findings from all the papers and my own experiments.

Benefits:

  1. Directional Occlusion

  2. Correct filtering

  3. Detail mapping

  4. Recovers bent normals

  5. Better use of byte precision

  6. Faster than tangent-space normal maps

    • NOTE: Only for RNM, not for general diffuse lighting

Drawbacks:

  1. Diffuse Lighting expense

    • In order to get dynamic lighting with directional occlusion, each light direction has to be projected onto the basis. This boils down to the same amount of math as a 3x3 matrix multiply, per light direction.

  2. Poor compression

    • DXTC causes artifacts at detail edges, and nasty blocky artifacts in specular/reflection

    • RGB5 is acceptable for diffuse lighting, but produces banding for specular/reflections. Also, this format is not supported by Direct3D 10/Xbox 360.

 

Visuals:

height normalMapRH bumpRH ss bumpRH (SS Bump) recovered tangent (SS Bump) no DO SS Bump (bumped) (SS Bump) reflection

Figure 1. 1, Height map; 2, Normal map; 3, SS Bump (no DO); 4, SS Bump (DO); 5, recovered tangent normals; 6, no DO; 7, DO; 8, reflection

 

Masking:

(10) Red,  Right (11) Green, Upper Left (12) Blue, Lower Left (13) Right (14) Upper Left (15) Lower Left (16) Right (17) Upper Left (18) Lower Left

Figure 2. 1-3, RGB channels; 4-6, lightmaps w/o masks; 7-9, lightmaps masked

I added these images to help visualize how these maps just store multiplicative masks for the lightmaps. Note how sides facing the light direction receive almost no darkening, while those that are occluded are darkened to give the impression of shadowing.

 

Detail Mapping:

(SS Bump) enhanced detail (SS Bump) detailed (9) Recovered normal 17 - VTF 2010-02-24 01-46-50-17

Figure 3. 1, SS Bump map; 2, Detail map; 3, combined with SS Bump; 4, recovered normal; 5, diffuse lighting

This trick works since an SS Bump map is just a series of masks that get multiplied with the lightmaps. Since the lightmap data will be multiplied with the diffuse texture, we can effectively multiply the diffuse texture by the detail map if we just apply the detail map evenly to each color channel in the SS Bump map. This even weighting of each channel is what ensures that the recovered tangent normals are unaffected by the detail map.

 

Compression Artifacts:

(SS Bump) DXT1 (SS Bump) DXT1 diffuse (SS Bump) DXT1 reflection

Figure 4. 1, DXT1 SS Bump map; 2, diffuse lighting; 3, reflection


(SS Bump) RGB5 diffuse (SS Bump) RGB5 reflection

Figure 5. 1, RGB5 diffuse lighting; 2, reflection

 

Usage Tips:

Most tools that generate SS Bump maps will format them for the Source Engine, so they assume a left-handed coordinate system. This will cause issues with any programs that use a right-handed coordinate system. Fortunately, this issue can easily be fixed in a decent image editing program (Photoshop, Gimp, etc).

Tangent-Space normals:

Here, you just need to invert the contents of the green channel to flip the Y-axis.

normalMapLH normalMapRH

Figure 6. 1, normal LH; 2, normal RH

 

Self-Shadowed Bump maps:

Here, you have to swap the contents of the green and blue channels.

SS BumpLH ss bumpRH

Figure 7. 1, SS Bump LH; 2, SS Bump RH

 

Other than that, I thought I'd mention a problem I encountered while writing shader code to use these maps. The order of the bump basis vectors as Valve listed them in the paper can be a bit confusing, and will produce incorrect results that will have you scratching your head for days. To save you that trouble, here is how they should appear:

Code:

static half3 bumpBasis[3] =
{
    half3(sqrt(2.0) / sqrt(3.0),              0.0, 1.0 / sqrt(3.0)),
    half3(     -1.0 / sqrt(6.0),  1.0 / sqrt(2.0), 1.0 / sqrt(3.0)),
    half3(     -1.0 / sqrt(6.0), -1.0 / sqrt(2.0), 1.0 / sqrt(3.0))
};

 

Final Thoughts:

Despite the added storage and lighting cost, the benefits from using this technique are too enticing for me to pass up. So I have decided to integrate this technology into my new renderer for handling static objects. Dynamic objects, or anything that needs detail normal mapping, will still use Partial Derivative Normal maps for efficiency & flexibility. Between these two solutions, I should have a sufficiently versatile system for generating pleasing visuals.

 

Bonus:

Something I should have added a long time ago, there are two screenshots that have been sitting the in the comments section for some time now. They use a, SS Bump map from Valve's wiki, and really shows just how much depth this technology can add to even a flat surface. Check them out! ;)

(SS Bump) detailed 1(SS Bump) detailed 2

SS Bump map from Valve's wiki, "$ssbump"

Figure 8. 1, 0 degrees; 2, 90 degrees