Files
MapLibreParticleLab/include/mln/shaders/vulkan/hillshade.hpp
Bart Louwers 2a8ebc4906 core: rename mbgl directories to mln (#4511)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-22 20:50:14 +02:00

258 lines
8.9 KiB
C++

#pragma once
#include <mln/shaders/shader_source.hpp>
#include <mln/shaders/vulkan/shader_program.hpp>
namespace mln {
namespace shaders {
constexpr auto hillshadeShaderPrelude = R"(
#define idHillshadeDrawableUBO idDrawableReservedVertexOnlyUBO
#define idHillshadeTilePropsUBO idDrawableReservedFragmentOnlyUBO
#define idHillshadeEvaluatedPropsUBO layerUBOStartId
)";
template <>
struct ShaderSource<BuiltIn::HillshadeShader, gfx::Backend::Type::Vulkan> {
static constexpr const char* name = "HillshadeShader";
static const std::array<AttributeInfo, 2> attributes;
static constexpr std::array<AttributeInfo, 0> instanceAttributes{};
static const std::array<TextureInfo, 1> textures;
static constexpr auto prelude = hillshadeShaderPrelude;
static constexpr auto vertex = R"(
layout(location = 0) in ivec2 in_position;
layout(location = 1) in ivec2 in_texture_position;
layout(push_constant) uniform Constants {
int ubo_index;
} constant;
struct HillshadeDrawableUBO {
mat4 matrix;
};
layout(std140, set = LAYER_SET_INDEX, binding = idHillshadeDrawableUBO) readonly buffer HillshadeDrawableUBOVector {
HillshadeDrawableUBO drawable_ubo[];
} drawableVector;
layout(location = 0) out vec2 frag_position;
void main() {
const HillshadeDrawableUBO drawable = drawableVector.drawable_ubo[constant.ubo_index];
gl_Position = drawable.matrix * vec4(in_position, 0.0, 1.0);
applySurfaceTransform();
frag_position = vec2(in_texture_position) / 8192.0;
frag_position.y = 1.0 - frag_position.y;
}
)";
static constexpr auto fragment = R"(
#define PI 3.141592653589793
#define STANDARD 0
#define COMBINED 1
#define IGOR 2
#define MULTIDIRECTIONAL 3
#define BASIC 4
layout(location = 0) in vec2 frag_position;
layout(location = 0) out vec4 out_color;
layout(push_constant) uniform Constants {
int ubo_index;
} constant;
struct HillshadeTilePropsUBO {
vec2 latrange;
float exaggeration;
int method;
int num_lights;
float pad0;
float pad1;
float pad2;
};
layout(std140, set = LAYER_SET_INDEX, binding = idHillshadeTilePropsUBO) readonly buffer HillshadeTilePropsUBOVector {
HillshadeTilePropsUBO tile_props_ubo[];
} tilePropsVector;
layout(set = LAYER_SET_INDEX, binding = idHillshadeEvaluatedPropsUBO) uniform HillshadeEvaluatedPropsUBO {
vec4 accent;
vec4 altitudes;
vec4 azimuths;
vec4 shadows[4];
vec4 highlights[4];
} props;
layout(set = DRAWABLE_IMAGE_SET_INDEX, binding = 0) uniform sampler2D image_sampler;
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: STANDARD)
void standard_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) {
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);
vec4 accent_color = (1.0 - accent) * props.accent * clamp(intensity * 2.0, 0.0, 1.0);
float shade = abs(mod((aspect + azimuth) / PI + 0.5, 2.0) - 1.0);
vec4 shade_color = mix(props.shadows[0], props.highlights[0], shade) * sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0);
out_color = accent_color * (1.0 - shade_color.a) + shade_color;
}
// Basic directional hillshade (Method 4: BASIC)
void basic_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) {
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) {
out_color = props.highlights[0] * (2.0 * shade - 1.0);
} else {
out_color = props.shadows[0] * (1.0 - 2.0 * shade);
}
}
// Multidirectional hillshade (Method 3: MULTIDIRECTIONAL)
void multidirectional_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) {
deriv = deriv * tileProps.exaggeration * 2.0;
vec4 total_color = vec4(0, 0, 0, 0);
// Access altitude and azimuth from vec4 UBOs
float altitudes[4] = float[4](props.altitudes.x, props.altitudes.y, props.altitudes.z, props.altitudes.w);
float azimuths[4] = float[4](props.azimuths.x, props.azimuths.y, props.azimuths.z, props.azimuths.w);
int num_lights = min(tileProps.num_lights, 4);
// Iterate through all light sources
for (int i = 0; i < num_lights; i++) {
float altitude = altitudes[i];
float azimuth = azimuths[i];
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);
}
}
out_color = total_color;
}
// Combined shadow and highlight method (Method 1: COMBINED)
void combined_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) {
// 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;
out_color = props.shadows[0] * shade + props.highlights[0] * highlight;
}
// Igor's shadow/highlight method (Method 2: IGOR)
void igor_hillshade(vec2 deriv, const HillshadeTilePropsUBO tileProps) {
// 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(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);
out_color = props.shadows[0] * shadow_strength + props.highlights[0] * highlight_strength;
}
void main() {
#if defined(OVERDRAW_INSPECTOR)
out_color = vec4(1.0);
return;
#endif
const HillshadeTilePropsUBO tileProps = tilePropsVector.tile_props_ubo[constant.ubo_index];
vec4 pixel = texture(image_sampler, frag_position);
// Scale the derivative based on the mercator distortion at this latitude
float scaleFactor = cos(radians((tileProps.latrange[0] - tileProps.latrange[1]) * frag_position.y + tileProps.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]
vec2 deriv = ((pixel.rg * 8.0) - 4.0) / scaleFactor;
// Dispatch to the selected hillshade method
if (tileProps.method == BASIC) {
basic_hillshade(deriv, tileProps);
} else if (tileProps.method == COMBINED) {
combined_hillshade(deriv, tileProps);
} else if (tileProps.method == IGOR) {
igor_hillshade(deriv, tileProps);
} else if (tileProps.method == MULTIDIRECTIONAL) {
multidirectional_hillshade(deriv, tileProps);
} else {
// Default to STANDARD
standard_hillshade(deriv, tileProps);
}
}
)";
};
} // namespace shaders
} // namespace mln