Soft clouds in SkyMaxx Pro 3This image from the upcoming SkyMaxx Pro version 3 add-on for X-Plane illustrates how our SilverLining Sky, 3D Cloud, and Weather SDK can be extended for application-specific effects. In this case, SilverLining is using a depth buffer texture given to it for soft blending between clouds and terrain.

OpenGL developers using SilverLining have access to a powerful shader extension system, allowing effects such as these. Let’s walk through how this soft cloud blending approach was implemented as an example of extending SilverLining’s shaders.

If you open up the file Resources/Shaders/UserFunctions-frag.glsl in a text editor, you’ll see many hooks available to you for extending how SilverLining does its rendering. In our case, we’ll be using the overrideStratusLighting() and overrideBillboardFragment() functions in order to adjust the final transparency of solid and particle-based clouds. The general idea is to use a copy of the scene’s depth buffer to determine the distance from the camera to the terrain at a given pixel, and compare that to the distance to the clouds at a given pixel. When clouds get near the terrain, we start to fade them out. This is the same idea behind “soft particles,” so it’s a technique that’s well understood.

In order to figure out camera depth just given a point on the screen, we’ll need to pass some additional information into UserFunctions-frag.glsl, by adding these uniform declarations to the top:

uniform vec4 viewport;
uniform mat4 persMatrix;
uniform mat4 invPersMatrix;
uniform float softness;
uniform sampler2D depthTexture;

These uniforms represent the viewport, perspective projection matrix for the scene, the inverse projection matrix, a “softness” value that defines the distance, in world units, over which softness is applied, and a depth texture holding a copy of the current depth buffer after terrain has been rendered.

After your application has initialized SilverLining, you’ll need to get handles to these uniforms so you can set them each frame. That can be done using glGetUniformLocation(), for example:

uniforms.depthTextureLoc = glGetUniformLocation(shader, "depthTexture");
uniforms.softnessLoc = glGetUniformLocation(shader, "softness");
uniforms.invPersMatrixLoc = glGetUniformLocation(shader, "invPersMatrix");
uniforms.persMatrixLoc = glGetUniformLocation(shader, "persMatrix");
uniforms.viewportLoc = glGetUniformLocation(shader, "viewport");

A tricky part is knowing what shader ID to use with glGetUniformLocation. As it turns out, there are several shaders within SilverLining responsible for drawing clouds, so we need to identify all of them and keep a map of the uniform locations for each shaders, so we can set them all. SilverLining provides methods to expose these shader IDs, like this:

// Get the billboard shader ID, and set its uniforms for
// soft particles
GLuint shader = atm->GetBillboardShader();
SetSoftUniforms(shader, …);

// Do the same for planar cloud shaders.
SL_VECTOR(GLuint) planarShaders = atm->GetActivePlanarCloudShaders();
SL_VECTOR(GLuint)::iterator it;
for (it = planarShaders.begin(); it != planarShaders.end(); it++) {
SetSoftUniforms(*it, …);
}

Obtaining a copy of the depth buffer in a texture is surprisingly easy in OpenGL. First, initialize your depth texture like this:

glGenTextures(1, &depthBuffer);
glBindTexture(GL_TEXTURE_2D, depthBuffer);

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

// texture should tile
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, screenWidth, screenHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);

And then each frame, after your terrain is drawn, capture it like this:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, depthBuffer);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenWidth, screenHeight);

It’s that easy.

Each frame, we’ll need to set the uniform parameters our custom shaders will need. In SkyMaxx Pro 3, this is done by maintaining a std::map structure that keeps track of the uniform locations for each cloud shader. One function gets those uniform locations if they haven’t already been found, and sets them. Putting it all together looks like this:

extern GLuint depthBuffer;

class SoftUniforms
{
public:
SoftUniforms() : depthTextureLoc(-1), softnessLoc(-1), invPersMatrixLoc(-1),
persMatrixLoc(-1), viewportLoc(-1) {}

GLint depthTextureLoc, softnessLoc, invPersMatrixLoc, persMatrixLoc, viewportLoc;
};

static std::map uniformMap;

void SetSoftUniforms(GLuint shader, float *invProjM, float *projM, GLint *viewport)
{
// Set the active shader
glUseProgram(shader);

// Get or set the cached uniform locations for this shader
SoftUniforms uniforms;
if (uniformMap.find(shader) != uniformMap.end()) {
uniforms = uniformMap[shader];
} else {
uniforms.depthTextureLoc = glGetUniformLocation(shader, "depthTexture");
uniforms.softnessLoc = glGetUniformLocation(shader, "softness");
uniforms.invPersMatrixLoc = glGetUniformLocation(shader, "invPersMatrix");
uniforms.persMatrixLoc = glGetUniformLocation(shader, "persMatrix");
uniforms.viewportLoc = glGetUniformLocation(shader, "viewport");

uniformMap[shader] = uniforms;
}

// Set the uniforms.
if (uniforms.depthTextureLoc != -1)
glUniform1i(uniforms.depthTextureLoc, 7);

if (uniforms.softnessLoc != -1)
glUniform1f(uniforms.softnessLoc, softness);

if (uniforms.invPersMatrixLoc != -1)
glUniformMatrix4fv(uniforms.invPersMatrixLoc, 1, 0, invProjM);

if (uniforms.persMatrixLoc != -1)
glUniformMatrix4fv(uniforms.persMatrixLoc, 1, 0, projM);

if (uniforms.viewportLoc != -1)
glUniform4f(uniforms.viewportLoc, (float)viewport[0], (float)viewport[1], (float)viewport[2], (float)viewport[3]);
}

void SetupSoftParticles()
{
if (hasSoftClouds) {
// Get the projection and inverse projection matrices
// from SilverLining, and convert them to floating
// point matrices suitable for glUniformMatrix4fv
const double * proj = atm->GetProjectionMatrix();
SilverLining::Matrix4 projM(const_cast(proj));
SilverLining::Matrix4 invProj = projM.Inverse();
float finvProjM[16];
int i = 0;
for (int row = 0; row < 4; row++) { for (int col = 0; col < 4; col++) { finvProjM[i++] = (float)(invProj.elem[row][col]); } } float fprojM[16]; i = 0; for (int row = 0; row < 4; row++) { for (int col = 0; col < 4; col++) { fprojM[i++] = (float)(projM.elem[row][col]); } } // Get the current viewport (ideally you'd get this // some other way than glGet) GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); // Get the billboard shader ID, and set its uniforms for // soft particles GLuint shader = atm->GetBillboardShader();
SetSoftUniforms(shader, finvProjM, fprojM, viewport);

// Do the same for planar cloud shaders.
SL_VECTOR(GLuint) planarShaders = atm->GetActivePlanarCloudShaders();
SL_VECTOR(GLuint)::iterator it;
for (it = planarShaders.begin(); it != planarShaders.end(); it++) {
SetSoftUniforms(*it, finvProjM, fprojM, viewport);
}

// Bind our depth texture to unit 7, which is unused by SilverLining.
// Anything above 2 is safe, really.
glActiveTexture(GL_TEXTURE7);
glBindTexture(GL_TEXTURE_2D, depthBuffer);

// Clean up
glUseProgram(0);
}
}

Now that we have all of the information our shaders need for soft clouds, we need to write the shader code. This function works the magic of determining the blending value for clouds given a fragment location; I’ll spare you how the math works.

float getSoftness()
{
if (softness <= 0.0) { return 1.0; } vec4 ndcPos; ndcPos.xy = ((2.0 * gl_FragCoord.xy) - (2.0 * viewport.xy)) / (viewport.zw) - 1.0; ndcPos.z = gl_FragCoord.z * 2.0 - 1.0; ndcPos.w = 1.0; vec4 clipPos = ndcPos / gl_FragCoord.w; vec4 eyePos = invPersMatrix * clipPos; float myDepth = abs(eyePos.z); vec2 tc = (gl_FragCoord.xy - viewport.xy) / viewport.zw; float depth = texture2D(depthTexture, tc).x; ndcPos.z = depth * 2.0 - 1.0; clipPos.w = persMatrix[3][2] / (ndcPos.z - (persMatrix[2][2] / persMatrix[2][3])); clipPos.xyz = ndcPos.xyz * clipPos.w; eyePos = invPersMatrix * clipPos; float sceneDepth = abs(eyePos.z); return clamp( abs(myDepth - sceneDepth) / softness, 0.0, 1.0); }

The last step is modifying the hooks for planar (stratus) clouds and cumulus (billboard) clouds to apply this softness value:

// Allows overriding of the fog color, fog blend factor, underlying cloud color, and alpha blending of the cloud.
void overrideStratusLighting(in vec3 fogColor, in float fogFactor, in vec3 cloudColor, in float cloudFade, inout vec4 finalColor)
{
finalColor.a *= getSoftness();
}

// Overrides fragment colors of billboards (cloud puffs, sun, moon.)
void overrideBillboardFragment(in vec4 texColor, in vec4 lightColor, inout vec4 finalColor)
{
finalColor.xyzw *= getSoftness();
}

Soft clouds in SkyMaxx Pro 3And that's the last piece that puts it all together! That's a complete example of using SilverLining's extensible shader framework to add an entirely new effect; in this case, soft clouds that smoothly blend both stratus and cumulus clouds with the terrain. You can use this for scenes like this one, with mountains poking up through the clouds, or for effects like ground fog where you don't want to see sharp intersections between cloud particles and the ground. Obviously many more effects are possible with this system, such as integrating your own light sources that illuminate the clouds, or adapting SilverLining to work with your own custom rendering pipelines or custom depth buffer formats. The sky, as we say, is the limit!