From 239c41eeb04d2e4f89b46e3bbeb62a2ff07d4bbb Mon Sep 17 00:00:00 2001 From: Pasukhin Dmitry Date: Fri, 5 Jun 2026 13:45:42 +0100 Subject: [PATCH] Visualization - Fix V3d shader grid issues (#1295) - Updated shader grid rendering to intersect the grid plane in view space and draw it with a single full-screen triangle. - Added shader grid bounds to camera Z-fit, so the grid is visible in empty views and after object display without an extra vfit. - Fixed shader grid echo to use the active shader grid plane, effective scale, bounds and clip-safe display point. - Kept the viewer-managed CPU grid path separate from the per-view shader grid path and preserved CPU grid type/mode handling in vgrid. - Added grid tests for empty/no-vfit visibility and perspective rendering without mirrored fragments. --- .../ViewerTest/ViewerTest_ViewerCommands.cxx | 207 ++-- src/Visualization/TKOpenGl/OpenGl/FILES.cmake | 2 + .../TKOpenGl/OpenGl/OpenGl_ShaderGrid.cxx | 929 ++++++++++++++++++ .../TKOpenGl/OpenGl/OpenGl_ShaderGrid.hxx | 160 +++ .../TKOpenGl/OpenGl/OpenGl_View.cxx | 412 ++------ .../TKOpenGl/OpenGl/OpenGl_View.hxx | 24 +- .../TKService/Aspect/Aspect_GridParams.hxx | 15 +- .../TKService/Graphic3d/Graphic3d_CView.hxx | 45 + .../Graphic3d/Graphic3d_ShaderManager.cxx | 301 +++--- .../TKV3d/AIS/AIS_ViewController.cxx | 68 +- src/Visualization/TKV3d/V3d/V3d_View.cxx | 86 +- src/Visualization/TKV3d/V3d/V3d_View.hxx | 38 +- src/Visualization/TKV3d/V3d/V3d_Viewer.cxx | 4 +- tests/v3d/grid/gpu_perspective_nomirror | 22 + tests/v3d/grid/gpu_visibility | 20 + 15 files changed, 1701 insertions(+), 632 deletions(-) create mode 100644 src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.cxx create mode 100644 src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.hxx create mode 100644 tests/v3d/grid/gpu_perspective_nomirror create mode 100644 tests/v3d/grid/gpu_visibility diff --git a/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx b/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx index 1d7537aefe..3c4da3de04 100644 --- a/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx +++ b/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx @@ -5038,19 +5038,20 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV return 1; } - Aspect_GridType aType = aViewer->GridType(); - Aspect_GridDrawMode aMode = aViewer->GridDrawMode(); + Aspect_GridType aType = Aspect_GT_Rectangular; + Aspect_GridDrawMode aMode = Aspect_GDM_Lines; NCollection_Vec2 aNewOriginXY, aNewStepXY, aNewSizeXY; double aNewRadius = 0.0, aNewRotAngle = 0.0, aNewZOffset = 0.0; double aNewArcStart = 0.0, aNewArcEnd = 0.0; Quantity_Color aNewColor, aNewTenthColor; bool hasOrigin = false, hasStep = false, hasRotAngle = false, hasSize = false, hasRadius = false, - hasZOffset = false; - bool isGpuGrid = false, hasGridOff = false, hasScale = false, hasArc = false; + hasZOffset = false; + bool isShaderGrid = false, hasGridOff = false, hasScale = false, hasArc = false; + bool hasGridType = false, hasGridMode = false; bool hasColor = false, hasTenthColor = false; - // Tracks whether any GPU-grid-only option was passed; used to warn when - // such options are silently ignored on the CPU path. - bool hasGpuOnlyOpt = false; + // Tracks whether any shader-grid-only option was passed; used to warn when + // such options are ignored by the non-shader grid path. + bool hasShaderOnlyOpt = false; Aspect_GridParams aGridParams; ViewerTest_AutoUpdater anUpdateTool(ViewerTest::GetAISContext(), aView); for (int anArgIter = 1; anArgIter < theArgNb; ++anArgIter) @@ -5067,15 +5068,17 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV anArgNext.LowerCase(); if (anArgNext == "r" || anArgNext == "rect" || anArgNext == "rectangular") { - aType = Aspect_GT_Rectangular; + aType = Aspect_GT_Rectangular; + hasGridType = true; } else if (anArgNext == "c" || anArgNext == "circ" || anArgNext == "circular") { - aType = Aspect_GT_Circular; + aType = Aspect_GT_Circular; + hasGridType = true; } else if (anArgNext == "gpu" || anArgNext == "shader") { - isGpuGrid = true; + isShaderGrid = true; } else { @@ -5089,11 +5092,13 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV anArgNext.LowerCase(); if (anArgNext == "l" || anArgNext == "line" || anArgNext == "lines") { - aMode = Aspect_GDM_Lines; + aMode = Aspect_GDM_Lines; + hasGridMode = true; } else if (anArgNext == "p" || anArgNext == "point" || anArgNext == "points") { - aMode = Aspect_GDM_Points; + aMode = Aspect_GDM_Points; + hasGridMode = true; } else { @@ -5158,9 +5163,8 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } else if (anArgIter + 3 < theArgNb && (anArg == "-color")) { - // Colors feed both backends through different sinks: aGridParams here - // for the GPU path, and Aspect_Grid::SetColors at the end of this - // function for the CPU path (so V3d_Viewer::syncViews picks them up). + // Colors feed both implementations through different sinks: aGridParams here + // for the shader path, and Aspect_Grid::SetColors at the end of this function. hasColor = true; aNewColor = Quantity_Color(Draw::Atof(theArgVec[anArgIter + 1]), Draw::Atof(theArgVec[anArgIter + 2]), @@ -5181,36 +5185,36 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } else if (anArgIter + 1 < theArgNb && anArg == "-scale") { - hasScale = true; - hasGpuOnlyOpt = true; + hasScale = true; + hasShaderOnlyOpt = true; aGridParams.SetScale(Draw::Atof(theArgVec[++anArgIter])); } else if (anArgIter + 1 < theArgNb && (anArg == "-linethickness" || anArg == "-thickness")) { - hasGpuOnlyOpt = true; + hasShaderOnlyOpt = true; aGridParams.SetLineThickness(Draw::Atof(theArgVec[++anArgIter])); } else if (anArgIter + 1 < theArgNb && anArg == "-background") { - hasGpuOnlyOpt = true; - const int aVal = Draw::Atoi(theArgVec[++anArgIter]); + hasShaderOnlyOpt = true; + const int aVal = Draw::Atoi(theArgVec[++anArgIter]); aGridParams.SetIsBackground(aVal != 0); } else if (anArgIter + 1 < theArgNb && anArg == "-drawaxis") { - hasGpuOnlyOpt = true; - const int aVal = Draw::Atoi(theArgVec[++anArgIter]); + hasShaderOnlyOpt = true; + const int aVal = Draw::Atoi(theArgVec[++anArgIter]); aGridParams.SetIsDrawAxis(aVal != 0); } else if (anArgIter + 1 < theArgNb && (anArg == "-viewadaptive" || anArg == "-adaptive")) { - hasGpuOnlyOpt = true; - const int aVal = Draw::Atoi(theArgVec[++anArgIter]); + hasShaderOnlyOpt = true; + const int aVal = Draw::Atoi(theArgVec[++anArgIter]); aGridParams.SetIsViewAdaptive(aVal != 0); } else if (anArg == "-gpu" || anArg == "-shader") { - isGpuGrid = true; + isShaderGrid = true; } else if (anArgIter + 2 < theArgNb && anArg == "-arc") { @@ -5223,19 +5227,23 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } else if (anArg == "r" || anArg == "rect" || anArg == "rectangular") { - aType = Aspect_GT_Rectangular; + aType = Aspect_GT_Rectangular; + hasGridType = true; } else if (anArg == "c" || anArg == "circ" || anArg == "circular") { - aType = Aspect_GT_Circular; + aType = Aspect_GT_Circular; + hasGridType = true; } else if (anArg == "l" || anArg == "line" || anArg == "lines") { - aMode = Aspect_GDM_Lines; + aMode = Aspect_GDM_Lines; + hasGridMode = true; } else if (anArg == "p" || anArg == "point" || anArg == "points") { - aMode = Aspect_GDM_Points; + aMode = Aspect_GDM_Points; + hasGridMode = true; } else if (anArg == "off") { @@ -5248,77 +5256,100 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } } - if (isGpuGrid && hasGridOff) + if (isShaderGrid && hasGridOff) { - Message::SendFail("Syntax error: 'off' cannot be combined with GPU grid display"); + Message::SendFail("Syntax error: 'off' cannot be combined with shader grid display"); return 1; } - // GPU-only options (-scale, -lineThickness, -background, -drawAxis, + // Shader-only options (-scale, -lineThickness, -background, -drawAxis, // -viewAdaptive) are stored on Aspect_GridParams and consumed only by the - // shader path. Silently ignoring them on the CPU path leaves the user + // shader path. Silently ignoring them on the non-shader path leaves the user // wondering why nothing changed; warn explicitly. - if (hasGpuOnlyOpt && !isGpuGrid && !hasGridOff) + if (hasShaderOnlyOpt && !isShaderGrid && !hasGridOff) { Message::SendWarning("vgrid: -scale / -lineThickness / -background / -drawAxis / " - "-viewAdaptive are GPU-grid only; pass -gpu (or -type gpu) to apply."); + "-viewAdaptive are shader-grid only; pass -gpu (or -type gpu) to apply."); } - if (isGpuGrid || hasGridOff) + if (!isShaderGrid) + { + if (!hasGridType) + { + aType = aViewer->GridType(); + } + if (!hasGridMode) + { + aMode = aViewer->GridDrawMode(); + } + } + + if (isShaderGrid || hasGridOff) { if (hasGridOff) { aView->GridErase(); } - if (isGpuGrid) + if (isShaderGrid) { - // The shader grid keeps a real Aspect_Grid to back snap selection, so we - // route through ActivateGrid(rect|circ) first and override the display - // with shader-specific params afterwards. Decide the shape from aType - // or from the presence of circular-only options. - const bool isGpuCircular = aType == Aspect_GT_Circular || hasRadius || hasArc; - const double anOrigX = hasOrigin ? aNewOriginXY.x() : 0.0; - const double anOrigY = hasOrigin ? aNewOriginXY.y() : 0.0; - const double aRotAngle = hasRotAngle ? aNewRotAngle : 0.0; + const bool isShaderCircular = aType == Aspect_GT_Circular || hasRadius || hasArc; + NCollection_Vec2 anOrigXY(0.0, 0.0); + double aRotAngle = 0.0; - if (isGpuCircular) + if (isShaderCircular) { - // Radial step + angular divisions. `-step R N` supplies both; otherwise - // use sensible defaults that keep snap consistent with the visible grid. - double aRadiusStep = 1.0; - int aDivisionCount = 16; + double aRadiusStep = 0.0; + int aDivisionCount = 0; + aViewer->CircularGridValues(anOrigXY.x(), + anOrigXY.y(), + aRadiusStep, + aDivisionCount, + aRotAngle); + if (hasOrigin) + { + anOrigXY = aNewOriginXY; + } + if (hasRotAngle) + { + aRotAngle = aNewRotAngle; + } if (hasStep) { aRadiusStep = aNewStepXY.x(); - aDivisionCount = int(aNewStepXY.y() > 0 ? aNewStepXY.y() : aDivisionCount); + aDivisionCount = int(aNewStepXY.y()); } else if (hasScale && aGridParams.Scale() > 0.0) { aRadiusStep = 1.0 / aGridParams.Scale(); } - aViewer->SetCircularGridValues(anOrigX, anOrigY, aRadiusStep, aDivisionCount, aRotAngle); - // Arc range reaches the circular grid base before ActivateGrid's first - // syncViews fires (otherwise syncViews would overwrite our override). - if (hasArc) + if (aRadiusStep <= 0.0) { - if (occ::handle aCircGrid = - occ::down_cast(aViewer->Grid(true))) - { - aCircGrid->SetArcRange(aNewArcStart, aNewArcEnd); - } + aRadiusStep = 1.0; + } + if (aDivisionCount <= 0) + { + aDivisionCount = 16; } - aViewer->ActivateGrid(Aspect_GT_Circular, aMode); - aGridParams.SetScale(1.0 / aRadiusStep); - aGridParams.SetScaleY(0.0); // unused in circular mode + aGridParams.SetScaleY(0.0); aGridParams.SetAngularDivisions(aDivisionCount); } else { - // Rectangular shader grid. Derive step from explicit -step or from the - // Aspect_GridParams scale; fall back to 1 world unit so the default is - // immediately useful (Aspect_GridParams::Scale defaults to 0.01 which - // would give step 100 - too coarse for typical scenes). + NCollection_Vec2 aStepXY(1.0, 1.0); + aViewer->RectangularGridValues(anOrigXY.x(), + anOrigXY.y(), + aStepXY.x(), + aStepXY.y(), + aRotAngle); + if (hasOrigin) + { + anOrigXY = aNewOriginXY; + } + if (hasRotAngle) + { + aRotAngle = aNewRotAngle; + } if (!hasScale) { if (hasStep) @@ -5328,27 +5359,17 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } else { - aGridParams.SetScale(1.0); - aGridParams.SetScaleY(1.0); + aGridParams.SetScale(aStepXY.x() > 0.0 ? 1.0 / aStepXY.x() : 1.0); + aGridParams.SetScaleY(aStepXY.y() > 0.0 ? 1.0 / aStepXY.y() : 1.0); } } - const double aInfStepX = 1.0 / aGridParams.Scale(); - const double aInfStepY = 1.0 / aGridParams.EffectiveScaleY(); - aViewer->SetRectangularGridValues(anOrigX, anOrigY, aInfStepX, aInfStepY, aRotAngle); - aViewer->ActivateGrid(Aspect_GT_Rectangular, aMode); - aGridParams.SetAngularDivisions(0); } - // Convert origin to the same world-offset convention used by V3d - // syncViews so the shader's aPlaneOrigin matches snap's aPnt0. - const gp_Ax3 aPlane = aViewer->PrivilegedPlane(); - const gp_XYZ aOriginOffset = - aPlane.XDirection().XYZ() * -anOrigX + aPlane.YDirection().XYZ() * -anOrigY; - aGridParams.SetOrigin(gp_Pnt(aOriginOffset)); + aGridParams.SetOrigin(gp_Pnt(-anOrigXY.x(), -anOrigXY.y(), 0.0)); aGridParams.SetDrawMode(aMode); aGridParams.SetRotationAngle(aRotAngle); - if (hasSize && !isGpuCircular) + if (hasSize && !isShaderCircular) { aGridParams.SetSizeX(aNewSizeXY.x()); aGridParams.SetSizeY(aNewSizeXY.y()); @@ -5363,9 +5384,9 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } aView->GridDisplay(aGridParams); } - if (hasGridOff && !isGpuGrid) + if (hasGridOff && !isShaderGrid) { - // plain 'vgrid off' still deactivates the classical grid + // Plain 'vgrid off' still deactivates the viewer-managed grid. aViewer->DeactivateGrid(); } return 0; @@ -5476,7 +5497,7 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } } // Apply -color / -tenthColor to the active grid so V3d syncViews picks - // them up on the next display. GPU-grid display drives these through + // them up on the next display. Shader-grid display drives these through // aGridParams directly (set earlier in the parser loop). if (hasColor || hasTenthColor) { @@ -6581,8 +6602,7 @@ static int VMoveTo(Draw_Interpretor& theDI, int theNbArgs, const char** theArgVe return 1; } - const bool toEchoGrid = - aContext->CurrentViewer()->IsGridActive() && aContext->CurrentViewer()->GridEcho(); + const bool toEchoGrid = aView->IsGridActive() && aContext->CurrentViewer()->GridEcho(); if (toEchoGrid) { aContext->CurrentViewer()->HideGridEcho(aView); @@ -14284,17 +14304,14 @@ vgrid [off] [-type {rect|circ|gpu|shader}] [-gpu] [-mode {line|point}] [-origin [-step StepRadius NbDivisions] [-radius Radius] [-arc AngleStart AngleEnd] [-color R G B] [-tenthColor R G B] [-scale N] [-lineThickness T] [-background {0|1}] [-drawAxis {0|1}] [-viewAdaptive {0|1}] -Two render backends are available: - - '-type rect' / '-type circ' (default) draws the legacy CPU grid into a - bounded Graphic3d_Structure shared by every active view. - - '-type gpu' / '-gpu' activates the shader-rendered grid on the active view. +Two grid implementations are available: + - '-type rect' / '-type circ' draws the viewer grid shared by every active view. + - '-type gpu' / '-gpu' activates the shader grid on the active view. Without '-size' or '-radius' it is unbounded; combine with '-size DX DY' for a rectangle, '-radius R' for a disc, '-arc S E' for a wedge. -'-color', '-tenthColor' apply to both backends. '-scale', '-lineThickness', -'-background', '-drawAxis', '-viewAdaptive {0|1}' are GPU-grid only. Switching -to GPU-grid display hides the CPU grid in the viewer; switching back to a CPU -grid type erases the shader grid in the active view. 'off' deactivates whichever -grid is currently visible. +'-color', '-tenthColor' apply to both implementations. '-scale', +'-lineThickness', '-background', '-drawAxis', '-viewAdaptive {0|1}' are +shader-grid only. 'off' deactivates the active grid in the current view/viewer. )" /* [vgrid] */); addCmd("vpriviledgedplane", VPriviledgedPlane, /* [vpriviledgedplane] */ R"( diff --git a/src/Visualization/TKOpenGl/OpenGl/FILES.cmake b/src/Visualization/TKOpenGl/OpenGl/FILES.cmake index 822da733c8..e6e2477acf 100644 --- a/src/Visualization/TKOpenGl/OpenGl/FILES.cmake +++ b/src/Visualization/TKOpenGl/OpenGl/FILES.cmake @@ -27,6 +27,8 @@ set(OCCT_OpenGl_FILES OpenGl_FrameStatsPrs.hxx OpenGl_Group.hxx OpenGl_Group.cxx + OpenGl_ShaderGrid.hxx + OpenGl_ShaderGrid.cxx OpenGl_Structure.hxx OpenGl_Structure.cxx OpenGl_StructureShadow.hxx diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.cxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.cxx new file mode 100644 index 0000000000..f2c3655b5a --- /dev/null +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.cxx @@ -0,0 +1,929 @@ +// Copyright (c) 2026 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include + +#include +#include + +namespace +{ +static constexpr double THE_TWO_PI = 2.0 * M_PI; + +static bool isFiniteCoord(const double theValue) +{ + return std::isfinite(theValue) && !Precision::IsInfinite(theValue); +} + +static bool isFinitePoint(const gp_Pnt& thePoint) +{ + return isFiniteCoord(thePoint.X()) && isFiniteCoord(thePoint.Y()) && isFiniteCoord(thePoint.Z()); +} + +static bool isFinitePoint(const gp_XYZ& thePoint) +{ + return isFiniteCoord(thePoint.X()) && isFiniteCoord(thePoint.Y()) && isFiniteCoord(thePoint.Z()); +} + +static double normalizedAngle(const double theAngle) +{ + double anAngle = std::fmod(theAngle, THE_TWO_PI); + if (anAngle < 0.0) + { + anAngle += THE_TWO_PI; + } + return anAngle; +} + +static double positiveAngleSpan(const double theStart, const double theEnd) +{ + double aSpan = std::fmod(theEnd - theStart, THE_TWO_PI); + if (aSpan < 0.0) + { + aSpan += THE_TWO_PI; + } + + if (std::abs(aSpan) <= Precision::Angular() + && std::abs(theEnd - theStart) >= THE_TWO_PI - Precision::Angular()) + { + return THE_TWO_PI; + } + return aSpan; +} + +static bool isSamePoint(const gp_Pnt& theFirst, const gp_Pnt& theSecond) +{ + return theFirst.SquareDistance(theSecond) <= Precision::SquareConfusion(); +} + +static bool isSameDirection(const gp_Dir& theFirst, const gp_Dir& theSecond) +{ + return theFirst.Angle(theSecond) <= Precision::Angular(); +} + +static bool isSameScalar(const double theFirst, const double theSecond) +{ + return std::abs(theFirst - theSecond) <= Precision::Confusion(); +} + +static bool isSameAngle(const double theFirst, const double theSecond) +{ + double aDelta = std::abs(normalizedAngle(theFirst) - normalizedAngle(theSecond)); + aDelta = std::min(aDelta, THE_TWO_PI - aDelta); + return aDelta <= Precision::Angular(); +} +} // namespace + +//================================================================================================= + +bool OpenGl_ShaderGrid::Display(const Aspect_GridParams& theParams, + const gp_Ax3& thePlane, + const occ::handle& theCamera, + const occ::handle& theContext) +{ + if (theParams.DrawMode() == Aspect_GDM_None) + { + Erase(); + return true; + } + + const bool wasShown = myIsShown; + const bool wasBackground = wasShown && myParams.IsBackground(); + const bool hasSameAnchor = wasShown && hasSameAnchorFrame(theParams, thePlane); + const bool toCaptureFrame = theParams.IsBackground() && !theContext.IsNull() + && (!wasShown || !wasBackground || !hasSameAnchor); + + myParams = theParams; + myPlane = thePlane; + myIsShown = true; + + if (theParams.IsViewAdaptive()) + { + const double aReferenceScale = + !theCamera.IsNull() && theCamera->Scale() > Precision::Confusion() ? theCamera->Scale() : 1.0; + myParams.SetScale(theParams.Scale() * aReferenceScale); + if (theParams.ScaleY() > 0.0) + { + myParams.SetScaleY(theParams.ScaleY() * aReferenceScale); + } + } + + if (toCaptureFrame) + { + myRefViewMatrix = theContext->WorldViewState.Current(); + } + return true; +} + +//================================================================================================= + +void OpenGl_ShaderGrid::Erase() +{ + myIsShown = false; +} + +//================================================================================================= + +void OpenGl_ShaderGrid::frame(gp_Pnt& theOrigin, gp_XYZ& theX, gp_XYZ& theY, gp_XYZ& theN) const +{ + const double aCosA = std::cos(myParams.RotationAngle()); + const double aSinA = std::sin(myParams.RotationAngle()); + const gp_XYZ aRawX = myPlane.XDirection().XYZ(); + const gp_XYZ aRawY = myPlane.YDirection().XYZ(); + theX = aRawX * aCosA - aRawY * aSinA; + theY = aRawX * aSinA + aRawY * aCosA; + theN = myPlane.Direction().XYZ(); + + const gp_Pnt& anOriginLocal = myParams.Origin(); + theOrigin.SetXYZ(myPlane.Location().XYZ() + aRawX * anOriginLocal.X() + aRawY * anOriginLocal.Y() + + theN * (anOriginLocal.Z() + myParams.ZOffset())); +} + +//================================================================================================= + +void OpenGl_ShaderGrid::effectiveScale(const occ::handle& theCamera, + double& theScaleX, + double& theScaleY) const +{ + theScaleX = myParams.Scale(); + theScaleY = myParams.EffectiveScaleY(); + if (!myParams.IsViewAdaptive()) + { + return; + } + + const double aCurrentScale = !theCamera.IsNull() && theCamera->Scale() > Precision::Confusion() + ? theCamera->Scale() + : Precision::Confusion(); + theScaleX /= aCurrentScale; + theScaleY /= aCurrentScale; +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::angleInArc(const double theStart, + const double theEnd, + const double theAngle) +{ + const double aSpan = positiveAngleSpan(theStart, theEnd); + if (aSpan >= THE_TWO_PI - Precision::Angular()) + { + return true; + } + + double aDelta = normalizedAngle(theAngle) - normalizedAngle(theStart); + if (aDelta < 0.0) + { + aDelta += THE_TWO_PI; + } + return aDelta <= aSpan + Precision::Angular(); +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::isPointInBounds(const double theLocalX, const double theLocalY) const +{ + if (myParams.IsCircular()) + { + const double aRadius = std::sqrt(theLocalX * theLocalX + theLocalY * theLocalY); + if (myParams.Radius() > 0.0 && aRadius > myParams.Radius()) + { + return false; + } + if (myParams.IsArc()) + { + if (!angleInArc(myParams.AngleStart(), myParams.AngleEnd(), std::atan2(theLocalY, theLocalX))) + { + return false; + } + } + return true; + } + + return (myParams.SizeX() <= 0.0 || std::abs(theLocalX) <= myParams.SizeX() * 0.5) + && (myParams.SizeY() <= 0.0 || std::abs(theLocalY) <= myParams.SizeY() * 0.5); +} + +//================================================================================================= + +void OpenGl_ShaderGrid::addLocalPoint(Bnd_Box& theBox, + const gp_Pnt& theOrigin, + const gp_XYZ& theX, + const gp_XYZ& theY, + const double theLocalX, + const double theLocalY) +{ + const gp_Pnt aPoint(theOrigin.XYZ() + theX * theLocalX + theY * theLocalY); + if (isFinitePoint(aPoint)) + { + theBox.Add(aPoint); + } +} + +//================================================================================================= + +void OpenGl_ShaderGrid::addFiniteBounds(Bnd_Box& theBox) const +{ + gp_Pnt anOrigin; + gp_XYZ aGridX, aGridY, aGridN; + frame(anOrigin, aGridX, aGridY, aGridN); + + addLocalPoint(theBox, anOrigin, aGridX, aGridY, 0.0, 0.0); + if (myParams.IsCircular()) + { + const double aRadius = myParams.Radius(); + if (aRadius <= 0.0) + { + return; + } + + if (!myParams.IsArc()) + { + addLocalPoint(theBox, anOrigin, aGridX, aGridY, aRadius, 0.0); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, -aRadius, 0.0); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, 0.0, aRadius); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, 0.0, -aRadius); + return; + } + + auto addArcPoint = [&](const double theAngle) { + addLocalPoint(theBox, + anOrigin, + aGridX, + aGridY, + aRadius * std::cos(theAngle), + aRadius * std::sin(theAngle)); + }; + + addArcPoint(myParams.AngleStart()); + addArcPoint(myParams.AngleEnd()); + + const double aCardinalAngles[] = {0.0, M_PI * 0.5, M_PI, -M_PI * 0.5}; + for (const double anAngle : aCardinalAngles) + { + if (angleInArc(myParams.AngleStart(), myParams.AngleEnd(), anAngle)) + { + addArcPoint(anAngle); + } + } + return; + } + + const double aHalfX = myParams.SizeX() > 0.0 ? myParams.SizeX() * 0.5 : 0.0; + const double aHalfY = myParams.SizeY() > 0.0 ? myParams.SizeY() * 0.5 : 0.0; + if (aHalfX <= 0.0 && aHalfY <= 0.0) + { + return; + } + + if (aHalfX > 0.0 && aHalfY > 0.0) + { + addLocalPoint(theBox, anOrigin, aGridX, aGridY, -aHalfX, -aHalfY); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, aHalfX, -aHalfY); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, aHalfX, aHalfY); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, -aHalfX, aHalfY); + return; + } + + if (aHalfX > 0.0) + { + addLocalPoint(theBox, anOrigin, aGridX, aGridY, -aHalfX, 0.0); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, aHalfX, 0.0); + } + if (aHalfY > 0.0) + { + addLocalPoint(theBox, anOrigin, aGridX, aGridY, 0.0, -aHalfY); + addLocalPoint(theBox, anOrigin, aGridX, aGridY, 0.0, aHalfY); + } +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::planeLocalHit(const occ::handle& theCamera, + const double theNdcX, + const double theNdcY, + double& theLocalX, + double& theLocalY, + gp_XYZ* theHit, + const bool theToRejectBehind) const +{ + if (theCamera.IsNull()) + { + return false; + } + + gp_Pnt aPlaneOrigin; + gp_XYZ aPlaneX, aPlaneY, aPlaneN; + frame(aPlaneOrigin, aPlaneX, aPlaneY, aPlaneN); + + const double aNearZ = theCamera->IsZeroToOneDepth() ? 0.0 : -1.0; + const gp_Pnt aNearP = theCamera->UnProject(gp_Pnt(theNdcX, theNdcY, aNearZ)); + const gp_Pnt aFarP = theCamera->UnProject(gp_Pnt(theNdcX, theNdcY, 1.0)); + const bool isPerspective = !theCamera->IsOrthographic(); + const gp_Pnt aRayOriginP = isPerspective ? theCamera->Eye() : aNearP; + const gp_XYZ aRay = aFarP.XYZ() - aRayOriginP.XYZ(); + const double aRayMod = aRay.Modulus(); + if (aRayMod <= Precision::Confusion()) + { + return false; + } + + const double aDenom = aPlaneN.Dot(aRay); + if (std::abs(aDenom / aRayMod) <= Precision::Angular()) + { + return false; + } + + const double aT = aPlaneN.Dot(aPlaneOrigin.XYZ() - aRayOriginP.XYZ()) / aDenom; + if (theToRejectBehind && isPerspective && aT < 0.0) + { + return false; + } + + const gp_XYZ aHit = aRayOriginP.XYZ() + aRay * aT; + const gp_XYZ aLocal3 = aHit - aPlaneOrigin.XYZ(); + theLocalX = aLocal3.Dot(aPlaneX); + theLocalY = aLocal3.Dot(aPlaneY); + if (theHit != nullptr) + { + *theHit = aHit; + } + return true; +} + +//================================================================================================= + +void OpenGl_ShaderGrid::addViewFootprintBounds(Bnd_Box& theBox, + const occ::handle& theCamera) const +{ + const double aSamples[][2] = {{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}, {0.0, 0.0}}; + for (const double* aSample : aSamples) + { + double aLocalX = 0.0; + double aLocalY = 0.0; + gp_XYZ aHit; + if (!planeLocalHit(theCamera, aSample[0], aSample[1], aLocalX, aLocalY, &aHit)) + { + continue; + } + if (!isPointInBounds(aLocalX, aLocalY)) + { + continue; + } + if (isFinitePoint(aHit)) + { + theBox.Add(gp_Pnt(aHit)); + } + } +} + +//================================================================================================= + +void OpenGl_ShaderGrid::AddZFitBounds(Bnd_Box& theGraphicBox, + const occ::handle& theCamera) const +{ + if (!myIsShown || myParams.DrawMode() == Aspect_GDM_None || myParams.IsBackground()) + { + return; + } + + Bnd_Box aGridBox; + if (myParams.IsBounded()) + { + addFiniteBounds(aGridBox); + } + else + { + addViewFootprintBounds(aGridBox, theCamera); + } + + if (aGridBox.IsVoid()) + { + addFiniteBounds(aGridBox); + } + if (!aGridBox.IsVoid()) + { + theGraphicBox.Add(aGridBox); + } +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::hasSameAnchorFrame(const Aspect_GridParams& theParams, + const gp_Ax3& thePlane) const +{ + return isSamePoint(myPlane.Location(), thePlane.Location()) + && isSameDirection(myPlane.Direction(), thePlane.Direction()) + && isSameDirection(myPlane.XDirection(), thePlane.XDirection()) + && isSameDirection(myPlane.YDirection(), thePlane.YDirection()) + && isSamePoint(myParams.Origin(), theParams.Origin()) + && isSameAngle(myParams.RotationAngle(), theParams.RotationAngle()) + && isSameScalar(myParams.ZOffset(), theParams.ZOffset()); +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::referenceLocal(const occ::handle& theCamera, + double& theLocalX, + double& theLocalY) const +{ + const double aSamples[][2] = {{0.0, 0.0}, + {-1.0, -1.0}, + {1.0, -1.0}, + {1.0, 1.0}, + {-1.0, 1.0}, + {0.0, -1.0}, + {1.0, 0.0}, + {0.0, 1.0}, + {-1.0, 0.0}}; + for (const double* aSample : aSamples) + { + if (planeLocalHit(theCamera, aSample[0], aSample[1], theLocalX, theLocalY)) + { + return true; + } + } + return false; +} + +//================================================================================================= + +double OpenGl_ShaderGrid::snappedLocalShift(const double theLocal, const double theScale) +{ + if (theScale <= Precision::Confusion()) + { + return 0.0; + } + + const double aStep = 1.0 / theScale; + return std::round(theLocal / aStep) * aStep; +} + +//================================================================================================= + +NCollection_Vec3 OpenGl_ShaderGrid::viewPoint(const NCollection_Mat4& theWorldView, + const gp_Pnt& thePoint) +{ + const NCollection_Vec4 aPoint(float(thePoint.X()), + float(thePoint.Y()), + float(thePoint.Z()), + 1.0f); + const NCollection_Vec4 aView = theWorldView * aPoint; + return NCollection_Vec3(aView.x(), aView.y(), aView.z()); +} + +//================================================================================================= + +NCollection_Vec3 OpenGl_ShaderGrid::viewDirection( + const NCollection_Mat4& theWorldView, + const gp_XYZ& theDirection) +{ + const NCollection_Vec4 aDir(float(theDirection.X()), + float(theDirection.Y()), + float(theDirection.Z()), + 0.0f); + const NCollection_Vec4 aView = theWorldView * aDir; + return NCollection_Vec3(aView.x(), aView.y(), aView.z()); +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::acceptEchoCandidate(const occ::handle& theCamera, + const int theWidth, + const int theHeight, + const int theX, + const int theY, + const gp_Pnt& theGridOrigin, + const gp_XYZ& theGridX, + const gp_XYZ& theGridY, + const double theLocalX, + const double theLocalY, + gp_XYZ& theBestSnapped, + double& theBestDist2, + bool& theHasBestPoint) const +{ + if (!isPointInBounds(theLocalX, theLocalY)) + { + return false; + } + + const gp_XYZ aCandidate = theGridOrigin.XYZ() + theGridX * theLocalX + theGridY * theLocalY; + if (!isFinitePoint(aCandidate)) + { + return false; + } + if (!theCamera->IsOrthographic() + && theCamera->Direction().XYZ().Dot(aCandidate - theCamera->Eye().XYZ()) + <= Precision::Confusion()) + { + return false; + } + + const gp_Pnt aProj = theCamera->Project(gp_Pnt(aCandidate)); + if (!isFinitePoint(aProj)) + { + return false; + } + + const double aPx = (aProj.X() + 1.0) * 0.5 * double(theWidth); + const double aPy = double(theHeight - 1) - (aProj.Y() + 1.0) * 0.5 * double(theHeight); + const double aDistX = aPx - double(theX); + const double aDistY = aPy - double(theY); + if (!isFiniteCoord(aDistX) || !isFiniteCoord(aDistY)) + { + return false; + } + + const double aDist2 = aDistX * aDistX + aDistY * aDistY; + if (!isFiniteCoord(aDist2)) + { + return false; + } + if (!theHasBestPoint || aDist2 < theBestDist2) + { + theHasBestPoint = true; + theBestDist2 = aDist2; + theBestSnapped = aCandidate; + } + return true; +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::echoDisplayPoint(const occ::handle& theCamera, + const gp_XYZ& theSnapped, + gp_Pnt& theDisplayPoint) +{ + const gp_Pnt aProjSnapped = theCamera->Project(gp_Pnt(theSnapped)); + if (!isFinitePoint(aProjSnapped)) + { + return false; + } + const double aDisplayZ = theCamera->IsZeroToOneDepth() ? 0.5 : 0.0; + theDisplayPoint = theCamera->UnProject(gp_Pnt(aProjSnapped.X(), aProjSnapped.Y(), aDisplayZ)); + return isFinitePoint(theDisplayPoint); +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::Echo(const occ::handle& theCamera, + const int theWidth, + const int theHeight, + const int theX, + const int theY, + Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theDisplayPoint) const +{ + if (!myIsShown || myParams.DrawMode() == Aspect_GDM_None || myParams.IsBackground() + || theCamera.IsNull() || theWidth <= 0 || theHeight <= 0) + { + return false; + } + + const double aNdcX = 2.0 * double(theX) / double(theWidth) - 1.0; + const double aNdcY = 2.0 * double(theHeight - 1 - theY) / double(theHeight) - 1.0; + double aLocalX = 0.0; + double aLocalY = 0.0; + if (!planeLocalHit(theCamera, aNdcX, aNdcY, aLocalX, aLocalY)) + { + return false; + } + if (!isPointInBounds(aLocalX, aLocalY)) + { + return false; + } + + double aScaleX = 0.0; + double aScaleY = 0.0; + effectiveScale(theCamera, aScaleX, aScaleY); + if (aScaleX <= Precision::Confusion() || aScaleY <= Precision::Confusion()) + { + return false; + } + + gp_Pnt aGridOrigin; + gp_XYZ aGridX, aGridY, aGridN; + frame(aGridOrigin, aGridX, aGridY, aGridN); + + gp_XYZ aSnapped = aGridOrigin.XYZ(); + const bool toSnapByScreen = myParams.DrawMode() == Aspect_GDM_Points; + double aBestDist2 = RealLast(); + bool hasBestPoint = false; + auto addCandidate = [&](const double theLocalX, const double theLocalY) { + acceptEchoCandidate(theCamera, + theWidth, + theHeight, + theX, + theY, + aGridOrigin, + aGridX, + aGridY, + theLocalX, + theLocalY, + aSnapped, + aBestDist2, + hasBestPoint); + }; + + if (myParams.IsCircular()) + { + const double aRadius = std::sqrt(aLocalX * aLocalX + aLocalY * aLocalY); + const double anAngle = std::atan2(aLocalY, aLocalX); + const double aRadiusStep = 1.0 / aScaleX; + const int aNbDivisions = myParams.AngularDivisions(); + const double anAngleStep = aNbDivisions > 0 ? M_PI / double(aNbDivisions) : M_PI; + if (toSnapByScreen) + { + const double aRadiusIndex = std::floor(aRadius / aRadiusStep); + const double anAngleIndex = std::floor(anAngle / anAngleStep); + const double aRadii[2] = {std::max(0.0, aRadiusIndex * aRadiusStep), + std::max(0.0, (aRadiusIndex + 1.0) * aRadiusStep)}; + const double anAngles[2] = {anAngleIndex * anAngleStep, (anAngleIndex + 1.0) * anAngleStep}; + for (double aCandidateRadius : aRadii) + { + for (double aCandidateAngle : anAngles) + { + addCandidate(aCandidateRadius * std::cos(aCandidateAngle), + aCandidateRadius * std::sin(aCandidateAngle)); + } + } + if (!hasBestPoint) + { + return false; + } + } + else + { + const double aSnapRadius = std::round(aRadius / aRadiusStep) * aRadiusStep; + const double aSnapAngle = std::round(anAngle / anAngleStep) * anAngleStep; + if (!acceptEchoCandidate(theCamera, + theWidth, + theHeight, + theX, + theY, + aGridOrigin, + aGridX, + aGridY, + aSnapRadius * std::cos(aSnapAngle), + aSnapRadius * std::sin(aSnapAngle), + aSnapped, + aBestDist2, + hasBestPoint)) + { + return false; + } + } + } + else + { + const double aStepX = 1.0 / aScaleX; + const double aStepY = 1.0 / aScaleY; + if (toSnapByScreen) + { + const double anIndexX = std::floor(aLocalX / aStepX); + const double anIndexY = std::floor(aLocalY / aStepY); + const double aLocalXs[2] = {anIndexX * aStepX, (anIndexX + 1.0) * aStepX}; + const double aLocalYs[2] = {anIndexY * aStepY, (anIndexY + 1.0) * aStepY}; + for (double aCandidateX : aLocalXs) + { + for (double aCandidateY : aLocalYs) + { + addCandidate(aCandidateX, aCandidateY); + } + } + if (!hasBestPoint) + { + return false; + } + } + else + { + aLocalX = std::round(aLocalX / aStepX) * aStepX; + aLocalY = std::round(aLocalY / aStepY) * aStepY; + if (!acceptEchoCandidate(theCamera, + theWidth, + theHeight, + theX, + theY, + aGridOrigin, + aGridX, + aGridY, + aLocalX, + aLocalY, + aSnapped, + aBestDist2, + hasBestPoint)) + { + return false; + } + } + } + + if (!hasBestPoint) + { + return false; + } + + thePoint.SetCoord(aSnapped.X(), aSnapped.Y(), aSnapped.Z()); + gp_Pnt aDisplayP; + if (!echoDisplayPoint(theCamera, aSnapped, aDisplayP)) + { + return false; + } + theDisplayPoint.SetCoord(aDisplayP.X(), aDisplayP.Y(), aDisplayP.Z()); + return true; +} + +//================================================================================================= + +bool OpenGl_ShaderGrid::SnapPoint(const occ::handle& theCamera, + const Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theGridPoint) const +{ + if (!myIsShown || myParams.DrawMode() == Aspect_GDM_None || myParams.IsBackground()) + { + return false; + } + + double aScaleX = 0.0; + double aScaleY = 0.0; + effectiveScale(theCamera, aScaleX, aScaleY); + if (aScaleX <= Precision::Confusion() || aScaleY <= Precision::Confusion()) + { + return false; + } + + gp_Pnt aGridOrigin; + gp_XYZ aGridX, aGridY, aGridN; + frame(aGridOrigin, aGridX, aGridY, aGridN); + + const gp_XYZ aPoint(thePoint.X(), thePoint.Y(), thePoint.Z()); + const gp_XYZ aLocal3 = aPoint - aGridOrigin.XYZ(); + double aLocalX = aLocal3.Dot(aGridX); + double aLocalY = aLocal3.Dot(aGridY); + + gp_XYZ aSnapped = aGridOrigin.XYZ(); + if (myParams.IsCircular()) + { + const double aRadius = std::sqrt(aLocalX * aLocalX + aLocalY * aLocalY); + const double anAngle = std::atan2(aLocalY, aLocalX); + const double aRadiusStep = 1.0 / aScaleX; + const int aNbDivisions = myParams.AngularDivisions(); + const double anAngleStep = aNbDivisions > 0 ? M_PI / double(aNbDivisions) : M_PI; + const double aSnapRadius = std::round(aRadius / aRadiusStep) * aRadiusStep; + const double aSnapAngle = std::round(anAngle / anAngleStep) * anAngleStep; + aLocalX = aSnapRadius * std::cos(aSnapAngle); + aLocalY = aSnapRadius * std::sin(aSnapAngle); + if (!isPointInBounds(aLocalX, aLocalY)) + { + return false; + } + aSnapped += aGridX * aLocalX + aGridY * aLocalY; + } + else + { + const double aStepX = 1.0 / aScaleX; + const double aStepY = 1.0 / aScaleY; + aLocalX = std::round(aLocalX / aStepX) * aStepX; + aLocalY = std::round(aLocalY / aStepY) * aStepY; + if (!isPointInBounds(aLocalX, aLocalY)) + { + return false; + } + aSnapped += aGridX * aLocalX + aGridY * aLocalY; + } + + theGridPoint.SetCoord(aSnapped.X(), aSnapped.Y(), aSnapped.Z()); + return true; +} + +//================================================================================================= + +NCollection_Mat4 OpenGl_ShaderGrid::DrawWorldView( + const NCollection_Mat4& theCurrentWorldView) const +{ + if (!myParams.IsBackground()) + { + return theCurrentWorldView; + } + + NCollection_Mat4 aRefInv; + if (!myRefViewMatrix.Inverted(aRefInv)) + { + aRefInv.InitIdentity(); + } + return theCurrentWorldView * aRefInv; +} + +//================================================================================================= + +void OpenGl_ShaderGrid::SetUniforms(const occ::handle& theContext, + const occ::handle& theProgram, + const occ::handle& theCamera, + const NCollection_Mat4& theWorldView) const +{ + double aScaleX = 0.0; + double aScaleY = 0.0; + effectiveScale(theCamera, aScaleX, aScaleY); + + gp_Pnt aPlaneOrigin; + gp_XYZ aXRotated, aYRotated, aNDir; + frame(aPlaneOrigin, aXRotated, aYRotated, aNDir); + + theProgram->SetUniform(theContext, "uScaleX", GLfloat(aScaleX)); + theProgram->SetUniform(theContext, "uScaleY", GLfloat(aScaleY)); + theProgram->SetUniform(theContext, "uThickness", GLfloat(myParams.LineThickness())); + theProgram->SetUniform(theContext, + "uColor", + NCollection_Vec3((float)myParams.Color().Red(), + (float)myParams.Color().Green(), + (float)myParams.Color().Blue())); + theProgram->SetUniform(theContext, + "uAccentColor", + NCollection_Vec3((float)myParams.AccentColor().Red(), + (float)myParams.AccentColor().Green(), + (float)myParams.AccentColor().Blue())); + theProgram->SetUniform(theContext, "uAccentScaleX", GLfloat(myParams.AccentScaleX())); + theProgram->SetUniform(theContext, "uAccentScaleY", GLfloat(myParams.AccentScaleY())); + theProgram->SetUniform(theContext, "uAccentAngularScale", GLfloat(myParams.AccentAngularScale())); + theProgram->SetUniform(theContext, "uIsDrawAxis", myParams.IsDrawAxis() ? 1 : 0); + theProgram->SetUniform(theContext, "uGridType", myParams.IsCircular() ? 1 : 0); + theProgram->SetUniform(theContext, "uIsBackground", myParams.IsBackground() ? 1 : 0); + + const double aAngularScale = + myParams.IsCircular() ? double(myParams.AngularDivisions()) / M_PI : 0.0; + theProgram->SetUniform(theContext, "uAngularScale", GLfloat(aAngularScale)); + theProgram->SetUniform(theContext, "uDrawMode", myParams.DrawMode() == Aspect_GDM_Points ? 1 : 0); + theProgram->SetUniform(theContext, "uIsPerspective", theCamera->IsOrthographic() ? 0 : 1); + theProgram->SetUniform(theContext, "uIsZeroToOneDepth", theCamera->IsZeroToOneDepth() ? 1 : 0); + theProgram->SetUniform(theContext, "uParallelTolerance", GLfloat(Precision::Angular())); + + NCollection_Vec2 aLocalOriginShift(0.0f, 0.0f); + NCollection_Vec2 anAccentLocalOriginShift(0.0f, 0.0f); + float aRadialOriginShift = 0.0f; + float anAccentRadialOriginShift = 0.0f; + double aLocalRefX = 0.0; + double aLocalRefY = 0.0; + if (referenceLocal(theCamera, aLocalRefX, aLocalRefY)) + { + if (myParams.IsCircular()) + { + const double aRadiusRef = std::sqrt(aLocalRefX * aLocalRefX + aLocalRefY * aLocalRefY); + aRadialOriginShift = float(snappedLocalShift(aRadiusRef, aScaleX)); + anAccentRadialOriginShift = myParams.AccentScaleX() > Precision::Confusion() + ? float(snappedLocalShift(aRadiusRef, myParams.AccentScaleX())) + : aRadialOriginShift; + } + else + { + aLocalOriginShift.SetValues(float(snappedLocalShift(aLocalRefX, aScaleX)), + float(snappedLocalShift(aLocalRefY, aScaleY))); + anAccentLocalOriginShift.SetValues( + myParams.AccentScaleX() > Precision::Confusion() + ? float(snappedLocalShift(aLocalRefX, myParams.AccentScaleX())) + : aLocalOriginShift.x(), + myParams.AccentScaleY() > Precision::Confusion() + ? float(snappedLocalShift(aLocalRefY, myParams.AccentScaleY())) + : aLocalOriginShift.y()); + } + } + + const gp_Pnt aPlaneRef(aPlaneOrigin.XYZ() + aXRotated * double(aLocalOriginShift.x()) + + aYRotated * double(aLocalOriginShift.y())); + theProgram->SetUniform(theContext, "uPlaneOriginView", viewPoint(theWorldView, aPlaneOrigin)); + theProgram->SetUniform(theContext, "uPlaneRefView", viewPoint(theWorldView, aPlaneRef)); + theProgram->SetUniform(theContext, "uPlaneXView", viewDirection(theWorldView, aXRotated)); + theProgram->SetUniform(theContext, "uPlaneYView", viewDirection(theWorldView, aYRotated)); + theProgram->SetUniform(theContext, "uPlaneNView", viewDirection(theWorldView, aNDir)); + theProgram->SetUniform(theContext, "uLocalOriginShift", aLocalOriginShift); + theProgram->SetUniform(theContext, "uAccentLocalOriginShift", anAccentLocalOriginShift); + theProgram->SetUniform(theContext, "uRadialOriginShift", GLfloat(aRadialOriginShift)); + theProgram->SetUniform(theContext, + "uAccentRadialOriginShift", + GLfloat(anAccentRadialOriginShift)); + + const float aHalfX = myParams.SizeX() > 0.0 ? float(myParams.SizeX() * 0.5) : 0.0f; + const float aHalfY = myParams.SizeY() > 0.0 ? float(myParams.SizeY() * 0.5) : 0.0f; + const float aRadius = myParams.Radius() > 0.0 ? float(myParams.Radius()) : 0.0f; + theProgram->SetUniform(theContext, "uBounds", NCollection_Vec3(aHalfX, aHalfY, aRadius)); + theProgram->SetUniform(theContext, + "uIsBoundFade", + myParams.IsBounded() && !myParams.IsViewAdaptive() ? 1 : 0); + theProgram->SetUniform( + theContext, + "uArcRange", + NCollection_Vec2(float(myParams.AngleStart()), float(myParams.AngleEnd()))); + theProgram->SetUniform(theContext, "uArcBounded", myParams.IsArc() ? 1 : 0); +} diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.hxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.hxx new file mode 100644 index 0000000000..1422ae25e9 --- /dev/null +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderGrid.hxx @@ -0,0 +1,160 @@ +// Copyright (c) 2026 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#ifndef _OpenGl_ShaderGrid_HeaderFile +#define _OpenGl_ShaderGrid_HeaderFile + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//! State and geometry model of the OpenGl shader-rendered grid. +class OpenGl_ShaderGrid +{ +public: + //! Return TRUE if the grid is currently shown. + bool IsShown() const { return myIsShown; } + + //! Return TRUE if the grid is rendered as a background. + bool IsBackground() const { return myParams.IsBackground(); } + + //! Return current parameters. + const Aspect_GridParams& Params() const { return myParams; } + + //! Store grid state. Returns FALSE when parameters cannot produce a shader grid. + bool Display(const Aspect_GridParams& theParams, + const gp_Ax3& thePlane, + const occ::handle& theCamera, + const occ::handle& theContext); + + //! Clear grid state. + void Erase(); + + //! Add helper bounds required by camera Z fitting. + void AddZFitBounds(Bnd_Box& theGraphicBox, const occ::handle& theCamera) const; + + //! Return snapped point for the grid under the window pixel. + bool Echo(const occ::handle& theCamera, + const int theWidth, + const int theHeight, + const int theX, + const int theY, + Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theDisplayPoint) const; + + //! Return snapped point for an arbitrary world point. + bool SnapPoint(const occ::handle& theCamera, + const Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theGridPoint) const; + + //! Upload shader uniforms for current grid state. + void SetUniforms(const occ::handle& theContext, + const occ::handle& theProgram, + const occ::handle& theCamera, + const NCollection_Mat4& theWorldView) const; + + //! Return worldview matrix to use for drawing. + NCollection_Mat4 DrawWorldView(const NCollection_Mat4& theCurrentWorldView) const; + +private: + //! Compute grid plane frame. + void frame(gp_Pnt& theOrigin, gp_XYZ& theX, gp_XYZ& theY, gp_XYZ& theN) const; + + //! Compute effective scales for the current camera. + void effectiveScale(const occ::handle& theCamera, + double& theScaleX, + double& theScaleY) const; + + //! Return TRUE if local coordinates are inside configured grid domain. + bool isPointInBounds(const double theLocalX, const double theLocalY) const; + + //! Add local point to box. + static void addLocalPoint(Bnd_Box& theBox, + const gp_Pnt& theOrigin, + const gp_XYZ& theX, + const gp_XYZ& theY, + const double theLocalX, + const double theLocalY); + + //! Add configured finite bounds to the box. + void addFiniteBounds(Bnd_Box& theBox) const; + + //! Add view footprint bounds to the box. + void addViewFootprintBounds(Bnd_Box& theBox, + const occ::handle& theCamera) const; + + //! Intersect camera ray at NDC point with the grid plane. + bool planeLocalHit(const occ::handle& theCamera, + const double theNdcX, + const double theNdcY, + double& theLocalX, + double& theLocalY, + gp_XYZ* theHit = nullptr, + const bool theToRejectBehind = true) const; + + //! Return stable visible reference point in local coordinates. + bool referenceLocal(const occ::handle& theCamera, + double& theLocalX, + double& theLocalY) const; + + //! Return local shift snapped to a grid phase. + static double snappedLocalShift(const double theLocal, const double theScale); + + //! Return TRUE if previous and new grid definitions share the same background anchor frame. + bool hasSameAnchorFrame(const Aspect_GridParams& theParams, const gp_Ax3& thePlane) const; + + //! Accept echo candidate if it is visible and closer to the requested pixel. + bool acceptEchoCandidate(const occ::handle& theCamera, + const int theWidth, + const int theHeight, + const int theX, + const int theY, + const gp_Pnt& theGridOrigin, + const gp_XYZ& theGridX, + const gp_XYZ& theGridY, + const double theLocalX, + const double theLocalY, + gp_XYZ& theBestSnapped, + double& theBestDist2, + bool& theHasBestPoint) const; + + //! Convert point to current draw view coordinates. + static NCollection_Vec3 viewPoint(const NCollection_Mat4& theWorldView, + const gp_Pnt& thePoint); + + //! Convert direction to current draw view coordinates. + static NCollection_Vec3 viewDirection(const NCollection_Mat4& theWorldView, + const gp_XYZ& theDirection); + + //! Return display-safe echo marker point. + static bool echoDisplayPoint(const occ::handle& theCamera, + const gp_XYZ& theSnapped, + gp_Pnt& theDisplayPoint); + + //! Return TRUE if angle belongs to arc. + static bool angleInArc(const double theStart, const double theEnd, const double theAngle); + +private: + Aspect_GridParams myParams; + gp_Ax3 myPlane; + NCollection_Mat4 myRefViewMatrix; + bool myIsShown = false; +}; + +#endif // _OpenGl_ShaderGrid_HeaderFile diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx index 99c789adbd..04cd5119d3 100644 --- a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx @@ -43,6 +43,7 @@ #include #include #include +#include #include #include "../Textures/Textures_EnvLUT.pxx" @@ -77,66 +78,6 @@ static bool checkWasFailedFbo(const occ::handle& theFboToChe theFboRef->NbSamples()); } -//! Unproject window-space (theWinX, theWinY) to a near/far ray, intersect the -//! grid plane, express the hit in plane-local (X, Y). Returns FALSE if the -//! unprojection fails or the ray is near-parallel to the plane. -static bool unprojectGridPointToPlaneLocal(const occ::handle& theCtx, - const int* theViewport, - const float theWinX, - const float theWinY, - const NCollection_Vec3& thePlaneN, - const NCollection_Vec3& thePlaneOriginV, - const NCollection_Vec3& thePlaneX, - const NCollection_Vec3& thePlaneY, - double& theOutLocalX, - double& theOutLocalY) -{ - if (theViewport == nullptr) - { - return false; - } - float aNearX = 0.0f, aNearY = 0.0f, aNearZ = 0.0f; - float aFarX = 0.0f, aFarY = 0.0f, aFarZ = 0.0f; - if (!Graphic3d_TransformUtils::UnProject(theWinX, - theWinY, - 0.0f, - theCtx->WorldViewState.Current(), - theCtx->ProjectionState.Current(), - theViewport, - aNearX, - aNearY, - aNearZ)) - { - return false; - } - if (!Graphic3d_TransformUtils::UnProject(theWinX, - theWinY, - 1.0f, - theCtx->WorldViewState.Current(), - theCtx->ProjectionState.Current(), - theViewport, - aFarX, - aFarY, - aFarZ)) - { - return false; - } - const NCollection_Vec3 aNearP(aNearX, aNearY, aNearZ); - const NCollection_Vec3 aFarP(aFarX, aFarY, aFarZ); - const NCollection_Vec3 aDir = aFarP - aNearP; - const float aDenom = thePlaneN.Dot(aDir); - if (std::abs(aDenom) <= 1.0e-6f) - { - return false; - } - const float aT = thePlaneN.Dot(thePlaneOriginV - aNearP) / aDenom; - const NCollection_Vec3 aHit = aNearP + aDir * aT; - const NCollection_Vec3 aLocal3 = aHit - thePlaneOriginV; - theOutLocalX = double(aLocal3.Dot(thePlaneX)); - theOutLocalY = double(aLocal3.Dot(thePlaneY)); - return true; -} - //! Chooses compatible internal color format for OIT frame buffer. static bool chooseOitColorConfiguration(const occ::handle& theGlContext, const int theConfigIndex, @@ -201,7 +142,6 @@ OpenGl_View::OpenGl_View(const occ::handle& theMgr, myCubeMapParams(new OpenGl_Aspects()), myColoredQuadParams(new OpenGl_Aspects()), myGridVao(0), - myToShowGrid(false), myPBREnvState(OpenGl_PBREnvState_NONEXISTENT), myPBREnvRequest(false), // ray-tracing fields initialization @@ -965,6 +905,15 @@ Bnd_Box OpenGl_View::MinMaxValues(const bool theToIncludeAuxiliary) const //================================================================================================= +void OpenGl_View::ZFitAllBounds(Bnd_Box& thePrimaryBox, Bnd_Box& theGraphicBox) const +{ + thePrimaryBox = base_type::MinMaxValues(false); + theGraphicBox = MinMaxValues(true); + myShaderGrid.AddZFitBounds(theGraphicBox, Camera()); +} + +//================================================================================================= + occ::handle OpenGl_View::FBO() const { return occ::handle(myFBO); @@ -3635,40 +3584,65 @@ void OpenGl_View::updatePBREnvironment(const occ::handle& theCtx void OpenGl_View::GridDisplay(const Aspect_GridParams& theParams, const gp_Ax3& thePlane) { - // Reference view matrix (for background-mode anchoring) is captured only on - // transitions into background mode or into the showing state. Otherwise live - // param edits (SetArcRange, SetSize, etc.) while the camera is mid-orbit would - // re-snapshot the current view and visually snap the anchored grid. - const bool wasShowing = myToShowGrid; - const bool wasBackground = wasShowing && myGridParams.IsBackground(); - const bool toCapture = theParams.IsBackground() && (!wasShowing || !wasBackground); - - myGridParams = theParams; - myGridPlane = thePlane; - myToShowGrid = true; - - if (toCapture) + const occ::handle& aCtx = myWorkspace->GetGlContext(); + if (!aCtx.IsNull() && aCtx->core30 == nullptr) { - const occ::handle& aCtx = myWorkspace->GetGlContext(); - if (!aCtx.IsNull()) - { - myGridRefViewMatrix = aCtx->WorldViewState.Current(); - } + myShaderGrid.Erase(); + Invalidate(); + return; } + + myShaderGrid.Display(theParams, thePlane, Camera(), aCtx); + Invalidate(); } //================================================================================================= void OpenGl_View::GridErase() { - myToShowGrid = false; + myShaderGrid.Erase(); + Invalidate(); +} + +//================================================================================================= + +bool OpenGl_View::ShaderGridEcho(const int theX, const int theY, Graphic3d_Vertex& thePoint) const +{ + Graphic3d_Vertex aDisplayPoint; + return ShaderGridEcho(theX, theY, thePoint, aDisplayPoint); +} + +//================================================================================================= + +bool OpenGl_View::ShaderGridEcho(const int theX, + const int theY, + Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theDisplayPoint) const +{ + if (Window().IsNull()) + { + return false; + } + + int aWidth = 0; + int aHeight = 0; + Window()->Size(aWidth, aHeight); + return myShaderGrid.Echo(Camera(), aWidth, aHeight, theX, theY, thePoint, theDisplayPoint); +} + +//================================================================================================= + +bool OpenGl_View::ShaderGridSnapPoint(const Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theGridPoint) const +{ + return myShaderGrid.SnapPoint(Camera(), thePoint, theGridPoint); } //================================================================================================= void OpenGl_View::renderGrid() { - if (!myToShowGrid || myGridParams.DrawMode() == Aspect_GDM_None) + if (!myShaderGrid.IsShown() || myShaderGrid.Params().DrawMode() == Aspect_GDM_None) { return; } @@ -3680,7 +3654,7 @@ void OpenGl_View::renderGrid() } if (aContext->core30 == nullptr) { - // The shader grid requires GL 3.0+ / GLES 3.0+ (VAO + gl_VertexID + gl_FragDepth). + // The shader grid requires GL 3.0+ / GLES 3.0+ (VAO + gl_VertexID). // Warn once per process so the caller knows why the grid isn't drawn; snap // math still works. The process scope avoids per-view state bloat - a stray // missed warning on a second view is less costly than a data member. @@ -3710,7 +3684,7 @@ void OpenGl_View::renderGrid() aContext->core30->glGenVertexArrays(1, &myGridVao); if (myGridVao == 0) { - myToShowGrid = false; + myShaderGrid.Erase(); return; } } @@ -3731,58 +3705,20 @@ void OpenGl_View::renderGrid() aContext->core11fwd->glGetIntegerv(GL_BLEND_DST_RGB, &aPrevBlendDstRgb); aContext->core11fwd->glGetIntegerv(GL_BLEND_SRC_ALPHA, &aPrevBlendSrcA); aContext->core11fwd->glGetIntegerv(GL_BLEND_DST_ALPHA, &aPrevBlendDstA); - const bool hasDepthClamp = aContext->arbDepthClamp; - const bool wasDepthClamp = - hasDepthClamp && aContext->core11fwd->glIsEnabled(GL_DEPTH_CLAMP) == GL_TRUE; const occ::handle aPrevProgram = aContext->ActiveProgram(); - // Capture the user-set camera ZRange BEFORE any grid-specific adjustment. - // ZFitAll below mutates the camera; the restore block at the end of this - // method targets these original values, otherwise vconvert and other APIs - // observing ZRange between frames see values inflated by the grid bounds. - const double aZNearKeep = aCamera->ZNear(); - const double aZFarKeep = aCamera->ZFar(); - const Graphic3d_Camera::Projection aProjKeep = aCamera->ProjectionType(); - - Bnd_Box aBnd = MinMaxValues(true); - const gp_Pnt aPlaneLoc = myGridPlane.Location(); - if (myGridParams.IsBackground() || aBnd.IsVoid() || aBnd.IsOut(aPlaneLoc)) - { - aBnd.Add(aPlaneLoc); - // ZFitAll asserts ZFar > ZNear on return (Graphic3d_Camera.cxx:1636), - // so no post-hoc SetZRange nudge is needed. - aCamera->ZFitAll(1.0, aBnd, aBnd); - } - if (myGridParams.IsBackground()) - { - aCamera->SetProjectionType(Graphic3d_Camera::Projection_Orthographic); - } - aContext->ProjectionState.Push(); aContext->ProjectionState.SetCurrent(aCamera->ProjectionMatrixF()); aContext->ApplyProjectionMatrix(); const NCollection_Mat4 aWorldViewCurrent = aContext->WorldViewState.Current(); aContext->WorldViewState.Push(); - if (myGridParams.IsBackground()) - { - // In background mode the grid lives in an unchanging reference frame. - // Derive the camera-motion delta from the captured reference view matrix so the - // grid stays fixed in world coords during pan/rotate, without exposing - // PanningVector / RotationPoint on Graphic3d_Camera. - NCollection_Mat4 aRefInv; - if (!myGridRefViewMatrix.Inverted(aRefInv)) - { - aRefInv.InitIdentity(); - } - NCollection_Mat4 aDelta = aWorldViewCurrent * aRefInv; - aContext->WorldViewState.SetCurrent(aDelta); - } + aContext->WorldViewState.SetCurrent(myShaderGrid.DrawWorldView(aWorldViewCurrent)); aContext->ApplyWorldViewMatrix(); aContext->core11fwd->glEnable(GL_DEPTH_TEST); - aContext->core11fwd->glDepthFunc(GL_LESS); - aContext->core11fwd->glDepthMask(GL_TRUE); + aContext->core11fwd->glDepthFunc(GL_LEQUAL); + aContext->core11fwd->glDepthMask(GL_FALSE); aContext->core11fwd->glEnable(GL_BLEND); aContext->core11fwd->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // The clip-space full-screen quad winds CW; if back-face culling is on @@ -3790,238 +3726,28 @@ void OpenGl_View::renderGrid() // is culled and the grid silently vanishes. Disable culling for the draw // call and restore at the end. aContext->core11fwd->glDisable(GL_CULL_FACE); - if (hasDepthClamp && !wasDepthClamp) - { - aContext->core11fwd->glEnable(GL_DEPTH_CLAMP); - } - aContext->core30->glBindVertexArray(myGridVao); - double aScaleX = myGridParams.Scale(); - double aScaleY = myGridParams.EffectiveScaleY(); - if (myGridParams.IsViewAdaptive()) - { - const double aTargetCellsY = - std::max(1.0, std::min(200.0, aScaleY > 0.0 ? 1.0 / aScaleY : 10.0)); - const double aViewHeight = std::max(aCamera->Scale(), 1.0e-9); - const double aCellSize = aViewHeight / aTargetCellsY; - aScaleX = 1.0 / aCellSize; - aScaleY = aScaleX; - } - if (aContext->ShaderManager()->BindGridProgram()) { const occ::handle& aProg = aContext->ActiveProgram(); - - aProg->SetUniform(aContext, "uScaleX", GLfloat(aScaleX)); - aProg->SetUniform(aContext, "uScaleY", GLfloat(aScaleY)); - aProg->SetUniform(aContext, "uThickness", GLfloat(myGridParams.LineThickness())); - aProg->SetUniform(aContext, - "uColor", - NCollection_Vec3((float)myGridParams.Color().Red(), - (float)myGridParams.Color().Green(), - (float)myGridParams.Color().Blue())); - aProg->SetUniform(aContext, - "uAccentColor", - NCollection_Vec3((float)myGridParams.AccentColor().Red(), - (float)myGridParams.AccentColor().Green(), - (float)myGridParams.AccentColor().Blue())); - aProg->SetUniform(aContext, "uAccentScaleX", GLfloat(myGridParams.AccentScaleX())); - aProg->SetUniform(aContext, "uAccentScaleY", GLfloat(myGridParams.AccentScaleY())); - aProg->SetUniform(aContext, "uAccentAngularScale", GLfloat(myGridParams.AccentAngularScale())); - aProg->SetUniform(aContext, "uIsDrawAxis", myGridParams.IsDrawAxis() ? 1 : 0); - aProg->SetUniform(aContext, "uIsBackground", myGridParams.IsBackground() ? 1 : 0); - aProg->SetUniform(aContext, "uGridType", myGridParams.IsCircular() ? 1 : 0); - // Angular spokes per radian: N spokes in 180 deg = N / pi spokes per radian. - const double aAngularScale = - myGridParams.IsCircular() ? double(myGridParams.AngularDivisions()) / M_PI : 0.0; - aProg->SetUniform(aContext, "uAngularScale", GLfloat(aAngularScale)); - aProg->SetUniform(aContext, "uDrawMode", myGridParams.DrawMode() == Aspect_GDM_Points ? 1 : 0); - - // Plane basis used for both the view-adaptive bounds search and the final - // shader uniforms. - // - In-plane rotation rotates X/Y around the plane normal. Sign matches - // V3d_View::SetGrid's Trsf2 so snap (V3d_View::Compute) and the drawn - // grid use the same basis: gridX = cos*planeX - sin*planeY, - // gridY = sin*planeX + cos*planeY. - // - ZOffset pushes the displayed plane along its normal to avoid - // z-fighting with coplanar geometry. Snap math uses the unshifted - // plane, so selection still lands on the true plane. - const double aCosA = std::cos(myGridParams.RotationAngle()); - const double aSinA = std::sin(myGridParams.RotationAngle()); - const gp_Dir aRawX = myGridPlane.XDirection(); - const gp_Dir aRawY = myGridPlane.YDirection(); - const gp_Dir aNDir = myGridPlane.Direction(); - const gp_XYZ aXRotated = aRawX.XYZ() * aCosA - aRawY.XYZ() * aSinA; - const gp_XYZ aYRotated = aRawX.XYZ() * aSinA + aRawY.XYZ() * aCosA; - const double aZOffset = myGridParams.ZOffset(); - const gp_Pnt aOriginLocal = myGridParams.Origin(); - const gp_Pnt aPlaneOrigin(aPlaneLoc.X() + aOriginLocal.X() + aNDir.X() * aZOffset, - aPlaneLoc.Y() + aOriginLocal.Y() + aNDir.Y() * aZOffset, - aPlaneLoc.Z() + aOriginLocal.Z() + aNDir.Z() * aZOffset); - const NCollection_Vec3 aPlaneNV((float)aNDir.X(), (float)aNDir.Y(), (float)aNDir.Z()); - const NCollection_Vec3 aPlaneOriginV((float)aPlaneOrigin.X(), - (float)aPlaneOrigin.Y(), - (float)aPlaneOrigin.Z()); - const NCollection_Vec3 aPlaneXV((float)aXRotated.X(), - (float)aXRotated.Y(), - (float)aXRotated.Z()); - const NCollection_Vec3 aPlaneYV((float)aYRotated.X(), - (float)aYRotated.Y(), - (float)aYRotated.Z()); - - const int* aViewport = aContext->Viewport(); - - // Bounded work area (HalfSizeX, HalfSizeY, Radius). 0 = unbounded along that axis. - float aHalfX = myGridParams.SizeX() > 0.0 ? float(myGridParams.SizeX() * 0.5) : 0.0f; - float aHalfY = myGridParams.SizeY() > 0.0 ? float(myGridParams.SizeY() * 0.5) : 0.0f; - float aRadius = myGridParams.Radius() > 0.0 ? float(myGridParams.Radius()) : 0.0f; - if (myGridParams.IsViewAdaptive()) - { - // Derive a tight world-space bound from the visible region: unproject - // the four viewport corners + center to the grid plane and enclose - // their plane-local extents. The center sample is a safety net when - // a corner ray is near-parallel to the plane and gets rejected. - double aMinLocalX = 0.0; - double aMaxLocalX = 0.0; - double aMinLocalY = 0.0; - double aMaxLocalY = 0.0; - double aMaxLocalRadius = 0.0; - bool aHasBounds = false; - if (aViewport != nullptr) - { - const float aWinMinX = float(aViewport[0]); - const float aWinMinY = float(aViewport[1]); - const float aWinMaxX = float(aViewport[0] + aViewport[2]); - const float aWinMaxY = float(aViewport[1] + aViewport[3]); - const float aWinMidX = (aWinMinX + aWinMaxX) * 0.5f; - const float aWinMidY = (aWinMinY + aWinMaxY) * 0.5f; - const NCollection_Vec2 aSamples[] = {NCollection_Vec2(aWinMinX, aWinMinY), - NCollection_Vec2(aWinMaxX, aWinMinY), - NCollection_Vec2(aWinMinX, aWinMaxY), - NCollection_Vec2(aWinMaxX, aWinMaxY), - NCollection_Vec2(aWinMidX, aWinMidY)}; - for (const NCollection_Vec2& aSample : aSamples) - { - double aLocalX = 0.0, aLocalY = 0.0; - if (!unprojectGridPointToPlaneLocal(aContext, - aViewport, - aSample.x(), - aSample.y(), - aPlaneNV, - aPlaneOriginV, - aPlaneXV, - aPlaneYV, - aLocalX, - aLocalY)) - { - continue; - } - const double aHitR = std::sqrt(aLocalX * aLocalX + aLocalY * aLocalY); - if (!aHasBounds) - { - aMinLocalX = aLocalX; - aMaxLocalX = aLocalX; - aMinLocalY = aLocalY; - aMaxLocalY = aLocalY; - aMaxLocalRadius = aHitR; - aHasBounds = true; - } - else - { - aMinLocalX = std::min(aMinLocalX, aLocalX); - aMaxLocalX = std::max(aMaxLocalX, aLocalX); - aMinLocalY = std::min(aMinLocalY, aLocalY); - aMaxLocalY = std::max(aMaxLocalY, aLocalY); - aMaxLocalRadius = std::max(aMaxLocalRadius, aHitR); - } - } - } - - const double aStepX = aScaleX > 0.0 ? 1.0 / aScaleX : 1.0; - const double aStepY = aScaleY > 0.0 ? 1.0 / aScaleY : aStepX; - if (myGridParams.IsCircular()) - { - const double aPad = std::max(aStepX, aStepY) * 4.0; - if (aHasBounds) - { - aRadius = std::max(aRadius, float(aMaxLocalRadius + aPad)); - } - else - { - aRadius = std::max(aRadius, float(std::max(aCamera->Scale(), aPad) * 2.0)); - } - } - else if (aHasBounds) - { - const double aPadX = std::max((aMaxLocalX - aMinLocalX) * 0.10, aStepX * 4.0); - const double aPadY = std::max((aMaxLocalY - aMinLocalY) * 0.10, aStepY * 4.0); - aHalfX = - std::max(aHalfX, float(std::max(std::abs(aMinLocalX), std::abs(aMaxLocalX)) + aPadX)); - aHalfY = - std::max(aHalfY, float(std::max(std::abs(aMinLocalY), std::abs(aMaxLocalY)) + aPadY)); - } - else - { - aHalfX = std::max(aHalfX, float(std::max(aCamera->Scale(), aStepX * 8.0))); - aHalfY = std::max(aHalfY, float(std::max(aCamera->Scale(), aStepY * 8.0))); - } - } - aProg->SetUniform(aContext, "uBounds", NCollection_Vec3(aHalfX, aHalfY, aRadius)); - aProg->SetUniform( - aContext, - "uArcRange", - NCollection_Vec2(float(myGridParams.AngleStart()), float(myGridParams.AngleEnd()))); - aProg->SetUniform(aContext, "uArcBounded", myGridParams.IsArc() ? 1 : 0); - - aProg->SetUniform(aContext, "uPlaneOrigin", aPlaneOriginV); - aProg->SetUniform(aContext, "uPlaneX", aPlaneXV); - aProg->SetUniform(aContext, "uPlaneY", aPlaneYV); - - // Stable per-frame rectangular-grid reference point in plane-local - // coordinates. Keeps shader fract() arguments bounded at shallow angles - // without re-running extra unproject/intersection work for every fragment. - int aHasStableRef = 0; - NCollection_Vec2 aStableRefLocal(0.0f, 0.0f); - if (!myGridParams.IsCircular() && aViewport != nullptr) - { - const float aWinX = float(aViewport[0]) + float(aViewport[2]) * 0.5f; - const float aWinY = float(aViewport[1]) + float(aViewport[3]) * 0.5f; - double aLocalX = 0.0, aLocalY = 0.0; - if (unprojectGridPointToPlaneLocal(aContext, - aViewport, - aWinX, - aWinY, - aPlaneNV, - aPlaneOriginV, - aPlaneXV, - aPlaneYV, - aLocalX, - aLocalY)) - { - aStableRefLocal.SetValues(float(aLocalX), float(aLocalY)); - aHasStableRef = 1; - } - } - aProg->SetUniform(aContext, "uStableRefLocal", aStableRefLocal); - aProg->SetUniform(aContext, "uHasStableRef", aHasStableRef); - aProg->SetUniform(aContext, "uPlaneN", aPlaneNV); - - aContext->core11fwd->glDrawArrays(GL_TRIANGLES, 0, 6); + myShaderGrid.SetUniforms(aContext, aProg, aCamera, aContext->WorldViewState.Current()); + aContext->core11fwd->glDrawArrays(GL_TRIANGLES, 0, 3); } aContext->BindProgram(aPrevProgram); aContext->core30->glBindVertexArray((GLuint)aPrevVao); - aCamera->SetZRange(aZNearKeep, aZFarKeep); - aCamera->SetProjectionType(aProjKeep); - aContext->WorldViewState.Pop(); aContext->ProjectionState.Pop(); aContext->ApplyWorldViewMatrix(); aContext->ApplyProjectionMatrix(); - if (wasDepthTest == GL_FALSE) + if (wasDepthTest == GL_TRUE) + { + aContext->core11fwd->glEnable(GL_DEPTH_TEST); + } + else { aContext->core11fwd->glDisable(GL_DEPTH_TEST); } @@ -4039,8 +3765,4 @@ void OpenGl_View::renderGrid() { aContext->core11fwd->glEnable(GL_CULL_FACE); } - if (hasDepthClamp && !wasDepthClamp) - { - aContext->core11fwd->glDisable(GL_DEPTH_CLAMP); - } } diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx index e806a2f496..8e7cab512a 100644 --- a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -162,6 +163,9 @@ public: //! @return computed bounding box Standard_EXPORT Bnd_Box MinMaxValues(const bool theToIncludeAuxiliary) const override; + //! Return primary and graphical bounding boxes used by camera Z fitting. + Standard_EXPORT void ZFitAllBounds(Bnd_Box& thePrimaryBox, Bnd_Box& theGraphicBox) const override; + //! Returns pointer to an assigned framebuffer object. Standard_EXPORT occ::handle FBO() const override; @@ -229,6 +233,21 @@ public: //! Erase the shader-rendered grid. Standard_EXPORT void GridErase() override; + //! Return snapped point for the shader-rendered grid under the window pixel. + Standard_EXPORT bool ShaderGridEcho(const int theX, + const int theY, + Graphic3d_Vertex& thePoint) const override; + + //! Return snapped point and clip-safe display point for the shader-rendered grid echo marker. + Standard_EXPORT bool ShaderGridEcho(const int theX, + const int theY, + Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theDisplayPoint) const override; + + //! Return snapped point for the shader-rendered grid from an arbitrary world point. + Standard_EXPORT bool ShaderGridSnapPoint(const Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theGridPoint) const override; + //! Returns number of mipmap levels used in specular IBL map. //! 0 if PBR environment is not created. Standard_EXPORT unsigned int SpecIBLMapLevels() const; @@ -539,11 +558,8 @@ protected: //! @name Background parameters OpenGl_Aspects* myTextureParams; //!< Stores texture and its parameters for textured background OpenGl_Aspects* myCubeMapParams; //!< Stores cubemap and its parameters for cubemap background OpenGl_Aspects* myColoredQuadParams; //!< Stores parameters for gradient (corner mode) background - Aspect_GridParams myGridParams; //!< parameters of shader grid - gp_Ax3 myGridPlane; //!< grid plane in world coordinates - NCollection_Mat4 myGridRefViewMatrix; //!< worldview captured at GridDisplay() for pan/rotate compensation + OpenGl_ShaderGrid myShaderGrid; //!< shader grid state and geometry model unsigned int myGridVao; //!< dedicated VAO for textureless grid draw - bool myToShowGrid; //!< flag indicating the grid is active OpenGl_BackgroundArray* myBackgrounds[Graphic3d_TypeOfBackground_NB]; //!< Array of primitive arrays of different background types // clang-format on occ::handle myTextureEnv; diff --git a/src/Visualization/TKService/Aspect/Aspect_GridParams.hxx b/src/Visualization/TKService/Aspect/Aspect_GridParams.hxx index 48ef46eec3..a46def9d2a 100644 --- a/src/Visualization/TKService/Aspect/Aspect_GridParams.hxx +++ b/src/Visualization/TKService/Aspect/Aspect_GridParams.hxx @@ -22,10 +22,8 @@ #include #include -//! Shader grid appearance (color, scale, bounds, arc, draw mode, background / adaptive flags). -//! Consumed only by the GPU path: V3d_View::GridDisplay -> OpenGl_View::renderGrid. -//! No effect on the CPU path (V3d_Viewer::ActivateGrid). Snap math is independent and -//! lives on Aspect_RectangularGrid / Aspect_CircularGrid. +//! Grid appearance for V3d_View::GridDisplay: color, scale, bounds, arc, draw mode, +//! background and adaptive flags. class Aspect_GridParams { public: @@ -183,8 +181,7 @@ public: //! Return signed plane-normal offset applied at render time. double ZOffset() const { return myZOffset; } - //! Set signed plane-normal offset applied at render time (display only; - //! snap math stays on the unshifted plane). + //! Set signed plane-normal offset applied at render and echo time. void SetZOffset(const double theOffset) { myZOffset = theOffset; } //! Return arc start angle (radians). Meaningful only when IsArc() is true. @@ -229,10 +226,8 @@ public: //! Return TRUE if grid spacing and visible extents adapt to the camera view. bool IsViewAdaptive() const { return myIsViewAdaptive; } - //! Set view-adaptive grid on/off. When enabled, renderer derives temporary - //! cell spacing and bounds from the current camera. The inverse of ScaleY() - //! (or Scale() when ScaleY() is zero) is used as the target number of cells - //! across the view height. + //! Set view-adaptive grid on/off. When enabled, shader renderer keeps the + //! screen-space grid step stable by scaling the cell spacing with camera zoom. void SetIsViewAdaptive(const bool theIsViewAdaptive) { myIsViewAdaptive = theIsViewAdaptive; } private: diff --git a/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx b/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx index 285d4360f9..14304cbd45 100644 --- a/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx +++ b/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -174,6 +175,13 @@ public: //! @return computed bounding box Standard_EXPORT virtual Bnd_Box MinMaxValues(const bool theToIncludeAuxiliary = false) const; + //! Return primary and graphical bounding boxes used by camera Z fitting. + virtual void ZFitAllBounds(Bnd_Box& thePrimaryBox, Bnd_Box& theGraphicBox) const + { + thePrimaryBox = MinMaxValues(false); + theGraphicBox = MinMaxValues(true); + } + //! Returns the coordinates of the boundary box of all structures in the set . //! If is TRUE, then the boundary box //! also includes minimum and maximum limits of graphical elements @@ -454,6 +462,43 @@ public: //! The default implementation is a no-op; drivers with shader support override it. virtual void GridErase() {} + //! Return snapped point for the shader-rendered grid under the window pixel. + //! The default implementation is a no-op; drivers with shader grid support override it. + virtual bool ShaderGridEcho(const int theX, const int theY, Graphic3d_Vertex& thePoint) const + { + (void)theX; + (void)theY; + (void)thePoint; + return false; + } + + //! Return snapped point and display point for the shader-rendered grid under the window pixel. + //! The snapped point is the geometric grid point in world coordinates. + //! The display point is a clip-safe proxy projected to the same window position for echo marker + //! presentation; it should not be used as the geometric snap result. + virtual bool ShaderGridEcho(const int theX, + const int theY, + Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theDisplayPoint) const + { + if (!ShaderGridEcho(theX, theY, thePoint)) + { + return false; + } + theDisplayPoint = thePoint; + return true; + } + + //! Return snapped point for the shader-rendered grid from an arbitrary world point. + //! The default implementation is a no-op; drivers with shader grid support override it. + virtual bool ShaderGridSnapPoint(const Graphic3d_Vertex& thePoint, + Graphic3d_Vertex& theGridPoint) const + { + (void)thePoint; + (void)theGridPoint; + return false; + } + //! Returns environment texture set for the view. const occ::handle& TextureEnv() const { return myTextureEnvData; } diff --git a/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx b/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx index 6983a006c0..0aa982eb35 100644 --- a/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx +++ b/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx @@ -2156,12 +2156,8 @@ occ::handle Graphic3d_ShaderManager::getGridProgram() c occ::handle aProgSrc = new Graphic3d_ShaderProgram(); Graphic3d_ShaderObject::ShaderVariableList aUniforms, aStageInOuts; - // Pass only NDC.xy to the fragment. Near/Far world points are computed - // per-pixel because the perspective-divide that unproject() performs is - // nonlinear in NDC, and linearly interpolating the resulting world-space - // points across the full-screen quad produces wrong rays along the - // diagonal seam between the two triangles (visible as empty triangular - // dead zones at oblique camera angles). + // Pass only NDC.xy to the fragment; each fragment reconstructs its view-space + // ray and intersects it with the grid plane. aStageInOuts.Append( Graphic3d_ShaderObject::ShaderVariable("vec2 vNdc", Graphic3d_TOS_VERTEX | Graphic3d_TOS_FRAGMENT)); @@ -2182,21 +2178,38 @@ occ::handle Graphic3d_ShaderManager::getGridProgram() c aUniforms.Append( Graphic3d_ShaderObject::ShaderVariable("int uIsDrawAxis", Graphic3d_TOS_FRAGMENT)); aUniforms.Append( - Graphic3d_ShaderObject::ShaderVariable("int uIsBackground", Graphic3d_TOS_FRAGMENT)); + Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneOriginView", Graphic3d_TOS_FRAGMENT)); aUniforms.Append( - Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneOrigin", Graphic3d_TOS_FRAGMENT)); - aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneX", Graphic3d_TOS_FRAGMENT)); - aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneY", Graphic3d_TOS_FRAGMENT)); - aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneN", Graphic3d_TOS_FRAGMENT)); + Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneRefView", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneXView", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneYView", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneNView", Graphic3d_TOS_FRAGMENT)); aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("int uGridType", Graphic3d_TOS_FRAGMENT)); aUniforms.Append( Graphic3d_ShaderObject::ShaderVariable("float uAngularScale", Graphic3d_TOS_FRAGMENT)); aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("int uDrawMode", Graphic3d_TOS_FRAGMENT)); aUniforms.Append( - Graphic3d_ShaderObject::ShaderVariable("vec2 uStableRefLocal", Graphic3d_TOS_FRAGMENT)); + Graphic3d_ShaderObject::ShaderVariable("int uIsPerspective", Graphic3d_TOS_FRAGMENT)); aUniforms.Append( - Graphic3d_ShaderObject::ShaderVariable("int uHasStableRef", Graphic3d_TOS_FRAGMENT)); + Graphic3d_ShaderObject::ShaderVariable("int uIsZeroToOneDepth", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("int uIsBackground", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("float uParallelTolerance", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec2 uLocalOriginShift", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec2 uAccentLocalOriginShift", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("float uRadialOriginShift", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("float uAccentRadialOriginShift", + Graphic3d_TOS_FRAGMENT)); aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uBounds", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("int uIsBoundFade", Graphic3d_TOS_FRAGMENT)); aUniforms.Append( Graphic3d_ShaderObject::ShaderVariable("vec2 uArcRange", Graphic3d_TOS_FRAGMENT)); aUniforms.Append( @@ -2204,171 +2217,165 @@ occ::handle Graphic3d_ShaderManager::getGridProgram() c TCollection_AsciiString aSrcVert = TCollection_AsciiString() - + EOL "const vec3 gridPlane[6] = vec3[] (" EOL - " vec3( 1.0, 1.0, 0.0), vec3(-1.0, -1.0, 0.0), vec3(-1.0, 1.0, 0.0)," EOL - " vec3(-1.0, -1.0, 0.0), vec3( 1.0, 1.0, 0.0), vec3( 1.0, -1.0, 0.0));" + + EOL "const vec2 gridPlane[3] = vec2[] (" EOL + " vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));" - EOL "void main()" EOL "{" EOL " vec3 aVertex = gridPlane[gl_VertexID];" EOL - " vNdc = aVertex.xy;" EOL " gl_Position = vec4 (aVertex, 1.0);" EOL "}"; + EOL "void main()" EOL "{" EOL " vec2 aVertex = gridPlane[gl_VertexID];" EOL + " vNdc = aVertex;" EOL " gl_Position = vec4 (aVertex, 0.0, 1.0);" EOL "}"; TCollection_AsciiString aSrcFrag = TCollection_AsciiString() + EOL - "vec4 gridLines2d (vec2 theUV, vec2 theAxisUV, vec3 theColor, vec2 theScale," EOL - " bool theIsDrawAxis, float theThickness)" EOL "{" EOL - " vec2 aCoord = theUV * theScale;" EOL " vec2 aFwidth = fwidth (aCoord);" EOL - " vec2 aDerivative = max (aFwidth, vec2 (theThickness));" EOL - " vec2 aGrid = abs (fract (aCoord - 0.5) - 0.5) / aDerivative;" EOL - // Per-axis Nyquist fade: when more than ~1.5 grid cells map to a pixel in a given - // direction, that set of lines fades to zero instead of smearing into a bright band. - // Lines mode (uDrawMode!=1): bright when either axis shows a line -> max of per-axis alphas. - // Points mode (uDrawMode==1): bright only at intersections -> product of per-axis alphas. - EOL " vec2 aNyq = vec2 (1.0) - smoothstep (vec2 (1.0), vec2 (2.0), aFwidth);" EOL - " float aAlphaX = (1.0 - min (aGrid.x, 1.0)) * aNyq.x;" EOL - " float aAlphaY = (1.0 - min (aGrid.y, 1.0)) * aNyq.y;" EOL - " float aAlpha = uDrawMode == 1 ? aAlphaX * aAlphaY : max (aAlphaX, aAlphaY);" EOL - " float aMinY = min (aDerivative.y, 1.0);" EOL - " float aMinX = min (aDerivative.x, 1.0);" EOL - " vec4 aColor = vec4 (theColor, aAlpha);" EOL - // Axis colouring in lines mode only; a "point" has no axis direction. - EOL " if (uIsDrawAxis != 0 && theIsDrawAxis && uDrawMode != 1)" EOL " {" EOL - " vec2 aAxisCoord = theAxisUV * theScale;" EOL - " bool isYAxis = abs (aAxisCoord.x) < aMinX;" EOL - " bool isXAxis = abs (aAxisCoord.y) < aMinY;" EOL - " if (isXAxis && isYAxis) { aColor.xyz = vec3 (0.0, 0.0, 1.0); }" EOL - " else if (isXAxis) { aColor.xyz = vec3 (1.0, 0.0, 0.0); }" EOL - " else if (isYAxis) { aColor.xyz = vec3 (0.0, 1.0, 0.0); }" EOL " }" EOL - " return aColor;" EOL "}" + "float gridLine1d (float theCoord, float theShift, float theScale, float theThickness)" EOL + "{" EOL " float aCoord = (theCoord - theShift) * theScale;" EOL + " float aPixelWidth = fwidth (aCoord);" EOL " if (!(aPixelWidth >= 0.0)) { return 0.0; }" EOL + " float aDist = abs (fract (aCoord + 0.5) - 0.5);" EOL + " float aWidth = max (aPixelWidth, theThickness);" EOL + " if (aWidth == 0.0) { return 0.0; }" EOL " return 1.0 - min (aDist / aWidth, 1.0);" EOL "}" - EOL "vec4 gridLines1d (float theCoord, float theScale, vec3 theColor, float theThickness)" EOL - "{" EOL " float aCoord = theCoord * theScale;" EOL - " float aFwidth = fwidth (aCoord);" EOL - " float aDerivative = max (aFwidth, theThickness);" EOL - " float aGrid = abs (fract (aCoord - 0.5) - 0.5) / aDerivative;" EOL - " float aNyq = 1.0 - smoothstep (1.0, 2.0, aFwidth);" EOL - " return vec4 (theColor, (1.0 - min (aGrid, 1.0)) * aNyq);" EOL "}" + EOL "float axisLine1d (float theCoord, float theThickness)" EOL "{" EOL + " float aWidth = max (fwidth (theCoord), theThickness);" EOL + " if (aWidth == 0.0) { return 0.0; }" EOL + " return 1.0 - min (abs (theCoord) / aWidth, 1.0);" EOL "}" + + EOL "vec4 gridLines2d (vec2 theUV, vec2 theAxisUV, vec3 theColor, vec2 theScale," EOL + " vec2 theShift, bool theIsDrawAxis, float theThickness)" EOL "{" EOL + " float aAlphaX = gridLine1d (theUV.x, theShift.x, theScale.x, theThickness);" EOL + " float aAlphaY = gridLine1d (theUV.y, theShift.y, theScale.y, theThickness);" EOL + " float aAlpha = uDrawMode == 1 ? aAlphaX * aAlphaY : max (aAlphaX, aAlphaY);" EOL + " vec4 aColor = vec4 (theColor, aAlpha);" EOL + " if (uIsDrawAxis != 0 && theIsDrawAxis && uDrawMode != 1)" EOL " {" EOL + " float anAxisX = axisLine1d (theAxisUV.y, theThickness);" EOL + " float anAxisY = axisLine1d (theAxisUV.x, theThickness);" EOL + " if (anAxisX > 0.0 && anAxisY > 0.0) { aColor = vec4 (0.0, 0.0, 1.0, max " + "(aColor.a, max (anAxisX, anAxisY))); }" EOL + " else if (anAxisX > 0.0) { aColor = vec4 (1.0, 0.0, 0.0, max " + "(aColor.a, anAxisX)); }" EOL + " else if (anAxisY > 0.0) { aColor = vec4 (0.0, 1.0, 0.0, max " + "(aColor.a, anAxisY)); }" EOL " }" EOL " return aColor;" EOL "}" + + EOL "vec4 gridLines1d (float theCoord, float theShift, float theScale, vec3 theColor, float " + "theThickness)" EOL "{" EOL + " return vec4 (theColor, gridLine1d (theCoord, theShift, theScale, theThickness));" EOL "}" EOL "vec4 overlayGrid (vec4 theBase, vec4 theAccent)" EOL "{" EOL " return theAccent.a >= theBase.a ? theAccent : theBase;" EOL "}" - EOL "vec3 unproject (float theX, float theY, float theZ)" EOL "{" EOL - " vec4 aUnproj = occModelWorldMatrixInverse * occWorldViewMatrixInverse" EOL - " * occProjectionMatrixInverse * vec4 (theX, theY, theZ, 1.0);" EOL - " return aUnproj.xyz / aUnproj.w;" EOL "}" + EOL "vec3 unprojectView (float theX, float theY, float theZ)" EOL "{" EOL + " vec4 aView = occProjectionMatrixInverse * vec4 (theX, theY, theZ, 1.0);" EOL + " return aView.xyz / aView.w;" EOL "}" - // sin(angle) threshold for "ray parallel to plane" - ~5.7e-5 deg. - EOL "const float GRID_PARALLEL_EPS = 1e-6;" + EOL "float gridNdcNear()" EOL "{" EOL " return uIsZeroToOneDepth != 0 ? 0.0 : -1.0;" EOL "}" - EOL "bool intersectPlane (vec3 theNearPoint, vec3 theFarPoint, out vec3 theHit)" EOL "{" EOL - " vec3 aDir = theFarPoint - theNearPoint;" EOL - " float aDenomN = dot (uPlaneN, normalize (aDir));" EOL - " if (abs (aDenomN) < GRID_PARALLEL_EPS)" EOL " {" EOL " theHit = theNearPoint;" EOL - " return false;" EOL " }" EOL " float aDenom = dot (uPlaneN, aDir);" EOL - " float aT = dot (uPlaneN, uPlaneOrigin - theNearPoint) / aDenom;" EOL - " theHit = theNearPoint + aT * aDir;" EOL " return true;" EOL "}" EOL + EOL "float gridDepthFromNdc (float theNdcZ)" EOL "{" EOL + " return uIsZeroToOneDepth != 0 ? theNdcZ : (theNdcZ * 0.5 + 0.5);" EOL "}" - EOL "float computeDepth (vec3 thePos)" EOL "{" EOL - " mat4 aMVP = occProjectionMatrix * occWorldViewMatrix * occModelWorldMatrix;" EOL - " vec4 aClip = aMVP * vec4 (thePos, 1.0);" EOL " return aClip.z / aClip.w;" EOL "}" + EOL "float fragmentDepthFromView (vec3 theViewPnt)" EOL "{" EOL + " vec4 aClip = occProjectionMatrix * vec4 (theViewPnt, 1.0);" EOL + " float aNdcZ = aClip.z / aClip.w;" EOL " return gridDepthFromNdc (aNdcZ);" EOL "}" - // Fade-band width (as a fraction of bound) applied inside the bound edge. - // Narrow smoothstep softens rectangular/disc cuts; angular cut stays hard. - EOL "const float GRID_FADE_BAND = 0.95;" - // Keep gl_FragDepth strictly less than 1.0 so the grid never lands exactly - // on the far plane (some drivers cull there). - EOL "const float GRID_DEPTH_EPS = 1e-5;" EOL "const float GRID_TWO_PI = 6.28318530718;" + EOL "bool intersectPlaneView (vec3 theNearPoint, vec3 theFarPoint, out vec3 theHit)" EOL "{" EOL + " vec3 aRayOrigin = uIsPerspective != 0 ? vec3 (0.0) : theNearPoint;" EOL + " vec3 aRayTarget = theFarPoint;" EOL " vec3 aDir = aRayTarget - aRayOrigin;" EOL + " float aDirLen = length (aDir);" EOL " if (aDirLen == 0.0) { return false; }" EOL + " float aDenomN = dot (uPlaneNView, aDir / aDirLen);" EOL + " if (abs (aDenomN) <= uParallelTolerance) { return false; }" EOL + " float aDenom = dot (uPlaneNView, aDir);" EOL + " float aT = dot (uPlaneNView, uPlaneOriginView - aRayOrigin) / aDenom;" EOL + " if (uIsBackground == 0 && uIsPerspective != 0 && aT < 0.0)" EOL " {" EOL + " theHit = theNearPoint;" EOL " return false;" EOL " }" EOL + " theHit = aRayOrigin + aT * aDir;" EOL " return true;" EOL "}" EOL + + EOL "const float GRID_TWO_PI = 6.28318530718;" + + EOL "float gridNormalizeAngle (float theAngle)" EOL "{" EOL + " float anAngle = mod (theAngle, GRID_TWO_PI);" EOL + " if (anAngle < 0.0) { anAngle += GRID_TWO_PI; }" EOL " return anAngle;" EOL "}" + + EOL "float gridPositiveAngleSpan (float theStart, float theEnd)" EOL "{" EOL + " float aSpan = mod (theEnd - theStart, GRID_TWO_PI);" EOL + " if (aSpan < 0.0) { aSpan += GRID_TWO_PI; }" EOL + " if (abs (aSpan) <= uParallelTolerance && abs (theEnd - theStart) >= GRID_TWO_PI - " + "uParallelTolerance)" EOL " {" EOL " return GRID_TWO_PI;" EOL " }" EOL + " return aSpan;" EOL "}" + + EOL "bool gridAngleInArc (float theAngle)" EOL "{" EOL + " float aSpan = gridPositiveAngleSpan (uArcRange.x, uArcRange.y);" EOL + " if (aSpan >= GRID_TWO_PI - uParallelTolerance) { return true; }" EOL + " float aDelta = gridNormalizeAngle (theAngle) - gridNormalizeAngle (uArcRange.x);" EOL + " if (aDelta < 0.0) { aDelta += GRID_TWO_PI; }" EOL + " return aDelta <= aSpan + uParallelTolerance;" EOL "}" EOL "void main()" EOL "{" - // Unproject per-pixel from vNdc (linearly interpolated NDC.xy) to avoid the - // nonlinearity of the perspective divide when ray endpoints are passed as - // varyings. Each fragment reconstructs its own Near/Far world points. - EOL " vec3 aNearPoint = unproject (vNdc.x, vNdc.y, -1.0);" EOL - " vec3 aFarPoint = unproject (vNdc.x, vNdc.y, 1.0);" EOL " vec3 aHit;" EOL - " if (!intersectPlane (aNearPoint, aFarPoint, aHit)) { discard; }" - // Plane-local 2D coords, origin-centered to tame fp precision at large world offsets. - EOL " vec3 aLocal3 = aHit - uPlaneOrigin;" EOL - " vec2 aLocal = vec2 (dot (aLocal3, uPlaneX), dot (aLocal3, uPlaneY));" + // Intersect in view space: the camera is the numerical origin, so zoom and + // world-coordinate magnitude do not destabilize the line phase. + EOL " vec3 aNearPoint = unprojectView (vNdc.x, vNdc.y, gridNdcNear());" EOL + " vec3 aFarPoint = unprojectView (vNdc.x, vNdc.y, 1.0);" EOL " vec3 aHit;" EOL + " if (!intersectPlaneView (aNearPoint, aFarPoint, aHit)) { discard; }" EOL + " float aFragDepth = fragmentDepthFromView (aHit);" EOL " if (uIsBackground == 0)" EOL + " {" EOL " if (aFragDepth < 0.0) { discard; }" EOL + " gl_FragDepth = min (aFragDepth, 1.0);" EOL " }" EOL " else" EOL " {" EOL + " gl_FragDepth = 1.0;" EOL " }" EOL " vec3 aLocalAbs3 = aHit - uPlaneOriginView;" EOL + " vec2 aLocal = vec2 (dot (aLocalAbs3, uPlaneXView), dot (aLocalAbs3, uPlaneYView));" EOL + " vec3 aLocalGrid3 = aHit - uPlaneRefView;" EOL + " vec2 aLocalGrid = vec2 (dot (aLocalGrid3, uPlaneXView), dot (aLocalGrid3, " + "uPlaneYView));" // Bounded work area. Rectangular, radial and angular clipping can be mixed. EOL " float aR = length (aLocal);" EOL " float aA = atan (aLocal.y, aLocal.x);" EOL " float aBoundFade = 1.0;" EOL " if (uBounds.z > 0.0)" EOL " {" EOL - " float aFwBR = fwidth (aR);" EOL " if (aR > uBounds.z + aFwBR) { discard; }" EOL - " aBoundFade *= 1.0 - smoothstep (GRID_FADE_BAND * uBounds.z, uBounds.z + aFwBR, aR);" EOL - " }" EOL " if (uArcBounded != 0)" EOL " {" EOL - " float aSpan = uArcRange.y - uArcRange.x;" EOL - " if (aSpan < 0.0) { aSpan += GRID_TWO_PI; }" EOL " float aDelta = aA - uArcRange.x;" EOL - " if (aDelta < 0.0) { aDelta += GRID_TWO_PI; }" EOL - " if (aDelta > aSpan) { discard; }" EOL " }" EOL " if (uBounds.x > 0.0)" EOL " {" EOL - // Extend the hard discard and smoothstep range by fwidth so the rectangular - // boundary gets sub-pixel AA (one screen-pixel transition) instead of a - // hard binary staircase at the clipping edge. + " float aFwBR = fwidth (aR);" EOL " if (aR > uBounds.z) { discard; }" EOL + " if (uIsBoundFade != 0)" EOL " {" EOL + " aBoundFade *= 1.0 - smoothstep (uBounds.z - aFwBR, uBounds.z, aR);" EOL " }" EOL + " }" EOL " if (uArcBounded != 0 && !gridAngleInArc (aA)) { discard; }" EOL + " if (uBounds.x > 0.0)" EOL " {" EOL + // Keep the bounded area geometrically strict; fwidth is used only for the + // optional fade inside the boundary, never to expand the valid area. " float aFwBX = fwidth (abs (aLocal.x));" EOL - " if (abs (aLocal.x) > uBounds.x + aFwBX) { discard; }" EOL - " aBoundFade *= 1.0 - smoothstep (GRID_FADE_BAND * uBounds.x, uBounds.x + aFwBX, abs " - "(aLocal.x));" EOL " }" EOL " if (uBounds.y > 0.0)" EOL " {" EOL + " if (abs (aLocal.x) > uBounds.x) { discard; }" EOL " if (uIsBoundFade != 0)" EOL + " {" EOL " aBoundFade *= 1.0 - smoothstep (uBounds.x - aFwBX, uBounds.x, abs " + "(aLocal.x));" EOL " }" EOL " }" EOL " if (uBounds.y > 0.0)" EOL " {" EOL " float aFwBY = fwidth (abs (aLocal.y));" EOL - " if (abs (aLocal.y) > uBounds.y + aFwBY) { discard; }" EOL - " aBoundFade *= 1.0 - smoothstep (GRID_FADE_BAND * uBounds.y, uBounds.y + aFwBY, abs " - "(aLocal.y));" EOL " }" + " if (abs (aLocal.y) > uBounds.y) { discard; }" EOL " if (uIsBoundFade != 0)" EOL + " {" EOL " aBoundFade *= 1.0 - smoothstep (uBounds.y - aFwBY, uBounds.y, abs " + "(aLocal.y));" EOL " }" EOL " }" // Grid coordinates depend on uGridType: 0=rectangular (X/Y), 1=circular (radius/angle). - // For rectangular grid, rebase UVs around a CPU-provided stable reference - // (same for all fragments in the frame) to keep fract() arguments small. - EOL " vec2 aStableLocal = aLocal;" EOL " if (uGridType == 0)" EOL " {" EOL - " if (uHasStableRef != 0)" EOL " {" EOL - " vec2 aScaleSafe = max (abs (vec2 (uScaleX, uScaleY)), vec2 (1e-9));" EOL - " vec2 aShift = floor (uStableRefLocal * aScaleSafe) / aScaleSafe;" EOL - " aStableLocal = aLocal - aShift;" EOL " }" EOL " }" EOL " vec2 aGridUv;" EOL - " vec2 aScale;" EOL " vec2 aAxisUv;" EOL " if (uGridType == 1)" EOL " {" EOL - " aGridUv = vec2 (aR, aA);" EOL " aScale = vec2 (uScaleX, uAngularScale);" EOL - " aAxisUv = aLocal;" EOL " }" EOL " else" EOL " {" EOL " aGridUv = aStableLocal;" EOL + EOL " vec2 aGridUv;" EOL " vec2 aScale;" EOL " vec2 aAxisUv;" EOL " if (uGridType == 1)" EOL + " {" EOL " aGridUv = vec2 (aR, aA);" EOL " aScale = vec2 (uScaleX, uAngularScale);" EOL + " aAxisUv = aLocal;" EOL " }" EOL " else" EOL " {" EOL " aGridUv = aLocalGrid;" EOL " aScale = vec2 (uScaleX, uScaleY);" EOL " aAxisUv = aLocal;" EOL " }" EOL - " vec4 aColor = gridLines2d (aGridUv, aAxisUv, uColor, aScale, uGridType == 0, " - "uThickness);" EOL " if (uDrawMode != 1)" EOL " {" EOL " if (uGridType == 0)" EOL + " vec2 aGridShift = uGridType == 1 ? vec2 (uRadialOriginShift, 0.0) : vec2 (0.0);" EOL + " vec4 aColor = gridLines2d (aGridUv, aAxisUv, uColor, aScale, aGridShift, " + "uGridType == 0, uThickness);" EOL " if (uDrawMode != 1)" EOL " {" EOL + " if (uGridType == 0)" EOL " {" EOL " if (uAccentScaleX > 0.0)" EOL " {" EOL + " aColor = overlayGrid (aColor, gridLines1d (aLocal.x, uAccentLocalOriginShift.x, " + "uAccentScaleX, uAccentColor, uThickness));" EOL " }" EOL + " if (uAccentScaleY > 0.0)" EOL " {" EOL + " aColor = overlayGrid (aColor, gridLines1d (aLocal.y, uAccentLocalOriginShift.y, " + "uAccentScaleY, uAccentColor, uThickness));" EOL " }" EOL " }" EOL " else" EOL " {" EOL " if (uAccentScaleX > 0.0)" EOL " {" EOL - " float aAccX = aLocal.x;" EOL " if (uHasStableRef != 0)" EOL " {" EOL - " float aScaleAccX = max (abs (uAccentScaleX), 1e-9);" EOL - " float aShiftAccX = floor (uStableRefLocal.x * aScaleAccX) / aScaleAccX;" EOL - " aAccX -= aShiftAccX;" EOL " }" EOL - " aColor = overlayGrid (aColor, gridLines1d (aAccX, uAccentScaleX, uAccentColor, " - "uThickness));" EOL " }" EOL " if (uAccentScaleY > 0.0)" EOL " {" EOL - " float aAccY = aLocal.y;" EOL " if (uHasStableRef != 0)" EOL " {" EOL - " float aScaleAccY = max (abs (uAccentScaleY), 1e-9);" EOL - " float aShiftAccY = floor (uStableRefLocal.y * aScaleAccY) / aScaleAccY;" EOL - " aAccY -= aShiftAccY;" EOL " }" EOL - " aColor = overlayGrid (aColor, gridLines1d (aAccY, uAccentScaleY, uAccentColor, " - "uThickness));" EOL " }" EOL " }" EOL " else" EOL " {" EOL - " if (uAccentScaleX > 0.0)" EOL " {" EOL - " aColor = overlayGrid (aColor, gridLines1d (aR, uAccentScaleX, uAccentColor, " - "uThickness));" EOL " }" EOL " if (uAccentAngularScale > 0.0)" EOL " {" EOL - " aColor = overlayGrid (aColor, gridLines1d (aA, uAccentAngularScale, uAccentColor, " - "uThickness));" EOL " }" EOL " }" EOL + " aColor = overlayGrid (aColor, gridLines1d (aR, uAccentRadialOriginShift, " + "uAccentScaleX, uAccentColor, uThickness));" EOL " }" EOL + " if (uAccentAngularScale > 0.0)" EOL " {" EOL + " aColor = overlayGrid (aColor, gridLines1d (aA, 0.0, uAccentAngularScale, " + "uAccentColor, uThickness));" EOL " }" EOL " }" EOL // For circular grid, paint X/Y axis lines explicitly using plane-local coords. " if (uIsDrawAxis != 0 && uGridType == 1)" EOL " {" EOL - " vec2 aDerivAxis = max (fwidth (aAxisUv), vec2 (uThickness));" EOL - " if (abs (aAxisUv.x) < aDerivAxis.x && abs (aAxisUv.y) < aDerivAxis.y)" EOL " {" EOL + " float anAxisX = axisLine1d (aAxisUv.y, uThickness);" EOL + " float anAxisY = axisLine1d (aAxisUv.x, uThickness);" EOL + " if (anAxisX > 0.0 && anAxisY > 0.0)" EOL " {" EOL " aColor = vec4 (0.0, 0.0, 1.0, 1.0);" EOL " }" EOL - " else if (abs (aAxisUv.y) < aDerivAxis.y)" EOL " {" EOL - " aColor = vec4 (1.0, 0.0, 0.0, 1.0);" EOL " }" EOL - " else if (abs (aAxisUv.x) < aDerivAxis.x)" EOL " {" EOL - " aColor = vec4 (0.0, 1.0, 0.0, 1.0);" EOL " }" EOL " }" EOL " }" + " else if (anAxisX > 0.0)" EOL " {" EOL + " aColor = vec4 (1.0, 0.0, 0.0, max (aColor.a, anAxisX));" EOL " }" EOL + " else if (anAxisY > 0.0)" EOL " {" EOL + " aColor = vec4 (0.0, 1.0, 0.0, max (aColor.a, anAxisY));" EOL " }" EOL " }" EOL + " }" - EOL " float aDepth = computeDepth (aHit);" EOL " float aFar = gl_DepthRange.far;" EOL - " float aNear = gl_DepthRange.near;" EOL - " aDepth = ((aFar - aNear) * aDepth + aNear + aFar) * 0.5;" - // No aT-sign discard: when the grid plane passes through / close to the - // near clip (the "grid going into camera view" case), aT can be <= 0 for - // fragments whose Near point is on the opposite side of the plane. The ray - // still intersects the plane; we just clamp depth via gl_FragDepth's [0,1] - // range so those fragments read as "in front of everything", which reads - // naturally as the ground/plane reaching under the camera. - EOL " if (aColor.a == 0.0) { discard; }" + EOL " if (aColor.a == 0.0) { discard; }" EOL " aColor.a *= aBoundFade;" EOL + " occFragColor = aColor;" EOL "}"; - EOL " float aMaxDepth = 1.0 - GRID_DEPTH_EPS;" EOL - " gl_FragDepth = uIsBackground != 0 ? aMaxDepth : min (aDepth, aMaxDepth);" EOL - " aColor.a *= aBoundFade;" EOL " occFragColor = aColor;" EOL "}"; - - // Requires gl_VertexID (GL 3.0/ES 3.0+), fwidth, gl_FragDepth. + // Requires gl_VertexID (GL 3.0/ES 3.0+) and fwidth. if (myGapi == Aspect_GraphicsLibrary_OpenGL) { aProgSrc->SetHeader(IsGapiGreaterEqual(3, 2) ? "#version 150" : "#version 130"); diff --git a/src/Visualization/TKV3d/AIS/AIS_ViewController.cxx b/src/Visualization/TKV3d/AIS/AIS_ViewController.cxx index de3e05979c..581ecdd1fc 100644 --- a/src/Visualization/TKV3d/AIS/AIS_ViewController.cxx +++ b/src/Visualization/TKV3d/AIS/AIS_ViewController.cxx @@ -2264,11 +2264,22 @@ void AIS_ViewController::handleCameraActions(const occ::handle& theView, const AIS_WalkDelta& theWalk) { + const bool hasGridEcho = theView->IsGridActive() && theView->Viewer()->GridEcho(); + const bool hasShaderGridEcho = hasGridEcho && theView->IsShaderGridActive(); + NCollection_Vec2 aMoveToAfterCamera = + HasPreviousMoveTo() ? PreviousMoveTo() : LastMousePosition(); + bool toUpdateMoveToAfterCamera = hasShaderGridEcho && !theWalk.IsEmpty(); + bool toHideGridEchoAfterCamera = false; + const bool isMouseRotation = + (myGL.OrbitRotation.ToRotate || myGL.ViewRotation.ToRotate || myGL.ZRotate.ToRotate) + && myToAllowRotation; + // apply view actions if (myGL.Orientation.ToSetViewOrient) { theView->SetProj(myGL.Orientation.ViewOrient); myGL.Orientation.ToFitAll = true; + toUpdateMoveToAfterCamera = hasShaderGridEcho; } // apply fit all @@ -2278,6 +2289,7 @@ void AIS_ViewController::handleCameraActions(const occ::handleFitAll(aFitMargin, false); theView->Invalidate(); myGL.Orientation.ToFitAll = false; + toUpdateMoveToAfterCamera = hasShaderGridEcho; } NCollection_List> anObjects; @@ -2360,6 +2372,14 @@ void AIS_ViewController::handleCameraActions(const occ::handleCamera()->IsOrthographic()) { @@ -2456,6 +2488,7 @@ void AIS_ViewController::handleCameraActions(const occ::handleViewer()->HideGridEcho(theView); + theView->InvalidateImmediate(); + ResetPreviousMoveTo(); + } + else if (toUpdateMoveToAfterCamera) + { + int aWidth = 0, aHeight = 0; + theView->Window()->Size(aWidth, aHeight); + if (aMoveToAfterCamera.x() >= 0 && aMoveToAfterCamera.x() < aWidth + && aMoveToAfterCamera.y() >= 0 && aMoveToAfterCamera.y() < aHeight) + { + ResetPreviousMoveTo(); + contextLazyMoveTo(theCtx, theView, aMoveToAfterCamera); + } + } } //================================================================================================= @@ -2865,14 +2918,19 @@ void AIS_ViewController::contextLazyMoveTo(const occ::handleCamera()->SetZRange(aZNear, aZFar); occ::handle aNewPicked = theCtx->DetectedOwner(); - - if (theView->Viewer()->IsGridActive() && theView->Viewer()->GridEcho()) + if (theView->IsGridActive() && theView->Viewer()->GridEcho()) { if (aNewPicked.IsNull()) { - NCollection_Vec3 aPnt3d; - theView->ConvertToGrid(thePnt.x(), thePnt.y(), aPnt3d[0], aPnt3d[1], aPnt3d[2]); - theView->Viewer()->ShowGridEcho(theView, Graphic3d_Vertex(aPnt3d[0], aPnt3d[1], aPnt3d[2])); + Graphic3d_Vertex aGridPoint, anEchoPoint; + if (theView->ConvertToGridEcho(thePnt.x(), thePnt.y(), aGridPoint, anEchoPoint)) + { + theView->Viewer()->ShowGridEcho(theView, anEchoPoint); + } + else + { + theView->Viewer()->HideGridEcho(theView); + } theView->InvalidateImmediate(); } else diff --git a/src/Visualization/TKV3d/V3d/V3d_View.cxx b/src/Visualization/TKV3d/V3d/V3d_View.cxx index aaca04fec2..14330c135a 100644 --- a/src/Visualization/TKV3d/V3d/V3d_View.cxx +++ b/src/Visualization/TKV3d/V3d/V3d_View.cxx @@ -445,10 +445,9 @@ void V3d_View::AutoZFit() const void V3d_View::ZFitAll(const double theScaleFactor) const { - Bnd_Box aMinMaxBox = myView->MinMaxValues(false); // applicative min max boundaries - // clang-format off - Bnd_Box aGraphicBox = myView->MinMaxValues (true); // real graphical boundaries (not accounting infinite flag). - // clang-format on + Bnd_Box aMinMaxBox; + Bnd_Box aGraphicBox; + myView->ZFitAllBounds(aMinMaxBox, aGraphicBox); myView->Camera()->ZFitAll(theScaleFactor, aMinMaxBox, aGraphicBox); } @@ -1785,6 +1784,29 @@ void V3d_View::ConvertToGrid(const int theXp, double& theYg, double& theZg) const { + Graphic3d_Vertex aGridPoint; + if (ConvertToGrid(theXp, theYp, aGridPoint)) + { + aGridPoint.Coord(theXg, theYg, theZg); + return; + } + + NCollection_Vec3 anXYZ; + Convert(theXp, theYp, anXYZ.x(), anXYZ.y(), anXYZ.z()); + theXg = anXYZ.x(); + theYg = anXYZ.y(); + theZg = anXYZ.z(); +} + +//================================================================================================= + +bool V3d_View::ConvertToGrid(const int theXp, const int theYp, Graphic3d_Vertex& theGridPoint) const +{ + if (myShaderGridActive) + { + return myView->ShaderGridEcho(theXp, theYp, theGridPoint); + } + NCollection_Vec3 anXYZ; Convert(theXp, theYp, anXYZ.x(), anXYZ.y(), anXYZ.z()); @@ -1792,13 +1814,32 @@ void V3d_View::ConvertToGrid(const int theXp, aVrp.SetCoord(anXYZ.x(), anXYZ.y(), anXYZ.z()); if (MyViewer->IsGridActive()) { - Graphic3d_Vertex aNewVrp = Compute(aVrp); - aNewVrp.Coord(theXg, theYg, theZg); + theGridPoint = Compute(aVrp); + return true; } - else + + return false; +} + +//================================================================================================= + +bool V3d_View::ConvertToGridEcho(const int theXp, + const int theYp, + Graphic3d_Vertex& theGridPoint, + Graphic3d_Vertex& theEchoPoint) const +{ + if (myShaderGridActive) { - aVrp.Coord(theXg, theYg, theZg); + return myView->ShaderGridEcho(theXp, theYp, theGridPoint, theEchoPoint); } + + if (!ConvertToGrid(theXp, theYp, theGridPoint)) + { + return false; + } + + theEchoPoint = theGridPoint; + return true; } //================================================================================================= @@ -1810,6 +1851,22 @@ void V3d_View::ConvertToGrid(const double theX, double& theYg, double& theZg) const { + if (myShaderGridActive) + { + const Graphic3d_Vertex aPoint(theX, theY, theZ); + Graphic3d_Vertex aShaderGridPoint; + if (myView->ShaderGridSnapPoint(aPoint, aShaderGridPoint)) + { + aShaderGridPoint.Coord(theXg, theYg, theZg); + return; + } + + theXg = theX; + theYg = theY; + theZg = theZ; + return; + } + if (MyViewer->IsGridActive()) { Graphic3d_Vertex aVrp(theX, theY, theZ); @@ -3523,6 +3580,10 @@ void V3d_View::SetGrid(const gp_Ax3& aPlane, const occ::handle& aGr void V3d_View::SetGridActivity(const bool AFlag) { + if (MyGrid.IsNull()) + { + return; + } if (AFlag) { MyGrid->Activate(); @@ -3544,16 +3605,13 @@ void V3d_View::GridDisplay(const Aspect_GridParams& theParams) void V3d_View::GridDisplay(const Aspect_GridParams& theParams, const gp_Ax3& thePlane) { - // Mutual exclusion: the CPU grid renders into a viewer-wide Graphic3d_Structure - // (visible in every active view), so enabling the per-view shader grid hides the - // CPU rendering at the viewer level. Snap geometry (Aspect_*Grid) is left alive - // and only the structure is erased; SetGrid / ActivateGrid restores it. + myView->GridDisplay(theParams, thePlane); + if (!MyGrid.IsNull() && MyGrid->IsDisplayed()) { MyGrid->Erase(); } - myShaderGridActive = true; - myView->GridDisplay(theParams, thePlane); + myShaderGridActive = theParams.DrawMode() != Aspect_GDM_None; } //================================================================================================= diff --git a/src/Visualization/TKV3d/V3d/V3d_View.hxx b/src/Visualization/TKV3d/V3d/V3d_View.hxx index b73d265ce1..18f8d5bf71 100644 --- a/src/Visualization/TKV3d/V3d/V3d_View.hxx +++ b/src/Visualization/TKV3d/V3d/V3d_View.hxx @@ -36,6 +36,7 @@ class Aspect_Window; class Graphic3d_Group; class Graphic3d_Structure; class Graphic3d_TextureEnv; +class Graphic3d_Vertex; //! Defines the application object VIEW for the //! VIEWER application. @@ -671,6 +672,21 @@ public: double& Yg, double& Zg) const; + //! Converts the projected point into the nearest visible grid point. + //! @return TRUE when an active grid accepts the point; FALSE otherwise. + //! Unlike the double-output overload, this method has no unproject fallback + //! and is intended for grid echo / snap-hit callers. + Standard_EXPORT bool ConvertToGrid(const int Xp, + const int Yp, + Graphic3d_Vertex& theGridPoint) const; + + //! Converts the projected point into the nearest visible grid point and echo display point. + //! The echo display point is suitable only for displaying the grid echo marker. + Standard_EXPORT bool ConvertToGridEcho(const int Xp, + const int Yp, + Graphic3d_Vertex& theGridPoint, + Graphic3d_Vertex& theEchoPoint) const; + //! Converts the point into the nearest grid point //! and display the grid marker. Standard_EXPORT void ConvertToGrid(const double X, @@ -907,11 +923,9 @@ public: const double theResolution = 0.0, const bool theToEnlargeIfLine = true) const; -public: //! @name CPU grid plumbing (deprecated, fed by V3d_Viewer::ActivateGrid) - //! Snap + CPU rendering. The CPU grid lives on the viewer's structure manager - //! and is visible in every active view; SetGrid on a view that has the shader - //! grid enabled erases the shader grid on this view, the CPU grid is left - //! intact (or re-displayed by V3d_Viewer::ActivateGrid). +public: //! @name Viewer grid plumbing + //! Viewer-managed grid plane and snap object. It is separate from the per-view + //! shader grid controlled by GridDisplay(). //! Defines or updates the grid plane and snap object on this view. //! @param[in] aPlane grid plane (origin + axes) @@ -922,11 +936,15 @@ public: //! @name CPU grid plumbing (deprecated, fed by V3d_Viewer::ActivateGrid //! @param[in] aFlag true to enable snap, false to disable Standard_EXPORT void SetGridActivity(const bool aFlag); -public: //! @name GPU shader grid (recommended) - //! Per-view immediate-mode shader; supports unbounded extents, AA, background, arc range. - //! GridDisplay erases the viewer-wide CPU grid rendering on entry (snap geometry - //! on Aspect_*Grid is preserved). GridErase only tears down the shader grid on - //! this view; restoring the CPU rendering needs V3d_Viewer::ActivateGrid. + //! Return TRUE if either viewer-managed grid or per-view shader grid is active. + bool IsGridActive() const { return MyViewer->IsGridActive() || myShaderGridActive; } + + //! Return TRUE if the per-view shader grid is active. + bool IsShaderGridActive() const { return myShaderGridActive; } + +public: //! @name Shader grid + //! Per-view immediate-mode shader grid; supports unbounded extents, AA, background, + //! circular grids, arc range and view-adaptive spacing. //! Display a shader-rendered grid on the viewer's privileged plane. //! @param[in] theParams appearance: color, scale, bounds, arc, draw-mode, background / diff --git a/src/Visualization/TKV3d/V3d/V3d_Viewer.cxx b/src/Visualization/TKV3d/V3d/V3d_Viewer.cxx index d5249d7f19..f481c26058 100644 --- a/src/Visualization/TKV3d/V3d/V3d_Viewer.cxx +++ b/src/Visualization/TKV3d/V3d/V3d_Viewer.cxx @@ -797,8 +797,8 @@ void V3d_Viewer::ShowGridEcho(const occ::handle& theView, myGridEchoGroup->SetPrimitivesAspect(myGridEchoAspect); } - if (theVertex.X() == myGridEchoLastVert.X() && theVertex.Y() == myGridEchoLastVert.Y() - && theVertex.Z() == myGridEchoLastVert.Z()) + if (!theView->IsShaderGridActive() && theVertex.X() == myGridEchoLastVert.X() + && theVertex.Y() == myGridEchoLastVert.Y() && theVertex.Z() == myGridEchoLastVert.Z()) { return; } diff --git a/tests/v3d/grid/gpu_perspective_nomirror b/tests/v3d/grid/gpu_perspective_nomirror new file mode 100644 index 0000000000..54a9926b5f --- /dev/null +++ b/tests/v3d/grid/gpu_perspective_nomirror @@ -0,0 +1,22 @@ +puts "==================================================================" +puts "GPU shader grid keeps foreground plane semantics in perspective view" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 w=400 h=400 +vaxo + +box b 10 10 10 +vdisplay b -dispMode 1 +vfit + +vgrid -type gpu -size 100 100 -drawAxis 0 +vcamera -persp +vdump $imagedir/${casename}_persp.png + +vrotate 0 0 0.5 +vdump $imagedir/${casename}_rotated.png + +vgrid off diff --git a/tests/v3d/grid/gpu_visibility b/tests/v3d/grid/gpu_visibility new file mode 100644 index 0000000000..52660100ad --- /dev/null +++ b/tests/v3d/grid/gpu_visibility @@ -0,0 +1,20 @@ +puts "==================================================================" +puts "GPU shader grid is visible without scene objects and without vfit" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 w=400 h=400 +vaxo + +# Empty view: shader grid must not depend on scene bounding boxes. +vgrid -type gpu -size 100 100 -drawAxis 0 +vdump $imagedir/${casename}_empty.png + +# Adding an object without vfit must not be required to make the grid visible. +box b 10 10 10 +vdisplay b -dispMode 1 +vdump $imagedir/${casename}_display_no_vfit.png + +vgrid off