Index: ps/trunk/source/graphics/Terrain.cpp =================================================================== --- ps/trunk/source/graphics/Terrain.cpp (revision 23638) +++ ps/trunk/source/graphics/Terrain.cpp (revision 23639) @@ -1,725 +1,848 @@ -/* Copyright (C) 2019 Wildfire Games. +/* 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 . */ /* * Describes ground via heightmap and array of CPatch. */ #include "precompiled.h" #include "lib/res/graphics/ogl_tex.h" #include "lib/sysdep/cpu.h" #include "renderer/Renderer.h" #include "TerrainProperties.h" #include "TerrainTextureEntry.h" #include "TerrainTextureManager.h" #include #include "Terrain.h" #include "Patch.h" #include "maths/FixedVector3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "simulation2/helpers/Pathfinding.h" /////////////////////////////////////////////////////////////////////////////// // CTerrain constructor CTerrain::CTerrain() : m_Heightmap(0), m_Patches(0), m_MapSize(0), m_MapSizePatches(0), m_BaseColor(255, 255, 255, 255) { } /////////////////////////////////////////////////////////////////////////////// // CTerrain constructor CTerrain::~CTerrain() { ReleaseData(); } /////////////////////////////////////////////////////////////////////////////// // ReleaseData: delete any data allocated by this terrain void CTerrain::ReleaseData() { m_HeightMipmap.ReleaseData(); delete[] m_Heightmap; delete[] m_Patches; } /////////////////////////////////////////////////////////////////////////////// // Initialise: initialise this terrain to the given size // using given heightmap to setup elevation data bool CTerrain::Initialize(ssize_t patchesPerSide, const u16* data) { // clean up any previous terrain ReleaseData(); // store terrain size - m_MapSize = patchesPerSide*PATCH_SIZE+1; + m_MapSize = patchesPerSide * PATCH_SIZE + 1; m_MapSizePatches = patchesPerSide; // allocate data for new terrain - m_Heightmap = new u16[m_MapSize*m_MapSize]; - m_Patches = new CPatch[m_MapSizePatches*m_MapSizePatches]; + m_Heightmap = new u16[m_MapSize * m_MapSize]; + m_Patches = new CPatch[m_MapSizePatches * m_MapSizePatches]; // given a heightmap? if (data) { // yes; keep a copy of it memcpy(m_Heightmap, data, m_MapSize*m_MapSize*sizeof(u16)); } else { // build a flat terrain memset(m_Heightmap, 0, m_MapSize*m_MapSize*sizeof(u16)); } // setup patch parents, indices etc InitialisePatches(); // initialise mipmap m_HeightMipmap.Initialize(m_MapSize, m_Heightmap); return true; } /////////////////////////////////////////////////////////////////////////////// CStr8 CTerrain::GetMovementClass(ssize_t i, ssize_t j) const { CMiniPatch* tile = GetTile(i, j); if (tile && tile->GetTextureEntry()) return tile->GetTextureEntry()->GetProperties().GetMovementClass(); return "default"; } /////////////////////////////////////////////////////////////////////////////// // CalcPosition: calculate the world space position of the vertex at (i,j) // If i,j is off the map, it acts as if the edges of the terrain are extended // outwards to infinity void CTerrain::CalcPosition(ssize_t i, ssize_t j, CVector3D& pos) const { ssize_t hi = Clamp(i, static_cast(0), m_MapSize - 1); ssize_t hj = Clamp(j, static_cast(0), m_MapSize - 1); u16 height = m_Heightmap[hj*m_MapSize + hi]; pos.X = float(i*TERRAIN_TILE_SIZE); pos.Y = float(height*HEIGHT_SCALE); pos.Z = float(j*TERRAIN_TILE_SIZE); } /////////////////////////////////////////////////////////////////////////////// // CalcPositionFixed: calculate the world space position of the vertex at (i,j) void CTerrain::CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) const { ssize_t hi = Clamp(i, static_cast(0), m_MapSize - 1); ssize_t hj = Clamp(j, static_cast(0), m_MapSize - 1); u16 height = m_Heightmap[hj*m_MapSize + hi]; pos.X = fixed::FromInt(i) * (int)TERRAIN_TILE_SIZE; // fixed max value is 32767, but height is a u16, so divide by two to avoid overflow pos.Y = fixed::FromInt(height/ 2 ) / ((int)HEIGHT_UNITS_PER_METRE / 2); pos.Z = fixed::FromInt(j) * (int)TERRAIN_TILE_SIZE; } /////////////////////////////////////////////////////////////////////////////// // CalcNormal: calculate the world space normal of the vertex at (i,j) void CTerrain::CalcNormal(ssize_t i, ssize_t j, CVector3D& normal) const { CVector3D left, right, up, down; // Calculate normals of the four half-tile triangles surrounding this vertex: // get position of vertex where normal is being evaluated CVector3D basepos; CalcPosition(i, j, basepos); if (i > 0) { CalcPosition(i-1, j, left); left -= basepos; left.Normalize(); } if (i < m_MapSize-1) { CalcPosition(i+1, j, right); right -= basepos; right.Normalize(); } if (j > 0) { CalcPosition(i, j-1, up); up -= basepos; up.Normalize(); } if (j < m_MapSize-1) { CalcPosition(i, j+1, down); down -= basepos; down.Normalize(); } CVector3D n0 = up.Cross(left); CVector3D n1 = left.Cross(down); CVector3D n2 = down.Cross(right); CVector3D n3 = right.Cross(up); // Compute the mean of the normals normal = n0 + n1 + n2 + n3; float nlen=normal.Length(); if (nlen>0.00001f) normal*=1.0f/nlen; } /////////////////////////////////////////////////////////////////////////////// // CalcNormalFixed: calculate the world space normal of the vertex at (i,j) void CTerrain::CalcNormalFixed(ssize_t i, ssize_t j, CFixedVector3D& normal) const { CFixedVector3D left, right, up, down; // Calculate normals of the four half-tile triangles surrounding this vertex: // get position of vertex where normal is being evaluated CFixedVector3D basepos; CalcPositionFixed(i, j, basepos); if (i > 0) { CalcPositionFixed(i-1, j, left); left -= basepos; left.Normalize(); } if (i < m_MapSize-1) { CalcPositionFixed(i+1, j, right); right -= basepos; right.Normalize(); } if (j > 0) { CalcPositionFixed(i, j-1, up); up -= basepos; up.Normalize(); } if (j < m_MapSize-1) { CalcPositionFixed(i, j+1, down); down -= basepos; down.Normalize(); } CFixedVector3D n0 = up.Cross(left); CFixedVector3D n1 = left.Cross(down); CFixedVector3D n2 = down.Cross(right); CFixedVector3D n3 = right.Cross(up); // Compute the mean of the normals normal = n0 + n1 + n2 + n3; normal.Normalize(); } CVector3D CTerrain::CalcExactNormal(float x, float z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp(static_cast(floor(x / TERRAIN_TILE_SIZE)), static_cast(0), m_MapSize - 2); const ssize_t zi = Clamp(static_cast(floor(z / TERRAIN_TILE_SIZE)), static_cast(0), m_MapSize - 2); const float xf = Clamp(x / TERRAIN_TILE_SIZE-xi, 0.0f, 1.0f); const float zf = Clamp(z / TERRAIN_TILE_SIZE-zi, 0.0f, 1.0f); float h00 = m_Heightmap[zi*m_MapSize + xi]; float h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; float h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Determine which terrain triangle this point is on, // then compute the normal of that triangle's plane if (GetTriangulationDir(xi, zi)) { if (xf + zf <= 1.f) { // Lower-left triangle (don't use h11) return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } else { // Upper-right triangle (don't use h00) return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } else { // Lower-right triangle (don't use h01) return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } } } /////////////////////////////////////////////////////////////////////////////// // GetPatch: return the patch at (i,j) in patch space, or null if the patch is // out of bounds CPatch* CTerrain::GetPatch(ssize_t i, ssize_t j) const { // range check (invalid indices are passed in by the culling and // patch blend code because they iterate from 0..#patches and examine // neighbors without checking if they're already on the edge) if( (size_t)i >= (size_t)m_MapSizePatches || (size_t)j >= (size_t)m_MapSizePatches ) return 0; return &m_Patches[(j*m_MapSizePatches)+i]; } /////////////////////////////////////////////////////////////////////////////// // GetTile: return the tile at (i,j) in tile space, or null if the tile is out // of bounds CMiniPatch* CTerrain::GetTile(ssize_t i, ssize_t j) const { // see comment above if( (size_t)i >= (size_t)(m_MapSize-1) || (size_t)j >= (size_t)(m_MapSize-1) ) return 0; CPatch* patch=GetPatch(i/PATCH_SIZE, j/PATCH_SIZE); // can't fail (due to above check) return &patch->m_MiniPatches[j%PATCH_SIZE][i%PATCH_SIZE]; } float CTerrain::GetVertexGroundLevel(ssize_t i, ssize_t j) const { i = Clamp(i, static_cast(0), m_MapSize - 1); j = Clamp(j, static_cast(0), m_MapSize - 1); return HEIGHT_SCALE * m_Heightmap[j*m_MapSize + i]; } fixed CTerrain::GetVertexGroundLevelFixed(ssize_t i, ssize_t j) const { i = Clamp(i, static_cast(0), m_MapSize - 1); j = Clamp(j, static_cast(0), m_MapSize - 1); // Convert to fixed metres (being careful to avoid intermediate overflows) return fixed::FromInt(m_Heightmap[j*m_MapSize + i] / 2) / (int)(HEIGHT_UNITS_PER_METRE / 2); } fixed CTerrain::GetSlopeFixed(ssize_t i, ssize_t j) const { // Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1) i = Clamp(i, static_cast(0), m_MapSize - 2); j = Clamp(j, static_cast(0), m_MapSize - 2); u16 h00 = m_Heightmap[j*m_MapSize + i]; u16 h01 = m_Heightmap[(j+1)*m_MapSize + i]; u16 h10 = m_Heightmap[j*m_MapSize + (i+1)]; u16 h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)]; // Difference of highest point from lowest point u16 delta = std::max(std::max(h00, h01), std::max(h10, h11)) - std::min(std::min(h00, h01), std::min(h10, h11)); // Compute fractional slope (being careful to avoid intermediate overflows) return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE; } fixed CTerrain::GetExactSlopeFixed(fixed x, fixed z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp((x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const ssize_t zi = Clamp((z / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const fixed one = fixed::FromInt(1); const fixed xf = Clamp((x / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(xi), fixed::Zero(), one); const fixed zf = Clamp((z / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(zi), fixed::Zero(), one); u16 h00 = m_Heightmap[zi*m_MapSize + xi]; u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; u16 delta; if (GetTriangulationDir(xi, zi)) { if (xf + zf <= one) { // Lower-left triangle (don't use h11) delta = std::max(std::max(h00, h01), h10) - std::min(std::min(h00, h01), h10); } else { // Upper-right triangle (don't use h00) delta = std::max(std::max(h01, h10), h11) - std::min(std::min(h01, h10), h11); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) delta = std::max(std::max(h00, h01), h11) - std::min(std::min(h00, h01), h11); } else { // Lower-right triangle (don't use h01) delta = std::max(std::max(h00, h10), h11) - std::min(std::min(h00, h10), h11); } } // Compute fractional slope (being careful to avoid intermediate overflows) return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE; } float CTerrain::GetFilteredGroundLevel(float x, float z, float radius) const { // convert to [0,1] interval float nx = x / (TERRAIN_TILE_SIZE*m_MapSize); float nz = z / (TERRAIN_TILE_SIZE*m_MapSize); float nr = radius / (TERRAIN_TILE_SIZE*m_MapSize); // get trilinear filtered mipmap height return HEIGHT_SCALE * m_HeightMipmap.GetTrilinearGroundLevel(nx, nz, nr); } float CTerrain::GetExactGroundLevel(float x, float z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp(floor(x / TERRAIN_TILE_SIZE), 0, m_MapSize - 2); const ssize_t zi = Clamp(floor(z / TERRAIN_TILE_SIZE), 0, m_MapSize - 2); const float xf = Clamp(x / TERRAIN_TILE_SIZE - xi, 0.0f, 1.0f); const float zf = Clamp(z / TERRAIN_TILE_SIZE - zi, 0.0f, 1.0f); float h00 = m_Heightmap[zi*m_MapSize + xi]; float h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; float h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Determine which terrain triangle this point is on, // then compute the linearly-interpolated height on that triangle's plane if (GetTriangulationDir(xi, zi)) { if (xf + zf <= 1.f) { // Lower-left triangle (don't use h11) return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h01-h00)*zf); } else { // Upper-right triangle (don't use h00) return HEIGHT_SCALE * (h11 + (h01-h11)*(1-xf) + (h10-h11)*(1-zf)); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) return HEIGHT_SCALE * (h00 + (h11-h01)*xf + (h01-h00)*zf); } else { // Lower-right triangle (don't use h01) return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h11-h10)*zf); } } } fixed CTerrain::GetExactGroundLevelFixed(fixed x, fixed z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp((x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const ssize_t zi = Clamp((z / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const fixed one = fixed::FromInt(1); const fixed xf = Clamp((x / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(xi), fixed::Zero(), one); const fixed zf = Clamp((z / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(zi), fixed::Zero(), one); u16 h00 = m_Heightmap[zi*m_MapSize + xi]; u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Intermediate scaling of xf, so we don't overflow in the multiplications below // (h00 <= 65535, xf <= 1, max fixed is < 32768; divide by 2 here so xf1*h00 <= 32767.5) const fixed xf0 = xf / 2; const fixed xf1 = (one - xf) / 2; // Linearly interpolate return ((one - zf).Multiply(xf1 * h00 + xf0 * h10) + zf.Multiply(xf1 * h01 + xf0 * h11)) / (int)(HEIGHT_UNITS_PER_METRE / 2); // TODO: This should probably be more like GetExactGroundLevel() // in handling triangulation properly } bool CTerrain::GetTriangulationDir(ssize_t i, ssize_t j) const { // Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1) i = Clamp(i, static_cast(0), m_MapSize - 2); j = Clamp(j, static_cast(0), m_MapSize - 2); int h00 = m_Heightmap[j*m_MapSize + i]; int h01 = m_Heightmap[(j+1)*m_MapSize + i]; int h10 = m_Heightmap[j*m_MapSize + (i+1)]; int h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)]; // Prefer triangulating in whichever direction means the midpoint of the diagonal // will be the highest. (In particular this means a diagonal edge will be straight // along the top, and jagged along the bottom, which makes sense for terrain.) int mid1 = h00+h11; int mid2 = h01+h10; return (mid1 < mid2); } -/////////////////////////////////////////////////////////////////////////////// -// Resize: resize this terrain to the given size (in patches per side) -void CTerrain::Resize(ssize_t size) +void CTerrain::ResizeAndOffset(ssize_t size, ssize_t horizontalOffset, ssize_t verticalOffset) { - if (size==m_MapSizePatches) { - // inexplicable request to resize terrain to the same size .. ignore it + if (size == m_MapSizePatches && horizontalOffset == 0 && verticalOffset == 0) + { + // Inexplicable request to resize terrain to the same size, ignore it. return; } - if (!m_Heightmap) { - // not yet created a terrain; build a default terrain of the given size now - Initialize(size,0); + if (!m_Heightmap || + std::abs(horizontalOffset) >= size / 2 + m_MapSizePatches / 2 || + std::abs(verticalOffset) >= size / 2 + m_MapSizePatches / 2) + { + // We have not yet created a terrain, or we are offsetting outside the current source. + // Let's build a default terrain of the given size now. + Initialize(size, 0); return; } - // allocate data for new terrain - ssize_t newMapSize=size*PATCH_SIZE+1; - u16* newHeightmap=new u16[newMapSize*newMapSize]; - CPatch* newPatches=new CPatch[size*size]; - - if (size>m_MapSizePatches) { - // new map is bigger than old one - zero the heightmap so we don't get uninitialised - // height data along the expanded edges - memset(newHeightmap,0,newMapSize*newMapSize*sizeof(u16)); - } - - // now copy over rows of data - u16* src=m_Heightmap; - u16* dst=newHeightmap; - ssize_t copysize=std::min(newMapSize, m_MapSize); - for (ssize_t j=0;jm_MapSize) { - // extend the last height to the end of the row - for (size_t i=0;im_MapSize) { - // copy over heights of the last row to any remaining rows - src=newHeightmap+((m_MapSize-1)*newMapSize); - dst=src+newMapSize; - for (ssize_t i=0;i(0), m_MapSizePatches / 2 - size / 2 + horizontalOffset); + const ssize_t sourceUpperLeftZ = std::max( + static_cast(0), m_MapSizePatches / 2 - size / 2 + verticalOffset); + + const ssize_t destUpperLeftX = std::max( + static_cast(0), (size / 2 - m_MapSizePatches / 2 - horizontalOffset)); + const ssize_t destUpperLeftZ = std::max( + static_cast(0), (size / 2 - m_MapSizePatches / 2 - verticalOffset)); + + const ssize_t width = + std::min(m_MapSizePatches, m_MapSizePatches / 2 + horizontalOffset + size / 2) - sourceUpperLeftX; + const ssize_t depth = + std::min(m_MapSizePatches, m_MapSizePatches / 2 + verticalOffset + size / 2) - sourceUpperLeftZ; + + for (ssize_t j = 0; j < depth * PATCH_SIZE; ++j) + { + // Copy the main part from the source. Destination heightmap: + // +----------+ + // | | + // | 1234 | < current j-th row for example. + // | 5678 | + // | | + // +----------+ + u16* dst = newHeightmap + (j + destUpperLeftZ * PATCH_SIZE) * newMapSize + destUpperLeftX * PATCH_SIZE; + u16* src = m_Heightmap + (j + sourceUpperLeftZ * PATCH_SIZE) * m_MapSize + sourceUpperLeftX * PATCH_SIZE; + std::copy_n(src, width * PATCH_SIZE, dst); + if (destUpperLeftX > 0) + { + // Fill the preceding part by copying the first elements of the + // main part. Destination heightmap: + // +----------+ + // | | + // |1111234 | < current j-th row for example. + // | 5678 | + // | | + // +----------+ + u16* dst_prefix = newHeightmap + (j + destUpperLeftZ * PATCH_SIZE) * newMapSize; + std::fill_n(dst_prefix, destUpperLeftX * PATCH_SIZE, dst[0]); + } + if ((destUpperLeftX + width) * PATCH_SIZE < newMapSize) + { + // Fill the succeeding part by copying the last elements of the + // main part. Destination heightmap: + // +----------+ + // | | + // |1111234444| < current j-th row for example. + // | 5678 | + // | | + // +----------+ + u16* dst_suffix = dst + width * PATCH_SIZE; + std::fill_n( + dst_suffix, + newMapSize - (width + destUpperLeftX) * PATCH_SIZE, + dst[width * PATCH_SIZE - 1]); + } + } + // Copy over heights from the preceding row. Destination heightmap: + // +----------+ + // |1111234444| < copied from the row below + // |1111234444| + // |5555678888| + // | | + // +----------+ + for (ssize_t j = 0; j < destUpperLeftZ * PATCH_SIZE; ++j) + { + + u16* dst = newHeightmap + j * newMapSize; + u16* src = newHeightmap + destUpperLeftZ * PATCH_SIZE * newMapSize; + std::copy_n(src, newMapSize, dst); + } + // Copy over heights from the succeeding row. Destination heightmap: + // +----------+ + // |1111234444| + // |1111234444| + // |5555678888| + // |5555678888| < copied from the row above + // +----------+ + for (ssize_t j = (destUpperLeftZ + depth) * PATCH_SIZE; j < newMapSize; ++j) + { + u16* dst = newHeightmap + j * newMapSize; + u16* src = newHeightmap + ((destUpperLeftZ + depth) * PATCH_SIZE - 1) * newMapSize; + std::copy_n(src, newMapSize, dst); + } + + // Now build new patches. The same process as for the heightmap. + for (ssize_t j = 0; j < depth; ++j) + { + for (size_t i = 0; i < width; ++i) + { + const CPatch& src = + m_Patches[(sourceUpperLeftZ + j) * m_MapSizePatches + sourceUpperLeftX + i]; + CPatch& dst = + newPatches[(destUpperLeftZ + j) * size + destUpperLeftX + i]; + std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); + } + for (ssize_t i = 0; i < destUpperLeftX; ++i) + for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch) + { + const CMiniPatch& src = + newPatches[(destUpperLeftZ + j) * size + destUpperLeftX] + .m_MiniPatches[jPatch][0]; + for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch) + { + CMiniPatch& dst = + newPatches[(destUpperLeftZ + j) * size + i] + .m_MiniPatches[jPatch][iPatch]; + dst = src; + } } - } - - if (jm_MapSizePatches) { - // copy over the last tile from each column - for (ssize_t n=0;nm_MapSizePatches) { - // copy over the last tile from each column - CPatch* srcpatch=&newPatches[(m_MapSizePatches-1)*size]; - CPatch* dstpatch=srcpatch+size; - for (ssize_t p=0;p<(ssize_t)size-m_MapSizePatches;p++) { - for (ssize_t n=0;n<(ssize_t)size;n++) { - for (ssize_t m=0;mm_MiniPatches[15][k]; - CMiniPatch& dst=dstpatch->m_MiniPatches[m][k]; - dst = src; - } + for (ssize_t j = 0; j < destUpperLeftZ; ++j) + for (ssize_t i = 0; i < size; ++i) + for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch) + { + const CMiniPatch& src = + newPatches[destUpperLeftZ * size + i].m_MiniPatches[0][iPatch]; + for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch) + { + CMiniPatch& dst = + newPatches[j * size + i].m_MiniPatches[jPatch][iPatch]; + dst = src; + } + } + for (ssize_t j = destUpperLeftZ + depth; j < size; ++j) + for (ssize_t i = 0; i < size; ++i) + for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch) + { + const CMiniPatch& src = + newPatches[(destUpperLeftZ + depth - 1) * size + i].m_MiniPatches[0][iPatch]; + for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch) + { + CMiniPatch& dst = + newPatches[j * size + i].m_MiniPatches[jPatch][iPatch]; + dst = src; } - srcpatch++; - dstpatch++; } - } - } - - // release all the original data + // Release all the original data. ReleaseData(); - // store new data - m_Heightmap=newHeightmap; - m_Patches=newPatches; - m_MapSize=(ssize_t)newMapSize; - m_MapSizePatches=(ssize_t)size; + // Store new data. + m_Heightmap = newHeightmap; + m_Patches = newPatches; + m_MapSize = newMapSize; + m_MapSizePatches = size; - // initialise all the new patches + // Initialise all the new patches. InitialisePatches(); - // initialise mipmap - m_HeightMipmap.Initialize(m_MapSize,m_Heightmap); + // Initialise mipmap. + m_HeightMipmap.Initialize(m_MapSize, m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// // InitialisePatches: initialise patch data void CTerrain::InitialisePatches() { for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail patch->Initialize(this, i, j); } } } /////////////////////////////////////////////////////////////////////////////// // SetHeightMap: set up a new heightmap from 16-bit source data; // assumes heightmap matches current terrain size void CTerrain::SetHeightMap(u16* heightmap) { // keep a copy of the given heightmap memcpy(m_Heightmap, heightmap, m_MapSize*m_MapSize*sizeof(u16)); // recalculate patch bounds, invalidate vertices for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail patch->InvalidateBounds(); patch->SetDirty(RENDERDATA_UPDATE_VERTICES); } } // update mipmap m_HeightMipmap.Update(m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// void CTerrain::MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags) { // Finds the inclusive limits of the patches that include the specified range of tiles ssize_t pi0 = Clamp( i0 /PATCH_SIZE, static_cast(0), m_MapSizePatches-1); ssize_t pi1 = Clamp((i1-1)/PATCH_SIZE, static_cast(0), m_MapSizePatches-1); ssize_t pj0 = Clamp( j0 /PATCH_SIZE, static_cast(0), m_MapSizePatches-1); ssize_t pj1 = Clamp((j1-1)/PATCH_SIZE, static_cast(0), m_MapSizePatches-1); for (ssize_t j = pj0; j <= pj1; j++) { for (ssize_t i = pi0; i <= pi1; i++) { CPatch* patch = GetPatch(i, j); // can't fail (i,j were clamped) if (dirtyFlags & RENDERDATA_UPDATE_VERTICES) patch->CalcBounds(); patch->SetDirty(dirtyFlags); } } if (m_Heightmap) { m_HeightMipmap.Update(m_Heightmap, Clamp(i0, static_cast(0), m_MapSize - 1), Clamp(j0, static_cast(0), m_MapSize - 1), Clamp(i1, static_cast(1), m_MapSize), Clamp(j1, static_cast(1), m_MapSize) ); } } void CTerrain::MakeDirty(int dirtyFlags) { for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail if (dirtyFlags & RENDERDATA_UPDATE_VERTICES) patch->CalcBounds(); patch->SetDirty(dirtyFlags); } } if (m_Heightmap) m_HeightMipmap.Update(m_Heightmap); } CBoundingBoxAligned CTerrain::GetVertexesBound(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) { i0 = Clamp(i0, static_cast(0), m_MapSize - 1); j0 = Clamp(j0, static_cast(0), m_MapSize - 1); i1 = Clamp(i1, static_cast(0), m_MapSize - 1); j1 = Clamp(j1, static_cast(0), m_MapSize - 1); u16 minH = 65535; u16 maxH = 0; for (ssize_t j = j0; j <= j1; ++j) { for (ssize_t i = i0; i <= i1; ++i) { minH = std::min(minH, m_Heightmap[j*m_MapSize + i]); maxH = std::max(maxH, m_Heightmap[j*m_MapSize + i]); } } CBoundingBoxAligned bound; bound[0].X = (float)(i0*TERRAIN_TILE_SIZE); bound[0].Y = (float)(minH*HEIGHT_SCALE); bound[0].Z = (float)(j0*TERRAIN_TILE_SIZE); bound[1].X = (float)(i1*TERRAIN_TILE_SIZE); bound[1].Y = (float)(maxH*HEIGHT_SCALE); bound[1].Z = (float)(j1*TERRAIN_TILE_SIZE); return bound; } Index: ps/trunk/source/graphics/Terrain.h =================================================================== --- ps/trunk/source/graphics/Terrain.h (revision 23638) +++ ps/trunk/source/graphics/Terrain.h (revision 23639) @@ -1,182 +1,183 @@ -/* Copyright (C) 2011 Wildfire Games. +/* 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 . */ /* * Describes ground via heightmap and array of CPatch. */ #ifndef INCLUDED_TERRAIN #define INCLUDED_TERRAIN -#include "maths/Vector3D.h" -#include "maths/Fixed.h" -#include "graphics/SColor.h" #include "graphics/HeightMipmap.h" +#include "graphics/SColor.h" +#include "maths/Fixed.h" +#include "maths/Vector3D.h" class CPatch; class CMiniPatch; class CFixedVector3D; class CStr8; class CBoundingBoxAligned; /////////////////////////////////////////////////////////////////////////////// // Terrain Constants: /// metres [world space units] per tile in x and z const ssize_t TERRAIN_TILE_SIZE = 4; /// number of u16 height units per metre const ssize_t HEIGHT_UNITS_PER_METRE = 92; /// metres per u16 height unit const float HEIGHT_SCALE = 1.f / HEIGHT_UNITS_PER_METRE; /////////////////////////////////////////////////////////////////////////////// // CTerrain: main terrain class; contains the heightmap describing elevation // data, and the smaller subpatches that form the terrain class CTerrain { public: CTerrain(); ~CTerrain(); // Coordinate naming convention: world-space coordinates are float x,z; // tile-space coordinates are ssize_t i,j. rationale: signed types can // more efficiently be converted to/from floating point. use ssize_t // instead of int/long because these are sizes. bool Initialize(ssize_t patchesPerSide, const u16* ptr); // return number of vertices along edge of the terrain ssize_t GetVerticesPerSide() const { return m_MapSize; } // return number of tiles along edge of the terrain ssize_t GetTilesPerSide() const { return GetVerticesPerSide()-1; } // return number of patches along edge of the terrain ssize_t GetPatchesPerSide() const { return m_MapSizePatches; } float GetMinX() const { return 0.0f; } float GetMinZ() const { return 0.0f; } float GetMaxX() const { return (float)((m_MapSize-1) * TERRAIN_TILE_SIZE); } float GetMaxZ() const { return (float)((m_MapSize-1) * TERRAIN_TILE_SIZE); } bool IsOnMap(float x, float z) const { return ((x >= GetMinX()) && (x < GetMaxX()) && (z >= GetMinZ()) && (z < GetMaxZ())); } CStr8 GetMovementClass(ssize_t i, ssize_t j) const; float GetVertexGroundLevel(ssize_t i, ssize_t j) const; fixed GetVertexGroundLevelFixed(ssize_t i, ssize_t j) const; float GetExactGroundLevel(float x, float z) const; fixed GetExactGroundLevelFixed(fixed x, fixed z) const; float GetFilteredGroundLevel(float x, float z, float radius) const; // get the approximate slope of a tile // (0 = horizontal, 0.5 = 30 degrees, 1.0 = 45 degrees, etc) fixed GetSlopeFixed(ssize_t i, ssize_t j) const; // get the precise slope of a point, accounting for triangulation direction fixed GetExactSlopeFixed(fixed x, fixed z) const; // Returns true if the triangulation diagonal for tile (i, j) // should be in the direction (1,-1); false if it should be (1,1) bool GetTriangulationDir(ssize_t i, ssize_t j) const; - // resize this terrain such that each side has given number of patches - void Resize(ssize_t size); + // Resize this terrain such that each side has given number of patches, + // with the center offset in patches from the center of the source. + void ResizeAndOffset(ssize_t size, ssize_t horizontalOffset = 0, ssize_t verticalOffset = 0); // set up a new heightmap from 16 bit data; assumes heightmap matches current terrain size void SetHeightMap(u16* heightmap); // return a pointer to the heightmap u16* GetHeightMap() const { return m_Heightmap; } // get patch at given coordinates, expressed in patch-space; return 0 if // coordinates represent patch off the edge of the map CPatch* GetPatch(ssize_t i, ssize_t j) const; // get tile at given coordinates, expressed in tile-space; return 0 if // coordinates represent tile off the edge of the map CMiniPatch* GetTile(ssize_t i, ssize_t j) const; // calculate the position of a given vertex void CalcPosition(ssize_t i, ssize_t j, CVector3D& pos) const; void CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) const; // calculate the vertex under a given position (rounding down coordinates) static void CalcFromPosition(const CVector3D& pos, ssize_t& i, ssize_t& j) { i = (ssize_t)(pos.X/TERRAIN_TILE_SIZE); j = (ssize_t)(pos.Z/TERRAIN_TILE_SIZE); } // calculate the vertex under a given position (rounding down coordinates) static void CalcFromPosition(float x, float z, ssize_t& i, ssize_t& j) { i = (ssize_t)(x/TERRAIN_TILE_SIZE); j = (ssize_t)(z/TERRAIN_TILE_SIZE); } // calculate the normal at a given vertex void CalcNormal(ssize_t i, ssize_t j, CVector3D& normal) const; void CalcNormalFixed(ssize_t i, ssize_t j, CFixedVector3D& normal) const; CVector3D CalcExactNormal(float x, float z) const; // Mark a specific square of tiles (inclusive lower bound, exclusive upper bound) // as dirty - use this after modifying the heightmap. // If you modify a vertex (i,j), you should dirty tiles // from (i-1, j-1) [inclusive] to (i+1, j+1) [exclusive] // since their geometry depends on that vertex. // If you modify a tile (i,j), you should dirty tiles // from (i-1, j-1) [inclusive] to (i+2, j+2) [exclusive] // since their texture blends depend on that tile. void MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags); // mark the entire map as dirty void MakeDirty(int dirtyFlags); /** * Returns a 3D bounding box encompassing the given vertex range (inclusive) */ CBoundingBoxAligned GetVertexesBound(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1); // get the base color for the terrain (typically pure white - other colors // will interact badly with LOS - but used by the Actor Viewer tool) SColor4ub GetBaseColor() const { return m_BaseColor; } // set the base color for the terrain void SetBaseColor(SColor4ub color) { m_BaseColor = color; } const CHeightMipmap& GetHeightMipmap() const { return m_HeightMipmap; } private: // delete any data allocated by this terrain void ReleaseData(); // setup patch pointers etc void InitialisePatches(); // size of this map in each direction, in vertices; ie. total tiles = sqr(m_MapSize-1) ssize_t m_MapSize; // size of this map in each direction, in patches; total patches = sqr(m_MapSizePatches) ssize_t m_MapSizePatches; // the patches comprising this terrain CPatch* m_Patches; // 16-bit heightmap data u16* m_Heightmap; // base color (usually white) SColor4ub m_BaseColor; // heightmap mipmap CHeightMipmap m_HeightMipmap; }; -#endif +#endif // INCLUDED_TERRAIN Index: ps/trunk/source/graphics/tests/test_Terrain.h =================================================================== --- ps/trunk/source/graphics/tests/test_Terrain.h (revision 23638) +++ ps/trunk/source/graphics/tests/test_Terrain.h (revision 23639) @@ -1,341 +1,473 @@ /* 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}, + {1, 2}, + {3, 4} } }, { + 0, 0, { {1, 2}, {3, 4} }, { - {1, 2, 0}, - {3, 4, 0}, - {0, 0, 0} + {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 iTile = 0; iTile < sourceSize; ++iTile) + for (ssize_t jTile = 0; jTile < sourceSize; ++jTile) { - TS_ASSERT_EQUALS(sourceSize, testCase.sourcePatches[iTile].size()); - 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 i = 0; i < PATCH_SIZE; ++i) - for (ssize_t j = 0; j < PATCH_SIZE; ++j) + 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[iTile][jTile]; + heightmap[idx] = testCase.sourcePatches[jTile][iTile]; } } } terrain.Initialize(sourceSize, heightmap.data()); } - terrain.Resize(expectedSize); + terrain.ResizeAndOffset(expectedSize, testCase.horizontalOffset, testCase.verticalOffset); TS_ASSERT_EQUALS(expectedMapSize, terrain.GetVerticesPerSide()); TS_ASSERT_EQUALS(expectedSize, terrain.GetPatchesPerSide()); - for (ssize_t iTile = 0; iTile < expectedSize; ++iTile) + for (ssize_t jTile = 0; jTile < expectedSize; ++jTile) { TS_ASSERT_EQUALS( - expectedSize, testCase.expectedPatches[iTile].size()); - for (ssize_t jTile = 0; jTile < expectedSize; ++jTile) + expectedSize, testCase.expectedPatches[jTile].size()); + for (ssize_t iTile = 0; iTile < expectedSize; ++iTile) { - for (ssize_t i = 0; i < PATCH_SIZE; ++i) - for (ssize_t j = 0; j < PATCH_SIZE; ++j) + 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[iTile][jTile] == + 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[iTile][jTile]; + << " 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 iTile = 0; iTile < expectedSize; ++iTile) + for (ssize_t jTile = 0; jTile < expectedSize; ++jTile) { ss.str(std::string()); ss << "["; - for (ssize_t jTile = 0; jTile < expectedSize; ++jTile) + for (ssize_t iTile = 0; iTile < expectedSize; ++iTile) { - if (jTile) + if (iTile) ss << ", "; ss << GetVertex(terrain, iTile * PATCH_SIZE, jTile * PATCH_SIZE); } ss << "]"; TS_WARN(ss.str()); } return; } } } } }; Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 23638) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 23639) @@ -1,368 +1,372 @@ -/* Copyright (C) 2019 Wildfire Games. +/* 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 "precompiled.h" #include "MessageHandler.h" #include "../GameLoop.h" #include "../CommandProc.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MapIO.h" #include "graphics/MapWriter.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "lib/bits.h" #include "lib/file/vfs/vfs_path.h" #include "lib/status.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Loader.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTerrain.h" namespace { void InitGame() { if (g_Game) { delete g_Game; g_Game = NULL; } g_Game = new CGame(false); // Default to player 1 for playtesting g_Game->SetPlayerID(1); } void StartGame(JS::MutableHandleValue attrs) { g_Game->StartGame(attrs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); // Disable fog-of-war - this must be done before starting the game, // as visual actors cache their visibility state on first render. CmpPtr cmpRangeManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(-1, true); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); } } namespace AtlasMessage { QUERYHANDLER(GenerateMap) { try { InitGame(); // Random map const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue settings(cx); scriptInterface.ParseJSON(*msg->settings, &settings); scriptInterface.SetProperty(settings, "mapType", "random"); JS::RootedValue attrs(cx); ScriptInterface::CreateObject( cx, &attrs, "mapType", "random", "script", *msg->filename, "settings", settings); StartGame(&attrs); msg->status = 0; } catch (PSERROR_Game_World_MapLoadFailed&) { // Cancel loading LDR_Cancel(); // Since map generation failed and we don't know why, use the blank map as a fallback InitGame(); const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); // Set up 8-element array of empty objects to satisfy init JS::RootedValue playerData(cx); ScriptInterface::CreateArray(cx, &playerData); for (int i = 0; i < 8; ++i) { JS::RootedValue player(cx); ScriptInterface::CreateObject(cx, &player); scriptInterface.SetPropertyInt(playerData, i, player); } JS::RootedValue settings(cx); ScriptInterface::CreateObject( cx, &settings, "mapType", "scenario", "PlayerData", playerData); JS::RootedValue attrs(cx); ScriptInterface::CreateObject( cx, &attrs, "mapType", "scenario", "map", "maps/scenarios/_default", "settings", settings); StartGame(&attrs); msg->status = -1; } } MESSAGEHANDLER(LoadMap) { InitGame(); const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); // Scenario CStrW map = *msg->filename; CStrW mapBase = map.BeforeLast(L".pmp"); // strip the file extension, if any JS::RootedValue attrs(cx); ScriptInterface::CreateObject( cx, &attrs, "mapType", "scenario", "map", mapBase); StartGame(&attrs); } MESSAGEHANDLER(ImportHeightmap) { std::vector heightmap_source; if (LoadHeightmapImageOs(*msg->filename, heightmap_source) != INFO::OK) { LOGERROR("Failed to decode heightmap."); return; } // resize terrain to heightmap size // Notice that the number of tiles/pixels per side of the heightmap image is // one less than the number of vertices per side of the heightmap. CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - terrain->Resize((sqrt(heightmap_source.size()) - 1) / PATCH_SIZE); + const ssize_t newSize = (sqrt(heightmap_source.size()) - 1) / PATCH_SIZE; + const ssize_t offset = (newSize - terrain->GetPatchesPerSide()) / 2; + terrain->ResizeAndOffset(newSize, offset, offset); // copy heightmap data into map u16* heightmap = g_Game->GetWorld()->GetTerrain()->GetHeightMap(); ENSURE(heightmap_source.size() == (std::size_t) SQR(g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide())); std::copy(heightmap_source.begin(), heightmap_source.end(), heightmap); // update simulation CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); g_Game->GetView()->GetLOSTexture().MakeDirty(); } MESSAGEHANDLER(SaveMap) { CMapWriter writer; VfsPath pathname = VfsPath(*msg->filename).ChangeExtension(L".pmp"); writer.SaveMap(pathname, g_Game->GetWorld()->GetTerrain(), g_Renderer.GetWaterManager(), g_Renderer.GetSkyManager(), &g_LightEnv, g_Game->GetView()->GetCamera(), g_Game->GetView()->GetCinema(), &g_Renderer.GetPostprocManager(), g_Game->GetSimulation2()); } QUERYHANDLER(GetMapSettings) { msg->settings = g_Game->GetSimulation2()->GetMapSettingsString(); } BEGIN_COMMAND(SetMapSettings) { std::string m_OldSettings, m_NewSettings; void SetSettings(const std::string& settings) { g_Game->GetSimulation2()->SetMapSettings(settings); } void Do() { m_OldSettings = g_Game->GetSimulation2()->GetMapSettingsString(); m_NewSettings = *msg->settings; SetSettings(m_NewSettings); } // TODO: we need some way to notify the Atlas UI when the settings are changed // externally, otherwise this will have no visible effect void Undo() { // SetSettings(m_OldSettings); } void Redo() { // SetSettings(m_NewSettings); } void MergeIntoPrevious(cSetMapSettings* prev) { prev->m_NewSettings = m_NewSettings; } }; END_COMMAND(SetMapSettings) MESSAGEHANDLER(LoadPlayerSettings) { g_Game->GetSimulation2()->LoadPlayerSettings(msg->newplayers); } QUERYHANDLER(GetMapSizes) { msg->sizes = g_Game->GetSimulation2()->GetMapSizes(); } QUERYHANDLER(GetRMSData) { msg->data = g_Game->GetSimulation2()->GetRMSData(); } BEGIN_COMMAND(ResizeMap) { int m_OldTiles, m_NewTiles; cResizeMap() { } void MakeDirty() { CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); // The LOS texture won't normally get updated when running Atlas // (since there's no simulation updates), so explicitly dirty it g_Game->GetView()->GetLOSTexture().MakeDirty(); } void ResizeTerrain(int tiles) { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - terrain->Resize(tiles / PATCH_SIZE); + const ssize_t newSize = tiles / PATCH_SIZE; + const ssize_t offset = (newSize - terrain->GetPatchesPerSide()) / 2; + terrain->ResizeAndOffset(newSize, offset, offset); MakeDirty(); } void Do() { CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpTerrain) { m_OldTiles = m_NewTiles = 0; } else { m_OldTiles = (int)cmpTerrain->GetTilesPerSide(); m_NewTiles = msg->tiles; } ResizeTerrain(m_NewTiles); } void Undo() { ResizeTerrain(m_OldTiles); } void Redo() { ResizeTerrain(m_NewTiles); } }; END_COMMAND(ResizeMap) QUERYHANDLER(VFSFileExists) { msg->exists = VfsFileExists(*msg->path); } QUERYHANDLER(VFSFileRealPath) { VfsPath pathname(*msg->path); if (pathname.empty()) return; OsPath realPathname; if (g_VFS->GetRealPath(pathname, realPathname) == INFO::OK) msg->realPath = realPathname.string(); } static Status AddToFilenames(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& filenames = *(std::vector*)cbData; filenames.push_back(pathname.string().c_str()); return INFO::OK; } QUERYHANDLER(GetMapList) { #define GET_FILE_LIST(path, list) \ std::vector list; \ vfs::ForEachFile(g_VFS, path, AddToFilenames, (uintptr_t)&list, L"*.xml", vfs::DIR_RECURSIVE); \ msg->list = list; GET_FILE_LIST(L"maps/scenarios/", scenarioFilenames); GET_FILE_LIST(L"maps/skirmishes/", skirmishFilenames); GET_FILE_LIST(L"maps/tutorials/", tutorialFilenames); #undef GET_FILE_LIST } }