1 /* Copyright (C) 2021 Wildfire Games.
2 * This file is part of 0 A.D.
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.
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.
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/>.
19 * Determine intersection of rays with a heightfield.
22 #include "precompiled.h"
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"
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;
42 ///////////////////////////////////////////////////////////////////////////////
43 // CHFTracer constructor
44 CHFTracer::CHFTracer(CTerrain
*pTerrain
):
46 m_Heightfield(m_pTerrain
->GetHeightMap()),
47 m_MapSize(m_pTerrain
->GetVerticesPerSide()),
48 m_CellSize((float)TERRAIN_TILE_SIZE
),
49 m_HeightScale(HEIGHT_SCALE
)
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,
58 static bool RayTriIntersect(const CVector3D
& v0
, const CVector3D
& v1
, const CVector3D
& v2
,
59 const CVector3D
& origin
, const CVector3D
& dir
, float& dist
)
61 const float EPSILON
=0.00001f
;
63 // calculate edge vectors
64 CVector3D edge0
=v1
-v0
;
65 CVector3D edge1
=v2
-v0
;
67 // begin calculating determinant - also used to calculate U parameter
68 CVector3D pvec
=dir
.Cross(edge1
);
70 // if determinant is near zero, ray lies in plane of triangle
71 float det
= edge0
.Dot(pvec
);
72 if (fabs(det
)<EPSILON
)
75 float inv_det
= 1.0f
/det
;
77 // calculate vector from vert0 to ray origin
78 CVector3D tvec
=origin
-v0
;
80 // calculate U parameter, test bounds
81 float u
=tvec
.Dot(pvec
)*inv_det
;
82 if (u
<-0.01f
|| u
>1.01f
)
85 // prepare to test V parameter
86 CVector3D qvec
=tvec
.Cross(edge0
);
88 // calculate V parameter and test bounds
89 float v
=dir
.Dot(qvec
)*inv_det
;
90 if (v
<0.0f
|| u
+v
>1.0f
)
93 // calculate distance to intersection point from ray origin
94 float d
=edge1
.Dot(qvec
)*inv_det
;
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 bool CHFTracer::CellIntersect(int cx
, int cz
, const CVector3D
& origin
, const CVector3D
& dir
, float& dist
) const
110 // get vertices for this cell
112 m_pTerrain
->CalcPosition(cx
,cz
,vpos
[0]);
113 m_pTerrain
->CalcPosition(cx
+1,cz
,vpos
[1]);
114 m_pTerrain
->CalcPosition(cx
+1,cz
+1,vpos
[2]);
115 m_pTerrain
->CalcPosition(cx
,cz
+1,vpos
[3]);
118 if (RayTriIntersect(vpos
[0],vpos
[1],vpos
[2],origin
,dir
,dist
)) {
122 if (RayTriIntersect(vpos
[0],vpos
[2],vpos
[3],origin
,dir
,dist
)) {
129 ///////////////////////////////////////////////////////////////////////////////
130 // RayIntersect: intersect ray with this heightfield; return true if
131 // intersection occurs (and fill in grid coordinates of intersection), or false
133 bool CHFTracer::RayIntersect(const CVector3D
& origin
, const CVector3D
& dir
, int& x
, int& z
, CVector3D
& ipt
) const
135 // If the map is empty (which should never happen),
136 // return early before we crash when reading zero-sized heightmaps
139 debug_warn(L
"CHFTracer::RayIntersect called with zero-size map");
143 // intersect first against bounding box
144 CBoundingBoxAligned bound
;
145 bound
[0] = CVector3D(-MARGIN_SIZE
* m_CellSize
, 0, -MARGIN_SIZE
* m_CellSize
);
146 bound
[1] = CVector3D((m_MapSize
+ MARGIN_SIZE
) * m_CellSize
, 65535 * m_HeightScale
, (m_MapSize
+ MARGIN_SIZE
) * m_CellSize
);
149 if (!bound
.RayIntersect(origin
,dir
,tmin
,tmax
)) {
150 // ray missed world bounds; no intersection
154 // project origin onto grid, if necessary, to get starting point for traversal
155 CVector3D traversalPt
;
157 traversalPt
=origin
+dir
*tmin
;
162 // setup traversal variables
163 int sx
=dir
.X
<0 ? -1 : 1;
164 int sz
=dir
.Z
<0 ? -1 : 1;
166 float invCellSize
=1.0f
/float(m_CellSize
);
168 float fcx
=traversalPt
.X
*invCellSize
;
169 int cx
=(int)floor(fcx
);
171 float fcz
=traversalPt
.Z
*invCellSize
;
172 int cz
=(int)floor(fcz
);
174 float invdx
= 1.0e20f
;
175 float invdz
= 1.0e20f
;
177 if (fabs(dir
.X
) > 1.0e-20)
178 invdx
= float(1.0/fabs(dir
.X
));
179 if (fabs(dir
.Z
) > 1.0e-20)
180 invdz
= float(1.0/fabs(dir
.Z
));
184 if (cx
>= -MARGIN_SIZE
&& cx
< int(m_MapSize
+ MARGIN_SIZE
- 1) && cz
>= -MARGIN_SIZE
&& cz
< int(m_MapSize
+ MARGIN_SIZE
- 1))
188 if (CellIntersect(cx
,cz
,origin
,dir
,dist
)) {
197 // Degenerate case: y close to zero
198 // catch travelling off the map
199 if ((cx
< -MARGIN_SIZE
) && (sx
< 0))
201 if ((cx
>= (int)(m_MapSize
+ MARGIN_SIZE
- 1)) && (sx
> 0))
203 if ((cz
< -MARGIN_SIZE
) && (sz
< 0))
205 if ((cz
>= (int)(m_MapSize
+ MARGIN_SIZE
- 1)) && (sz
> 0))
209 // get coords of current cell
210 fcx
=traversalPt
.X
*invCellSize
;
211 fcz
=traversalPt
.Z
*invCellSize
;
213 // get distance to next cell in x,z
214 float dx
=(sx
==-1) ? fcx
-float(cx
) : 1-(fcx
-float(cx
));
216 float dz
=(sz
==-1) ? fcz
-float(cz
) : 1-(fcz
-float(cz
));
229 traversalPt
+=dir
*dist
;
230 } while (traversalPt
.Y
>=0);
232 // fell off end of heightmap with no intersection; return a miss
236 static bool TestTile(u16
* heightmap
, int stride
, int i
, int j
, const CVector3D
& pos
, const CVector3D
& dir
, CVector3D
& isct
)
238 u16 y00
= heightmap
[i
+ j
*stride
];
239 u16 y10
= heightmap
[i
+1 + j
*stride
];
240 u16 y01
= heightmap
[i
+ (j
+1)*stride
];
241 u16 y11
= heightmap
[i
+1 + (j
+1)*stride
];
243 CVector3D
p00( i
* TERRAIN_TILE_SIZE
, y00
* HEIGHT_SCALE
, j
* TERRAIN_TILE_SIZE
);
244 CVector3D
p10((i
+1) * TERRAIN_TILE_SIZE
, y10
* HEIGHT_SCALE
, j
* TERRAIN_TILE_SIZE
);
245 CVector3D
p01( i
* TERRAIN_TILE_SIZE
, y01
* HEIGHT_SCALE
, (j
+1) * TERRAIN_TILE_SIZE
);
246 CVector3D
p11((i
+1) * TERRAIN_TILE_SIZE
, y11
* HEIGHT_SCALE
, (j
+1) * TERRAIN_TILE_SIZE
);
250 int triDir
= (mid1
< mid2
);
252 float dist
= FLT_MAX
;
256 if (RayTriIntersect(p00
, p10
, p01
, pos
, dir
, dist
) || // lower-left triangle
257 RayTriIntersect(p11
, p01
, p10
, pos
, dir
, dist
)) // upper-right triangle
259 isct
= pos
+ dir
* dist
;
265 if (RayTriIntersect(p00
, p11
, p01
, pos
, dir
, dist
) || // upper-left triangle
266 RayTriIntersect(p00
, p10
, p11
, pos
, dir
, dist
)) // lower-right triangle
268 isct
= pos
+ dir
* dist
;
275 bool CHFTracer::PatchRayIntersect(CPatch
* patch
, const CVector3D
& origin
, const CVector3D
& dir
, CVector3D
* out
)
277 // (TODO: This largely duplicates RayIntersect - some refactoring might be
278 // nice in the future.)
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.
287 // Start by jumping to the point where the ray enters the bounding box
288 CBoundingBoxAligned bound
= patch
->GetWorldBounds();
290 if (!bound
.RayIntersect(origin
, dir
, tmin
, tmax
))
292 // Ray missed patch; no intersection
296 int heightmapStride
= patch
->m_Parent
->GetVerticesPerSide();
298 // Get heightmap, offset to start at this patch
299 u16
* heightmap
= patch
->m_Parent
->GetHeightMap() +
300 patch
->m_X
* PATCH_SIZE
+
301 patch
->m_Z
* PATCH_SIZE
* heightmapStride
;
303 // Get patch-space position of ray origin and bbox entry point
305 patch
->m_X
* PATCH_SIZE
* TERRAIN_TILE_SIZE
,
307 patch
->m_Z
* PATCH_SIZE
* TERRAIN_TILE_SIZE
);
308 CVector3D originPatch
= origin
- patchPos
;
309 CVector3D entryPatch
= originPatch
+ dir
* tmin
;
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.
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.
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).)
325 // Work out which tile we're starting in
326 int i
= Clamp
<int>(entryPatch
.X
/ TERRAIN_TILE_SIZE
, 0, PATCH_SIZE
- 1);
327 int j
= Clamp
<int>(entryPatch
.Z
/ TERRAIN_TILE_SIZE
, 0, PATCH_SIZE
- 1);
329 // Work out which direction the ray is going in
330 int di
= (dir
.X
>= 0 ? 1 : 0);
331 int dj
= (dir
.Z
>= 0 ? 1 : 0);
336 if (TestTile(heightmap
, heightmapStride
, i
, j
, originPatch
, dir
, isct
))
339 *out
= isct
+ patchPos
;
343 // Get the vertex between the two possible next cells
344 float nx
= (i
+ di
) * (int)TERRAIN_TILE_SIZE
;
345 float nz
= (j
+ dj
) * (int)TERRAIN_TILE_SIZE
;
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
350 float dot
= dir
.Z
* (nx
- originPatch
.X
) - dir
.X
* (nz
- originPatch
.Z
);
351 if ((di
== dj
) == (dot
> 0.0f
))
356 while (i
>= 0 && j
>= 0 && i
< PATCH_SIZE
&& j
< PATCH_SIZE
);
358 // Ran off the edge of the patch, so no intersection