Landing Lights illuminating clouds in X-PlaneSometimes, users want things like airplane landing lights to illuminate SilverLining’s clouds as you fly through them at night. A few customers have done this on their own by using SilverLining’s extensible shader framework. Recently, we had to do this ourselves as well – so I’ll share the code we used. Note, this article is specific to the OPENGL renderer in SilverLining.

To do this, you’ll use the “user shader” files that allow you to inject your own shader code into SilverLining’s rendering. In the Resources/Shaders folder, you’ll find UserFunctions.glsl and UserFunctions-frag.glsl files, which contain entry points for extending our vertex and fragment programs, respectively.

The first thing we need for adding a spotlight to the clouds is the interpolated eye-space position of each cloud fragment. To get this, we’ll compute the eye-space coordinate of each point in our vertex programs. Start by adding this at the top of UserFunctions.glsl:

varying vec3 eyePos;
uniform mat4 invPersMatrix;

And then, to actually compute eyePos, modify the overridePosition function like this:

// Provides a point to override the final value of gl_Position.
// Useful for implementing logarithmic depth buffers etc.
vec4 overridePosition(in vec4 position)
{
eyePos = (invPersMatrix * position).xyz;
return position;
}

Now, we’ll compute the actual spotlight on a per-pixel basis in our fragment shaders. Add the following to the top of UserFunctions-frag.glsl:

varying vec3 eyePos;

uniform bool landingLights;
uniform vec3 lightDir;
uniform vec3 lightPos;
uniform mat4 viewMat;
uniform mat4 normalMat;

#define SPOTEXPONENT 40.0
#define LINEARATT 0.005

And, add this function that actually computes the spotlight effect. Note, we’re hard-coding the properties of the spotlight itself, and limiting ourselves to a single spotlight. You may want to make your implementation more flexible. Also, there are a few tricks noted in the comments for making the spotlight look like it’s scattering throughout the cloud volume, and not just lighting up flat billboards.

float spotLight(in vec3 eyePos)
{
float spotDot; // cosine of angle between spotlight
float spotAttenuation; // spotlight attenuation factor
float attenuation; // computed attenuation factor
float d; // distance from surface to light source
vec3 VP; // direction from surface to light position

// Compute vector from surface to light position
VP = (viewMat * vec4(lightPos, 1.0)).xyz - eyePos;

// Compute distance between surface and light position
d = length(VP);

// Bail early if it's far away
if (d > 10000.0) return 0;

// Normalize the vector from surface to light position
VP /= d;

// Compute distance attenuation (with minimum distance
// of 500 meters to try and capture the effect of scattering)
attenuation = 1.0 / (LINEARATT * max(d, 500.0));

vec4 eyeNormal = normalMat * vec4(lightDir, 1.0);
spotDot = dot(-VP, normalize(eyeNormal.xyz));
spotDot = clamp(spotDot, 0.0, 1.0);

// Compute directional attenuation, flaring it out as the
// point gets close to the light to approximate scattering
float alpha = min(d * 0.001, 1.0);
spotAttenuation = pow(spotDot, mix(1.0, SPOTEXPONENT, alpha));

// Combine the spotlight and distance attenuation.
attenuation *= spotAttenuation;

return clamp(attenuation, 0.0, 1.0);
}

Now we need to hook this spotLight function into the fragment shaders for the cloud billboards and stratus clouds. Modify the following functions like so:

void overrideStratusLighting(in vec3 fogColor, in float fogFactor, in float vertDist, in vec3 cloudColor, in float cloudFade, inout vec4 finalColor)
{
vec3 light = vec3(0.0,0.0,0.0);

if (landingLights)
{
float att = spotLight(eyePos);
light = vec3(att,att,att);
}

finalColor.xyz += light;
}

void overrideBillboardFragment(in vec4 texColor, in vec4 lightColor, inout vec4 finalColor)
{
if (landingLights)
{
float att = spotLight(eyePos);
vec4 adjLight = min(lightColor + vec4(att,att,att,0), vec4(1,1,1,1));
finalColor = adjLight * texColor;
}
}

The last thing you need to do is to actually populate all those new uniform variables from your application. How to do that will vary depending on how you keep track of your matrices, whether landing lights are on, etc. But, here are a couple of code snippets that should point you in the right direction:

class LightUniforms
{
public:
LightUniforms() : landingLightsLoc(-1), lightDirLoc(-1), lightPosLoc(-1), viewMatLoc(-1), normalMatLoc(-1), invPersMatrixLoc(-1) {}

GLint landingLightsLoc, lightDirLoc, lightPosLoc, viewMatLoc, normalMatLoc, invPersMatrixLoc;
};

static std::map uniformMapLights;

void SetLandingLightUniforms(GLuint shader, bool landingLights, const Vector3& lightPos, const Vector3& lightDir, float *viewM, float *normalM, float *invProjM)
{
glUseProgram(shader);

LightUniforms uniforms;
if (uniformMapLights.find(shader) != uniformMapLights.end()) {
uniforms = uniformMapLights[shader];
} else {
uniforms.landingLightsLoc = glGetUniformLocation(shader, "landingLights");
uniforms.lightDirLoc = glGetUniformLocation(shader, "lightDir");
uniforms.lightPosLoc = glGetUniformLocation(shader, "lightPos");
uniforms.viewMatLoc = glGetUniformLocation(shader, "viewMat");
uniforms.normalMatLoc = glGetUniformLocation(shader, "normalMat");
uniforms.invPersMatrixLoc = glGetUniformLocation(shader, "invPersMatrix");
uniformMapLights[shader] = uniforms;
}

if (uniforms.landingLightsLoc != -1)
glUniform1i(uniforms.landingLightsLoc, landingLights ? 1 : 0);

if (uniforms.lightDirLoc != -1)
glUniform3f(uniforms.lightDirLoc, lightDir.x, lightDir.y, lightDir.z);

if (uniforms.lightPosLoc != -1)
glUniform3f(uniforms.lightPosLoc, lightPos.x, lightPos.y, lightPos.z);

if (uniforms.viewMatLoc != -1)
glUniformMatrix4fv(uniforms.viewMatLoc, 1, 0, viewM);

if (uniforms.normalMatLoc != -1)
glUniformMatrix4fv(uniforms.normalMatLoc, 1, 0, normalM);

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


void SetupLandingLights()
{
bool landingLights = false;
Vector3 lightPos, lightDir;

// Populate the above variables with the state of your landing lights
// switch, the position of the landing lights in world coordinates,
// and the direction of the landing lights in world-space.
// "atm" below is your SilverLining::Atmosphere instance.

const double * view = atm->GetCameraMatrix();
SilverLining::Matrix4 viewM(const_cast<double*>(view));
SilverLining::Matrix4 normalM = viewM.Inverse();
normalM.Transpose();

const double *proj = atm->GetProjectionMatrix();
SilverLining::Matrix4 projM(const_cast<double*>(proj));
SilverLining::Matrix4 invProjM = projM.Inverse();

float fViewM[16];
int i = 0;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
fViewM[i++] = (float)(viewM.elem[row][col]);
}
}

float fNormalM[16];
i = 0;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
fNormalM[i++] = (float)(normalM.elem[row][col]);
}
}

float fInvProjM[16];
i = 0;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
fInvProjM[i++] = (float)(invProjM.elem[row][col]);
}
}

GLuint shader = atm->GetBillboardShader();
SetLandingLightUniforms(shader, landingLights, lightPos, lightDir, fViewM, fNormalM, fInvProjM);

// 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++) {
SetLandingLightUniforms(*it, landingLights, lightPos, lightDir, fViewM, fNormalM, fInvProjM);
}
}

If you have similar requirements to illuminate SilverLining’s clouds with your own light sources; I hope this saves you some time!

Be careful though – SilverLining is often bound by fill rate, and adding complexity to its fragment programs will only make matters worse. Don’t try to process too many light sources at once on the clouds, as it may affect performance. Cull out any light sources that aren’t close enough to affect the clouds in the current view before sending them to the shaders, or consider alternate schemes like Forward-Plus lighting (that’s how MUSE OpenIG does it!)