Skip to content

Make contour maps available in 3D views - #1045

Open
magnesj wants to merge 23 commits into
devfrom
1044-contour-maps-in-3d-view
Open

Make contour maps available in 3D views#1045
magnesj wants to merge 23 commits into
devfrom
1044-contour-maps-in-3d-view

Conversation

@magnesj

@magnesj magnesj commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Makes contour maps available for visualization inside normal 3D views.

Implements the plan in #1044, which addresses #1043.

What it does

A new Contour Maps collection in every 3D view mirrors the project level contour maps. Each entry is a thin wrapper pointing at an existing contour map view, so the contour map itself, its result, aggregation and computed geometry, stays in one place. The 3D view only renders it, and carries the presentation state that belongs to that view.

Setting Values Default
Map Position Top of Case / Bottom of Case / User Defined Depth Top of Case
Depth Offset 0
Show Map Surface on
Project Surface on Geometry off
Show Contour Lines on
Project Lines on Geometry off
Line Color Mode Contrast to Map / Single Color Contrast to Map
Line Color shown for Single Color Black
Line Thickness 1 to 10 1
Show Contour Labels off

Contour maps of every case are mirrored, not only those of the case the view belongs to, so an ensemble statistics map can be overlaid on a view of a single realization. New entries start unchecked.

How it fits together

The contour map coordinate space is flat, with z always zero, lifted by consumers to RigContourMapGrid::origin3d(), which is the minimum z of the expanded bounding box. In a 3D view that puts the map under the reservoir, so vertical placement had to become a decision rather than a constant.

RivContourMapElevationProvider is that decision point. RivContourMapProjectionPartMgr routes all four of its local to domain conversions through one pair of helpers that consult the provider, and drops geometry where the provider reports no elevation. Passing no provider reproduces the previous coordinates exactly, which is what the 2D contour map views do, so they are unaffected throughout.

Two providers exist: a flat plane, and RigContourMapTopography, a raster of the top of the visible geometry sampled on the contour map vertices. The raster is built by dropping a vertical ray at each vertex and keeping the highest hit among the visible grid cells and the visible surfaces, so the drape follows what is actually on screen, including cell and property filters. It is rebuilt when the visibility array, the map grid or the surface set changes.

Since contour polygons are simplified with a Douglas-Peucker epsilon of up to a fifth of the sample spacing, a straight stretch of a contour line collapses to one long segment. Elevating only its end points would leave it cutting through the geometry, so segments are resampled before being elevated, at a density the provider asks for.

Notes for review

Three things are worth a second opinion.

Polygon offset does not work on contour lines. They are PT_LINES primitives, and OpenGL polygon offset applies to polygon primitives only, the enable flags selecting which polygon mode is offset rather than which primitive type. The existing call in RivContourMapProjectionPartMgr discarded its return value and was inert in any case. Contour lines are therefore lifted above the surface geometrically, by 0.02 * sampleSpacing, rather than through a render state.

Two tuning constants are judgement calls made without a real model to look at: the line lift above, and eight samples per raster cell for the resampling density.

Contour label layout is fixed when the display model is built. cvf::DrawableText re-projects its anchors every frame, so labels stay readable and correctly placed while navigating, but which labels were dropped for overlapping, and the in plane rotation of the text, reflect the camera at build time. The 2D views rebuild on a camera z change above five percent through onViewNavigationChanged. That hook is deliberately not added here, since it would rebuild the whole contour map model on navigation for every 3D view.

Scope

Eclipse contour maps and ensemble statistics contour maps. GeoMech contour maps are not covered: their collection lives per case on RimGeoMechCase rather than on RimOilField, and there is no common view base type across the two families.

Verification

Clean build, and the 1023 unit tests in ResInsight-tests pass. A snapshot run over TEST10K_FLT_LGR_NNC loads the case, builds the display model through the new rendering and legend paths and exits cleanly.

The visual behaviour has not been verified end to end. #1044 carries a checklist for that, covering placement, the two drape options, filters, time steps, ensembles, deletion and project round trip.

@magnesj

magnesj commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

The clang-19 build failure is fixed in 1162eae. RimNamedObject::name is virtual, so the wrapper was overriding it implicitly, which clang rejects under -Winconsistent-missing-override with warnings as errors. MSVC accepts it, so local builds were clean. That job now passes.

One check is still red, and it is not from this branch:

build (Release, Windows_2022_qt_610, ...) fails in the Configure step, not the build. vcpkg cannot download msys2-runtime-3.5.4-2-x86_64.pkg.tar.zst, all six mirrors return 404, so nanoflann:x64-windows fails to build.

The same job, same step and same 404s fail on dev in run 31707377016, which started before this branch was pushed. A re-run will not help either, since a 404 on every mirror means that package version has been rotated off upstream rather than a transient network problem. Fixing it means moving the vcpkg baseline, which is repository infrastructure and outside the scope of this PR.

magnesj added 23 commits August 14, 2026 14:10
Contour map geometry is generated in a flat, contour map local coordinate
space where z is always zero, and consumers lift it to the minimum z of the
expanded bounding box. Introduce RivContourMapElevationProvider so the
vertical placement becomes a decision made in one place, and route the four
local-to-domain conversions in RivContourMapProjectionPartMgr through a pair
of shared helpers. Without a provider the resulting coordinates are unchanged,
so the 2d contour map views render exactly as before.

Geometry is dropped where the provider reports no elevation. This splits a
draped polyline into separate pieces wherever there is nothing underneath it.

Also assign the polygon offset render state to the contour line effect. The
return value of createAndConfigurePolygonOffsetRenderState was discarded, so
the lines had no offset at all. This is invisible in the 2d views where the
lines are coplanar with the map, but causes z-fighting once the lines are
draped on grid geometry.
Sample the top of the visible grid geometry on the vertices of a contour map
grid, by dropping a vertical ray at every vertex and keeping the highest
intersection with a visible cell. The result is a raster that is cheap to
interpolate, which is what draping contour lines needs. Positions with no
visible cell underneath stay undefined so the consumer can leave a gap.

The visibility array is supplied by the caller and is the authority on what
the view is showing, so the drape follows cell filters and property filters.
RimContourMapInView points at a contour map view and renders the geometry that
view has already computed. Keeping the contour map itself in one place means
there is no second projection to configure and no result to recompute, so the
wrapper only carries the state that belongs to the 3d view: where the map is
placed vertically, and which parts of it are drawn.

The map plane is placed at the top of the case by default, since the contour
map coordinate space would otherwise put it below the reservoir. Contour lines
can follow the top of the visible geometry instead of the map plane.

Contour labels are left out. Their placement depends on the camera and is only
kept correct in the 2d views, which regenerate them while navigating.
The project level collection of contour map views owns the contour maps. This
collection mirrors it into a single 3d view, holding one wrapper per contour
map. The sync sweeps wrappers whose contour map has been deleted, creates
wrappers for new ones, and reorders to follow the source.

The source collection is flat rather than nested, so the sync is modelled on
RimSurfaceInViewCollection rather than the RimNestedMirrorCollectionInView
template.

Contour maps of every case are mirrored, not only those of the case the view
belongs to, so an ensemble statistics map can be overlaid on a view of a single
realization. New wrappers start out unchecked.
Give RimGridView the in-view collection, sync it from the project contour maps
through the existing updateViewTreeItems mechanism, and render it from
RimEclipseView. The parts go into a frame scene model rather than a static one,
since contour map results depend on the time step.

The 2d contour map views return null for the collection. They already show
their own contour map, and should not offer to show the other contour maps of
the project.
The 3d views mirror the project contour map collection, so they have to be told
when a contour map appears or disappears. All creation paths go through addView,
including the new contour map feature and paste, so the notification covers them
all.

Only deletion schedules a redraw. A newly mirrored contour map starts out
unchecked and therefore draws nothing.
The contour lines were drawn as hairlines coloured to contrast the map level
underneath them. That works on the flat 2d map, but reads poorly in a 3d view
where the lines are draped on grid geometry of arbitrary colour.

Give RimContourMapInView a line colour mode, a colour and a thickness, and pass
them to the part manager as an appearance struct. The defaults reproduce the
existing appearance, so the 2d contour map views, which pass no appearance at
all, are unaffected.
The name field is the user description field, and is what the project tree
displays. It was never set, so the collection showed up without a label.
…the surface

Contour lines were sometimes hidden by the map surface. The two are coplanar,
and the polygon offset added earlier to pull the lines forward never had any
effect: OpenGL polygon offset applies to polygon primitives only, and the
enable flags select which polygon mode is offset, not which primitive type.
These are line primitives, so no polygon offset can reach them. Drop the
render state and lift the lines above the surface geometrically instead, by a
fraction of the sample spacing so the lift scales with the model.

Also add Project Surface on Geometry, so the map surface can follow the top of
the visible geometry the same way the contour lines already could. Triangles
with no visible cell underneath are dropped, which the part manager already
handled. This makes the lift matter more, since a draped surface and draped
lines are coplanar everywhere.

The topography raster is now rebuilt when the view produces a new cell
visibility array or the projection a new map grid, so the drape follows cell
and property filter changes rather than keeping the raster it was first built
with.
Labels were left out on the assumption that they would not survive navigation.
They do: DrawableText re-projects each anchor to window coordinates every frame
and draws screen aligned text, so a label stays readable and stays on its
contour line whatever the camera does.

What is decided once, from the camera as it is when the display model is built,
is the layout: which labels are dropped for overlapping each other, and the in
plane rotation of the text. Rotating the view can therefore leave labels
overlapping or turned at an odd angle until the model is rebuilt.

Off by default, and follows the elevation of the contour lines, so the labels
are lifted clear of the map surface along with them.
The labels derived their own color from the map level, so a label kept the
contrasting color even when the lines had been given an explicit one, and the
two disagreed.

Both now go through RivContourLineAppearance::colorForLevel, so a label cannot
drift away from the line it belongs to. Behaviour is unchanged when no explicit
color is set, which is what the 2d contour map views use.
Contour polygons are simplified with a Douglas-Peucker epsilon of up to a fifth
of the sample spacing, so a straight stretch of a contour line collapses to a
single long segment. Elevating only the end points of such a segment leaves it
cutting straight through everything in between, which is why the lines did not
look draped.

Resample each segment before elevating it, so the line picks up the shape of
the geometry along its whole length. How densely is asked of the elevation
provider: the topography provider samples eight times per raster cell, and the
flat provider asks for no resampling at all, since a plane is followed exactly
by its end points.

Samples where no elevation is defined still leave a gap, so a draped line is
now split at the true edge of the visible geometry rather than at the nearest
polygon vertex.
The topography only searched grid cells, filtered by the cell visibility of the
view. Hiding the grid cells makes that visibility all false, so nothing was hit
anywhere, every elevation came back undefined, and the gap handling swallowed
the whole contour line. A view showing only a surface therefore drew no lines
at all.

Search the visible surfaces of the view too, and keep whichever of the two is
higher, so the geometry follows what is actually on screen. Each surface gets a
ray range derived from its own vertices, since a surface can sit well outside
the depth range of the case.

Surfaces are collected through a new visibleSurfacesInView on
RimSurfaceInViewCollection, which recurses the checked sub collections the same
way the existing appendPartsToModel and legendConfigs do. The cached raster is
rebuilt when that set changes, so checking a surface on or off updates the
drape.
RimNamedObject declares name as virtual, so the wrapper was overriding it
implicitly. Clang rejects that under -Winconsistent-missing-override with
warnings as errors, which broke the Linux clang build. MSVC accepts it, so it
was not caught locally.

Moved to the public section along with the override, matching how
RimSurfaceInViewCollection exposes the same thing. The name still follows the
contour map the wrapper points at.
Draped contour lines disappeared into the geometry wherever the cells step,
because the elevation raster was built on the contour map grid vertices. That
spacing, not the sampling of the curve, is what limits how closely a draped
curve can follow the geometry: between two raster vertices the interpolated
elevation ramps smoothly while the cells step, so the curve sinks below the top
face of the cell it just stepped down from and is hidden by it.

Build the raster on its own lattice at a quarter of the contour map sample
spacing, over the same extent. Curve sampling follows automatically, since the
resampling distance is derived from the raster spacing rather than from the
contour map grid.

Costs the refinement squared in rays, sixteen times as many, which is why the
factor is not larger. The raster is cached and only rebuilt when the visible
geometry changes, so this is paid once rather than per redraw.
The label direction handed to the text drawer is a screen space one, but it was
being filled in with the contour segment direction taken from domain space.
Those only agree while the camera looks straight down at the map, which is
always true in the 2d contour map views and almost never true in a 3d view, so
the text came out rotated arbitrarily.

Keep the labels upright on screen in the 3d views. The 2d views are unchanged
and still run their labels along the contour lines, which is what a map should
look like when seen from above.

The font size was fixed at 10 points, too small to read against the geometry.
It is now a field on the wrapper, so the labels can be sized to the view.
Projected onto the geometry the map surface took its elevation straight from
the topography, leaving it coplanar with the cell faces it follows, so the two
z-fought. Lift it by a fraction of the sample spacing instead, in the same way
the contour lines already are.

The lift is geometric rather than a depth buffer adjustment, so it holds from
any camera angle and does not depend on render state.

The contour line lift is now expressed as twice the surface lift, which keeps
the ordering of geometry, then map surface, then lines explicit, and keeps the
lines on top of the surface whether it is draped or flat.
White lines of thickness two, with labels shown, read better against grid
geometry than the previous single pixel contrast coloured lines with labels
switched off.

The line color mode defaults to Single Color as well, since the white default
has no effect while every level takes a color contrasting the map underneath
it.

Only affects contour maps added to a view from now on. Ones already stored in a
project keep the values they were saved with.
Uses RiaDefines::betaFeaturePostfix, the same marker the cloud data and OSDU
features carry. Applied both to the object name and to the name field, since
the project tree reads the label from the latter.
A contour map read from a project was not drawn in the 3d views showing it.

RimEclipseCase::allSpecialViews returns the ordinary 3d views before the
contour map views, and the project loader loads them in that order. A 3d view
therefore built its display model while the contour map view it mirrors had not
been loaded, and the projection derives its cell visibility from that view.
Cell visibility is accumulated over the visible grid parts, which are only
populated once the view has created its display model, so every cell read as
hidden and the projection produced no results. Both the empty visibility and
the empty result were then cached, and nothing invalidated them once the
contour map view did load.

Tell the mirroring views once the contour map view is ready, so they drop the
geometry they built too early and rebuild against real data. Creating a contour
map interactively was never affected, since the contour map view is loaded
before it can be checked in a 3d view.
The previous attempt at this notified the 3d views from onLoadDataAndUpdate,
and asked them for a scheduled redraw. Both were wrong, and logging each early
return against a project that reproduces it showed why.

onLoadDataAndUpdate only schedules the contour map view's own rebuild, so the
results and the geometry did not exist yet when the 3d views were told to
redraw. They rebuilt against an empty projection, zero contour polygons and
zero triangles, and nothing invalidated that afterwards.

A scheduled redraw would not have helped either. It reaches
createDisplayModelAndRedraw, which never calls onUpdateDisplayModelForCurrent
TimeStep, and that is where the contour map parts are appended, since they
belong to the frame scene.

Notify from the end of updateGeometry instead, where the results and the
geometry have just been generated, and ask the views for the time step update
directly. This also covers changes to the contour map itself, which previously
left the 3d views showing what they had built earlier.
Drop the map edge no longer. The topography raster spans the full extent of
the map, so a position exactly on the far edge is inside it and belongs to the
last cell. Clamp the cell index instead of rejecting the position, which
silently discarded the outermost row and column of every draped map.

Cast the drape rays over the range of the grid they are cast against. The
range came from the contour map grid, which belongs to the case the map was
computed from rather than to the view showing it. A ray that starts inside a
cell only reports its exit point, and cells above it are missed entirely.

Mirror the ensemble statistics contour maps as well. They live on their
ensemble rather than in the oil field collection, so enumerate every contour
map view of the project instead. Their collection now refreshes the 3d views
on add and delete, as the oil field collection already did.

Append the contour maps in onCreateDisplayModel for a view showing only static
results. Such a view has no frame scenes at all, and never reaches
onUpdateDisplayModelForCurrentTimeStep, so nothing was drawn and no user
action recovered it. Fall back to the main scene for the same reason.

Generate the projection for the time step the contour map view is on, not for
the host view's. The projection is owned by that view and shared with it, so
driving it elsewhere left it holding results for a step it was not showing,
and made the two views recompute against each other on every redraw.

Keep the topography raster when only the contour map has changed. It follows
the host view, which has not, and rebuilding it on every time step change of
the contour map view re-rasters a ray per lattice vertex against the whole
main grid.

Use the documented defaults for the line appearance. The enum default and the
field default disagreed, and the field default won.

Drop the context menu entry, which could never be enabled from that node, and
the unused collection level clearGeometry. What it was reaching for is a main
grid identity in the topography cache key, which is now there.

Return no contour map collection for a GeoMech view, as the contour map views
already do. It cannot render them, and mirroring them filled the view and the
project file with entries that could never be shown.
@magnesj
magnesj force-pushed the 1044-contour-maps-in-3d-view branch from d5461ae to a9b7d63 Compare August 14, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant