diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4e2d9cbe..6c39d246 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -98,6 +98,9 @@ if(ARMADILLO_FOUND) add_executable(scatter_hex_mercator scatter_hex_mercator.cpp) target_link_libraries(scatter_hex_mercator OpenGL::GL glfw Freetype::Freetype) + add_executable(scatter3 scatter3.cpp) + target_link_libraries(scatter3 OpenGL::GL glfw Freetype::Freetype) + add_executable(rosenbrock rosenbrock.cpp) target_compile_definitions(rosenbrock PUBLIC FLT=float) target_link_libraries(rosenbrock OpenGL::GL glfw Freetype::Freetype) @@ -369,6 +372,12 @@ endif() add_executable(ellipsoid ellipsoid.cpp) target_link_libraries(ellipsoid OpenGL::GL glfw Freetype::Freetype) +add_executable(component_models component_models.cpp) +target_link_libraries(component_models OpenGL::GL glfw Freetype::Freetype) + +add_executable(component_models2 component_models2.cpp) +target_link_libraries(component_models2 OpenGL::GL glfw Freetype::Freetype) + # constexpr code won't work on Apple Clang if(NOT APPLE) add_executable(icosahedron icosahedron.cpp) diff --git a/examples/component_models.cpp b/examples/component_models.cpp new file mode 100644 index 00000000..3f207626 --- /dev/null +++ b/examples/component_models.cpp @@ -0,0 +1,66 @@ +/* + * "Component model" example. + * + * This is an example of building a VisualModel that contains another VisualModel as a component. + * + * IHaveAComonentVisual visualizes an ellipsoid with normals all in one VisualModel. The + * NormalsVisual is a component of IHaveAComonentVisual. + * + * \author Seb James + * \date 27 December 2025 + */ +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +// This VisualModel draws an ellipsoid, and has a component model (NormalsVisual) that draws arrows for the normals. +template +struct IHaveAComponentVisual : public mplot::VisualModel +{ + IHaveAComponentVisual (const sm::vec _offset) + { + this->viewmatrix.translate (_offset); + + // The NormalsVisual is the component + auto nrm = std::make_unique> (this); + // NB: You DON'T bindmodel() a component model at this point. components will use the binding of the owning VM + // NB: ALSO, you don't finalize before adding. The owning VM's finalize will call the component finalize() + this->nrms = this->addVisualModel (nrm); // bindmodel and finalize have to happen when IHaveAComponentVisual::finalize runs + } + + // Holding a pointer to the component allows access to its features by client code + mplot::NormalsVisual* nrms = nullptr; + + void initializeVertices() + { + sm::mat44 tr; + tr.rotate (sm::vec<>::uz(), sm::mathconst::pi_over_4); + this->computeEllipsoid (sm::vec{0}, + mplot::colour::royalblue, + mplot::colour::maroon3, + sm::vec{1,2,3}, + 40, 40, tr); + } +}; + +int main() +{ + mplot::Visual v(1024, 768, "Component model"); + v.lightingEffects (true); + + // When you *use* a component model in client code, you can't tell any difference: + auto pvm = std::make_unique> (sm::vec<>{}); // just like usual + v.bindmodel (pvm); // just as you always bindmodel here + pvm->finalize(); // as usual, finalize before addVisualModel + v.addVisualModel (pvm); // as usual + + v.keepOpen(); +} diff --git a/examples/component_models2.cpp b/examples/component_models2.cpp new file mode 100644 index 00000000..45c97d38 --- /dev/null +++ b/examples/component_models2.cpp @@ -0,0 +1,55 @@ +/* + * This program shows that you can make two separate VisualModels into a combined model in your + * client code. It's not the primary intended way to use component VisualModels, but is left as an + * example. + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +// Quick visual that simply draws ellipsoid +template +class PrimitiveVisual : public mplot::VisualModel +{ +public: + PrimitiveVisual (const sm::vec _offset) { this->viewmatrix.translate (_offset); } + + void initializeVertices() + { + sm::mat44 tr; + tr.rotate (sm::vec<>::uz(), sm::mathconst::pi_over_4); + this->computeEllipsoid (sm::vec{0}, + mplot::colour::royalblue, + mplot::colour::maroon3, + sm::vec{1,2,3}, + 40, 40, tr); + } +}; + +int main() +{ + mplot::Visual v(1024, 768, "Ellipsoid primitive"); + v.lightingEffects (true); + + auto pvm = std::make_unique> (sm::vec<>{}); + v.bindmodel (pvm); + pvm->finalize(); + auto pvmp = v.addVisualModel (pvm); + + // Create an associate normals model + auto nrm = std::make_unique> (pvmp); + v.bindmodel (nrm); + nrm->finalize(); + pvmp->addVisualModel (nrm); // Note we are adding to pvmp, and not to v + + v.keepOpen(); +} diff --git a/examples/scatter3.cpp b/examples/scatter3.cpp new file mode 100644 index 00000000..3a08e529 --- /dev/null +++ b/examples/scatter3.cpp @@ -0,0 +1,72 @@ +/* + * Visualize a test surface + */ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +int main() +{ + int rtn = -1; + + mplot::Visual v(1024, 768, "mplot::ScatterVisual"); + v.zNear = 0.001; + v.showCoordArrows (true); + v.coordArrowsInScene (true); + // Blueish background: + v.bgcolour = {0.6f, 0.6f, 0.8f, 0.5f}; + v.lightingEffects(); + + try { + sm::vec offset = { 0.0, 0.0, 0.0 }; + sm::scale scale1; + scale1.setParams (1.0, 0.0); + + // Note use of sm::vvecs here, which can be passed into + // VisualDataModel::setDataCoords(std::vector>* _coords) + // and setScalarData(const std::vector* _data) + // This is possible because sm::vvec derives from std::vector. + sm::vvec> points(20*20); + sm::vvec data(20*20); + size_t k = 0; + for (int i = -10; i < 10; ++i) { + for (int j = -10; j < 10; ++j) { + float x = 0.1*i; + float y = 0.1*j; + // z is some function of x, y + float z = x * std::exp(-(x*x) - (y*y)); + points[k] = {x, y, z}; + data[k] = z; + k++; + } + } + + auto sv = std::make_unique> (offset); + v.bindmodel (sv); + sv->setDataCoords (&points); + sv->setScalarData (&data); + sv->radiusFixed = 0.03f; + sv->colourScale = scale1; + sv->cm.setType (mplot::ColourMapType::Plasma); + sv->labelIndices = true; + sv->finalize(); + v.addVisualModel (sv); + + v.keepOpen(); + + } catch (const std::exception& e) { + std::cerr << "Caught exception: " << e.what() << std::endl; + rtn = -1; + } + + return rtn; +} diff --git a/mplot/CMakeLists.txt b/mplot/CMakeLists.txt index 830f9412..75ea62b1 100644 --- a/mplot/CMakeLists.txt +++ b/mplot/CMakeLists.txt @@ -91,6 +91,7 @@ install( RingVisual.h RodVisual.h ScatterVisual.h + Scatter3Visual.h SphereVisual.h SphericalProjectionVisual.h TriangleVisual.h diff --git a/mplot/NormalsVisual.h b/mplot/NormalsVisual.h index dfeb1a48..719fcb4e 100644 --- a/mplot/NormalsVisual.h +++ b/mplot/NormalsVisual.h @@ -20,6 +20,8 @@ namespace mplot { this->mymodel = _mymodel; this->viewmatrix = _mymodel->getViewMatrix(); + this->name = "Normals"; + if (!_mymodel->name.empty()) { this->name += " for " + _mymodel->name; } // We create the model's navmesh, in case it wasn't already done this->mymodel->make_navmesh(); } diff --git a/mplot/Scatter3Visual.h b/mplot/Scatter3Visual.h new file mode 100644 index 00000000..3b4e2198 --- /dev/null +++ b/mplot/Scatter3Visual.h @@ -0,0 +1,192 @@ +/*! + * \file + * + * 3D scatter plot with axes. + * + * \author Seb James + * \date 2019 + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include // axes will be a component + +namespace mplot +{ + //! The template argument Flt is the type of the data which this Scatter3Visual + //! will visualize. + template + class Scatter3Visual : public VisualDataModel + { + public: + Scatter3Visual(const sm::vec _offset) + { + this->viewmatrix.translate (_offset); + this->zScale.setParams (1, 0); + this->colourScale.do_autoscale = true; + // The NormalsVisual is the component + auto tax = std::make_unique>(_offset); + // NB: You DON'T bindmodel() a component model at this point. components will use the binding of the owning VM + // NB: ALSO, you don't finalize before adding. The owning VM's finalize will call the component finalize() + this->axes = this->addVisualModel (tax); // bindmodel and finalize have to happen when IHaveAComponentVisual::finalize runs + } + + // Access to the axes VisualModel + mplot::TriaxesVisual* axes = nullptr; + + void marker (const sm::vec coord, const std::array& clr, const Flt size) + { + if (this->markers == mplot::markerstyle::rod) { + // Draw a rod. markerdirn gives length and dirn. Radius from size + sm::vec hr = this->markerdirn * 0.5f; // half rod + sm::vec rs = coord + hr; + sm::vec re = coord - hr; + this->computeTube (rs, re, clr, clr, size, 12); + } else { + if constexpr (draw_spheres_as_geodesics) { + // Slower than regular computeSphere(). 2 iterations gives 320 faces + this->template computeSphereGeoFast (coord, clr, size); + } else { + // (16+2) * 20 gives 360 faces + this->computeSphere (coord, clr, size, 16, 20); + } + } + } + + //! Quick hack to add an additional point + void add (sm::vec coord, Flt value) + { + std::array clr = this->cm.convert (this->colourScale.transform_one (value)); + this->marker (coord, clr, this->radiusFixed); + this->reinit_buffers(); + } + + //! Additional point with variable size + void add (sm::vec coord, Flt value, Flt size) + { + std::array clr = this->cm.convert (this->colourScale.transform_one (value)); + this->marker (coord, clr, size); + this->reinit_buffers(); + } + + //! Compute spheres for a scatter plot + void initializeVertices() + { + unsigned int ncoords = this->dataCoords == nullptr ? 0 : this->dataCoords->size(); + if (ncoords == 0) { return; } + unsigned int ndata = this->scalarData == nullptr ? 0 : this->scalarData->size(); + // If we have vector data, then manipulate colour accordingly. + unsigned int nvdata = this->vectorData == nullptr ? 0 : this->vectorData->size(); + + if (ndata > 0 && ncoords != ndata) { + std::cout << "Scatter3Visual Error: ncoords ("< 0 && ncoords != nvdata) { + std::cout << "Scatter3Visual Error: ncoords ("< dcopy; + std::vector vdcopy1; + std::vector vdcopy2; + std::vector vdcopy3; + if (ndata && !nvdata) { + dcopy = *(this->scalarData); + this->colourScale.do_autoscale = true; + this->colourScale.transform (*this->scalarData, dcopy); + } else if (nvdata) { + vdcopy1.resize(this->vectorData->size()); + vdcopy2.resize(this->vectorData->size()); + vdcopy3.resize(this->vectorData->size()); + + std::vector dcopy2, dcopy3; + dcopy.resize(this->vectorData->size()); + dcopy2.resize(this->vectorData->size()); + dcopy3.resize(this->vectorData->size()); + + for (unsigned int i = 0; i < this->vectorData->size(); ++i) { + dcopy[i] = (*this->vectorData)[i][0]; + dcopy2[i] = (*this->vectorData)[i][1]; + dcopy3[i] = (*this->vectorData)[i][2]; + } + + this->colourScale.do_autoscale = true; + this->colourScale2.do_autoscale = true; + this->colourScale3.do_autoscale = true; + + this->colourScale.transform (dcopy, vdcopy1); + this->colourScale2.transform (dcopy2, vdcopy2); + this->colourScale3.transform (dcopy3, vdcopy3); + + } // else no scaling required - spheres will be one colour + + for (unsigned int i = 0; i < ncoords; ++i) { + // Scale colour (or use single colour) + std::array clr = this->cm.getHueRGB(); + if (ndata && !nvdata) { + clr = this->cm.convert (dcopy[i]); + } else if (nvdata) { + // Combine colour from two values. vdcopy1, vdcopy2? OR just do RGB for now? + // ColourMap in 'dual hue' (or triple hue) mode. + //std::cout << "Convert colour from vdcopy1[i]: " << vdcopy1[i] << ", vdcopy2[i]: " << vdcopy2[i] << std::endl; + clr = this->cm.convert (vdcopy1[i], vdcopy2[i]); + } + + if (this->sizeFactor == Flt{0}) { + this->marker ((*this->dataCoords)[i], clr, this->radiusFixed); + } else { + this->marker ((*this->dataCoords)[i], clr, dcopy[i] * this->sizeFactor); + } + + if (this->labelIndices == true) { + // Draw an index label... + this->addLabel (std::to_string (i), (*this->dataCoords)[i] + labelOffset, mplot::TextFeatures(labelSize) ); + } + } + } + + // The constexpr, unordered geodesic code is no slower than the regular + // VisualModel::computeSphere(), but leave this off for now (if true, C++-20 is + // required) + static constexpr bool draw_spheres_as_geodesics = false; + + //! Set this->radiusFixed, then re-compute vertices. + void setRadius (float fr) + { + this->radiusFixed = fr; + this->reinit(); + } + + // How to show the scatter points? + markerstyle markers = mplot::markerstyle::sphere; + + // Marker direction, if relevant. Used for length of rod markers + sm::vec markerdirn = sm::vec<>::uz(); + + //! Change this to get larger or smaller spheres. + Flt radiusFixed = Flt{0.05}; + Flt sizeFactor = Flt{0}; + + // Hues for colour control with vectorData + float hue1 = 0.1f; + float hue2 = 0.5f; + float hue3 = -1.0f; + + // Do we add index labels? + bool labelIndices = false; + + sm::vec labelOffset = { 0.04f, 0.0f, 0.0f }; + float labelSize = 0.03f; + }; + +} // namespace mplot diff --git a/mplot/VisualModelBase.h b/mplot/VisualModelBase.h index c2e65d8f..bcde3e0b 100644 --- a/mplot/VisualModelBase.h +++ b/mplot/VisualModelBase.h @@ -90,6 +90,26 @@ namespace mplot { VisualModelBase() {} VisualModelBase (const sm::vec _offset) { this->viewmatrix.translate (_offset); } + virtual ~VisualModelBase() {} // virtual deconstructor to keep clang happy + + // A VisualModel may contain a number of component VisualModels. When render is called, each + // component is rendered. + std::vector>> components; + + //! Add a VisualModel as a component to this VisualModel + template + T* addVisualModel (std::unique_ptr& model) + { + std::unique_ptr> vmp = std::move(model); + vmp->name += " component"; + this->components.push_back (std::move(vmp)); + return static_cast(this->components.back().get()); + } + + // Copy function bindings to the components + virtual void bindComponents() = 0; + + void finalizeComponents() { for (auto& model : this->components) { model->finalize(); } } /*! * Set up the passed-in VisualTextModel with functions that need access to the parent Visual attributes. @@ -402,6 +422,9 @@ namespace mplot // Release context after creating and finalizing this VisualModel. On Visual::render(), // context will be re-acquired. if (this->releaseContext != nullptr) { this->releaseContext (this->parentVis); } + + this->bindComponents(); + this->finalizeComponents(); } //! Render the VisualModel. Note that it is assumed that the OpenGL context has been @@ -420,10 +443,16 @@ namespace mplot virtual void setSceneMatrixTexts (const sm::mat44& sv) = 0; + void setSceneMatrixComponents (const sm::mat44& sv) + { + for (auto& cmp : this->components) { cmp->setSceneMatrix (sv); } + } + //! When setting the scene matrix, also have to set the text's scene matrices. void setSceneMatrix (const sm::mat44& sv) { this->scenematrix = sv; + this->setSceneMatrixComponents (sv); this->setSceneMatrixTexts (sv); } @@ -815,7 +844,14 @@ namespace mplot bool wireframe() const { return this->flags.test (vm_bools::wireframe); } void instanced (const bool val) { this->flags.set (vm_bools::instanced, val); } - bool instanced() const { return this->flags.test (vm_bools::instanced); } + bool instanced() const + { + bool cmpts_inst = this->flags.test (vm_bools::instanced); + for (auto& cmp : this->components) { + if (cmp->instanced()) { cmpts_inst = true; } + } + return cmpts_inst; + } //! Getter for vertex positions (for mplot::NormalsVisual) std::vector getVertexPositions() { return this->vertexPositions; } diff --git a/mplot/VisualModelImplMX.h b/mplot/VisualModelImplMX.h index 861b0a4d..fd1b34c0 100644 --- a/mplot/VisualModelImplMX.h +++ b/mplot/VisualModelImplMX.h @@ -70,6 +70,22 @@ namespace mplot model->releaseContext = &mplot::VisualBase::release_context; } + void bindComponents() + { + for (auto& cmpt : this->components) { + auto model = reinterpret_cast*>(cmpt.get()); + model->set_parent (this->parentVis); + model->get_shaderprogs = this->get_shaderprogs; + model->get_gprog = this->get_gprog; + model->get_tprog = this->get_tprog; + model->instanced_needs_update = this->instanced_needs_update; + model->get_glfn = this->get_glfn; + model->init_instance_data = this->init_instance_data; + model->insert_instance_data = this->insert_instance_data; + model->insert_instparam_data = this->insert_instparam_data; + } + } + void set_instance_data (const sm::vvec>& position) final { sm::vvec> c = { mplot::colour::crimson }; @@ -266,6 +282,9 @@ namespace mplot { if (this->hidden() == true) { return; } + // render any components + for (uint32_t i = 0; i < this->components.size(); ++i) { this->components[i]->render(); } + // Execute post-vertex init at render, as GL should be available. if (this->flags.test (vm_bools::postVertexInitRequired) == true) { this->postVertexInit(); } diff --git a/mplot/VisualModelImplNoMX.h b/mplot/VisualModelImplNoMX.h index 6ef6f8d2..bb7150af 100644 --- a/mplot/VisualModelImplNoMX.h +++ b/mplot/VisualModelImplNoMX.h @@ -66,6 +66,21 @@ namespace mplot model->releaseContext = &mplot::VisualBase::release_context; } + void bindComponents() + { + for (auto& cmpt : this->components) { + auto model = reinterpret_cast*>(cmpt.get()); + model->set_parent (this->parentVis); + model->get_shaderprogs = this->get_shaderprogs; + model->get_gprog = this->get_gprog; + model->get_tprog = this->get_tprog; + model->instanced_needs_update = this->instanced_needs_update; + model->init_instance_data = this->init_instance_data; + model->insert_instance_data = this->insert_instance_data; + model->insert_instparam_data = this->insert_instparam_data; + } + } + void set_instance_data (const sm::vvec>& position) final { sm::vvec> c = { mplot::colour::crimson }; @@ -257,6 +272,9 @@ namespace mplot { if (this->hidden() == true) { return; } + // render any components + for (uint32_t i = 0; i < this->components.size(); ++i) { this->components[i]->render(); } + // Execute post-vertex init at render, as GL should be available. if (this->flags.test (vm_bools::postVertexInitRequired) == true) { this->postVertexInit(); }