#pragma once #include #include #include namespace mbgl { namespace shaders { constexpr auto hillshadeShaderPrelude = R"( enum { idHillshadeDrawableUBO = idDrawableReservedVertexOnlyUBO, idHillshadeTilePropsUBO = idDrawableReservedFragmentOnlyUBO, idHillshadeEvaluatedPropsUBO = drawableReservedUBOCount, hillshadeUBOCount }; struct alignas(16) HillshadeDrawableUBO { /* 0 */ float4x4 matrix; /* 64 */ }; static_assert(sizeof(HillshadeDrawableUBO) == 4 * 16, "wrong size"); struct alignas(16) HillshadeTilePropsUBO { /* 0 */ float2 latrange; /* 8 */ float exaggeration; /* 12 */ int32_t method; /* 16 */ int32_t num_lights; /* 20 */ float pad0; /* 24 */ float pad1; /* 28 */ float pad2; /* 32 */ }; static_assert(sizeof(HillshadeTilePropsUBO) == 2 * 16, "wrong size"); /// Evaluated properties that do not depend on the tile struct alignas(16) HillshadeEvaluatedPropsUBO { /* 0 */ float4 accent; /* 16 */ float4 altitudes; // Up to 4 altitude values (in radians) /* 32 */ float4 azimuths; // Up to 4 azimuth values (in radians) /* 48 */ float4 shadows[4]; // Shadow colors (up to 4 lights) /* 112 */ float4 highlights[4]; // Highlight colors (up to 4 lights) /* 176 */ }; static_assert(sizeof(HillshadeEvaluatedPropsUBO) == 11 * 16, "wrong size"); )"; template <> struct ShaderSource { static constexpr auto name = "HillshadeShader"; static constexpr auto vertexMainFunction = "vertexMain"; static constexpr auto fragmentMainFunction = "fragmentMain"; static const std::array attributes; static constexpr std::array instanceAttributes{}; static const std::array textures; static constexpr auto prelude = hillshadeShaderPrelude; static constexpr auto source = R"( #define PI 3.141592653589793 #define STANDARD 0 #define COMBINED 1 #define IGOR 2 #define MULTIDIRECTIONAL 3 #define BASIC 4 struct VertexStage { short2 pos [[attribute(hillshadeUBOCount + 0)]]; short2 texture_pos [[attribute(hillshadeUBOCount + 1)]]; }; struct FragmentStage { float4 position [[position, invariant]]; float2 pos; }; FragmentStage vertex vertexMain(thread const VertexStage vertx [[stage_in]], device const uint32_t& uboIndex [[buffer(idGlobalUBOIndex)]], device const HillshadeDrawableUBO* drawableVector [[buffer(idHillshadeDrawableUBO)]]) { device const HillshadeDrawableUBO& drawable = drawableVector[uboIndex]; const float4 position = drawable.matrix * float4(float2(vertx.pos), 0, 1); float2 pos = float2(vertx.texture_pos) / 8192.0; // Metal's texture coordinate origin differs from some renderers; // restore Y-flip to match prepare pass and historical behavior. pos.y = 1.0 - pos.y; return { .position = position, .pos = pos, }; } // Helper function to calculate aspect (normalized direction of slope) float get_aspect(float2 deriv) { return deriv.x != 0.0 ? atan2(deriv.y, -deriv.x) : PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0); } // MapLibre's legacy hillshade algorithm (Method 0: STANDARD) void standard_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { float azimuth = props.azimuths.x + PI; float slope = atan(0.625 * length(deriv)); float aspect = get_aspect(deriv); float intensity = tileProps.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); float4 accent_color = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0); float shade = abs(glMod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0); float4 shade_color = mix(props.shadows[0], props.highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0); fragColor = half4(accent_color * (1.0 - shade_color.a) + shade_color); } // Basic directional hillshade (Method 4: BASIC) void basic_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { deriv = deriv * tileProps.exaggeration * 2.0; float azimuth = props.azimuths.x + PI; float cos_az = cos(azimuth); float sin_az = sin(azimuth); float cos_alt = cos(props.altitudes.x); float sin_alt = sin(props.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); // Blend shadow and highlight based on intensity if (shade > 0.5) { fragColor = half4(props.highlights[0] * (2.0 * shade - 1.0)); } else { fragColor = half4(props.shadows[0] * (1.0 - 2.0 * shade)); } } // Multidirectional hillshade (Method 3: MULTIDIRECTIONAL) void multidirectional_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { deriv = deriv * tileProps.exaggeration * 2.0; float4 total_color = float4(0, 0, 0, 0); int num_lights = min(tileProps.num_lights, 4); // Iterate through all light sources for (int i = 0; i < num_lights; i++) { // Access altitude and azimuth from vec4 UBOs float altitude = (i == 0) ? props.altitudes.x : (i == 1) ? props.altitudes.y : (i == 2) ? props.altitudes.z : props.altitudes.w; float azimuth = (i == 0) ? props.azimuths.x : (i == 1) ? props.azimuths.y : (i == 2) ? props.azimuths.z : props.azimuths.w; float cos_alt = cos(altitude); float sin_alt = sin(altitude); // Negate cos/sin azimuth for correct light direction 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); // Accumulate shadow/highlight contribution from each light if (shade > 0.5) { total_color += props.highlights[i] * (2.0 * shade - 1.0) / float(num_lights); } else { total_color += props.shadows[i] * (1.0 - 2.0 * shade) / float(num_lights); } } fragColor = half4(total_color); } // Combined shadow and highlight method (Method 1: COMBINED) void combined_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { // Only supports one light source (index 0) deriv = deriv * tileProps.exaggeration * 2.0; float azimuth = props.azimuths.x + PI; float cos_az = cos(azimuth); float sin_az = sin(azimuth); float cos_alt = cos(props.altitudes.x); float sin_alt = sin(props.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); // Calculate shade and highlight components from angle and slope magnitude float shade = cang * atan(length(deriv)) * 4.0 / PI / PI; float highlight = (PI / 2.0 - cang) * atan(length(deriv)) * 4.0 / PI / PI; fragColor = half4(props.shadows[0] * shade + props.highlights[0] * highlight); } // Igor's shadow/highlight method (Method 2: IGOR) void igor_hillshade(float2 deriv, device const HillshadeTilePropsUBO& tileProps, device const HillshadeEvaluatedPropsUBO& props, thread half4& fragColor) { // Only supports one light source (index 0) deriv = deriv * tileProps.exaggeration * 2.0; float aspect = get_aspect(deriv); float azimuth = props.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(glMod((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 = half4(props.shadows[0] * shadow_strength + props.highlights[0] * highlight_strength); } half4 fragment fragmentMain(FragmentStage in [[stage_in]], device const uint32_t& uboIndex [[buffer(idGlobalUBOIndex)]], device const HillshadeTilePropsUBO* tilePropsVector [[buffer(idHillshadeTilePropsUBO)]], device const HillshadeEvaluatedPropsUBO& props [[buffer(idHillshadeEvaluatedPropsUBO)]], texture2d image [[texture(0)]], sampler image_sampler [[sampler(0)]]) { #if defined(OVERDRAW_INSPECTOR) return half4(1.0); #endif device const HillshadeTilePropsUBO& tileProps = tilePropsVector[uboIndex]; thread half4 fragColor; float4 pixel = image.sample(image_sampler, in.pos); // Scale the derivative based on the mercator distortion at this latitude float latitude = (tileProps.latrange.x - tileProps.latrange.y) * in.pos.y + tileProps.latrange.y; float scaleFactor = cos(radians(latitude)); // The derivative is scaled back from [0, 1] texture range to world-space slope // Texture range [0, 1] corresponds to slope range [-4, 4] float2 deriv = ((pixel.rg * 8.0) - 4.0) / scaleFactor; // Dispatch to the selected hillshade method if (tileProps.method == BASIC) { basic_hillshade(deriv, tileProps, props, fragColor); } else if (tileProps.method == COMBINED) { combined_hillshade(deriv, tileProps, props, fragColor); } else if (tileProps.method == IGOR) { igor_hillshade(deriv, tileProps, props, fragColor); } else if (tileProps.method == MULTIDIRECTIONAL) { multidirectional_hillshade(deriv, tileProps, props, fragColor); } else { // Default to STANDARD standard_hillshade(deriv, tileProps, props, fragColor); } return fragColor; } )"; }; } // namespace shaders } // namespace mbgl