SilverLining

A simple example

Initialization

Complete sample code for Vulkan, OpenGL and Direct3D is found in the SampleCode/VulkanExample, SampleCode/OpenGLExample, SampleCode/DirectX9Example, SampleCode/DirectX10Example, and SampleCode/DirectX11Example directories of the SDK (The Linux SDK only includes OpenGL and Vulkan). Here's an overview of some of the key bits of code found in each.

When your application starts up, you'll want to initialize Vulkan, OpenGL or Direct3D, and create and initialize an Atmosphere object. If you want your clouds to be different every time your application is run, be sure to seed the random number generator as well.

Under DirectX9, it's recommended that you create your IDirect3DDevice object without the D3DCREATE_PUREDEVICE flag. Silverlining needs to call GetTransform and GetRenderState on your device, in order to put things back the way we found them when we're done drawing. We also recommend creating your device with the D3DPRESENTFLAG_LOCKABLE_BACKBUFFER flag, which will allow you more flexibility in configuring how SilverLining does its lighting (specifically, this will allow you to set render-offscreen to "no" in SilverLining.config.)

Under Vulkan, you must pass several objects into Atmosphere::Initialize via the environment parameter; this must be a pointer to a populated SilverLining::Vulkan::VulkanInitInfo object. Include our VulkanInitInfo.h header for the definition of this structure.

If your Atmosphere initializes successfully, you can go ahead and start configuring it to add your clouds, and specify the time and location you wish to simulate. We'll get into that next. If it does not initialize successfully, it's likely that the user needs to update the graphics driver. SilverLining has fallback cases for most cases where compatibility might be an issue, but some older drivers have issues even with standard functionality.

// Initialize your Direct3D device object, glut, Vulkan, or wgl as appropriate
InitializeGraphicsSubsystem();
// Instantiate an Atmosphere object. Substitute your own purchased license name and code here.
atm = new Atmosphere("Your Company Name", "Your License Code");
// Tell SilverLining we're rendering in OpenGL, the Resources directory is 2 directories
// above the working directory, and we're using a right-handed coordinate system.
if (atm->Initialize(Atmosphere::OPENGL, "..\\..\\Resources\\", true, 0) == Atmosphere::E_NOERROR)
{
// If you want different clouds to be generated every time, remember to seed the
// random number generator.
atm->GetRandomNumberGenerator()->Seed(time(NULL));
// Tell SilverLining what your axis conventions are.
atm->SetUpVector(0.0, 1.0, 0.0);
atm->SetRightVector(1.0, 0.0, 0.0);
// Set up all the clouds
SetupAtmosphericConditions();
// Configure where and when we want to be
SetTimeAndLocation();
// Start rendering.
glutMainLoop();
}

Here is an illustrative example of calling Atmosphere::Initialize() under Vulkan, from the Diligent engine. Your own application or engine will likely have its own means of obtaining the various resources needed here.

RefCntAutoPtr<IRenderDeviceVk> pRenderDeviceVk{m_pDevice, IID_RenderDeviceVk};
RefCntAutoPtr<IDeviceContextVk> pContextVk{m_pImmediateContext, IID_DeviceContextVk};
RefCntAutoPtr<ICommandQueueVk> pQueueVk{pContextVk->LockCommandQueue(), IID_CommandQueueVk};
RefCntAutoPtr<IPipelineStateVk> pPipelineStateVk{m_pPipeline, IID_PipelineStateVk};
SilverLining::Vulkan::VulkanInitInfo info
{
pRenderDeviceVk->GetVkInstance(),
pRenderDeviceVk->GetVkPhysicalDevice(),
pRenderDeviceVk->GetVkDevice(),
pQueueVk->GetVkQueue(),
pQueueVk->GetQueueFamilyIndex(),
pPipelineStateVk->GetRenderPass()->GetVkRenderPass(),
(VkSampleCountFlagBits)pPipelineStateVk->GetRenderPass()->GetDesc().pAttachments[0].SampleCount,
TexFormatToVkFormat(m_pSwapChain->GetDesc().ColorBufferFormat),
TexFormatToVkFormat(m_pSwapChain->GetDesc().DepthBufferFormat),
0.0f, 1.0f, m_pSwapChain->GetDesc().BufferCount, nullptr
};
m_Atmosphere = new SilverLining::Atmosphere(SILVERLINING_LICENSE_USER, SILVERLINING_LICENSE_CODE);
int err = m_Atmosphere->Initialize(SilverLining::Atmosphere::VULKAN, ".\\Resources", true, &info);
if (err != SilverLining::Atmosphere::E_NOERROR)
{
throw std::runtime_error("could not initialize silverlining!!!");
}
This class is the main interface to SilverLining.
Definition: Atmosphere.h:84

Under VulkanSceneGraph, this initialization might look like this instead:

auto vsgDevice = window->getOrCreateDevice();
VkDevice device = *vsgDevice;
VkPhysicalDevice physicalDevice = *(vsgDevice->getPhysicalDevice());
VkInstance instance = *(vsgDevice->getInstance());
uint32_t graphicsQueueFamilyIndex = 0;
std::tie(graphicsQueueFamilyIndex, std::ignore) = vsgDevice->getPhysicalDevice()->getQueueFamily(window->traits()->queueFlags, window->getSurface());
auto queue = vsgDevice->getQueue(graphicsQueueFamilyIndex);
VkQueue graphicsQueue = *queue;
VkRenderPass renderPass = *(window->getOrCreateRenderPass());
VkSampleCountFlagBits sampleCount = (VkSampleCountFlagBits)(window->traits()->samples);
VkFormat colorFormat = window->traits()->swapchainPreferences.surfaceFormat.format;
VkFormat depthFormat = window->traits()->depthFormat;
SilverLining::Vulkan::VulkanInitInfo info{ instance, physicalDevice, device
, graphicsQueue, graphicsQueueFamilyIndex
, renderPass, sampleCount, colorFormat, depthFormat
, 1.0f, 0.0f, window->traits()->swapchainPreferences.imageCount, nullptr};
// note, reversed because vsg uses rev z by default
int err = _atmosphere->Initialize(SilverLining::Atmosphere::VULKAN, ".\\Resources", true, &info);

Let's flesh out SetupAtmosphericConditions() from above. Once you have an Atmosphere object initialized, you can access its AtmosphericConditions object to do things like add cloud decks, change the time of day, and simulate wind. To get up and running fast, you can set up cloud conditions with a single line of code like this:

atm->GetConditions()->SetPresetConditions(AtmosphericConditions::MOSTLY_CLOUDY, *atm);

But, you might want finer control of the cloud layers. Let's write a code snippet to add a cirrus deck, a cumulus congestus deck, and make it a windy day. The code is self-explanatory:

static void SetupCirrusClouds()
{
CloudLayer *cirrusCloudLayer;
cirrusCloudLayer = CloudLayerFactory::Create(CIRRUS_FIBRATUS, *atm);
cirrusCloudLayer->SetBaseAltitude(8000);
cirrusCloudLayer->SetThickness(500);
cirrusCloudLayer->SetBaseLength(100000);
cirrusCloudLayer->SetBaseWidth(100000);
cirrusCloudLayer->SetLayerPosition(0, 0);
cirrusCloudLayer->SeedClouds(*atm);
atm->GetConditions()->AddCloudLayer(cirrusCloudLayer);
}
@ CIRRUS_FIBRATUS
Definition: CloudTypes.h:23

The above routine will create a Cirrus cloud (the high, wispy ones) at an altitude of 8,000 meters and 100 km across. It's centered at the camera position.

Anytime you create a new CloudLayer, you must first

  • Instantiate it using the CloudLayerFactory class factory.
  • Set its size, coverage, and position parameters
  • Call CloudLayer::SeedClouds() to populate the layer with clouds
  • Add the layer to your atmosphere via AtmosphericConditions::AddCloudLayer()

Similarly, let's set up a cumulus congestus deck:

// Add a cumulus congestus deck with 40% sky coverage, which stays centered around the camera position.
static void SetupCumulusCongestusClouds()
{
CloudLayer *cumulusCongestusLayer;
cumulusCongestusLayer = CloudLayerFactory::Create(CUMULUS_CONGESTUS, *atm);
cumulusCongestusLayer->SetIsInfinite(true);
cumulusCongestusLayer->SetBaseAltitude(1500);
cumulusCongestusLayer->SetThickness(100);
cumulusCongestusLayer->SetBaseLength(30000);
cumulusCongestusLayer->SetBaseWidth(30000);
cumulusCongestusLayer->SetDensity(0.4);
cumulusCongestusLayer->SetLayerPosition(0, 0);
cumulusCongestusLayer->SetFadeTowardEdges(true);
cumulusCongestusLayer->SetAlpha(0.8);
// Enable convection effects, but not growth:
cumulusCongestusLayer->SetCloudAnimationEffects(0.1, false, 0);
cumulusCongestusLayer->SeedClouds(*atm);
atm->GetConditions()->AddCloudLayer(cumulusCongestusLayer);
}
@ CUMULUS_CONGESTUS
Definition: CloudTypes.h:26

Our SetupAtmosphericConditions function will set up the simulated wind, call the above two functions to create the cirrus and cumulus cloud decks, and finally set the simulated visibility - which will affect the fog effects on the clouds themselves:

// Configure SilverLining for the desired wind, clouds, and visibility.
static void SetupAtmosphericConditions()
{
// Set up wind blowing south at 50 meters/sec
WindVolume wv;
wv.SetDirection(180);
wv.SetMinAltitude(0);
wv.SetMaxAltitude(10000);
wv.SetWindSpeed(50);
atm->GetConditions()->SetWind(wv);
// Set up the desired cloud types.
SetupCirrusClouds();
SetupCumulusCongestusClouds();
// Set visibility in meters
atm->GetConditions()->SetVisibility(100000);
}

If you wish to simulate a particular place and time, you should also set that up in your initialization. You can also change this at any time while the application is running. Be sure that the time zone you specify in the LocalTime object is consistent with the longitude you specify in the Location object, or else you'll be very confused by the results! (It's also OK to specify all times as in the GMT time zone if you want to use UTC time consistently instead of local times.)

// Sets the simulated location and local time.
// Note, it's important that your longitude in the Location agrees with
// the time zone in the LocalTime.
static void SetTimeAndLocation()
{
Location loc;
loc.SetLatitude(45);
loc.SetLongitude(-122);
LocalTime tm;
tm.SetYear(1971);
tm.SetMonth(8);
tm.SetDay(5);
tm.SetHour(14);
tm.SetMinutes(0);
tm.SetSeconds(0);
tm.SetObservingDaylightSavingsTime(true);
tm.SetTimeZone(PST);
atm->GetConditions()->SetTime(tm);
atm->GetConditions()->SetLocation(loc);
}

Infinite Cloud Layers

Cumulus congestus, cumulus mediocris, stratus, cirrus, stratocumulus, and cirrocumulus cloud layers may be modified by using the CloudLayer::SetIsInfinite() method.

Infinite cloud layers will stay centered at the camera location; as the camera moves, clouds that leave the bounding area defined by the cloud layer's length and width will be repositioned to pop in where the camera's moving toward. Similarly, if wind blows clouds outside of the cloud layer, they will wrap around to the other side of the layer.

Infinite cloud layers allow you to not worry about positioning the cloud layer, or setting up multiple cloud layers to cover large areas.

This gives you the effect of an "infinite" cloud layer, where the clouds will never blow away and you can't move the camera away from them. The larger the length and width you create the cloud layer with, the less noticable the popping will be as clouds are repositioned - especially if the clouds are being fogged in the distance by setting AtmosphericConditions::SetVisibility() to a value similar to the cloud layer's dimensions. You can also use CloudLayer::SetFadeTowardEdges() to fade the clouds out before they reach the edge of the layer, ensuring popping is never visible.

CloudLayer::SetIsInfinite() should be called when initializing the cloud layer; by default, cloud layers are not infinite. Cumulonimbus cloud layers are not affected by SetIsInfinite(), as they only contain a single cloud.

Cloud Animation Effects

Cumulus cloud layers support optional animation effects, to simulate convection and growth of the clouds at runtime. Use the CloudLayer::SetCloudAnimationEffects() method to control these effects.

The first parameter controls whether individual cloud puffs will rotate at a random rate, giving the clouds a convection effect. The value is the maximum rotation in radians per second. There is no performance cost to this effect.

The second parameter controls a real dynamic cloud growth simulation, powered by cellular automata. This does incur a performance hit on the CPU, but in most cases it is negligable. If you do enable growth effects, the third parameter lets you control how "evolved" the cloud is at startup time. Leave it set to zero to start off with fully grown clouds that change their shape gradually, or to a smaller value such as one to watch the clouds form in real-time. The fourth parameter controls the number of seconds between iteration of the cellular automata. Set this to longer values for slower growth effects, or pass 0 to use the default values.

When used together with time-lapse effects (by using a custom MillisecondTimer class,) dramatic effects are possible with clouds shifting and growing over time.

Integrating SilverLining with your Rendering Loop

That's pretty much it for initialization. Now, how do you integrate SilverLining with the rendering of each frame? It's quite simple. When you render a frame of animation, you'll want to follow these steps:

  • Set the modelview and projection matrices to represent your camera position and scene.
  • If using Vulkan, call Atmosphere::GetDefaultTcsData::SetStream() with a pointer to your VkCommandBuffer and current frame buffer index
  • Call Atmosphere::DrawSky() to draw the sky and compute lighting information.
  • Set up your scene's lighting by using the information returned from Atmosphere::GetSunOrMoonPosition(), Atmosphere::GetSunOrMoonColor(), and Atmosphere::GetAmbientColor().
  • Set up your scene's fog by using the information returned from Atmosphere::GetFogEnabled() and Atmosphere::GetFogSettings().
  • Draw your scene's objects.
  • Call Atmosphere::DrawObjects() to draw the clouds.

Here's an example of the render loop under OpenGL:

void Display()
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, aspectRatio, 2, 100000);
// Increment the yaw each frame to spin the camera around
yaw += 0.05;
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glRotatef(-10, 1, 0, 0);
glRotatef(yaw, 0, 1, 0);
glTranslatef(0, -100, 0);
// Pass in the view and projection matrices to SilverLining.
double mv[16], proj[16];
glGetDoublev(GL_MODELVIEW_MATRIX, mv);
glGetDoublev(GL_PROJECTION_MATRIX, proj);
atm->SetCameraMatrix(mv);
atm->SetProjectionMatrix(proj);
// After setting up your projection and modelview matrices to reflect the current
// camera position, call Atmosphere::DrawSky() to draw the sky and do the lighting
// pass on the clouds, if necessary.
atm->DrawSky(true);
// Now, do all your own drawing...
SetSceneLighting();
SetSceneFog();
DrawGroundPlane();
// When you're done, call Atmosphere::DrawObjects() to draw all the clouds from back to front.
atm->DrawObjects();
// Now swap the back and front buffers.
glutSwapBuffers();
glutPostRedisplay();
}

And a main rendering loop under DirectX 9... note that this example uses a right-handed coordinate system. The handedness must match what you specified when initializing the Atmosphere object. This example doesn't handle lost devices; see the Atmosphere::D3D9DeviceLost() and Atmosphere::D3D9DeviceReset() methods for proper handling of that.

DirectX11 would be similar, but instead of setting the view and projection matrices into the device's fixed function pipeline as you would in DirectX9, you'll need to pass those to your vertex programs directly. See the DirectX 11 sample code provided with the SDK for a complete example.

static void RenderFrame(HWND hWnd)
{
static float lastTime = (float)timeGetTime();
if (atm && device)
{
D3DXMATRIX Rot, Yaw, Pitch;
D3DXMatrixRotationX(&Pitch, -10.0f * (3.14f / 360.0f));
D3DXMatrixRotationY(&Yaw, yaw);
D3DXMatrixMultiply(&Rot, &Yaw, &Pitch);
D3DXMATRIX Pos;
D3DXMatrixTranslation(&Pos, 0, -100, 0);
D3DXMATRIX view;
D3DXMatrixMultiply(&view, &Pos, &Rot);
device->SetTransform(D3DTS_VIEW, &view);
//
// Set projection matrix.
//
D3DVIEWPORT9 vp;
device->GetViewport(&vp);
D3DXMATRIX proj;
D3DXMatrixPerspectiveFovRH(
&proj,
45.0 * (D3DX_PI / 180.0),
(float)vp.Width / (float)vp.Height,
2.0f,
200000.0f);
device->SetTransform(D3DTS_PROJECTION, &proj);
// Set view and proj matrices with SilverLining
if (atm)
{
double pView[16], pProj[16];
int i = 0;
for (int row = 0; row < 4; row++)
{
for (int col = 0; col < 4; col++)
{
pView[i] = view(row, col);
pProj[i] = proj(row, col);
i++;
}
}
atm->SetCameraMatrix(pView);
atm->SetProjectionMatrix(pProj);
}
device->BeginScene();
// Call DrawSky after scene has begun and modelview / projection matrices
// properly set for the camera position. This will draw the sky if you pass true.
atm->DrawSky(true);
// Now, do all your own drawing...
SetSceneLighting();
SetSceneFog();
DrawGroundPlane();
// Call DrawObjects to draw all the clouds from back to front.
atm->DrawObjects();
device->EndScene();
device->Present(0, 0, 0, 0);
// Trigger another redraw.
InvalidateRect(hWnd, NULL, FALSE);
lastTime = currTime;
}
}

For Vulkan-based applications, the details of integration will depend on the particulars of the engine or framework you are using. We refer you to the source of the SilverLiningVulkanExample included in the samples of the SDK. The heart of it is all within the SilverLiningVulkanExample.cpp file; each frame, render() is called, which in turn calls rasterizeDynamicRendering() within the SilverLiningVulkanExamplePerWindowData class, which finally calls setUpSlViewProjection() and drawSkyAndClouds(), where the sky and clouds are actually rendered.

This Vulkan sample illustrates all features of SilverLining, including generation of environment maps and shadow maps, multi-threaded rendering, and rendering multiple views at once. Refer to the readme file within the sample folder for more details on how to enable these features, and how the code of the sample is structured.

The vast majority of the code in the SilverLiningVulkanExample is all the support code the surrounds Vulkan itself, as is the case with any Vulkan application. For a much simpler example that relies on a third-party framework for Vulkan, the VulkanSceneGraphExample may be easier to wrap your head around. If you have a Vulkan command buffer that is just handed to you at an appropriate time for drawing the sky and clouds, this sample will be much closer to what you need.

The main difference in Vulkan is that the clouds and sky are not drawn immediately, but recorded into a command buffer. You must set that command buffer and your frame buffer index via the SetStream() method on the tcsData object you are using. Some care in ensuring the proper viewport and scissoring area is in place may also be required. For example, here is how drawing is handled from within our VulkanSceneGraph sample:

void SilverLiningCommand::record(vsg::CommandBuffer& cb) const
{
VkCommandBuffer commandBuffer = cb.vk();
_atmosphere->GetDefaultTcsData()->SetStream(commandBuffer, (int)_parent->imageIndex()));
int viewportsl[4];
_atmosphere->GetDefaultTcsData()->GetCamera()->GetViewport(viewportsl);
int width = viewportsl[2];
int height = viewportsl[3];
VkViewport viewport{};
viewport.width = (float)width;
if (false) {
// https://www.saschawillems.de/blog/2019/03/29/flipping-the-vulkan-viewport/
viewport.height = (float) - height;
viewport.y = (float)height;
} else {
viewport.height = (float)height;
}
viewport.minDepth = 0;
viewport.maxDepth = 1;
VkRect2D scissor{};
scissor.offset.x = 0;
scissor.offset.y = 0;
scissor.extent.width = viewportsl[2];
scissor.extent.height = viewportsl[3];
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
_atmosphere->DrawSky(true, false, 0, false, false, false, 0, 0, 0, 0);
const bool drawClouds = true;
const bool drawPrecipitation = false;
const bool enableDepthTest = true;
const bool enableDepthWrites = false;
SilverLining::CameraHandle cameraHandle = 0;
const bool backFaceCullClockWise = true;
const bool drawBackdrops = false;
const bool drawLightning = false;
const bool geocentricMode = false;
_atmosphere->DrawObjects(drawClouds, drawPrecipitation, enableDepthTest, 0, enableDepthWrites, cameraHandle, backFaceCullClockWise, drawBackdrops, drawLightning, geocentricMode);
}

As a reminder, Vulkan also requires a VulkanInitInfo structure passed into Atmosphere::Initialize() via its "environment" parameter. This tells SilverLining about the other Vulkan resources from your application it needs.

Lighting Your Scene with SilverLining

In order to light the objects in your scene consistently with the appearance of the sky, SilverLining allows you to query for its modeled directional and ambient light information. It's easy to use this information to light your scene. Here's an example of setting up lighting under OpenGL using SilverLining as guidance:

void SetSceneLighting()
{
float x, y, z, r, g, b, ra, ga, ba;
atm->GetSunOrMoonPosition(&x, &y, &z);
atm->GetSunOrMoonColor(&r, &g, &b);
atm->GetAmbientColor(&ra, &ga, &ba);
GLfloat light_ambient[] = {ra, ga, ba, 1.0};
GLfloat light_diffuse[] = {r, g, b, 1.0};
GLfloat light_specular[] = {0.0, 0.0, 0.0, 1.0};
GLfloat light_position[] = {x, y, z, 0};
glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glEnable(GL_LIGHT0);
GLfloat mat_amb_diff[] = {1.0, 1.0, 1.0, 1.0};
GLfloat no_mat[] = {0, 0, 0, 0};
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_amb_diff);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_amb_diff);
glMaterialfv(GL_FRONT, GL_SPECULAR, no_mat);
glMaterialfv(GL_FRONT, GL_EMISSION, no_mat);
}

And the same function for setting the scene's lighting, under DirectX9:

static void SetSceneLighting()
{
D3DXCOLOR light_ambient, light_diffuse, light_specular;
D3DXVECTOR3 light_position;
atm->GetSunOrMoonPosition(&light_position.x, &light_position.y, &light_position.z);
atm->GetSunOrMoonColor(&light_diffuse.r, &light_diffuse.g, &light_diffuse.b);
atm->GetAmbientColor(&light_ambient.r, &light_ambient.g, &light_ambient.b);
light_diffuse.a = light_ambient.a = 1.0;
D3DLIGHT9 light;
::ZeroMemory(&light, sizeof(light));
light.Type = D3DLIGHT_DIRECTIONAL;
light.Ambient = light_ambient;
light.Diffuse = light_diffuse;
light.Specular = D3DXCOLOR(1, 1, 1, 1);
light.Direction = D3DXVECTOR3(-light_position.x, -light_position.y, -light_position.z);
if (device)
{
device->SetLight(0, &light);
device->LightEnable(0, true);
device->SetRenderState(D3DRS_NORMALIZENORMALS, true);
device->SetRenderState(D3DRS_SPECULARENABLE, false);
}
}

DirectX 11 would be similar, but you'd pass the lighting values and position into your shaders instead of to the device.

Vulkan and OpenGL 3+ provides no high-level lighting mechanism, so you would feed the lighting values returned from the Atmosphere object into your own shaders to power their lighting calculations.

Fog Effects with SilverLining

Finally, SilverLining will also give you guidance on how to configure fog for your scene. If you're inside a stratus cloud deck or inside precipitation, SilverLining will request that you set the fog in your scene appropriately to simulate being inside a thick cloud or inside rain or snow. If Atmosphere::GetFogEnabled() returns true, then SilverLining is asking that you query Atmosphere::GetFogSettings() to obtain information about the fog volume you're currently inside.

Even if you're not inside or under a cloud, SilverLining can help you set your fog to blend your distant terrain into the sky. SilverLining constantly computes the average color of the sky at the horizon within the current field of view, and makes this accessible via Atmosphere::GetHorizonColor(). Setting the fog color to this, and setting the density consistently with the visibility you passed earlier to Atmosphere::SetVisibility(), will yield realistic results for scenes with light haze.

It is possible to simulate a thick layer of colored haze hugging the ground. This can be useful if Atmosphere::GetHorizonColor() does not produce desirable results for your application. Effectively, this allows you to blend the skybox to a specified color as it approaches the horizon. If you fog your terrain with this same color, you can obscure the horizon line quite nicely for applications that do not render terrain all the way out to the horizon.

The haze layer is set via Atmosphere::SetHaze(). You may simulate any depth of haze, any color, and any density. Set the depth to 0 to disable haze. It's important to realize that no lighting is performed on the haze; if you want the sky to blend toward a darker color at night, you must pre-multiply the haze color by the light in the scene.

AtmosphericConditions::SetVisibility() may be used to apply atmospheric perspective effects to the clouds in the scene - it causes the clouds to blend into the sky with distance. It does not fog the sky itself - for thicker fog, use Atmosphere::SetHaze() for volumetric-style fog, or AtmosphericConditions::SetFog() for exponential fog. Think of SetVisibility() of just simulating particulate matter in the atmosphere that affects atmospheric perspective; it only makes sense with relatively high visibilities. If you're really simulating being in fog, use SetFog() instead.

Here's an example of setting fog under OpenGL, using the GetHorizonColor() method instead of a haze layer of a specified color. Note that this code will not result in the sky being fogged; it only provides guidance to your application for fogging the objects in your scene toward a color that blends with the horizon. For thicker fog that does obscure the sky, you'll want to use Atmosphere::SetHaze(), or AtmosphericConditions::SetFog() with a backbuffer cleared to your fog color, in addition to this code.

void SetSceneFog()
{
glEnable(GL_FOG);
glFogi(GL_FOG_MODE, GL_EXP);
float hazeDensity = 1.0 / kVisibility;
// Decrease fog density with altitude, to avoid fog effects through the vacuum of space.
static const double H = 8435.0; // Pressure scale height of Earth's atmosphere
double isothermalEffect = exp(-(atm->GetConditions()->GetLocation().GetAltitude() / H));
if (isothermalEffect <= 0) isothermalEffect = 1E-9;
if (isothermalEffect > 1.0) isothermalEffect = 1.0;
hazeDensity *= isothermalEffect;
bool silverLiningHandledTheFog = false;
if (atm->GetFogEnabled())
{
float density, r, g, b;
// Note, the fog color returned is already lit
atm->GetFogSettings(&density, &r, &g, &b);
if (density > hazeDensity)
{
glFogf(GL_FOG_DENSITY, density);
GLfloat fogColor[4] = {r, g, b, 1.0};
glFogfv(GL_FOG_COLOR, fogColor);
silverLiningHandledTheFog = true;
}
}
if (!silverLiningHandledTheFog)
{
GLfloat fogColor[4];
atm->GetHorizonColor(0, &fogColor[0], &fogColor[1], &fogColor[2]);
glFogfv(GL_FOG_COLOR, fogColor);
glFogf(GL_FOG_DENSITY, hazeDensity);
}
}

And here's the equivalent DirectX9 code for setting fog:

static void SetSceneFog()
{
DWORD fogColor;
float density, r, g, b;
// If you're inside a cloud, SilverLining will request that you set the fog accordingly.
if (atm->GetFogEnabled())
{
atm->GetFogSettings(&density, &r, &g, &b); // This fog color is pre-lit
}
else // Otherwise, setting the fog to the average color of the sky at the horizon works well.
{
atm->GetHorizonColor(0, &r, &g, &b);
density = 1.0f / kVisibility;
// Decrease fog density with altitude, to avoid fog effects through the vacuum of space.
static const double H = 8435.0; // Pressure scale height of Earth's atmosphere
double isothermalEffect = exp(-(atm->GetConditions()->GetLocation().GetAltitude() / H));
if (isothermalEffect <= 0) isothermalEffect = 1E-9;
if (isothermalEffect > 1.0) isothermalEffect = 1.0;
density *= isothermalEffect;
}
BYTE cr, cg, cb, ca;
cr = (BYTE)(r * 255.0);
cg = (BYTE)(g * 255.0);
cb = (BYTE)(b * 255.0);
ca = 255;
fogColor = D3DCOLOR_RGBA(cr, cg, cb, ca);
// Enable fog blending.
device->SetRenderState(D3DRS_FOGENABLE, TRUE);
// Set the fog color.
device->SetRenderState(D3DRS_FOGCOLOR, fogColor);
// Set fog parameters.
device->SetRenderState(D3DRS_FOGTABLEMODE, D3DFOG_EXP);
device->SetRenderState(D3DRS_FOGDENSITY, *(DWORD *)(&density));
}

Again, DirectX11, Vulkan, and OpenGL 3+ is similar, but you would need to pass in the fog parameters to your shaders instead of to the device.

That's all there is to it! Using the techniques above, you'll be able to integrate SilverLining's sky and cloud rendering into your 3D application, and light and fog the objects in your scene consistently with the sky and clouds rendered by SilverLining.

If your application specifies its own fog - for example, you're simulating being inside a fog bank - you can tell SilverLining to fog its clouds to a specified color and density. See the AtmosphericConditions::SetFog() method for more information. When you're in thick fog, you'll usually want to just clear your back buffer to the fog color and not call SilverLining at all. The sky won't be visible in such conditions anyhow. Remember to call ClearFog() when you leave the simulated fog bank, as calling SetFog() will override all other atmospheric perspective effects in SilverLining.

Drawing Your Skybox Last Instead of First

Our instructions above describe calling Atmosphere::DrawSky() as the first thing in your frame in order to keep things simple and as compatible as possible with other engines. As you're getting your SilverLining integration up and running, it makes sense to start with this approach. However, in some situations, drawing the sky box last instead of first will actually lead to performance benefits.

The issue is that if you draw your sky box first, a lot of the box ends up getting overdrawn by the objects in your scene, which is a waste of fill rate. By drawing the sky box last, with depth reads enabled, only parts of the scene with a still-cleared depth buffer will be filled with the sky.

To do this, you must clear the depth buffer yourself at the beginning of each frame (but don't clear the color buffer.) Draw the non-translucent objects in your scene with depth writes on. Then, call Atmosphere::DrawSky() with the clearDepth parameter set to false - this will tell SilverLining that you don't want DrawSky() to clear the depth buffer before drawing the sky box. Finally, call Atmosphere::DrawObjects() to draw the clouds and precipitation, and the translucent objects in your scene.

Some engines don't use the depth buffer at all, and use depth textures instead - this technique will not work in such situations. However, for engines that do rely on the depth buffer, this technique can lead to slight gains in performance if your application is fill-rate bound.

Integration with Multi-Threaded Renderers

People sometimes mean different things by "multi-threaded," but SilverLining supports a couple of different approaches.

Some engines may perform updates, culling, and drawing in separate passes or threads. SilverLining allows you to perform these operations separately for better performance in such multi-threaded engines (such as OpenSceneGraph.)

By default, Atmosphere::DrawSky() will perform updates, culling, and drawing of the sky. However, you may call Atmosphere::UpdateSkyAndClouds() prior to Atmosphere::DrawSky() to update the sky's ephemeris model and the shapes and positions of the clouds at a more opportune time. If Atmosphere::DrawSky() is called without a call to Atmosphere::UpdateSkyAndClouds() before the prior call to Atmosphere::DrawSky(), DrawSky() will call UpdateSkyAndClouds() on its own prior to rendering.

Similarly, Atmosphere::CullObjects() may be used to cull the clouds outside of the call to Atmosphere::DrawObjects(). If CullObjects() is not called prior to DrawSky(), DrawSky() will call CullObjects() itself.

Once Atmosphere::UpdateSkyAndClouds() has bene called, you may query the bounding volume of the clouds that are in the scene via Atmosphere::GetCloudBounds().

Thread-safety is in place to prevent updates, culling, and drawing from happening simultaneously within SilverLining.

You may however be after true multi-threaded rendering, where command buffers are built up for different views in parallel. For the OPENGL32CORE renderer, we support this as well, and under VULKAN that is the only way to go.

Please refer to the SilverLining::ThreadCameraStreamData class for details on how this works. It's basically a matter of instantiating some local storage for each view, and passing that as an optional parameter to SilverLining's methods. This allows SilverLining to manage rendering for each view independently in a thread-safe manner. There is also a OpenGL Multi-Threaded Sample application included in the samples directory of the SDK illustrating the use of the ThreadCameraStream system for concurrent rendering of three views at once.

Note that if you are rendering multiple views from OpenGL contexts that cannot share resources, you'll want to set the configuration setting textures-are-shared to "no".

Virtual Reality Integration Tips

The biggest consideration with VR headsets is performance. Drawing thousands of complex, translucent clouds is hard enough when you are only drawing a single view, let alone one for each eye. Be sure to refer to the appropriate sample code within the SDK for a starting point on how to draw multiple views in the most efficient manner. With OpenSceneGraph, the OpenSceneGraph multi-window sample within the SDK's sample code folder is the place to start. If you are developing direclty on top of OpenGL, the OpenGL multi-threaded sample is a good reference. And for Vulkan, the Vulkan example can draw multiple views simultaneously, and it illustrated maintaining the required per-view data in its architecture. In every case, using SilverLining::ThreadCameraStreamData is the key to the best performance; see Integration with Multi-Threaded Renderers for more information.

Care must also be taken to associate the correct view and projection matrices as each eye is drawn in SilverLining.

Another challenge in VR is dealing with cloud types that are represented by collections of 2D billboards, such as CUMULUS_CONGESTUS. Billboards and VR don't get along, as each eye has a slightly different view vector for the billboards to align with. This can yield blurriness when you get too close to a billboard, such as when you are flying through the clouds. This can be mitigated by the cumulus-billboards-per-cloud setting in the Resources/SilverLining.config file. Try setting that to "yes", which will compute billboard matrices per cloud instead of per billboard. This helps a lot.

The only real solution is to not use billboards in the first place; try using the STRATOCUMULUS cloud layer type as a replacement for CUMULUS_CONGESTUS. As it is a volumetric, GPU ray-casted cloud, it is immune to the problems of 2D billboards in VR. Many customers prefer the visual detail billboards offer, however.

Another problem that isn't specific to VR is changes to the sort order of clouds as the camera moves. This can result in clouds suddenly popping in front of others, when clouds intersect each other. In VR, this is more distracting. Experiment with the sort-by-screen-depth config setting for this; setting it to "no" may help in some applications.

Memory Management with SilverLining

By default, SilverLining will allocate memory using new, delete, malloc, and free. However, you may redirect SilverLining's memory management into your own memory manager if you wish.

The SilverLining::Allocator class is defined in the public header MemAlloc.h. If you extend this class, you may implement your own Allocator::alloc() and Allocator::dealloc() methods to manage memory however you wish. Pass an instantiation of your Allocator-derived class into Allocator::SetAllocator() prior to creating any SilverLining objects, and the SilverLining library and rendering DLL's will use your own allocation scheme.

The Allocator will capture all calls to new, delete, malloc, and free within SilverLining. Every object in SilverLining derives from a SilverLining::MemObject class that overloads the new and delete operators, rather than overloading new and delete globally. Our STL objects also use a custom allocator that routes through SilverLining::Allocator. The macros SL_VECTOR, SL_MAP, SL_LIST, and SL_STRING are used as a convenience for STL objects using our allocator.

Resource Management with SilverLining

By default, SilverLining will load its texture, data, and shader resources directly from disk relative to the path to the resources directory you specify in Atmosphere::Initialize(). However, all disk access in SilverLining is abstracted by a ResourceLoader class, so you have the ability to hook in any resource management scheme you wish.

See the documentation for ResourceLoader for more details. For example, if you wanted to include all of SilverLining's data, textures, and shaders within your own pack files, you could extend the ResourceLoader class and hook it into your own resource manager. Then, pass a pointer to your derived ResourceLoader into Atmosphere::SetResourceLoader() prior to calling Atmosphere::Initialize(), and all disk access will be routed through your own resource management.

It's important to note that the renderer DLL's for SilverLining also live inside our resource folder, and Windows must load these DLL's directly from disk. So, although you can move all of the other resources in SilverLining to a virtual file system, you do need to retain a resources folder containing the renderer DLL's at least. If you want to avoid this, you can rebuild the SilverLining libraries to statically link in the renderer you want, and eliminate the DLL dependencies altogether. Licensed customers will find build targets in the SilverLining project file to statically link the OpenGL or various DirectX renderers directly into the SilverLining library.

Stratocumulus Cloud Layers

Take care when using STRATOCUMULUS cloud layers in your scene - they are rendered using GPU ray-casting and depend on a sophisticated fragment program. Unlike other cloud layers that will run on pretty much any hardware, stratocumulus clouds do require a system that support Shader Model 3.0, and a newer GPU that has a fast fragment processor. If you're running a simulation on known, modern graphics hardware, stratocumulus cloud layers will reward you with per-fragment lighting and extremely dense clouds with constant rendering time. However, for consumer applications where the system requirements are less controlled, you may want to stick with cumulus congestus clouds instead.

There is also a new STRATOCUMULUS_PARTICLES cloud type that may be a better choice for you. Like stratocumulus, it represents very dense, low cloud layers - but it uses billboard particles instead of GPU ray-casting. It can be a very efficient choice for large, dense cloud layers.

Simulating Sand Storms

SilverLining includes a SANDSTORM cloud layer type, used to simulate sand storms, dust storms, or "haboobs".

Sandstorms should have a base altitude equal to your local ground level. They use "soft particles" to avoid artifacts where the cloud puffs intersect the ground at the altitude you specify.

It is not necessary to specify a thickness or density with sandstorms. Here is an example of setting up a 50km x 50km sandstorm positioned 25km from the origin, with a ground altitude at sea level:

// Sandstorms should be positioned at ground level. There is no need to set their
// density or thickness.
static void SetupSandstorm()
{
CloudLayer *sandstormLayer;
sandstormLayer = CloudLayerFactory::Create(SANDSTORM, *atm);
sandstormLayer->SetIsInfinite(false);
sandstormLayer->SetLayerPosition(0, -25000);
sandstormLayer->SetBaseAltitude(0);
sandstormLayer->SetBaseLength(50000);
sandstormLayer->SetBaseWidth(50000);
sandstormLayer->SeedClouds(*atm);
atm->GetConditions()->AddCloudLayer(sandstormLayer);
}
@ SANDSTORM
Definition: CloudTypes.h:32

Integration Tips with Popular Scene Graphs

Integrating with OpenSceneGraph

Silverlining should integrate easily into any rendering engine built on top of OpenGL or DirectX, but we get the most questions about integrating with OpenSceneGraph. We've included some sample code for OpenSceneGraph 2.4 and up with the SDK to get you started; it's a modified osgviewer application that integrates the sky, clouds, and lighting. Also included is an example of using SilverLining with multiple windows in OSG.

A more limited example for OSG 1.0 is also provided.

We also include a public domain integration of SilverLining with OSG from osgRecipes (https://github.com/xarray/osgRecipes). In many ways, this integration is better than our own - it's simpler, with a single SilverLiningNode. For non-geocentric scenes, it's an easier way to get started.

Studying or extending this sample code is the easiest way to get started, but if you want to start from scratch, here are the things you need to know about OpenSceneGraph integration:

  • The easiest way to ensure Atmosphere::DrawSky and Atmosphere::DrawObjects are called at the right time is to wrap these calls in osg::Drawable objects. Then, create an osg::Geode for each and add the Drawables to them, and set the Geodes into a renderbin that's used with the rest of your scene. Just set the object that calls DrawSky to have an early position in the renderbin (like -1) and the DrawObjects drawable to a very high position.
  • If you're going to use multithreaded mode, you need to take care that Atmosphere::Initialize is called in the rendering thread. An easy way to do this is keeping a static boolean in the drawable that calls DrawSky, and call the initialization right before DrawSky is called for the first time. That's also the right time to set up your cloud layers.
  • Any call to SilverLining that might result in OpenGL calls needs to happen while a valid GL context is active. This can include seeding cloud layers and removing them, as well as calls that you'd expect to draw the sky and clouds. Make sure these calls are all done within a drawImplementation() method to be safe.
  • You'll probably want to disable how OpenSceneGraph automatically computes the near and far clip planes and set these explicitly, since OSG won't know about the bounds of SilverLining's sky box and its clouds. Alternately, you can use the implementation of ClampProjectionMatrixCallback included with the sample code to try and include the bounds of the sky's objects and modify the size of the sky box as necessary. We recommend simply disabling the automatic computation of the clip planes unless you need to do viewpoints from space where it's necessary to push out the near clip plane in order to preserve depth buffer resolution.
  • Remember when SilverLining draws the sky, it will clear the color buffer and the depth buffer in the process. You can get a little performance back by disabling buffer clearing in OpenSceneGraph, as it's redundant.

Integrating with osgEarth

If you are using osgEarth, getting started is even easier; a sample driver for SilverLining is included with osgEarth, and it can be a quick way to get something up and running quickly. You'll find a sample silverlining.earth file inside osgEarth's tests directory to get you started, and the source of their built-in integration is in the src/osgEarthSilverLining directory. Should you need to extend the capabilities of the built-in driver, the underlying SilverLining::Atmosphere object is available via their SilverLiningContext::getAtmosphere() method - from the Atmosphere interface, you should be able to do anything.

In order to use osgEarth's logarithmic depth buffer, you will need to copy the user shaders provided in src/osgEarthSilverLining/Shaders into SilverLining's resources/shaders directory in your application.

Be sure to rebuild osgEarth after intalling the SilverLining SDK, in order to enable SilverLining support in osgEarth. osgEarth code from June 12, 2023 or later (or release 4.5 or later) is required.

Integrating with VulkanSceneGraph

Refer to the VulkanSceneGraphExample included in the SDK's samples for guidance on integrating SilverLining with VulkanSceneGraph. It is surprisingly simple; we subclass a SilverLiningGroup with a SilverLiningCommand as a child node and just add it to the scene graph. The SilverLiningGroup is responsible for initializing SilverLining and maintaining its location, time, and other environmental conditions. SilverLiningCommand handles the actual drawing of the sky and clouds each frame, using the command buffer passed into it from VSG.

Integrating with Rocky

Rocky is an in-development 3D geospatial engine built under VulkanSceneGraph, from the makers of osgEarth. See https://github.com/pelicanmapping/rocky for more information about Rocky.

We have included a sample Rocky / SilverLining integration in the Rocky Example directory in the SDK's samples. This sample is based on a modified "rdemo" application from Rocky, that just inserts our SilverLiningGroup and SilverLiningCommand nodes at the end of Rocky's main scene. This sample syncs up the position, latitude, longitude, and altitude in Rocky with SilverLining, and also integrates SilverLining's sun, moon, and ambient light with Rocky's lighting on the terrain. This is a good example of integrating SilverLining with a geocentric coordinate system (ECEF / WGS84) and with Vulkan and VSG.

Refer to the README file within the Rocky Example directory for more information and details about how this integration works. Note that Rocky is, as of this writing, not yet at version 1.0 and API changes to Rocky that break this sample are likely to occur.

Integrating with Carmenta Engine

Sample code for integrating SilverLining into a 3D runtime scene powered by the Carmenta geospatial engine is also provided with the SDK.

The main challenge when integrating with Carmenta is that the modelview and projection matrices are not set when the "beforeUpdate" callback is executed. This means the view matrix needs to be constructed from scratch from the 3D view object's properties, and passed to SilverLining. Refer to the sample Carmenta integration code to see how to construct this matrix correctly.

Integrating with the Diligent Engine

Sample code for integrating SilverLining into Diligent's cross-platform graphics system is found under SampleCode/DiligentExample.

A modified "cube" tutorial is provided that illustrates how to obtain the various platform-specific resources SilverLining needs in order to render under DirectX11, Vulkan, or OpenGL. SilverLining does not support DirectX 12 at this time. Other than Vulkan, integration is fairly straightforward. Be sure to read the README.TXT file included in the example for build guidance.

Note that SilverLining may change the state of your rendering pipeline from what Diligent thinks it is. If you are doing any drawing in your scene after SilverLining, it may be necessary to call IDeviceContext::InvalidateState() followed by IDeviceContext::SetRenderContext to let Diligent know that things have changed.

Integrating with NVidia SceniX

We also provide a sample integration with NVidia's SceniX scene engine. You'll find a modified "simple viewer" sample application in the SDK, which uses the SilverLiningManager class included in the sample to wrap your scene's root node with SilverLining's rendering methods, and to keep a directional light object in sync with the simulated conditions.

Examine the SimpleViewerView.cpp file for an example of configuring the atmospheric conditions.

Using SilverLining in Geocentric or ECEF (Earth Centered Earth Fixed) Coordinates

SilverLining has successfully been integrated into many "whole-Earth" applications, using coordinate systems where the origin is at the center of the Earth, with major axes pointing through the North Pole and through latitude, longitude (0,0). Wrapping your head around how to configure SilverLining's basis vectors and how to position cloud layers in this sort of environment is a bit tricky, so here are some tips.

First, you will need to pass true for the geocentric parameter in Atmosphere::DrawSky() each frame.

Prior to seeding your clouds, and once per frame, you must pass SilverLining new Up and Right vectors such that "up" is the normalized vector from the origin (center of the earth) to your location, and "right" points East from your location. The best way to do this is to construct your "up" vector by normalizing the vector from the center of the Earth to your viewpoint. Construct a "north" vector pointing in the direction of your north pole (ie, 0,0,1) and compute the cross product north X up. Normalize the resulting cross product to create your "right" vector pointing east.

If your Earth model is in a system where Y is North instead of Z, you'll need to set the configuration setting geocentric-z-is-up to "no" in the file resources/silverlining.config. Otherwise, the position of the sun, moon, and stars will be incorrect relative to your skies and your Earth.

To position a localized cloud layer, first set the up and right vectors to reflect the layer's desired position on the Earth, and call SetLayerPosition() with coordinates (0,0). For infinite cloud layers, it's not necessary to set the layer position at all. Silverlining will automatically keep track of where sea level is at the current camera location, and adjust the positions of the clouds to ensure their altitudes remain correct.

Here is some sample code that might make it easier to understand, which creates a cloud layer over the camera position in geocentric space:

// cameraPosition is a Vector3 in ECEF coordinates, relative to the
// center of the Earth.
Vector3 up = cameraPosition;
up.Normalize();
Vector3 north(0, 0, 1); // We assume Z is up
Vector3 east = north.Cross(up);
east.Normalize();
atm->SetUpVector(up.x, up.y, up.z);
atm->SetRightVector(east.x, east.y, east.z);
// Note - be sure to also update AtmosphericConditions::SetLocation
// if your position changes, and update SetUpVector and SetRightVector
// as your position changes each frame.
// Creates a cumulus congestus cloud layer at 2500 meters above the camera.
cumulusCongestusLayer = CloudLayerFactory::Create(CUMULUS_CONGESTUS, *atm);
cumulusCongestusLayer->SetBaseAltitude(2500);
cumulusCongestusLayer->SetThickness(100);
cumulusCongestusLayer->SetBaseLength(80000);
cumulusCongestusLayer->SetBaseWidth(80000);
cumulusCongestusLayer->SetDensity(0.4);
cumulusCongestusLayer->SetLayerPosition(0, 0);
cumulusCongestusLayer->SeedClouds(*atm);
atm->GetConditions()->AddCloudLayer(cumulusCongestusLayer);

Some whole-earth environments render all objects relative to the current camera position in order to preserve floating point precision. If this applies to your system, the above technique still works, but you'll want to multiply a translation matrix to negate out the camera position into the view matrix passed into Atmosphere::SetCameraMatrix() every frame for the clouds to appear in the correct positions.

In a geocentric / ECEF system, be sure to use Atmosphere::GetSunPositionGeographic(), Atmosphere::GetMoonPositionGeographic(), and/or Atmosphere::GetSunOrMoonPositionGeographic() to retrieve the direction of the sun or moon light in the correct coordinate system. The usual calls to Atmosphere::GetSunPosition() etc. will return horizon coordinates, which isn't what you want.

If you intend to render the Earth from space, be sure to also look at the geocentric parameter of Atmosphere::DrawSky(). At very high altitudes, drawing the sun, moon, and stars using horizon coordinates will break down; this parameter will cause everything to be rendered in geographic coordinates instead. Also check out the enable-atmosphere-from-space setting in resources/SilverLining.config. With this on, SilverLining will render a ring around the Earth representing the atmosphere as you leave it - it assumes that a unit in your coordinate system represents one meter, and you're rendering the Earth itself with a realistic size. Be sure that your near and far clip planes take this ring into account, or you may end up clipping it out - see the projection matrix callback class included in the OpenSceneGraph sample code for an example of how to account for the atmosphere's geometry when setting the near and far clip planes.

We've included some sample code in the osgEarth example project included with the SDK. It illustrates the techniques described above within the context of an OpenSceneGraph / osgEarth application in geocentric space.

Simulating Precipitation with SilverLining

SilverLining includes particle systems that will simulate any amount of rain, sleet, or snowfall that's likely to occur in nature. It will also pass back fog settings that will let you accurately simulate the reduction in visiblity due to the precipitation. SilverLining takes the precipitation rate you specify, and calculates the distribution of particle sizes, particle velocities, and visiblity using research from the meteorology community based on real-world observations.

Precipitation may be attached to cloud layers, so as you pass underneath a cloud, the precipitation will automatically start and stop.

Although it's sophisticated, it's easy to use. All you need to do is call CloudLayer::SetPrecipitation() on a stratus or cumulus CloudLayer object. You can do this when you're initializing your CloudLayers, or change it any any time thereafter.

CloudLayer::SetPrecipitation only takes two values - the precipitation type (CloudLayer::RAIN , CloudLayer::SLEET, or CloudLayer::SNOW), and the precipitation rate. The rate is specified in millimeters per hour - in the case of snow, this is the liquid equivalent precipitation rate, and not the accumulation rate. Reasonable values for the rate would range from 1.0 to 30.0.

You may simulate mixed precipitation by calling CloudLayer::SetPrecipitation consecutively with different precipitation types. For example, to simulate a 50/50 blend of sleet and snow at an overall rate of 20 mm/hr, you could call SetPrecipitation(CloudLayer::SLEET, 10.0); SetPrecipitation(CloudLayer::WET_SNOW, 10.0);

If you want to turn precipitation off on a CloudLayer, just call CloudLayer::SetPrecipitation() with a precipitation type of CloudLayer::NONE. Doing this will remove all precipitation effects you set previously on this CloudLayer.

If you want precipitation effects to be applied globally, independently of CloudLayers, you may instead use the AtmosphericConditions::SetPrecipitation() method. This allows you to use SilverLining's precipitation effects without using SilverLining's clouds, if you so desire.

To take advantage of the visibility reduction effects, be sure to implement the code described in "Fog Effects with SilverLining" below.

Manipulating Time with SilverLining

By default, SilverLining will simulate the time you specify with AtmosphericConditions::SetTime(). Clouds will move with the wind over time, but the sun, moon, stars, and appearance of the sky won't change until you call SetTime() again. This is adequate for most applications, and it ensures good performance.

Some applications need to control the passage of time. You might need to go backwards in time in order to replay a scene. Or you might want to accelerate time for a time-lapse photography sort of effect. Or, you may simply want to filter the time between frames to ensure smooth animation. SilverLining allows you to replace its own internal millisecond timer with your own, to accomplish such effects.

To do so, implement a MillisecondTimer class of your own, and pass it to AtmosphericConditions::SetMillisecondTimer(). Here's an example of a MillisecondTimer that speeds up the passage of time 10X:

class MyMillisecondTimer : public MillisecondTimer
{
public:
virtual unsigned long GetMilliseconds() const
{
return timeGetTime() * 10;
}
};

To use this timer, pass it into your AtmosphericConditions class, like this:

MyMillisecondTimer *timer = new MyMillisecondTimer();
atm->GetConditions()->SetMillisecondTimer(timer);

You are responsible for deleting your timer class at shutdown. To restore the default timer, call SetMillisecondTimer(NULL).

Your new timer will influence the rate at which clouds move, but by default, that's all that millisecond timers influence. You may also set the sun, moon, stars, and appearance of the sky to change dynamically over time. To do so, call AtmosphericConditions::EnableTimePassage(). For example:

atm->GetConditions()->EnableTimePassage(true, 20000);

The first parameter enables dynamic passage of the simulated time. The second specifies the minimum time interval, in milliseconds (as defined by your own MillisecondTimer if you're using one) between cloud deck relighting passes. Relighting a cloud deck is a relatively expensive operation, and you may not want to relight during an interactive scene. You'll at least want to do so infrequently, as the above example does. If you want the sun, moon, stars, and sky to change smoothly over time but don't want to incur the costs of cloud relighting at all, pass -1 for the second parameter.

One thing to watch out for when accelerating time - if you have cumulonimbus clouds in your scene, this might result in lightning being rendered much more frequently, which can both look strange and affect performance. To counteract this, increase the config setting lightning-max-discharge-period accordingly. Alternately, lightning may be disabled altogether by setting cumulonimbus-lightning-density to 0.

Considerations for Multi-Channel, Multi-Viewport, and Multi-Context Setups

Many training simulator applications require a consistent scene drawn across several systems, or "channels", together. SilverLining provides two approaches to ensuring the clouds are consistent across multiple computers.

The simplest solution is to seed SilverLining's random number generator, using Atmosphere::GetRandomNumberGenerator() and RandomNumberGenerator::Seed() (for example, atm->GetRandomNumberGenerator()->Seed(0)) Cloud layers generated in the same order with the same parameters will then be consistent.

Alternately, you may use the CloudLayer::Save() method to pre-generate cloud layers for your scene, and save them to disk. Then, you can distribute these saved cloud layers to each channel, and load them at runtime using CloudLayer::Restore(). This approach also has the benefit of speeding up your application's initialization a bit.

Some applications also require drawing multiple scenes on a single channel, across multiple windows or graphics contexts. Other applications render multiple viewports within a single context. Using SilverLining across multiple contexts or viewports in the same application is supported, but requires some care.

If you are drawing across multiple windows that may span different graphics contexts, one approach is to associate a SilverLining::Atmosphere instance with each viewport. This means that for each viewport, you would instantiate and initialize a separate Atmosphere object, and use the correct Atmosphere for the context you are currently rendering to. The Atmosphere must be initialized while its associated graphics context is currently active, and any cloud layers added to the scene must also be added to each Atmosphere while the associated context is active.

Internally, SilverLining maintains a concept of a "current atmosphere" that is updated whenever Atmosphere::Initialize() or Atmosphere::DrawSky() is called. Any operations to the Atmosphere, such as adding or removing cloud layers, must happen following one of these calls.

So, to recap: if you're rendering across multiple graphics contexts, make sure you associate an Atmosphere object for each viewport. You should only add or remove cloud layers following a call to Atmosphere::Initialize() or Atmosphere::DrawObjects() in this situation, and the Atmosphere and cloud layers must be initialized and drawn while the correct window or graphics context is currently active. Generally, the easiest way to accomplish this is by initializing and modifying the Atmosphere object within your application's drawing method.

The other case is drawing across multiple windows or viewports that share a graphics context, for example using WGL's "context sharing" functionality. In this case, it is safe to re-use the same Atmosphere object across each viewport, taking care to set the new camera and projection matrix prior to drawing the sky and clouds on each viewport. In order to ensure precipitation effects work properly across the different views, make sure to specify the "camera" parameter on the Atmosphere::DrawSky() and Atmosphere::DrawObjects() methods.

Another approach for multiple channels is to use our multi-threaded rendering feature; if you are careful to maintain separate ThreadCameraStreamData objects for each view, it is possible to render multiple views using the same Atmosphere and CloudLayer instances. If you set the config option textures-are-shared to "no" it can even work across contexts that do not share resources. See Integration with Multi-Threaded Renderers for more information.

Managing Your Own Drawing

For many applications, allowing SilverLining to draw its own clouds is a simple approach that will get you up and running quickly with good results. Other applications may prefer to manage their own cloud drawing, in order to ensure that clouds are sorted properly with respect to other translucent objects in the scene. SilverLining provides you with access to the underlying translucent objects it normally renders in Atmosphere::DrawObjects() for this purpose.

If you call Atmosphere::DrawObjects(false), this tells SilverLining that it should build up a list of translucent objects to draw within the DrawObjects call, but not to actually draw them. After calling DrawObjects(false), you may then obtain a list of translucent objects with the Atmosphere::GetObjects() call.

Translucent objects are generally rendered last in a scene, in back-to-front order with respect to the current viewpoint. Use the Atmosphere::GetObjectDistance() method to obtain the distance from a given viewpoint to each object for sorting purposes. Once your translucent objects are sorted and you're ready to draw them, you may draw a SilverLining ObjectHandle using the Atmosphere::DrawObject() method. Note, you must have blending enabled prior to calling DrawObject().

Here's an example of manually drawing clouds following a call to Atmosphere::DrawObjects(false):

static bool comp(ObjectHandle c1, ObjectHandle c2)
{
double d1 = atm->GetObjectDistance(c1, c2, camX, camY, camZ);
double d2 = atm->GetObjectDistance(c2, c1, camX, camY, camZ);
return (d1 > d2);
}
void DrawClouds()
{
SL_VECTOR(ObjectHandle>& objs = atm-)GetObjects();
sort(objs.begin(), objs.end(), comp);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_DEPTH_TEST);
glDepthMask(0);
glEnable(GL_TEXTURE_2D);
glDisable(GL_LIGHTING);
glDisable(GL_FOG);
SL_VECTOR(ObjectHandle)::iterator it;
for (it = objs.begin(); it != objs.end(); it++)
{
atm->DrawObject(*it);
}
glDepthMask(1);
}

If you instead call DrawObjects(true), this code isn't necessary, and DrawObjects() will draw the cloud objects on its own.

Crepuscular Rays

SilverLining can automatically render crepuscular rays from the sun when it is occluded by a cloud layer. This effect is also known as "God rays", shafts of light, and volumetric lighting.

To enable crepuscular rays in any renderer other than Vulkan, simply pass a value greater than 0 (and less than 1.0) for the crepuscularRays parameter in SilverLining::Atmosphere::DrawObjects(). Vulkan requires crepuscular rays to be drawn in a separate render pass, so your Vulkan application must instead call Atmosphere::GenerateCrepuscularRays() after calling Atmosphere::DrawOjbects(), and then call Atmosphere::DrawCrepuscularRays() within a separate begin/end render pass block. An example of usage is in the Vulkan sample application's SilverLiningVulkanExample.cpp file.

When enabled, rays will be drawn over the scene when the camera is facing the sun. The intensity of the rays will vary based on the cloud coverage above the camera, so no rays will be drawn if there are no cloud layers present.

Our implementation of crepuscular rays is a postprocessing effect, so it does not have access to depth information from the scene. By default, the rays will render in front of anything drawn before Atmosphere::DrawObjects() is called and in front of the clouds. The SilverLining.config setting "crepuscular-rays-depth" may be used to adjust the depth of the rays in the scene; you may prefer to have them render behind your scene's objects, in which case you would set the depth to 1.

Also bear in mind that SilverLining does not have access to your scene's geometry, so rays will only be cast based on the clouds in the scene. Other objects in your scene that might occlude the sun will not affect the rays.

The maximum intensity of the rays may be adjusted with the crepuscular-rays-exposure setting in Resources/SilverLining.config.

Advanced: using shadow maps

SilverLining makes it easy to cast shadows from the clouds onto your terrain. Just have a look at the SilverLining::Atmosphere::GetShadowMap() method.

When calling GetShadowMap(), SilverLining will render the clouds from the point of view of the sun or moon into a texture map returned to you. Along with this texture map you'll get a matrix that will transform world coordinates into texture coordinates on the shadow map. All you need to do is multiply this texture in while rendering your terrain, while applying the matrix provided to obtain texture coordinates for it. This is straightforward when incorporating the shadow map into a fragment shader.

The best time to call Atmosphere::GetShadowMap() is after calling Atmosphere::DrawObjects(). Avoid calling it in the middle of rendering your scene, and especially in between calls to Atmosphere::DrawSky() and Atmosphere::DrawObjects() - this will require SilverLining to perform an extra culling pass, and hurt performance.

If you're using fixed function pipeline rendering under OpenGL or DirectX 9, here are examples of applying the shadow map to a surface. First, in OpenGL:

void *texHandle;
Matrix4 mat, shadowMatrix;
atm->GetShadowMap(texHandle, &mat, &shadowMatrix, false);
Gluint shadowMap = (GLuint)texHandle;
glActiveTexture(GL_TEXTURE1);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, shadowMap);
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGendv(GL_S, GL_EYE_PLANE, shadowMatrix.GetRow(0));
glEnable(GL_TEXTURE_GEN_S);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGendv(GL_T, GL_EYE_PLANE, shadowMatrix.GetRow(1));
glEnable(GL_TEXTURE_GEN_T);

The above code configures automatic texture coordinate generation using the matrix provided by Atmosphere::GetShadowMap(), and binds the shadow map to texture unit 1. You'll also need to configure OpenGL to multiply this texture in with your base texture on unit 0, like this:

// Stage 1 = prev * shadow
glActiveTexture(GL_TEXTURE1);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glEnable(GL_TEXTURE_2D);
// Stage 0 = tex * primary color
glActiveTexture(GL_TEXTURE0);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glEnable(GL_TEXTURE_2D);

Again, this is more flexible if you're using shaders. Here's equivalent code under DirectX 9 - note that you need to multiply in the inverse of the view matrix with DirectX 9 in order to get into world space, since it doesn't do it automatically like OpenGL does.

void *texture;
if (atm->GetShadowMap(texture, &mvp, &xform, false))
{
device->SetTexture(1, (IDirect3DTexture9*)texture);
float mf[16];
int i = 0;
for (int row = 0; row < 4; row++)
{
for (int col = 0; col < 4; col++)
{
mf[i++] = (float)xform.elem[row][col];
}
}
D3DXMATRIX worldToTextureMatrix(mf);
D3DXMATRIX invView, textureMatrix;
D3DXMatrixInverse(&invView, NULL, &view);
D3DXMatrixMultiply(&textureMatrix, &invView, &worldToTextureMatrix);
device->SetSamplerState(1, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
device->SetSamplerState(1, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
device->SetSamplerState(1, D3DSAMP_BORDERCOLOR, 0xFFFFFFFF);
device->SetSamplerState(1, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
device->SetSamplerState(1, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
device->SetTransform(D3DTS_TEXTURE1, &textureMatrix);
device->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX,
D3DTSS_TCI_CAMERASPACEPOSITION | 1);
device->SetTextureStageState(1, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_PROJECTED | D3DTTFF_COUNT3);
device->SetTextureStageState(1, D3DTSS_COLOROP,D3DTOP_MODULATE);
device->SetTextureStageState(1, D3DTSS_COLORARG1,D3DTA_TEXTURE);
device->SetTextureStageState(1, D3DTSS_COLORARG2,D3DTA_CURRENT);
device->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1 );
device->SetTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_CURRENT );
}
An implementation of a 4x4 matrix and some simple operations on it.
Definition: Matrix4.h:23
double elem[4][4]
Data members are public for convenience.
Definition: Matrix4.h:249

Options exist in Atmosphere::GetShadowMap() to control the amount of clouds included in the shadow map, and the darkness of the shadows. See the documentation for the method for more details. If you configure it to render entire cloud layers into the shadow map, you might be able to avoid generating the shadow map each frame - but this will come at the cost of lower-resolution shadows.

Creating environment maps from SilverLining

SilverLining provides the SilverLining::Atmosphere::GetEnvironmentMap() method to automatically generate a cube map texture that represents the sky and clouds from the view of the current camera position.

By default, each face of the cube map will be 256x256. If you require higher resolution, this may be adjusted in the resources/silverlining.config file under the "environment-map-size" setting.

If there are clouds in your scene, you will need to regenerate your cube map whenever the camera or the clouds move by some significant amount for accurate reflections.

Cube maps generated such as this may be passed into Sundog Software's Triton Ocean SDK for generating water reflections from SilverLining's skies.

Using SilverLining with High Dynamic Range applications (HDR)

Under the hood, all of SilverLining's lighting is physically simulated and done in units of kilo-candelas per square meter - we then tone-map these raw physical values down to a displayable range.

This tone-mapping may be disabled by calling Atmosphere::EnableHDR(). This may be used in conjunction with a floating-point render target to receive the sky and clouds rendered in units of kilo-candelas per square meter, instead of [0,1.0]. The user is responsible for performing their own tone mapping on the entire scene in this case, as the floating point render target is transferred to a displayable surface.

When HDR is enabled, lighting values returned such as Atmosphere::GetSunOrMoonColor() will also no longer be tone mapped or clamped.

HDR support requires shader model 3.0 support (since pixel shader inputs will be clamped otherwise.)

If the rest of the scene is not rendered in units of kCD/m^2, you can scale the HDR output to better match the rest of your scene. In the resources/silverlining.config file, look for settings such as sun-transmission-scale and sun-scattered-scale which will multiply our internal lighting values by the given scale factor, thereby brightening or darkening the scene. You may also influence the brightness of the clouds (as opposed to the sky) independently with the setting sun-luminance-scale, which will further modulate the lighting values applied to the clouds.

Scaling the overall brightness of the sky itself may be accomplished with hosek-wilkie-radiance-scale.

As part of disabling tone-mapping, gamma correction will also be disabled in HDR mode. Be sure to account for gamma correction in your own tone-mapping operator to avoid skies that look too dark.

The brightness of lightning in HDR mode may be adjusted using the lightning-hdr-boost config setting.

Selecting a Sky Model

SilverLining allows you to choose between two different mathematical models for simulating sky colors for a given time and location; each has their own advantages and disadvantages. You may select your sky model using the SilverLining::Atmosphere::SetSkyModel() method. The sky model may be changed at runtime, allowing you to compare the two models we offer side by side.

The PREETHAM sky model is an extension of the Preetham paper from the 1999 SIGGRAPH proceedings. It offers a good mixture of simplicity, speed, and accuracy, and our implementation of it has the benefit of having been refined since SilverLining's initial launch in 2006. We use our own solar radiance model with it, as well as our own model of twilight and nighttime luminance to extend the original algorithms.

The HOSEK_WILKIE sky model is newer, from the 2012 SIGGRAPH proceedings. It is similar to the Preetham approach, but is more complex and handles higher turbidities and colors near the horizon more accurately. You'll find that the Hosek-Wilkie model produces sky colors that may be more realistic, but are less dramatic at lower turbidity values. Game developers may prefer the PREETHAM model, while simulation developers may prefer HOSEK_WILKIE. Our implementation of Hosek-Wilkie also uses our own solar radiance model and tone-mapping. One limitation of the Hosek-Wilkie approach is that it can only simulate the effects of sunlight when the sun is above the horizon. Due to this, we blend Hosek-Wilkie with our Preetham model implementation as the sun approaches the horizon, in order to retain SilverLining's twilight effects and sky illumination from moonlight.

Positioning Individual Clouds Within a Cloud Layer

Cumulus congestus and cumulus mediocris cloud layers will, by default, fill the layer with clouds with a realistic size distribution in order to achieve the cloud coverage requested with CloudLayer::SetDensity(). However, the placement of these clouds will be random. If you are attempting to represent something like a frontal boundary or a specific cloud formation, randomly scattered clouds may not be sufficient for your needs.

We offer the SilverLining::CloudLayer::AddCloudAt() method to allow you to place your own clouds in addition to, or instead of, the randomly distributed clouds in the cloud layer. To use this capability, first the seed the cloud layer with any randomly-positioned clouds you want. Even if you want all clouds to be hand-positioned, you must still call CloudLayer::SeedClouds() prior to placing individual clouds (you would just use CloudLayer::SetDensity(0) to prevent random clouds from being created.) Then, for each cloud, call CloudLayer::AddCloudAt() with the position and dimension of each individual cloud. The positions passed in must be relative to the center of the cloud layer, at its base, and represent the position of the center of each cloud's base. You must ensure the positions passed in are within the bounds of the cloud layer as defined by its SetBaseWidth() and SetBaseLength() calls.

Here is an example of setting up a cumulus congestus cloud layer, with 20% coverage from random scattered clouds, and a straight line of 2x2x1 km clouds extending across the center of the layer.

cumulusCongestusLayer = CloudLayerFactory::Create(hasCumulusCongestusHiRes ? CUMULUS_CONGESTUS_HI_RES : CUMULUS_CONGESTUS, *atm);
cumulusCongestusLayer->SetBaseAltitude(4000);
cumulusCongestusLayer->SetThickness(500);
cumulusCongestusLayer->SetBaseLength(80000);
cumulusCongestusLayer->SetBaseWidth(80000);
cumulusCongestusLayer->SetLayerPosition(0, 0);
cumulusCongestusLayer->SetFadeTowardEdges(true);
cumulusCongestusLayer->SetIsInfinite(true);
cumulusCongestusLayer->SetCurveTowardGround(false);
if (cumulusCongestusLayer->SupportsAddCloudAt())
{
cumulusCongestusLayer->SetDensity(0.2);
cumulusCongestusLayer->SeedClouds(*atm);
for (double x = -40000; x < 40000; x += 5000)
{
Vector3 pos(x, 0, 0); // position relative to layer center
Vector3 size(2000, 1000, 2000); // each cloud is 2km across and 1km high
cumulusCongestusLayer->AddCloudAt(*atm, pos, size);
}
}
@ CUMULUS_CONGESTUS_HI_RES
Definition: CloudTypes.h:27

Please refer to the documentation for SilverLining::CloudLayer::AddCloudAt() for more information.

Using SilverLining with Linear Color Space

Some applications render internally in linear color space, which negates the effect of gamma correction in order to improve lighting and shading. If your application does this, you'll probably find that SilverLining's skies appear too bright, since it is applying gamma correction to the sky.

To correct this, use Atmosphere::SetGamma(1.0) - this will disable SilverLining's gamma correction and draw the sky in linear color space instead.

Tweaking SilverLining's Appearance

SilverLining is highly configurable. Its default settings attempt to accurately simulate the sky for the conditions prescribed, together with tone-mapping to account for human perception and how your eyes adjust to low light levels. But, if you don't like the appearance of the sky, clouds, or precipitation under a given set of circumstances, odds are you can adjust it to your liking. Most of the "knobs and dials" for SilverLining may be found in the resources/SilverLining.config file. Open it up in your development environment and examine the available settings; most are well-documented within the file.

To change a setting, save your changes into the SilverLining.config file, and restart your application. Some changes may be configurable at runtime by using SilverLining::Atmosphere::SetConfigOption().

The config file consists of the following main sections:

  • General Settings - These control high-level rendering strategies, the overall tone mapping brightness, and defaults.
  • Sky Settings - Controls the appearance of the sky box, and scaling values for the simualted sun and moonlight.
  • Precipitation - These settings control the appearance and density of precipitation particles.
  • Atmosphere From Space - Controls the size and resolution of the "atmospheric limb" when viewing the atmosphere from space.
  • Glare Settings - Controls physically-based glare effects surrounding the sun, moon, and bright stars.
  • Lightning Settings - Tweaks the complexity and color of lightning strikes from cumulonimbus clouds.
  • Cloud Settings - Adjusts the appearance of cumulus clouds in general.
  • Specific Cloud Type Settings - Adjusts the simulated droplet particle properties, cloud growth models, and cloud distribution models for each cloud type.

For example, if you want to increase the brightness of the clouds overall, adjust the setting ambient-scattering up. For deeper shadows within the clouds, adjust cumulus-lighting-quick-and-dirty-attenuation down.

The overall brightness of the sky and light may be influenced with the "brightness" setting.

For richer colors in the atmosphere, experiment with the other air-mass-model settings (ICQ or NREL.) You can also adjust the air mass directly using the air-mass-multiplier setting; higher air masses result in more atmospheric scattering.

The color of sunsets and sunrises may also be directly adjusted, using the settings sunset-X-boost, sunset-Y-boost, sunset-Z-boost, and sunset-boost-exponent. To make the sunsets redder, increase the sunset-X-boost a bit. These settings represent a boost applied to the scattered sunlight color in XYZ color space. The exponent controls how these boosts are applied as the sun's angle from the horizon changes. Higher integers for sunset-boost-exponent will limit the effect of the boost to be closer to the horizon; lower values will make the boosts more pervasive.

For more visible rain particles, increase rain-streak-brightness and rain-streak-width-multiplier.

The color and density of the fog inside stratus clouds may be adjusted using the stratus-fog-* settings.

Want a bigger sun or moon? Increase sun-width-degrees or moon-width-degrees.

If you wish to keep your changes to SilverLining.config separate from the default configuration file, you may keep your changes in a SilverLining.override file placed alongside SilverLining.config. This way, you can update to SilverLining.config files in new versions of SilverLining without worrying about merging your changes back in.

Many other tweaks are possible; examine the config file for more options, and feel free to contact us at suppo.nosp@m.rt@s.nosp@m.undog.nosp@m.-sof.nosp@m.t.com if you have any questions about them.