About Me

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


View Joshua Ols's profile on LinkedIn


SH 0th order as an ambient light sensor


The other day an Alloy customer came to me with a weird problem that I never could have anticipated. Basically, they have a full PBR pipeline including physical materials, high intensity lights, and filmic tonemapping with automatic eye adaption. Their game also has a slick in-world 3D UI system involving an emissive holographic display. Independently, these things are great, but apparently they break down when put together.



You see, the emissive display looked great in typical indoor lighting settings, but would become unreadably dark when the player went outside into direct sunlight. The culprit was the automatic eye adaption responding to the bright sunlit surroundings and dropping the exposure to darken everything, including the emissive display. Essentially, this is the exact same problem that smartphone displays face in the real world.



So how do smartphones try to deal with this problem? They use ambient light sensors to determine the overall brightness of their environment and crank up their screen brightness to compensate. So how do we accomplish this with a shader?

It turns out that Unity provides all the information we need in the form of light probes. Specially, the probe’s Spherical Harmonic’s (SH) 0th order is a constant color representing the average of all incoming ambient light at that point in space. Thus, the luminance of this color is a pretty accurate measure of the light intensity of its surroundings.

With this information in hand it was easy to dynamically scale the display’s emission intensity based on a curve, which now made it viewable under all lighting conditions.



Unity shader code injection


I’m sure many of you have had this thought, “Boy, Alloy is great but I really wish I could use its area lights and nice attenuation with other Unity shaders!” As luck would have it, I am implementing that very ability using a little known feature of Unity’s shader system.

“What might that feature be?”, I hear you ask. Well…


Shader code injection

That’s right, it is possible to force a shader to use custom code without having to modify the shader. It works because of how Unity’s shader system resolves “#include” directives for handling headers. If you use it like “#include “myHeader.cginc” with just the filename and no path then Unity will first look in the same directory as the shader file, and then search the editor’s hidden include directory.

So basically, if the shader uses an editor header (eg. “UnityCG.cginc”) you can trick that shader into using a customized local version by copying the header into the same directory as the shader. Then you can simply modify that local copy and the shader will use that instead. This will work for any shader code, even the Standard shader and the generated output of a Unity surface shader.



After mucking around in surface shader output, and mangling some of Unity’s macros, I now have it working with Alloy. So any surface shaders that use the Standard and StandardSpecular lighting models can now be lit using Alloy’s lighting model and material options. This way, forward-only shaders can now match deferred-compatible shaders when used alongside Alloy.

Keep an eye out, as we will be releasing this new feature in beta form in our next bugfix release.



Unity, Consoles, Shaders...


Also it seems like it would be a good idea to test in OpenGL mode on an AMD/Intel GPU. Their GLSL compilers seem especially strict, so you're likely to catch the most bugs on those platforms.



Apparently you also need to assume some of the strict assignment and swizzling rules of GLSL. Certain things like accidentally assigning a four component vector input to a three component variable will pass for Direct3D, but blow up once you hit OpenGL.



To all the people authoring shaders for Unity with the goal of having them work on all of Unity's supported platforms I say the following:

  1. The shaders are HLSL! Unity still calls them Cg for legacy reasons, but they've all but abandoned that platform. So when looking up how to do something in the shaders, look for HLSL tips!
  2. Read the header "HLSLSupport.cginc" and burn its contents into your memory. Inside you will find all the crazy preprocessor juggling that Unity has to do to hide platform and language API differences. 

Today I learned this lesson the hard way with Alloy's parallax occlusion mapping code. Basically, in Cg if you want to apply texture coordinate derivatives then you use an overloaded version of tex2D(). In HLSL, you have to use an explicit intrinsic called tex2Dgrad().

Guess which one is platform-safe in Unity's shaders? To find the answer, follow tip #2. :p


Alloy 3.1 - Asset Store Madness

It's that time again! The Unity Asset Store is having another madness sale. For this week only, you can pick up Alloy 3.1 half off at just $62.50! If you've been hesitating due to price, now's the time to jump in and grab it! XD

Alloy - Unity Asset Store


TeamColor refinements 


Not long ago, I realized that Alloy's Team Color formulation was grossly inflexible and not super intuitive. The primary reason being that if you had more than one color mask overlapping a given pixel, the top one would dominate. This presented a problem if you wanted to mix multiple colors and precisely control their contributions to the final combined color.

It looked a little something like this:

half3 tint = lerp(half3(1,1,1), aTint, masks.a);
tint = lerp(tint, rTint, masks.r);
tint = lerp(tint, gTint, masks.g);
baseColor *= lerp(tint, bTint, masks.b);


As you can see, it was layer order-dependent and made it basically impossible to have all the colors influence one fragment in a clean way. The only problem it really solved was avoiding black zones between masked colors by ensuring that the starting color was white.


New Approach

I needed something that would still prevent the black zone problem, but also allow easy blending between all the masked colors covering a given pixel. Plus, when the total weight of the masks is above 1, it needs to renormalize all the mask weights so that they total 1. Finally, when the total weight is below 1, it needs to add the color white by the remaining weight. 

So I switched to something more like this:

half weight = dot(masks, half4(1,1,1,1));
masks /= max(1, weight);
baseColor *= rTint * masks.r 
            + gTint * masks.g 
            + bTint * masks.b 
            + aTint * masks.a 
            + (1 - min(1, weight)).rrr;


This new approach does everything I need without being overly complex and/or costly.