diff --git a/Compositor/HDR/env-capture-pass.xml b/Compositor/HDR/env-capture-pass.xml
index 2abcdad44..403051d22 100644
--- a/Compositor/HDR/env-capture-pass.xml
+++ b/Compositor/HDR/env-capture-pass.xml
@@ -9,7 +9,7 @@
   <!-- TODO: Explicitly select the LOD level -->
   <cull-mask>0x800</cull-mask>
   <binding>
-    <unit>11</unit>
+    <unit>13</unit>
     <buffer>sky-view</buffer>
   </binding>
 </PropertyList>
diff --git a/Compositor/HDR/hdr.xml b/Compositor/HDR/hdr.xml
index fc26a7e30..df2eb8bbe 100644
--- a/Compositor/HDR/hdr.xml
+++ b/Compositor/HDR/hdr.xml
@@ -704,7 +704,7 @@
       <buffer>depth</buffer>
     </binding>
     <binding>
-      <unit>8</unit>
+      <unit>7</unit>
       <buffer>ao0</buffer>
     </binding>
     <binding>
@@ -730,7 +730,7 @@
   </pass>
 
   <!--
-      Forward pass
+      Main forward pass
       Render all objects that couldn't be rendered on the G-Buffer, mainly
       transparent objects. This is also done in HDR.
       We reuse the depth buffer from the G-Buffer stage so we can take advantage
@@ -740,17 +740,29 @@
     <name>forward</name>
     <type>scene</type>
     <effect-scheme>hdr-forward</effect-scheme>
+    <use-shadow-pass>csm0</use-shadow-pass>
+    <use-shadow-pass>csm1</use-shadow-pass>
+    <use-shadow-pass>csm2</use-shadow-pass>
+    <use-shadow-pass>csm3</use-shadow-pass>
     <binding>
       <unit>9</unit>
       <buffer>prefiltered-envmap</buffer>
     </binding>
+    <binding>
+      <unit>10</unit>
+      <buffer>sun-shadowmap-atlas</buffer>
+    </binding>
     <binding>
       <unit>11</unit>
-      <buffer>sky-view</buffer>
+      <buffer>aerial-perspective</buffer>
+    </binding>
+    <binding>
+      <unit>12</unit>
+      <buffer>transmittance</buffer>
     </binding>
     <binding>
       <unit>13</unit>
-      <buffer>transmittance</buffer>
+      <buffer>sky-view</buffer>
     </binding>
     <attachment>
       <component>color0</component>
diff --git a/Effects/HDR/lighting.eff b/Effects/HDR/lighting.eff
index cc129bbba..8c4e4675d 100644
--- a/Effects/HDR/lighting.eff
+++ b/Effects/HDR/lighting.eff
@@ -2,10 +2,11 @@
 <PropertyList>
   <name>Effects/HDR/lighting</name>
   <parameters>
-    <texture n="7">
+    <texture n="8">
       <image>Textures/PBR/dfg_lut.dds</image>
-	  <type>2d</type>
+      <type>2d</type>
       <filter>linear</filter>
+      <mag-filter>linear</mag-filter>
       <wrap-s>clamp-to-edge</wrap-s>
       <wrap-t>clamp-to-edge</wrap-t>
       <internal-format>normalized</internal-format>
@@ -14,18 +15,21 @@
   <technique n="1">
     <pass>
       <texture-unit>
-        <unit>7</unit>
-        <image><use>texture[7]/image</use></image>
-		<type><use>texture[7]/type</use></type>
-        <filter><use>texture[7]/filter</use></filter>
-        <wrap-s><use>texture[7]/wrap-s</use></wrap-s>
-        <wrap-t><use>texture[7]/wrap-t</use></wrap-t>
-        <internal-format><use>texture[7]/internal-format</use></internal-format>
+        <unit>8</unit>
+        <image><use>texture[8]/image</use></image>
+        <type><use>texture[8]/type</use></type>
+        <filter><use>texture[8]/filter</use></filter>
+        <mag-filter><use>texture[8]/mag-filter</use></mag-filter>
+        <wrap-s><use>texture[8]/wrap-s</use></wrap-s>
+        <wrap-t><use>texture[8]/wrap-t</use></wrap-t>
+        <internal-format><use>texture[8]/internal-format</use></internal-format>
       </texture-unit>
       <program>
         <vertex-shader>Shaders/HDR/trivial.vert</vertex-shader>
         <fragment-shader>Shaders/HDR/lighting.frag</fragment-shader>
         <fragment-shader>Shaders/HDR/gbuffer-include.frag</fragment-shader>
+        <fragment-shader>Shaders/HDR/lighting-include.frag</fragment-shader>
+        <fragment-shader>Shaders/HDR/aerial-perspective-include.frag</fragment-shader>
       </program>
       <uniform>
         <name>gbuffer0_tex</name>
@@ -48,12 +52,13 @@
         <value type="int">3</value>
       </uniform>
       <uniform>
-        <name>dfg_lut</name>
+        <name>ao_tex</name>
         <type>sampler-2d</type>
         <value type="int">7</value>
       </uniform>
+      <!-- Lighting include -->
       <uniform>
-        <name>ao_tex</name>
+        <name>dfg_lut</name>
         <type>sampler-2d</type>
         <value type="int">8</value>
       </uniform>
@@ -67,6 +72,7 @@
         <type>sampler-2d-shadow</type>
         <value type="int">10</value>
       </uniform>
+      <!-- Aerial perspective include -->
       <uniform>
         <name>aerial_perspective_lut</name>
         <type>sampler-2d</type>
diff --git a/Effects/model-pbr-transparent.eff b/Effects/model-pbr-transparent.eff
new file mode 100644
index 000000000..225552fa3
--- /dev/null
+++ b/Effects/model-pbr-transparent.eff
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<PropertyList>
+  <name>Effects/model-pbr-transparent</name>
+  <inherits-from>Effects/model-pbr</inherits-from>
+  <parameters>
+    <texture n="8">
+      <image>Textures/PBR/dfg_lut.dds</image>
+	  <type>2d</type>
+      <filter>linear</filter>
+      <mag-filter>linear</mag-filter>
+      <wrap-s>clamp-to-edge</wrap-s>
+      <wrap-t>clamp-to-edge</wrap-t>
+      <internal-format>normalized</internal-format>
+    </texture>
+    <!-- Alpha Coverage -->
+    <blend>1</blend>
+    <alpha-cutoff>-1.0</alpha-cutoff>
+  </parameters>
+
+  <technique n="19">
+    <scheme>hdr-geometry</scheme>
+  </technique>
+
+  <technique n="40">
+    <scheme>hdr-forward</scheme>
+    <pass>
+      <!-- Reverse floating point depth buffer -->
+      <depth>
+        <function>gequal</function>
+        <near>1.0</near>
+        <far>0.0</far>
+      </depth>
+      <texture-unit>
+        <unit>0</unit>
+        <type><use>texture[0]/type</use></type>
+        <image><use>texture[0]/image</use></image>
+        <filter><use>texture[0]/filter</use></filter>
+        <mag-filter><use>texture[0]/mag-filter</use></mag-filter>
+        <wrap-s><use>texture[0]/wrap-s</use></wrap-s>
+        <wrap-t><use>texture[0]/wrap-t</use></wrap-t>
+      </texture-unit>
+      <texture-unit>
+        <unit>1</unit>
+        <type><use>texture[1]/type</use></type>
+        <image><use>texture[1]/image</use></image>
+        <filter><use>texture[1]/filter</use></filter>
+        <mag-filter><use>texture[1]/mag-filter</use></mag-filter>
+        <wrap-s><use>texture[1]/wrap-s</use></wrap-s>
+        <wrap-t><use>texture[1]/wrap-t</use></wrap-t>
+      </texture-unit>
+      <texture-unit>
+        <unit>2</unit>
+        <type><use>texture[2]/type</use></type>
+        <image><use>texture[2]/image</use></image>
+        <filter><use>texture[2]/filter</use></filter>
+        <mag-filter><use>texture[2]/mag-filter</use></mag-filter>
+        <wrap-s><use>texture[2]/wrap-s</use></wrap-s>
+        <wrap-t><use>texture[2]/wrap-t</use></wrap-t>
+      </texture-unit>
+      <texture-unit>
+        <unit>3</unit>
+        <type><use>texture[3]/type</use></type>
+        <image><use>texture[3]/image</use></image>
+        <filter><use>texture[3]/filter</use></filter>
+        <mag-filter><use>texture[3]/mag-filter</use></mag-filter>
+        <wrap-s><use>texture[3]/wrap-s</use></wrap-s>
+        <wrap-t><use>texture[3]/wrap-t</use></wrap-t>
+      </texture-unit>
+      <texture-unit>
+        <unit>4</unit>
+        <type><use>texture[4]/type</use></type>
+        <image><use>texture[4]/image</use></image>
+        <filter><use>texture[4]/filter</use></filter>
+        <mag-filter><use>texture[4]/mag-filter</use></mag-filter>
+        <wrap-s><use>texture[4]/wrap-s</use></wrap-s>
+        <wrap-t><use>texture[4]/wrap-t</use></wrap-t>
+      </texture-unit>
+      <texture-unit>
+        <unit>8</unit>
+        <image><use>texture[8]/image</use></image>
+		<type><use>texture[8]/type</use></type>
+        <filter><use>texture[8]/filter</use></filter>
+        <mag-filter><use>texture[8]/mag-filter</use></mag-filter>
+        <wrap-s><use>texture[8]/wrap-s</use></wrap-s>
+        <wrap-t><use>texture[8]/wrap-t</use></wrap-t>
+        <internal-format><use>texture[8]/internal-format</use></internal-format>
+      </texture-unit>
+      <blend><use>blend</use></blend>
+      <rendering-hint>transparent</rendering-hint>
+      <cull-face><use>cull-face</use></cull-face>
+      <program>
+        <vertex-shader>Shaders/HDR/geometry-pbr-transparent.vert</vertex-shader>
+        <fragment-shader>Shaders/HDR/geometry-pbr-transparent.frag</fragment-shader>
+        <fragment-shader>Shaders/HDR/gbuffer-include.frag</fragment-shader>
+        <fragment-shader>Shaders/HDR/lighting-include.frag</fragment-shader>
+        <fragment-shader>Shaders/HDR/aerial-perspective-include.frag</fragment-shader>
+        <attribute>
+          <name>tangent</name>
+          <index>6</index>
+        </attribute>
+        <attribute>
+          <name>binormal</name>
+          <index>7</index>
+        </attribute>
+      </program>
+      <uniform>
+        <name>base_color_tex</name>
+        <type>sampler-2d</type>
+        <value type="int">0</value>
+      </uniform>
+      <uniform>
+        <name>normal_tex</name>
+        <type>sampler-2d</type>
+        <value type="int">1</value>
+      </uniform>
+      <uniform>
+        <name>metallic_roughness_tex</name>
+        <type>sampler-2d</type>
+        <value type="int">2</value>
+      </uniform>
+      <uniform>
+        <name>occlusion_tex</name>
+        <type>sampler-2d</type>
+        <value type="int">3</value>
+      </uniform>
+      <uniform>
+        <name>emissive_tex</name>
+        <type>sampler-2d</type>
+        <value type="int">4</value>
+      </uniform>
+      <uniform>
+        <name>base_color_factor</name>
+        <type>float-vec4</type>
+        <value><use>base-color-factor</use></value>
+      </uniform>
+      <uniform>
+        <name>metallic_factor</name>
+        <type>float</type>
+        <value><use>metallic-factor</use></value>
+      </uniform>
+      <uniform>
+        <name>roughness_factor</name>
+        <type>float</type>
+        <value><use>roughness-factor</use></value>
+      </uniform>
+      <uniform>
+        <name>emissive_factor</name>
+        <type>float-vec3</type>
+        <value><use>emissive-factor</use></value>
+      </uniform>
+      <uniform>
+        <name>flip_vertically</name>
+        <type>bool</type>
+        <value><use>flip-vertically</use></value>
+      </uniform>
+      <uniform>
+        <name>alpha_cutoff</name>
+        <type>float</type>
+        <value><use>alpha-cutoff</use></value>
+      </uniform>
+      <!-- Lighting include -->
+      <uniform>
+        <name>dfg_lut</name>
+        <type>sampler-2d</type>
+        <value type="int">8</value>
+      </uniform>
+      <uniform>
+        <name>prefiltered_envmap</name>
+        <type>sampler-cube</type>
+        <value type="int">9</value>
+      </uniform>
+      <uniform>
+        <name>shadow_tex</name>
+        <type>sampler-2d-shadow</type>
+        <value type="int">10</value>
+      </uniform>
+      <!-- Aerial perspective include -->
+      <uniform>
+        <name>aerial_perspective_lut</name>
+        <type>sampler-2d</type>
+        <value type="int">11</value>
+      </uniform>
+      <uniform>
+        <name>transmittance_lut</name>
+        <type>sampler-2d</type>
+        <value type="int">12</value>
+      </uniform>
+    </pass>
+  </technique>
+</PropertyList>
diff --git a/Effects/model-pbr.eff b/Effects/model-pbr.eff
index 5ea62b6e7..a000eea4e 100644
--- a/Effects/model-pbr.eff
+++ b/Effects/model-pbr.eff
@@ -27,12 +27,6 @@
       <type>white</type>
     </texture>
     <emissive-factor type="vec3d">0.0 0.0 0.0</emissive-factor>
-    <!-- Alpha Coverage -->
-    <blend>
-      <active>false</active>
-    </blend>
-    <rendering-hint>opaque</rendering-hint>
-    <alpha-cutoff>-1.0</alpha-cutoff>
     <!-- Double Sided -->
     <cull-face>back</cull-face>
     <!-- Whether to flip the texture vertically -->
@@ -98,8 +92,8 @@
         <wrap-s><use>texture[4]/wrap-s</use></wrap-s>
         <wrap-t><use>texture[4]/wrap-t</use></wrap-t>
       </texture-unit>
-      <blend><active><use>blend/active</use></active></blend>
-      <rendering-hint><use>rendering-hint</use></rendering-hint>
+      <blend>0</blend>
+      <rendering-hint>opaque</rendering-hint>
       <cull-face><use>cull-face</use></cull-face>
       <program>
         <vertex-shader>Shaders/HDR/geometry-pbr.vert</vertex-shader>
@@ -159,11 +153,6 @@
         <type>float-vec3</type>
         <value><use>emissive-factor</use></value>
       </uniform>
-      <uniform>
-        <name>alpha_cutoff</name>
-        <type>float</type>
-        <value><use>alpha-cutoff</use></value>
-      </uniform>
       <uniform>
         <name>flip_vertically</name>
         <type>bool</type>
diff --git a/Effects/skydome.eff b/Effects/skydome.eff
index f853cf769..03de89cc0 100644
--- a/Effects/skydome.eff
+++ b/Effects/skydome.eff
@@ -320,12 +320,12 @@
         <value type="bool">true</value>
       </uniform>
       <uniform>
-        <name>sky_view_lut</name>
+        <name>transmittance_lut</name>
         <type>sampler-2d</type>
-        <value type="int">11</value>
+        <value type="int">12</value>
       </uniform>
       <uniform>
-        <name>transmittance_lut</name>
+        <name>sky_view_lut</name>
         <type>sampler-2d</type>
         <value type="int">13</value>
       </uniform>
@@ -345,12 +345,12 @@
         <value type="bool">false</value>
       </uniform>
       <uniform>
-        <name>sky_view_lut</name>
+        <name>transmittance_lut</name>
         <type>sampler-2d</type>
-        <value type="int">11</value>
+        <value type="int">12</value>
       </uniform>
       <uniform>
-        <name>transmittance_lut</name>
+        <name>sky_view_lut</name>
         <type>sampler-2d</type>
         <value type="int">13</value>
       </uniform>
diff --git a/Shaders/HDR/aerial-perspective-include.frag b/Shaders/HDR/aerial-perspective-include.frag
new file mode 100644
index 000000000..13062e63d
--- /dev/null
+++ b/Shaders/HDR/aerial-perspective-include.frag
@@ -0,0 +1,73 @@
+#version 330 core
+
+uniform sampler2D aerial_perspective_lut;
+uniform sampler2D transmittance_lut;
+
+uniform float fg_SunZenithCosTheta;
+uniform float fg_CameraDistanceToEarthCenter;
+uniform float fg_EarthRadius;
+
+const float AERIAL_SLICES = 32.0;
+const float AERIAL_LUT_TILE_SIZE = 1.0 / AERIAL_SLICES;
+const float AERIAL_LUT_TEXEL_SIZE = 1.0 / 1024.0;
+const float AERIAL_MAX_DEPTH = 128000.0;
+const vec3 EXTRATERRESTRIAL_SOLAR_ILLUMINANCE = vec3(128.0);
+
+const float ATMOSPHERE_RADIUS = 6471e3;
+
+vec4 sampleAerialPerspectiveSlice(vec2 coord, int slice)
+{
+    // Sample at the pixel center
+    float offset = slice * AERIAL_LUT_TILE_SIZE + AERIAL_LUT_TEXEL_SIZE * 0.5;
+    float x = coord.x * (AERIAL_LUT_TILE_SIZE - AERIAL_LUT_TEXEL_SIZE) + offset;
+    return texture(aerial_perspective_lut, vec2(x, coord.y));
+}
+
+vec4 sampleAerialPerspective(vec2 coord, float depth)
+{
+    vec4 color;
+    // Map to [0,1]
+    float w = depth / AERIAL_MAX_DEPTH;
+    // Squared distribution
+    w = sqrt(clamp(w, 0.0, 1.0));
+    w *= AERIAL_SLICES;
+    if (w <= 1.0) {
+        // Handle special case of fragments behind the first slice
+        color = mix(vec4(0.0, 0.0, 0.0, 1.0),
+                    sampleAerialPerspectiveSlice(coord, 0),
+                    w);
+    } else {
+        w -= 1.0;
+        // Manually interpolate between slices
+        color = mix(sampleAerialPerspectiveSlice(coord, int(floor(w))),
+                    sampleAerialPerspectiveSlice(coord, int(ceil(w))),
+                    sqrt(fract(w)));
+    }
+    return color;
+}
+
+vec3 addAerialPerspective(vec3 color, vec2 coord, float depth)
+{
+    vec4 aerialPerspective = sampleAerialPerspective(coord, depth);
+    return color * aerialPerspective.a + aerialPerspective.rgb
+        * EXTRATERRESTRIAL_SOLAR_ILLUMINANCE;
+}
+
+/**
+ * Get the illuminance of the Sun for a surface perpendicular to the Sun
+ * direction. The illuminance is calculated at the altitude of the viewer,
+ * which might or might not be correct in certain circumstances. If the object
+ * being illuminated is not too far from the viewer it's a good enough
+ * approximation.
+ */
+vec3 getSunIntensity()
+{
+    float normalizedHeight = (fg_CameraDistanceToEarthCenter - fg_EarthRadius)
+        / (ATMOSPHERE_RADIUS - fg_EarthRadius);
+
+    vec2 coord = vec2(fg_SunZenithCosTheta * 0.5 + 0.5,
+                       clamp(normalizedHeight, 0.0, 1.0));
+    vec3 transmittance = texture(transmittance_lut, coord).rgb;
+
+    return EXTRATERRESTRIAL_SOLAR_ILLUMINANCE * transmittance;
+}
diff --git a/Shaders/HDR/geometry-pbr-transparent.frag b/Shaders/HDR/geometry-pbr-transparent.frag
new file mode 100644
index 000000000..0de99bbe3
--- /dev/null
+++ b/Shaders/HDR/geometry-pbr-transparent.frag
@@ -0,0 +1,110 @@
+#version 330 core
+
+out vec4 fragColor;
+
+in vec2 texCoord;
+in mat3 TBN;
+in vec3 ecPos;
+
+uniform sampler2D base_color_tex;
+uniform sampler2D normal_tex;
+uniform sampler2D metallic_roughness_tex;
+uniform sampler2D occlusion_tex;
+uniform sampler2D emissive_tex;
+uniform vec4 base_color_factor;
+uniform float metallic_factor;
+uniform float roughness_factor;
+uniform vec3 emissive_factor;
+uniform float alpha_cutoff;
+
+uniform mat4 osg_ViewMatrixInverse;
+
+uniform vec4 fg_Viewport;
+uniform vec3 fg_SunDirection;
+
+vec3 decodeSRGB(vec3 screenRGB);
+vec3 getF0Reflectance(vec3 baseColor, float metallic);
+float getShadowing(vec3 p, vec3 n, float NdotL);
+vec3 evaluateLight(
+    vec3 baseColor,
+    float metallic,
+    float roughness,
+    float clearcoat,
+    float clearcoatRoughness,
+    vec3 f0,
+    vec3 intensity,
+    float occlusion,
+    vec3 n,
+    vec3 l,
+    vec3 v,
+    float NdotL,
+    float NdotV);
+vec3 evaluateIBL(
+    vec3 baseColor,
+    float metallic,
+    float roughness,
+    vec3 f0,
+    float occlusion,
+    vec3 nWorldSpace,
+    float NdotV,
+    vec3 reflected);
+vec3 addAerialPerspective(vec3 color, vec2 coord, float depth);
+vec3 getSunIntensity();
+
+void main()
+{
+    vec4 baseColorTexel = texture(base_color_tex, texCoord);
+    vec4 baseColor = vec4(decodeSRGB(baseColorTexel.rgb), baseColorTexel.a)
+        * base_color_factor;
+    if (baseColor.a < alpha_cutoff)
+        discard;
+
+    float occlusion = texture(occlusion_tex, texCoord).r;
+    vec3 n = texture(normal_tex, texCoord).rgb * 2.0 - 1.0;
+    n = normalize(TBN * n);
+
+    vec4 metallicRoughness = texture(metallic_roughness_tex, texCoord);
+    float metallic = metallicRoughness.r * metallic_factor;
+    float roughness = metallicRoughness.g * roughness_factor;
+
+    vec3 emissive = texture(emissive_tex, texCoord).rgb * emissive_factor;
+
+    vec3 v = normalize(-ecPos);
+    vec3 l = fg_SunDirection;
+
+    float NdotL = dot(n, l);
+    float NdotV = clamp(abs(dot(n, v)), 0.001, 1.0);
+
+    vec3 f0 = getF0Reflectance(baseColor.rgb, metallic);
+
+    vec3 sunIlluminance = getSunIntensity() * clamp(NdotL, 0.0, 1.0);
+    float shadowFactor = getShadowing(ecPos, n, NdotL);
+
+    vec3 color = evaluateLight(baseColor.rgb,
+                               metallic,
+                               roughness,
+                               0.0,
+                               0.0,
+                               f0,
+                               sunIlluminance,
+                               shadowFactor,
+                               n, l, v,
+                               NdotL, NdotV);
+
+    vec3 worldNormal = (osg_ViewMatrixInverse * vec4(n, 0.0)).xyz;
+    vec3 worldReflected = (osg_ViewMatrixInverse * vec4(reflect(-v, n), 0.0)).xyz;
+
+    color += evaluateIBL(baseColor.rgb,
+                         metallic,
+                         roughness,
+                         f0,
+                         occlusion,
+                         worldNormal,
+                         NdotV,
+                         worldReflected);
+
+    vec2 coord = (gl_FragCoord.xy - fg_Viewport.xy) / fg_Viewport.zw;
+    color = addAerialPerspective(color, coord, length(ecPos));
+
+    fragColor = vec4(color, baseColor.a);
+}
diff --git a/Shaders/HDR/geometry-pbr-transparent.vert b/Shaders/HDR/geometry-pbr-transparent.vert
new file mode 100644
index 000000000..23179eb19
--- /dev/null
+++ b/Shaders/HDR/geometry-pbr-transparent.vert
@@ -0,0 +1,32 @@
+#version 330 core
+
+layout(location = 0) in vec4 pos;
+layout(location = 1) in vec3 normal;
+layout(location = 3) in vec4 multiTexCoord0;
+layout(location = 6) in vec3 tangent;
+layout(location = 7) in vec3 binormal;
+
+out vec2 texCoord;
+out mat3 TBN;
+out vec3 ecPos;
+
+uniform mat4 osg_ModelViewMatrix;
+uniform mat4 osg_ModelViewProjectionMatrix;
+uniform mat3 osg_NormalMatrix;
+
+uniform bool flip_vertically;
+
+void main()
+{
+    gl_Position = osg_ModelViewProjectionMatrix * pos;
+    texCoord = multiTexCoord0.st;
+    if (flip_vertically)
+        texCoord.y = 1.0 - texCoord.y;
+
+    vec3 T = normalize(osg_NormalMatrix * tangent);
+    vec3 B = normalize(osg_NormalMatrix * binormal);
+    vec3 N = normalize(osg_NormalMatrix * normal);
+    TBN = mat3(T, B, N);
+
+    ecPos = (osg_ModelViewMatrix * pos).xyz;
+}
diff --git a/Shaders/HDR/geometry-pbr.frag b/Shaders/HDR/geometry-pbr.frag
index ce486ab5e..7f9b70116 100644
--- a/Shaders/HDR/geometry-pbr.frag
+++ b/Shaders/HDR/geometry-pbr.frag
@@ -16,7 +16,6 @@ uniform vec4 base_color_factor;
 uniform float metallic_factor;
 uniform float roughness_factor;
 uniform vec3 emissive_factor;
-uniform float alpha_cutoff;
 
 vec2 encodeNormal(vec3 n);
 vec3 decodeSRGB(vec3 screenRGB);
@@ -26,8 +25,6 @@ void main()
     vec4 baseColorTexel = texture(base_color_tex, texCoord);
     vec4 baseColor = vec4(decodeSRGB(baseColorTexel.rgb), baseColorTexel.a)
         * base_color_factor;
-    if (baseColor.a < alpha_cutoff)
-        discard;
     gbuffer0.rgb = baseColor.rgb;
 
     float occlusion = texture(occlusion_tex, texCoord).r;
diff --git a/Shaders/HDR/lighting-include.frag b/Shaders/HDR/lighting-include.frag
new file mode 100644
index 000000000..816f2120a
--- /dev/null
+++ b/Shaders/HDR/lighting-include.frag
@@ -0,0 +1,354 @@
+#version 330 core
+
+uniform sampler2D dfg_lut;
+uniform samplerCube prefiltered_envmap;
+uniform sampler2DShadow shadow_tex;
+
+uniform mat4 fg_LightMatrix_csm0;
+uniform mat4 fg_LightMatrix_csm1;
+uniform mat4 fg_LightMatrix_csm2;
+uniform mat4 fg_LightMatrix_csm3;
+
+// Shadow mapping constants
+const int sun_atlas_size = 8192;
+const float DEPTH_BIAS = 2.0;
+const float BAND_SIZE = 0.1;
+const vec2 BAND_BOTTOM_LEFT = vec2(BAND_SIZE);
+const vec2 BAND_TOP_RIGHT   = vec2(1.0 - BAND_SIZE);
+// Ideally these should be passed as an uniform, but we don't support uniform
+// arrays yet
+const vec2 uv_shifts[4] = vec2[4](
+    vec2(0.0, 0.0), vec2(0.5, 0.0),
+    vec2(0.0, 0.5), vec2(0.5, 0.5));
+const vec2 uv_factor = vec2(0.5, 0.5);
+
+// BRDF constants
+const float PI = 3.14159265359;
+const float RECIPROCAL_PI = 0.31830988618;
+const float DIELECTRIC_SPECULAR = 0.04;
+const float MAX_PREFILTERED_LOD = 4.0;
+
+//------------------------------------------------------------------------------
+// Shadow mapping related stuff
+
+float sampleOffset(vec4 pos, vec2 offset, vec2 invTexelSize)
+{
+    return texture(
+        shadow_tex, vec3(
+            pos.xy + offset * invTexelSize,
+            pos.z - DEPTH_BIAS * invTexelSize));
+}
+
+// OptimizedPCF from https://github.com/TheRealMJP/Shadows
+// Original by Ignacio Castaño for The Witness
+// Released under The MIT License
+float sampleOptimizedPCF(vec4 pos)
+{
+    vec2 invTexSize = vec2(1.0 / float(sun_atlas_size));
+
+    vec2 uv = pos.xy * sun_atlas_size;
+    vec2 base_uv = floor(uv + 0.5);
+    float s = (uv.x + 0.5 - base_uv.x);
+    float t = (uv.y + 0.5 - base_uv.y);
+    base_uv -= vec2(0.5);
+    base_uv *= invTexSize;
+    pos.xy = base_uv.xy;
+
+    float sum = 0.0;
+
+    float uw0 = (4.0 - 3.0 * s);
+    float uw1 = 7.0;
+    float uw2 = (1.0 + 3.0 * s);
+
+    float u0 = (3.0 - 2.0 * s) / uw0 - 2.0;
+    float u1 = (3.0 + s) / uw1;
+    float u2 = s / uw2 + 2.0;
+
+    float vw0 = (4.0 - 3.0 * t);
+    float vw1 = 7.0;
+    float vw2 = (1.0 + 3.0 * t);
+
+    float v0 = (3.0 - 2.0 * t) / vw0 - 2.0;
+    float v1 = (3.0 + t) / vw1;
+    float v2 = t / vw2 + 2.0;
+
+    sum += uw0 * vw0 * sampleOffset(pos, vec2(u0, v0), invTexSize);
+    sum += uw1 * vw0 * sampleOffset(pos, vec2(u1, v0), invTexSize);
+    sum += uw2 * vw0 * sampleOffset(pos, vec2(u2, v0), invTexSize);
+
+    sum += uw0 * vw1 * sampleOffset(pos, vec2(u0, v1), invTexSize);
+    sum += uw1 * vw1 * sampleOffset(pos, vec2(u1, v1), invTexSize);
+    sum += uw2 * vw1 * sampleOffset(pos, vec2(u2, v1), invTexSize);
+
+    sum += uw0 * vw2 * sampleOffset(pos, vec2(u0, v2), invTexSize);
+    sum += uw1 * vw2 * sampleOffset(pos, vec2(u1, v2), invTexSize);
+    sum += uw2 * vw2 * sampleOffset(pos, vec2(u2, v2), invTexSize);
+
+    return sum / 144.0;
+}
+
+float sampleCascade(vec4 p, vec2 shift)
+{
+    vec4 pos = p;
+    pos.xy *= uv_factor;
+    pos.xy += shift;
+    return sampleOptimizedPCF(pos);
+}
+
+float sampleAndBlendBand(vec4 p1, vec4 p2, vec2 s1, vec2 s2)
+{
+    vec2 s = smoothstep(vec2(0.0), BAND_BOTTOM_LEFT, p1.xy)
+        - smoothstep(BAND_TOP_RIGHT, vec2(1.0), p1.xy);
+    float blend = 1.0 - s.x * s.y;
+    return mix(sampleCascade(p1, s1),
+               sampleCascade(p2, s2),
+               blend);
+}
+
+bool checkWithinBounds(vec2 coords, vec2 bottomLeft, vec2 topRight)
+{
+    vec2 r = step(bottomLeft, coords) - step(topRight, coords);
+    return bool(r.x * r.y);
+}
+
+bool isInsideCascade(vec4 p)
+{
+    return checkWithinBounds(p.xy, vec2(0.0), vec2(1.0)) && ((p.z / p.w) <= 1.0);
+}
+
+bool isInsideBand(vec4 p)
+{
+    return !checkWithinBounds(p.xy, BAND_BOTTOM_LEFT, BAND_TOP_RIGHT);
+}
+
+/**
+ * Get the light space position of point p.
+ * Both p and n must be in view space. The light matrix is also assumed to
+ * transform from view space to light space.
+ */
+vec4 getLightSpacePosition(vec3 p, vec3 n, float NdotL, float bias,
+                           mat4 lightMatrix)
+{
+    float sinTheta = sqrt(1.0 - NdotL * NdotL);
+    vec3 offset = p + n * (sinTheta * bias);
+    return lightMatrix * vec4(offset, 1.0);
+}
+
+/**
+ * Get shadowing factor for a given position. 1.0 corresponds to a fragment
+ * being completely lit, and 0.0 to a fragment being completely in shadow.
+ * Both p and n must be in view space.
+ */
+float getShadowing(vec3 p, vec3 n, float NdotL)
+{
+    // Ignore fragments that don't face the light
+    if (NdotL <= 0.0)
+        return 0.0;
+
+    float shadow = 1.0;
+
+    vec4 lightSpacePos[4];
+    lightSpacePos[0] = getLightSpacePosition(p, n, NdotL, 0.05, fg_LightMatrix_csm0);
+    lightSpacePos[1] = getLightSpacePosition(p, n, NdotL, 0.2, fg_LightMatrix_csm1);
+    lightSpacePos[2] = getLightSpacePosition(p, n, NdotL, 1.0, fg_LightMatrix_csm2);
+    lightSpacePos[3] = getLightSpacePosition(p, n, NdotL, 5.0, fg_LightMatrix_csm3);
+
+    for (int i = 0; i < 4; ++i) {
+        // Map-based cascade selection
+        // We test if we are inside the cascade bounds to find the tightest
+        // map that contains the fragment.
+        if (isInsideCascade(lightSpacePos[i])) {
+            if (isInsideBand(lightSpacePos[i]) && ((i+1) < 4)) {
+                // Blend between cascades if the fragment is near the
+                // next cascade to avoid abrupt transitions.
+                shadow = clamp(sampleAndBlendBand(lightSpacePos[i],
+                                                  lightSpacePos[i+1],
+                                                  uv_shifts[i],
+                                                  uv_shifts[i+1]),
+                               0.0, 1.0);
+            } else {
+                // We are far away from the borders of the cascade, so
+                // we skip the blending to avoid the performance cost
+                // of sampling the shadow map twice.
+                shadow = clamp(sampleCascade(lightSpacePos[i], uv_shifts[i]),
+                               0.0, 1.0);
+            }
+            break;
+        }
+    }
+
+    return shadow;
+}
+
+//------------------------------------------------------------------------------
+// BRDF utility functions
+
+/**
+ * Fresnel term with included roughness to get a pleasant visual result.
+ * See https://seblagarde.wordpress.com/2011/08/17/hello-world/
+ */
+vec3 F_SchlickRoughness(float NdotV, vec3 F0, float r)
+{
+    return F0 + (max(vec3(1.0 - r), F0) - F0) * pow(max(1.0 - NdotV, 0.0), 5.0);
+}
+
+/**
+ * Fresnel (specular F)
+ * Schlick's approximation for the Cook-Torrance BRDF.
+ */
+vec3 F_Schlick(float VdotH, vec3 F0)
+{
+    return F0 + (vec3(1.0) - F0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0);
+}
+
+/**
+ * Normal distribution function (NDF) (specular D)
+ * Trowbridge-Reitz/GGX microfacet distribution. Includes Disney's
+ * reparametrization of a=roughness*roughness
+ */
+float D_GGX(float NdotH, float a2)
+{
+    float f = (NdotH * a2 - NdotH) * NdotH + 1.0;
+    return a2 / (PI * f * f);
+}
+
+/**
+ * Geometric attenuation (specular G)
+ * Smith-GGX formulation.
+ */
+float G_SmithGGX(float NdotV, float NdotL, float a2)
+{
+    float attV = 2.0 * NdotV / (NdotV + sqrt(a2 + (1.0 - a2) * (NdotV * NdotV)));
+    float attL = 2.0 * NdotL / (NdotL + sqrt(a2 + (1.0 - a2) * (NdotL * NdotL)));
+    return attV * attL;
+}
+
+/**
+ * Basic Lambertian diffuse BRDF
+ */
+vec3 Fd_Lambert(vec3 c_diff)
+{
+    return c_diff * RECIPROCAL_PI;
+}
+
+/**
+ * Get the fresnel reflectance at 0 degrees (light hitting the surface
+ * perpendicularly).
+ */
+vec3 getF0Reflectance(vec3 baseColor, float metallic)
+{
+    return mix(vec3(DIELECTRIC_SPECULAR), baseColor, metallic);
+}
+
+//------------------------------------------------------------------------------
+// IBL evaluation
+
+/**
+ * Indirect diffuse irradiance
+ * To get better results we should be precomputing the irradiance into a cubemap
+ * or calculating spherical harmonics coefficients on the CPU.
+ * Sampling the roughness=1 mipmap level of the prefiltered specular map
+ * works too. :)
+ */
+vec3 evaluateDiffuseIrradianceIBL(vec3 n)
+{
+    int roughnessOneLevel = int(MAX_PREFILTERED_LOD);
+    ivec2 s = textureSize(prefiltered_envmap, roughnessOneLevel);
+    float du = 1.0 / float(s.x);
+    float dv = 1.0 / float(s.y);
+    vec3 m0 = normalize(cross(n, vec3(0.0, 1.0, 0.0)));
+    vec3 m1 = cross(m0, n);
+    vec3 m0du = m0 * du;
+    vec3 m1dv = m1 * dv;
+
+    vec3 c;
+    c  = textureLod(prefiltered_envmap, n - m0du - m1dv, roughnessOneLevel).rgb;
+    c += textureLod(prefiltered_envmap, n + m0du - m1dv, roughnessOneLevel).rgb;
+    c += textureLod(prefiltered_envmap, n + m0du + m1dv, roughnessOneLevel).rgb;
+    c += textureLod(prefiltered_envmap, n - m0du + m1dv, roughnessOneLevel).rgb;
+    return c * 0.25;
+}
+
+/**
+ * Indirect specular (ambient specular)
+ * Sample from the prefiltered environment map.
+ */
+vec3 evaluateSpecularIBL(float NdotV, vec3 reflected, float roughness, vec3 f)
+{
+    vec3 prefilteredColor = textureLod(prefiltered_envmap,
+                                       reflected,
+                                       roughness * MAX_PREFILTERED_LOD).rgb;
+    vec2 envBRDF = texture(dfg_lut, vec2(NdotV, roughness)).rg;
+    return prefilteredColor * (f * envBRDF.x + envBRDF.y);
+}
+
+vec3 evaluateIBL(
+    vec3 baseColor,
+    float metallic,
+    float roughness,
+    vec3 f0,                  // Use getF0Reflectance() to obtain this
+    float occlusion,
+    vec3 nWorldSpace,         // Normal in world space
+    float NdotV,              // Must be positive and non-zero
+    vec3 reflected            // Reflected vector in world space: reflect(-v, n)
+    )
+{
+    vec3 f = F_SchlickRoughness(NdotV, f0, roughness);
+
+    vec3 specular = evaluateSpecularIBL(NdotV, reflected, roughness, f);
+    vec3 diffuse = evaluateDiffuseIrradianceIBL(nWorldSpace) * baseColor
+        * (vec3(1.0) - f) * (1.0 - metallic);
+
+    return (diffuse + specular) * occlusion;
+}
+
+//------------------------------------------------------------------------------
+// Analytical light source evaluation
+
+vec3 evaluateLight(
+    vec3 baseColor,
+    float metallic,
+    float roughness,
+    float clearcoat,
+    float clearcoatRoughness,
+    vec3 f0,                  // Use getF0Reflectance() to obtain this
+    vec3 intensity,
+    float occlusion,
+    vec3 n,
+    vec3 l,
+    vec3 v,
+    float NdotL,              // Must not be clamped to [0,1]
+    float NdotV               // Must be positive and non-zero
+    )
+{
+    // Skip fragments that are completely occluded or that are not facing the light
+    if (occlusion <= 0.0 || NdotL <= 0.0)
+        return vec3(0.0);
+
+    NdotL = clamp(NdotL, 0.001, 1.0);
+
+    vec3 h = normalize(v + l);
+    float NdotH = clamp(dot(n, h), 0.0, 1.0);
+    float VdotH = clamp(dot(v, h), 0.0, 1.0);
+
+    vec3 c_diff = mix(baseColor * (1.0 - DIELECTRIC_SPECULAR), vec3(0.0), metallic);
+
+    float a = roughness * roughness;
+    float a2 = a * a;
+
+    vec3 F = F_Schlick(VdotH, f0);
+    float D = D_GGX(NdotH, a2);
+    float G = G_SmithGGX(NdotV, NdotL, a2);
+
+    // Diffuse term
+    // Lambertian diffuse model
+    vec3 diffuse = (vec3(1.0) - F) * Fd_Lambert(c_diff);
+    // Specular term
+    // Cook-Torrance specular microfacet model
+    vec3 specular = (F * D * G) / (4.0 * NdotV * NdotL);
+
+    vec3 material = diffuse + specular;
+
+    vec3 color = material * intensity * occlusion;
+    return color;
+}
diff --git a/Shaders/HDR/lighting.frag b/Shaders/HDR/lighting.frag
index 5de8a01ae..4f834118c 100644
--- a/Shaders/HDR/lighting.frag
+++ b/Shaders/HDR/lighting.frag
@@ -9,376 +9,39 @@ uniform sampler2D gbuffer1_tex;
 uniform sampler2D gbuffer2_tex;
 uniform sampler2D depth_tex;
 uniform sampler2D ao_tex;
-uniform samplerCube prefiltered_envmap;
-uniform sampler2DShadow shadow_tex;
-uniform sampler2D dfg_lut;
-uniform sampler2D aerial_perspective_lut;
-uniform sampler2D transmittance_lut;
 
-uniform mat4 fg_ViewMatrix;
 uniform mat4 fg_ViewMatrixInverse;
 uniform vec3 fg_SunDirection;
-uniform vec3 fg_SunDirectionWorld;
-uniform vec3 fg_CameraPositionCart;
-uniform vec3 fg_CameraPositionGeod;
-
-uniform mat4 fg_LightMatrix_csm0;
-uniform mat4 fg_LightMatrix_csm1;
-uniform mat4 fg_LightMatrix_csm2;
-uniform mat4 fg_LightMatrix_csm3;
-
-const float PI = 3.14159265359;
-const float RECIPROCAL_PI = 0.31830988618;
-
-const int sun_atlas_size = 8192;
-
-const float DEPTH_BIAS = 2.0;
-const float BAND_SIZE = 0.1;
-const vec2 BAND_BOTTOM_LEFT = vec2(BAND_SIZE);
-const vec2 BAND_TOP_RIGHT   = vec2(1.0 - BAND_SIZE);
-
-// Ideally these should be passed as an uniform, but we don't support uniform
-// arrays yet
-const vec2 uv_shifts[4] = vec2[4](
-    vec2(0.0, 0.0), vec2(0.5, 0.0),
-    vec2(0.0, 0.5), vec2(0.5, 0.5));
-const vec2 uv_factor = vec2(0.5, 0.5);
-
-const float AERIAL_SLICES = 32.0;
-const float AERIAL_LUT_TILE_SIZE = 1.0 / AERIAL_SLICES;
-const float AERIAL_LUT_TEXEL_SIZE = 1.0 / 1024.0;
-const float AERIAL_MAX_DEPTH = 128000.0;
-
-const float MAX_PREFILTERED_LOD = 4.0;
-
-const float ATMOSPHERE_RADIUS = 6471e3;
-const vec3 EXTRATERRESTRIAL_SOLAR_ILLUMINANCE = vec3(128.0);
 
 vec3 decodeNormal(vec2 enc);
 vec3 positionFromDepth(vec2 pos, float depth);
-
-//------------------------------------------------------------------------------
-// Shadow mapping related stuff
-
-float sampleOffset(vec4 pos, vec2 offset, vec2 invTexelSize)
-{
-    return texture(
-        shadow_tex, vec3(
-            pos.xy + offset * invTexelSize,
-            pos.z - DEPTH_BIAS * invTexelSize));
-}
-
-// OptimizedPCF from https://github.com/TheRealMJP/Shadows
-// Original by Ignacio Castaño for The Witness
-// Released under The MIT License
-float sampleOptimizedPCF(vec4 pos)
-{
-    vec2 invTexSize = vec2(1.0 / float(sun_atlas_size));
-
-    vec2 uv = pos.xy * sun_atlas_size;
-    vec2 base_uv = floor(uv + 0.5);
-    float s = (uv.x + 0.5 - base_uv.x);
-    float t = (uv.y + 0.5 - base_uv.y);
-    base_uv -= vec2(0.5);
-    base_uv *= invTexSize;
-    pos.xy = base_uv.xy;
-
-    float sum = 0.0;
-
-    float uw0 = (4.0 - 3.0 * s);
-    float uw1 = 7.0;
-    float uw2 = (1.0 + 3.0 * s);
-
-    float u0 = (3.0 - 2.0 * s) / uw0 - 2.0;
-    float u1 = (3.0 + s) / uw1;
-    float u2 = s / uw2 + 2.0;
-
-    float vw0 = (4.0 - 3.0 * t);
-    float vw1 = 7.0;
-    float vw2 = (1.0 + 3.0 * t);
-
-    float v0 = (3.0 - 2.0 * t) / vw0 - 2.0;
-    float v1 = (3.0 + t) / vw1;
-    float v2 = t / vw2 + 2.0;
-
-    sum += uw0 * vw0 * sampleOffset(pos, vec2(u0, v0), invTexSize);
-    sum += uw1 * vw0 * sampleOffset(pos, vec2(u1, v0), invTexSize);
-    sum += uw2 * vw0 * sampleOffset(pos, vec2(u2, v0), invTexSize);
-
-    sum += uw0 * vw1 * sampleOffset(pos, vec2(u0, v1), invTexSize);
-    sum += uw1 * vw1 * sampleOffset(pos, vec2(u1, v1), invTexSize);
-    sum += uw2 * vw1 * sampleOffset(pos, vec2(u2, v1), invTexSize);
-
-    sum += uw0 * vw2 * sampleOffset(pos, vec2(u0, v2), invTexSize);
-    sum += uw1 * vw2 * sampleOffset(pos, vec2(u1, v2), invTexSize);
-    sum += uw2 * vw2 * sampleOffset(pos, vec2(u2, v2), invTexSize);
-
-    return sum / 144.0;
-}
-
-float sampleCascade(vec4 p, vec2 shift)
-{
-    vec4 pos = p;
-    pos.xy *= uv_factor;
-    pos.xy += shift;
-    return sampleOptimizedPCF(pos);
-}
-
-float sampleAndBlendBand(vec4 p1, vec4 p2, vec2 s1, vec2 s2)
-{
-    vec2 s = smoothstep(vec2(0.0), BAND_BOTTOM_LEFT, p1.xy)
-        - smoothstep(BAND_TOP_RIGHT, vec2(1.0), p1.xy);
-    float blend = 1.0 - s.x * s.y;
-    return mix(sampleCascade(p1, s1),
-               sampleCascade(p2, s2),
-               blend);
-}
-
-bool checkWithinBounds(vec2 coords, vec2 bottomLeft, vec2 topRight)
-{
-    vec2 r = step(bottomLeft, coords) - step(topRight, coords);
-    return bool(r.x * r.y);
-}
-
-bool isInsideCascade(vec4 p)
-{
-    return checkWithinBounds(p.xy, vec2(0.0), vec2(1.0)) && ((p.z / p.w) <= 1.0);
-}
-
-bool isInsideBand(vec4 p)
-{
-    return !checkWithinBounds(p.xy, BAND_BOTTOM_LEFT, BAND_TOP_RIGHT);
-}
-
-/**
- * Get the light space position of point p.
- * Both p and n must be in view space. The light matrix is also assumed to
- * transform from view space to light space.
- */
-vec4 getLightSpacePosition(vec3 p, vec3 n, float NdotL, float bias,
-                           mat4 lightMatrix)
-{
-    float sinTheta = sqrt(1.0 - NdotL * NdotL);
-    vec3 offset = p + n * (sinTheta * bias);
-    return lightMatrix * vec4(offset, 1.0);
-}
-
-/**
- * Get shadowing factor for a given position. 1.0 corresponds to a fragment
- * being completely lit, and 0.0 to a fragment being completely in shadow.
- * Both p and n must be in view space.
- */
-float getShadowing(vec3 p, vec3 n, float NdotL)
-{
-    float shadow = 1.0;
-
-    vec4 lightSpacePos[4];
-    lightSpacePos[0] = getLightSpacePosition(p, n, NdotL, 0.05, fg_LightMatrix_csm0);
-    lightSpacePos[1] = getLightSpacePosition(p, n, NdotL, 0.2, fg_LightMatrix_csm1);
-    lightSpacePos[2] = getLightSpacePosition(p, n, NdotL, 1.0, fg_LightMatrix_csm2);
-    lightSpacePos[3] = getLightSpacePosition(p, n, NdotL, 5.0, fg_LightMatrix_csm3);
-
-    for (int i = 0; i < 4; ++i) {
-        // Map-based cascade selection
-        // We test if we are inside the cascade bounds to find the tightest
-        // map that contains the fragment.
-        if (isInsideCascade(lightSpacePos[i])) {
-            if (isInsideBand(lightSpacePos[i]) && ((i+1) < 4)) {
-                // Blend between cascades if the fragment is near the
-                // next cascade to avoid abrupt transitions.
-                shadow = clamp(sampleAndBlendBand(lightSpacePos[i],
-                                                  lightSpacePos[i+1],
-                                                  uv_shifts[i],
-                                                  uv_shifts[i+1]),
-                               0.0, 1.0);
-            } else {
-                // We are far away from the borders of the cascade, so
-                // we skip the blending to avoid the performance cost
-                // of sampling the shadow map twice.
-                shadow = clamp(sampleCascade(lightSpacePos[i], uv_shifts[i]),
-                               0.0, 1.0);
-            }
-            break;
-        }
-    }
-
-    return shadow;
-}
-
-//------------------------------------------------------------------------------
-// BRDF related stuff
-
-/**
- * Indirect diffuse irradiance
- * To get better results we should be precomputing the irradiance into a cubemap
- * or calculating spherical harmonics coefficients on the CPU.
- * Sampling the roughness=1 mipmap level of the prefiltered specular map
- * works too. :)
- */
-vec3 IBL_DiffuseIrradiance(vec3 n)
-{
-    vec4 worldSpaceNormal = fg_ViewMatrixInverse * vec4(n, 0.0);
-    vec3 coord = worldSpaceNormal.xyz;
-
-    int roughnessOneLevel = int(MAX_PREFILTERED_LOD);
-    ivec2 s = textureSize(prefiltered_envmap, roughnessOneLevel);
-    float du = 1.0 / float(s.x);
-    float dv = 1.0 / float(s.y);
-    vec3 m0 = normalize(cross(n, vec3(0.0, 1.0, 0.0)));
-    vec3 m1 = cross(m0, n);
-    vec3 m0du = m0 * du;
-    vec3 m1dv = m1 * dv;
-
-    vec3 c;
-    c  = textureLod(prefiltered_envmap, coord - m0du - m1dv, roughnessOneLevel).rgb;
-    c += textureLod(prefiltered_envmap, coord + m0du - m1dv, roughnessOneLevel).rgb;
-    c += textureLod(prefiltered_envmap, coord + m0du + m1dv, roughnessOneLevel).rgb;
-    c += textureLod(prefiltered_envmap, coord - m0du + m1dv, roughnessOneLevel).rgb;
-    return c * 0.25;
-}
-
-/**
- * Indirect specular (ambient specular)
- * Sample from the prefiltered environment map.
- */
-vec3 IBL_Specular(vec3 n, vec3 v, float NdotV, float roughness, vec3 F)
-{
-    vec4 reflectVec = vec4(reflect(-v, n), 0.0);
-    vec4 worldReflectVec = fg_ViewMatrixInverse * reflectVec;
-
-    vec3 prefilteredColor = textureLod(prefiltered_envmap,
-                                       worldReflectVec.xyz,
-                                       roughness * MAX_PREFILTERED_LOD).rgb;
-
-    vec2 envBRDF = texture(dfg_lut, vec2(NdotV, roughness)).rg;
-
-    return prefilteredColor * (F * envBRDF.x + envBRDF.y);
-}
-
-/**
- * Fresnel term with included roughness to get a pleasant visual result.
- * See https://seblagarde.wordpress.com/2011/08/17/hello-world/
- */
-vec3 F_SchlickRoughness(float NdotV, vec3 F0, float r)
-{
-    return F0 + (max(vec3(1.0 - r), F0) - F0) * pow(max(1.0 - NdotV, 0.0), 5.0);
-}
-
-/**
- * Fresnel (specular F)
- * Schlick's approximation for the Cook-Torrance BRDF.
- */
-vec3 F_Schlick(float VdotH, vec3 F0)
-{
-    return F0 + (vec3(1.0) - F0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0);
-}
-
-/**
- * Normal distribution function (NDF) (specular D)
- * Trowbridge-Reitz/GGX microfacet distribution. Includes Disney's
- * reparametrization of a=roughness*roughness
- */
-float D_GGX(float NdotH, float a2)
-{
-    float f = (NdotH * a2 - NdotH) * NdotH + 1.0;
-    return a2 / (PI * f * f);
-}
-
-/**
- * Geometric attenuation (specular G)
- * Smith-GGX formulation.
- */
-float G_SmithGGX(float NdotV, float NdotL, float a2)
-{
-    float attV = 2.0 * NdotV / (NdotV + sqrt(a2 + (1.0 - a2) * (NdotV * NdotV)));
-    float attL = 2.0 * NdotL / (NdotL + sqrt(a2 + (1.0 - a2) * (NdotL * NdotL)));
-    return attV * attL;
-}
-
-/**
- * Basic Lambertian diffuse BRDF
- */
-vec3 BRDF_Diffuse_Lambert(vec3 c_diff)
-{
-    return c_diff * RECIPROCAL_PI;
-}
-
-vec3 BRDF(in vec3 albedo, in float metalness, in float roughness,
-          in float clearcoat, in float clearcoatRoughness,
-          in float NdotL, in float NdotV, in float NdotH, in float VdotH,
-          out vec3 f0)
-{
-    const float dielectricSpecular = 0.04;
-    vec3 c_diff = mix(albedo * (1.0 - dielectricSpecular), vec3(0.0), metalness);
-    f0 = mix(vec3(dielectricSpecular), albedo, metalness);
-
-    float a = roughness * roughness;
-    float a2 = a * a;
-
-    vec3 F = F_Schlick(VdotH, f0);
-    float D = D_GGX(NdotH, a2);
-    float G = G_SmithGGX(NdotV, NdotL, a2);
-
-    // Diffuse term
-    // Lambertian diffuse model
-    vec3 f_diffuse = (vec3(1.0) - F) * BRDF_Diffuse_Lambert(c_diff);
-
-    // Specular term
-    // Cook-Torrance specular microfacet model
-    vec3 f_specular = F * D * G / (4.0 * NdotV * NdotL);
-
-    return f_diffuse + f_specular;
-}
-
-//------------------------------------------------------------------------------
-// Atmospheric scattering
-
-vec4 sampleAerialPerspectiveSlice(int slice)
-{
-    // Sample at the pixel center
-    float offset = slice * AERIAL_LUT_TILE_SIZE + AERIAL_LUT_TEXEL_SIZE * 0.5;
-    float x = texCoord.x * (AERIAL_LUT_TILE_SIZE - AERIAL_LUT_TEXEL_SIZE) + offset;
-    return texture(aerial_perspective_lut, vec2(x, texCoord.y));
-}
-
-vec4 sampleAerialPerspective(float depth)
-{
-    vec4 color;
-    // Map to [0,1]
-    float w = depth / AERIAL_MAX_DEPTH;
-    // Squared distribution
-    w = sqrt(clamp(w, 0.0, 1.0));
-    w *= AERIAL_SLICES;
-    if (w <= 1.0) {
-        // Handle special case of fragments behind the first slice
-        color = mix(vec4(0.0, 0.0, 0.0, 1.0), sampleAerialPerspectiveSlice(0), w);
-    } else {
-        w -= 1.0;
-        // Manually interpolate between slices
-        color = mix(sampleAerialPerspectiveSlice(int(floor(w))),
-                    sampleAerialPerspectiveSlice(int(ceil(w))),
-                    sqrt(fract(w)));
-    }
-    return color;
-}
-
-vec3 getSunIlluminance()
-{
-    float cameraHeight = length(fg_CameraPositionCart);
-    vec3 up = fg_CameraPositionCart / cameraHeight;
-    float cosTheta = dot(fg_SunDirectionWorld, up);
-
-    float earthRadius = cameraHeight - max(fg_CameraPositionGeod.z, 0.0);
-    float normalizedHeight = (cameraHeight - earthRadius)
-        / (ATMOSPHERE_RADIUS - earthRadius);
-
-    vec2 coords = vec2(cosTheta * 0.5 + 0.5, clamp(normalizedHeight, 0.0, 1.0));
-    vec3 transmittance = texture(transmittance_lut, coords).rgb;
-
-    return EXTRATERRESTRIAL_SOLAR_ILLUMINANCE * transmittance;
-}
-
-//------------------------------------------------------------------------------
+vec3 getF0Reflectance(vec3 baseColor, float metallic);
+float getShadowing(vec3 p, vec3 n, float NdotL);
+vec3 evaluateLight(
+    vec3 baseColor,
+    float metallic,
+    float roughness,
+    float clearcoat,
+    float clearcoatRoughness,
+    vec3 f0,
+    vec3 intensity,
+    float occlusion,
+    vec3 n,
+    vec3 l,
+    vec3 v,
+    float NdotL,
+    float NdotV);
+vec3 evaluateIBL(
+    vec3 baseColor,
+    float metallic,
+    float roughness,
+    vec3 f0,
+    float occlusion,
+    vec3 nWorldSpace,
+    float NdotV,
+    vec3 reflected);
+vec3 addAerialPerspective(vec3 color, vec2 coord, float depth);
+vec3 getSunIntensity();
 
 void main()
 {
@@ -394,42 +57,48 @@ void main()
     vec3 pos = positionFromDepth(texCoord, depth);
     vec3 v = normalize(-pos);
     vec3 n = decodeNormal(gbuffer1);
+    vec3 l = fg_SunDirection;
 
-    vec3 albedo = gbuffer0.rgb;
+    float NdotL = dot(n, l);
+    float NdotV = clamp(abs(dot(n, v)), 0.001, 1.0);
+
+    vec3 baseColor = gbuffer0.rgb;
     float cavity = gbuffer0.a;
-    float metalness = gbuffer2.r;
+    float metallic = gbuffer2.r;
     float roughness = gbuffer2.g;
     float clearcoat = gbuffer2.b;
     float clearcoatRoughness = gbuffer2.a;
 
-    vec3 l = fg_SunDirection;
-    vec3 h = normalize(v + l);
+    vec3 f0 = getF0Reflectance(baseColor, metallic);
 
-    float NdotL = clamp(dot(n, l), 0.001, 1.0);
-    float NdotV = clamp(abs(dot(n, v)), 0.001, 1.0);
-    float NdotH = clamp(dot(n, h), 0.0, 1.0);
-    float VdotH = clamp(dot(v, h), 0.0, 1.0);
-
-    vec3 f0;
-    vec3 brdf = BRDF(albedo, metalness, roughness,
-                     clearcoat, clearcoatRoughness,
-                     NdotL, NdotV, NdotH, VdotH,
-                     f0);
-
-    vec3 sunIlluminance = getSunIlluminance() * NdotL;
-
-    vec3 f = F_SchlickRoughness(NdotV, f0, roughness);
-    vec3 indirectSpecular = IBL_Specular(n, v, NdotV, roughness, f);
-    vec3 indirectDiffuse = IBL_DiffuseIrradiance(n) * albedo
-        * (vec3(1.0) - f) * (1.0 - metalness);
-
-    vec3 ambient = (indirectDiffuse + indirectSpecular) * ao * cavity;
+    vec3 sunIlluminance = getSunIntensity() * clamp(NdotL, 0.0, 1.0);
     float shadowFactor = getShadowing(pos, n, NdotL);
-    vec3 color = ambient + brdf * sunIlluminance * shadowFactor;
 
-    vec4 aerialPerspective = sampleAerialPerspective(length(pos));
-    color = color * aerialPerspective.a + aerialPerspective.rgb
-        * EXTRATERRESTRIAL_SOLAR_ILLUMINANCE;
+    vec3 color = evaluateLight(baseColor,
+                               metallic,
+                               roughness,
+                               clearcoat,
+                               clearcoatRoughness,
+                               f0,
+                               sunIlluminance,
+                               shadowFactor,
+                               n, l, v,
+                               NdotL, NdotV);
+
+    float ambientOcclusion = ao * cavity;
+    vec3 worldNormal = (fg_ViewMatrixInverse * vec4(n, 0.0)).xyz;
+    vec3 worldReflected = (fg_ViewMatrixInverse * vec4(reflect(-v, n), 0.0)).xyz;
+
+    color += evaluateIBL(baseColor,
+                         metallic,
+                         roughness,
+                         f0,
+                         ambientOcclusion,
+                         worldNormal,
+                         NdotV,
+                         worldNormal);
+
+    color = addAerialPerspective(color, texCoord, length(pos));
 
     fragHdrColor = color;
 }
diff --git a/Shaders/HDR/skydome.frag b/Shaders/HDR/skydome.frag
index 9592f62d2..0e293bd20 100644
--- a/Shaders/HDR/skydome.frag
+++ b/Shaders/HDR/skydome.frag
@@ -39,7 +39,7 @@ void main()
             float altitude = length(groundPoint);
             float scaledAltitude = altitude / 100000.0;
 
-            vec3 up = normalize(vec4(0.0, 0.0, 0.0, 1.0) - groundPoint).xyz;
+            vec3 up = normalize(vec3(0.0, 0.0, 0.0) - groundPoint.xyz);
             float sunZenithCosTheta = dot(rayDirView, up);
 
             vec2 coords = vec2(sunZenithCosTheta * 0.5 + 0.5,