Skip to content

Show contour maps in 3D views via an in-view contour map collection #1044

Description

@magnesj

Implementation plan for #1043.

Description

What

Make contour maps available inside normal 3D views. A new in-view mirror collection on RimGridView lists every contour map in the project; a per-view wrapper object points at an existing contour map view and renders its geometry in the 3D scene at a controllable elevation, with an option to drape the contour lines onto the visible grid geometry.

Why

Contour maps today can only be seen in their own dedicated 2D view. RimEclipseContourMapView derives from RimEclipseView but locks the camera top-down (sm_defaultViewMatrix, navigation rotation disabled, orthographic projection), and the projection geometry is generated in a flat local coordinate space with z = 0 that the part manager lifts to RigContourMapGrid::origin3d() — the minimum z of the expanded bounding box, i.e. below the reservoir.

The result is that a contour map cannot be correlated visually with the 3D model it was computed from. Users want to overlay a contour map (including ensemble statistics maps) on a normal 3D view: see the aggregated values as a coloured plane above the grid, see the contour lines, and optionally drape those lines onto the visible grid geometry so they read as isolines on the reservoir surface.

How

Mirror, don't duplicate. RimContourMapInView holds a caf::PdmPtrField<RimEclipseContourMapView*> and renders that view's already-computed trianglesWithVertexValues(), contourPolygons() and legendConfig(). No second projection, no second result computation, no duplicated result/aggregation/resolution UI. The wrapper owns only presentation state: where the map sits in z, whether the surface and lines are drawn, and whether lines are draped.

This follows the established in-view collection pattern used by RimSurfaceInViewCollection and RimPolygonInViewCollection: project-level master data under RimOilField, a caf::PdmChildField mirror on the view, thin wrapper objects with a PdmPtrField back to the master item, and a sync routine that orphan-sweeps, find-or-creates and reorders.

Scope

Eclipse contour maps and ensemble statistics contour maps. RimStatisticsContourMapView derives from RimEclipseContourMapView, so a single pointer type covers both.

GeoMech contour maps are deliberately out of scope: their collection lives per-case on RimGeoMechCase rather than on RimOilField, and there is no common view base type across the two families.

Key design decisions

Decision Choice Rationale
What the wrapper holds Pointer to an existing contour map view Reuses the computed projection, triangles, contour polygons and legend. RimContourMapProjection::baseView() already assumes a contour-map-view ancestor, so owning an independent projection would require reworking that.
Vertical placement MapPosition enum (Top of case / Bottom of case / User defined depth) + Depth Offset Default TOP_OF_CASE puts the map above the reservoir instead of underneath it. TOP_OF_CASE uses expandedBoundingBox().max().z(), the same value RicCreateContourMapPolygonTools uses for contour-map-derived polygons.
Which maps are mirrored All of them, unchecked by default A 3D view of one realization may legitimately want to overlay an ensemble statistics map whose primary case is a different one. Filtering by case would exclude exactly that.
Scene model Per-frame named cvf::ModelBasicList, not a static viz model Contour map data is time-step dependent. Follows the pattern already in RimEclipseContourMapView::appendContourMapProjectionToModel.
Contour labels Not drawn in 3D for this iteration Label placement is camera-dependent; the 2D view only keeps them correct via onViewNavigationChanged().

Prerequisites

None. All work is inside ApplicationLibCode; no external resources or configuration changes are needed.

Steps

1. Elevation control in the part manager

Every consumer of contour map geometry repeats the same conversion in four places — createProjectionMapPart (~line 245), createContourPolygons (~line 304), createContourLabels, and createPickPointVisDrawable (~line 552):

cvf::Vec3d globalVertex  = localVertex + contourMapGrid.origin3d();
cvf::Vec3d displayVertex = displayCoordTransform->transformToDisplayCoord( globalVertex );

Introduce an elevation provider so the z component can be replaced without touching geometry generation.

  • Add ApplicationLibCode/ModelVisualization/RivContourMapElevationProvider.{h,cpp} with an abstract provider and two implementations:
    class RivContourMapElevationProvider
    {
    public:
        virtual ~RivContourMapElevationProvider();
        // localPos2d is relative to RigContourMapGrid::origin2d().
        // Returns the domain z to use, or nullopt where no elevation is defined.
        virtual std::optional<double> domainElevation( const cvf::Vec2d& localPos2d ) const = 0;
    };
    
    class RivContourMapFlatElevation : public RivContourMapElevationProvider { /* constant z */ };
    class RivContourMapTopographyElevation : public RivContourMapElevationProvider { /* backed by RigContourMapTopography */ };
  • Centralise the coordinate conversion in RivContourMapProjectionPartMgr behind a private helper:
    std::optional<cvf::Vec3d> toDisplayCoord( const cvf::Vec3d&                     localVertex,
                                              const RigContourMapGrid&              contourMapGrid,
                                              const caf::DisplayCoordTransform*     displayCoordTransform,
                                              const RivContourMapElevationProvider* elevationProvider ) const;
    With elevationProvider == nullptr it must return exactly today's value (+ origin3d()) and never nullopt, so existing 2D contour map views are byte-for-byte unchanged.
  • Add a trailing const RivContourMapElevationProvider* elevationProvider = nullptr parameter to appendProjectionToModel and appendContourLinesToModel. Existing call sites in RimEclipseContourMapView.cpp and RimGeoMechContourMapView.cpp need no edit.
  • Handle gaps in createContourPolygons: when toDisplayCoord returns nullopt for either end of a segment, skip that segment. Dropping segments naturally splits a draped polyline wherever the grid is not visible below it — the same gap handling RivSeismicSectionPartMgr::projectPolyLineOntoSurface uses.
  • Fix the discarded polygon offset. At RivContourMapProjectionPartMgr.cpp:174 (and :92) the return value of caf::MeshEffectGenerator::createAndConfigurePolygonOffsetRenderState( caf::PO_1 ) is discarded and never assigned to the effect, so contour lines get no polygon offset at all. In the 2D view the lines are coplanar with the map so nothing shows; draped onto grid geometry they will z-fight badly. Assign the render state to an un-cached effect (generateUnCachedEffect(), otherwise the shared cached effect is polluted), using the recipe already established in RivSurfaceIntersectionCurveTools::createCurvePart (factor = -5, units = -1000). This is required for the feature, not a drive-by cleanup.

2. Top-elevation lookup over the contour map grid

Nothing in the repo does "drape onto grid topography" today (grep -i drape returns nothing), and RigMainGrid has no topZAtXY()-style helper. Rather than ray-casting per polygon vertex, precompute a raster once over the contour map grid's vertices and bilinearly interpolate — the same shape as RigContourMapProjection::interpolateValue. The vertex count is the same order as the map itself (a 100×100 map is ~10k rays, computed once per display model rebuild).

  • Add ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapTopography.{h,cpp}:
    class RigContourMapTopography
    {
    public:
        RigContourMapTopography( const RigContourMapGrid& contourMapGrid,
                                 const RigMainGrid&       mainGrid,
                                 const cvf::UByteArray*   cellVisibility );
    
        std::optional<double> elevationAtLocalPos( const cvf::Vec2d& localPos2d ) const;
    
    private:
        std::vector<double> m_vertexElevations; // infinity marks undefined
        cvf::Vec2ui         m_vertexCount;
    };
  • Implement the per-vertex ray drop using the idiom already present at RigContourMapCalculator.cpp:450: build a thin vertical cvf::BoundingBox at the vertex x/y spanning the grid's z range → RigMainGrid::findIntersectingCells( bbox ) → for each cell set in cellVisibility, RigHexIntersectionTools::lineHexCellIntersection with a vertical segment top→bottom → keep the maximum hit z. Use #pragma omp parallel for over vertices, matching the surrounding contour map code.
  • Implement bilinear interpolation in elevationAtLocalPos, returning nullopt when any contributing vertex is undefined.

cellVisibility is the host view's RimGridView::currentTotalCellVisibility(), so the drape follows the 3D view's cell filters and property filters — this is what "project onto the visible geometry" means.

3. The in-view wrapper

  • Add ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapInView.{h,cpp}, deriving from RimCheckableNamedObject:

    using SourceItemT = RimEclipseContourMapView;   // matches the in-view wrapper convention
    RimEclipseContourMapView* sourceItem() const;
    
    enum class MapPosition { TOP_OF_CASE, BOTTOM_OF_CASE, USER_DEFINED_DEPTH };
    Field Purpose
    caf::PdmPtrField<RimEclipseContourMapView*> m_contourMapView the mirrored source
    caf::PdmProxyValueField<QString> m_nameProxy tracks the source view's name; nameField() uiHidden
    caf::PdmField<caf::AppEnum<MapPosition>> m_mapPosition default TOP_OF_CASE
    caf::PdmField<double> m_depthOffset applied in all modes, default 0
    caf::PdmField<double> m_userDefinedDepth positive downwards (z = -depth), as RimPolygonAppearance does with m_polygonPlaneDepth
    caf::PdmField<bool> m_showMapSurface the coloured plane, default true
    caf::PdmField<bool> m_showContourLines default true
    caf::PdmField<bool> m_projectLinesOnGeometry drape lines onto visible grid geometry, default false

    Non-PDM members: cvf::ref<RivContourMapProjectionPartMgr> m_partMgr (lazily created, exactly as RimSurfaceInView::surfacePartMgr() does) and a cached std::shared_ptr<RigContourMapTopography> cleared by clearGeometry().

  • Implement appendPartsToModel( cvf::ModelBasicList*, const caf::DisplayCoordTransform*, const cvf::Camera*, int hostTimeStep ):

    1. Guard: source view non-null, its case has loaded data, isChecked().
    2. auto* proj = m_contourMapView->contourMapProjection(); then proj->generateResultsIfNecessary( timeStep ); proj->generateGeometryIfNecessary(); — clamp timeStep to the source case's time step count, since the host view and the contour map's case need not have the same number of steps.
    3. Build a RivContourMapFlatElevation from mapElevation() and call appendProjectionToModel(...) when m_showMapSurface.
    4. Build either the flat provider or a RivContourMapTopographyElevation and call appendContourLinesToModel(..., showContourLines = m_showContourLines, showContourLabels = false, ...). Passing showContourLabels = false already short-circuits createContourLabels (RivContourMapProjectionPartMgr.cpp:149).
  • Implement double mapElevation() const:

    • TOP_OF_CASEproj->mapGrid()->expandedBoundingBox().max().z()
    • BOTTOM_OF_CASEproj->mapGrid()->origin3d().z() (today's 2D behaviour)
    • USER_DEFINED_DEPTH-m_userDefinedDepth()

    m_depthOffset() is added in all three cases.

  • Implement legend forwarding: legendConfig() returns m_contourMapView->contourMapProjection()->legendConfig(), and updateLegendRangesTextAndVisibility( RiuViewer*, bool ) calls the source projection's updateLegend() (which sets ranges and title) then addColorLegendToBottomLeftCorner( legendConfig()->titledOverlayFrame(), ... ), mirroring RimSurfaceInView::updateLegendRangesTextAndVisibility (RimSurfaceInView.cpp:269).

  • Implement fieldChangedByUi: clearGeometry() for m_projectLinesOnGeometry (invalidates the topography cache), then firstAncestorOfType<Rim3dView>()->scheduleCreateDisplayModelAndRedraw().

4. The in-view collection

The master collection RimEclipseContourMapViewCollection is a flat caf::PdmObjectCollection<RimEclipseContourMapView>, not a caf::PdmNestedCollection, so the CRTP template RimNestedMirrorCollectionInView (used by RimPolygonInViewCollection) does not apply. Model the sync on the flat half of RimSurfaceInViewCollection::syncSurfacesWithView().

  • Add ApplicationLibCode/ProjectDataModel/ContourMap/RimContourMapInViewCollection.{h,cpp}, deriving from RimCheckableNamedObject:
    caf::PdmChildArrayField<RimContourMapInView*>         m_contourMapsInView;
    caf::PdmPtrField<RimEclipseContourMapViewCollection*> m_sourceCollection;   // uiHidden
    
    void updateFromContourMapCollection();        // sync entry, resolves the source via RimProject
    std::vector<RimContourMapInView*>    visibleContourMapsInView() const;      // {} when !isChecked()
    void appendPartsToModel( cvf::ModelBasicList*, const caf::DisplayCoordTransform*, const cvf::Camera*, int timeStep );
    std::vector<RimRegularLegendConfig*> legendConfigs() const;
    void updateLegendRangesTextAndVisibility( RiuViewer*, bool );
    void clearGeometry();
    void appendMenuItems( caf::CmdFeatureMenuBuilder& ) const override;         // "RicNewContourMapViewFeature"
  • Implement the sync in updateFromContourMapCollection(): orphan-sweep children whose PdmPtrField has been auto-nulled by PDM, find-or-create a wrapper per source view, then clearWithoutDelete() and push back in source order. Every new wrapper starts unchecked.

5. Wiring into the views

  • Extend the bitmask at ApplicationLibCode/Application/RiaDefines.h:269:
    enum class ItemIn3dView { NONE = 0b00000000, SURFACE = 0b00000001, POLYGON = 0b00000010,
                              CONTOUR_MAP = 0b00000100, ALL = 0b00000111 };
  • RimGridView.{h,cpp}: add caf::PdmChildField<RimContourMapInViewCollection*> m_contourMapInViewCollection plus accessor, created eagerly in the constructor next to m_polygonInViewCollection (RimGridView.cpp:106). Handle ItemIn3dView::CONTOUR_MAP in updateViewTreeItems (RimGridView.cpp:533) by calling updateFromContourMapCollection().
  • RimEclipseView.cpp — scene assembly in onUpdateDisplayModelForCurrentTimeStep(), using the per-frame named-model pattern from RimEclipseContourMapView.cpp:333 (not a static viz model, because the data is time-step dependent):
    cvf::Scene* frameScene = nativeOrOverrideViewer()->frame( m_currentTimeStep, isUsingOverrideViewer() );
    Rim3dView::removeModelByName( frameScene, "ContourMapsInView" );
    cvf::ref<cvf::ModelBasicList> model = new cvf::ModelBasicList;
    model->setName( "ContourMapsInView" );
    m_contourMapInViewCollection->appendPartsToModel( model.p(), displayCoordTransform().p(),
                                                      viewer()->mainCamera(), m_currentTimeStep() );
    model->updateBoundingBoxesRecursive();
    frameScene->addModel( model.p() );
  • RimEclipseView.cpp — tree and legends: uiTreeOrdering.add( contourMapInViewCollection() ) in defineUiTreeOrdering (~line 2141); append the collection's legend configs in legendConfigs() (~line 2536); call updateLegendRangesTextAndVisibility(...) in onUpdateLegends() (~line 1674) when checked.
  • RimEclipseContourMapView.cpp: suppress the collection inside contour map views themselves (they derive from RimEclipseView), the same way surfaceInViewCollection() returns nullptr there (RimEclipseContourMapView.cpp:641) — don't add it to the tree and skip the append. Otherwise every contour map view would offer to render every other contour map.

6. Change notification

  • RimEclipseContourMapViewCollection.cpp: after addView, removeChild and in onChildDeleted, broadcast to all views, following RimPolygonCollection::updateViewTreeItems() (RimPolygonCollection.cpp:203):
    for ( auto view : RimProject::current()->allViews() )
    {
        view->updateViewTreeItems( RiaDefines::ItemIn3dView::CONTOUR_MAP );
        view->scheduleCreateDisplayModelAndRedraw();
    }
  • RicNewContourMapViewFeature.cpp: trigger the same broadcast after creating a map, so it appears in already-open 3D views without a reload.

7. Build files

Headers are not listed in this project's cmake fragments — only .cpp files.

  • ApplicationLibCode/ProjectDataModel/ContourMap/CMakeLists_files.cmake — add RimContourMapInView.cpp, RimContourMapInViewCollection.cpp
  • ApplicationLibCode/ReservoirDataModel/ContourMap/CMakeLists_files.cmake — add RigContourMapTopography.cpp
  • ApplicationLibCode/ModelVisualization/CMakeLists_files.cmake — add RivContourMapElevationProvider.cpp
  • New files get a Copyright (C) 2026- Equinor ASA header (current year, per docs/agents/coding-style.md)

8. Verification

  • Build and run unit tests
    cmake --build --preset x64-relwithdebinfo --target ResInsight
    ctest -R "UnitTests" -C RelWithDebInfo
  • Regression check on the 2D view: create a contour map (New Contour Map from a 3D view) and confirm the 2D view is visually unchanged. This validates the toDisplayCoord refactor and the polygon offset fix.
  • Basic overlay: open a normal 3D view. Contour Maps appears in the tree with the map listed and unchecked. Check it — the coloured plane renders above the grid (TOP_OF_CASE) with contour lines, and its legend appears in the bottom-left corner.
  • Placement: switch Map Position through Bottom of Case and User Defined Depth, and vary Depth Offset. The plane moves accordingly.
  • Visibility toggles: toggle Show Map Surface and Show Contour Lines independently.
  • Draping: enable Project Lines on Geometry. Lines follow the grid topography and are cut where no cell is visible. Apply a cell filter or property filter in the 3D view and confirm the drape follows the new visibility and does not z-fight.
  • Time steps: step through time steps in the 3D view and confirm the map updates.
  • Ensemble: create an ensemble statistics contour map and confirm it also appears and renders.
  • Deletion: delete the contour map view and confirm the wrapper disappears from every 3D view without a crash.
  • Project round-trip: save the project, reopen it, and confirm check state, map position, offset and drape flag all persist.

Files touched

New (8 files)

File Purpose
ProjectDataModel/ContourMap/RimContourMapInView.{h,cpp} per-view wrapper around a contour map view
ProjectDataModel/ContourMap/RimContourMapInViewCollection.{h,cpp} in-view mirror collection
ReservoirDataModel/ContourMap/RigContourMapTopography.{h,cpp} top-elevation raster over the contour map grid
ModelVisualization/RivContourMapElevationProvider.{h,cpp} elevation provider interface + flat/topography implementations

Modified (~9 files)

ModelVisualization/RivContourMapProjectionPartMgr.{h,cpp}, ProjectDataModel/RimGridView.{h,cpp}, ProjectDataModel/RimEclipseView.cpp, ProjectDataModel/ContourMap/RimEclipseContourMapView.cpp, ProjectDataModel/ContourMap/RimEclipseContourMapViewCollection.cpp, Commands/RicNewContourMapViewFeature.cpp, Application/RiaDefines.h, plus 3 CMakeLists_files.cmake.

Known limitations (documented, not fixed here)

  • Contour labels are not drawn in the 3D view. They are camera-dependent and would need an onViewNavigationChanged() hook in RimEclipseView to stay correct while rotating.
  • The map surface is always flat; only the contour lines can be draped.
  • If both the 2D contour map view and a 3D view showing it are open on different time steps, the shared projection cache (m_currentResultTimestep) will recompute on each redraw. Correct, but it costs time; worth revisiting if it proves annoying.
  • GeoMech contour maps are not covered.

Links

Issue

Contour map implementation (to reuse)

In-view collection pattern (to follow)

Draping / z-fighting prior art

Project conventions

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions