Implementing Exponential Height Fog in a Custom Shader

I need to be able to control Exponential Height Fog on a per-pixel basis, so I am trying to implement it into an HLSL custom node inside a material. I feel like I’m on the right track, but struggling to figure out how it’s actually done in the engine version (tried to sniff out where the equation/s are lurking in the source but didn’t get very far). I’m wondering if anyone else has ever tried this, or can point me in the right direction. Here is what I have so far (not looking so great):

As you can see, (among many other things!) I’m having trouble getting a nice falloff between the scattering and inscattering colours. Here is my network:

And here is the code in the custom node:


float distance = length( Pixel_Position - Camera_Position );

float fog_amount = 1 - exp( - distance * Fog_Intensity);

float3 pixel_to_cam = normalize ( Camera_Position - Pixel_Position );

float sun_dot = dot ( pixel_to_cam, Sun_Direction );

float sun_dot_ranged = ( ( ( sun_dot - 1 ) / ( Sun_Intensity - 1 ) ) * ( 0 - 1 ) ) + 1;

return lerp (Scattering_Colour.xyz, Inscattering_Colour.xyz, sun_dot_pow) * fog_amount;

I absolutely love the look of the standard UE4 Exponential Height Fog, can anyone help me out for implementing this in a shader?

I should mention, I haven’t tried to actually implement the code for height falloff yet, I’m currently just trying to get the scattering/inscattering setup working.

Thanks!

https://github.com/EpicGames/UnrealEngine/blob/master/Engine/Shaders/HeightFogCommon.usf

https://github.com/EpicGames/UnrealEngine/blob/master/Engine/Shaders/HeightFogCommon.usf

Thanks! Looking through that now. Will see if I can get it working!

Hmmm… implementing the code from HeightFogCommon isn’t working out quite as easily as I had hoped.

Below is the adjusted code. All I have done is:

  1. Strip out the branch for USE_GLOBAL_CLIP_PLANE
  2. Added a line for defining ExponentialFogParameters.x using the definition in the code comment

The rest is the same.


/*=============================================================================
	HeightFog - HLSL Version
=============================================================================*/

float FLT_EPSILON = 0.001f;
float FLT_EPSILON2 = 0.01f;

ExponentialFogParameters.x =  ExponentialFogParameters3.x * exp2(-ExponentialFogParameters.y * (CameraWorldPosition.z - ExponentialFogParameters3.y));

/** Color to use. */
float3 ExponentialFogColor = ExponentialFogColorParameter.xyz;
float MinFogOpacity = ExponentialFogColorParameter.w;

float3 CameraToReceiver = PixelWorldPosition - CameraWorldPosition;
float CameraToReceiverLengthSqr = dot(CameraToReceiver, CameraToReceiver);
float CameraToReceiverLengthInv = rsqrt(CameraToReceiverLengthSqr);
float CameraToReceiverLength = CameraToReceiverLengthSqr * CameraToReceiverLengthInv;
half3 CameraToReceiverNormalized = CameraToReceiver * CameraToReceiverLengthInv;

float RayOriginTerms = ExponentialFogParameters.x;
float RayLength = CameraToReceiverLength;
float RayDirectionZ = CameraToReceiver.z;

// Calculate the line integral of the ray from the camera to the receiver position through the fog density function
// The exponential fog density function is d = GlobalDensity * exp(-HeightFalloff * z)
float EffectiveZ = (abs(RayDirectionZ) > FLT_EPSILON2) ? RayDirectionZ : FLT_EPSILON2;
float Falloff = max(-127.0f, ExponentialFogParameters.y * EffectiveZ);	// if it's lower than -127.0, then exp2() goes crazy in OpenGL's GLSL.
float ExponentialHeightLineIntegralShared = RayOriginTerms * (1.0f - exp2(-Falloff) ) / Falloff;
float ExponentialHeightLineIntegral = ExponentialHeightLineIntegralShared * max(RayLength - ExponentialFogParameters.w, 0.0f);

float3 DirectionalInscattering = 0;

if (InscatteringLightDirection.w > 0)
{
	// Setup a cosine lobe around the light direction to approximate inscattering from the directional light off of the ambient haze;
	float3 DirectionalLightInscattering = DirectionalInscatteringColor.xyz * pow(saturate(dot(CameraToReceiverNormalized, InscatteringLightDirection.xyz)), DirectionalInscatteringColor.w);

	// Calculate the line integral of the eye ray through the haze, using a special starting distance to limit the inscattering to the distance
	float DirExponentialHeightLineIntegral = ExponentialHeightLineIntegralShared * max(RayLength - DirectionalInscatteringStartDistance, 0.0f);
	// Calculate the amount of light that made it through the fog using the transmission equation
	float DirectionalInscatteringFogFactor = saturate(exp2(-DirExponentialHeightLineIntegral));
	// Final inscattering from the light
	DirectionalInscattering = DirectionalLightInscattering * (1 - DirectionalInscatteringFogFactor);
}

// Calculate the amount of light that made it through the fog using the transmission equation
float ExpFogFactor = max(saturate(exp2(-ExponentialHeightLineIntegral)), MinFogOpacity);

return float4((ExponentialFogColor) * (1 - ExpFogFactor) + DirectionalInscattering, ExpFogFactor);

Here is the shader network, into which I am feeding in the same values I am using in the standard height fog:

And here is the current result compared with the standard one:

Obviously I am not expecting any of the fog effect outside of the cube itself at this point, but all I am getting is a very straight line across the geometry. Additionally, if I tilt the camera up slightly, the entire box snaps to becomes the fog colour, and if I tilt downwards, it snaps to have no fog colour.

Unfortunately, since I don’t really understand most of the code in the function, I can’t understand what’s actually happening here, or what’s going wrong, what I’m missing.

Does anyone know if this is even possible?

It would probably be easier if you would edit the existing shader. With the forward renderer you can do the fog per vertex I think, and then it has to run in the vertex shader where you should have access to the vertex color and UVs. So you could modify the code to set the fog intensity to 0 on some predefined vertex colors :slight_smile:

Phew, got it working in the end!

Just thought I would share an example:

So this is all taking place within the material itself. I need this for such a weird and specific purpose in my project, so I can’t imagine anyone will actually need to be able to do such a thing, but on the off-chance anyone needs height fog inside a shader, let me know and I can clean it up and share it!

Can you post the final material + custom code. I can double check that everything is right.

Sure, here is the material setup:

The Absolute World Position and Camera Position nodes aren’t connected here because they are connected inside the Material Function you can see there, so don’t need to be connected externally.

Here is the custom node code:


//////////////////////////////////////////////////
//
// Directional Inscattering Height Fog
//
// With imaginary-sun-based falloff
//
//////////////////////////////////////////////////

float distance = length( Pixel_Position - Camera_Position );

float3 ray_direction = normalize ( Pixel_Position - Camera_Position );

float fog_amount = 1.0 - exp ( - distance * Fog_Density);

float3 imaginary_sun_position = Directional_Inscattering_Light_Direction * Imaginary_Sun_Distance;

float pixel_to_sun_distance = length ( Pixel_Position - imaginary_sun_position );

// Correction multiplier to ensure inscattering is behind near objects
float distance_correction = clamp ( ( ( ( ( pixel_to_sun_distance - Directional_Inscattering_Start_Distance ) / ( Directional_Inscattering_End_Distance - Directional_Inscattering_Start_Distance ) ) * ( 0 - 1) ) + 1 ), 0, 1);

float directional_inscattering_amount = max ( dot ( ray_direction, ( normalize ( Directional_Inscattering_Light_Direction ) * Directional_Inscattering_Multiplier ) ), 0.0 ) * distance_correction;

float3 output_colour = lerp ( Inscattering_Colour, Directional_Inscattering_Colour, pow ( directional_inscattering_amount, Directional_Inscattering_Exponent ) );

float falloff_correction = clamp ( ( ( ( ( Pixel_Position.z - Height_Falloff_Start ) / ( Height_Falloff_End - Height_Falloff_Start ) ) * ( 0 - 1) ) + 1 ), 0, 1 );

return output_colour * fog_amount * pow ( falloff_correction, Height_Falloff_Exponent );

I couldn’t get the HeightFogCommon one to work, so I built this one up from scratch. A lot of it comes from here, with various additions of my own in terms of control. I couldn’t get the height fog implementation from that page to work, so I came up with my own. I’ve also done something which might be quite strange: setting the light position using a normalised direction vector (Directional_Inscattering_Light_Direction) and multiplying it by a number (Imaginary_Sun_Distance) to place it where I want it. Could easily be adapted to take positional and directional information from an actual light source in the scene (which would probably make more sense), but this is how I want it for my purposes right now. You can then control how much influence the directional scattering has over geometry behind it using Directional_Inscattering_End_Distance.

I’m pretty happy with the final result, as I lets me create exactly the look I was hoping for, but it’s not necessarily completely optimised, so let me know what you think!

PS.
Just wanted to add, in case it just looks messy: I have a naming convention for my custom nodes where I used Capitalised_Variable_Names for variables that are coming from outside the node and uncapitalised_variable_names for variables created internally to the custom node.

My question may be stupid but is this postprocess material or? Are all the objects on the scene using the same material or?

Definitely not a stupid question. As I mentioned, I’m using this in a weird and specific way that currently requires me to use it in an ordinary material. So yes, all the materials implement this as part of their emissive channel, but have different diffuse/normal/etc.

I had the thought yesterday that perhaps I should be doing this as a post process material instead, so it is something I’m going to be looking at, but for my purposes I think it might still be better inside the normal material.

Your fog isn’t using extinction at all? When object is further a way fog should block visibility to that object based on fog amount.

@Kalle-H: I am doing that with fog_amount (float fog_amount = 1.0 - exp ( - distance * Fog_Density);), which is a multiplier for the final output. Here are some example settings for the Fog_Density input:

Is that what you mean?

Original exp fog function outputs float4. Alpha channel is used to prevent you to see object behind fog. Ideally you should multiply base color, specular and material based emissive with that alpha value. You are not seeing problem probably because everything is unlit.

Ah, interesting! My scene uses only baked lighting, so for my current purposes the setup should be fine, right? Should I ever update this for dynamic lighting I’ll bear in mind what you said. Thanks!

In post process materials, you can directly use the World Position node to access the position of every pixel. Should be pretty simple to convert this to a post effect since that would make it easier to apply to a whole scene.

For baked scenes you need to do exactly same than with dynamic lighting. Normal fog equation is like this: FoggedColor = Color * extinction + inscatter. So when fog is really thick you can’t see object at all but only lot of inscatter.

It says, undeclared identifier Pixel Position…