Skip to content

bugfix(view): Fix view ray cast behavior for mouse picks and drawable occlusion#2818

Merged
xezon merged 14 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-view-ray-casts
Jun 26, 2026
Merged

bugfix(view): Fix view ray cast behavior for mouse picks and drawable occlusion#2818
xezon merged 14 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-view-ray-casts

Conversation

@xezon

@xezon xezon commented Jun 21, 2026

Copy link
Copy Markdown

This change fixes view ray casting behavior for mouse picks and drawable occlusion. The behavior for the radar view box is still not working correctly at low camera pitch but is otherwise still ok for regular top-down camera views. I do not yet know how to deal with the view box at low camera pitch.

The view ray now has a very long range making sure that the camera far clip plane is not limiting mouse picks. Also, view rays into the sky will no longer send units to 0,0,0: Invalid ray picks are now treated as no input.

The drawable updates made in the area created with getAxisAlignedViewRegion are now working with low camera pitch. The function falls back to the terrain draw area if the view frustum does not fully intersect with the Z plane.

Known issues

The Radar View Box still looks bad at low camera pitch as did always.

TODO

  • Replicate in Generals

@xezon xezon added Bug Something is not working right, typically is user facing Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour ThisProject The issue was introduced by this project, or this task is specific to this project Critical Severity: Minor < Major < Critical < Blocker and removed Minor Severity: Minor < Major < Critical < Blocker labels Jun 21, 2026
@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes view ray-casting behavior for mouse picks and drawable occlusion. The core fix makes screenToTerrain and screenToWorldAtZ return intersection status rather than silently writing garbage coordinates, and updates all callers to guard on the result.

  • PlaneClass::Compute_Intersection is promoted from bool to a richer IntersectionResType enum (NO_INTERSECTION / INSIDE_SEGMENT / OUTSIDE_LINE), and all callers have been audited and updated.
  • getPickRay now scales the ray by sqr(depth) instead of depth to produce a very long ray that bypasses the far-clip-plane limit at low camera pitch, and getAxisAlignedViewRegion falls back to the drawn terrain area when screen corners don't fully intersect the Z plane.
  • All input-command translators (CommandXlat, GUICommandTranslator, PlaceEventTranslator, SelectionXlat) and UI handlers (InGameUI, W3DDisplay) now early-out gracefully when the cursor is over sky instead of dispatching commands with coordinates at world-origin (0, 0, 0).

Confidence Score: 5/5

Safe to merge — the change correctly propagates intersection-failure status through all callers without introducing new garbage-coordinate dispatches.

The Compute_Intersection API change from bool to IntersectionResType is backward-compatible: every existing caller in the codebase either discards the return value (polygon-clipping, shatter, decal, bridge) or has been explicitly updated. The new screenToTerrain / screenToWorldAtZ return-value discipline is applied consistently across all 15+ call sites. The sqr(Get_Depth()) ray extension is intentional and safe within float precision. No data loss or command mis-dispatch paths were found.

No files require special attention. The W3DView.cpp and View.cpp changes are the most complex but follow a clear and consistent pattern.

Important Files Changed

Filename Overview
Core/Libraries/Source/WWVegas/WWMath/plane.h Promotes Compute_Intersection return type from bool to new IntersectionResType enum distinguishing NO_INTERSECTION, INSIDE_SEGMENT, and OUTSIDE_LINE; all existing callers in the codebase either discard the return value or have been updated, so no behavioral regression.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp Key fixes: screenToTerrain now returns Bool and only writes to world when a terrain or bridge intersection exists; screenToWorldAtZ now uses PlaneClass::Compute_Intersection and returns IntersectionResType; getPickRay and the lookAt ray scale by sqr(Get_Depth()) for a very long ray; getAxisAlignedViewRegion falls back to the drawn terrain region when screen corners do not fully intersect the Z=0 plane.
Core/GameEngine/Source/GameClient/View.cpp getScreenCornerWorldPointsAtZ now accepts a ViewportClass parameter (defaults to full-screen), returns IntersectionResType reflecting the worst-case intersection result across all four corners, and correctly propagates OUTSIDE_LINE vs NO_INTERSECTION.
Core/GameEngineDevice/Source/W3DDevice/Common/System/W3DRadar.cpp reconstructViewBox resets m_reconstructViewBox at the function start (preventing every-frame retry storms when the camera looks at sky) and returns early on NO_INTERSECTION; drawViewBox likewise guards its screenToWorldAtZ call; local variables renamed from start/end to pixelStart/pixelEnd for clarity.
Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp All screenToTerrain call sites now guard with if(!...) break before dispatching guard/move commands; isPoint is now computed after the terrain check to avoid the unused calculation on early exit.
Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp Radius cursor position, double-click attack-move hint, and build-placement icons are all now guarded on successful terrain intersection; when the cursor is over sky, icons freeze at their last valid position rather than jumping to the world origin.
Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp Adds getDrawRegion2D() which consolidates the repeated inline heightmap-to-world-space region calculation into a single reusable method, used by both updateCameraClipPlanes and getAxisAlignedViewRegion.
Core/Libraries/Include/Lib/BaseType.h Adds Region3D::setXY(const Region2D&) and renames setFromPointsNoZ to setXYFromPoints with updated doc-comments; callers are fully updated and Z is always explicitly assigned after these calls.
Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp One-line update in getMaximumVisibleBox to explicitly compare Compute_Intersection against INSIDE_SEGMENT rather than relying on implicit bool conversion; semantically identical to the old code since the old function returned false for OUTSIDE_LINE.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp Adds centerPos.zero() before calling screenToTerrain so that when the cursor is over sky and the call returns false, the mask texture shader projects from world-origin rather than from an uninitialized value.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User as Mouse Input
    participant Translator as Command Translator
    participant View as W3DView
    participant Plane as PlaneClass
    participant Terrain as TerrainRenderObj

    User->>Translator: Mouse click / move event
    Translator->>View: screenToTerrain(screen, world)
    View->>View: getPickRay() → rayStart, rayEnd (scaled by sqr(depth))
    View->>Terrain: Cast_Ray(raytest)
    alt Terrain hit
        Terrain-->>View: ContactPoint
        View-->>Translator: true + world coords
        Translator->>Translator: Dispatch game command
    else No terrain hit (sky)
        Terrain-->>View: no hit
        View-->>Translator: false
        Translator->>Translator: break / COMMAND_COMPLETE (no command sent)
    end

    User->>View: getAxisAlignedViewRegion(region)
    View->>View: "getScreenCornerWorldPointsAtZ(corners, z=0)"
    View->>Plane: Compute_Intersection(rayStart, rayEnd, t)
    alt All 4 corners: INSIDE_SEGMENT
        Plane-->>View: INSIDE_SEGMENT
        View->>View: setXYFromPoints(corners)
    else Any corner: OUTSIDE_LINE or NO_INTERSECTION
        Plane-->>View: OUTSIDE_LINE / NO_INTERSECTION
        View->>View: fallback to heightMap.getDrawRegion2D()
    end
    View-->>View: set Z extents from mapExtent +/- safeValue
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User as Mouse Input
    participant Translator as Command Translator
    participant View as W3DView
    participant Plane as PlaneClass
    participant Terrain as TerrainRenderObj

    User->>Translator: Mouse click / move event
    Translator->>View: screenToTerrain(screen, world)
    View->>View: getPickRay() → rayStart, rayEnd (scaled by sqr(depth))
    View->>Terrain: Cast_Ray(raytest)
    alt Terrain hit
        Terrain-->>View: ContactPoint
        View-->>Translator: true + world coords
        Translator->>Translator: Dispatch game command
    else No terrain hit (sky)
        Terrain-->>View: no hit
        View-->>Translator: false
        Translator->>Translator: break / COMMAND_COMPLETE (no command sent)
    end

    User->>View: getAxisAlignedViewRegion(region)
    View->>View: "getScreenCornerWorldPointsAtZ(corners, z=0)"
    View->>Plane: Compute_Intersection(rayStart, rayEnd, t)
    alt All 4 corners: INSIDE_SEGMENT
        Plane-->>View: INSIDE_SEGMENT
        View->>View: setXYFromPoints(corners)
    else Any corner: OUTSIDE_LINE or NO_INTERSECTION
        Plane-->>View: OUTSIDE_LINE / NO_INTERSECTION
        View->>View: fallback to heightMap.getDrawRegion2D()
    end
    View-->>View: set Z extents from mapExtent +/- safeValue
Loading

Reviews (5): Last reviewed commit: "Replicate in Generals" | Re-trigger Greptile

Comment thread Core/Libraries/Include/Lib/BaseType.h Outdated
@xezon xezon force-pushed the xezon/fix-view-ray-casts branch from 7c85fda to 0db89be Compare June 21, 2026 15:55
Comment thread Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
Comment thread Core/GameEngine/Source/GameClient/MessageStream/GUICommandTranslator.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/MessageStream/PlaceEventTranslator.cpp Outdated
}
if( individualResults[i] == PlaneClass::OUTSIDE_LINE )
{
combinedResult = PlaneClass::OUTSIDE_LINE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should break here?

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.

No because NO_INTERSECTION will take precedence over OUTSIDE_LINE

@stephanmeesters stephanmeesters left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks correct to me, also did a test using the map Arabia v2 and the issue is fixed as expected

@xezon xezon force-pushed the xezon/fix-view-ray-casts branch from 2d92831 to f824bb7 Compare June 26, 2026 17:06
@xezon

xezon commented Jun 26, 2026

Copy link
Copy Markdown
Author

Replicated to Generals with one conflict in InGameUI.cpp

D:\Projects\TheSuperHackers\GeneralsGameCode>FOR /F "delims=" %b IN ('git merge-base --fork-point main') DO git diff %b  1>changes.patch

D:\Projects\TheSuperHackers\GeneralsGameCode>git diff a7447e558036e281ca769a7d70abe90f720ec6d3  1>changes.patch

D:\Projects\TheSuperHackers\GeneralsGameCode>git apply -p2 --directory=Generals --reject --whitespace=fix changes.patch
changes.patch:474: space before tab in indent.
        ulRadar.x = ulWorld.x / (m_mapExtent.width() / RADAR_CELL_WIDTH);
Checking patch Generals/GameEngine/Include/GameClient/View.h...
error: Generals/GameEngine/Include/GameClient/View.h: No such file or directory
Checking patch Generals/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp...
error: Generals/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp: No such file or directory
Checking patch Generals/GameEngine/Source/GameClient/MessageStream/GUICommandTranslator.cpp...
error: Generals/GameEngine/Source/GameClient/MessageStream/GUICommandTranslator.cpp: No such file or directory
Checking patch Generals/GameEngine/Source/GameClient/MessageStream/PlaceEventTranslator.cpp...
error: Generals/GameEngine/Source/GameClient/MessageStream/PlaceEventTranslator.cpp: No such file or directory
Checking patch Generals/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp...
error: Generals/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp: No such file or directory
Checking patch Generals/GameEngine/Source/GameClient/View.cpp...
error: Generals/GameEngine/Source/GameClient/View.cpp: No such file or directory
Checking patch Generals/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h...
error: Generals/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h: No such file or directory
Checking patch Generals/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h...
error: Generals/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h: No such file or directory
Checking patch Generals/GameEngineDevice/Source/W3DDevice/Common/System/W3DRadar.cpp...
error: Generals/GameEngineDevice/Source/W3DDevice/Common/System/W3DRadar.cpp: No such file or directory
Checking patch Generals/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp...
error: Generals/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp: No such file or directory
Checking patch Generals/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp...
error: Generals/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp: No such file or directory
Checking patch Generals/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp...
error: Generals/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp: No such file or directory
Checking patch Generals/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWaterTracks.cpp...
error: Generals/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWaterTracks.cpp: No such file or directory
Checking patch Generals/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp...
error: Generals/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp: No such file or directory
Checking patch Generals/Libraries/Include/Lib/BaseType.h...
error: Generals/Libraries/Include/Lib/BaseType.h: No such file or directory
Checking patch Generals/Libraries/Source/WWVegas/WWMath/plane.h...
error: Generals/Libraries/Source/WWVegas/WWMath/plane.h: No such file or directory
Checking patch Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp...
Hunk #1 succeeded at 1479 (offset -33 lines).
Hunk #2 succeeded at 1487 (offset -33 lines).
Hunk #3 succeeded at 1520 (offset -33 lines).
Hunk #4 succeeded at 1617 (offset -33 lines).
error: while searching for:
                // set the location and angle of the place icon
                /**@todo this whole orientation vector thing is LAME! Must replace, all I want to
                to do is set a simple angle and have it automatically change, ug! */
                TheTacticalView->screenToTerrain( &loc, &world );
                m_placeIcon[ 0 ]->setPosition( &world );
                m_placeIcon[ 0 ]->setOrientation( angle );


                //
                // check to see if this is a legal location to build something at and tint or "un-tint"
                // the cursor icons as appropriate.  This involves a pathfind which could be
                // expensive so we don't want to do it on every frame (although that would be ideal)
                // If we discover there are cases that this is just too slow we should increase the
                // delay time between checks or we need to come up with a way of recording what is
                // valid and what isn't or "fudge" the results to feel "ok"
                //
                if( TheGameClient->getFrame() & 0x1 )
                {
                        TheTerrainVisual->removeAllBibs();

                        Object *builderObject = TheGameLogic->findObjectByID( getPendingPlaceSourceObjectID() );

                        LegalBuildCode lbc;
                        lbc = TheBuildAssistant->isLocationLegalToBuild( &world,
                                                                                                                        m_pendingPlaceType,
                                                                                                                        angle,
                                                                                                                        BuildAssistant::USE_QUICK_PATHFIND |
                                                                                                                        BuildAssistant::TERRAIN_RESTRICTIONS |
                                                                                                                        BuildAssistant::CLEAR_PATH |
                                                                                                                        BuildAssistant::NO_OBJECT_OVERLAP |
                                                                                                                        BuildAssistant::SHROUD_REVEALED |
                                                                                                                        BuildAssistant::IGNORE_STEALTHED,
                                                                                                                        builderObject,
                                                                                                                        nullptr );

                        if( lbc != LBC_OK )
                                m_placeIcon[ 0 ]->colorTint( &IllegalBuildColor );
                        else
                                m_placeIcon[ 0 ]->colorTint( nullptr );




                        // Add the bibs around the structure.
                        if (lbc != LBC_OK)
                        {
                                TheTerrainVisual->addFactionBibDrawable(m_placeIcon[0], lbc != LBC_OK);
                        } else {
                                TheTerrainVisual->removeFactionBibDrawable(m_placeIcon[0]);
                        }
                }



                //
                // we have additional place icons when we're placing down a line of walls or other
                // similarly placed object ... for those we will have them be oriented the same way

error: patch failed: Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp:1668
Hunk #6 succeeded at 1705 (offset -30 lines).
Checking patch Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp...
Hunk #1 succeeded at 1402 (offset -51 lines).
Checking patch Generals/Code/Tools/WorldBuilder/src/wbview3d.cpp...
Hunk #1 succeeded at 174 (offset -1 lines).
Applying patch Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp with 1 reject...
Hunk #1 applied cleanly.
Hunk #2 applied cleanly.
Hunk #3 applied cleanly.
Hunk #4 applied cleanly.
Rejected hunk #5.
Hunk #6 applied cleanly.
Applied patch Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp cleanly.
Applied patch Generals/Code/Tools/WorldBuilder/src/wbview3d.cpp cleanly.

@xezon xezon merged commit 21cc640 into TheSuperHackers:main Jun 26, 2026
17 checks passed
@xezon xezon deleted the xezon/fix-view-ray-casts branch June 26, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Critical Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ThisProject The issue was introduced by this project, or this task is specific to this project ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recent camera change changed behavior for move order to off map position

2 participants