#include #include #include #include "testHarness.h" #include "vf/Block.h" #include "vf/TileTexture.h" using namespace vf; namespace { int luminance(TilePixel texel) { return (texel.r * 299 + texel.g * 587 + texel.b * 114) / 1000; } int colourDistance(TilePixel texel, TileRgb reference) { int dr = static_cast(texel.r) - reference.r; int dg = static_cast(texel.g) - reference.g; int db = static_cast(texel.b) - reference.b; return dr * dr + dg * dg + db * db; } bool leansToAccent(TilePixel texel, const TileSpec& spec) { return colourDistance(texel, spec.accent) < colourDistance(texel, spec.base); } // The palette owns how many tiles exist. A table short of a row must surface as // a reported failure, not as a read past the end of the table proving it. int renderedTiles(const TileSampler& sampler) { int held = sampler.palette().size(); return held < tileCount ? held : tileCount; } std::vector renderTile(const TileSampler& sampler, int tile) { int size = sampler.params().tileSize; std::vector bytes(static_cast(size) * size * 4); sampler.writeTile(tile, bytes.data()); return bytes; } std::vector renderAtlas(const TileSampler& sampler, bool backwards) { int size = sampler.params().tileSize; std::size_t tileBytes = static_cast(size) * size * 4; int held = renderedTiles(sampler); std::vector bytes(tileBytes * static_cast(held)); if (backwards) { for (int tile = held - 1; tile >= 0; --tile) { sampler.writeTile(tile, bytes.data() + tileBytes * static_cast(tile)); } } else { for (int tile = 0; tile < held; ++tile) { sampler.writeTile(tile, bytes.data() + tileBytes * static_cast(tile)); } } return bytes; } // The first row and column carry a deliberate edge darkening, which would hand // every tile a luminance spread for free. Contrast is therefore only ever // measured inside that border. struct LuminanceRange { int lowest = 255; int highest = 0; int spread() const { return highest - lowest; } }; LuminanceRange interiorRange(const TileSampler& sampler, int tile) { LuminanceRange range; int size = sampler.params().tileSize; for (int y = 1; y < size; ++y) { for (int x = 1; x < size; ++x) { int value = luminance(sampler.pixelAt(tile, x, y)); if (value < range.lowest) range.lowest = value; if (value > range.highest) range.highest = value; } } return range; } TileSpec withoutGrain(TileSpec spec) { spec.noise = 0.0f; return spec; } // Columns in one row that the pattern painted with the accent colour. For a // brick tile those are the vertical mortar joints. std::vector accentColumns(const TileSampler& sampler, const TileSpec& spec, int tileSalt, int y) { std::vector columns; for (int x = 0; x < sampler.params().tileSize; ++x) { if (leansToAccent(sampler.pixelOf(spec, tileSalt, x, y), spec)) columns.push_back(x); } return columns; } // A brick course is offset from the one above it, so the set of mortar columns // has to differ between courses. Rows that are entirely mortar are the // horizontal joints and say nothing about the offset. int distinctJointLayouts(const TileSampler& sampler, const TileSpec& spec, int tileSalt) { int size = sampler.params().tileSize; std::set> layouts; for (int y = 0; y < size; ++y) { std::vector columns = accentColumns(sampler, spec, tileSalt, y); if (columns.empty()) continue; if (static_cast(columns.size()) > size / 2) continue; layouts.insert(columns); } return static_cast(layouts.size()); } TileSpec syntheticBrick() { TileSpec spec; spec.name = "syntheticBrick"; spec.pattern = TilePattern::Brick; spec.material = TileMaterial::Stone; spec.base = TileRgb{30, 30, 30}; spec.accent = TileRgb{240, 240, 240}; spec.noise = 0.0f; spec.scale = 0.40f; spec.density = 0.50f; return spec; } } // namespace // Same params in, same bytes out. Anything that reached for a clock, a global // counter or an uninitialised buffer would show up here first. VF_TEST(renderingATileTwiceProducesIdenticalBytes) { TileSampler first; TileSampler second; CHECK_EQ(first.palette().size(), tileCount); for (int tile = 0; tile < renderedTiles(first); ++tile) { std::vector a = renderTile(first, tile); std::vector b = renderTile(second, tile); bool identical = a == b; CHECK(identical); if (!identical) std::printf(" tile %d is not reproducible\n", tile); } } // A tile must not care which tiles were generated before it. The atlas built // back to front has to match the atlas built front to back byte for byte. VF_TEST(theAtlasDoesNotDependOnTheOrderTilesAreGenerated) { TileSampler sampler; std::vector forwards = renderAtlas(sampler, false); std::vector backwards = renderAtlas(sampler, true); CHECK_EQ(forwards.size(), backwards.size()); CHECK(forwards == backwards); }