diff --git a/application/testing/CMakeLists.txt b/application/testing/CMakeLists.txt index b1aba24464..bad7aecd70 100644 --- a/application/testing/CMakeLists.txt +++ b/application/testing/CMakeLists.txt @@ -129,6 +129,9 @@ f3d_test(NAME TestNRRD DATA beach.nrrd ARGS -s) f3d_test(NAME TestGridX DATA suzanne.ply ARGS -g --up=+X) f3d_test(NAME TestGridY DATA suzanne.ply ARGS -g --up=+Y) f3d_test(NAME TestGridZ DATA suzanne.ply ARGS -g --up=+Z) +f3d_test(NAME TestGridUp123 DATA suzanne.ply ARGS -g --up=1,2,3) +f3d_test(NAME TestGridUp100 DATA suzanne.ply ARGS -g --up=1,0,0) +f3d_test(NAME TestGridUp000 DATA suzanne.ply ARGS -g --up=0,0,0) f3d_test(NAME TestGridOptions DATA suzanne.ply ARGS -g --camera-elevation-angle=45 --grid-unit=2 --grid-subdivisions=3) f3d_test(NAME TestGridAbsolute DATA f3d.vtp ARGS -g --up=-Y --camera-direction=-.5,+1,+1 --grid-absolute) f3d_test(NAME TestGridClipping DATA offset-flat-box.glb ARGS -g --grid-absolute --camera-position=70,120,350) diff --git a/testing/baselines/TestGridUp000.png b/testing/baselines/TestGridUp000.png new file mode 100644 index 0000000000..2478e92502 --- /dev/null +++ b/testing/baselines/TestGridUp000.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:317358f35dd1d55e215c16c8459ae486bc4aa2cd95a830ea7414039f84ff256b +size 31140 diff --git a/testing/baselines/TestGridUp100.png b/testing/baselines/TestGridUp100.png new file mode 100644 index 0000000000..c0133bf1d6 --- /dev/null +++ b/testing/baselines/TestGridUp100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec32b9deb15aab8273b719daa51545c8a1af611e805261ce2b268e0d2d009d5c +size 24152 diff --git a/testing/baselines/TestGridUp123.png b/testing/baselines/TestGridUp123.png new file mode 100644 index 0000000000..1e30e3b0ba --- /dev/null +++ b/testing/baselines/TestGridUp123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bac662d04fc7d743989c9209bc9787d90ab8cc6062b0734ed481905155a85450 +size 22389 diff --git a/testing/baselines/TestGridX.png b/testing/baselines/TestGridX.png index 3d133b013d..c0133bf1d6 100644 --- a/testing/baselines/TestGridX.png +++ b/testing/baselines/TestGridX.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f77f362288ecec58cce5a81b8ede81f2b9ee4a39a13890944bf5d0143641e17a -size 23757 +oid sha256:ec32b9deb15aab8273b719daa51545c8a1af611e805261ce2b268e0d2d009d5c +size 24152 diff --git a/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx b/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx index e32cd54ffb..d3095bc510 100644 --- a/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx +++ b/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx @@ -62,17 +62,8 @@ int TestF3DOpenGLGridMapper(int argc, char* argv[]) /* `OriginOffset` offset is only for drawing the axes within the actor, * it should not affect the actual bounding box */ - mapper->SetUpIndex(0); - if (!CheckBounds("YZ with offset", mapper, -safeMargin, +safeMargin, -r, +r, -r, +r)) - return EXIT_FAILURE; - - mapper->SetUpIndex(1); if (!CheckBounds("XZ with offset", mapper, -r, +r, -safeMargin, +safeMargin, -r, +r)) return EXIT_FAILURE; - - mapper->SetUpIndex(2); - if (!CheckBounds("XY with offset", mapper, -r, +r, -r, +r, -safeMargin, +safeMargin)) - return EXIT_FAILURE; } return EXIT_SUCCESS; diff --git a/vtkext/private/module/vtkF3DOpenGLGridMapper.cxx b/vtkext/private/module/vtkF3DOpenGLGridMapper.cxx index ee53aa89f5..1ed98634db 100644 --- a/vtkext/private/module/vtkF3DOpenGLGridMapper.cxx +++ b/vtkext/private/module/vtkF3DOpenGLGridMapper.cxx @@ -28,7 +28,6 @@ void vtkF3DOpenGLGridMapper::PrintSelf(ostream& os, vtkIndent indent) os << indent << "FadeDistance: " << this->FadeDistance << "\n"; os << indent << "UnitSquare: " << this->UnitSquare << "\n"; os << indent << "Subdivisions: " << this->Subdivisions << "\n"; - os << indent << "UpIndex: " << this->UpIndex << "\n"; } //---------------------------------------------------------------------------- @@ -40,9 +39,6 @@ void vtkF3DOpenGLGridMapper::ReplaceShaderValues( std::string VSSource = shaders[vtkShader::Vertex]->GetSource(); std::string FSSource = shaders[vtkShader::Fragment]->GetSource(); - const std::string axes3d = this->UpIndex == 0 ? "zyx" : this->UpIndex == 1 ? "xzy" : "xyz"; - const std::string axes2d = this->UpIndex == 0 ? "zy" : this->UpIndex == 1 ? "xz" : "xy"; - // clang-format off vtkShaderProgram::Substitute(VSSource, "//VTK::PositionVC::Dec", "uniform vec3 originOffset;\n" @@ -52,8 +48,8 @@ void vtkF3DOpenGLGridMapper::ReplaceShaderValues( ); vtkShaderProgram::Substitute(VSSource, "//VTK::PositionVC::Impl", "gridCoord = vertexMC.xy * fadeDist;\n" - "gridOffset = originOffset." + axes2d + ";\n" - "gl_Position = MCDCMatrix * vec4(vertexMC." + axes3d + " * fadeDist, 1.0);\n" + "gridOffset = originOffset.xz;\n" + "gl_Position = MCDCMatrix * vec4(vertexMC.xzy * fadeDist, 1.0);\n" ); vtkShaderProgram::Substitute(FSSource, "//VTK::CustomUniforms::Dec", @@ -157,26 +153,8 @@ void vtkF3DOpenGLGridMapper::SetMapperShaderParameters( cellBO.Program->SetUniformf("gridLineWidth", 0.6); cellBO.Program->SetUniformf("minorOpacity", 0.5); cellBO.Program->SetUniformf("lineAntialias", 1); - - const float xColor[4] = { 1, 0, 0, 1 }; - const float yColor[4] = { 0, 1, 0, 1 }; - const float zColor[4] = { 0, 0, 1, 1 }; - switch (this->UpIndex) - { - case 0: - cellBO.Program->SetUniform4f("axis1Color", zColor); - cellBO.Program->SetUniform4f("axis2Color", yColor); - break; - case 1: - cellBO.Program->SetUniform4f("axis1Color", xColor); - cellBO.Program->SetUniform4f("axis2Color", zColor); - break; - case 2: - default: - cellBO.Program->SetUniform4f("axis1Color", xColor); - cellBO.Program->SetUniform4f("axis2Color", yColor); - break; - } + cellBO.Program->SetUniform4f("axis1Color", this->Axis1Color); + cellBO.Program->SetUniform4f("axis2Color", this->Axis2Color); } //---------------------------------------------------------------------------- @@ -210,14 +188,12 @@ void vtkF3DOpenGLGridMapper::BuildBufferObjects(vtkRenderer* ren, vtkActor* vtkN //----------------------------------------------------------------------------- double* vtkF3DOpenGLGridMapper::GetBounds() { - double r[3] = { this->FadeDistance, this->FadeDistance, this->FadeDistance }; - r[this->UpIndex] = 1e-4; - this->Bounds[0] = -r[0]; - this->Bounds[1] = +r[0]; - this->Bounds[2] = -r[1]; - this->Bounds[3] = +r[1]; - this->Bounds[4] = -r[2]; - this->Bounds[5] = +r[2]; + this->Bounds[0] = -this->FadeDistance; + this->Bounds[1] = +this->FadeDistance; + this->Bounds[2] = -1e-4; + this->Bounds[3] = +1e-4; + this->Bounds[4] = -this->FadeDistance; + this->Bounds[5] = +this->FadeDistance; return this->Bounds; } diff --git a/vtkext/private/module/vtkF3DOpenGLGridMapper.h b/vtkext/private/module/vtkF3DOpenGLGridMapper.h index 5eeaadf2a6..a8aee59fc8 100644 --- a/vtkext/private/module/vtkF3DOpenGLGridMapper.h +++ b/vtkext/private/module/vtkF3DOpenGLGridMapper.h @@ -38,9 +38,14 @@ class vtkF3DOpenGLGridMapper : public vtkOpenGLPolyDataMapper vtkSetMacro(Subdivisions, int); /** - * Set the up vector index (X, Y, Z axis respectively). + * Set the color (RGBA) of the first axes */ - vtkSetClampMacro(UpIndex, int, 0, 2); + vtkSetVector4Macro(Axis1Color, float); + + /** + * Set the color (RGBA) of the second axes + */ + vtkSetVector4Macro(Axis2Color, float); using vtkOpenGLPolyDataMapper::GetBounds; double* GetBounds() override; @@ -68,7 +73,8 @@ class vtkF3DOpenGLGridMapper : public vtkOpenGLPolyDataMapper double FadeDistance = 10.0; double UnitSquare = 1.0; int Subdivisions = 10; - int UpIndex = 1; + float Axis1Color[4] = { 0.0, 0.0, 0.0, 1.0 }; + float Axis2Color[4] = { 0.0, 0.0, 0.0, 1.0 }; }; #endif diff --git a/vtkext/private/module/vtkF3DRenderer.cxx b/vtkext/private/module/vtkF3DRenderer.cxx index 08ff8f39d7..07239d9067 100644 --- a/vtkext/private/module/vtkF3DRenderer.cxx +++ b/vtkext/private/module/vtkF3DRenderer.cxx @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +33,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -226,6 +231,9 @@ void vtkF3DRenderer::Initialize(const std::string& up) // Importer rely on the Environment being set, so this is needed in the initialization const std::regex re("([-+]?)([XYZ])", std::regex_constants::icase); + const std::regex re2("([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+))," + "([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+))," + "([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+))"); std::smatch match; if (std::regex_match(up, match, re)) { @@ -233,32 +241,24 @@ void vtkF3DRenderer::Initialize(const std::string& up) const int index = std::toupper(match[2].str()[0]) - 'X'; assert(index >= 0 && index < 3); - this->UpIndex = index; + std::array upDir = { 0, 0, 0 }; + upDir[index] = sign; - std::fill(this->UpVector, this->UpVector + 3, 0); - this->UpVector[this->UpIndex] = sign; + std::array rightDir = { 0, 0, 0 }; + rightDir[index == 0 ? 1 : 0] = 1.0; - std::fill(this->RightVector, this->RightVector + 3, 0); - this->RightVector[this->UpIndex == 0 ? 1 : 0] = 1.0; - - double pos[3]; - vtkMath::Cross(this->UpVector, this->RightVector, pos); - vtkMath::MultiplyScalar(pos, -1.0); - - vtkCamera* cam = this->GetActiveCamera(); - cam->SetFocalPoint(0.0, 0.0, 0.0); - cam->SetPosition(pos); - cam->SetViewUp(this->UpVector); - - // skybox orientation - double front[3]; - vtkMath::Cross(this->RightVector, this->UpVector, front); - this->SkyboxActor->SetFloorPlane(this->UpVector[0], this->UpVector[1], this->UpVector[2], 0.0); - this->SkyboxActor->SetFloorRight(front[0], front[1], front[2]); + this->InitializeEnvironment(upDir, rightDir); + } + else if (std::regex_match(up, match, re2)) + { + const std::array upDir = { + std::stod(match[1].str()), // + std::stod(match[4].str()), // + std::stod(match[7].str()), // + }; + const std::array rightDir = { 1, 0, 0 }; - // environment orientation - this->SetEnvironmentUp(this->UpVector); - this->SetEnvironmentRight(this->RightVector); + this->InitializeEnvironment(upDir, rightDir); } else { @@ -266,6 +266,69 @@ void vtkF3DRenderer::Initialize(const std::string& up) } } +//---------------------------------------------------------------------------- +void vtkF3DRenderer::InitializeEnvironment( + const std::array& upDir, const std::array& rightDir) +{ + const auto isNullVector = [](const std::array& v) + { + constexpr double e = 1e-8; + return ::abs(v[0]) < e && ::abs(v[1]) < e && ::abs(v[2]) < e; + }; + + /* if `up` is `(0,0,0)` make it `(0,1,0)` */ + std::array up = upDir; + if (isNullVector(up)) + { + up[1] = 1.0; + } + vtkMath::Normalize(up.data()); + + /* make sure `right` is not `(0,0,0)` or colinear with `up` */ + std::array right = rightDir; + vtkMath::Normalize(right.data()); + for (size_t i = 0; (isNullVector(right) || ::abs(vtkMath::Dot(right, up)) > 0.999) && i < 3; ++i) + { + right[0] = 0; + right[1] = 0; + right[2] = 0; + right[i] = 1; + } + + /* make `front` orthogonal */ + std::array front; + vtkMath::Cross(right.data(), up.data(), front.data()); + vtkMath::Normalize(front.data()); + + /* ensure `right` is orthogonal */ + vtkMath::Cross(up.data(), front.data(), right.data()); + vtkMath::Normalize(right.data()); + + this->UpVector[0] = up[0]; + this->UpVector[1] = up[1]; + this->UpVector[2] = up[2]; + this->RightVector[0] = right[0]; + this->RightVector[1] = right[1]; + this->RightVector[2] = right[2]; + + double pos[3]; + vtkMath::Cross(this->UpVector, this->RightVector, pos); + vtkMath::MultiplyScalar(pos, -1.0); + + vtkCamera* cam = this->GetActiveCamera(); + cam->SetFocalPoint(0.0, 0.0, 0.0); + cam->SetPosition(pos); + cam->SetViewUp(this->UpVector); + + // skybox orientation + this->SkyboxActor->SetFloorPlane(this->UpVector[0], this->UpVector[1], this->UpVector[2], 0.0); + this->SkyboxActor->SetFloorRight(front[0], front[1], front[2]); + + // environment orientation + this->SetEnvironmentUp(this->UpVector); + this->SetEnvironmentRight(this->RightVector); +} + //---------------------------------------------------------------------------- void vtkF3DRenderer::ConfigureRenderPasses() { @@ -500,10 +563,24 @@ void vtkF3DRenderer::ConfigureGridUsingCurrentActors() bool show = this->GridVisible; if (show) { - double bounds[6]; - this->ComputeVisiblePropBounds(bounds); - - vtkBoundingBox bbox(bounds); + double* up = this->GetEnvironmentUp(); + double* right = this->GetEnvironmentRight(); + double front[3]; + vtkMath::Cross(right, up, front); + + vtkNew upMatrix; + const double m[16] = { + right[0], right[1], right[2], 0, // + up[0], up[1], up[2], 0, // + front[0], front[1], front[2], 0, // + 0, 0, 0, 1, // + }; + upMatrix->DeepCopy(m); + vtkNew upMatrixInv; + upMatrixInv->DeepCopy(upMatrix); + upMatrixInv->Transpose(); // matrix is orthonormal, no need to use `Invert()` + + const vtkBoundingBox bbox = this->ComputeVisiblePropOrientedBounds(upMatrix); if (!bbox.IsValid()) { @@ -518,26 +595,30 @@ void vtkF3DRenderer::ConfigureGridUsingCurrentActors() tmpUnitSquare = pow(10.0, round(log10(diag * 0.1))); } - double gridPos[3] = { 0, 0, 0 }; + double center[4] = { 0, 0, 0, 1 }; + bbox.GetCenter(center); + + double* gridPos = upMatrixInv->MultiplyDoublePoint(center); + + double downShift = 0; if (this->GridAbsolute) { - for (int i = 0; i < 3; i++) - { - gridPos[i] = this->UpVector[i] ? 0 : 0.5 * (bounds[2 * i] + bounds[2 * i + 1]); - } + double origin[3] = { 0, 0, 0 }; + downShift += vtkPlane::DistanceToPlane(center, up, origin); } else { - for (int i = 0; i < 3; i++) - { - // a small margin is added to the size to avoid z-fighting if large translucent - // triangles are exactly aligned with the grid bounds - constexpr double margin = 1.0001; - double size = margin * (bounds[2 * i + 1] - bounds[2 * i]); - gridPos[i] = 0.5 * (bounds[2 * i] + bounds[2 * i + 1] - this->UpVector[i] * size); - } + // a small margin is added to the size to avoid z-fighting if large translucent + // triangles are exactly aligned with the grid bounds + constexpr double margin = 0.0001; + downShift += bbox.GetLength(1) / 2 + margin; } + double delta[3]; + this->GetEnvironmentUp(delta); + vtkMath::MultiplyScalar(delta, downShift); + vtkMath::Subtract(gridPos, delta, gridPos); + std::stringstream stream; stream << "Using grid unit square size = " << tmpUnitSquare << "\n" << "Grid origin set to [" << gridPos[0] << ", " << gridPos[1] << ", " << gridPos[2] @@ -548,13 +629,20 @@ void vtkF3DRenderer::ConfigureGridUsingCurrentActors() gridMapper->SetFadeDistance(diag); gridMapper->SetUnitSquare(tmpUnitSquare); gridMapper->SetSubdivisions(this->GridSubdivisions); - gridMapper->SetUpIndex(this->UpIndex); + if (this->GridAbsolute) - gridMapper->SetOriginOffset(-gridPos[0], -gridPos[1], -gridPos[2]); + gridMapper->SetOriginOffset(-center[0], -center[1], -center[2]); + + double orientation[3]; + vtkTransform::GetOrientation(orientation, upMatrixInv); + this->GridActor->SetOrientation(orientation); + this->GridActor->SetPosition(gridPos); this->GridActor->GetProperty()->SetColor(this->GridColor); + gridMapper->SetAxis1Color(::abs(right[0]), ::abs(right[1]), ::abs(right[2]), 1); + gridMapper->SetAxis2Color(::abs(front[0]), ::abs(front[1]), ::abs(front[2]), 1); + this->GridActor->ForceTranslucentOn(); - this->GridActor->SetPosition(gridPos); this->GridActor->SetMapper(gridMapper); this->GridActor->UseBoundsOff(); this->GridActor->PickableOff(); @@ -1548,6 +1636,109 @@ void vtkF3DRenderer::ResetCameraClippingRange() this->GridActor->SetUseBounds(gridUseBounds); } +vtkBoundingBox vtkF3DRenderer::ComputeVisiblePropOrientedBounds(const vtkMatrix4x4* matrix) +{ + const auto isMatrixAxisAligned = [](const vtkMatrix4x4* m, const double tol = 1e-8) + { + for (size_t i = 0; i < 3; ++i) + { + size_t nonzerosI = 0; + size_t nonzerosJ = 0; + for (size_t j = 0; j < 3; ++j) + { + if (::abs(m->Element[i][j]) > tol) + nonzerosI++; + if (::abs(m->Element[j][i]) > tol) + nonzerosJ++; + } + if (nonzerosI > 1 || nonzerosJ > 1) + return false; + } + return true; + }; + + /* Use `PokeMatrix` around the call to `GetBounds()` to extend box. + * Only gives the thightest bounds if the transformation is axis-aligned. */ + const auto extendBoxAxisAligned = [&](vtkProp3D* prop3d, vtkBoundingBox& box) + { + vtkNew tmpMatrix; + vtkMatrix4x4::Multiply4x4(matrix, prop3d->GetMatrix(), tmpMatrix); + prop3d->PokeMatrix(tmpMatrix); + + box.AddBounds(prop3d->GetBounds()); + + prop3d->PokeMatrix(nullptr); + }; + + /* Use custom logic to extend box. + * Should give the tightest bounds even when non-axis-aligned */ + const auto extendBoxArbitrary = [&](vtkProp3D* prop3d, vtkBoundingBox& box) + { + vtkActor* actor = vtkActor::SafeDownCast(prop3d); + if (actor) + { + vtkPolyDataMapper* polyMapper = vtkPolyDataMapper::SafeDownCast(actor->GetMapper()); + if (polyMapper) + { + vtkNew tmpMatrix; + vtkMatrix4x4::Multiply4x4(matrix, actor->GetMatrix(), tmpMatrix); + vtkPolyData* polydata = polyMapper->GetInput(); + if (polydata) + { + double p[4] = { 0, 0, 0, 1 }; + double q[4]; + for (vtkIdType i = 0; i < polydata->GetNumberOfPoints(); ++i) + { + polydata->GetPoint(i, p); + tmpMatrix->MultiplyPoint(p, q); + box.AddPoint(q); + } + return true; + } + } + } + return false; + }; + + const bool isAxisAligned = isMatrixAxisAligned(matrix); + vtkBoundingBox box; + + /* use `ComputeVisiblePropBounds()`'s logic to iterate `vtkProp3D`s contributing to the bounds */ + vtkProp* prop; + vtkCollectionSimpleIterator pit; + for (this->Props->InitTraversal(pit); (prop = this->Props->GetNextProp(pit));) + { + if (prop->GetVisibility() && prop->GetUseBounds()) + { + const double* bounds = prop->GetBounds(); + if (bounds != nullptr && vtkMath::AreBoundsInitialized(bounds)) + { + vtkProp3D* prop3d = vtkProp3D::SafeDownCast(prop); + if (prop3d) + { + if (isAxisAligned) + { + extendBoxAxisAligned(prop3d, box); + } + else + { + if (!extendBoxArbitrary(prop3d, box)) + { + const std::string repr = std::string(prop3d->GetClassName()); + F3DLog::Print(F3DLog::Severity::Warning, + "Could not properly account for " + repr + + " in non-axis-aligned bounds computation"); + extendBoxAxisAligned(prop3d, box); + } + } + } + } + } + } + + return box; +} + //---------------------------------------------------------------------------- int vtkF3DRenderer::UpdateLights() { diff --git a/vtkext/private/module/vtkF3DRenderer.h b/vtkext/private/module/vtkF3DRenderer.h index 58cf3217db..53a52bc651 100644 --- a/vtkext/private/module/vtkF3DRenderer.h +++ b/vtkext/private/module/vtkF3DRenderer.h @@ -11,6 +11,7 @@ #ifndef vtkF3DRenderer_h #define vtkF3DRenderer_h +#include #include #include @@ -133,6 +134,19 @@ class vtkF3DRenderer : public vtkOpenGLRenderer */ virtual void Initialize(const std::string& up); + /** + * Initialize the environment orientation (camera, skybox, axes). + * The `up` direction will be normalized. + * The `right` direction will be normalized and may be adjusted to ensure it is orthogonal. + */ + virtual void InitializeEnvironment( + const std::array& upDir, const std::array& rightDir); + + /** + * Compute bounds of visible props as transformed by given matrix. + */ + vtkBoundingBox ComputeVisiblePropOrientedBounds(const vtkMatrix4x4*); + /** * Get the OpenGL skybox */ @@ -295,7 +309,6 @@ class vtkF3DRenderer : public vtkOpenGLRenderer bool InvertZoom = false; int RaytracingSamples = 0; - int UpIndex = 1; double UpVector[3] = { 0.0, 1.0, 0.0 }; double RightVector[3] = { 1.0, 0.0, 0.0 }; double CircleOfConfusionRadius = 20.0;