diff --git a/CMakeLists.txt b/CMakeLists.txt index 9122f2ec..e4e1d4d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,6 +175,9 @@ if(wxWidgets_FOUND) endif() endif() +# Make it possible to compile complex constexpr functions +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fconstexpr-ops-limit=5000000000") + # rapidxml is bundled in the source, but its headers will be installed in ${CMAKE_INSTALL_PREFIX}/morph/, and they're symlinked in ${PROJECT_SOURCE_DIR}/morph #include_directories("${PROJECT_SOURCE_DIR}/include/rapidxml-1.13") diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 802027c9..6a2455ef 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -195,6 +195,7 @@ if(USE_GLEW) endif() add_executable(scatter scatter.cpp) +set_property(TARGET scatter PROPERTY CXX_STANDARD 20) target_link_libraries(scatter OpenGL::GL glfw Freetype::Freetype) if(USE_GLEW) target_link_libraries(scatter GLEW::GLEW) @@ -326,8 +327,11 @@ if(USE_GLEW) target_link_libraries(icosahedron GLEW::GLEW) endif() +# Need C++20 for the way I use constexpr here so that it is legal to +# have "a definition of a variable for which no initialization is +# performed". add_executable(testce testce.cpp) -set_property(TARGET testce PROPERTY CXX_STANDARD 23) +set_property(TARGET testce PROPERTY CXX_STANDARD 20) add_executable(geodesic geodesic.cpp) target_link_libraries(geodesic OpenGL::GL glfw Freetype::Freetype) @@ -335,6 +339,13 @@ if(USE_GLEW) target_link_libraries(geodesic GLEW::GLEW) endif() +add_executable(geodesic_ce geodesic_ce.cpp) +set_property(TARGET geodesic_ce PROPERTY CXX_STANDARD 20) +target_link_libraries(geodesic_ce OpenGL::GL glfw Freetype::Freetype) +if(USE_GLEW) + target_link_libraries(geodesic_ce GLEW::GLEW) +endif() + add_executable(tri tri.cpp) target_link_libraries(tri OpenGL::GL glfw Freetype::Freetype) if(USE_GLEW) diff --git a/examples/geodesic.cpp b/examples/geodesic.cpp index 480972ca..684d6ace 100644 --- a/examples/geodesic.cpp +++ b/examples/geodesic.cpp @@ -16,7 +16,7 @@ int main() { int rtn = -1; - morph::Visual v(1024, 768, "Geodesic Polyhedron"); + morph::Visual v(1024, 768, "Geodesic Polyhedra (ordered vertices/faces)"); v.showCoordArrows = true; try { @@ -31,25 +31,15 @@ int main() std::string lbl = std::string("iterations = ") + std::to_string(i); gv1->addLabel (lbl, {0, -1, 0}, morph::TextFeatures(0.06f)); gv1->cm.setType (morph::ColourMapType::Jet); - //gv1->colourScale.do_autoscale = false; - //gv1->colourScale.compute_autoscale (0.0f, 2.0f); gv1->finalize(); -#if 0 // no re-colouring - v.addVisualModel (gv1); -#else // re-colour after construction - auto gv1p = v.addVisualModel (gv1); + // re-colour after construction + auto gv1p = v.addVisualModel (gv1); float imax_mult = 1.0f / static_cast(imax); -# if 0 // Funky pattern colouring - for (unsigned int j = 0; j < gv1p->data.size(); ++j) { - gv1p->data[j] = j%3 ? 0 : (1+i) * imax_mult; - } -# else // sequential colouring + // sequential colouring: size_t sz1 = gv1p->data.size(); gv1p->data.linspace (0.0f, 1+i * imax_mult, sz1); -# endif gv1p->updateColours(); -#endif } v.keepOpen(); diff --git a/examples/geodesic_ce.cpp b/examples/geodesic_ce.cpp new file mode 100644 index 00000000..d8d0c812 --- /dev/null +++ b/examples/geodesic_ce.cpp @@ -0,0 +1,64 @@ +/* + * Visualize an Icosahedron using the Geodesic Visual that co-ops the unordered + * constexpr geodesic function. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main() +{ + int rtn = -1; + + morph::Visual v(1024, 768, "(constexpr) Geodesic Polyhedra"); + v.showCoordArrows = true; + v.lightingEffects (true); + + try { + morph::vec offset = { 0.0, 0.0, 0.0 }; + morph::vec step = { 2.2, 0.0, 0.0 }; + + auto gv1 = std::make_unique> (offset + step * 0, 0.9f); + v.bindmodel (gv1); + std::string lbl = std::string("iterations = 0"); + gv1->addLabel (lbl, {0, -1, 0}, morph::TextFeatures(0.06f)); + gv1->finalize(); + v.addVisualModel (gv1); + + auto gv2 = std::make_unique> (offset + step * 1, 0.9f); + v.bindmodel (gv2); + lbl = std::string("iterations = 1"); + gv2->addLabel (lbl, {0, -1, 0}, morph::TextFeatures(0.06f)); + gv2->finalize(); + v.addVisualModel (gv2); + + auto gv3 = std::make_unique> (offset + step * 2, 0.9f); + v.bindmodel (gv3); + lbl = std::string("iterations = 2"); + gv3->addLabel (lbl, {0, -1, 0}, morph::TextFeatures(0.06f)); + gv3->finalize(); + v.addVisualModel (gv3); + + auto gv4 = std::make_unique> (offset + step * 3, 0.9f); + v.bindmodel (gv4); + lbl = std::string("iterations = 4"); + gv4->addLabel (lbl, {0, -1, 0}, morph::TextFeatures(0.06f)); + gv4->finalize(); + v.addVisualModel (gv4); + + v.keepOpen(); + + } catch (const std::exception& e) { + std::cerr << "Caught exception: " << e.what() << std::endl; + rtn = -1; + } + + return rtn; +} diff --git a/examples/scatter.cpp b/examples/scatter.cpp index 8ca56cad..8764db60 100644 --- a/examples/scatter.cpp +++ b/examples/scatter.cpp @@ -64,6 +64,7 @@ int main() while (v.readyToFinish == false) { v.waitevents (0.018); v.render(); + v.readyToFinish = true; // debug/profile } } catch (const std::exception& e) { diff --git a/examples/testce.cpp b/examples/testce.cpp index 5f328c0d..baf44dca 100644 --- a/examples/testce.cpp +++ b/examples/testce.cpp @@ -5,6 +5,10 @@ int main() { // Note that we force the compile time evaluation of geometry_ce::icosahedron by returning a constexpr object constexpr morph::geometry_ce::polyhedron ico = morph::geometry_ce::icosahedron(); - std::cout << "constexpr vertices:\n" << ico.vertices.str() << std::endl; + std::cout << "constexpr 1st vertices:\n" << static_cast>(ico.vertices[0]) << std::endl; + + constexpr + auto geo = morph::geometry_ce::make_icosahedral_geodesic(); + std::cout << "constexpr geo.vertices: " << static_cast>(geo.poly.vertices[0]).str() << std::endl; return 0; } diff --git a/morph/GeodesicVisualCE.h b/morph/GeodesicVisualCE.h new file mode 100644 index 00000000..c3157bcd --- /dev/null +++ b/morph/GeodesicVisualCE.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace morph { + + /*! + * This class creates the vertices for an geodesic polyhedron in a 3D scene using + * the constexpr function. + * + * \tparam T the type for the data to be visualized as face (or maybe vertex) colours + * + * \tparam glver The usual OpenGL version code; match this to everything else in + * your program. + */ + template + class GeodesicVisualCE : public VisualModel + { + public: + GeodesicVisualCE() { this->init ({0.0, 0.0, 0.0}, 1.0f); } + + //! Initialise with offset, start and end coordinates, radius and a single colour. + GeodesicVisualCE(const vec _offset, const float _radius) + { + this->init (_offset, _radius); + } + + ~GeodesicVisualCE () {} + + void init (const vec _offset, const float _radius) + { + // Set up... + this->mv_offset = _offset; + this->viewmatrix.translate (this->mv_offset); + this->radius = _radius; + } + + //! Initialize vertex buffer objects and vertex array object. + void initializeVertices() + { + this->vertexPositions.clear(); + this->vertexNormals.clear(); + this->vertexColors.clear(); + this->indices.clear(); + + if (iterations > 5) { + // Note odd necessity to stick in the 'template' keyword after this-> + this->template computeSphereGeoFast (this->idx, morph::vec({0,0,0}), + this->colour, this->radius); + } else { + // computeSphereGeo F defaults to float + this->template computeSphereGeoFast (this->idx, morph::vec({0,0,0}), + this->colour, this->radius); + } + } + + //! The radius of the geodesic + float radius = 1.0f; + //! Fixed colour. + std::array colour = { 0.2f, 0.1f, 0.7f }; + }; + +} // namespace morph diff --git a/morph/ScatterVisual.h b/morph/ScatterVisual.h index 3102be33..c1db9141 100644 --- a/morph/ScatterVisual.h +++ b/morph/ScatterVisual.h @@ -114,14 +114,14 @@ namespace morph { if (this->sizeFactor == Flt{0}) { if constexpr (draw_spheres_as_geodesics) { // Slower than regular computeSphere(). 2 iterations gives 320 faces - this->computeSphereGeo (this->idx, (*this->dataCoords)[i], clr, this->radiusFixed, 2); + this->template computeSphereGeoFast (this->idx, (*this->dataCoords)[i], clr, this->radiusFixed); } else { // (16+2) * 20 gives 360 faces this->computeSphere (this->idx, (*this->dataCoords)[i], clr, this->radiusFixed, 16, 20); } } else { if constexpr (draw_spheres_as_geodesics) { - this->computeSphereGeo (this->idx, (*this->dataCoords)[i], clr, dcopy[i]*this->sizeFactor, 2); + this->template computeSphereGeoFast (this->idx, (*this->dataCoords)[i], clr, dcopy[i]*this->sizeFactor); } else { this->computeSphere (this->idx, (*this->dataCoords)[i], clr, dcopy[i]*this->sizeFactor, 16, 20); } @@ -134,13 +134,9 @@ namespace morph { } } - // I've tested geodesic sphere drawing code, but it probably isn't yet suitable - // for use where many spheres are repeatedly computed (the base geodesic is - // repeatedly computed, which is a waste of cycles). In fact, the overall - // approach here, where each sphere model is repeatedly re-computed for each - // point is probably about the least performant way you could draw a bunch of - // spheres in OpenGL! However, using geodesic code is noticably slower even than - // the regular VisualModel::computeSphere() + // 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. diff --git a/morph/VisualModel.h b/morph/VisualModel.h index 9d8cd8b7..71f7e562 100644 --- a/morph/VisualModel.h +++ b/morph/VisualModel.h @@ -1395,6 +1395,40 @@ namespace morph { return gi.n_faces; } + //! Fast computeSphereGeo, which uses constexpr make_icosahedral_geodesic. The + //! resulting vertices and faces are NOT in any kind of order, but ok for + //! plotting, e.g. scatter graph spheres. + template + int computeSphereGeoFast (GLuint& idx, vec so, std::array sc, + float r = 1.0f) + { + // test if type F is float + if constexpr (std::is_same, float>::value == true) { + static_assert (iterations <= 5, "computeSphereGeoFast: For iterations > 5, F needs to be double precision"); + } else { + static_assert (iterations <= 10, "computeSphereGeoFast: This is an abitrary iterations limit (10 gives 20971520 faces)"); + } + // Note that we need double precision to compute higher iterations of the geodesic (iterations > 5) + constexpr morph::geometry_ce::icosahedral_geodesic geo = morph::geometry_ce::make_icosahedral_geodesic(); + + // Now essentially copy geo into vertex buffers + for (auto v : geo.poly.vertices) { + this->vertex_push (v.as_float() * r + so, this->vertexPositions); + this->vertex_push (v.as_float(), this->vertexNormals); + this->vertex_push (sc, this->vertexColors); + } + for (auto f : geo.poly.faces) { + this->indices.push_back (idx + f[0]); + this->indices.push_back (idx + f[1]); + this->indices.push_back (idx + f[2]); + } + // idx is the *vertex index* and should be incremented by the number of vertices in the polyhedron + int n_verts = static_cast(geo.poly.vertices.size()); + idx += n_verts; + + return n_verts; + } + /*! * Sphere, 1 colour version. * diff --git a/morph/geometry.h b/morph/geometry.h index 203970e1..a4e1bc6f 100644 --- a/morph/geometry.h +++ b/morph/geometry.h @@ -396,7 +396,7 @@ namespace morph { namespace geometry_ce { //! a container class for the vertices and faces of a polyhedron. Note this contains vec not vvec - template + template struct polyhedron { //! A list of the vertices @@ -405,6 +405,17 @@ namespace morph { morph::vec, n_faces> faces; }; + // Container class specific to icosahedral geodesic + template + struct icosahedral_geodesic + { + static constexpr int T = std::pow(4, iterations); + static constexpr int n_verts = 10 * T + 2; + static constexpr int n_faces = 20 * T; + geometry_ce::polyhedron poly; + morph::vec fivefold_vertices; + }; + //! Return a geometry::polyhedron object containing vertices and face indices //! for an icosahedron. To compute the vertices, I ran the non-constexpr capable //! version in morph::geometry and transcribed the vertex numbers - a bit tedious. @@ -454,6 +465,179 @@ namespace morph { return ico; } + + //! Experimental for constexpr. No ordering of vertices as in morph::geometry version. DO want to test for duplicate vertices. + template + constexpr morph::geometry_ce::icosahedral_geodesic make_icosahedral_geodesic() + { + morph::geometry_ce::icosahedral_geodesic geo; // our return object + + // geo must be fully initialized in a constexpr function (this could be constructor in icosahedral_geodesic<> + constexpr morph::vec fz = { 0, 0, 0 }; + constexpr morph::vec vz = { F{0}, F{0}, F{0} }; + for (unsigned int i = 0; i < geo.poly.faces.size(); ++i) { geo.poly.faces[i] = fz; } + for (unsigned int i = 0; i < geo.poly.vertices.size(); ++i) { geo.poly.vertices[i] = vz; } + + // Start out with an icosahedron + constexpr morph::geometry_ce::polyhedron initial_ico = morph::geometry_ce::icosahedron(); + + // copy initial_ico into geo.poly As geo is currently an icosahedron, all + // vertices in geo are five-fold at this point, so populate this (and then + // leave it alone) + for (int i = 0; i < 12; ++i) { + geo.poly.vertices[i] = initial_ico.vertices[i]; + geo.fivefold_vertices[i] = i; + } + for (int i = 0; i < 20; ++i) { + geo.poly.faces[i] = initial_ico.faces[i]; + } +#ifdef NON_CONSTEXPR_DEBUG + for (auto ver : geo.poly.vertices) { + std::cout << "vertex: " << ver << std::endl; + } + for (auto fac : geo.poly.faces) { + std::cout << "face: " << fac << std::endl; + } +#endif + for (int i = 0; i < iterations; ++i) { + + // Compute n_verts/n_faces for current iterations i + int _T = std::pow(4, i); + int _n_verts = 10 * _T + 2; // i=0; 12 i=1; 42 + int _n_faces = 20 * _T; // i=0; 20 i=1; 80 +#ifdef NON_CONSTEXPR_DEBUG + // Also compute n_verts_nxt/n_faces_nxt + int _T_nxt = std::pow(4, i+1); + int _n_verts_nxt = 10 * _T_nxt + 2; // i=0; 12 i=1; 42 + int _n_faces_nxt = 20 * _T_nxt; // i=0; 20 i=1; 80 + std::cout << "iteration i=" << i << " for which _T=" << _T + << " and _n_verts=" << _n_verts << ", _n_faces=" << _n_faces << " and next: _T_nxt=" << _T_nxt + << " with _n_verts_nxt=" << _n_verts_nxt << ", _n_faces_nxt=" << _n_faces_nxt << std::endl; +#endif + int next_face = _n_faces; + for (int f = 0; f < _n_faces; ++f) { // Loop over existing faces +#ifdef NON_CONSTEXPR_DEBUG + std::cout << "Start of loop. f: " << f << " _n_faces: " << _n_faces << " _n_faces_nxt: " << _n_faces_nxt + << " geo.poly.faces.size(): " << geo.poly.faces.size() << " geo.poly.vertices.size(): " << geo.poly.vertices.size() << std::endl; + std::cout << " geo.poly.faces["< va = (geo.poly.vertices[geo.poly.faces[f][1]] + geo.poly.vertices[geo.poly.faces[f][0]]) / 2.0f; + morph::vec vb = (geo.poly.vertices[geo.poly.faces[f][2]] + geo.poly.vertices[geo.poly.faces[f][1]]) / 2.0f; + morph::vec vc = (geo.poly.vertices[geo.poly.faces[f][0]] + geo.poly.vertices[geo.poly.faces[f][2]]) / 2.0f; + va.renormalize(); // constexpr capable function in morph::vec + vb.renormalize(); + vc.renormalize(); + + constexpr F thresh = F{3}; // for vertex element comparison + // Is va/vb/vc new? + int a = -1; + for (int v = 0; v < _n_verts; ++v) { + if (std::abs(geo.poly.vertices[v][0] - va[0]) < thresh * std::numeric_limits::epsilon() + && std::abs(geo.poly.vertices[v][1] - va[1]) < thresh * std::numeric_limits::epsilon() + && std::abs(geo.poly.vertices[v][2] - va[2]) < thresh * std::numeric_limits::epsilon()) { + // va is not a new vertex, set a to be the correct index + a = v; + } + } + if (a == -1) { + // va is a new vertex, add to poly.vertices +#ifdef NON_CONSTEXPR_DEBUG + if (_n_verts >= static_cast(geo.poly.vertices.size())) { + throw std::runtime_error ("off the end of vertices..."); + } +#endif + geo.poly.vertices[_n_verts] = va; + a = _n_verts; + _n_verts++; + } + + int b = -1; + for (int v = 0; v < _n_verts; ++v) { + if (std::abs(geo.poly.vertices[v][0] - vb[0]) < thresh * std::numeric_limits::epsilon() + && std::abs(geo.poly.vertices[v][1] - vb[1]) < thresh * std::numeric_limits::epsilon() + && std::abs(geo.poly.vertices[v][2] - vb[2]) < thresh * std::numeric_limits::epsilon()) { + // vb is not a new vertex, set b to be the correct index + b = v; + } + } + if (b == -1) { + // vb is a new vertex, add to poly.vertices +#ifdef NON_CONSTEXPR_DEBUG + if (_n_verts >= static_cast(geo.poly.vertices.size())) { + throw std::runtime_error ("off the end of vertices..."); + } +#endif + geo.poly.vertices[_n_verts] = vb; + b = _n_verts; + _n_verts++; + } + + int c = -1; + for (int v = 0; v < _n_verts; ++v) { + if (std::abs(geo.poly.vertices[v][0] - vc[0]) < thresh * std::numeric_limits::epsilon() + && std::abs(geo.poly.vertices[v][1] - vc[1]) < thresh * std::numeric_limits::epsilon() + && std::abs(geo.poly.vertices[v][2] - vc[2]) < thresh * std::numeric_limits::epsilon()) { + // vc is not a new vertex, set c to be the correct index + c = v; + } + } + if (c == -1) { + // vc is a new vertex, add to poly.vertices +#ifdef NON_CONSTEXPR_DEBUG + if (_n_verts >= static_cast(geo.poly.vertices.size())) { + throw std::runtime_error ("off the end of vertices..."); + } +#endif + geo.poly.vertices[_n_verts] = vc; + c = _n_verts; + _n_verts++; + } + + // Can't ONLY add to faces. We actually replace one face with 4. +#ifdef NON_CONSTEXPR_DEBUG + std::cout << "First newface from geo.poly.faces["< newface = { geo.poly.faces[f][0], a, c }; +#ifdef NON_CONSTEXPR_DEBUG + std::cout << "add " << newface << " to poly.faces (size " << geo.poly.faces.size() << ") at index " << next_face << std::endl; +#endif + geo.poly.faces[next_face++] = newface; +#ifdef NON_CONSTEXPR_DEBUG + std::cout << "Second newface from geo.poly.faces["<>::value, int> = 0 > - void renormalize() + constexpr void renormalize() { auto add_squared = [](_S a, _S b) { return a + b * b; }; const _S denom = std::sqrt (std::accumulate (this->begin(), this->end(), _S{0}, add_squared)); @@ -1195,9 +1195,10 @@ namespace morph { //! Scalar divide by s template >::value, int> = 0 > - vec operator/ (const _S& s) const + constexpr vec operator/ (const _S& s) const { vec rtn; + for (std::size_t i = 0; i < N; ++i) { rtn[i] = S{0}; } // init rtn auto div_by_s = [s](S elmnt) { return elmnt / s; }; std::transform (this->begin(), this->end(), rtn.begin(), div_by_s); return rtn; @@ -1213,7 +1214,7 @@ namespace morph { //! vec addition operator template - vec operator+ (const vec<_S, N>& v) const + constexpr vec operator+ (const vec<_S, N>& v) const { vec vrtn{}; auto vi = v.begin(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e93f47d9..2d0ed304 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -253,6 +253,14 @@ add_test(testQuaternion testQuaternion) add_executable(testvec testvec.cpp) add_test(testvec testvec) +# constexpr functionality in morph::vec +# Need C++20 for the way I use constexpr here so that it is legal to +# have "a definition of a variable for which no initialization is +# performed". +add_executable(testvec_constexpr testvec_constexpr.cpp) +set_property(TARGET testvec_constexpr PROPERTY CXX_STANDARD 20) +add_test(testvec_constexpr testvec_constexpr) + add_executable(testvec_asmapkey testvec_asmapkey.cpp) add_test(testvec_asmapkey testvec_asmapkey) diff --git a/tests/testvec_constexpr.cpp b/tests/testvec_constexpr.cpp new file mode 100644 index 00000000..7d59c521 --- /dev/null +++ b/tests/testvec_constexpr.cpp @@ -0,0 +1,46 @@ +/* + * Test constexpr capable functions in vec. + * + * This file must be compiled with a C++20 compiler so that it is legal to have "a + * definition of a variable for which no initialization is performed". + */ + +#include + +static constexpr morph::vec vec_add() +{ + morph::vec v1 = { 1.0, 2.0, 3.0 }; + morph::vec v2 = { 1.0, 2.0, 3.0 }; + morph::vec vce = v1 + v2; + return vce; +} + +static constexpr morph::vec vec_div_scalar() +{ + morph::vec v1 = { 1.0, 2.0, 3.0 }; + morph::vec vce = v1 / 2.0; + return vce; +} + +static constexpr morph::vec vec_renormalize() +{ + morph::vec v1 = { 1.0, 2.0, 3.0 }; + v1.renormalize(); + return v1; +} + +int main() +{ + int rtn = 0; + + constexpr morph::vec result1 = vec_add(); + if (result1[0] != 2.0) { std::cout << "Fail 1\n"; rtn -= 1; } + + constexpr morph::vec result2 = vec_div_scalar(); + if (result2[0] != 1.0/2.0) { std::cout << "Fail 2\n"; rtn -= 1; } + + constexpr morph::vec result3 = vec_renormalize(); + if (result3.length() != 1.0) { std::cout << "Fail 3\n"; rtn -= 1; } + + return rtn; +}