HLSL Sky Demo Manual

by Zaknafein (Renaud Bédard)

Last updated on January 18th, 2006.


Foreword

This document (hopefully) will help you integrate the sky module of my HLSL Sky Demo into your own project.
It covers the following points :
  1. The engine model, what the overriden methods do and where the data comes from;
  2. The sky model, how the meshes and shaders work;
  3. The different settings that have an effect on how the sky is rendered;
  4. What can be taken off or moved to initialization;
  5. The known problems and annoyances, and what causes them;
  6. Future work, things you could do to make it look better.

1 - Engine Model

You'll find that most classes, including the one that interests us (Sky.cs), inherit from CoreUser. This is a little pattern that probably has a name in some book somewhere but I just made it from the top of my head.
Basically, all CoreUsers have access to all "Factory" TrueVision3D classes; i.e. TVMaterialFactory, TVTextureFactory, TVScene, TVLightEngine, etc. With the exception of TVInputEngine which is added for convenience. The thing is that they did not create them; they access singleton instances made in Core.cs.

Now, during the mainloop in Core, there are three stages : I am saying all this because there is a whole lot of stuff in Sky that must NOT be called each and every loop, because they are very CPU-intensive and instruction heavy : the sun position calculation, the distrubution coefficients calculation, the zenith chromacity calculation.
These two last calculations were taken out of the sky Vertex Shader for this very reason; a vertex shader has to be run each loop, it transforms the vertices to clip space. But my calculations did not need that, so I transfered them to C# code (following Waterman's advice, by the way).
So, however your engine works, make sure you have a seperate "thread" (not in a literal sense...) that does those calculations. You'll lose pointless FPS otherwise.
Waterman proposed another (and better) way of load balancing, which spans the action on different frames based on a modulus operation. This way even less work is done every loop, and it still looks good. I encourage you to implement that in some way. :)
Other thing, I don't use AccurateTimeElapsed or any TV3D time-calculation stuff of any kind. I've had nothing but bad experiences with it. If it works for you, fine, use it. I use WinAPI functions to get precision timers.
Anyway, the _accumulatedTime(for Update) and _elapsedTime(for ForcedUpdate) represent the amount of milliseconds that occured since the last update, or in the current loop, so it should be fairly easy to adapt it.
I also use a .Net DateTime object for the game-time. This was the simplest way to get useable time calculations, and I encourage you to use this too. It's just so damn simple. :)

On the file structure side, the only two files that you'd need to carry in your project would be Sky.cs and SkyMaths.cs. The separation between the two files is really conceptual, because you could put everything from SkyMaths to Sky without problems. But it helps on readability and clarity.

2 - Sky Model

Here is a wireframe screenshot of the domes. You can tell that there are two meshes; one finely tessellated and the other alot less :

Hemisphere

The finer one is Hemisphere.tvm. It is a half-geosphere made with 3D Studio MAX. I opted for a geosphere, because the normal sphere primitive has a lot more tessellation in the top, and little on the sides; as the color changes are alot "tighter" on the horizon than at zenith, this was not a good option. A geosphere has perfect distribution of polygons all across its surface, and looked like the best option.
The hemisphere mesh could be tessellated a little less for more speed, but he less polygons you put, the less detail you have in the sunrise/sunset transitions.

The hemisphere is used to represent the sky colors depending on the sun position, and to paint the stars during night-time.
It is mapped to SkyShader.fx. This shader effect file is mostly a huge vertex shader which calculates the un-lit vertex colors for each vertex, and a small pixel shader which interpolates the vertex color with the stars texture when the night falls.

Clouds Dome

The rougher one is Dome.tvm. It is a squeezed half-sphere made with 3D Studio MAX. It's made so that the zenith is almost planar and the horizon drops really fast. It doesn't need high poly-count because no work is done in the vertex shader, only the pixel shader.

This one is mapped to CloudsShader.fx. The shader is actually very simple. It takes the cloud colors, the layers opacities, the clouds translation and scale, and then mixes both cloud layers to the single resulting texel.
The former implementation (in my 6.2 Sky Demo) used two separate domes. This was a total waste of polygons, since they were the same shape and size. Now, it's a single dome but two blended texture layers, and better performance.

3 - Settings

Several settings have been made modifiable in real-time in the demo platform : turbidity, wind power, clouds size, cloud layers opacity, and the game-time factor. I'll skip on the game-time factor because it's not related to the sky rendering stuff, it's just for demo purposes.
There is also a couple of other settings that I classified as "constants" in code, because they shouldn't be changed at runtime.

Turbidity

Turbidity measures how thick the atmosphere is. A high (4-6) turbidity level implies the following : The fog is EXP2 model, and the density is set with TV3D code using the following factors :
_atmo.Fog_SetParameters(0f, 0f, (float)(_turbidity / 8500d));
_atmo is there a TVAtmosphere object.

The turbidity settings are also clamped from 2 to 6. The reasons behind this are a little obscure, but all implementations I've seen on GameDev.Net agree on this : even if the original paper uses turbidity levels up to 30 to do entierly overcast skies, the Perez function (used in sky color computation) return values are almost undefined after turbidities of 6-7, and below 2.
They suggest using the CIE Overcast Sky Model for turbidities over 6, but it looked like a waste of time to me for the time being. Instead, maybe the darken the clouds' color, put both layers' opacity to 1.0f and make the cloud size bigger, with a turbidity of 6.

Here are a couple of thumbnails of the same time at different turbidities (2, 3.5 and 6). First series of shots is taken at 6:00 AM, and second at 11:00 AM.


It might like look like the sunrise/sunset is clearer in higher turbidities; that's because at the same time, the sky colors are later in the sunrise sequence at higher turbidities. That's what I meant by "longer sunset/sunrise sequences".
The blue-green tones are also noticeable in the day sky. It may be an error in the color distribution, but it gives a "stormy skies" feel too...

Another thing that's worth mentioning is that turbidity changes have a lot more effect in the early numbers (2, 3) than in the later ones (4-6).

Turbidity is a complex effect, and it has incidences on nearly everything in the scene. I'll cover other points later. I find that the perfect all-around turbidity is ~2.25, but that's up to you of course...

Wind Power

Well, that's alot more straightforward.
Wind power defines the direction and speed at which the cloud layers scroll on the cloud-dome. It's a TV_2DVECTOR, so that .x defines the X component of the direction, and .y defines the Z component. It's in very small numbers, and the speed in accumulated in two other vectors (_cloudsTranslationOuter and _cloudsTranslationInner) that define the amount of u/v texture coordinates that are scrolled.
You can't control each layer's wind speed, because the inner layer (composed of smaller clouds) are hard-coded to move two times faster than the outside layer, based on the general speed that is set.

Clouds Size

It's a linear scale of the cloud-dome texture coordinates for each layer. In this case, the .x of the TV_2DVECTOR means the inner layer, and .y the outer layer.

Cloud Layers Opacity

Alpha level of both cloud layers, how much they appear. On a 2 turbidity level, you should lay off the clouds normally. Or put nice big cumulus ones (outer layer), not the thin saturated inner layer.

Other Settings ("Constants")

In the "Constants" region, you'll find some other parameters : The other two aren't parameters, they're quite absolute (trial and error).

4 - Optional And Movable Parts

For optimization's sake, you might want to tear my classes up to bits and shreds then make it whole again. It's open source, I can't stop you from doing that. I wish I could though. But I'd still like to give you some pointers on what will make it all crash, and what doesn't matter at all.

Optional Parts

Movable Parts

Another thing is that many calculations are done every Update and could very well be done once at initialization in other contexts :
The rest is pretty much fixed, but nothing restrains you from trying to chop out some more stuff.

5 - Known Issues

Sadly, there is a number of issues that I couldn't really fix, and had to work around. Some of these workarounds are costly on the FPS side or on the code structure side, so you'd better know about them.

Luminance Clamping = Headache

The xyY color values calculated by the distribution function are composed of two Chromacity coordinates (x, y) and one Luminance factor (Y). Of course to make that renderable, we need to transfer them to RGB space, and there are documented ways to do that, with linear transformations.
The problem is that the luminance is not 0-1 like chromacity coordinates are. I guess that the initial paper planned to use HDRI so that the actual lighting emitted by the sun is weighted properly. But since we're in LDR colors, we need to clamp that to 0-1 in some way.
But luminance isn't only a measure of how bright the color is; it also kinda defines its saturation and how pastel it is. It relates to color temperature I believe.

So, after a couple of days of experimentation, I found that not one, but two exponential factors on luminance and another gamma correction on the final RGB color produced the best results. Here are the C# code lines :
float gamma = 1f / (1.6f + (_turbidity - 2f) * 0.1f);
_skyShader.SetEffectParamFloat("_invGammaCorrection", 1.5f * gamma);
_skyShader.SetEffectParamFloat("_invPowLumFactor", gamma);
_skyShader.SetEffectParamFloat("_invNegMaxLum", -1.25f / SkyMaths.MaximumLuminance(_turbidity, sunTheta, _zenithColors, _distribCoeffs));
...And how they're used in the HLSL vertex shader :
skyColor[2] = pow(1.0f - exp(_invNegMaxLum * skyColor[2]), _invPowLumFactor);
...
OUT.vertexColor = pow(OUT.vertexColor, _invGammaCorrection);
skyColor[2] is the third index, so the Y luminance factor.

So basically : Two pow and one exp HLSL instrisic calls per vertex is not what I'd call "fully optimized". But I just can't manage it otherwise. If you have the courage to venture in there, try other values and methods, and tell me if you get something good. :)

Moon and Ambient Lighting Hacks

You might see a TODO tag in my code in the following lines :
// TODO : A bug with ambient lighting forces me to use the material instead...
float ambientColorRG = -0.05f * (1f - sunIntensity) + 0.21f;
// _lights.SetGlobalAmbient(ambientColorRG, ambientColorRG, 0.2f);
// _sunLight.SetAmbientColor(ambientColorRG, ambientColorRG, 0.2f);
_matFact.SetAmbient(_globals.GetMat("Matte"), ambientColorRG, ambientColorRG, 0.2f, 1.0f);
First, I wanted to implement another directional light for the moon. Then Sylvain told me that for the time being, only a single simultaneous directional light is supported by the LightEngine.
OK. Ambient lighting was then the logical way, and anyway it'd be faster this way. But another wall; the SetGlobalAmbient method does not work! Sylvain had no idea why when I asked him.
Alright... I can manage it... let's just use the light's ambient color, with a fully ambient-recieving terrain material. STILL DOESN'T WORK! Aaah, the joy of beta software...

So I had to resort to the last option; play with the ambient color of the material itself. That is really annoying, because if you have ten materials in your scene, you'd have to iterate through all of them and change their color every Update call. But eh. Can't say I didn't try now can you.

AttachTo vs. Code Repetition

Another TODO tag a little later :
camPos = _camera.GetPosition();
_skyHemisphere.SetPosition(camPos.x, 0f, camPos.z);
_cloudsDome.SetPosition(camPos.x, 0f, camPos.z); // TODO : AttachTo should work soon
Soon... well the next beta DLL should work.

Thing is that I can't attach the clouds dome to the sky hemisphere and use relative scale and position. Looks like the child doesn't inherit its parent mesh matrix properly. There were a number of posts in the Beta forum about this, and it should get fixed soon enough. Anyway, it's not that bad, only a couple unneeded lines.

6 - Future Work

For one thing, clouds are nowhere near being perfect in this demo. Waterman's pixel-shaded clouds have a lot more depth, and a little research on that would surely help making the sky alot more realistic.

It would be nice making the shaders with SM 1.1-compatibility, with ps_1_4 and vs_1_1 codepaths. I tried, and suceeded with the clouds shader (just replace the pow intrinsic by a texture lookup), but it made it slower so I left it behind. The sky shader is something else though... It has a full 249 vertex shader instructions, and must fit in 128. The biggest problem is the lack of preshaders in vs_1_1, which makes all calculations done every vertex. Lots of stuff would need to be done in the C# code, and it would probably make things alot messier.

But of course I don't plan on doing any of this. Unless I have a lot of courage and time on my hands. :P


...thanks for reading!