in vec2 v_pos; uniform sampler2D u_image; layout(std140) uniform HillshadeTilePropsUBO { highp vec2 u_latrange; highp float u_exaggeration; highp int u_method; // Hillshade method (0: STANDARD, 1: COMBINED, 2: IGOR, 3: MULTIDIRECTIONAL, 4: BASIC) highp int u_num_lights; // Number of light sources (1-4) highp float u_pad0; highp float u_pad1; highp float u_pad2; }; layout(std140) uniform HillshadeEvaluatedPropsUBO { highp vec4 u_accent; highp vec4 u_altitudes; // Up to 4 altitude values (in radians) highp vec4 u_azimuths; // Up to 4 azimuth values (in radians) highp vec4 u_shadows[4]; // Shadow colors (up to 4 lights) highp vec4 u_highlights[4]; // Highlight colors (up to 4 lights) }; #define PI 3.141592653589793 #define STANDARD 0 #define COMBINED 1 #define IGOR 2 #define MULTIDIRECTIONAL 3 #define BASIC 4 float get_aspect(vec2 deriv) { return deriv.x != 0.0 ? atan(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); } // MapLibre's legacy hillshade algorithm (Method 0) void standard_hillshade(vec2 deriv) { float azimuth = u_azimuths.x + PI; float slope = atan(0.625 * length(deriv)); float aspect = get_aspect(deriv); // Note: This implementation uses u_exaggeration as intensity, though it typically should be derived from u_altitudes.x. float intensity = u_exaggeration; // Scale the slope exponentially based on intensity float base = 1.875 - intensity * 1.75; float maxValue = 0.5 * PI; float scaledSlope = abs(intensity - 0.5) > 1e-6 ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue : slope; float accent = cos(scaledSlope); vec4 accent_color = (1.0 - accent) * u_accent * clamp(intensity * 2.0, 0.0, 1.0); // Calculate shadow/highlight based on aspect float shade = abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); vec4 shade_color = mix(u_shadows[0], u_highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); fragColor = accent_color * (1.0 - shade_color.a) + shade_color; } // Basic directional hillshade (Method 4) void basic_hillshade(vec2 deriv) { deriv = deriv * u_exaggeration * 2.0; // Exaggerate the slope derivative float azimuth = u_azimuths.x + PI; float cos_az = cos(azimuth); float sin_az = sin(azimuth); float cos_alt = cos(u_altitudes.x); float sin_alt = sin(u_altitudes.x); // Calculate the cosine of the angle between the light vector and the surface normal float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); float shade = clamp(cang, 0.0, 1.0); // cang is the hillshade intensity [0, 1] // Blend shadow and highlight based on intensity if (shade > 0.5) { fragColor = u_highlights[0] * (2.0 * shade - 1.0); // Highlight strength [0, 1] } else { fragColor = u_shadows[0] * (1.0 - 2.0 * shade); // Shadow strength [0, 1] } } // Multidirectional hillshade (Method 3) void multidirectional_hillshade(vec2 deriv) { deriv = deriv * u_exaggeration * 2.0; // Exaggerate the slope derivative fragColor = vec4(0, 0, 0, 0); // Iterate through all light sources (up to u_num_lights, max 4) for (int i = 0; i < 4; i++) { if (i >= u_num_lights) break; // Access altitude and azimuth from vec4 UBOs float altitude = (i == 0) ? u_altitudes.x : (i == 1) ? u_altitudes.y : (i == 2) ? u_altitudes.z : u_altitudes.w; float azimuth = (i == 0) ? u_azimuths.x : (i == 1) ? u_azimuths.y : (i == 2) ? u_azimuths.z : u_azimuths.w; float cos_alt = cos(altitude); float sin_alt = sin(altitude); // Negate cos/sin azimuth for correct light direction in the normal calculation float cos_az = -cos(azimuth); float sin_az = -sin(azimuth); // Calculate the cosine of the angle between the light vector and the surface normal float cang = (sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv)); float shade = clamp(cang, 0.0, 1.0); // cang is the hillshade intensity [0, 1] // Accumulate shadow/highlight contribution from each light if (shade > 0.5) { fragColor += u_highlights[i] * (2.0 * shade - 1.0) / float(u_num_lights); } else { fragColor += u_shadows[i] * (1.0 - 2.0 * shade) / float(u_num_lights); } } } // Combined shadow and highlight method (Method 1) void combined_hillshade(vec2 deriv) { // Only supports one light source (index 0) deriv = deriv * u_exaggeration * 2.0; float azimuth = u_azimuths.x + PI; float cos_az = cos(azimuth); float sin_az = sin(azimuth); float cos_alt = cos(u_altitudes.x); float sin_alt = sin(u_altitudes.x); // Calculate the angle between the light vector and the surface normal float cang = acos((sin_alt - (deriv.y * cos_az * cos_alt - deriv.x * sin_az * cos_alt)) / sqrt(1.0 + dot(deriv, deriv))); cang = clamp(cang, 0.0, PI / 2.0); // Clamp angle to 90 degrees (half hemisphere) // The "shade" and "highlight" components are calculated from cang (angle) and the magnitude of the slope float shade = cang * atan(length(deriv)) * 4.0 / PI / PI; float highlight = (PI / 2.0 - cang) * atan(length(deriv)) * 4.0 / PI / PI; fragColor = u_shadows[0] * shade + u_highlights[0] * highlight; } // Igor's shadow/highlight method (Method 2) void igor_hillshade(vec2 deriv) { // Only supports one light source (index 0) deriv = deriv * u_exaggeration * 2.0; float aspect = get_aspect(deriv); float azimuth = u_azimuths.x + PI; // Slope strength is magnitude of slope vector, normalized to [0, 1] float slope_strength = atan(length(deriv)) * 2.0 / PI; // Aspect strength is difference between aspect and light azimuth, normalized to [0, 1] float aspect_strength = 1.0 - abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); float shadow_strength = slope_strength * aspect_strength; float highlight_strength = slope_strength * (1.0 - aspect_strength); fragColor = u_shadows[0] * shadow_strength + u_highlights[0] * highlight_strength; } void main() { vec4 pixel = texture(u_image, v_pos); // Scale the derivative based on the mercator distortion at this latitude (v_pos.y) float scaleFactor = cos(radians((u_latrange[0] - u_latrange[1]) * (1.0 - v_pos.y) + u_latrange[1])); // The derivative is scaled back from [0, 1] texture range to world-space slope // Texture range [0, 1] corresponds to slope range [-4, 4] (8.0 * 0.5 * deriv) vec2 deriv = ((pixel.rg * 8.0) - 4.0) / scaleFactor; // Dispatch to the selected hillshade method switch (u_method) { case BASIC: basic_hillshade(deriv); break; case COMBINED: combined_hillshade(deriv); break; case IGOR: igor_hillshade(deriv); break; case MULTIDIRECTIONAL: multidirectional_hillshade(deriv); break; case STANDARD: default: standard_hillshade(deriv); break; } #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); #endif }