Map graphics
Update map rendering for drawing pretty ocean and regions. !Changes are made with Claude!
This commit is contained in:
parent
1adddd3f1c
commit
8c41f36e80
@ -1,10 +1,232 @@
|
||||
#version 450
|
||||
#extension GL_ARB_separate_shader_objects : enable
|
||||
|
||||
layout(location = 0) in vec3 fragColor;
|
||||
layout(location = 0) in vec2 worldPos;
|
||||
layout(location = 1) flat in uint instance;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = vec4(fragColor, 1.0);
|
||||
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);
|
||||
}
|
||||
@ -1,35 +1,32 @@
|
||||
#version 450
|
||||
#extension GL_ARB_separate_shader_objects : enable
|
||||
|
||||
// Per-vertex data: contour point of a province polygon and its outward miter normal.
|
||||
// The ocean plane is drawn through the same multi-draw with a zero normal.
|
||||
layout(location = 0) in vec2 position;
|
||||
layout(location = 1) in vec2 normal;
|
||||
|
||||
layout(binding = 0) uniform Camera {
|
||||
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;
|
||||
|
||||
layout(push_constant) uniform Model {
|
||||
mat4 model;
|
||||
} model;
|
||||
|
||||
struct Province {
|
||||
uint regionId;
|
||||
uint color;
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer Data {
|
||||
Province provinces[];
|
||||
};
|
||||
|
||||
layout(location = 0) out vec3 fragColor;
|
||||
|
||||
vec3 unpackColor(uint c) {
|
||||
return vec3(
|
||||
c & 0xFFu,
|
||||
(c >> 8) & 0xFFu,
|
||||
(c >> 16) & 0xFFu
|
||||
) / 255.0;
|
||||
}
|
||||
layout(location = 0) out vec2 worldPos;
|
||||
layout(location = 1) flat out uint instance;
|
||||
|
||||
void main() {
|
||||
gl_Position = camera.viewProjection * vec4(position, 0.0, 1.0);
|
||||
fragColor = unpackColor(provinces[gl_InstanceIndex].color);
|
||||
// Province polygons are simplified pixel contours, so they may miss up to ~1.7 map pixels of
|
||||
// their own area, plus up to a raster texel lost to rounding. They are pushed outwards and the
|
||||
// fragment shader clips them exactly against the province raster, which also closes the seams
|
||||
// between neighbouring provinces.
|
||||
float coverageExpand = 2.0 + 1.75 * camera.coast.x;
|
||||
vec2 world = position + normal * coverageExpand;
|
||||
|
||||
worldPos = world;
|
||||
instance = uint(gl_InstanceIndex);
|
||||
gl_Position = camera.viewProjection * vec4(world, 0.0, 1.0);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@ -16,13 +16,40 @@ namespace ee_vulkan {
|
||||
constexpr std::uint64_t TIMEOUT = std::numeric_limits<std::uint64_t>::max();
|
||||
|
||||
struct DrawConfig {
|
||||
static constexpr vk::ClearValue CLEAR_COLOR = {.color = {std::array{1.0f, 1.0f, 1.0f, 1.0f}}};
|
||||
// Deep ocean color (linear). Normally hidden by the ocean plane drawn with the provinces.
|
||||
static constexpr vk::ClearValue CLEAR_COLOR = {.color = {std::array{0.007f, 0.122f, 0.474f, 1.0f}}};
|
||||
static constexpr vk::ClearValue CLEAR_DEPTH = {.depthStencil = {1.0f, 0}};
|
||||
};
|
||||
|
||||
/**
|
||||
* Vertex of a province polygon. Normal is the outward miter of the province contour,
|
||||
* used by the shader to slightly grow polygons before clipping them against the province raster.
|
||||
*/
|
||||
struct ProvinceVertex {
|
||||
glm::vec2 position;
|
||||
glm::vec2 normal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-frame data of the map shaders (std140 layout, see provinces-flat shaders).
|
||||
*/
|
||||
struct MapUniform {
|
||||
glm::mat4 viewProjection;
|
||||
glm::mat4 inverseViewProjection;
|
||||
/// x, y: framebuffer size; z: world units per screen pixel; w: 1 if the target is sRGB.
|
||||
glm::vec4 viewport;
|
||||
/// x, y: world position of province raster texel (0, 0) center; z, w: province raster size in texels.
|
||||
glm::vec4 raster;
|
||||
/// x: map pixels per province raster texel; y: raster texels per coast field texel; z, w: coast field size.
|
||||
glm::vec4 coast;
|
||||
/// x: instance index of the ocean plane.
|
||||
glm::uvec4 info;
|
||||
};
|
||||
static_assert(sizeof(MapUniform) == 192, "MapUniform must match std140 layout of the map shaders");
|
||||
|
||||
struct ProvinceMesh {
|
||||
lazy<vulkan_render::Buffer<
|
||||
glm::vec2,
|
||||
ProvinceVertex,
|
||||
vk::BufferUsageFlagBits::eVertexBuffer,
|
||||
vulkan_render::BufferMode::STAGING_ACCESS
|
||||
>> vertices;
|
||||
@ -42,6 +69,24 @@ namespace ee_vulkan {
|
||||
vk::BufferUsageFlagBits::eIndirectBuffer,
|
||||
vulkan_render::BufferMode::STAGING_ACCESS
|
||||
>> indirectCommands;
|
||||
|
||||
/// Province index + 1 for every map pixel, 0 for water (R16/R32 uint).
|
||||
lazy<vulkan_render::PartialTexture> provinceIds;
|
||||
/// Signed distance to the coastline in province raster texels, positive in water (R16 float).
|
||||
lazy<vulkan_render::PartialTexture> coastDistance;
|
||||
vk::raii::Sampler nearestSampler = nullptr;
|
||||
vk::raii::Sampler linearSampler = nullptr;
|
||||
|
||||
/// World position of the center of province raster texel (0, 0).
|
||||
glm::ivec2 rasterOrigin {0, 0};
|
||||
glm::uvec2 rasterSize {1, 1};
|
||||
/// Map pixels per province raster texel (more than 1 for maps exceeding texture limits).
|
||||
std::uint32_t rasterTexelSize = 1;
|
||||
glm::uvec2 coastFieldSize {1, 1};
|
||||
/// Province raster texels per coast distance field texel.
|
||||
std::uint32_t coastFieldFactor = 1;
|
||||
/// Index of the ocean plane in the storage buffer and in the instances of multi-draw.
|
||||
std::uint32_t oceanInstance = 0;
|
||||
};
|
||||
}
|
||||
|
||||
@ -80,7 +125,7 @@ export namespace ee_vulkan {
|
||||
lazy<vulkan_render::SyncManager> syncManager;
|
||||
|
||||
lazy<vulkan_render::Texture<vulkan_render::TextureMode::DEPTH>> depth;
|
||||
lazy<vulkan_render::Uniform<glm::mat4>> viewProjectionUniform;
|
||||
lazy<vulkan_render::Uniform<MapUniform>> mapUniform;
|
||||
ProvinceMesh provinces;
|
||||
|
||||
lazy<ee_rml::RmlVulkan> rmlRenderInterface;
|
||||
@ -93,6 +138,11 @@ export namespace ee_vulkan {
|
||||
void LoadProvincesMaterial();
|
||||
void LoadRmlMaterial();
|
||||
void LoadProvinces();
|
||||
void LoadMapTextures(
|
||||
const std::vector<std::uint32_t> &ids,
|
||||
const std::vector<float> &coastField,
|
||||
std::uint32_t provincesCount
|
||||
);
|
||||
void UpdateData(std::uint32_t frame);
|
||||
};
|
||||
}
|
||||
@ -4,11 +4,448 @@ module;
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/packing.hpp>
|
||||
#include <RmlUi/Core/Core.h>
|
||||
|
||||
module VulkanRender;
|
||||
import utils;
|
||||
|
||||
namespace {
|
||||
/// Region id of the storage buffer element describing the ocean plane.
|
||||
constexpr std::uint32_t OCEAN_REGION_ID = std::numeric_limits<std::uint32_t>::max();
|
||||
/// Half-size of the ocean plane in world units, large enough to fill the view at any zoom.
|
||||
constexpr float OCEAN_PLANE_EXTENT = 1.0e6f;
|
||||
/// Water margin around the map in rasters (texels), so coastal effects are not cut at the map edge.
|
||||
constexpr std::int32_t RASTER_PADDING = 32;
|
||||
/// Distances stored in the coast distance field are clamped to this value (raster texels).
|
||||
constexpr float MAX_COAST_DISTANCE = 4096.0f;
|
||||
/// Upper bound of the province raster size; bigger maps are rasterized with several map pixels per texel.
|
||||
constexpr std::uint64_t MAX_RASTER_TEXELS = 48ull * 1024 * 1024;
|
||||
/// Upper bound of the coast distance field size; it is smooth, so it may be coarser than the province raster.
|
||||
constexpr std::uint64_t MAX_COAST_FIELD_TEXELS = 12ull * 1024 * 1024;
|
||||
|
||||
struct Bounds {
|
||||
glm::vec2 min {std::numeric_limits<float>::max()};
|
||||
glm::vec2 max {std::numeric_limits<float>::lowest()};
|
||||
|
||||
void Add(const glm::vec2 &point) {
|
||||
min = glm::min(min, point);
|
||||
max = glm::max(max, point);
|
||||
}
|
||||
|
||||
bool IsEmpty() const { return min.x > max.x || min.y > max.y; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Map rasterized with texelSize x texelSize map pixels per texel.
|
||||
* Texel (0, 0) center is located at world position origin, texel (x, y) at origin + (x, y) * texelSize.
|
||||
*/
|
||||
struct ProvinceRaster {
|
||||
glm::ivec2 origin {0, 0};
|
||||
glm::ivec2 size {1, 1};
|
||||
std::int32_t texelSize = 1;
|
||||
/// Province index + 1, 0 for water.
|
||||
std::vector<std::uint32_t> ids;
|
||||
|
||||
ProvinceRaster(const Bounds &bounds, const std::int32_t mapPixelsPerTexel) : texelSize(mapPixelsPerTexel) {
|
||||
if (!bounds.IsEmpty()) {
|
||||
const glm::ivec2 low(static_cast<std::int32_t>(std::floor(bounds.min.x)), static_cast<std::int32_t>(std::floor(bounds.min.y)));
|
||||
const glm::ivec2 high(static_cast<std::int32_t>(std::ceil(bounds.max.x)), static_cast<std::int32_t>(std::ceil(bounds.max.y)));
|
||||
origin = low - RASTER_PADDING * texelSize;
|
||||
size = (high - low) / texelSize + 1 + 2 * RASTER_PADDING;
|
||||
}
|
||||
ids.assign(static_cast<std::size_t>(size.x) * static_cast<std::size_t>(size.y), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smallest integer texel size that keeps the raster within texture limits and memory budget.
|
||||
*/
|
||||
static std::int32_t ChooseTexelSize(const Bounds &bounds, const std::uint32_t maxDimension, const std::uint64_t maxTexels) {
|
||||
if (bounds.IsEmpty()) return 1;
|
||||
|
||||
const glm::vec2 extent = bounds.max - bounds.min + 1.0f;
|
||||
for (std::int32_t candidate = 1; ; ++candidate) {
|
||||
const float step = static_cast<float>(candidate);
|
||||
const std::uint64_t width = static_cast<std::uint64_t>(std::ceil(extent.x / step)) + 2 * RASTER_PADDING + 1;
|
||||
const std::uint64_t height = static_cast<std::uint64_t>(std::ceil(extent.y / step)) + 2 * RASTER_PADDING + 1;
|
||||
if (width <= maxDimension && height <= maxDimension && width * height <= maxTexels)
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
/// Integer world position relative to the raster origin.
|
||||
glm::ivec2 ToLocal(const glm::vec2 &world) const {
|
||||
return glm::ivec2(
|
||||
static_cast<std::int32_t>(std::lround(world.x)),
|
||||
static_cast<std::int32_t>(std::lround(world.y))
|
||||
) - origin;
|
||||
}
|
||||
};
|
||||
|
||||
std::int64_t EdgeFunction(const glm::ivec2 &a, const glm::ivec2 &b, const glm::ivec2 &p) {
|
||||
return static_cast<std::int64_t>(b.x - a.x) * (p.y - a.y) - static_cast<std::int64_t>(b.y - a.y) * (p.x - a.x);
|
||||
}
|
||||
|
||||
std::int32_t FloorDiv(const std::int32_t value, const std::int32_t divisor) {
|
||||
return value >= 0 ? value / divisor : -((-value + divisor - 1) / divisor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks texels whose centers lie inside (or on the edge of) province triangles.
|
||||
* Mesh vertices are pixel centers of the source bitmap, so with texel size 1 the result closely reproduces the source map.
|
||||
*/
|
||||
void RasterizeProvince(
|
||||
ProvinceRaster &raster,
|
||||
const std::vector<glm::vec2> &vertices,
|
||||
const std::vector<std::uint16_t> &indices,
|
||||
const std::uint32_t id
|
||||
) {
|
||||
std::uint32_t *ids = raster.ids.data();
|
||||
const std::int32_t width = raster.size.x;
|
||||
const std::int32_t step = raster.texelSize;
|
||||
|
||||
for (std::size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||
if (indices[i] >= vertices.size() || indices[i + 1] >= vertices.size() || indices[i + 2] >= vertices.size())
|
||||
continue;
|
||||
|
||||
const glm::ivec2 a = raster.ToLocal(vertices[indices[i]]);
|
||||
glm::ivec2 b = raster.ToLocal(vertices[indices[i + 1]]);
|
||||
glm::ivec2 c = raster.ToLocal(vertices[indices[i + 2]]);
|
||||
|
||||
const std::int64_t area = EdgeFunction(a, b, c);
|
||||
if (area == 0) continue;
|
||||
if (area < 0) std::swap(b, c);
|
||||
|
||||
const std::int32_t minX = std::max(FloorDiv(std::min({a.x, b.x, c.x}) + step - 1, step), 0);
|
||||
const std::int32_t minY = std::max(FloorDiv(std::min({a.y, b.y, c.y}) + step - 1, step), 0);
|
||||
const std::int32_t maxX = std::min(FloorDiv(std::max({a.x, b.x, c.x}), step), raster.size.x - 1);
|
||||
const std::int32_t maxY = std::min(FloorDiv(std::max({a.y, b.y, c.y}), step), raster.size.y - 1);
|
||||
|
||||
for (std::int32_t y = minY; y <= maxY; ++y) {
|
||||
for (std::int32_t x = minX; x <= maxX; ++x) {
|
||||
const glm::ivec2 p(x * step, y * step);
|
||||
if (EdgeFunction(b, c, p) >= 0 && EdgeFunction(c, a, p) >= 0 && EdgeFunction(a, b, p) >= 0)
|
||||
ids[static_cast<std::size_t>(y) * width + x] = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Province contours are simplified, so thin slivers between neighbouring provinces stay unassigned.
|
||||
* Assigns such texels to the dominating neighbour when they are mostly surrounded by land.
|
||||
*/
|
||||
void CloseRasterGaps(ProvinceRaster &raster, const std::uint32_t iterations) {
|
||||
const std::size_t width = static_cast<std::size_t>(raster.size.x);
|
||||
const std::size_t height = static_cast<std::size_t>(raster.size.y);
|
||||
std::uint32_t *ids = raster.ids.data();
|
||||
|
||||
std::vector<std::size_t> candidates;
|
||||
std::vector<std::pair<std::size_t, std::uint32_t>> changes;
|
||||
bool firstPass = true;
|
||||
|
||||
const auto evaluate = [&](const std::size_t i) {
|
||||
if (ids[i] != 0) return;
|
||||
const std::size_t x = i % width;
|
||||
const std::size_t y = i / width;
|
||||
if (x == 0 || y == 0 || x + 1 >= width || y + 1 >= height) return;
|
||||
|
||||
// 4-neighbours first, then diagonals
|
||||
const std::array<std::uint32_t, 8> neighbours {
|
||||
ids[i - 1], ids[i + 1], ids[i - width], ids[i + width],
|
||||
ids[i - width - 1], ids[i - width + 1], ids[i + width - 1], ids[i + width + 1]
|
||||
};
|
||||
std::int32_t sides = 0;
|
||||
std::int32_t assigned = 0;
|
||||
for (std::size_t n = 0; n < neighbours.size(); ++n) {
|
||||
if (neighbours[n] == 0) continue;
|
||||
++assigned;
|
||||
if (n < 4) ++sides;
|
||||
}
|
||||
if (assigned == 0) return;
|
||||
|
||||
const bool enclosed = (neighbours[0] != 0 && neighbours[1] != 0) || (neighbours[2] != 0 && neighbours[3] != 0);
|
||||
if (sides < 3 && assigned < 5 && !enclosed) return;
|
||||
|
||||
std::uint32_t best = 0;
|
||||
std::int32_t bestVotes = 0;
|
||||
for (const std::uint32_t candidate : neighbours) {
|
||||
if (candidate == 0) continue;
|
||||
const std::int32_t votes = static_cast<std::int32_t>(std::ranges::count(neighbours, candidate));
|
||||
if (votes > bestVotes) {
|
||||
best = candidate;
|
||||
bestVotes = votes;
|
||||
}
|
||||
}
|
||||
changes.emplace_back(i, best);
|
||||
};
|
||||
|
||||
for (std::uint32_t iteration = 0; iteration < iterations; ++iteration) {
|
||||
changes.clear();
|
||||
if (firstPass) {
|
||||
for (std::size_t i = 0; i < raster.ids.size(); ++i) evaluate(i);
|
||||
firstPass = false;
|
||||
}
|
||||
else {
|
||||
for (const std::size_t i : candidates) evaluate(i);
|
||||
}
|
||||
if (changes.empty()) break;
|
||||
|
||||
// Apply after the scan, so every texel of an iteration sees the same state
|
||||
candidates.clear();
|
||||
for (const auto &[i, id] : changes) {
|
||||
ids[i] = id;
|
||||
for (const std::size_t n : {i - 1, i + 1, i - width, i + width, i - width - 1, i - width + 1, i + width - 1, i + width + 1})
|
||||
candidates.push_back(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Province meshes have no holes, so small water pockets fully enclosed by land are leftovers
|
||||
* of contour simplification. Fills them with the most common surrounding province.
|
||||
*/
|
||||
void FillEnclosedWater(ProvinceRaster &raster, const std::size_t maxArea) {
|
||||
enum : std::uint8_t { UNVISITED, CURRENT, OPEN_WATER };
|
||||
|
||||
const std::size_t width = static_cast<std::size_t>(raster.size.x);
|
||||
const std::size_t height = static_cast<std::size_t>(raster.size.y);
|
||||
std::uint32_t *ids = raster.ids.data();
|
||||
std::vector<std::uint8_t> state(raster.ids.size(), UNVISITED);
|
||||
std::vector<std::size_t> component;
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> shore;
|
||||
|
||||
for (std::size_t start = 0; start < raster.ids.size(); ++start) {
|
||||
if (ids[start] != 0 || state[start] != UNVISITED) continue;
|
||||
|
||||
// Bounded flood fill: stops as soon as the pocket proves to be open water
|
||||
component.assign(1, start);
|
||||
state[start] = CURRENT;
|
||||
shore.clear();
|
||||
bool open = false;
|
||||
|
||||
for (std::size_t head = 0; head < component.size() && !open; ++head) {
|
||||
const std::size_t i = component[head];
|
||||
const std::size_t x = i % width;
|
||||
const std::size_t y = i / width;
|
||||
if (x == 0 || y == 0 || x + 1 >= width || y + 1 >= height) {
|
||||
open = true;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const std::size_t n : {i - 1, i + 1, i - width, i + width}) {
|
||||
if (ids[n] != 0) {
|
||||
const auto entry = std::ranges::find(shore, ids[n], &std::pair<std::uint32_t, std::uint32_t>::first);
|
||||
if (entry != shore.end()) ++entry->second;
|
||||
else shore.emplace_back(ids[n], 1);
|
||||
}
|
||||
else if (state[n] == OPEN_WATER) {
|
||||
open = true;
|
||||
break;
|
||||
}
|
||||
else if (state[n] == UNVISITED) {
|
||||
state[n] = CURRENT;
|
||||
component.push_back(n);
|
||||
if (component.size() > maxArea) {
|
||||
open = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (open || shore.empty()) {
|
||||
for (const std::size_t i : component) state[i] = OPEN_WATER;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto dominant = std::ranges::max_element(shore, {}, &std::pair<std::uint32_t, std::uint32_t>::second);
|
||||
for (const std::size_t i : component) ids[i] = dominant->first;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Felzenszwalb & Huttenlocher squared Euclidean distance transform of a 1D sampled function.
|
||||
*/
|
||||
void DistanceTransform1D(
|
||||
const float *f, float *d, std::int32_t *v, float *z, const std::int32_t n
|
||||
) {
|
||||
std::int32_t k = 0;
|
||||
v[0] = 0;
|
||||
z[0] = -std::numeric_limits<float>::max();
|
||||
z[1] = std::numeric_limits<float>::max();
|
||||
|
||||
const auto intersection = [f, v](const std::int32_t q, const std::int32_t index) {
|
||||
const std::int32_t p = v[index];
|
||||
return ((f[q] + static_cast<float>(q) * static_cast<float>(q)) - (f[p] + static_cast<float>(p) * static_cast<float>(p)))
|
||||
/ (2.0f * static_cast<float>(q - p));
|
||||
};
|
||||
|
||||
for (std::int32_t q = 1; q < n; ++q) {
|
||||
float s = intersection(q, k);
|
||||
// z[0] is the lowest float, so k never drops below 0
|
||||
while (s <= z[k]) {
|
||||
--k;
|
||||
s = intersection(q, k);
|
||||
}
|
||||
++k;
|
||||
v[k] = q;
|
||||
z[k] = s;
|
||||
z[k + 1] = std::numeric_limits<float>::max();
|
||||
}
|
||||
|
||||
k = 0;
|
||||
for (std::int32_t q = 0; q < n; ++q) {
|
||||
while (z[k + 1] < static_cast<float>(q)) ++k;
|
||||
const float delta = static_cast<float>(q - v[k]);
|
||||
d[q] = delta * delta + f[v[k]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-place 2D squared Euclidean distance transform. Feature cells must be 0, others a large value.
|
||||
*/
|
||||
void DistanceTransform2D(std::vector<float> &grid, const std::int32_t width, const std::int32_t height) {
|
||||
const std::int32_t length = std::max(width, height);
|
||||
std::vector<float> f(length), d(length), z(length + 1);
|
||||
std::vector<std::int32_t> v(length);
|
||||
|
||||
for (std::int32_t x = 0; x < width; ++x) {
|
||||
for (std::int32_t y = 0; y < height; ++y) f[y] = grid[static_cast<std::size_t>(y) * width + x];
|
||||
DistanceTransform1D(f.data(), d.data(), v.data(), z.data(), height);
|
||||
for (std::int32_t y = 0; y < height; ++y) grid[static_cast<std::size_t>(y) * width + x] = d[y];
|
||||
}
|
||||
|
||||
for (std::int32_t y = 0; y < height; ++y) {
|
||||
float *row = grid.data() + static_cast<std::size_t>(y) * width;
|
||||
std::copy_n(row, width, f.data());
|
||||
DistanceTransform1D(f.data(), row, v.data(), z.data(), width);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signed distance to the coastline, measured in province raster texels: positive in water, negative on land.
|
||||
* The field is sampled every factor texels of the province raster to bound memory on large maps.
|
||||
* The coastline is placed on the edges of the land texels.
|
||||
*/
|
||||
std::vector<float> MakeCoastDistanceField(const ProvinceRaster &raster, const std::int32_t factor, glm::ivec2 &fieldSize) {
|
||||
constexpr float FAR = 1.0e20f;
|
||||
fieldSize = (raster.size - 1) / factor + 1;
|
||||
const std::size_t count = static_cast<std::size_t>(fieldSize.x) * static_cast<std::size_t>(fieldSize.y);
|
||||
|
||||
std::vector<std::uint8_t> land(count);
|
||||
std::vector<float> toLand(count), toWater(count);
|
||||
for (std::int32_t y = 0; y < fieldSize.y; ++y) {
|
||||
for (std::int32_t x = 0; x < fieldSize.x; ++x) {
|
||||
const std::size_t i = static_cast<std::size_t>(y) * fieldSize.x + x;
|
||||
land[i] = raster.ids[static_cast<std::size_t>(y * factor) * raster.size.x + x * factor] != 0;
|
||||
toLand[i] = land[i] ? 0.0f : FAR;
|
||||
toWater[i] = land[i] ? FAR : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
DistanceTransform2D(toLand, fieldSize.x, fieldSize.y);
|
||||
DistanceTransform2D(toWater, fieldSize.x, fieldSize.y);
|
||||
|
||||
std::vector<float> field(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
const float distance = land[i]
|
||||
? -(std::sqrt(toWater[i]) - 0.5f)
|
||||
: std::sqrt(toLand[i]) - 0.5f;
|
||||
field[i] = std::clamp(distance * static_cast<float>(factor), -MAX_COAST_DISTANCE, MAX_COAST_DISTANCE);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outward miter normals of the province outline for every vertex (zero for inner vertices).
|
||||
* The outline consists of the triangle edges that are not shared by two triangles.
|
||||
*/
|
||||
std::vector<glm::vec2> ComputeOutlineNormals(
|
||||
const std::vector<glm::vec2> &vertices,
|
||||
const std::vector<std::uint16_t> &indices
|
||||
) {
|
||||
struct Edge {
|
||||
std::uint32_t count = 0;
|
||||
std::uint16_t a = 0;
|
||||
std::uint16_t b = 0;
|
||||
std::uint16_t opposite = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<std::uint32_t, Edge> edges;
|
||||
edges.reserve(indices.size());
|
||||
|
||||
const auto addEdge = [&edges](const std::uint16_t a, const std::uint16_t b, const std::uint16_t opposite) {
|
||||
const std::uint32_t key = a < b
|
||||
? (static_cast<std::uint32_t>(a) << 16) | b
|
||||
: (static_cast<std::uint32_t>(b) << 16) | a;
|
||||
Edge &edge = edges[key];
|
||||
++edge.count;
|
||||
edge.a = a;
|
||||
edge.b = b;
|
||||
edge.opposite = opposite;
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||
const std::uint16_t a = indices[i], b = indices[i + 1], c = indices[i + 2];
|
||||
if (a >= vertices.size() || b >= vertices.size() || c >= vertices.size()) continue;
|
||||
addEdge(a, b, c);
|
||||
addEdge(b, c, a);
|
||||
addEdge(c, a, b);
|
||||
}
|
||||
|
||||
struct OutlineEdge {
|
||||
std::uint16_t a, b;
|
||||
glm::vec2 normal;
|
||||
};
|
||||
std::vector<OutlineEdge> outline;
|
||||
std::vector<glm::vec2> sums(vertices.size(), glm::vec2(0.0f));
|
||||
|
||||
for (const Edge &edge : edges | std::views::values) {
|
||||
if (edge.count != 1) continue;
|
||||
|
||||
const glm::vec2 direction = vertices[edge.b] - vertices[edge.a];
|
||||
if (glm::dot(direction, direction) < 1e-8f) continue;
|
||||
|
||||
glm::vec2 normal = glm::normalize(glm::vec2(direction.y, -direction.x));
|
||||
if (glm::dot(normal, vertices[edge.opposite] - vertices[edge.a]) > 0.0f)
|
||||
normal = -normal;
|
||||
|
||||
sums[edge.a] += normal;
|
||||
sums[edge.b] += normal;
|
||||
outline.push_back({edge.a, edge.b, normal});
|
||||
}
|
||||
|
||||
std::vector<glm::vec2> normals(vertices.size(), glm::vec2(0.0f));
|
||||
std::vector<float> minCos(vertices.size(), 1.0f);
|
||||
for (std::size_t i = 0; i < vertices.size(); ++i) {
|
||||
if (glm::dot(sums[i], sums[i]) > 1e-8f)
|
||||
normals[i] = glm::normalize(sums[i]);
|
||||
}
|
||||
for (const auto &[a, b, normal] : outline) {
|
||||
minCos[a] = std::min(minCos[a], glm::dot(normals[a], normal));
|
||||
minCos[b] = std::min(minCos[b], glm::dot(normals[b], normal));
|
||||
}
|
||||
for (std::size_t i = 0; i < vertices.size(); ++i) {
|
||||
// Miter length, limited on sharp spikes
|
||||
normals[i] /= std::max(minCos[i], 0.35f);
|
||||
}
|
||||
|
||||
return normals;
|
||||
}
|
||||
|
||||
bool IsSrgbFormat(const vk::Format format) {
|
||||
switch (format) {
|
||||
case vk::Format::eB8G8R8A8Srgb:
|
||||
case vk::Format::eR8G8B8A8Srgb:
|
||||
case vk::Format::eA8B8G8R8SrgbPack32:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ee_vulkan::VulkanRenderer::VulkanRenderer(GLFWwindow *window) : IRenderer(window) {
|
||||
std::uint32_t glfwExtensionsCount;
|
||||
const char **glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionsCount);
|
||||
@ -56,7 +493,7 @@ void ee_vulkan::VulkanRenderer::Init() {
|
||||
};
|
||||
rmlRenderInterface.emplace(rmlContext);
|
||||
|
||||
viewProjectionUniform.emplace(*context, MAX_FRAMES_IN_FLIGHT);
|
||||
mapUniform.emplace(*context, MAX_FRAMES_IN_FLIGHT);
|
||||
|
||||
LoadProvinces();
|
||||
|
||||
@ -224,25 +661,31 @@ void ee_vulkan::VulkanRenderer::LoadProvincesMaterial() {
|
||||
const vulkan_render::ShaderBindings shaderBindings {
|
||||
.vertexBinding = {
|
||||
.binding = 0,
|
||||
.stride = sizeof(glm::vec2),
|
||||
.stride = sizeof(ProvinceVertex),
|
||||
.inputRate = vk::VertexInputRate::eVertex
|
||||
},
|
||||
.vertexAttributesCount = 1,
|
||||
.vertexAttributesCount = 2,
|
||||
.vertexAttributes = {
|
||||
{
|
||||
.location = 0,
|
||||
.binding = 0,
|
||||
.format = vulkan_render::ShaderAttributeFormat::VEC2,
|
||||
.offset = 0
|
||||
},
|
||||
{
|
||||
.location = 1,
|
||||
.binding = 0,
|
||||
.format = vulkan_render::ShaderAttributeFormat::VEC2,
|
||||
.offset = sizeof(glm::vec2)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const vk::DescriptorSetLayoutBinding viewProjection {
|
||||
const vk::DescriptorSetLayoutBinding mapData {
|
||||
.binding = 0,
|
||||
.descriptorType = vk::DescriptorType::eUniformBuffer,
|
||||
.descriptorCount = 1,
|
||||
.stageFlags = vk::ShaderStageFlagBits::eVertex,
|
||||
.stageFlags = vk::Flags(vk::ShaderStageFlagBits::eVertex) | vk::ShaderStageFlagBits::eFragment,
|
||||
.pImmutableSamplers = nullptr
|
||||
};
|
||||
const vk::DescriptorSetLayoutBinding provincesSSBO {
|
||||
@ -252,6 +695,20 @@ void ee_vulkan::VulkanRenderer::LoadProvincesMaterial() {
|
||||
.stageFlags = vk::Flags(vk::ShaderStageFlagBits::eVertex) | vk::ShaderStageFlagBits::eFragment,
|
||||
.pImmutableSamplers = nullptr
|
||||
};
|
||||
const vk::DescriptorSetLayoutBinding provinceIds {
|
||||
.binding = 2,
|
||||
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
|
||||
.descriptorCount = 1,
|
||||
.stageFlags = vk::ShaderStageFlagBits::eFragment,
|
||||
.pImmutableSamplers = nullptr
|
||||
};
|
||||
const vk::DescriptorSetLayoutBinding coastDistance {
|
||||
.binding = 3,
|
||||
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
|
||||
.descriptorCount = 1,
|
||||
.stageFlags = vk::ShaderStageFlagBits::eFragment,
|
||||
.pImmutableSamplers = nullptr
|
||||
};
|
||||
|
||||
const vk::PushConstantRange model {
|
||||
.stageFlags = vk::ShaderStageFlagBits::eVertex,
|
||||
@ -262,7 +719,7 @@ void ee_vulkan::VulkanRenderer::LoadProvincesMaterial() {
|
||||
vulkan_render::PipelineConfig provincesMaterial = vulkan_render::Pipeline::MakeConfig(
|
||||
surface,
|
||||
shaderBindings,
|
||||
{viewProjection, provincesSSBO},
|
||||
{mapData, provincesSSBO, provinceIds, coastDistance},
|
||||
model
|
||||
);
|
||||
vulkan_render::ShaderProgram provincesShader {
|
||||
@ -349,27 +806,72 @@ void ee_vulkan::VulkanRenderer::LoadRmlMaterial() {
|
||||
}
|
||||
|
||||
void ee_vulkan::VulkanRenderer::LoadProvinces() {
|
||||
const auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
// Map extent and number of provinces
|
||||
Bounds bounds;
|
||||
std::uint32_t provincesCount = 0;
|
||||
for (const auto &provs : mapMesh->regions | std::views::values) {
|
||||
provincesCount += static_cast<std::uint32_t>(provs.size());
|
||||
for (const auto &province : provs) {
|
||||
for (const glm::vec2 &vertex : province.vertices) bounds.Add(vertex);
|
||||
}
|
||||
}
|
||||
|
||||
const std::uint32_t maxDimension = std::min<std::uint32_t>(
|
||||
context->physicalDeviceCapabilities.properties.limits.maxImageDimension2D,
|
||||
16384
|
||||
);
|
||||
ProvinceRaster raster(bounds, ProvinceRaster::ChooseTexelSize(bounds, maxDimension, MAX_RASTER_TEXELS));
|
||||
provinces.oceanInstance = provincesCount;
|
||||
|
||||
std::vector<ee_engine::ProvinceSSBO> provincesData;
|
||||
std::vector<glm::vec2> verticesData;
|
||||
std::vector<ProvinceVertex> verticesData;
|
||||
std::vector<std::uint32_t> indicesData;
|
||||
std::vector<vk::DrawIndexedIndirectCommand> indirectCommandsData;
|
||||
provincesData.reserve(provincesCount + 1);
|
||||
indirectCommandsData.reserve(provincesCount + 1);
|
||||
|
||||
// Ocean plane is the first command of the multi-draw, so provinces are drawn on top of it.
|
||||
// Its storage element is the last one to keep regions offsets unchanged.
|
||||
verticesData.append_range(std::array<ProvinceVertex, 4>{{
|
||||
{.position = {-OCEAN_PLANE_EXTENT, -OCEAN_PLANE_EXTENT}, .normal = {0.0f, 0.0f}},
|
||||
{.position = { OCEAN_PLANE_EXTENT, -OCEAN_PLANE_EXTENT}, .normal = {0.0f, 0.0f}},
|
||||
{.position = { OCEAN_PLANE_EXTENT, OCEAN_PLANE_EXTENT}, .normal = {0.0f, 0.0f}},
|
||||
{.position = {-OCEAN_PLANE_EXTENT, OCEAN_PLANE_EXTENT}, .normal = {0.0f, 0.0f}}
|
||||
}});
|
||||
indicesData.append_range(std::array<std::uint32_t, 6>{0, 1, 2, 0, 2, 3});
|
||||
indirectCommandsData.push_back({
|
||||
.indexCount = 6,
|
||||
.instanceCount = 1,
|
||||
.firstIndex = 0,
|
||||
.vertexOffset = 0,
|
||||
.firstInstance = provinces.oceanInstance
|
||||
});
|
||||
|
||||
std::size_t regionOffset = 0;
|
||||
std::uint32_t indicesOffset = 0;
|
||||
std::int32_t verticesOffset = 0;
|
||||
std::uint32_t indicesOffset = static_cast<std::uint32_t>(indicesData.size());
|
||||
std::int32_t verticesOffset = static_cast<std::int32_t>(verticesData.size());
|
||||
for (auto& [region, provs] : mapMesh->regions) {
|
||||
for (auto&[vertices, indices] : provs) {
|
||||
const auto instance = static_cast<std::uint32_t>(provincesData.size());
|
||||
|
||||
indirectCommandsData.push_back({
|
||||
.indexCount = static_cast<std::uint32_t>(indices.size()),
|
||||
.instanceCount = 1,
|
||||
.firstIndex = indicesOffset,
|
||||
.vertexOffset = verticesOffset,
|
||||
.firstInstance = static_cast<std::uint32_t>(provincesData.size())
|
||||
.firstInstance = instance
|
||||
});
|
||||
|
||||
verticesData.append_range(vertices);
|
||||
const std::vector<glm::vec2> normals = ComputeOutlineNormals(vertices, indices);
|
||||
for (std::size_t i = 0; i < vertices.size(); ++i) {
|
||||
verticesData.push_back({.position = vertices[i], .normal = normals[i]});
|
||||
}
|
||||
indicesData.append_range(indices);
|
||||
|
||||
RasterizeProvince(raster, vertices, indices, instance + 1);
|
||||
|
||||
ee_engine::ProvinceSSBO data{
|
||||
.regionId = region,
|
||||
.color = 0xFF0000
|
||||
@ -384,6 +886,11 @@ void ee_vulkan::VulkanRenderer::LoadProvinces() {
|
||||
regionOffset += provs.size() * sizeof(ee_engine::ProvinceSSBO);
|
||||
}
|
||||
|
||||
provincesData.push_back({
|
||||
.regionId = OCEAN_REGION_ID,
|
||||
.color = 0
|
||||
});
|
||||
|
||||
provinces.vertices.emplace(*context, verticesData.size());
|
||||
provinces.indices.emplace(*context, indicesData.size());
|
||||
provinces.storage.emplace(*context, provincesData.size());
|
||||
@ -398,18 +905,155 @@ void ee_vulkan::VulkanRenderer::LoadProvinces() {
|
||||
renderCommands->SubmitStagingBuffer(*provinces.indices);
|
||||
renderCommands->SubmitStagingBuffer(*provinces.storage);
|
||||
renderCommands->SubmitStagingBuffer(*provinces.indirectCommands);
|
||||
|
||||
// Pixel-exact province coverage and coastal distance field for the map shaders
|
||||
CloseRasterGaps(raster, 3);
|
||||
FillEnclosedWater(raster, 32);
|
||||
|
||||
std::int32_t fieldFactor = 1;
|
||||
while (
|
||||
static_cast<std::uint64_t>((raster.size.x - 1) / fieldFactor + 1) *
|
||||
static_cast<std::uint64_t>((raster.size.y - 1) / fieldFactor + 1) > MAX_COAST_FIELD_TEXELS
|
||||
) ++fieldFactor;
|
||||
|
||||
glm::ivec2 fieldSize;
|
||||
const std::vector<float> coastField = MakeCoastDistanceField(raster, fieldFactor, fieldSize);
|
||||
|
||||
provinces.rasterOrigin = raster.origin;
|
||||
provinces.rasterSize = glm::uvec2(raster.size);
|
||||
provinces.rasterTexelSize = static_cast<std::uint32_t>(raster.texelSize);
|
||||
provinces.coastFieldSize = glm::uvec2(fieldSize);
|
||||
provinces.coastFieldFactor = static_cast<std::uint32_t>(fieldFactor);
|
||||
|
||||
LoadMapTextures(raster.ids, coastField, provincesCount);
|
||||
|
||||
LOG(INFO, "Map loaded: %d provinces, raster %dx%d (%d map px per texel), coast field %dx%d, %d ms",
|
||||
static_cast<int>(provincesCount),
|
||||
raster.size.x, raster.size.y, raster.texelSize,
|
||||
fieldSize.x, fieldSize.y,
|
||||
static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - startTime
|
||||
).count())
|
||||
);
|
||||
}
|
||||
|
||||
void ee_vulkan::VulkanRenderer::LoadMapTextures(
|
||||
const std::vector<std::uint32_t> &ids,
|
||||
const std::vector<float> &coastField,
|
||||
const std::uint32_t provincesCount
|
||||
) {
|
||||
const glm::uvec2 size = provinces.rasterSize;
|
||||
const glm::uvec2 fieldSize = provinces.coastFieldSize;
|
||||
|
||||
const auto upload = [this](vulkan_render::PartialTexture &texture, const std::span<const std::byte> data) {
|
||||
const vulkan_render::Buffer<
|
||||
std::byte,
|
||||
vk::BufferUsageFlagBits::eTransferSrc,
|
||||
vulkan_render::BufferMode::STATIC
|
||||
> stagingBuffer(*context, data);
|
||||
|
||||
renderCommands->TranslateTextureLayout(
|
||||
texture,
|
||||
vk::ImageLayout::eUndefined,
|
||||
vk::ImageLayout::eTransferDstOptimal
|
||||
);
|
||||
renderCommands->SubmitTexture(stagingBuffer, texture);
|
||||
renderCommands->TranslateTextureLayout(
|
||||
texture,
|
||||
vk::ImageLayout::eTransferDstOptimal,
|
||||
vk::ImageLayout::eReadOnlyOptimal
|
||||
);
|
||||
texture.Init(vk::ImageAspectFlagBits::eColor);
|
||||
};
|
||||
|
||||
// Province ids, 16 bit when possible
|
||||
if (provincesCount < std::numeric_limits<std::uint16_t>::max()) {
|
||||
std::vector<std::uint16_t> packed(ids.size());
|
||||
std::ranges::transform(ids, packed.begin(), [](const std::uint32_t id) { return static_cast<std::uint16_t>(id); });
|
||||
|
||||
provinces.provinceIds.emplace(*context, size.x, size.y, vk::Format::eR16Uint);
|
||||
upload(*provinces.provinceIds, std::as_bytes(std::span(packed)));
|
||||
}
|
||||
else {
|
||||
provinces.provinceIds.emplace(*context, size.x, size.y, vk::Format::eR32Uint);
|
||||
upload(*provinces.provinceIds, std::as_bytes(std::span(ids)));
|
||||
}
|
||||
|
||||
// Coast distance field, half floats
|
||||
{
|
||||
std::vector<std::uint16_t> packed(coastField.size());
|
||||
std::ranges::transform(coastField, packed.begin(), [](const float distance) { return glm::packHalf1x16(distance); });
|
||||
|
||||
provinces.coastDistance.emplace(*context, fieldSize.x, fieldSize.y, vk::Format::eR16Sfloat);
|
||||
upload(*provinces.coastDistance, std::as_bytes(std::span(packed)));
|
||||
}
|
||||
|
||||
provinces.nearestSampler = context->device.handle.createSampler({
|
||||
.magFilter = vk::Filter::eNearest,
|
||||
.minFilter = vk::Filter::eNearest,
|
||||
.mipmapMode = vk::SamplerMipmapMode::eNearest,
|
||||
.addressModeU = vk::SamplerAddressMode::eClampToEdge,
|
||||
.addressModeV = vk::SamplerAddressMode::eClampToEdge,
|
||||
.addressModeW = vk::SamplerAddressMode::eClampToEdge,
|
||||
.mipLodBias = 0.0f,
|
||||
.anisotropyEnable = vk::False,
|
||||
.compareEnable = vk::False,
|
||||
.compareOp = vk::CompareOp::eAlways,
|
||||
.minLod = 0.0f,
|
||||
.maxLod = 0.0f,
|
||||
.borderColor = vk::BorderColor::eIntOpaqueBlack,
|
||||
.unnormalizedCoordinates = vk::False
|
||||
});
|
||||
provinces.linearSampler = context->device.handle.createSampler({
|
||||
.magFilter = vk::Filter::eLinear,
|
||||
.minFilter = vk::Filter::eLinear,
|
||||
.mipmapMode = vk::SamplerMipmapMode::eNearest,
|
||||
.addressModeU = vk::SamplerAddressMode::eClampToEdge,
|
||||
.addressModeV = vk::SamplerAddressMode::eClampToEdge,
|
||||
.addressModeW = vk::SamplerAddressMode::eClampToEdge,
|
||||
.mipLodBias = 0.0f,
|
||||
.anisotropyEnable = vk::False,
|
||||
.compareEnable = vk::False,
|
||||
.compareOp = vk::CompareOp::eAlways,
|
||||
.minLod = 0.0f,
|
||||
.maxLod = 0.0f,
|
||||
.borderColor = vk::BorderColor::eFloatOpaqueBlack,
|
||||
.unnormalizedCoordinates = vk::False
|
||||
});
|
||||
}
|
||||
|
||||
void ee_vulkan::VulkanRenderer::UpdateData(const std::uint32_t frame) {
|
||||
std::span<glm::mat4> &vpBuffer = viewProjectionUniform->buffers[frame].content;
|
||||
vpBuffer[0] = viewProjection;
|
||||
MapUniform &mapData = mapUniform->buffers[frame].content[0];
|
||||
|
||||
const float projectionScale = std::abs(viewProjection[0][0]) * static_cast<float>(surface.extent.width);
|
||||
mapData.viewProjection = viewProjection;
|
||||
mapData.inverseViewProjection = glm::inverse(viewProjection);
|
||||
mapData.viewport = glm::vec4(
|
||||
static_cast<float>(surface.extent.width),
|
||||
static_cast<float>(surface.extent.height),
|
||||
projectionScale > 0.0f ? 2.0f / projectionScale : 1.0f,
|
||||
IsSrgbFormat(surface.format.format) ? 1.0f : 0.0f
|
||||
);
|
||||
mapData.raster = glm::vec4(
|
||||
static_cast<float>(provinces.rasterOrigin.x),
|
||||
static_cast<float>(provinces.rasterOrigin.y),
|
||||
static_cast<float>(provinces.rasterSize.x),
|
||||
static_cast<float>(provinces.rasterSize.y)
|
||||
);
|
||||
mapData.coast = glm::vec4(
|
||||
static_cast<float>(provinces.rasterTexelSize),
|
||||
static_cast<float>(provinces.coastFieldFactor),
|
||||
static_cast<float>(provinces.coastFieldSize.x),
|
||||
static_cast<float>(provinces.coastFieldSize.y)
|
||||
);
|
||||
mapData.info = glm::uvec4(provinces.oceanInstance, 0, 0, 0);
|
||||
|
||||
provincesPipeline->UpdateDescriptors({
|
||||
{
|
||||
.info = vk::DescriptorBufferInfo{
|
||||
.buffer = viewProjectionUniform->buffers[frame].handle,
|
||||
.buffer = mapUniform->buffers[frame].handle,
|
||||
.offset = 0,
|
||||
.range = viewProjectionUniform->buffers[frame].memorySize
|
||||
.range = mapUniform->buffers[frame].memorySize
|
||||
},
|
||||
.bindingPosition = 0,
|
||||
.type = vk::DescriptorType::eUniformBuffer
|
||||
@ -422,6 +1066,24 @@ void ee_vulkan::VulkanRenderer::UpdateData(const std::uint32_t frame) {
|
||||
},
|
||||
.bindingPosition = 1,
|
||||
.type = vk::DescriptorType::eStorageBuffer
|
||||
},
|
||||
{
|
||||
.info = vk::DescriptorImageInfo{
|
||||
.sampler = provinces.nearestSampler,
|
||||
.imageView = provinces.provinceIds->view,
|
||||
.imageLayout = vk::ImageLayout::eReadOnlyOptimal
|
||||
},
|
||||
.bindingPosition = 2,
|
||||
.type = vk::DescriptorType::eCombinedImageSampler
|
||||
},
|
||||
{
|
||||
.info = vk::DescriptorImageInfo{
|
||||
.sampler = provinces.linearSampler,
|
||||
.imageView = provinces.coastDistance->view,
|
||||
.imageLayout = vk::ImageLayout::eReadOnlyOptimal
|
||||
},
|
||||
.bindingPosition = 3,
|
||||
.type = vk::DescriptorType::eCombinedImageSampler
|
||||
}
|
||||
}, frame);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user