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
}
}