legatt 8c41f36e80 Map graphics
Update map rendering for drawing pretty ocean and regions.
!Changes are made with Claude!
2026-10-14 14:56:14 +03:00

233 lines
7.8 KiB
GLSL

#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(location = 0) in vec2 worldPos;
layout(location = 1) flat in uint instance;
layout(location = 0) out vec4 outColor;
layout(std140, binding = 0) uniform Camera {
mat4 viewProjection;
mat4 inverseViewProjection;
vec4 viewport; // x, y: framebuffer size; z: world units per screen pixel; w: 1 if target is sRGB
vec4 raster; // x, y: world position of province raster texel (0, 0) center; z, w: raster size in texels
vec4 coast; // x: map pixels per raster texel; y: raster texels per coast field texel; z, w: coast field size
uvec4 info; // x: instance index of the ocean plane
} camera;
struct Province {
uint regionId;
uint color;
};
layout(std430, binding = 1) readonly buffer Data {
Province provinces[];
};
// Province index + 1 for every raster texel, 0 for water.
layout(binding = 2) uniform usampler2D provinceIds;
// Signed distance to the coastline in raster texels: positive in water, negative on land.
layout(binding = 3) uniform sampler2D coastDistance;
// ---------------------------------------------------------------------------------------------
// Palette (sRGB)
// ---------------------------------------------------------------------------------------------
const vec3 OCEAN_SHELF_NEAR = vec3( 88.0, 148.0, 204.0) / 255.0;
const vec3 OCEAN_SHELF_MID = vec3( 60.0, 128.0, 196.0) / 255.0;
const vec3 OCEAN_SHELF_FAR = vec3( 38.0, 113.0, 189.0) / 255.0;
const vec3 OCEAN_DEEP = vec3( 20.0, 98.0, 183.0) / 255.0;
const vec3 OCEAN_ABYSS = vec3( 15.0, 86.0, 168.0) / 255.0;
// All distances below are in province raster texels ("map art pixels").
// Terraces of the coastal shelf, distance from the shore.
const float SHELF_NEAR_END = 2.5;
const float SHELF_MID_END = 4.5;
const float SHELF_FAR_FADE_START = 6.5;
const float SHELF_FAR_FADE_END = 16.0;
const float GRID_SPACING = 32.0;
const float GRID_STRENGTH_OCEAN = 0.07;
const float GRID_STRENGTH_LAND = 0.10;
const int COUNTRY_SHADE_RADIUS = 3;
const float COUNTRY_SHADE = 0.82;
// ---------------------------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------------------------
vec3 unpackColor(uint c) {
return vec3(
c & 0xFFu,
(c >> 8) & 0xFFu,
(c >> 16) & 0xFFu
) / 255.0;
}
vec3 srgbToLinear(vec3 c) {
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c));
}
vec3 saturateColor(vec3 c, float amount) {
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722));
return clamp(mix(vec3(luma), c, amount), 0.0, 1.0);
}
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float valueNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash12(i);
float b = hash12(i + vec2(1.0, 0.0));
float c = hash12(i + vec2(0.0, 1.0));
float d = hash12(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 4; ++i) {
value += amplitude * valueNoise(p);
p = p * 2.03 + vec2(17.1, 9.7);
amplitude *= 0.5;
}
return value / 0.9375;
}
// Raster texels per screen pixel.
float artPixelSize() {
return camera.viewport.z / camera.coast.x;
}
// Map-space grid lines with a constant on-screen thickness, faded out when they get too dense.
float gridMask(vec2 art) {
float pixel = artPixelSize();
vec2 cell = abs(fract(art / GRID_SPACING + 0.5) - 0.5) * GRID_SPACING / pixel;
float line = 1.0 - smoothstep(0.6, 1.6, min(cell.x, cell.y));
float density = smoothstep(10.0, 24.0, GRID_SPACING / pixel);
return line * density;
}
uint fetchProvince(ivec2 texel) {
texel = clamp(texel, ivec2(0), ivec2(camera.raster.zw) - 1);
return texelFetch(provinceIds, texel, 0).r;
}
uint ownerColor(uint id) {
return provinces[id - 1u].color & 0xFFFFFFu;
}
// ---------------------------------------------------------------------------------------------
// Ocean
// ---------------------------------------------------------------------------------------------
vec3 shadeOcean(vec2 art) {
vec2 uv = (art / camera.coast.y + 0.5) / camera.coast.zw;
float shoreDistance = texture(coastDistance, uv).r;
// Ragged terrace edges instead of perfect offset curves of the coastline.
float d = shoreDistance + (fbm(art * 0.15) - 0.5) * 1.6;
float edge = 0.5 * max(artPixelSize(), 0.75);
// Large, soft deep-water patches far away from the shore.
float abyss = smoothstep(0.45, 0.8, fbm(art * 0.012)) * smoothstep(10.0, 30.0, shoreDistance);
vec3 color = mix(OCEAN_DEEP, OCEAN_ABYSS, abyss);
color = mix(OCEAN_SHELF_FAR, color, smoothstep(SHELF_FAR_FADE_START, SHELF_FAR_FADE_END, d));
color = mix(OCEAN_SHELF_MID, color, smoothstep(SHELF_MID_END - edge, SHELF_MID_END + edge, d));
color = mix(OCEAN_SHELF_NEAR, color, smoothstep(SHELF_NEAR_END - edge, SHELF_NEAR_END + edge, d));
color *= 1.0 + (valueNoise(art * 0.35) - 0.5) * 0.025;
color *= 1.0 - GRID_STRENGTH_OCEAN * gridMask(art);
return color;
}
// ---------------------------------------------------------------------------------------------
// Land
// ---------------------------------------------------------------------------------------------
const ivec2 DIRECTIONS[8] = ivec2[](
ivec2(1, 0), ivec2(-1, 0), ivec2(0, 1), ivec2(0, -1),
ivec2(1, 1), ivec2(-1, 1), ivec2(1, -1), ivec2(-1, -1)
);
vec3 shadeLand(vec2 art, ivec2 texel, uint ownId) {
uint ownColorBits = ownerColor(ownId);
vec3 base = unpackColor(ownColorBits);
// When zoomed out a raster texel gets smaller than a screen pixel; widen borders to stay visible.
int stride = max(1, int(artPixelSize() + 0.5));
bool coast = false;
bool provinceBorder = false;
bool countryBorder = false;
float countryDistance = 1.0e6;
for (int d = 0; d < 8; ++d) {
bool axis = d < 4;
float stepLength = axis ? 1.0 : 1.41421356;
for (int r = 1; r <= COUNTRY_SHADE_RADIUS; ++r) {
uint neighbour = fetchProvince(texel + DIRECTIONS[d] * (r * stride));
if (neighbour == ownId) continue;
if (neighbour == 0u) {
coast = coast || (axis && r == 1);
continue;
}
if (ownerColor(neighbour) != ownColorBits) {
countryDistance = min(countryDistance, float(r) * stepLength);
countryBorder = countryBorder || (axis && r == 1);
break;
}
provinceBorder = provinceBorder || (axis && r == 1);
}
}
// Subtle per-pixel grain plus low-frequency tint variation.
float grain = (hash12(vec2(texel)) - 0.5) * 0.035 + (valueNoise(art * 0.05) - 0.5) * 0.04;
vec3 color = base * (1.0 + grain);
if (provinceBorder) color = base * 0.84;
if (coast) color = saturateColor(base, 1.15) * 0.72;
color *= mix(COUNTRY_SHADE, 1.0, smoothstep(1.0, float(COUNTRY_SHADE_RADIUS) + 0.5, countryDistance));
if (countryBorder) color = saturateColor(base, 1.6) * 0.9;
color *= 1.0 - GRID_STRENGTH_LAND * gridMask(art);
return color;
}
void main() {
vec3 color;
// Position in province raster texels
vec2 art = (worldPos - camera.raster.xy) / camera.coast.x;
if (instance == camera.info.x) {
color = shadeOcean(art);
}
else {
ivec2 texel = ivec2(floor(art + 0.5));
uint ownId = fetchProvince(texel);
// Pixel-exact province coverage (see COVERAGE_EXPAND in the vertex shader).
if (ownId != instance + 1u) discard;
color = shadeLand(art, texel, ownId);
}
outColor = vec4(camera.viewport.w > 0.5 ? srgbToLinear(color) : color, 1.0);
}