diff --git a/shaders/terrain.frag b/shaders/terrain.frag index 5e89592..8b742f2 100644 --- a/shaders/terrain.frag +++ b/shaders/terrain.frag @@ -3,6 +3,7 @@ layout(location = 0) in vec3 fragNormal; layout(location = 1) in vec2 fragTexCoord; layout(location = 2) in vec3 fragWorldPos; +layout(location = 3) in vec2 fragAtlasCoord; layout(location = 0) out vec4 outColor; @@ -20,7 +21,7 @@ void main() { vec3 baseColor; if (material.useTexture == 1u) { - baseColor = texture(texSampler, fragTexCoord).rgb; + baseColor = texture(texSampler, fragAtlasCoord).rgb; } else { float height = fragWorldPos.y; float t = clamp(height / 100.0, 0.0, 1.0); diff --git a/shaders/terrain.vert b/shaders/terrain.vert index 5da3722..6dfc732 100644 --- a/shaders/terrain.vert +++ b/shaders/terrain.vert @@ -9,10 +9,12 @@ layout(set = 0, binding = 0) uniform UniformBufferObject { layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inNormal; layout(location = 2) in vec2 inTexCoord; +layout(location = 3) in vec2 inAtlasCoord; layout(location = 0) out vec3 fragNormal; layout(location = 1) out vec2 fragTexCoord; layout(location = 2) out vec3 fragWorldPos; +layout(location = 3) out vec2 fragAtlasCoord; void main() { vec4 worldPos = ubo.model * vec4(inPosition, 1.0); @@ -21,4 +23,5 @@ void main() { fragNormal = mat3(transpose(inverse(ubo.model))) * inNormal; fragTexCoord = inTexCoord; fragWorldPos = worldPos.xyz; + fragAtlasCoord = inAtlasCoord; } diff --git a/src/lib/gfx/pipeline.hpp b/src/lib/gfx/pipeline.hpp index def58e1..5bd8e75 100644 --- a/src/lib/gfx/pipeline.hpp +++ b/src/lib/gfx/pipeline.hpp @@ -159,11 +159,12 @@ struct PipelineCreateInfo { info.fragShaderPath = "shaders/terrain.frag.spv"; info.vertexInput.binding = - vk::VertexInputBindingDescription{0, 32, vk::VertexInputRate::eVertex}; + vk::VertexInputBindingDescription{0, 40, vk::VertexInputRate::eVertex}; info.vertexInput.attributes = { vk::VertexInputAttributeDescription{0, 0, vk::Format::eR32G32B32Sfloat, 0 }, vk::VertexInputAttributeDescription{1, 0, vk::Format::eR32G32B32Sfloat, 12}, - vk::VertexInputAttributeDescription{2, 0, vk::Format::eR32G32Sfloat, 24} + vk::VertexInputAttributeDescription{2, 0, vk::Format::eR32G32Sfloat, 24}, + vk::VertexInputAttributeDescription{3, 0, vk::Format::eR32G32Sfloat, 32} }; info.descriptorBindings = { diff --git a/src/render/terrain/terrain_atlas.cpp b/src/render/terrain/terrain_atlas.cpp new file mode 100644 index 0000000..6f7f988 --- /dev/null +++ b/src/render/terrain/terrain_atlas.cpp @@ -0,0 +1,148 @@ +#include "render/terrain/terrain_atlas.hpp" + +#include +#include + +namespace w3d::terrain { + +int32_t decodeTileIndex(int16_t tileNdx) { + return static_cast((static_cast(tileNdx) >> 2) & 0x3FFF); +} + +int32_t decodeQuadrant(int16_t tileNdx) { + return static_cast(static_cast(tileNdx) & 0x3); +} + +TileUV computeQuadrantUV(const TileUV &tileUV, int32_t quadrant) { + TileUV result = tileUV; + result.uSize = tileUV.uSize * 0.5f; + result.vSize = tileUV.vSize * 0.5f; + + if (quadrant == 1 || quadrant == 3) { + result.u = tileUV.u + result.uSize; + } + if (quadrant == 2 || quadrant == 3) { + result.v = tileUV.v + result.vSize; + } + return result; +} + +TileUV decodeTileNdxUV(int16_t tileNdx, const std::vector &tileUVs) { + int32_t tileIndex = decodeTileIndex(tileNdx); + int32_t quadrant = decodeQuadrant(tileNdx); + + if (tileIndex < 0 || static_cast(tileIndex) >= tileUVs.size()) { + return TileUV{}; + } + + return computeQuadrantUV(tileUVs[static_cast(tileIndex)], quadrant); +} + +std::vector computeTileUVTable(const std::vector &textureClasses, + int32_t atlasWidth, int32_t tilePixelSize) { + if (textureClasses.empty() || atlasWidth <= 0 || tilePixelSize <= 0) { + return {}; + } + + int32_t tilesPerRow = atlasWidth / tilePixelSize; + if (tilesPerRow <= 0) { + return {}; + } + + int32_t totalTiles = 0; + for (const auto &tc : textureClasses) { + totalTiles += tc.numTiles; + } + + if (totalTiles <= 0) { + return {}; + } + + int32_t totalRows = (totalTiles + tilesPerRow - 1) / tilesPerRow; + int32_t atlasHeight = totalRows * tilePixelSize; + + float uStep = static_cast(tilePixelSize) / static_cast(atlasWidth); + float vStep = static_cast(tilePixelSize) / static_cast(atlasHeight); + + std::vector result; + result.reserve(static_cast(totalTiles)); + + int32_t tileIdx = 0; + for (const auto &tc : textureClasses) { + for (int32_t i = 0; i < tc.numTiles; ++i) { + int32_t col = tileIdx % tilesPerRow; + int32_t row = tileIdx / tilesPerRow; + + TileUV uv; + uv.u = static_cast(col) * uStep; + uv.v = static_cast(row) * vStep; + uv.uSize = uStep; + uv.vSize = vStep; + result.push_back(uv); + + ++tileIdx; + } + } + + return result; +} + +TerrainAtlasData buildProceduralAtlas(int32_t numTiles, int32_t atlasWidth, int32_t tilePixelSize) { + if (numTiles <= 0 || atlasWidth <= 0 || tilePixelSize <= 0) { + return {}; + } + + int32_t tilesPerRow = atlasWidth / tilePixelSize; + if (tilesPerRow <= 0) { + return {}; + } + + int32_t totalRows = (numTiles + tilesPerRow - 1) / tilesPerRow; + int32_t atlasHeight = totalRows * tilePixelSize; + + TerrainAtlasData data; + data.atlasWidth = atlasWidth; + data.atlasHeight = atlasHeight; + data.tilePixelSize = tilePixelSize; + data.tilesPerRow = tilesPerRow; + + data.pixels.resize(static_cast(atlasWidth * atlasHeight * 4), 0); + + float uStep = static_cast(tilePixelSize) / static_cast(atlasWidth); + float vStep = static_cast(tilePixelSize) / static_cast(atlasHeight); + + data.tileUVs.reserve(static_cast(numTiles)); + + for (int32_t t = 0; t < numTiles; ++t) { + int32_t col = t % tilesPerRow; + int32_t row = t / tilesPerRow; + + uint8_t r = static_cast((t * 37 + 50) & 0xFF); + uint8_t g = static_cast((t * 73 + 100) & 0xFF); + uint8_t b = static_cast((t * 113 + 150) & 0xFF); + + int32_t startPx = col * tilePixelSize; + int32_t startPy = row * tilePixelSize; + + for (int32_t py = startPy; py < startPy + tilePixelSize && py < atlasHeight; ++py) { + for (int32_t px = startPx; px < startPx + tilePixelSize && px < atlasWidth; ++px) { + size_t idx = static_cast((py * atlasWidth + px) * 4); + data.pixels[idx + 0] = r; + data.pixels[idx + 1] = g; + data.pixels[idx + 2] = b; + data.pixels[idx + 3] = 255; + } + } + + TileUV uv; + uv.u = static_cast(col) * uStep; + uv.v = static_cast(row) * vStep; + uv.uSize = uStep; + uv.vSize = vStep; + data.tileUVs.push_back(uv); + } + + return data; +} + +} // namespace w3d::terrain diff --git a/src/render/terrain/terrain_atlas.hpp b/src/render/terrain/terrain_atlas.hpp new file mode 100644 index 0000000..83e3539 --- /dev/null +++ b/src/render/terrain/terrain_atlas.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "lib/formats/map/types.hpp" + +namespace w3d::terrain { + +struct TileUV { + float u = 0.0f; + float v = 0.0f; + float uSize = 0.0f; + float vSize = 0.0f; +}; + +struct TerrainAtlasData { + int32_t atlasWidth = 0; + int32_t atlasHeight = 0; + int32_t tilePixelSize = 64; + int32_t tilesPerRow = 0; + std::vector pixels; + std::vector tileUVs; + + bool isValid() const { return atlasWidth > 0 && atlasHeight > 0 && !pixels.empty(); } +}; + +[[nodiscard]] int32_t decodeTileIndex(int16_t tileNdx); + +[[nodiscard]] int32_t decodeQuadrant(int16_t tileNdx); + +[[nodiscard]] TileUV computeQuadrantUV(const TileUV &tileUV, int32_t quadrant); + +[[nodiscard]] TileUV decodeTileNdxUV(int16_t tileNdx, const std::vector &tileUVs); + +[[nodiscard]] std::vector +computeTileUVTable(const std::vector &textureClasses, int32_t atlasWidth = 2048, + int32_t tilePixelSize = 64); + +[[nodiscard]] TerrainAtlasData buildProceduralAtlas(int32_t numTiles, int32_t atlasWidth = 2048, + int32_t tilePixelSize = 64); + +} // namespace w3d::terrain diff --git a/src/render/terrain/terrain_blend.cpp b/src/render/terrain/terrain_blend.cpp new file mode 100644 index 0000000..bf472ef --- /dev/null +++ b/src/render/terrain/terrain_blend.cpp @@ -0,0 +1,113 @@ +#include "render/terrain/terrain_blend.hpp" + +#include +#include + +namespace w3d::terrain { + +BlendPattern generateBlendPattern(BlendDirection direction) { + BlendPattern pattern; + pattern.size = BLEND_PATTERN_SIZE; + pattern.alpha.resize(static_cast(BLEND_PATTERN_SIZE * BLEND_PATTERN_SIZE), 0); + + for (int32_t y = 0; y < BLEND_PATTERN_SIZE; ++y) { + for (int32_t x = 0; x < BLEND_PATTERN_SIZE; ++x) { + float nx = static_cast(x) / static_cast(BLEND_PATTERN_SIZE - 1); + float ny = static_cast(y) / static_cast(BLEND_PATTERN_SIZE - 1); + float value = 0.0f; + + switch (direction) { + case BlendDirection::Horizontal: + value = nx; + break; + case BlendDirection::HorizontalInv: + value = 1.0f - nx; + break; + case BlendDirection::Vertical: + value = ny; + break; + case BlendDirection::VerticalInv: + value = 1.0f - ny; + break; + case BlendDirection::DiagonalRight: + value = std::clamp((nx + ny) * 0.5f, 0.0f, 1.0f); + break; + case BlendDirection::DiagonalRightInv: + value = 1.0f - std::clamp((nx + ny) * 0.5f, 0.0f, 1.0f); + break; + case BlendDirection::DiagonalLeft: + value = std::clamp(((1.0f - nx) + ny) * 0.5f, 0.0f, 1.0f); + break; + case BlendDirection::DiagonalLeftInv: + value = 1.0f - std::clamp(((1.0f - nx) + ny) * 0.5f, 0.0f, 1.0f); + break; + case BlendDirection::LongDiagonal: + value = std::clamp((2.0f * nx + ny) / 3.0f, 0.0f, 1.0f); + break; + case BlendDirection::LongDiagonalInv: + value = 1.0f - std::clamp((2.0f * nx + ny) / 3.0f, 0.0f, 1.0f); + break; + case BlendDirection::LongDiagonalAlt: + value = std::clamp((nx + 2.0f * ny) / 3.0f, 0.0f, 1.0f); + break; + case BlendDirection::LongDiagonalAltInv: + value = 1.0f - std::clamp((nx + 2.0f * ny) / 3.0f, 0.0f, 1.0f); + break; + } + + size_t idx = static_cast(y * BLEND_PATTERN_SIZE + x); + pattern.alpha[idx] = static_cast(std::clamp(value, 0.0f, 1.0f) * 255.0f); + } + } + + return pattern; +} + +std::vector generateAllBlendPatterns() { + std::vector patterns; + patterns.reserve(NUM_BLEND_PATTERNS); + + for (int32_t i = 0; i < NUM_BLEND_PATTERNS; ++i) { + patterns.push_back(generateBlendPattern(static_cast(i))); + } + + return patterns; +} + +BlendDirection blendDirectionFromInfo(const map::BlendTileInfo &info) { + if (info.horiz != 0) { + return (info.inverted & map::INVERTED_MASK) ? BlendDirection::HorizontalInv + : BlendDirection::Horizontal; + } + if (info.vert != 0) { + return (info.inverted & map::INVERTED_MASK) ? BlendDirection::VerticalInv + : BlendDirection::Vertical; + } + if (info.rightDiagonal != 0) { + return (info.inverted & map::INVERTED_MASK) ? BlendDirection::DiagonalRightInv + : BlendDirection::DiagonalRight; + } + if (info.leftDiagonal != 0) { + return (info.inverted & map::INVERTED_MASK) ? BlendDirection::DiagonalLeftInv + : BlendDirection::DiagonalLeft; + } + if (info.longDiagonal != 0) { + bool alt = (info.inverted & map::FLIPPED_MASK) != 0; + bool inv = (info.inverted & map::INVERTED_MASK) != 0; + if (alt && inv) + return BlendDirection::LongDiagonalAltInv; + if (alt) + return BlendDirection::LongDiagonalAlt; + if (inv) + return BlendDirection::LongDiagonalInv; + return BlendDirection::LongDiagonal; + } + return BlendDirection::Horizontal; +} + +bool cellHasBlend(const map::BlendTileInfo &info) { + return info.horiz != 0 || info.vert != 0 || info.rightDiagonal != 0 || info.leftDiagonal != 0 || + info.longDiagonal != 0; +} + +} // namespace w3d::terrain diff --git a/src/render/terrain/terrain_blend.hpp b/src/render/terrain/terrain_blend.hpp new file mode 100644 index 0000000..797ba18 --- /dev/null +++ b/src/render/terrain/terrain_blend.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include "lib/formats/map/types.hpp" + +namespace w3d::terrain { + +constexpr int32_t BLEND_PATTERN_SIZE = 64; +constexpr int32_t NUM_BLEND_PATTERNS = 12; + +enum class BlendDirection : int32_t { + Horizontal = 0, + HorizontalInv = 1, + Vertical = 2, + VerticalInv = 3, + DiagonalRight = 4, + DiagonalRightInv = 5, + DiagonalLeft = 6, + DiagonalLeftInv = 7, + LongDiagonal = 8, + LongDiagonalInv = 9, + LongDiagonalAlt = 10, + LongDiagonalAltInv = 11, +}; + +struct BlendPattern { + int32_t size = BLEND_PATTERN_SIZE; + std::vector alpha; +}; + +[[nodiscard]] BlendPattern generateBlendPattern(BlendDirection direction); + +[[nodiscard]] std::vector generateAllBlendPatterns(); + +[[nodiscard]] BlendDirection blendDirectionFromInfo(const map::BlendTileInfo &info); + +[[nodiscard]] bool cellHasBlend(const map::BlendTileInfo &info); + +} // namespace w3d::terrain diff --git a/src/render/terrain/terrain_mesh.cpp b/src/render/terrain/terrain_mesh.cpp index ed96484..2a6f0b8 100644 --- a/src/render/terrain/terrain_mesh.cpp +++ b/src/render/terrain/terrain_mesh.cpp @@ -64,6 +64,7 @@ TerrainChunk generateChunk(const map::HeightMap &heightMap, int32_t chunkX, int3 vert.position = heightmapToWorld(heightMap, x, y); vert.normal = computeNormal(heightMap, x, y); vert.texCoord = computeTexCoord(heightMap, x, y); + vert.atlasCoord = vert.texCoord; chunk.bounds.expand(vert.position); chunk.vertices.push_back(vert); @@ -138,4 +139,162 @@ TerrainMeshData generateTerrainMesh(const map::HeightMap &heightMap, int32_t chu return meshData; } +namespace { + +glm::vec2 cliffAtlasUV(const map::CliffInfo &cliff, int32_t cornerIdx, + const std::vector &tileUVs) { + if (cliff.tileIndex < 0 || static_cast(cliff.tileIndex) >= tileUVs.size()) { + return {0.0f, 0.0f}; + } + + const TileUV &tile = tileUVs[static_cast(cliff.tileIndex)]; + + float localU = 0.0f; + float localV = 0.0f; + switch (cornerIdx) { + case 0: + localU = cliff.u0; + localV = cliff.v0; + break; + case 1: + localU = cliff.u1; + localV = cliff.v1; + break; + case 2: + localU = cliff.u2; + localV = cliff.v2; + break; + case 3: + default: + localU = cliff.u3; + localV = cliff.v3; + break; + } + + return {tile.u + localU * tile.uSize, tile.v + localV * tile.vSize}; +} + +} // namespace + +TerrainChunk generateChunkFromBlendData(const map::HeightMap &heightMap, + const map::BlendTileData &blendTileData, + const std::vector &tileUVs, int32_t chunkX, + int32_t chunkY, int32_t chunkSize) { + TerrainChunk chunk; + chunk.chunkX = chunkX; + chunk.chunkY = chunkY; + + int32_t startX = chunkX * chunkSize; + int32_t startY = chunkY * chunkSize; + int32_t endCellX = std::min(startX + chunkSize, heightMap.width - 1); + int32_t endCellY = std::min(startY + chunkSize, heightMap.height - 1); + + int32_t cellsX = endCellX - startX; + int32_t cellsY = endCellY - startY; + + if (cellsX <= 0 || cellsY <= 0) { + return chunk; + } + + chunk.vertices.reserve(static_cast(cellsX * cellsY * 4)); + chunk.indices.reserve(static_cast(cellsX * cellsY * 6)); + + for (int32_t cy = startY; cy < endCellY; ++cy) { + for (int32_t cx = startX; cx < endCellX; ++cx) { + int32_t cellIdx = cy * heightMap.width + cx; + + bool isCliff = false; + int32_t cliffNdx = 0; + if (!blendTileData.cliffInfoNdxes.empty() && + cellIdx < static_cast(blendTileData.cliffInfoNdxes.size())) { + cliffNdx = static_cast(blendTileData.cliffInfoNdxes[static_cast(cellIdx)]); + isCliff = + cliffNdx > 0 && (cliffNdx - 1) < static_cast(blendTileData.cliffInfos.size()); + } + + TileUV cellTileUV{}; + if (!tileUVs.empty() && !blendTileData.tileNdxes.empty() && + cellIdx < static_cast(blendTileData.tileNdxes.size())) { + int16_t tileNdx = blendTileData.tileNdxes[static_cast(cellIdx)]; + cellTileUV = decodeTileNdxUV(tileNdx, tileUVs); + } + + uint32_t baseIdx = static_cast(chunk.vertices.size()); + + auto makeVert = [&](int32_t vx, int32_t vy, int32_t corner) { + TerrainVertex vert; + vert.position = heightmapToWorld(heightMap, vx, vy); + vert.normal = computeNormal(heightMap, vx, vy); + vert.texCoord = computeTexCoord(heightMap, vx, vy); + + if (isCliff) { + const auto &cliff = blendTileData.cliffInfos[static_cast(cliffNdx - 1)]; + vert.atlasCoord = cliffAtlasUV(cliff, corner, tileUVs); + } else { + float localU = static_cast(vx - cx) * cellTileUV.uSize; + float localV = static_cast(vy - cy) * cellTileUV.vSize; + vert.atlasCoord = {cellTileUV.u + localU, cellTileUV.v + localV}; + } + + chunk.bounds.expand(vert.position); + chunk.vertices.push_back(vert); + }; + + makeVert(cx, cy, 0); + makeVert(cx + 1, cy, 1); + makeVert(cx, cy + 1, 2); + makeVert(cx + 1, cy + 1, 3); + + if (shouldFlipDiagonal(heightMap, cx, cy)) { + chunk.indices.push_back(baseIdx + 0); + chunk.indices.push_back(baseIdx + 2); + chunk.indices.push_back(baseIdx + 1); + + chunk.indices.push_back(baseIdx + 1); + chunk.indices.push_back(baseIdx + 2); + chunk.indices.push_back(baseIdx + 3); + } else { + chunk.indices.push_back(baseIdx + 0); + chunk.indices.push_back(baseIdx + 2); + chunk.indices.push_back(baseIdx + 3); + + chunk.indices.push_back(baseIdx + 0); + chunk.indices.push_back(baseIdx + 3); + chunk.indices.push_back(baseIdx + 1); + } + } + } + + return chunk; +} + +TerrainMeshData generateTerrainMeshFromBlendData(const map::HeightMap &heightMap, + const map::BlendTileData &blendTileData, + const std::vector &tileUVs, + int32_t chunkSize) { + TerrainMeshData meshData; + + if (!heightMap.isValid() || heightMap.width < 2 || heightMap.height < 2) { + return meshData; + } + + int32_t cellsX = heightMap.width - 1; + int32_t cellsY = heightMap.height - 1; + + meshData.chunksX = (cellsX + chunkSize - 1) / chunkSize; + meshData.chunksY = (cellsY + chunkSize - 1) / chunkSize; + + meshData.chunks.reserve(static_cast(meshData.chunksX * meshData.chunksY)); + + for (int32_t cy = 0; cy < meshData.chunksY; ++cy) { + for (int32_t cx = 0; cx < meshData.chunksX; ++cx) { + auto chunk = generateChunkFromBlendData(heightMap, blendTileData, tileUVs, cx, cy, chunkSize); + meshData.totalBounds.expand(chunk.bounds); + meshData.chunks.push_back(std::move(chunk)); + } + } + + return meshData; +} + } // namespace w3d::terrain diff --git a/src/render/terrain/terrain_mesh.hpp b/src/render/terrain/terrain_mesh.hpp index daa07d0..85c48ff 100644 --- a/src/render/terrain/terrain_mesh.hpp +++ b/src/render/terrain/terrain_mesh.hpp @@ -7,6 +7,7 @@ #include "lib/formats/map/types.hpp" #include "lib/gfx/bounding_box.hpp" +#include "render/terrain/terrain_atlas.hpp" namespace w3d::terrain { @@ -14,6 +15,7 @@ struct TerrainVertex { glm::vec3 position; glm::vec3 normal; glm::vec2 texCoord; + glm::vec2 atlasCoord; }; struct TerrainChunk { @@ -48,4 +50,14 @@ constexpr int32_t CHUNK_SIZE = 32; [[nodiscard]] TerrainMeshData generateTerrainMesh(const map::HeightMap &heightMap, int32_t chunkSize = CHUNK_SIZE); +[[nodiscard]] TerrainChunk generateChunkFromBlendData(const map::HeightMap &heightMap, + const map::BlendTileData &blendTileData, + const std::vector &tileUVs, + int32_t chunkX, int32_t chunkY, + int32_t chunkSize = CHUNK_SIZE); + +[[nodiscard]] TerrainMeshData generateTerrainMeshFromBlendData( + const map::HeightMap &heightMap, const map::BlendTileData &blendTileData, + const std::vector &tileUVs, int32_t chunkSize = CHUNK_SIZE); + } // namespace w3d::terrain diff --git a/src/render/terrain/terrain_renderable.cpp b/src/render/terrain/terrain_renderable.cpp index e3ab3d2..430e19c 100644 --- a/src/render/terrain/terrain_renderable.cpp +++ b/src/render/terrain/terrain_renderable.cpp @@ -48,6 +48,23 @@ void TerrainRenderable::updateFrustum(const glm::mat4 &viewProjection) { frustumValid_ = true; } +void TerrainRenderable::loadWithBlendData(gfx::VulkanContext &context, + const map::HeightMap &heightMap, + const map::BlendTileData &blendTileData, + const std::vector &tileUVs, + const map::GlobalLighting &lighting) { + destroy(); + + auto meshData = generateTerrainMeshFromBlendData(heightMap, blendTileData, tileUVs); + if (meshData.chunks.empty()) { + return; + } + + bounds_ = meshData.totalBounds; + setLighting(lighting); + uploadChunks(context, meshData); +} + void TerrainRenderable::destroy() { for (auto &chunk : gpuChunks_) { chunk.destroy(); @@ -56,6 +73,7 @@ void TerrainRenderable::destroy() { bounds_ = gfx::BoundingBox{}; frustumValid_ = false; visibleChunkCount_ = 0; + atlasTextureIndex_ = ~0u; descriptorManager_.destroy(); pipeline_.destroy(); @@ -68,7 +86,7 @@ void TerrainRenderable::setLighting(const map::GlobalLighting &lighting) { pushConstant_.ambientColor = glm::vec4(light.ambient, 1.0f); pushConstant_.diffuseColor = glm::vec4(light.diffuse, 1.0f); pushConstant_.lightDirection = light.lightPos; - pushConstant_.useTexture = 0; + pushConstant_.useTexture = hasAtlas() ? 1u : 0u; } void TerrainRenderable::initPipeline(gfx::VulkanContext &context, @@ -83,6 +101,31 @@ void TerrainRenderable::initPipeline(gfx::VulkanContext &context, } } +void TerrainRenderable::initPipelineWithAtlas(gfx::VulkanContext &context, + gfx::TextureManager &textureManager, + const TerrainAtlasData &atlasData, + uint32_t frameCount) { + pipeline_.create(context, gfx::PipelineCreateInfo::terrain()); + descriptorManager_.create(context, pipeline_.descriptorSetLayout(), frameCount); + + if (atlasData.isValid()) { + atlasTextureIndex_ = textureManager.createTexture( + "terrain_atlas", static_cast(atlasData.atlasWidth), + static_cast(atlasData.atlasHeight), atlasData.pixels.data()); + + const auto &atlasTex = textureManager.texture(atlasTextureIndex_); + for (uint32_t i = 0; i < frameCount; ++i) { + descriptorManager_.updateTexture(i, atlasTex.view, atlasTex.sampler); + } + pushConstant_.useTexture = 1u; + } else { + const auto &defaultTex = textureManager.texture(0); + for (uint32_t i = 0; i < frameCount; ++i) { + descriptorManager_.updateTexture(i, defaultTex.view, defaultTex.sampler); + } + } +} + void TerrainRenderable::updateDescriptors(uint32_t frameIndex, vk::Buffer uniformBuffer, vk::DeviceSize uboSize) { descriptorManager_.updateUniformBuffer(frameIndex, uniformBuffer, uboSize); diff --git a/src/render/terrain/terrain_renderable.hpp b/src/render/terrain/terrain_renderable.hpp index 2c6c7a9..33da52f 100644 --- a/src/render/terrain/terrain_renderable.hpp +++ b/src/render/terrain/terrain_renderable.hpp @@ -15,6 +15,7 @@ #include "lib/gfx/frustum.hpp" #include "lib/gfx/renderable.hpp" #include "lib/gfx/texture.hpp" +#include "render/terrain/terrain_atlas.hpp" #include "render/terrain/terrain_mesh.hpp" namespace w3d::gfx { @@ -47,6 +48,10 @@ class TerrainRenderable : public gfx::IRenderable { void load(gfx::VulkanContext &context, const map::HeightMap &heightMap, const map::GlobalLighting &lighting); + void loadWithBlendData(gfx::VulkanContext &context, const map::HeightMap &heightMap, + const map::BlendTileData &blendTileData, + const std::vector &tileUVs, const map::GlobalLighting &lighting); + void draw(vk::CommandBuffer cmd) override; const gfx::BoundingBox &bounds() const override { return bounds_; } @@ -67,6 +72,9 @@ class TerrainRenderable : public gfx::IRenderable { void initPipeline(gfx::VulkanContext &context, gfx::TextureManager &textureManager, uint32_t frameCount); + void initPipelineWithAtlas(gfx::VulkanContext &context, gfx::TextureManager &textureManager, + const TerrainAtlasData &atlasData, uint32_t frameCount); + void updateDescriptors(uint32_t frameIndex, vk::Buffer uniformBuffer, vk::DeviceSize uboSize); void drawWithPipeline(vk::CommandBuffer cmd, uint32_t frameIndex); @@ -76,6 +84,8 @@ class TerrainRenderable : public gfx::IRenderable { uint32_t visibleChunkCount() const { return visibleChunkCount_; } uint32_t totalChunkCount() const { return static_cast(gpuChunks_.size()); } + bool hasAtlas() const { return atlasTextureIndex_ != ~0u; } + private: void uploadChunks(gfx::VulkanContext &context, const TerrainMeshData &meshData); @@ -89,6 +99,8 @@ class TerrainRenderable : public gfx::IRenderable { gfx::Frustum frustum_; uint32_t visibleChunkCount_ = 0; bool frustumValid_ = false; + + uint32_t atlasTextureIndex_ = ~0u; }; } // namespace w3d::terrain diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a3fd7b9..ffed879 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -318,3 +318,33 @@ else() endif() add_test(NAME frustum_tests COMMAND frustum_tests) + +# Terrain atlas tests (requires GLM, no Vulkan) +add_executable(terrain_atlas_tests + terrain/test_terrain_atlas.cpp +) + +target_link_libraries(terrain_atlas_tests PRIVATE w3d_lib gtest gtest_main) + +if(MSVC) + target_compile_options(terrain_atlas_tests PRIVATE /W4 /permissive-) +else() + target_compile_options(terrain_atlas_tests PRIVATE -Wall -Wextra -Wpedantic -Werror) +endif() + +add_test(NAME terrain_atlas_tests COMMAND terrain_atlas_tests) + +# Terrain blend pattern tests (requires GLM, no Vulkan) +add_executable(terrain_blend_tests + terrain/test_terrain_blend.cpp +) + +target_link_libraries(terrain_blend_tests PRIVATE w3d_lib gtest gtest_main) + +if(MSVC) + target_compile_options(terrain_blend_tests PRIVATE /W4 /permissive-) +else() + target_compile_options(terrain_blend_tests PRIVATE -Wall -Wextra -Wpedantic -Werror) +endif() + +add_test(NAME terrain_blend_tests COMMAND terrain_blend_tests) diff --git a/tests/gfx/test_pipeline_create_info.cpp b/tests/gfx/test_pipeline_create_info.cpp index 9f846c4..3784912 100644 --- a/tests/gfx/test_pipeline_create_info.cpp +++ b/tests/gfx/test_pipeline_create_info.cpp @@ -78,8 +78,8 @@ TEST(PipelineCreateInfoTest, TerrainPresetHasCorrectDefaults) { EXPECT_EQ(info.topology, vk::PrimitiveTopology::eTriangleList); EXPECT_EQ(info.vertexInput.binding.binding, 0); - EXPECT_EQ(info.vertexInput.binding.stride, 32); - EXPECT_EQ(info.vertexInput.attributes.size(), 3); + EXPECT_EQ(info.vertexInput.binding.stride, 40u); + EXPECT_EQ(info.vertexInput.attributes.size(), 4u); EXPECT_EQ(info.vertexInput.attributes[0].location, 0); EXPECT_EQ(info.vertexInput.attributes[0].format, vk::Format::eR32G32B32Sfloat); @@ -93,6 +93,10 @@ TEST(PipelineCreateInfoTest, TerrainPresetHasCorrectDefaults) { EXPECT_EQ(info.vertexInput.attributes[2].format, vk::Format::eR32G32Sfloat); EXPECT_EQ(info.vertexInput.attributes[2].offset, 24u); + EXPECT_EQ(info.vertexInput.attributes[3].location, 3); + EXPECT_EQ(info.vertexInput.attributes[3].format, vk::Format::eR32G32Sfloat); + EXPECT_EQ(info.vertexInput.attributes[3].offset, 32u); + EXPECT_EQ(info.descriptorBindings.size(), 2); EXPECT_EQ(info.descriptorBindings[0].binding, 0); EXPECT_EQ(info.descriptorBindings[0].descriptorType, vk::DescriptorType::eUniformBuffer); @@ -106,6 +110,6 @@ TEST(PipelineCreateInfoTest, TerrainPresetHasCorrectDefaults) { TEST(PipelineCreateInfoTest, TerrainVertexSizeMatchesStride) { auto info = PipelineCreateInfo::terrain(); - EXPECT_EQ(info.vertexInput.binding.stride, 32u); - EXPECT_EQ(sizeof(float) * 3 + sizeof(float) * 3 + sizeof(float) * 2, 32u); + EXPECT_EQ(info.vertexInput.binding.stride, 40u); + EXPECT_EQ(sizeof(float) * 3 + sizeof(float) * 3 + sizeof(float) * 2 + sizeof(float) * 2, 40u); } diff --git a/tests/map/test_lighting_parser.cpp b/tests/map/test_lighting_parser.cpp index 56714ef..4b97760 100644 --- a/tests/map/test_lighting_parser.cpp +++ b/tests/map/test_lighting_parser.cpp @@ -1,3 +1,4 @@ +#include #include #include "../../src/lib/formats/map/data_chunk_reader.hpp" diff --git a/tests/terrain/test_terrain_atlas.cpp b/tests/terrain/test_terrain_atlas.cpp new file mode 100644 index 0000000..2eeb15a --- /dev/null +++ b/tests/terrain/test_terrain_atlas.cpp @@ -0,0 +1,227 @@ +#include "render/terrain/terrain_atlas.hpp" + +#include + +using namespace w3d::terrain; +using namespace w3d; + +class TerrainAtlasTest : public ::testing::Test {}; + +TEST_F(TerrainAtlasTest, DecodeTileIndexExtractsTop14Bits) { + int16_t tileNdx = static_cast(0x0014); + EXPECT_EQ(decodeTileIndex(tileNdx), 5); +} + +TEST_F(TerrainAtlasTest, DecodeTileIndexZero) { + EXPECT_EQ(decodeTileIndex(0), 0); +} + +TEST_F(TerrainAtlasTest, DecodeQuadrantExtractsBottom2Bits) { + int16_t tileNdx0 = static_cast(0x0000); + int16_t tileNdx1 = static_cast(0x0001); + int16_t tileNdx2 = static_cast(0x0002); + int16_t tileNdx3 = static_cast(0x0003); + + EXPECT_EQ(decodeQuadrant(tileNdx0), 0); + EXPECT_EQ(decodeQuadrant(tileNdx1), 1); + EXPECT_EQ(decodeQuadrant(tileNdx2), 2); + EXPECT_EQ(decodeQuadrant(tileNdx3), 3); +} + +TEST_F(TerrainAtlasTest, DecodeTileIndexAndQuadrantCombined) { + int16_t tileNdx = static_cast((3 << 2) | 2); + EXPECT_EQ(decodeTileIndex(tileNdx), 3); + EXPECT_EQ(decodeQuadrant(tileNdx), 2); +} + +TEST_F(TerrainAtlasTest, ComputeQuadrantUVTopLeft) { + TileUV tile{0.0f, 0.0f, 0.5f, 0.5f}; + auto uv = computeQuadrantUV(tile, 0); + EXPECT_FLOAT_EQ(uv.u, 0.0f); + EXPECT_FLOAT_EQ(uv.v, 0.0f); + EXPECT_FLOAT_EQ(uv.uSize, 0.25f); + EXPECT_FLOAT_EQ(uv.vSize, 0.25f); +} + +TEST_F(TerrainAtlasTest, ComputeQuadrantUVTopRight) { + TileUV tile{0.0f, 0.0f, 0.5f, 0.5f}; + auto uv = computeQuadrantUV(tile, 1); + EXPECT_FLOAT_EQ(uv.u, 0.25f); + EXPECT_FLOAT_EQ(uv.v, 0.0f); + EXPECT_FLOAT_EQ(uv.uSize, 0.25f); + EXPECT_FLOAT_EQ(uv.vSize, 0.25f); +} + +TEST_F(TerrainAtlasTest, ComputeQuadrantUVBottomLeft) { + TileUV tile{0.0f, 0.0f, 0.5f, 0.5f}; + auto uv = computeQuadrantUV(tile, 2); + EXPECT_FLOAT_EQ(uv.u, 0.0f); + EXPECT_FLOAT_EQ(uv.v, 0.25f); +} + +TEST_F(TerrainAtlasTest, ComputeQuadrantUVBottomRight) { + TileUV tile{0.0f, 0.0f, 0.5f, 0.5f}; + auto uv = computeQuadrantUV(tile, 3); + EXPECT_FLOAT_EQ(uv.u, 0.25f); + EXPECT_FLOAT_EQ(uv.v, 0.25f); +} + +TEST_F(TerrainAtlasTest, ComputeTileUVTableEmptyTextureClasses) { + auto uvs = computeTileUVTable({}, 2048, 64); + EXPECT_TRUE(uvs.empty()); +} + +TEST_F(TerrainAtlasTest, ComputeTileUVTableSingleTileClass) { + map::TextureClass tc; + tc.firstTile = 0; + tc.numTiles = 4; + tc.width = 64; + tc.name = "TestTerrain"; + + auto uvs = computeTileUVTable({tc}, 2048, 64); + EXPECT_EQ(uvs.size(), 4u); +} + +TEST_F(TerrainAtlasTest, ComputeTileUVTableFirstTileIsAtOrigin) { + map::TextureClass tc; + tc.numTiles = 1; + tc.width = 64; + + auto uvs = computeTileUVTable({tc}, 2048, 64); + ASSERT_EQ(uvs.size(), 1u); + EXPECT_FLOAT_EQ(uvs[0].u, 0.0f); + EXPECT_FLOAT_EQ(uvs[0].v, 0.0f); +} + +TEST_F(TerrainAtlasTest, ComputeTileUVTableTileUVsAreNormalized) { + map::TextureClass tc; + tc.numTiles = 10; + + auto uvs = computeTileUVTable({tc}, 2048, 64); + for (const auto &uv : uvs) { + EXPECT_GE(uv.u, 0.0f); + EXPECT_LT(uv.u, 1.0f); + EXPECT_GE(uv.v, 0.0f); + EXPECT_LT(uv.u + uv.uSize, 1.01f); + EXPECT_GE(uv.uSize, 0.0f); + EXPECT_GE(uv.vSize, 0.0f); + } +} + +TEST_F(TerrainAtlasTest, ComputeTileUVTableSecondTileOffset) { + map::TextureClass tc; + tc.numTiles = 2; + + auto uvs = computeTileUVTable({tc}, 2048, 64); + ASSERT_EQ(uvs.size(), 2u); + + float tileWidth = 64.0f / 2048.0f; + EXPECT_FLOAT_EQ(uvs[0].u, 0.0f); + EXPECT_NEAR(uvs[1].u, tileWidth, 1e-5f); +} + +TEST_F(TerrainAtlasTest, ComputeTileUVTableMultipleClasses) { + map::TextureClass tc1; + tc1.numTiles = 3; + + map::TextureClass tc2; + tc2.numTiles = 5; + + auto uvs = computeTileUVTable({tc1, tc2}, 2048, 64); + EXPECT_EQ(uvs.size(), 8u); +} + +TEST_F(TerrainAtlasTest, DecodeTileNdxUVReturnsTileUV) { + std::vector tileUVs; + tileUVs.push_back({0.0f, 0.0f, 0.5f, 0.5f}); + tileUVs.push_back({0.5f, 0.0f, 0.5f, 0.5f}); + + int16_t tileNdx = static_cast(1 << 2); + auto uv = decodeTileNdxUV(tileNdx, tileUVs); + EXPECT_NEAR(uv.u, 0.5f, 1e-5f); +} + +TEST_F(TerrainAtlasTest, DecodeTileNdxUVOutOfRangeReturnsZero) { + std::vector tileUVs; + tileUVs.push_back({0.1f, 0.2f, 0.3f, 0.4f}); + + int16_t tileNdx = static_cast(100 << 2); + auto uv = decodeTileNdxUV(tileNdx, tileUVs); + EXPECT_FLOAT_EQ(uv.u, 0.0f); + EXPECT_FLOAT_EQ(uv.v, 0.0f); +} + +TEST_F(TerrainAtlasTest, BuildProceduralAtlasCreatesValidData) { + auto atlas = buildProceduralAtlas(4, 256, 64); + + EXPECT_TRUE(atlas.isValid()); + EXPECT_EQ(atlas.atlasWidth, 256); + EXPECT_GT(atlas.atlasHeight, 0); + EXPECT_EQ(atlas.tilePixelSize, 64); + EXPECT_EQ(atlas.tileUVs.size(), 4u); +} + +TEST_F(TerrainAtlasTest, BuildProceduralAtlasPixelDataSize) { + auto atlas = buildProceduralAtlas(2, 128, 64); + + size_t expectedSize = static_cast(atlas.atlasWidth * atlas.atlasHeight * 4); + EXPECT_EQ(atlas.pixels.size(), expectedSize); +} + +TEST_F(TerrainAtlasTest, BuildProceduralAtlasUVsAreNormalized) { + auto atlas = buildProceduralAtlas(8, 512, 64); + + for (const auto &uv : atlas.tileUVs) { + EXPECT_GE(uv.u, 0.0f); + EXPECT_LT(uv.u, 1.0f); + EXPECT_GE(uv.v, 0.0f); + EXPECT_LT(uv.u + uv.uSize, 1.01f); + } +} + +TEST_F(TerrainAtlasTest, BuildProceduralAtlasAlphaIsOpaque) { + auto atlas = buildProceduralAtlas(1, 64, 64); + + for (size_t i = 3; i < atlas.pixels.size(); i += 4) { + EXPECT_EQ(atlas.pixels[i], 255u); + } +} + +TEST_F(TerrainAtlasTest, BuildProceduralAtlasInvalidInputReturnsEmpty) { + auto atlas = buildProceduralAtlas(0, 256, 64); + EXPECT_FALSE(atlas.isValid()); + + auto atlas2 = buildProceduralAtlas(4, 0, 64); + EXPECT_FALSE(atlas2.isValid()); +} + +TEST_F(TerrainAtlasTest, BuildProceduralAtlasTilesHaveDistinctColors) { + auto atlas = buildProceduralAtlas(3, 192, 64); + ASSERT_TRUE(atlas.isValid()); + + auto getPixel = [&](int32_t x, int32_t y) -> std::array { + size_t idx = static_cast((y * atlas.atlasWidth + x) * 4); + return {atlas.pixels[idx], atlas.pixels[idx + 1], atlas.pixels[idx + 2]}; + }; + + auto color0 = getPixel(32, 32); + auto color1 = getPixel(64 + 32, 32); + auto color2 = getPixel(128 + 32, 32); + + bool distinct01 = (color0 != color1); + bool distinct02 = (color0 != color2); + bool distinct12 = (color1 != color2); + + EXPECT_TRUE(distinct01 || distinct02 || distinct12); +} + +TEST_F(TerrainAtlasTest, TileUVTableWrapsToNextRow) { + map::TextureClass tc; + tc.numTiles = 5; + + auto uvs = computeTileUVTable({tc}, 256, 64); + ASSERT_EQ(uvs.size(), 5u); + + EXPECT_FLOAT_EQ(uvs[4].u, 0.0f); + EXPECT_GT(uvs[4].v, 0.0f); +} diff --git a/tests/terrain/test_terrain_blend.cpp b/tests/terrain/test_terrain_blend.cpp new file mode 100644 index 0000000..c97d78e --- /dev/null +++ b/tests/terrain/test_terrain_blend.cpp @@ -0,0 +1,208 @@ +#include "render/terrain/terrain_blend.hpp" + +#include + +using namespace w3d::terrain; +using namespace w3d; + +class TerrainBlendTest : public ::testing::Test {}; + +TEST_F(TerrainBlendTest, GenerateBlendPatternHasCorrectSize) { + auto pattern = generateBlendPattern(BlendDirection::Horizontal); + EXPECT_EQ(pattern.size, BLEND_PATTERN_SIZE); + EXPECT_EQ(static_cast(pattern.alpha.size()), BLEND_PATTERN_SIZE * BLEND_PATTERN_SIZE); +} + +TEST_F(TerrainBlendTest, GenerateBlendPatternAlphaValuesInRange) { + for (int32_t i = 0; i < NUM_BLEND_PATTERNS; ++i) { + auto pattern = generateBlendPattern(static_cast(i)); + for (uint8_t val : pattern.alpha) { + EXPECT_GE(val, 0); + EXPECT_LE(val, 255); + } + } +} + +TEST_F(TerrainBlendTest, HorizontalPatternIncreasesLeftToRight) { + auto pattern = generateBlendPattern(BlendDirection::Horizontal); + + int leftCol = 0; + int rightCol = BLEND_PATTERN_SIZE - 1; + int midRow = BLEND_PATTERN_SIZE / 2; + + uint8_t leftVal = pattern.alpha[static_cast(midRow * BLEND_PATTERN_SIZE + leftCol)]; + uint8_t rightVal = pattern.alpha[static_cast(midRow * BLEND_PATTERN_SIZE + rightCol)]; + + EXPECT_LT(leftVal, rightVal); +} + +TEST_F(TerrainBlendTest, HorizontalInvPatternDecreasesLeftToRight) { + auto pattern = generateBlendPattern(BlendDirection::HorizontalInv); + + int midRow = BLEND_PATTERN_SIZE / 2; + uint8_t leftVal = pattern.alpha[static_cast(midRow * BLEND_PATTERN_SIZE + 0)]; + uint8_t rightVal = + pattern.alpha[static_cast(midRow * BLEND_PATTERN_SIZE + BLEND_PATTERN_SIZE - 1)]; + + EXPECT_GT(leftVal, rightVal); +} + +TEST_F(TerrainBlendTest, VerticalPatternIncreasesTopToBottom) { + auto pattern = generateBlendPattern(BlendDirection::Vertical); + + int midCol = BLEND_PATTERN_SIZE / 2; + uint8_t topVal = pattern.alpha[static_cast(0 * BLEND_PATTERN_SIZE + midCol)]; + uint8_t bottomVal = + pattern.alpha[static_cast((BLEND_PATTERN_SIZE - 1) * BLEND_PATTERN_SIZE + midCol)]; + + EXPECT_LT(topVal, bottomVal); +} + +TEST_F(TerrainBlendTest, VerticalInvPatternDecreasesTopToBottom) { + auto pattern = generateBlendPattern(BlendDirection::VerticalInv); + + int midCol = BLEND_PATTERN_SIZE / 2; + uint8_t topVal = pattern.alpha[static_cast(0 * BLEND_PATTERN_SIZE + midCol)]; + uint8_t bottomVal = + pattern.alpha[static_cast((BLEND_PATTERN_SIZE - 1) * BLEND_PATTERN_SIZE + midCol)]; + + EXPECT_GT(topVal, bottomVal); +} + +TEST_F(TerrainBlendTest, HorizontalAndInvAreComplementary) { + auto horiz = generateBlendPattern(BlendDirection::Horizontal); + auto horizInv = generateBlendPattern(BlendDirection::HorizontalInv); + + int midRow = BLEND_PATTERN_SIZE / 2; + int midCol = BLEND_PATTERN_SIZE / 2; + size_t idx = static_cast(midRow * BLEND_PATTERN_SIZE + midCol); + + int sum = static_cast(horiz.alpha[idx]) + static_cast(horizInv.alpha[idx]); + EXPECT_NEAR(sum, 255, 2); +} + +TEST_F(TerrainBlendTest, VerticalAndInvAreComplementary) { + auto vert = generateBlendPattern(BlendDirection::Vertical); + auto vertInv = generateBlendPattern(BlendDirection::VerticalInv); + + int midRow = BLEND_PATTERN_SIZE / 2; + int midCol = BLEND_PATTERN_SIZE / 2; + size_t idx = static_cast(midRow * BLEND_PATTERN_SIZE + midCol); + + int sum = static_cast(vert.alpha[idx]) + static_cast(vertInv.alpha[idx]); + EXPECT_NEAR(sum, 255, 2); +} + +TEST_F(TerrainBlendTest, GenerateAllBlendPatternsReturnsCorrectCount) { + auto patterns = generateAllBlendPatterns(); + EXPECT_EQ(static_cast(patterns.size()), NUM_BLEND_PATTERNS); +} + +TEST_F(TerrainBlendTest, AllPatternsHaveCorrectDimensions) { + auto patterns = generateAllBlendPatterns(); + for (const auto &pattern : patterns) { + EXPECT_EQ(pattern.size, BLEND_PATTERN_SIZE); + EXPECT_EQ(static_cast(pattern.alpha.size()), BLEND_PATTERN_SIZE * BLEND_PATTERN_SIZE); + } +} + +TEST_F(TerrainBlendTest, BlendDirectionFromInfoHoriz) { + map::BlendTileInfo info; + info.horiz = 1; + info.inverted = 0; + + auto dir = blendDirectionFromInfo(info); + EXPECT_EQ(dir, BlendDirection::Horizontal); +} + +TEST_F(TerrainBlendTest, BlendDirectionFromInfoHorizInv) { + map::BlendTileInfo info; + info.horiz = 1; + info.inverted = map::INVERTED_MASK; + + auto dir = blendDirectionFromInfo(info); + EXPECT_EQ(dir, BlendDirection::HorizontalInv); +} + +TEST_F(TerrainBlendTest, BlendDirectionFromInfoVert) { + map::BlendTileInfo info; + info.vert = 1; + info.inverted = 0; + + auto dir = blendDirectionFromInfo(info); + EXPECT_EQ(dir, BlendDirection::Vertical); +} + +TEST_F(TerrainBlendTest, BlendDirectionFromInfoVertInv) { + map::BlendTileInfo info; + info.vert = 1; + info.inverted = map::INVERTED_MASK; + + auto dir = blendDirectionFromInfo(info); + EXPECT_EQ(dir, BlendDirection::VerticalInv); +} + +TEST_F(TerrainBlendTest, BlendDirectionFromInfoDiagonalRight) { + map::BlendTileInfo info; + info.rightDiagonal = 1; + info.inverted = 0; + + auto dir = blendDirectionFromInfo(info); + EXPECT_EQ(dir, BlendDirection::DiagonalRight); +} + +TEST_F(TerrainBlendTest, BlendDirectionFromInfoLongDiagonal) { + map::BlendTileInfo info; + info.longDiagonal = 1; + info.inverted = 0; + + auto dir = blendDirectionFromInfo(info); + EXPECT_EQ(dir, BlendDirection::LongDiagonal); +} + +TEST_F(TerrainBlendTest, CellHasBlendReturnsTrueForHoriz) { + map::BlendTileInfo info; + info.horiz = 1; + EXPECT_TRUE(cellHasBlend(info)); +} + +TEST_F(TerrainBlendTest, CellHasBlendReturnsFalseForEmpty) { + map::BlendTileInfo info{}; + EXPECT_FALSE(cellHasBlend(info)); +} + +TEST_F(TerrainBlendTest, CellHasBlendReturnsTrueForVert) { + map::BlendTileInfo info; + info.vert = 1; + EXPECT_TRUE(cellHasBlend(info)); +} + +TEST_F(TerrainBlendTest, CellHasBlendReturnsTrueForDiagonal) { + map::BlendTileInfo info; + info.leftDiagonal = 1; + EXPECT_TRUE(cellHasBlend(info)); +} + +TEST_F(TerrainBlendTest, DiagonalRightPatternHasCorrectGradient) { + auto pattern = generateBlendPattern(BlendDirection::DiagonalRight); + + uint8_t topLeft = pattern.alpha[0]; + uint8_t bottomRight = pattern.alpha[static_cast( + (BLEND_PATTERN_SIZE - 1) * BLEND_PATTERN_SIZE + BLEND_PATTERN_SIZE - 1)]; + + EXPECT_LT(topLeft, bottomRight); +} + +TEST_F(TerrainBlendTest, PatternsAreNotAllSame) { + auto horiz = generateBlendPattern(BlendDirection::Horizontal); + auto vert = generateBlendPattern(BlendDirection::Vertical); + + bool different = false; + for (size_t i = 0; i < horiz.alpha.size(); ++i) { + if (horiz.alpha[i] != vert.alpha[i]) { + different = true; + break; + } + } + EXPECT_TRUE(different); +}