Files
MapLibreParticleLab/include/mbgl/shaders/mtl/hillshade.hpp

265 lines
11 KiB
C++

#pragma once
#include <mbgl/shaders/hillshade_layer_ubo.hpp>
#include <mbgl/shaders/shader_source.hpp>
#include <mbgl/shaders/mtl/shader_program.hpp>
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<BuiltIn::HillshadeShader, gfx::Backend::Type::Metal> {
static constexpr auto name = "HillshadeShader";
static constexpr auto vertexMainFunction = "vertexMain";
static constexpr auto fragmentMainFunction = "fragmentMain";
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 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<float, access::sample> 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