Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
265 lines
10 KiB
C++
265 lines
10 KiB
C++
#pragma once
|
|
|
|
#include <mln/shaders/hillshade_layer_ubo.hpp>
|
|
#include <mln/shaders/shader_source.hpp>
|
|
#include <mln/shaders/mtl/shader_program.hpp>
|
|
|
|
namespace mln {
|
|
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(0)]];
|
|
short2 texture_pos [[attribute(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 mln
|