Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 41 additions & 23 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -656,10 +656,6 @@ void BaseHeightMapRenderObjClass::reset()
}
}

/**@todo: Ray intersection needs to be optimized with some sort of grid-tracing
(ala line drawing). We should also try making the search in a front->back order
relative to the ray so we can early exit as soon as we have a hit.
*
//=============================================================================
// BaseHeightMapRenderObjClass::Cast_Ray
//=============================================================================
Expand All @@ -671,18 +667,27 @@ map plane so this is very quick (small bounding box). But it can become slow
for arbitrary rays such as those used in AI visibility checks(2 units on
opposite corners of the map would check every polygon in the map).
*/
/** @todo: Ray intersection needs to be optimized with some sort of grid-tracing
(ala line drawing). We should also try making the search in a front->back order
relative to the ray so we can early exit as soon as we have a hit.
*/
// TheSuperHackers @fix The ray cast can now correctly collide with the initial
// hit box even if the ray starts inside of it and no longer falls back to an
// infinitely large search region if the initial box cannot be collided with.
//=============================================================================
bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest)
{
if (!m_map)
return false; //need valid pointer to heightmap samples

TriClass tri;
Bool hit = false;
Int X,Y;
Vector3 normal,P0,P1,P2,P3;
Bool hasP0 = false;
Bool hasP1 = false;

if (!m_map)
return false; //need valid pointer to heightmap samples
P0.Set(FLT_MAX, FLT_MAX, FLT_MAX); // Set initial bogus value
P1.Set(FLT_MAX, FLT_MAX, FLT_MAX); // Set initial bogus value
Int P0HitCount = 0;
Int P1HitCount = 0;

//Clip ray to extents of height map
AABoxClass hbox;
Expand All @@ -694,16 +699,19 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest)
Int endCellY = 0;
const Int borderSize = m_map->getBorderSizeInline();
const Int overhang = 2*VERTEX_BUFFER_TILE_LENGTH + borderSize; // Allow picking past the edge for scrolling & objects.
Vector3 minPt(MAP_XY_FACTOR*(-overhang), MAP_XY_FACTOR*(-overhang), -MAP_XY_FACTOR);
Vector3 maxPt(MAP_XY_FACTOR*(m_map->getXExtent()+overhang),
MAP_XY_FACTOR*(m_map->getYExtent()+overhang), MAP_HEIGHT_SCALE*m_map->getMaxHeightValue()+MAP_XY_FACTOR);
MinMaxAABoxClass mmbox(minPt, maxPt);

// The first hit box is very rough and is only meant to narrow the search.
const Real mapMinHeight = MAP_HEIGHT_SCALE * m_map->getMinHeightValue();
const Real mapMaxHeight = MAP_HEIGHT_SCALE * m_map->getMaxHeightValue();
const Vector3 minPt(MAP_XY_FACTOR*(-overhang), MAP_XY_FACTOR*(-overhang), mapMinHeight);
const Vector3 maxPt(MAP_XY_FACTOR*(m_map->getXExtent()+overhang), MAP_XY_FACTOR*(m_map->getYExtent()+overhang), mapMaxHeight);
const MinMaxAABoxClass mmbox(minPt, maxPt);
hbox.Init(mmbox);

lineseg=raytest.Ray;

Int p;
for (p=0; p<3; p++) {
Int terrainIntersectionIteration = 0;
for (; ; ++terrainIntersectionIteration) {
//find intersection point of ray and terrain bounding box
result.Reset();
result.ComputeContactPoint=true;
Expand All @@ -712,12 +720,12 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest)

if (CollisionMath::Collide(lineseg,hbox,&result))
{
//ray intersects terrain or starts inside the terrain.
if (!result.StartBad) //check if start point inside terrain
// Go ahead if point is not starting inside terrain or it is the first rough box collision.
if (!result.StartBad || terrainIntersectionIteration == 0)
{
newP0 = P0 != result.ContactPoint;
hasP0 = true;
P0 = result.ContactPoint; //make intersection point the new start of the ray.
++P0HitCount;
}

//reverse direction of original ray and clip again to extent of heightmap
Expand All @@ -726,16 +734,22 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest)
lineseg2.Set(lineseg.Get_P1(),lineseg.Get_P0()); //reverse line segment
if (CollisionMath::Collide(lineseg2,hbox,&result))
{
if (!result.StartBad) //check if end point inside terrain
// Go ahead if point is not starting inside terrain or it is the first rough box collision.
if (!result.StartBad || terrainIntersectionIteration == 0)
{
newP1 = P1 != result.ContactPoint;
hasP1 = true;
P1 = result.ContactPoint; //make intersection point the new end point of ray
++P1HitCount;
}
}
}

if (!newP0 || !newP1)
// Has not even hit the first hit box?
if (P0HitCount == 0 || P1HitCount == 0)
return false;

// Has no new hit points?
if (!newP0 && !newP1)
break;

// Take the 2D bounding box of ray and check heights
Expand All @@ -755,6 +769,10 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest)
endCellY = REAL_TO_INT_CEIL(P1.Y/MAP_XY_FACTOR);
}

// Stop narrowing after the third iteration
if (terrainIntersectionIteration == 2)
break;

Int i, j, minHt, maxHt;

minHt = m_map->getMaxHeightValue();
Expand All @@ -773,7 +791,8 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest)
hbox.Init(mmbox);
}

if (!hasP0 || !hasP1)
// Needs at least 2 hit counts for a proper terrain collision.
if (P0HitCount < 2 || P1HitCount < 2)
return false;
Comment on lines +795 to 796

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Inside endpoint is rejected

When a close camera ray starts inside the narrowed terrain box, the first iteration records that endpoint, but later StartBad results intentionally do not increment its counter. The other endpoint can clip normally, yet this check returns false when the inside endpoint remains at one hit. That skips triangle testing and leaves close-range terrain picking unable to find valid terrain intersections.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp
Line: 795-796

Comment:
**Inside endpoint is rejected**

When a close camera ray starts inside the narrowed terrain box, the first iteration records that endpoint, but later `StartBad` results intentionally do not increment its counter. The other endpoint can clip normally, yet this check returns `false` when the inside endpoint remains at one hit. That skips triangle testing and leaves close-range terrain picking unable to find valid terrain intersections.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@greptileai I tested camera with sub 1 height above terrain and it worked correctly. I do not understand the observation. Please elaborate and explain in more detail.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — let me trace through the specific scenario more carefully.

When the issue I described applies:

On iteration 0, the initial box's Z max is mapMaxHeight (the global map maximum). If the camera is below that, StartBad = true for the forward ray. The new code allows this on iteration 0, so P0HitCount reaches 1.

On iteration 1, the narrowed box is built from the actual terrain heights of the specific cells between P0 and P1. If the camera is 1 unit above the terrain directly below it, and the terrain in those cells is at height H, then the narrowed box's Z max is approximately H. The camera at H+1 is above the narrowed box → StartBad = false → P0HitCount reaches 2. This is why your test works.

The edge case where it could fail:

If the camera is in a depression (low terrain below it) while adjacent cells have significantly higher terrain, the narrowed box's Z max could be set by those taller neighbors — placing the narrowed box's ceiling above the camera again. In that case StartBad = true on iteration 1, P0HitCount stays at 1, and the P0HitCount < 2 guard returns false.

So the failure would require: camera low above terrain in a valley, with tall terrain in nearby cells driving the narrowed box ceiling higher than the camera. Your flat-terrain test wouldn't hit this.

That said, it's genuinely a narrow edge case. If this scenario isn't realistic in Generals maps, the concern is largely academic.


raytest.Result->ComputeContactPoint=true; //tell CollisionMath that we need point.
Expand All @@ -788,7 +807,6 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest)
Int offset;
for (offset = 1; offset < 5; offset *= 3) {
for (Y=startCellY-offset; Y<=endCellY+offset; Y++) {

for (X=startCellX-offset; X<=endCellX+offset; X++) {
//test the 2 triangles in this cell
// 3-----2
Expand Down
2 changes: 1 addition & 1 deletion Core/Libraries/Source/WWVegas/WWMath/castres.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct CastResultStruct
CastResultStruct() { Reset(); }
void Reset() { StartBad = false; Fraction = 1.0f; Normal.Set(0,0,0); SurfaceType = 0; ComputeContactPoint = false; ContactPoint.Set(0,0,0); }

bool StartBad; // was the initial configuration interpenetrating something?
bool StartBad; // was the initial configuration interpenetrating something? true if ray starts inside the collided geometry
float Fraction; // fraction of the move up until collision
Vector3 Normal; // surface normal at the collision point
uint32 SurfaceType; // surface type of polygon at collision point (see W3D_SURFACE_TYPES in w3d_file.h)
Expand Down
8 changes: 8 additions & 0 deletions Core/Libraries/Source/WWVegas/WWMath/colmathline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ bool CollisionMath::Collide(const LineSegClass & line,const AABoxClass & box,Cas
// if ray starts inside the box, note that fact and bail.
if (test.Inside) {
res->StartBad = true;
// TheSuperHackers @tweak The contact point will be where the line has started.
if (res->ComputeContactPoint) {
res->ContactPoint = line.Get_P0();
}
return true;
}

Expand Down Expand Up @@ -328,6 +332,10 @@ bool CollisionMath::Collide(const LineSegClass & line,const OBBoxClass & box,Cas
// if ray starts inside the box, don't collide
if (test.Inside) {
result->StartBad = true;
// TheSuperHackers @tweak The contact point will be where the line has started.
if (result->ComputeContactPoint) {
result->ContactPoint = line.Get_P0();
}
return true;
}

Expand Down
Loading