///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2026 Equinor ASA // // ResInsight is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. // // See the GNU General Public License at // for more details. // ///////////////////////////////////////////////////////////////////////////////// #include "RivRefinementRegionPartMgr.h" #include "RiaPreferences.h" #include "RivBoxGeometryGenerator.h" #include "RivPartPriority.h" #include "RivScalarMapperUtils.h" #include "ResultAccessors/RigResultAccessor.h" #include "ResultAccessors/RigResultAccessorFactory.h" #include "RigEclipseCaseData.h" #include "RigGridExportAdapter.h" #include "RigMainGrid.h" #include "RigRefinement.h" #include "Rim3dView.h" #include "RimEclipseCase.h" #include "RimEclipseCellColors.h" #include "RimEclipseView.h" #include "RimRefinementRegion.h" #include "RimRefinementRegionCollection.h" #include "RimRegularLegendConfig.h" #include "cafDisplayCoordTransform.h" #include "cafEffectGenerator.h" #include "cvfDrawableGeo.h" #include "cvfMatrix4.h" #include "cvfModelBasicList.h" #include "cvfPart.h" #include "cvfPrimitiveSetIndexedUInt.h" #include "cvfScalarMapper.h" #include "cvfStructGrid.h" #include "cvfStructGridGeometryGenerator.h" #include #include namespace { // Above this refined-cell count, the per-cell solid + wireframe rendering is skipped in favour of the outer-box wireframe. constexpr size_t MAX_CELLS_FOR_PER_CELL_GEOMETRY = 200000; constexpr int VERTICES_PER_SUB_CELL = 24; // 6 faces x 4 corners constexpr int TRI_INDICES_PER_SUB_CELL = 36; // 6 faces x 2 triangles x 3 indices } // namespace //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RivRefinementRegionPartMgr::RivRefinementRegionPartMgr() = default; //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RivRefinementRegionPartMgr::~RivRefinementRegionPartMgr() = default; //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RivRefinementRegionPartMgr::clearGeometryCache() { m_regionCaches.clear(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RivRefinementRegionPartMgr::buildGeometry( const RimRefinementRegionCollection* collection, RimEclipseCase* eclipseCase, const caf::DisplayCoordTransform* coordTransform ) { clearGeometryCache(); if ( !collection || !eclipseCase || !coordTransform ) return; if ( !collection->isActive() ) return; for ( auto* region : collection->regions() ) { if ( !region || !region->isActive() ) continue; buildRegionCache( region, eclipseCase, coordTransform ); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RivRefinementRegionPartMgr::appendStaticPartsToModel( cvf::ModelBasicList* model ) { if ( !model ) return; for ( auto& cache : m_regionCaches ) { if ( cache.facePart.notNull() ) model->addPart( cache.facePart.p() ); if ( cache.meshPart.notNull() ) model->addPart( cache.meshPart.p() ); if ( cache.outerBoxPart.notNull() ) model->addPart( cache.outerBoxPart.p() ); } } //-------------------------------------------------------------------------------------------------- /// Build the cache entry for a single region: per-sub-cell vertex array, triangle index list for /// the solid faces, line index list for the mesh overlay, and parent-cell index list used by /// updateCellResultColor() to look up the result value of the original (un-refined) cell. //-------------------------------------------------------------------------------------------------- void RivRefinementRegionPartMgr::buildRegionCache( const RimRefinementRegion* region, RimEclipseCase* eclipseCase, const caf::DisplayCoordTransform* coordTransform ) { auto* caseData = eclipseCase->eclipseCaseData(); if ( !caseData ) return; auto* mainGrid = caseData->mainGrid(); if ( !mainGrid ) return; const size_t gridI = mainGrid->cellCountI(); const size_t gridJ = mainGrid->cellCountJ(); const size_t gridK = mainGrid->cellCountK(); if ( gridI == 0 || gridJ == 0 || gridK == 0 ) return; const auto rmin = region->ijkMin(); const auto rmax = region->ijkMax(); const size_t minI = std::min( rmin.x(), gridI - 1 ); const size_t minJ = std::min( rmin.y(), gridJ - 1 ); const size_t minK = std::min( rmin.z(), gridK - 1 ); const size_t maxI = std::min( rmax.x(), gridI - 1 ); const size_t maxJ = std::min( rmax.y(), gridJ - 1 ); const size_t maxK = std::min( rmax.z(), gridK - 1 ); auto refinement = region->effectiveRefinement(); if ( !refinement ) { RegionCache cache; cache.fallbackColor = region->previewColor(); cache.gridIndex = 0; cache.outerBoxPart = createOuterBoxPart( region, eclipseCase, coordTransform ); if ( cache.outerBoxPart.notNull() ) m_regionCaches.push_back( cache ); return; } cvf::Vec3st sectorMin( minI, minJ, minK ); cvf::Vec3st sectorMax( maxI, maxJ, maxK ); RigGridExportAdapter adapter( caseData, sectorMin, sectorMax, *refinement ); const size_t refinedI = adapter.cellCountI(); const size_t refinedJ = adapter.cellCountJ(); const size_t refinedK = adapter.cellCountK(); const size_t totalRefined = refinedI * refinedJ * refinedK; if ( totalRefined == 0 ) return; if ( totalRefined > MAX_CELLS_FOR_PER_CELL_GEOMETRY ) { RegionCache cache; cache.fallbackColor = region->previewColor(); cache.gridIndex = 0; cache.outerBoxPart = createOuterBoxPart( region, eclipseCase, coordTransform ); if ( cache.outerBoxPart.notNull() ) m_regionCaches.push_back( cache ); return; } // RigGridExportAdapter applies MAPAXES to its output. The 3D view works in reservoir-native // (pre-MAPAXES) coords, so undo it for the preview if active. cvf::Mat4d invMapAxes = cvf::Mat4d::IDENTITY; bool applyInverse = adapter.useMapAxes(); if ( applyInverse ) invMapAxes = adapter.mapAxisTransform().getInverted(); std::vector vertices; vertices.reserve( totalRefined * VERTICES_PER_SUB_CELL ); std::vector parentCellIdx; parentCellIdx.reserve( totalRefined ); for ( size_t k = 0; k < refinedK; ++k ) { for ( size_t j = 0; j < refinedJ; ++j ) { for ( size_t i = 0; i < refinedI; ++i ) { auto corners = adapter.getCellCorners( i, j, k ); if ( applyInverse ) { for ( auto& c : corners ) c.transformPoint( invMapAxes ); } for ( int faceEnum = cvf::StructGridInterface::POS_I; faceEnum < cvf::StructGridInterface::NO_FACE; ++faceEnum ) { auto face = static_cast( faceEnum ); cvf::ubyte faceConn[4]; cvf::StructGridInterface::cellFaceVertexIndices( face, faceConn ); for ( int n = 0; n < 4; ++n ) { auto display = coordTransform->transformToDisplayCoord( corners[faceConn[n]] ); vertices.push_back( cvf::Vec3f( display ) ); } } auto mapping = adapter.mapRefinedToOriginal( i, j, k ); parentCellIdx.push_back( mainGrid->cellIndexFromIJK( mapping.originalI, mapping.originalJ, mapping.originalK ) ); } } } if ( vertices.empty() ) return; cvf::ref vertexArray = new cvf::Vec3fArray; vertexArray->assign( vertices ); // Triangle indices: each face quad [a,b,c,d] becomes two triangles [a,b,c] and [a,c,d]. cvf::ref triIndices = new cvf::UIntArray; triIndices->resize( totalRefined * TRI_INDICES_PER_SUB_CELL ); for ( size_t cell = 0; cell < totalRefined; ++cell ) { const cvf::uint baseVertex = static_cast( cell * VERTICES_PER_SUB_CELL ); const size_t baseIndex = cell * TRI_INDICES_PER_SUB_CELL; for ( int face = 0; face < 6; ++face ) { const cvf::uint q = baseVertex + face * 4; const size_t t = baseIndex + face * 6; triIndices->set( t + 0, q + 0 ); triIndices->set( t + 1, q + 1 ); triIndices->set( t + 2, q + 2 ); triIndices->set( t + 3, q + 0 ); triIndices->set( t + 4, q + 2 ); triIndices->set( t + 5, q + 3 ); } } cvf::ref faceGeo = new cvf::DrawableGeo; faceGeo->setVertexArray( vertexArray.p() ); cvf::ref trianglePrim = new cvf::PrimitiveSetIndexedUInt( cvf::PT_TRIANGLES ); trianglePrim->setIndices( triIndices.p() ); faceGeo->addPrimitiveSet( trianglePrim.p() ); faceGeo->computeNormals(); cvf::ref facePart = new cvf::Part; facePart->setName( "RivRefinementRegionPartMgr - refined cell faces" ); facePart->setDrawable( faceGeo.p() ); facePart->setPriority( RivPartPriority::PartType::BaseLevel ); // Mesh wireframe overlay. cvf::ref lineIndices = cvf::StructGridGeometryGenerator::lineIndicesFromQuadVertexArray( vertexArray.p() ); cvf::ref meshGeo = new cvf::DrawableGeo; meshGeo->setVertexArray( vertexArray.p() ); cvf::ref linePrim = new cvf::PrimitiveSetIndexedUInt( cvf::PT_LINES ); linePrim->setIndices( lineIndices.p() ); meshGeo->addPrimitiveSet( linePrim.p() ); cvf::ref meshPart = new cvf::Part; meshPart->setName( "RivRefinementRegionPartMgr - refined cell mesh" ); meshPart->setDrawable( meshGeo.p() ); meshPart->setPriority( RivPartPriority::PartType::MeshLines ); caf::MeshEffectGenerator meshEffGen( RiaPreferences::current()->defaultGridLineColors() ); meshPart->setEffect( meshEffGen.generateCachedEffect().p() ); RegionCache cache; cache.facePart = facePart; cache.meshPart = meshPart; cache.parentGlobalCellIdx = std::move( parentCellIdx ); cache.fallbackColor = region->previewColor(); cache.gridIndex = 0; cache.textureCoords = new cvf::Vec2fArray; cache.textureCoords->resize( vertices.size() ); // Apply a flat fallback color so the part renders sensibly before updateCellResultColor() runs. applyFlatColorToFacePart( cache ); m_regionCaches.push_back( std::move( cache ) ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RivRefinementRegionPartMgr::applyFlatColorToFacePart( RegionCache& cache ) { if ( cache.facePart.isNull() ) return; caf::SurfaceEffectGenerator surfEff( cvf::Color4f( cache.fallbackColor, 1.0f ), caf::PO_1 ); cache.facePart->setEffect( surfEff.generateCachedEffect().p() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RivRefinementRegionPartMgr::updateCellResultColor( size_t timeStepIndex, RimEclipseCellColors* cellResultColors ) { if ( m_regionCaches.empty() ) return; if ( !cellResultColors ) return; auto* view = cellResultColors->reservoirView(); if ( !view ) return; auto* eclipseCase = view->eclipseCase(); if ( !eclipseCase || !eclipseCase->eclipseCaseData() ) return; const bool noResult = !cellResultColors->hasResult(); const cvf::ScalarMapper* mapper = noResult ? nullptr : cellResultColors->legendConfig()->scalarMapper(); const bool ternary = cellResultColors->isTernarySaturationSelected(); const bool lighting = view->isLightingDisabled(); // Ternary saturations are not supported for the preview; fall back to a flat color so the // user still sees the region clearly. if ( noResult || !mapper || ternary ) { for ( auto& cache : m_regionCaches ) applyFlatColorToFacePart( cache ); return; } cvf::ref accessor = RigResultAccessorFactory::createFromResultDefinition( eclipseCase->eclipseCaseData(), 0, timeStepIndex, cellResultColors ); if ( accessor.isNull() ) { for ( auto& cache : m_regionCaches ) applyFlatColorToFacePart( cache ); return; } for ( auto& cache : m_regionCaches ) { if ( cache.facePart.isNull() || cache.textureCoords.isNull() ) continue; if ( cache.parentGlobalCellIdx.empty() ) continue; const size_t numSubCells = cache.parentGlobalCellIdx.size(); cvf::Vec2f* rawTexCoord = cache.textureCoords->ptr(); for ( size_t s = 0; s < numSubCells; ++s ) { double value = accessor->cellScalar( cache.parentGlobalCellIdx[s] ); cvf::Vec2f texCoord; if ( value == HUGE_VAL || std::isnan( value ) || std::isinf( value ) ) { // y=1.0 targets the "undefined" texel row of the legend texture, rendered in undefColor. texCoord = cvf::Vec2f( 0.0f, 1.0f ); } else { texCoord = mapper->mapToTextureCoord( value ); } const size_t base = s * VERTICES_PER_SUB_CELL; for ( int v = 0; v < VERTICES_PER_SUB_CELL; ++v ) { rawTexCoord[base + v] = texCoord; } } RivScalarMapperUtils::applyTextureResultsToPart( cache.facePart.p(), cache.textureCoords.p(), mapper, 1.0f, caf::FC_NONE, lighting ); } } //-------------------------------------------------------------------------------------------------- /// Fallback for very large regions: an 8-vertex wireframe box at the region's extremes. //-------------------------------------------------------------------------------------------------- cvf::ref RivRefinementRegionPartMgr::createOuterBoxPart( const RimRefinementRegion* region, RimEclipseCase* eclipseCase, const caf::DisplayCoordTransform* coordTransform ) { auto* caseData = eclipseCase->eclipseCaseData(); if ( !caseData ) return nullptr; auto* mainGrid = caseData->mainGrid(); if ( !mainGrid ) return nullptr; const size_t gridI = mainGrid->cellCountI(); const size_t gridJ = mainGrid->cellCountJ(); const size_t gridK = mainGrid->cellCountK(); const auto rmin = region->ijkMin(); const auto rmax = region->ijkMax(); const size_t minI = std::min( rmin.x(), gridI - 1 ); const size_t minJ = std::min( rmin.y(), gridJ - 1 ); const size_t minK = std::min( rmin.z(), gridK - 1 ); const size_t maxI = std::min( rmax.x(), gridI - 1 ); const size_t maxJ = std::min( rmax.y(), gridJ - 1 ); const size_t maxK = std::min( rmax.z(), gridK - 1 ); struct CornerPick { size_t i; size_t j; size_t k; size_t localCorner; }; const std::array picks = { { { minI, minJ, minK, 0 }, { maxI, minJ, minK, 1 }, { maxI, maxJ, minK, 2 }, { minI, maxJ, minK, 3 }, { minI, minJ, maxK, 4 }, { maxI, minJ, maxK, 5 }, { maxI, maxJ, maxK, 6 }, { minI, maxJ, maxK, 7 }, } }; std::vector vertices; vertices.reserve( 8 ); for ( const auto& p : picks ) { size_t cellIdx = mainGrid->cellIndexFromIJK( p.i, p.j, p.k ); auto corners = mainGrid->cellCornerVertices( cellIdx ); auto domain = corners[p.localCorner]; auto display = coordTransform->transformToDisplayCoord( domain ); vertices.push_back( cvf::Vec3f( display ) ); } return RivBoxGeometryGenerator::createBoxFromVertices( vertices, region->previewColor() ); }