Skip to content

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

Open
xezon wants to merge 13 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-view-ray-casts
Open

bugfix(view): Fix view ray cast behavior for mouse picks and drawable occlusion#2818
xezon wants to merge 13 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 so that mouse-picks into sky/void no longer implicitly use (0,0,0) as the world position. screenToTerrain and screenToWorldAtZ now return success/type indicators, and every call site has been updated to bail out gracefully on failure.

  • Core API changePlaneClass::Compute_Intersection signature changed from bool to a three-value IntersectionResType enum (NO_INTERSECTION, INSIDE_SEGMENT, OUTSIDE_LINE). Pick-ray length is now sqr(Get_Depth()) to ensure the ray always reaches terrain at low camera pitch. All existing unmodified callers of Compute_Intersection ignore the return value, so there is no implicit bool-conversion regression.
  • getAxisAlignedViewRegion fallback – when screen corners don't project cleanly onto the Z=0 plane (low pitch), the function falls back to the heightmap draw region rather than constructing a bogus bounding box.
  • Caller hardening – all screenToTerrain / screenToWorldAtZ call sites in CommandXlat, GUICommandTranslator, PlaceEventTranslator, SelectionXlat, InGameUI, W3DDisplay, and W3DWaterTracks now check the return value before issuing game messages or moving cursors.

Confidence Score: 5/5

Safe to merge; the change is a targeted, well-scoped fix with no regressions found.

All 19 changed files correctly propagate the new return values, and every call site has been updated to handle the failure path. The one broader API change — Compute_Intersection returning an enum instead of bool — was verified to have no impact on any unmodified caller in the repo because none of them inspect the return value. The known radar-view-box limitation at low pitch is explicitly acknowledged and pre-existing.

No files require special attention.

Important Files Changed

Filename Overview
Core/Libraries/Source/WWVegas/WWMath/plane.h Compute_Intersection return type changed from bool to IntersectionResType enum; enum values 0/1/2 preserve truthiness for old bool-style callers except OUTSIDE_LINE(2) is now truthy where the old code returned false — confirmed safe because no unmodified caller in the repo checks the return value.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp screenToTerrain now returns Bool, screenToWorldAtZ returns IntersectionResType; pick-ray extended to sqr(Get_Depth()) to avoid far-clip truncation at low pitch; getAxisAlignedViewRegion falls back to draw region or map extent when view corners don't cleanly project onto Z=0.
Core/GameEngine/Source/GameClient/View.cpp getScreenCornerWorldPointsAtZ returns worst-case IntersectionResType across all 4 corners; supports optional ViewportClass parameter for sub-viewport sampling; logic correctly distinguishes NO_INTERSECTION from OUTSIDE_LINE.
Core/GameEngineDevice/Source/W3DDevice/Common/System/W3DRadar.cpp m_reconstructViewBox cleared at function entry (before early-return) so stale data persists on failure rather than triggering busy retries; variable renames start/end improve clarity; drawViewBox returns early on NO_INTERSECTION to avoid drawing an undefined box.
GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp handleRadiusCursor, handleBuildPlacements, and triggerDoubleClickAttackMoveGuardHint all guarded against invalid terrain picks; placement icon and legality tint are only updated when a valid world position is available.
Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp MSG_DOUBLE_CLICK_ATTACK_MOVE and MSG_MOUSE_LEFT_CLICK/RIGHT_CLICK handlers now break early when screenToTerrain fails; isPoint computed after the terrain check.
Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp New getDrawRegion2D() helper extracts the existing inline heightmap-to-world-region computation into a reusable method.
Core/Libraries/Include/Lib/BaseType.h setFromPointsNoZ renamed to setXYFromPoints; new setXY(Region2D) added; both intentionally leave Z unchanged, safe because all call sites unconditionally overwrite Z afterwards.
Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWaterTracks.cpp screenToTerrain return value checked; wave logic only executes on valid terrain hit; pauseWaves moved outside the terrain-check block.
GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp Stub implementations updated to match the new signatures; all return appropriate NO_INTERSECTION / false defaults.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Input as Mouse Input
    participant Caller as Command/UI Caller
    participant View as W3DView
    participant Ray as getPickRay
    participant Terrain as TerrainRenderObj
    participant Plane as PlaneClass

    Input->>Caller: screen coordinate (ICoord2D)

    alt screenToTerrain path
        Caller->>View: screenToTerrain(screen, world)
        View->>Ray: "getPickRay, rayEnd length = sqr(depth)"
        Ray-->>View: line segment
        View->>Terrain: Cast_Ray(lineseg)
        alt terrain hit
            Terrain-->>View: ContactPoint
            View-->>Caller: "return true, world = hit point"
            Caller->>Caller: issue game command
        else no hit (sky / off-map)
            Terrain-->>View: false
            View-->>Caller: return false
            Caller->>Caller: break / COMMAND_COMPLETE
        end
    end

    alt screenToWorldAtZ path
        Caller->>View: screenToWorldAtZ(screen, world, z)
        View->>Ray: getPickRay
        Ray-->>View: line segment
        View->>Plane: Compute_Intersection(rayStart, rayEnd, t)
        alt INSIDE_SEGMENT
            Plane-->>View: INSIDE_SEGMENT
            View-->>Caller: "return INSIDE_SEGMENT, world = intersection"
        else OUTSIDE_LINE
            Plane-->>View: OUTSIDE_LINE
            View-->>Caller: "return OUTSIDE_LINE, world = extrapolated"
        else NO_INTERSECTION
            Plane-->>View: NO_INTERSECTION
            View-->>Caller: return NO_INTERSECTION, world unchanged
        end
    end
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 Input as Mouse Input
    participant Caller as Command/UI Caller
    participant View as W3DView
    participant Ray as getPickRay
    participant Terrain as TerrainRenderObj
    participant Plane as PlaneClass

    Input->>Caller: screen coordinate (ICoord2D)

    alt screenToTerrain path
        Caller->>View: screenToTerrain(screen, world)
        View->>Ray: "getPickRay, rayEnd length = sqr(depth)"
        Ray-->>View: line segment
        View->>Terrain: Cast_Ray(lineseg)
        alt terrain hit
            Terrain-->>View: ContactPoint
            View-->>Caller: "return true, world = hit point"
            Caller->>Caller: issue game command
        else no hit (sky / off-map)
            Terrain-->>View: false
            View-->>Caller: return false
            Caller->>Caller: break / COMMAND_COMPLETE
        end
    end

    alt screenToWorldAtZ path
        Caller->>View: screenToWorldAtZ(screen, world, z)
        View->>Ray: getPickRay
        Ray-->>View: line segment
        View->>Plane: Compute_Intersection(rayStart, rayEnd, t)
        alt INSIDE_SEGMENT
            Plane-->>View: INSIDE_SEGMENT
            View-->>Caller: "return INSIDE_SEGMENT, world = intersection"
        else OUTSIDE_LINE
            Plane-->>View: OUTSIDE_LINE
            View-->>Caller: "return OUTSIDE_LINE, world = extrapolated"
        else NO_INTERSECTION
            Plane-->>View: NO_INTERSECTION
            View-->>Caller: return NO_INTERSECTION, world unchanged
        end
    end
Loading

Reviews (4): Last reviewed commit: "Fix style" | 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

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