LCOV - code coverage report
Current view: top level - source/graphics - HFTracer.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 1 150 0.7 %
Date: 2023-01-19 00:18:29 Functions: 2 8 25.0 %

          Line data    Source code
       1             : /* Copyright (C) 2021 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : /*
      19             :  * Determine intersection of rays with a heightfield.
      20             :  */
      21             : 
      22             : #include "precompiled.h"
      23             : 
      24             : #include "HFTracer.h"
      25             : 
      26             : #include "graphics/Patch.h"
      27             : #include "graphics/Terrain.h"
      28             : #include "maths/BoundingBoxAligned.h"
      29             : #include "maths/MathUtil.h"
      30             : #include "maths/Vector3D.h"
      31             : 
      32             : #include <cfloat>
      33             : 
      34             : // To cope well with points that are slightly off the edge of the map,
      35             : // we act as if there's an N-tile margin around the edges of the heightfield.
      36             : // (N shouldn't be too huge else it'll hurt performance a little when
      37             : // RayIntersect loops through it all.)
      38             : // CTerrain::CalcPosition implements clamp-to-edge behaviour so the tracer
      39             : // will have that behaviour.
      40             : static const int MARGIN_SIZE = 64;
      41             : 
      42             : ///////////////////////////////////////////////////////////////////////////////
      43             : // CHFTracer constructor
      44           0 : CHFTracer::CHFTracer(CTerrain *pTerrain):
      45             :     m_pTerrain(pTerrain),
      46           0 :     m_Heightfield(m_pTerrain->GetHeightMap()),
      47           0 :     m_MapSize(m_pTerrain->GetVerticesPerSide()),
      48             :     m_CellSize((float)TERRAIN_TILE_SIZE),
      49           0 :     m_HeightScale(HEIGHT_SCALE)
      50             : {
      51           0 : }
      52             : 
      53             : 
      54             : ///////////////////////////////////////////////////////////////////////////////
      55             : // RayTriIntersect: intersect a ray with triangle defined by vertices
      56             : // v0,v1,v2; return true if ray hits triangle at distance less than dist,
      57             : // or false otherwise
      58           0 : static bool RayTriIntersect(const CVector3D& v0, const CVector3D& v1, const CVector3D& v2,
      59             :                                 const CVector3D& origin, const CVector3D& dir, float& dist)
      60             : {
      61           0 :     const float EPSILON=0.00001f;
      62             : 
      63             :     // calculate edge vectors
      64           0 :     CVector3D edge0=v1-v0;
      65           0 :     CVector3D edge1=v2-v0;
      66             : 
      67             :     // begin calculating determinant - also used to calculate U parameter
      68           0 :     CVector3D pvec=dir.Cross(edge1);
      69             : 
      70             :     // if determinant is near zero, ray lies in plane of triangle
      71           0 :     float det = edge0.Dot(pvec);
      72           0 :     if (fabs(det)<EPSILON)
      73           0 :         return false;
      74             : 
      75           0 :     float inv_det = 1.0f/det;
      76             : 
      77             :     // calculate vector from vert0 to ray origin
      78           0 :     CVector3D tvec=origin-v0;
      79             : 
      80             :     // calculate U parameter, test bounds
      81           0 :     float u=tvec.Dot(pvec)*inv_det;
      82           0 :     if (u<-0.01f || u>1.01f)
      83           0 :         return false;
      84             : 
      85             :     // prepare to test V parameter
      86           0 :     CVector3D qvec=tvec.Cross(edge0);
      87             : 
      88             :     // calculate V parameter and test bounds
      89           0 :     float v=dir.Dot(qvec)*inv_det;
      90           0 :     if (v<0.0f || u+v>1.0f)
      91           0 :         return false;
      92             : 
      93             :     // calculate distance to intersection point from ray origin
      94           0 :     float d=edge1.Dot(qvec)*inv_det;
      95           0 :     if (d>=0 && d<dist) {
      96           0 :         dist=d;
      97           0 :         return true;
      98             :     }
      99             : 
     100           0 :     return false;
     101             : }
     102             : 
     103             : ///////////////////////////////////////////////////////////////////////////////
     104             : // CellIntersect: test if ray intersects either of the triangles in the given
     105             : // cell - return hit result, and distance to hit, if hit occurred
     106           0 : bool CHFTracer::CellIntersect(int cx, int cz, const CVector3D& origin, const CVector3D& dir, float& dist) const
     107             : {
     108           0 :     bool res=false;
     109             : 
     110             :     // get vertices for this cell
     111           0 :     CVector3D vpos[4];
     112           0 :     m_pTerrain->CalcPosition(cx,cz,vpos[0]);
     113           0 :     m_pTerrain->CalcPosition(cx+1,cz,vpos[1]);
     114           0 :     m_pTerrain->CalcPosition(cx+1,cz+1,vpos[2]);
     115           0 :     m_pTerrain->CalcPosition(cx,cz+1,vpos[3]);
     116             : 
     117           0 :     dist=1.0e30f;
     118           0 :     if (RayTriIntersect(vpos[0],vpos[1],vpos[2],origin,dir,dist)) {
     119           0 :         res=true;
     120             :     }
     121             : 
     122           0 :     if (RayTriIntersect(vpos[0],vpos[2],vpos[3],origin,dir,dist)) {
     123           0 :         res=true;
     124             :     }
     125             : 
     126           0 :     return res;
     127             : }
     128             : 
     129             : ///////////////////////////////////////////////////////////////////////////////
     130             : // RayIntersect: intersect ray with this heightfield; return true if
     131             : // intersection occurs (and fill in grid coordinates of intersection), or false
     132             : // otherwise
     133           0 : bool CHFTracer::RayIntersect(const CVector3D& origin, const CVector3D& dir, int& x, int& z, CVector3D& ipt) const
     134             : {
     135             :     // If the map is empty (which should never happen),
     136             :     // return early before we crash when reading zero-sized heightmaps
     137           0 :     if (!m_MapSize)
     138             :     {
     139           0 :         debug_warn(L"CHFTracer::RayIntersect called with zero-size map");
     140           0 :         return false;
     141             :     }
     142             : 
     143             :     // intersect first against bounding box
     144           0 :     CBoundingBoxAligned bound;
     145           0 :     bound[0] = CVector3D(-MARGIN_SIZE * m_CellSize, 0, -MARGIN_SIZE * m_CellSize);
     146           0 :     bound[1] = CVector3D((m_MapSize + MARGIN_SIZE) * m_CellSize, 65535 * m_HeightScale, (m_MapSize + MARGIN_SIZE) * m_CellSize);
     147             : 
     148             :     float tmin,tmax;
     149           0 :     if (!bound.RayIntersect(origin,dir,tmin,tmax)) {
     150             :         // ray missed world bounds; no intersection
     151           0 :         return false;
     152             :     }
     153             : 
     154             :     // project origin onto grid, if necessary, to get starting point for traversal
     155           0 :     CVector3D traversalPt;
     156           0 :     if (tmin>0) {
     157           0 :         traversalPt=origin+dir*tmin;
     158             :     } else {
     159           0 :         traversalPt=origin;
     160             :     }
     161             : 
     162             :     // setup traversal variables
     163           0 :     int sx=dir.X<0 ? -1 : 1;
     164           0 :     int sz=dir.Z<0 ? -1 : 1;
     165             : 
     166           0 :     float invCellSize=1.0f/float(m_CellSize);
     167             : 
     168           0 :     float fcx=traversalPt.X*invCellSize;
     169           0 :     int cx=(int)floor(fcx);
     170             : 
     171           0 :     float fcz=traversalPt.Z*invCellSize;
     172           0 :     int cz=(int)floor(fcz);
     173             : 
     174           0 :     float invdx = 1.0e20f;
     175           0 :     float invdz = 1.0e20f;
     176             : 
     177           0 :     if (fabs(dir.X) > 1.0e-20)
     178           0 :         invdx = float(1.0/fabs(dir.X));
     179           0 :     if (fabs(dir.Z) > 1.0e-20)
     180           0 :         invdz = float(1.0/fabs(dir.Z));
     181             : 
     182           0 :     do {
     183             :         // test current cell
     184           0 :         if (cx >= -MARGIN_SIZE && cx < int(m_MapSize + MARGIN_SIZE - 1) && cz >= -MARGIN_SIZE && cz < int(m_MapSize + MARGIN_SIZE - 1))
     185             :         {
     186             :             float dist;
     187             : 
     188           0 :             if (CellIntersect(cx,cz,origin,dir,dist)) {
     189           0 :                 x=cx;
     190           0 :                 z=cz;
     191           0 :                 ipt=origin+dir*dist;
     192           0 :                 return true;
     193           0 :             }
     194             :         }
     195             :         else
     196             :         {
     197             :             // Degenerate case: y close to zero
     198             :             // catch travelling off the map
     199           0 :             if ((cx < -MARGIN_SIZE) && (sx < 0))
     200           0 :                 return false;
     201           0 :             if ((cx >= (int)(m_MapSize + MARGIN_SIZE - 1)) && (sx > 0))
     202           0 :                 return false;
     203           0 :             if ((cz < -MARGIN_SIZE) && (sz < 0))
     204           0 :                 return false;
     205           0 :             if ((cz >= (int)(m_MapSize + MARGIN_SIZE - 1)) && (sz > 0))
     206           0 :                 return false;
     207             :         }
     208             : 
     209             :         // get coords of current cell
     210           0 :         fcx=traversalPt.X*invCellSize;
     211           0 :         fcz=traversalPt.Z*invCellSize;
     212             : 
     213             :         // get distance to next cell in x,z
     214           0 :         float dx=(sx==-1) ? fcx-float(cx) : 1-(fcx-float(cx));
     215           0 :         dx*=invdx;
     216           0 :         float dz=(sz==-1) ? fcz-float(cz) : 1-(fcz-float(cz));
     217           0 :         dz*=invdz;
     218             : 
     219             :         // advance ..
     220             :         float dist;
     221           0 :         if (dx<dz) {
     222           0 :             cx+=sx;
     223           0 :             dist=dx;
     224             :         } else {
     225           0 :             cz+=sz;
     226           0 :             dist=dz;
     227             :         }
     228             : 
     229           0 :         traversalPt+=dir*dist;
     230           0 :     } while (traversalPt.Y>=0);
     231             : 
     232             :     // fell off end of heightmap with no intersection; return a miss
     233           0 :     return false;
     234             : }
     235             : 
     236           0 : static bool TestTile(u16* heightmap, int stride, int i, int j, const CVector3D& pos, const CVector3D& dir, CVector3D& isct)
     237             : {
     238           0 :     u16 y00 = heightmap[i + j*stride];
     239           0 :     u16 y10 = heightmap[i+1 + j*stride];
     240           0 :     u16 y01 = heightmap[i + (j+1)*stride];
     241           0 :     u16 y11 = heightmap[i+1 + (j+1)*stride];
     242             : 
     243           0 :     CVector3D p00(    i * TERRAIN_TILE_SIZE, y00 * HEIGHT_SCALE,     j * TERRAIN_TILE_SIZE);
     244           0 :     CVector3D p10((i+1) * TERRAIN_TILE_SIZE, y10 * HEIGHT_SCALE,     j * TERRAIN_TILE_SIZE);
     245           0 :     CVector3D p01(    i * TERRAIN_TILE_SIZE, y01 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE);
     246           0 :     CVector3D p11((i+1) * TERRAIN_TILE_SIZE, y11 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE);
     247             : 
     248           0 :     int mid1 = y00+y11;
     249           0 :     int mid2 = y01+y10;
     250           0 :     int triDir = (mid1 < mid2);
     251             : 
     252           0 :     float dist = FLT_MAX;
     253             : 
     254           0 :     if (triDir)
     255             :     {
     256           0 :         if (RayTriIntersect(p00, p10, p01, pos, dir, dist) || // lower-left triangle
     257           0 :             RayTriIntersect(p11, p01, p10, pos, dir, dist))   // upper-right triangle
     258             :         {
     259           0 :             isct = pos + dir * dist;
     260           0 :             return true;
     261             :         }
     262             :     }
     263             :     else
     264             :     {
     265           0 :         if (RayTriIntersect(p00, p11, p01, pos, dir, dist) || // upper-left triangle
     266           0 :             RayTriIntersect(p00, p10, p11, pos, dir, dist))   // lower-right triangle
     267             :         {
     268           0 :             isct = pos + dir * dist;
     269           0 :             return true;
     270             :         }
     271             :     }
     272           0 :     return false;
     273             : }
     274             : 
     275           0 : bool CHFTracer::PatchRayIntersect(CPatch* patch, const CVector3D& origin, const CVector3D& dir, CVector3D* out)
     276             : {
     277             :     // (TODO: This largely duplicates RayIntersect - some refactoring might be
     278             :     // nice in the future.)
     279             : 
     280             :     // General approach:
     281             :     // Given the ray defined by origin + dir * t, we increase t until it
     282             :     // enters the patch's bounding box. The x,z coordinates identify which
     283             :     // tile it is currently above/below. Do an intersection test vs the tile's
     284             :     // two triangles. If it doesn't hit, do a 2D line rasterisation to find
     285             :     // the next tiles the ray will pass through, and test each of them.
     286             : 
     287             :     // Start by jumping to the point where the ray enters the bounding box
     288           0 :     CBoundingBoxAligned bound = patch->GetWorldBounds();
     289             :     float tmin, tmax;
     290           0 :     if (!bound.RayIntersect(origin, dir, tmin, tmax))
     291             :     {
     292             :         // Ray missed patch; no intersection
     293           0 :         return false;
     294             :     }
     295             : 
     296           0 :     int heightmapStride = patch->m_Parent->GetVerticesPerSide();
     297             : 
     298             :     // Get heightmap, offset to start at this patch
     299           0 :     u16* heightmap = patch->m_Parent->GetHeightMap() +
     300           0 :             patch->m_X * PATCH_SIZE +
     301           0 :             patch->m_Z * PATCH_SIZE * heightmapStride;
     302             : 
     303             :     // Get patch-space position of ray origin and bbox entry point
     304             :     CVector3D patchPos(
     305           0 :             patch->m_X * PATCH_SIZE * TERRAIN_TILE_SIZE,
     306             :             0.0f,
     307           0 :             patch->m_Z * PATCH_SIZE * TERRAIN_TILE_SIZE);
     308           0 :     CVector3D originPatch = origin - patchPos;
     309           0 :     CVector3D entryPatch = originPatch + dir * tmin;
     310             : 
     311             :     // We want to do a simple 2D line rasterisation (with the 3D ray projected
     312             :     // down onto the Y plane). That will tell us which cells are intersected
     313             :     // in 2D dimensions, then we can do a more precise 3D intersection test.
     314             :     //
     315             :     // WLOG, assume the ray has direction dir.x > 0, dir.z > 0, and starts in
     316             :     // cell (i,j). The next cell intersecting the line must be either (i+1,j)
     317             :     // or (i,j+1). To tell which, just check whether the point (i+1,j+1) is
     318             :     // above or below the ray. Advance into that cell and repeat.
     319             :     //
     320             :     // (If the ray passes precisely through (i+1,j+1), we can pick either.
     321             :     // If the ray is parallel to Y, only the first cell matters, then we can
     322             :     // carry on rasterising in any direction (a bit of a waste of time but
     323             :     // should be extremely rare, and it's safe and simple).)
     324             : 
     325             :     // Work out which tile we're starting in
     326           0 :     int i = Clamp<int>(entryPatch.X / TERRAIN_TILE_SIZE, 0, PATCH_SIZE - 1);
     327           0 :     int j = Clamp<int>(entryPatch.Z / TERRAIN_TILE_SIZE, 0, PATCH_SIZE - 1);
     328             : 
     329             :     // Work out which direction the ray is going in
     330           0 :     int di = (dir.X >= 0 ? 1 : 0);
     331           0 :     int dj = (dir.Z >= 0 ? 1 : 0);
     332             : 
     333           0 :     do
     334             :     {
     335           0 :         CVector3D isct;
     336           0 :         if (TestTile(heightmap, heightmapStride, i, j, originPatch, dir, isct))
     337             :         {
     338           0 :             if (out)
     339           0 :                 *out = isct + patchPos;
     340           0 :             return true;
     341             :         }
     342             : 
     343             :         // Get the vertex between the two possible next cells
     344           0 :         float nx = (i + di) * (int)TERRAIN_TILE_SIZE;
     345           0 :         float nz = (j + dj) * (int)TERRAIN_TILE_SIZE;
     346             : 
     347             :         // Test which side of the ray the vertex is on, and advance into the
     348             :         // appropriate cell, using a test that works for all 4 combinations
     349             :         // of di,dj
     350           0 :         float dot = dir.Z * (nx - originPatch.X) - dir.X * (nz - originPatch.Z);
     351           0 :         if ((di == dj) == (dot > 0.0f))
     352           0 :             j += dj*2-1;
     353             :         else
     354           0 :             i += di*2-1;
     355             :     }
     356           0 :     while (i >= 0 && j >= 0 && i < PATCH_SIZE && j < PATCH_SIZE);
     357             : 
     358             :     // Ran off the edge of the patch, so no intersection
     359           0 :     return false;
     360           3 : }

Generated by: LCOV version 1.13