From 5b1a0f8516f9f7912e18860af8b8e46c29615b6a Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:43:12 +0200 Subject: [PATCH 1/6] fix(heightmap): Fix ray casting in BaseHeightMapRenderObjClass::Cast_Ray (2) --- .../W3DDevice/GameClient/BaseHeightMap.cpp | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp index 95f41b57f24..ef777222f08 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp @@ -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 //============================================================================= @@ -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 This function now creates its initial hit box +// with dimensions that fit into the ray cast 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; @@ -694,16 +699,24 @@ 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); + + Real rayMinHeight = std::min(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); + Real rayMaxHeight = std::max(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); + Real mapMinHeight = MAP_HEIGHT_SCALE * m_map->getMinHeightValue(); // Begin with min map height. + Real mapMaxHeight = MAP_HEIGHT_SCALE * m_map->getMaxHeightValue(); // Begin with max map height. + mapMinHeight = std::max(mapMinHeight, rayMinHeight + 1.0f); // But not lower than the ray end plus a margin. + mapMaxHeight = std::min(mapMaxHeight, rayMaxHeight - 1.0f); // But not higher than the ray start minus a margin. + + // The first hit box is very rough and is only meant to narrow the search. + Vector3 minPt(MAP_XY_FACTOR*(-overhang), MAP_XY_FACTOR*(-overhang), mapMinHeight); + Vector3 maxPt(MAP_XY_FACTOR*(m_map->getXExtent()+overhang), MAP_XY_FACTOR*(m_map->getYExtent()+overhang), mapMaxHeight); 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; @@ -716,8 +729,8 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) if (!result.StartBad) //check if start point inside terrain { 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 @@ -729,13 +742,18 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) if (!result.StartBad) //check if end point inside terrain { 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 @@ -755,6 +773,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(); @@ -773,7 +795,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; raytest.Result->ComputeContactPoint=true; //tell CollisionMath that we need point. @@ -788,7 +811,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 From b525921a1fdaec819d2365a28b7c93b960688937 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:20:14 +0200 Subject: [PATCH 2/6] Fix ray height margin --- .../Source/W3DDevice/GameClient/BaseHeightMap.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp index ef777222f08..193a8609259 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp @@ -700,12 +700,14 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) const Int borderSize = m_map->getBorderSizeInline(); const Int overhang = 2*VERTEX_BUFFER_TILE_LENGTH + borderSize; // Allow picking past the edge for scrolling & objects. - Real rayMinHeight = std::min(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); - Real rayMaxHeight = std::max(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); + const Real rayMinHeight = std::min(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); + const Real rayMaxHeight = std::max(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); + const Real rayHeightDelta = rayMaxHeight - rayMinHeight; + const Real rayHeightMargin = std::min(1.0f, rayHeightDelta * 0.49f); Real mapMinHeight = MAP_HEIGHT_SCALE * m_map->getMinHeightValue(); // Begin with min map height. Real mapMaxHeight = MAP_HEIGHT_SCALE * m_map->getMaxHeightValue(); // Begin with max map height. - mapMinHeight = std::max(mapMinHeight, rayMinHeight + 1.0f); // But not lower than the ray end plus a margin. - mapMaxHeight = std::min(mapMaxHeight, rayMaxHeight - 1.0f); // But not higher than the ray start minus a margin. + mapMinHeight = std::max(mapMinHeight, rayMinHeight + rayHeightMargin); // But not lower than the ray end plus a margin. + mapMaxHeight = std::min(mapMaxHeight, rayMaxHeight - rayHeightMargin); // But not higher than the ray start minus a margin. // The first hit box is very rough and is only meant to narrow the search. Vector3 minPt(MAP_XY_FACTOR*(-overhang), MAP_XY_FACTOR*(-overhang), mapMinHeight); From a9cc94b3786368c0bf734ce60ff3e941828748e0 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:21:14 +0200 Subject: [PATCH 3/6] Simplify implementation a bit --- .../W3DDevice/GameClient/BaseHeightMap.cpp | 24 +++++++------------ .../Libraries/Source/WWVegas/WWMath/castres.h | 2 +- .../Source/WWVegas/WWMath/colmathline.cpp | 8 +++++++ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp index 193a8609259..900bbebe769 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp @@ -700,19 +700,12 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) const Int borderSize = m_map->getBorderSizeInline(); const Int overhang = 2*VERTEX_BUFFER_TILE_LENGTH + borderSize; // Allow picking past the edge for scrolling & objects. - const Real rayMinHeight = std::min(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); - const Real rayMaxHeight = std::max(raytest.Ray.Get_P0().Z, raytest.Ray.Get_P1().Z); - const Real rayHeightDelta = rayMaxHeight - rayMinHeight; - const Real rayHeightMargin = std::min(1.0f, rayHeightDelta * 0.49f); - Real mapMinHeight = MAP_HEIGHT_SCALE * m_map->getMinHeightValue(); // Begin with min map height. - Real mapMaxHeight = MAP_HEIGHT_SCALE * m_map->getMaxHeightValue(); // Begin with max map height. - mapMinHeight = std::max(mapMinHeight, rayMinHeight + rayHeightMargin); // But not lower than the ray end plus a margin. - mapMaxHeight = std::min(mapMaxHeight, rayMaxHeight - rayHeightMargin); // But not higher than the ray start minus a margin. - // The first hit box is very rough and is only meant to narrow the search. - Vector3 minPt(MAP_XY_FACTOR*(-overhang), MAP_XY_FACTOR*(-overhang), mapMinHeight); - Vector3 maxPt(MAP_XY_FACTOR*(m_map->getXExtent()+overhang), MAP_XY_FACTOR*(m_map->getYExtent()+overhang), mapMaxHeight); - MinMaxAABoxClass mmbox(minPt, maxPt); + 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; @@ -727,8 +720,8 @@ 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; P0 = result.ContactPoint; //make intersection point the new start of the ray. @@ -741,7 +734,8 @@ 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; P1 = result.ContactPoint; //make intersection point the new end point of ray diff --git a/Core/Libraries/Source/WWVegas/WWMath/castres.h b/Core/Libraries/Source/WWVegas/WWMath/castres.h index 7b8330cf9a3..123611cfcf8 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/castres.h +++ b/Core/Libraries/Source/WWVegas/WWMath/castres.h @@ -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) diff --git a/Core/Libraries/Source/WWVegas/WWMath/colmathline.cpp b/Core/Libraries/Source/WWVegas/WWMath/colmathline.cpp index c55036b636d..75e110bd84c 100644 --- a/Core/Libraries/Source/WWVegas/WWMath/colmathline.cpp +++ b/Core/Libraries/Source/WWVegas/WWMath/colmathline.cpp @@ -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; } @@ -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; } From cd806a373be53f7abcb26d6bdea3cd6d943f1f11 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:04:59 +0200 Subject: [PATCH 4/6] Fix comment --- .../Source/W3DDevice/GameClient/BaseHeightMap.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp index 900bbebe769..3477ce05ba1 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp @@ -671,8 +671,8 @@ opposite corners of the map would check every polygon in the map). (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 This function now creates its initial hit box -// with dimensions that fit into the ray cast and no longer falls back to an +// 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) From 4dd7f430c1bb33a5c9e11aa8b4664996b37a34fb Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 18 Jul 2026 12:10:18 +0200 Subject: [PATCH 5/6] Prevent negative far Z in W3DView --- .../GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index 67256611aae..b0d3d970a4b 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -822,7 +822,10 @@ void W3DView::updateCameraClipPlanes(const Matrix3D &transform) } } - m_3DCamera->Set_Clip_Planes(NearZ, farZ); + if (farZ >= 0.0f) + { + m_3DCamera->Set_Clip_Planes(NearZ, farZ); + } } //------------------------------------------------------------------------------------------------- From ed26a4128cf7663c1663e1f73f533dbb648bb61b Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 18 Jul 2026 12:12:27 +0200 Subject: [PATCH 6/6] Allow ray casts to start inside all boxes --- .../W3DDevice/GameClient/BaseHeightMap.cpp | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp index 3477ce05ba1..e39de0b27ac 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp @@ -672,8 +672,8 @@ opposite corners of the map would check every polygon in the map). 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. +// hit boxes even if the ray starts inside of it and no longer falls back to an +// infinitely large search region if the initial boxes cannot be collided with. //============================================================================= bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) { @@ -700,7 +700,7 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) const Int borderSize = m_map->getBorderSizeInline(); const Int overhang = 2*VERTEX_BUFFER_TILE_LENGTH + borderSize; // Allow picking past the edge for scrolling & objects. - // The first hit box is very rough and is only meant to narrow the search. + // The initial hit boxes are very rough and are only meant to narrow the search before the triangle intersection. 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); @@ -720,13 +720,9 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) if (CollisionMath::Collide(lineseg,hbox,&result)) { - // 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; - P0 = result.ContactPoint; //make intersection point the new start of the ray. - ++P0HitCount; - } + newP0 = P0 != result.ContactPoint; + 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 result.Fraction=1.0f; //reset the result @@ -734,13 +730,9 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) lineseg2.Set(lineseg.Get_P1(),lineseg.Get_P0()); //reverse line segment if (CollisionMath::Collide(lineseg2,hbox,&result)) { - // 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; - P1 = result.ContactPoint; //make intersection point the new end point of ray - ++P1HitCount; - } + newP1 = P1 != result.ContactPoint; + P1 = result.ContactPoint; //make intersection point the new end point of ray + ++P1HitCount; } } @@ -791,10 +783,6 @@ bool BaseHeightMapRenderObjClass::Cast_Ray(RayCollisionTestClass & raytest) hbox.Init(mmbox); } - // Needs at least 2 hit counts for a proper terrain collision. - if (P0HitCount < 2 || P1HitCount < 2) - return false; - raytest.Result->ComputeContactPoint=true; //tell CollisionMath that we need point. // Adjust indexes into the bordered height map.