/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "graphics/Terrain.h" #include "graphics/Patch.h" #include "graphics/RenderableObject.h" #include "maths/Fixed.h" #include "maths/FixedVector3D.h" #include #include class TestTerrain : public CxxTest::TestSuite { void SetVertex(CTerrain& terrain, ssize_t i, ssize_t j, u16 height) { terrain.GetHeightMap()[j*terrain.GetVerticesPerSide() + i] = height; terrain.MakeDirty(RENDERDATA_UPDATE_VERTICES); } u16 GetVertex(CTerrain& terrain, ssize_t i, ssize_t j) { return terrain.GetHeightMap()[j*terrain.GetVerticesPerSide() + i]; } void Set45Slope(CTerrain& terrain) { SetVertex(terrain, 0, 0, 100); SetVertex(terrain, 0, 1, 100); SetVertex(terrain, 0, 2, 100); SetVertex(terrain, 1, 0, 100 + TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 1, 1, 100 + TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 1, 2, 100 + TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 2, 0, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 2, 1, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 2, 2, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 3, 0, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 3, 1, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 3, 2, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); } void SetHighPlateau(CTerrain& terrain, int height) { SetVertex(terrain, 4, 0, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 4, 1, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 4, 2, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 5, 0, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 5, 1, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); SetVertex(terrain, 5, 2, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE); } public: void test_GetExactGroundLevel() { CTerrain terrain; terrain.Initialize(4, NULL); Set45Slope(terrain); SetHighPlateau(terrain, 20); float ground; ground = terrain.GetExactGroundLevel(0.f, 1.5f*TERRAIN_TILE_SIZE); TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE, 0.01f); ground = terrain.GetExactGroundLevel(0.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE); TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+0.5f*TERRAIN_TILE_SIZE, 0.01f); ground = terrain.GetExactGroundLevel(1.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE); TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+1.5f*TERRAIN_TILE_SIZE, 0.01f); ground = terrain.GetExactGroundLevel(2.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE); TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+2.f*TERRAIN_TILE_SIZE, 0.01f); ground = terrain.GetExactGroundLevel(3.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE); TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+11.f*TERRAIN_TILE_SIZE, 0.01f); ground = terrain.GetExactGroundLevel(4.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE); TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+20.f*TERRAIN_TILE_SIZE, 0.01f); } void test_GetExactGroundLevelFixed() { CTerrain terrain; terrain.Initialize(4, NULL); Set45Slope(terrain); SetHighPlateau(terrain, 20); const double maxDelta = 0.0001; fixed ground; ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(0.f), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE)); TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE, maxDelta); ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(0.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE)); TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+0.5*TERRAIN_TILE_SIZE, maxDelta); ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE)); TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+1.5*TERRAIN_TILE_SIZE, maxDelta); ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(2.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE)); TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+2.0*TERRAIN_TILE_SIZE, maxDelta); ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(3.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE)); TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+11.0*TERRAIN_TILE_SIZE, maxDelta); ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(4.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE)); TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+20.0*TERRAIN_TILE_SIZE, maxDelta); } void test_GetExactGroundLevelFixed_max() { CTerrain terrain; terrain.Initialize(4, NULL); SetVertex(terrain, 0, 0, 65535); SetVertex(terrain, 0, 1, 65535); SetVertex(terrain, 1, 0, 65535); SetVertex(terrain, 1, 1, 65535); const double maxDelta = 0.024; int p = 255; for (int zi = 0; zi < p; ++zi) { for (int xi = 0; xi < p; ++xi) { fixed ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(xi/(float)p*TERRAIN_TILE_SIZE), fixed::FromFloat(zi/(float)p*TERRAIN_TILE_SIZE)); TS_ASSERT_DELTA(ground.ToDouble(), 65535.0/HEIGHT_UNITS_PER_METRE, maxDelta); } } } void test_CalcNormal() { CTerrain terrain; terrain.Initialize(4, NULL); Set45Slope(terrain); CVector3D vec; terrain.CalcNormal(1, 1, vec); TS_ASSERT_DELTA(vec.X, -1.f/sqrt(2.f), 0.01f); TS_ASSERT_DELTA(vec.Y, 1.f/sqrt(2.f), 0.01f); TS_ASSERT_EQUALS(vec.Z, 0.f); terrain.CalcNormal(2, 1, vec); TS_ASSERT_DELTA(vec.X, (-1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f); TS_ASSERT_DELTA(vec.Y, (1.f+1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f); TS_ASSERT_EQUALS(vec.Z, 0); terrain.CalcNormal(5, 1, vec); TS_ASSERT_EQUALS(vec.X, 0.f); TS_ASSERT_EQUALS(vec.Y, 1.f); TS_ASSERT_EQUALS(vec.Z, 0.f); } void test_CalcNormalFixed() { CTerrain terrain; terrain.Initialize(4, NULL); Set45Slope(terrain); CFixedVector3D vec; terrain.CalcNormalFixed(1, 1, vec); TS_ASSERT_DELTA(vec.X.ToFloat(), -1.f/sqrt(2.f), 0.01f); TS_ASSERT_DELTA(vec.Y.ToFloat(), 1.f/sqrt(2.f), 0.01f); TS_ASSERT_EQUALS(vec.Z.ToFloat(), 0.f); terrain.CalcNormalFixed(2, 1, vec); TS_ASSERT_DELTA(vec.X.ToFloat(), (-1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f); TS_ASSERT_DELTA(vec.Y.ToFloat(), (1.f+1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f); TS_ASSERT_EQUALS(vec.Z.ToFloat(), 0); terrain.CalcNormalFixed(5, 1, vec); TS_ASSERT_EQUALS(vec.X.ToFloat(), 0.f); TS_ASSERT_EQUALS(vec.Y.ToFloat(), 1.f); TS_ASSERT_EQUALS(vec.Z.ToFloat(), 0.f); } void test_Resize() { // We do resize by size in patches, so it doesn't make sense to // fill each vertex with a different value. Instead we use a single // value per a patch. struct ResizeTestCase { ssize_t horizontalOffset, verticalOffset; std::vector> sourcePatches; std::vector> expectedPatches; }; const ResizeTestCase testCases[] = { // Without offset. { 0, 0, { {42} }, { {42} } }, { 0, 0, { {1, 2}, {3, 4} }, { {1, 2}, {3, 4} } }, { 0, 0, { {1, 2}, {3, 4} }, { {1, 1, 2, 2}, {1, 1, 2, 2}, {3, 3, 4, 4}, {3, 3, 4, 4} } }, { 0, 0, { { 1, 2 , 3, 4}, { 5, 6 , 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16} }, { { 6, 7}, {10, 11}, } }, // With offset. { -2, -2, { {1, 2}, {3, 4} }, { {0, 0}, {0, 0} } }, { -2, 0, { {1, 2}, {3, 4} }, { {0, 0}, {0, 0} } }, { 4, 4, { {1, 2}, {3, 4} }, { {0, 0}, {0, 0} } }, { 1, 1, { { 1, 2 , 3, 4}, { 5, 6 , 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16} }, { { 6 , 7, 8, 8}, {10, 11, 12, 12}, {14, 15, 16, 16}, {14, 15, 16, 16} } }, { 1, 1, { {1, 2}, {3, 4} }, { {4, 4}, {4, 4} } }, { -2, 0, { { 1, 2 , 3, 4}, { 5, 6 , 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16} }, { {5, 5}, {9, 9} } }, { 2, -2, { { 1, 2 , 3, 4}, { 5, 6 , 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16} }, { {4, 4}, {4, 4} } }, { 3, -1, { { 1, 2 , 3, 4}, { 5, 6 , 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16} }, { {0, 0}, {0, 0} } }, { -2, -1, { {1, 2}, {3, 4} }, { {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {3, 3, 3, 3} } }, }; for (const ResizeTestCase& testCase : testCases) { const ssize_t sourceSize = testCase.sourcePatches.size(); const ssize_t expectedSize = testCase.expectedPatches.size(); TS_ASSERT_LESS_THAN(0, sourceSize); TS_ASSERT_LESS_THAN(0, expectedSize); const ssize_t sourceMapSize = sourceSize * PATCH_SIZE + 1; const ssize_t expectedMapSize = expectedSize * PATCH_SIZE + 1; CTerrain terrain; { std::vector heightmap(sourceMapSize * sourceMapSize); for (ssize_t jTile = 0; jTile < sourceSize; ++jTile) { TS_ASSERT_EQUALS(sourceSize, testCase.sourcePatches[jTile].size()); for (ssize_t iTile = 0; iTile < sourceSize; ++iTile) { for (ssize_t j = 0; j < PATCH_SIZE; ++j) for (ssize_t i = 0; i < PATCH_SIZE; ++i) { const ssize_t idx = (jTile * PATCH_SIZE + j) * sourceMapSize + iTile * PATCH_SIZE + i; heightmap[idx] = testCase.sourcePatches[jTile][iTile]; } } } terrain.Initialize(sourceSize, heightmap.data()); } terrain.ResizeAndOffset(expectedSize, testCase.horizontalOffset, testCase.verticalOffset); TS_ASSERT_EQUALS(expectedMapSize, terrain.GetVerticesPerSide()); TS_ASSERT_EQUALS(expectedSize, terrain.GetPatchesPerSide()); for (ssize_t jTile = 0; jTile < expectedSize; ++jTile) { TS_ASSERT_EQUALS( expectedSize, testCase.expectedPatches[jTile].size()); for (ssize_t iTile = 0; iTile < expectedSize; ++iTile) { for (ssize_t j = 0; j < PATCH_SIZE; ++j) for (ssize_t i = 0; i < PATCH_SIZE; ++i) { // The whole patch should have the same height, // since we resize by patches. if (GetVertex(terrain, iTile * PATCH_SIZE, jTile * PATCH_SIZE) == GetVertex(terrain, iTile * PATCH_SIZE + i, jTile * PATCH_SIZE + j)) continue; TS_FAIL("The whole patch should have the same height"); std::stringstream ss; ss << "iTile=" << iTile << " jTile=" << jTile << " i=" << i << " j=" << j; TS_WARN(ss.str()); ss.str(std::string()); ss << "found=" << GetVertex(terrain, iTile * PATCH_SIZE + i, jTile * PATCH_SIZE + j) << " expected=" << GetVertex(terrain, iTile * PATCH_SIZE, jTile * PATCH_SIZE); TS_WARN(ss.str()); return; } if (testCase.expectedPatches[jTile][iTile] == GetVertex(terrain, iTile * PATCH_SIZE, jTile * PATCH_SIZE)) continue; std::stringstream ss; ss << "The patch has wrong height" << " (i=" << iTile << " j=" << jTile << "):" << " found=" << GetVertex(terrain, iTile * PATCH_SIZE, jTile * PATCH_SIZE) << " expected=" << testCase.expectedPatches[jTile][iTile]; TS_FAIL(ss.str()); ss.str(std::string()); ss << "Terrain (" << terrain.GetPatchesPerSide() << "x" << terrain.GetPatchesPerSide() << "):"; TS_WARN(ss.str()); for (ssize_t jj = 0; jj < expectedSize; ++jj) { ss.str(std::string()); ss << "["; for (ssize_t ii = 0; ii < expectedSize; ++ii) { if (ii) ss << ", "; ss << GetVertex(terrain, ii * PATCH_SIZE, jj * PATCH_SIZE); } ss << "]"; TS_WARN(ss.str()); } return; } } } } };