diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a3f30007..4e2d9cbe 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -247,6 +247,12 @@ if(NOT APPLE) add_executable(scatter_geodesic scatter_geodesic.cpp) target_link_libraries(scatter_geodesic OpenGL::GL glfw Freetype::Freetype) + + # Instancing also not available on Apple (limited to OpenGL 4.1) + add_executable(scatter_instanced scatter_instanced.cpp) + target_link_libraries(scatter_instanced OpenGL::GL glfw Freetype::Freetype) + add_executable(breadcrumbs breadcrumbs.cpp) + target_link_libraries(breadcrumbs OpenGL::GL glfw Freetype::Freetype) endif() add_executable(scatter_dynamic scatter_dynamic.cpp) diff --git a/examples/breadcrumbs.cpp b/examples/breadcrumbs.cpp new file mode 100644 index 00000000..648da57f --- /dev/null +++ b/examples/breadcrumbs.cpp @@ -0,0 +1,128 @@ +/* + * Visualize a test surface + */ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +// Instanced rendering requires OpenGL 4.3 or higher (for the SSBO) +constexpr int glver = mplot::gl::version_4_3; + +constexpr float st = sm::mathconst::two_pi / 360; +constexpr float z = 0.0f; +sm::vec f (int i, float _z = 0.0f) +{ + float phi = st * i; + float r = 1.0f + 0.1f * std::sin (phi * 10.0f); + sm::vec xyz = { + r * std::sin (phi), + r * std::cos (phi), + _z + }; + return xyz; +} + +int main() +{ + int rtn = -1; + + mplot::Visual v(1024, 768, "mplot::InstancedScatterVisual"); + v.lightingEffects(); + + // A normal, non instanced model. A sphere to orbit around. + auto gv1 = std::make_unique> (sm::vec<>{}, 0.2f); + gv1->name = "geodesic"; + v.bindmodel (gv1); + gv1->iterations = 3; + gv1->cm.setType (mplot::ColourMapType::Tofino); + gv1->finalize(); + // re-colour after construction + auto gv1p = v.addVisualModel (gv1); + // sequential colouring: + size_t sz1 = gv1p->data.size(); + gv1p->data.linspace (0.0f, 1.0f, sz1); + gv1p->reinitColours(); + + constexpr int dsz = 260; + sm::vvec> points(dsz); + sm::vvec psz(dsz); + + int i = 0; + sm::vec xyz = {}; + for (i = 0; i < dsz; ++i) { + xyz = f (i); + points[i] = xyz; + psz[i] = xyz[2]; + } + + auto isv = std::make_unique> (sm::vec<>{}); + isv->name = "isv1"; + v.bindmodel (isv); + isv->max_instances = dsz; + isv->radiusFixed = 0.03f; + isv->finalize(); + auto isvp = v.addVisualModel (isv); + + // Another one + isv = std::make_unique> (sm::vec<>{0,0.1,0}); + isv->name = "isv2"; + v.bindmodel (isv); + isv->max_instances = dsz; + isv->radiusFixed = 0.03f; + isv->finalize(); + auto isvp2 = v.addVisualModel (isv); + + v.render(); + isvp->set_instance_data (points); // colour, alpha, scale + + isvp2->set_instance_data (points * 1.2f, mplot::colour::black, 0.7f, 1.0f); + + sm::vvec> col = { mplot::colour::blue, mplot::colour::springgreen }; + sm::vvec alph = { 0.5f, 1.0f }; + sm::vvec scl = { 1.0f, 1.2f }; + + while (!v.readyToFinish()) { + + xyz = f (i%360); + //std::cout << "i = " << i << ", i%dsz = " << (i%dsz) << ", i%360 = " << (i%360) << " f(i%360) = " << xyz << " -> points[" << (i%dsz)<< "]" << std::endl; + + points[i%dsz] = xyz; + psz[i%dsz] = xyz[2]; + +#if 1 + // Update all points/psz + // Place data in SSBO. first call of set_data must occur after first call to v.render() + isvp->set_instance_data (points, col, alph, scl); + + v.render(); + v.waitevents (0.018); + +#else // I haven't mastered updating a subrange of data in an SSBO + // update circularly, change isvp->instance_start each time + std::cout << "updating data with points[i%dsz] = " << points[i%dsz] << std::endl; + isvp->update_instance_data (points, data, (i % dsz), (i % dsz)); + + std::cout << "Before render, isvp->instance_start is " << isvp->instance_start << std::endl; + + v.render(); + v.wait (0.4); + + isvp->instance_start += 1; + if (isvp->instance_start >= isvp->instance_count) { isvp->instance_start = 0; } + +#endif + ++i; + } + + return rtn; +} diff --git a/examples/gl_compute/shader_naive_scan.cpp b/examples/gl_compute/shader_naive_scan.cpp index df5666e9..59ca1fee 100644 --- a/examples/gl_compute/shader_naive_scan.cpp +++ b/examples/gl_compute/shader_naive_scan.cpp @@ -11,7 +11,7 @@ #include #include -#include +#include #include #include diff --git a/examples/gl_compute/shader_naive_scan_cli.cpp b/examples/gl_compute/shader_naive_scan_cli.cpp index 593f30de..2dd4cecf 100644 --- a/examples/gl_compute/shader_naive_scan_cli.cpp +++ b/examples/gl_compute/shader_naive_scan_cli.cpp @@ -17,7 +17,7 @@ #include #include -#include +#include #include namespace my { diff --git a/examples/gl_compute/shader_ssbo.cpp b/examples/gl_compute/shader_ssbo.cpp index 44e77c00..532d51b0 100644 --- a/examples/gl_compute/shader_ssbo.cpp +++ b/examples/gl_compute/shader_ssbo.cpp @@ -4,7 +4,7 @@ */ // You have to include the GL headers manually so that you will be sure you have the -// right ones. THis is because for OpenGL version 4.3, you would include GL3/gl3.h and +// right ones. This is because for OpenGL version 4.3, you would include GL3/gl3.h and // GL/glext.h whereas if you are targeting OpenGL 3.1 ES, you want to include // GLES3/gl3[12].h (and maybe GLES3/gl3ext.h). #include @@ -13,13 +13,15 @@ #include #include #include -#include +#include #include namespace my { + static constexpr int glver = mplot::gl::version_3_1_es; + // Use OpenGL 3.1 ES here - struct compute_manager : public mplot::gl::compute_manager + struct compute_manager : public mplot::gl::compute_manager { static constexpr int dwidth = 256; static constexpr int dheight = 65; @@ -86,9 +88,12 @@ namespace my { sm::vec _dims = mplot::loadpng ("../examples/gl_compute/bike.png", inputvv); if ((_dims - dims) > 0) { throw std::runtime_error ("Loaded image is not expected size"); } + this->input_ssbo.init (); // could take the image data as arg + // Set that data into the SSBO object (where it is stored in a vec<>) - std::copy (inputvv.begin(), inputvv.end(), this->input_ssbo.data.begin()); - this->input_ssbo.init(); + sm::vec data; + std::copy (inputvv.begin(), inputvv.end(), data.begin()); + this->input_ssbo.copy_to_gpu (data); } ~compute_manager() @@ -119,8 +124,9 @@ namespace my { this->compute_program.load_shaders (shaders); // We'll reuse the vertex/fragment shaders from the shadercompute example + std::string defVtxShdr = mplot::getDefaultVtxShader (glver); std::vector vtxshaders = { - {GL_VERTEX_SHADER, "../examples/gl_compute/shader_ssbo.vert.glsl", mplot::defaultVtxShader, 0 }, + {GL_VERTEX_SHADER, "../examples/gl_compute/shader_ssbo.vert.glsl", defVtxShdr.c_str(), 0 }, {GL_FRAGMENT_SHADER, "../examples/gl_compute/shader_ssbo.frag.glsl", mplot::defaultFragShader, 0 } }; this->vtxprog = mplot::gl::LoadShaders (vtxshaders); @@ -130,10 +136,12 @@ namespace my { // Override your one time/non-rendering compute function void compute() final { - // To copy updated data: - this->input_ssbo.data[0] = std::abs (std::sin (compstep)); // Make a pixel pulse + // To copy updated data. Would be nice to give a data subrange to update. + sm::vec data_update; + data_update[0] = std::abs (std::sin (compstep)); // Make a pixel pulse + data_update[1] = std::abs (std::cos (compstep)); compstep += 0.0001; - this->input_ssbo.copy_to_gpu(); + this->input_ssbo.copy_to_gpu (data_update, 2); this->measure_compute(); // optional this->compute_program.use(); @@ -142,8 +150,14 @@ namespace my { // To retreive data from the SSBO: // sm::range ssbo_range = this->input_ssbo.get_range(); // or - this->input_ssbo.copy_from_gpu(); - // and access input_ssbo.data. +#if 0 + sm::vec data_rtn; + this->input_ssbo.copy_from_gpu (data_rtn, 0); + std::cout << "data_rtn[0-5] = " + << data_rtn[0] << ", " << data_rtn[1] << ", " + << data_rtn[2] << ", " << data_rtn[3] << ", " + << data_rtn[4] << ", " << data_rtn[5] << std::endl; +#endif } // Override the render method to do whatever visualization you want @@ -194,7 +208,7 @@ namespace my { // CPU side input data. This will be SSBO index 1. mplot::gl::ssbo<1, float, dsz> input_ssbo; // You will need at least one gl::compute_shaderprog - mplot::gl::compute_shaderprog compute_program; + mplot::gl::compute_shaderprog compute_program; }; } // namespace my diff --git a/examples/gl_compute/shadercompute.cpp b/examples/gl_compute/shadercompute.cpp index 11ffa136..0a26fc49 100644 --- a/examples/gl_compute/shadercompute.cpp +++ b/examples/gl_compute/shadercompute.cpp @@ -12,7 +12,6 @@ * https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction */ - // You have to include the GL headers manually so that you will be sure you have the // right ones. This is because for OpenGL version 4.3, you would include GL3/gl3.h and // GL/glext.h whereas if you are targeting OpenGL 3.1 ES, you want to include @@ -25,8 +24,10 @@ namespace my { + static constexpr int glver = mplot::gl::version_4_5; + // Specify OpenGL version 4.5 (4.3 is min for compute) - struct compute_manager : public mplot::gl::compute_manager + struct compute_manager : public mplot::gl::compute_manager { // Call init in your constructor, ensuring *your* version of load_shaders() is called. compute_manager() @@ -80,8 +81,9 @@ namespace my { }; this->compute_program.load_shaders (shaders); + std::string defVtxShdr = mplot::getDefaultVtxShader (glver); std::vector vtxshaders = { - {GL_VERTEX_SHADER, "../examples/gl_compute/shadercompute.vert.glsl", mplot::defaultVtxShader, 0 }, + {GL_VERTEX_SHADER, "../examples/gl_compute/shadercompute.vert.glsl", defVtxShdr.c_str(), 0 }, {GL_FRAGMENT_SHADER, "../examples/gl_compute/shadercompute.frag.glsl", mplot::defaultFragShader, 0 } }; this->vtxprog = mplot::gl::LoadShaders (vtxshaders); @@ -137,7 +139,7 @@ namespace my { unsigned int vao = 0; unsigned int vbo = 0; // You will need at least one gl::compute_shaderprog - mplot::gl::compute_shaderprog compute_program; + mplot::gl::compute_shaderprog compute_program; }; } // namespace my diff --git a/examples/hexgrid.cpp b/examples/hexgrid.cpp index 4153c741..223a1c10 100644 --- a/examples/hexgrid.cpp +++ b/examples/hexgrid.cpp @@ -41,14 +41,15 @@ int main() // Make some dummy data (a sine wave) to make an interesting surface std::vector data(hg.num(), 0.0f); - for (unsigned int ri=0; ri1 + for (unsigned int ri = 0; ri < hg.num(); ++ri) { + data[ri] = 0.05f + 0.05f * std::sin (20.0f * hg.d_x[ri]) * std::sin (10.0f * hg.d_y[ri]) ; // Range 0->1 } // Add a HexGridVisual to display the HexGrid within the sm::Visual scene sm::vec offset = { 0.0f, -0.05f, 0.0f }; auto hgv = std::make_unique>(&hg, offset); v.bindmodel (hgv); + hgv->wireframe (true); hgv->cm.setType (mplot::ColourMapType::Ice); hgv->setScalarData (&data); hgv->hexVisMode = mplot::HexVisMode::HexInterp; // Or sm::HexVisMode::Triangles for a smoother surface plot diff --git a/examples/scatter_instanced.cpp b/examples/scatter_instanced.cpp new file mode 100644 index 00000000..7997046a --- /dev/null +++ b/examples/scatter_instanced.cpp @@ -0,0 +1,60 @@ +/* + * Visualize a test surface + */ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +// Instanced rendering requires OpenGL 4.3 or higher (for the SSBO) +constexpr int glver = mplot::gl::version_4_3; + +int main() +{ + int rtn = -1; + + mplot::Visual v(1024, 768, "mplot::InstancedScatterVisual"); + v.lightingEffects(); + + sm::vvec> points(20*20); + sm::vvec alpha(20*20, 1.0f); + sm::vvec scale(20*20); + sm::vvec> clrs(20*20); + size_t k = 0; + + mplot::ColourMap cm (mplot::ColourMapType::Plasma); + 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}; + scale[k] = 1.0f + z; + clrs[k] = cm.convert (z); + k++; + } + } + + auto isv = std::make_unique> (sm::vec<>{}); + v.bindmodel (isv); + isv->radiusFixed = 0.03f; + isv->finalize(); + auto isvp = v.addVisualModel (isv); + + v.render(); + // We set the instance data, which adds points, colours, alpha and scale + isvp->set_instance_data (points, clrs, alpha, scale); + + v.keepOpen(); + + return rtn; +} diff --git a/mplot/CMakeLists.txt b/mplot/CMakeLists.txt index 3c149f64..830f9412 100644 --- a/mplot/CMakeLists.txt +++ b/mplot/CMakeLists.txt @@ -76,6 +76,7 @@ install( HexGridVisual.h HSVWheelVisual.h IcosaVisual.h + InstancedScatterVisual.h LengthscaleVisual.h NormalsVisual.h PointRowsMeshVisual.h diff --git a/mplot/InstancedScatterVisual.h b/mplot/InstancedScatterVisual.h new file mode 100644 index 00000000..009fda02 --- /dev/null +++ b/mplot/InstancedScatterVisual.h @@ -0,0 +1,77 @@ +/*! + * An example of a scatter plot using instanced rendering + * + * \author Seb James + * \date 2025 + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace mplot +{ + template + class InstancedScatterVisual : public VisualModel + { + public: + InstancedScatterVisual (const sm::vec _offset) + { + this->instanced (true); + this->viewmatrix.translate (_offset); + } + + void marker (const sm::vec coord, const std::array& clr, const float 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); + } + } + } + + //! Compute spheres for a scatter plot + void initializeVertices() + { + // Draw one marker. It will then be instanced as many using instanced rendering + this->marker (sm::vec<>{}, mplot::colour::gold1, this->radiusFixed); + } + + // 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. + float radiusFixed = 0.05f; + }; + +} // namespace mplot diff --git a/mplot/VisualBase.h b/mplot/VisualBase.h index 6bb78ec1..5a73e54d 100644 --- a/mplot/VisualBase.h +++ b/mplot/VisualBase.h @@ -61,7 +61,11 @@ namespace mplot //! When true, cursor movements induce translation of scene translateMode, //! We are scrolling (and so we will need to zero scenetrans_delta after enacting the change) - scrolling + scrolling, + //! True means that at least one of our VisualModels is an instanced rendering model + haveInstanced, + //! When true, the instanced data SSBO needs to be copied to the GPU + instancedNeedsUpdate }; //! Boolean options - similar to state, but more likely to be modified by client code @@ -238,6 +242,7 @@ namespace mplot model->get_shaderprogs = &mplot::VisualBase::get_shaderprogs; model->get_gprog = &mplot::VisualBase::get_gprog; model->get_tprog = &mplot::VisualBase::get_tprog; + model->instanced_needs_update = &mplot::VisualBase::instanced_needs_update; } /*! @@ -248,6 +253,7 @@ namespace mplot unsigned int addVisualModelId (std::unique_ptr& model) { std::unique_ptr> vmp = std::move(model); + if (vmp->instanced()) { this->state.set (visual_state::haveInstanced, true); } this->vm.push_back (std::move(vmp)); unsigned int rtn = (this->vm.size()-1); return rtn; @@ -260,6 +266,7 @@ namespace mplot T* addVisualModel (std::unique_ptr& model) { std::unique_ptr> vmp = std::move(model); + if (vmp->instanced()) { this->state.set (visual_state::haveInstanced, true); } this->vm.push_back (std::move(vmp)); return static_cast(this->vm.back().get()); } @@ -372,6 +379,8 @@ namespace mplot static GLuint get_gprog (mplot::VisualBase* _v) { return _v->shaders.gprog; }; static GLuint get_tprog (mplot::VisualBase* _v) { return _v->shaders.tprog; }; + static void instanced_needs_update (mplot::VisualBase* _v) { _v->instancedNeedsUpdate (true); } + //! The colour of ambient and diffuse light sources sm::vec light_colour = { 1.0f, 1.0f, 1.0f }; //! Strength of the ambient light @@ -461,6 +470,13 @@ namespace mplot //! Returns true if we are in the paused state bool paused() const { return this->state.test (visual_state::paused); } + //! True if one of our added VisualModels is an instanced model + bool haveInstanced() const { return this->state.test (visual_state::haveInstanced); } + + //! Does our instanced data need to be pushed over to the GPU during render()? + bool instancedNeedsUpdate() const { return this->state.test (visual_state::instancedNeedsUpdate); } + void instancedNeedsUpdate (const bool val) { this->state.set (visual_state::instancedNeedsUpdate, val); } + /* * User-settable projection values for the near clipping distance, the far clipping distance * and the field of view of the camera. @@ -1015,6 +1031,7 @@ namespace mplot << "Ctrl-u: Reduce zNear cutoff plane\n" << "Ctrl-i: Increase zNear cutoff plane\n" << "Ctrl-j: Toggle bounding boxes\n" + << "Ctrl-Shift-s: Output shaders to stdout\n" << "F1-F10: Select model index (with shift: toggle hide)\n" << "Shift-Left: Decrease opacity of selected model\n" << "Shift-Right: Increase opacity of selected model\n" @@ -1037,14 +1054,33 @@ namespace mplot } // else no-op } - if (_key == key::s && (mods & keymod::control) && action == keyaction::press) { - std::string fname (this->title); - mplot::tools::stripFileSuffix (fname); - fname += ".png"; - // Make fname 'filename safe' - mplot::tools::conditionAsFilename (fname); - this->saveImage (fname); - std::cout << "Saved image to '" << fname << "'\n"; + if (_key == key::s && (mods & (keymod::control | keymod::shift)) && action == keyaction::press) { + + if ((mods & (keymod::control | keymod::shift)) == (keymod::control | keymod::shift)) { + // Ctrl-Shift-s gives you the default shaders + std::cout << "The built-in shader programs are:\n"; + std::cout << "\nVisual.vert.glsl\n" + << "----------------\n" + << mplot::getDefaultVtxShader(glver) << std::endl; + std::cout << "\nVisual.frag.glsl\n" + << "----------------\n" + << mplot::getDefaultFragShader(glver) << std::endl; + std::cout << "\nVisText.vert.glsl\n" + << "----------------\n" + << mplot::getDefaultTextVtxShader(glver) << std::endl; + std::cout << "\nVisText.frag.glsl\n" + << "----------------\n" + << mplot::getDefaultTextFragShader(glver) << std::endl; + } else if (mods & keymod::control) { + // Ctrl-s saves a PNG + std::string fname (this->title); + mplot::tools::stripFileSuffix (fname); + fname += ".png"; + // Make fname 'filename safe' + mplot::tools::conditionAsFilename (fname); + this->saveImage (fname); + std::cout << "Saved image to '" << fname << "'\n"; + } } // Save gltf 3D file diff --git a/mplot/VisualDefaultShaders.h b/mplot/VisualDefaultShaders.h index e88c68d7..41d8c33d 100644 --- a/mplot/VisualDefaultShaders.h +++ b/mplot/VisualDefaultShaders.h @@ -3,109 +3,27 @@ #pragma once -// Define a version string for the shaders -#ifdef __APPLE__ -// Mac support is fixed at OpenGL 4.1 in favour of their own graphics API. -#define OpenGL_VersionString "#version 410\n" -#else -/* - On other platforms ALSO specify OpenGL 4.1, because only relatively simple features - of OpenGL are in use. In future, may wish to change this, in case there's a need for - any of the following major features released since OpenGL 4.1 (this list from the - wikipedia OpenGL page): - -OpenGL 4.2 - - Release date: August 8, 2011 - - Support for shaders with atomic counters and load-store-atomic read-modify-write - operations to one level of a texture - - Drawing multiple instances of data captured from GPU vertex processing (including - tessellation), to enable complex objects to be efficiently repositioned and - replicated - - Support for modifying an arbitrary subset of a compressed texture, without having to - re-download the whole texture to the GPU for significant performance improvements - -OpenGL 4.3 - - Release date: August 6, 2012 - - Compute shaders leveraging GPU parallelism within the context of the graphics pipeline - - Shader storage buffer objects, allowing shaders to read and write buffer objects like - image load/store from 4.2, but through the language rather than function calls. - - Image format parameter queries - - ETC2/EAC texture compression as a standard feature - - Full compatibility with OpenGL ES 3.0 APIs - - Debug abilities to receive debugging messages during application development - - Texture views to interpret textures in different ways without data replication - - Increased memory security and multi-application robustness - -OpenGL 4.4 - - Release date: July 22, 2013 - - Enforced buffer object usage controls - - Asynchronous queries into buffer objects - - Expression of more layout controls of interface variables in shaders - - Efficient binding of multiple objects simultaneously - -OpenGL 4.5 - - Release date: August 11, 2014 - - Direct State Access (DSA) - object accessors enable state to be queried and modified - without binding objects to contexts, for increased application and middleware - efficiency and flexibility. - - Flush Control - applications can control flushing of pending commands before context - switching - enabling high-performance multithreaded applications; - - Robustness - providing a secure platform for applications such as WebGL browsers, - including preventing a GPU reset affecting any other running applications; - - OpenGL ES 3.1 API and shader compatibility - to enable the easy development and - execution of the latest OpenGL ES applications on desktop systems. - - OpenGL 4.6 - - Release date: July 31, 2017 - - more efficient, GPU-sided, geometry processing - more efficient shader execution (AZDO) - more information through statistics, overflow query and counters - higher performance through no error handling contexts - clamping of polygon offset function, solves a shadow rendering problem - SPIR-V shaders - Improved anisotropic filtering -*/ -#define OpenGL_VersionString "#version 310 es\n" -#endif - #include namespace mplot { // The default vertex shader. To study this GLSL, see Visual.vert.glsl, which has // some code comments. - const char* defaultVtxShader = "uniform mat4 m_matrix;\n" + const char* defaultVtxShader_part1a = + "uniform mat4 m_matrix;\n" "uniform mat4 v_matrix;\n" "uniform mat4 p_matrix;\n" - "uniform float alpha;\n" + "uniform float alpha;\n"; + const char* defaultVtxShader_instance_uniforms = + "uniform int instance_count = 0;\n" + "uniform int instance_start = -1;\n" + "uniform int instparam_count = 0;\n"; + const char* defaultVtxShader_part1b = "layout(location = 0) in vec4 position;\n" "layout(location = 1) in vec4 normalin;\n" - "layout(location = 2) in vec3 color;\n" + "layout(location = 2) in vec3 color;\n"; + + const char* defaultVtxShader_part2 = "out VERTEX\n" "{\n" " vec4 normal;\n" @@ -120,11 +38,56 @@ namespace mplot " vertex.normal = normalin;\n" "}\n"; + const char* defaultVtxShader_part2_inst = + "out VERTEX\n" + "{\n" + " vec4 normal;\n" + " vec4 color;\n" + " vec3 fragpos;\n" + "} vertex;\n" + "void main()\n" + "{\n" + " if (instance_count > 0) {\n" + " int ipos_i = instance_start * 3 + gl_InstanceID * 3;\n" + " vec4 iposv = { ipos[ipos_i], ipos[ipos_i + 1], ipos[ipos_i + 2], 0 };\n" + " if (instparam_count > 0) {\n" + " int idx = gl_InstanceID % instparam_count;\n" + " int ippos_i = instance_start * 5;\n" + " float s = iparam[ippos_i + idx * 5 + 4];\n" + " vec3 p_three = vec3(position) * s;\n" + " vec4 p_scaled = vec4(p_three, 1.0);\n" + " gl_Position = (p_matrix * v_matrix * m_matrix * (p_scaled + iposv));\n" + " vertex.color = vec4(iparam[ippos_i + idx * 5], iparam[ippos_i + idx * 5 + 1], iparam[ippos_i + idx * 5 + 2], iparam[ippos_i + idx * 5 + 3]);\n" + " vertex.fragpos = vec3(m_matrix * p_scaled);\n" + " } else {\n" + " gl_Position = (p_matrix * v_matrix * m_matrix * (position + iposv));\n" + " vertex.color = vec4(color, alpha);\n" + " vertex.fragpos = vec3(m_matrix * position);\n" + " }\n" + " } else {\n" + " gl_Position = (p_matrix * v_matrix * m_matrix * position);\n" + " vertex.color = vec4(color, alpha);\n" + " vertex.fragpos = vec3(m_matrix * position);\n" + " }\n" + " vertex.normal = normalin;\n" + "}\n"; + std::string getDefaultVtxShader (const int glver) { std::string shdr; shdr += mplot::gl::version::shaderpreamble (glver); - shdr += defaultVtxShader; + shdr += defaultVtxShader_part1a; + if (mplot::gl::version::has_ssbo (glver)) { + shdr += defaultVtxShader_instance_uniforms; + } + shdr += defaultVtxShader_part1b; + if (mplot::gl::version::has_ssbo (glver)) { + shdr += "layout (std430, binding = 1) buffer InstPos { float ipos[]; };\n"; + shdr += "layout (std430, binding = 2) buffer InstParam { float iparam[]; };\n"; + shdr += defaultVtxShader_part2_inst; + } else { + shdr += defaultVtxShader_part2; + } return shdr; } diff --git a/mplot/VisualMX.h b/mplot/VisualMX.h index bd9b9da9..9d3cc2b5 100644 --- a/mplot/VisualMX.h +++ b/mplot/VisualMX.h @@ -165,17 +165,19 @@ namespace mplot void bindmodel (std::unique_ptr& model) { mplot::VisualBase::template bindmodel (model); // base class binds - model->setContext = &mplot::VisualBase::set_context; - model->releaseContext = &mplot::VisualBase::release_context; - model->get_glfn = &mplot::VisualOwnableMX::get_glfn; + this->bindextra (model); } + // The GL-dependent binds template void bindextra (std::unique_ptr& model) { model->setContext = &mplot::VisualBase::set_context; model->releaseContext = &mplot::VisualBase::release_context; model->get_glfn = &mplot::VisualOwnableMX::get_glfn; + model->init_instance_data = &mplot::VisualOwnableMX::init_instance_data; + model->insert_instance_data = &mplot::VisualOwnableMX::insert_instance_data; + model->insert_instparam_data = &mplot::VisualOwnableMX::insert_instparam_data; } /* diff --git a/mplot/VisualModelBase.h b/mplot/VisualModelBase.h index 4c262ecd..c2e65d8f 100644 --- a/mplot/VisualModelBase.h +++ b/mplot/VisualModelBase.h @@ -64,6 +64,8 @@ namespace mplot postVertexInitRequired, twodimensional, // If true, then this VisualModel should always be viewed in a plane - it's a 2D model hide, // If true, then calls to VisualModel::render should return + wireframe, // If true, draw in GL's polygon GL_LINES mode (instead of GL_FILL) + instanced, // If true, draw this VisualModel with 'instancing' 1 or more times show_bb, // If true, draw vertices/indices for the bounding box frame compute_bb // For some models, it's not useful to compute the bounding box (e.g. coordinate arrows) }; @@ -716,6 +718,21 @@ namespace mplot GLuint idx = 0u; GLuint idx_bb = 0u; + /*! + * This is the upper limit for instance_count. We reserve max_isntances of space in the SSBO. + * + * Max number of instances in a model. Each *model* has to reserve space in the SSBOs which + * are managed by VisualResources. VisualResources::max_instances must be larger than this. + */ + unsigned int max_instances = 16 * 1024; + //! The offset into the SSBOs for the instance data for this VisualModel + unsigned int instance_start = 0; + //! If drawing with instancing, how many instances? <= this->max_instances + unsigned int instance_count = 0; + //! If drawing with instancing, how many params will be used (these will be cycled through + //! per-instance and there may be fewer than instance_count parameters) + unsigned int instparam_count = 0; + /*! * A function that will be runtime defined to get_shaderprogs from a pointer to * Visual (saving a boilerplate argument and avoiding that killer circular @@ -732,6 +749,34 @@ namespace mplot //! Release OpenGL context. Should call parentVis->releaseContext(). std::function*)> releaseContext; + //! Init the SSBOs for instanced rendering + std::function*, const unsigned int)> init_instance_data; + std::function&)> insert_instance_data; + std::function&, const float, const float)> insert_instparam_data; + std::function*)> instanced_needs_update; + + //! Set up the instance positions (with default params for colour, rotn, scale) + virtual void set_instance_data (const sm::vvec>& position) = 0; + + //! Set instance positions, with a single colour, alpha and scale to apply to all points + virtual void set_instance_data (const sm::vvec>& position, + const std::array& colour, + const float alpha, + const float scale) = 0; + + //! Set instance positions, with an array containing colour, alpha and scale values to apply + //! to the instances. The size of colour, alpha and scale must match, but it may be of a + //! different size to position. + virtual void set_instance_data (const sm::vvec>& position, + const sm::vvec>& colour, + const sm::vvec& alpha, + const sm::vvec& scale) = 0; + +#if 0 + //! Update the instance positions and params (colour, rotn, scale) in a range + virtual void update_instance_data (const sm::vvec>& points, const sm::vvec& data, + std::size_t i_s, std::size_t i_e) = 0; +#endif //! Setter for the parent pointer, parentVis void set_parent (mplot::VisualBase* _vis) { @@ -746,6 +791,7 @@ namespace mplot _flags.set (vm_bools::postVertexInitRequired, false); _flags.set (vm_bools::twodimensional, false); _flags.set (vm_bools::hide, false); + _flags.set (vm_bools::wireframe, false); _flags.set (vm_bools::show_bb, false); _flags.set (vm_bools::compute_bb, true); return _flags; @@ -765,6 +811,12 @@ namespace mplot void twodimensional (const bool val) { this->flags.set (vm_bools::twodimensional, val); } bool twodimensional() const { return this->flags.test (vm_bools::twodimensional); } + void wireframe (const bool val) { this->flags.set (vm_bools::wireframe, val); } + 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); } + //! Getter for vertex positions (for mplot::NormalsVisual) std::vector getVertexPositions() { return this->vertexPositions; } //! Getter for vertex normals (for mplot::NormalsVisual) @@ -895,7 +947,10 @@ namespace mplot /** * START vertex/index computation code * - * ALL methods below this point are for computing vertices + * ALL methods below this point are for computing vertices. + * + * The metheds compute vertexPositions/Normals/Colors along with indices in a form suitable + * for use with GL's DrawElements (or DrawElementsInstanced) drawing call. */ /*! diff --git a/mplot/VisualModelImplMX.h b/mplot/VisualModelImplMX.h index b752d3f1..861b0a4d 100644 --- a/mplot/VisualModelImplMX.h +++ b/mplot/VisualModelImplMX.h @@ -32,7 +32,7 @@ namespace mplot /*! * Multiple context safe implementation (mplot::gl::multicontext == 1) */ - template = true > + template requires (mx == 1) struct VisualModelImpl : public mplot::VisualModelBase { VisualModelImpl() : mplot::VisualModelBase::VisualModelBase() {} @@ -44,7 +44,7 @@ namespace mplot // Explicitly clear owned VisualTextModels this->texts.clear(); if (this->vbos != nullptr) { - GladGLContext* _glfn = this->get_glfn(this->parentVis); + GladGLContext* _glfn = this->get_glfn (this->parentVis); _glfn->DeleteBuffers (this->numVBO, this->vbos.get()); _glfn->DeleteVertexArrays (1, &this->vao); } @@ -70,6 +70,59 @@ namespace mplot model->releaseContext = &mplot::VisualBase::release_context; } + void set_instance_data (const sm::vvec>& position) final + { + sm::vvec> c = { mplot::colour::crimson }; + sm::vvec a = { 1.0f }; + sm::vvec s = { 1.0f }; + this->set_instance_data (position, c, a, s); + } + + void set_instance_data (const sm::vvec>& position, const std::array& colour, + const float alpha = 1.0f, const float scale = 1.0f) final + { + sm::vvec> c = { colour }; + sm::vvec a = { alpha }; + sm::vvec s = { scale }; + this->set_instance_data (position, c, a, s); + } + + //! Set up the instance positions and params (colour, alpha, scale). Add rotation later on. + void set_instance_data (const sm::vvec>& position, + const sm::vvec>& colour, + const sm::vvec& alpha, const sm::vvec& scale) final + { + if (position.size() < 1) { + throw std::runtime_error ("set_instance_data: pass some instance positions in"); + } + if (position.size() > this->max_instances) { + throw std::runtime_error ("set_instance_data: Haven't reserved enough space for that"); + } + if (!this->insert_instance_data) { + throw std::runtime_error ("set_instance_data: Function insert_instance_data is not bound"); + } + if (!this->insert_instparam_data) { + throw std::runtime_error ("set_instance_data: Function insert_instparam_data is not bound"); + } + if (colour.size() != scale.size() || colour.size() != alpha.size()) { + throw std::runtime_error ("set_instance_data: params vvecs should all have same size (colour, rotn, scale)"); + } + + for (size_t i = 0; i < position.size(); ++i) { + // Get access to the SSBO in VisualResources and add the 3 floats in position[i] at + // the location defined by this->instance_start + i + this->insert_instance_data (this->instance_start + i, position[i]); + } + this->instance_count = position.size(); + + for (size_t i = 0; i < colour.size(); ++i) { + this->insert_instparam_data (this->instance_start + i, colour[i], alpha[i], scale[i]); + } + this->instparam_count = colour.size(); + + this->instanced_needs_update (this->parentVis); + } + //! Common code to call after the vertices have been set up. GL has to have been initialised. void postVertexInit() final { @@ -105,6 +158,15 @@ namespace mplot _glfn->BindVertexArray(0); // carefully unbind and rebind mplot::gl::Util::checkError (__FILE__, __LINE__, _glfn); + if (this->flags.test (vm_bools::instanced) && this->init_instance_data) { + // Here, we cause the SSBOs to be intialized if they haven't already, and we reserve + // some space in the SSBOs for *this model* + this->instance_start = this->init_instance_data (this->parentVis, this->max_instances); + if (this->instance_start == std::numeric_limits::max()) { + throw std::runtime_error ("Failed to reserve space in SSBO"); + } + } + /* * Now do the same for the bounding box */ @@ -215,6 +277,14 @@ namespace mplot _glfn->UseProgram (this->get_gprog(this->parentVis)); if (!this->indices.empty()) { + + // Enable/disable wireframe mode per-model on each render call + if (this->flags.test (vm_bools::wireframe)) { + _glfn->PolygonMode (GL_FRONT_AND_BACK, GL_LINE); + } else { + _glfn->PolygonMode (GL_FRONT_AND_BACK, GL_FILL); + } + // It is only necessary to bind the vertex array object before rendering // (not the vertex buffer objects) _glfn->BindVertexArray (this->vao); @@ -237,7 +307,19 @@ namespace mplot } // Draw the triangles - _glfn->DrawElements (GL_TRIANGLES, static_cast(this->indices.size()), GL_UNSIGNED_INT, 0); + GLint loc_is = _glfn->GetUniformLocation (this->get_gprog(this->parentVis), static_cast("instance_start")); + GLint loc_ic = _glfn->GetUniformLocation (this->get_gprog(this->parentVis), static_cast("instance_count")); + GLint loc_ipc = _glfn->GetUniformLocation (this->get_gprog(this->parentVis), static_cast("instparam_count")); + if (this->flags.test (vm_bools::instanced)) { + if (loc_is != -1) { _glfn->Uniform1i (loc_is, this->instance_start); } + if (loc_ic != -1) { _glfn->Uniform1i (loc_ic, this->instance_count); } + if (loc_ipc != -1) { _glfn->Uniform1i (loc_ipc, this->instparam_count); } + _glfn->DrawElementsInstanced (GL_TRIANGLES, static_cast(this->indices.size()), GL_UNSIGNED_INT, 0, this->instance_count); + } else { + if (loc_is != -1) { _glfn->Uniform1i (loc_is, -1); } + if (loc_ic != -1) { _glfn->Uniform1i (loc_ic, -1); } + _glfn->DrawElements (GL_TRIANGLES, static_cast(this->indices.size()), GL_UNSIGNED_INT, 0); + } // Unbind the VAO _glfn->BindVertexArray(0); @@ -252,6 +334,7 @@ namespace mplot mplot::gl::Util::checkError (__FILE__, __LINE__, _glfn); // Now render any VisualTextModels + _glfn->PolygonMode (GL_FRONT_AND_BACK, GL_FILL); auto ti = this->texts.begin(); while (ti != this->texts.end()) { (*ti)->render(); ti++; } diff --git a/mplot/VisualModelImplNoMX.h b/mplot/VisualModelImplNoMX.h index 70f236c3..6ef6f8d2 100644 --- a/mplot/VisualModelImplNoMX.h +++ b/mplot/VisualModelImplNoMX.h @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -31,7 +32,7 @@ namespace mplot /*! * Single context safe implementation (mplot::gl::multicontext == 0) */ - template = true > + template requires (mx == 0) struct VisualModelImpl : public mplot::VisualModelBase { VisualModelImpl() : mplot::VisualModelBase::VisualModelBase() {} @@ -65,6 +66,59 @@ namespace mplot model->releaseContext = &mplot::VisualBase::release_context; } + void set_instance_data (const sm::vvec>& position) final + { + sm::vvec> c = { mplot::colour::crimson }; + sm::vvec a = { 1.0f }; + sm::vvec s = { 1.0f }; + this->set_instance_data (position, c, a, s); + } + + void set_instance_data (const sm::vvec>& position, const std::array& colour, + const float alpha = 1.0f, const float scale = 1.0f) final + { + sm::vvec> c = { colour }; + sm::vvec a = { alpha }; + sm::vvec s = { scale }; + this->set_instance_data (position, c, a, s); + } + + //! Set up the instance positions and params (colour, alpha, scale). Add rotation later on. + void set_instance_data (const sm::vvec>& position, + const sm::vvec>& colour, + const sm::vvec& alpha, const sm::vvec& scale) final + { + if (position.size() < 1) { + throw std::runtime_error ("set_instance_data: pass some instance positions in"); + } + if (position.size() > this->max_instances) { + throw std::runtime_error ("set_instance_data: Haven't reserved enough space for that"); + } + if (!this->insert_instance_data) { + throw std::runtime_error ("set_instance_data: Function insert_instance_data is not bound"); + } + if (!this->insert_instparam_data) { + throw std::runtime_error ("set_instance_data: Function insert_instparam_data is not bound"); + } + if (colour.size() != scale.size() || colour.size() != alpha.size()) { + throw std::runtime_error ("set_instance_data: params vvecs should all have same size (colour, rotn, scale)"); + } + + for (size_t i = 0; i < position.size(); ++i) { + // Get access to the SSBO in VisualResources and add the 3 floats in position[i] at + // the location defined by this->instance_start + i + this->insert_instance_data (this->instance_start + i, position[i]); + } + this->instance_count = position.size(); + + for (size_t i = 0; i < colour.size(); ++i) { + this->insert_instparam_data (this->instance_start + i, colour[i], alpha[i], scale[i]); + } + this->instparam_count = colour.size(); + + this->instanced_needs_update (this->parentVis); + } + //! Common code to call after the vertices have been set up. GL has to have been initialised. void postVertexInit() final { @@ -98,6 +152,15 @@ namespace mplot glBindVertexArray(0); // carefully unbind and rebind mplot::gl::Util::checkError (__FILE__, __LINE__); + if (this->flags.test (vm_bools::instanced) && this->init_instance_data) { + // Here, we cause the SSBOs to be intialized if they haven't already, and we reserve + // some space in the SSBOs for *this model* + this->instance_start = this->init_instance_data (this->parentVis, this->max_instances); + if (this->instance_start == std::numeric_limits::max()) { + throw std::runtime_error ("Failed to reserve space in SSBO"); + } + } + /* * Now do the same for the bounding box */ @@ -204,6 +267,14 @@ namespace mplot glUseProgram (this->get_gprog(this->parentVis)); if (!this->indices.empty()) { + + // Enable/disable wireframe mode per-model on each render call + if (this->flags.test (vm_bools::wireframe)) { + glPolygonMode (GL_FRONT_AND_BACK, GL_LINE); + } else { + glPolygonMode (GL_FRONT_AND_BACK, GL_FILL); + } + // It is only necessary to bind the vertex array object before rendering // (not the vertex buffer objects) glBindVertexArray (this->vao); @@ -228,6 +299,20 @@ namespace mplot // Draw the triangles glDrawElements (GL_TRIANGLES, static_cast(this->indices.size()), GL_UNSIGNED_INT, 0); + GLint loc_is = glGetUniformLocation (this->get_gprog(this->parentVis), static_cast("instance_start")); + GLint loc_ic = glGetUniformLocation (this->get_gprog(this->parentVis), static_cast("instance_count")); + GLint loc_ipc = glGetUniformLocation (this->get_gprog(this->parentVis), static_cast("instparam_count")); + if (this->flags.test (vm_bools::instanced)) { + if (loc_is != -1) { glUniform1i (loc_is, this->instance_start); } + if (loc_ic != -1) { glUniform1i (loc_ic, this->instance_count); } + if (loc_ipc != -1) { glUniform1i (loc_ipc, this->instparam_count); } + glDrawElementsInstanced (GL_TRIANGLES, static_cast(this->indices.size()), GL_UNSIGNED_INT, 0, this->instance_count); + } else { + if (loc_is != -1) { glUniform1i (loc_is, -1); } + if (loc_ic != -1) { glUniform1i (loc_ic, -1); } + glDrawElements (GL_TRIANGLES, static_cast(this->indices.size()), GL_UNSIGNED_INT, 0); + } + // Unbind the VAO glBindVertexArray(0); @@ -241,6 +326,7 @@ namespace mplot mplot::gl::Util::checkError (__FILE__, __LINE__); // Now render any VisualTextModels + glPolygonMode (GL_FRONT_AND_BACK, GL_FILL); auto ti = this->texts.begin(); while (ti != this->texts.end()) { (*ti)->render(); ti++; } diff --git a/mplot/VisualNoMX.h b/mplot/VisualNoMX.h index d4efc038..27086ef0 100644 --- a/mplot/VisualNoMX.h +++ b/mplot/VisualNoMX.h @@ -164,15 +164,18 @@ namespace mplot void bindmodel (std::unique_ptr& model) { mplot::VisualBase::template bindmodel (model); - model->setContext = &mplot::VisualBase::set_context; - model->releaseContext = &mplot::VisualBase::release_context; + this->bindextra (model); } + // GL dependent function binds template void bindextra (std::unique_ptr& model) { model->setContext = &mplot::VisualBase::set_context; model->releaseContext = &mplot::VisualBase::release_context; + model->init_instance_data = &mplot::VisualOwnableNoMX::init_instance_data; + model->insert_instance_data = &mplot::VisualOwnableNoMX::insert_instance_data; + model->insert_instparam_data = &mplot::VisualOwnableNoMX::insert_instparam_data; } /* diff --git a/mplot/VisualOwnableMX.h b/mplot/VisualOwnableMX.h index 8aedc7b4..6b67f6c8 100644 --- a/mplot/VisualOwnableMX.h +++ b/mplot/VisualOwnableMX.h @@ -259,6 +259,11 @@ namespace mplot this->userFrame->render(); } + if (this->haveInstanced() && this->instancedNeedsUpdate()) { + this->copy_instance_data_to_gpu(); + this->instancedNeedsUpdate (false); + } + auto vmi = this->vm.begin(); while (vmi != this->vm.end()) { if ((*vmi)->twodimensional() == true) { @@ -327,6 +332,7 @@ namespace mplot } } + // Note: We have to have both VisualOwnableMX::bindmodel AND VisualMX::bindmodel (which calls VisualBase::bindmodel) template void bindmodel (std::unique_ptr& model) { @@ -334,7 +340,11 @@ namespace mplot model->get_shaderprogs = &mplot::VisualBase::get_shaderprogs; model->get_gprog = &mplot::VisualBase::get_gprog; model->get_tprog = &mplot::VisualBase::get_tprog; + model->instanced_needs_update = &mplot::VisualBase::instanced_needs_update; model->get_glfn = &mplot::VisualOwnableMX::get_glfn; + model->init_instance_data = &mplot::VisualOwnableMX::init_instance_data; + model->insert_instance_data = &mplot::VisualOwnableMX::insert_instance_data; + model->insert_instparam_data = &mplot::VisualOwnableMX::insert_instparam_data; } //! Add a label _text to the scene at position _toffset. Font features are @@ -387,6 +397,29 @@ namespace mplot return tm->getTextGeometry(); } + static unsigned int init_instance_data (mplot::VisualBase* _v, const unsigned int n_to_reserve) + { + auto __v = reinterpret_cast*>(_v); + unsigned int reservation = mplot::VisualResourcesMX::i().init_instance_ssbo (__v->glfn, n_to_reserve); + return reservation; + } + + static void insert_instance_data (const unsigned int instance_idx, const sm::vec& coord) + { + mplot::VisualResourcesMX::i().insert_instance_data (instance_idx, coord); + } + + static void insert_instparam_data (const unsigned int instance_idx, + const std::array& colour, const float& alpha, const float& scale) + { + mplot::VisualResourcesMX::i().insert_instparam_data (instance_idx, colour, alpha, scale); + } + + static void copy_instance_data_to_gpu() + { + mplot::VisualResourcesMX::i().copy_instance_ssbo_to_gpu(); + } + protected: // Initialize OpenGL shaders, set some flags (Alpha, Anti-aliasing), read in any external // state from json, and set up the coordinate arrows and any VisualTextModels that will be diff --git a/mplot/VisualOwnableNoMX.h b/mplot/VisualOwnableNoMX.h index b1690c3f..af8f7d41 100644 --- a/mplot/VisualOwnableNoMX.h +++ b/mplot/VisualOwnableNoMX.h @@ -253,6 +253,11 @@ namespace mplot this->userFrame->render(); } + if (this->haveInstanced() && this->instancedNeedsUpdate()) { + this->copy_instance_data_to_gpu(); + this->instancedNeedsUpdate (false); + } + auto vmi = this->vm.begin(); while (vmi != this->vm.end()) { if ((*vmi)->twodimensional() == true) { @@ -345,6 +350,28 @@ namespace mplot return tm->getTextGeometry(); } + static unsigned int init_instance_data ([[maybe_unused]]mplot::VisualBase* _v, const unsigned int n_to_reserve) + { + unsigned int reservation = mplot::VisualResourcesNoMX::i().init_instance_ssbo (n_to_reserve); + return reservation; + } + + static void insert_instance_data (const unsigned int instance_idx, const sm::vec& coord) + { + mplot::VisualResourcesNoMX::i().insert_instance_data (instance_idx, coord); + } + + static void insert_instparam_data (const unsigned int instance_idx, + const std::array& colour, const float& alpha, const float& scale) + { + mplot::VisualResourcesNoMX::i().insert_instparam_data (instance_idx, colour, alpha, scale); + } + + static void copy_instance_data_to_gpu() + { + mplot::VisualResourcesNoMX::i().copy_instance_ssbo_to_gpu(); + } + protected: // Initialize OpenGL shaders, set some flags (Alpha, Anti-aliasing), read in any external // state from json, and set up the coordinate arrows and any VisualTextModels that will be diff --git a/mplot/VisualResourcesBase.h b/mplot/VisualResourcesBase.h index 32b986d4..4f262e5e 100644 --- a/mplot/VisualResourcesBase.h +++ b/mplot/VisualResourcesBase.h @@ -73,6 +73,29 @@ namespace mplot // Note: get/clearVisualFace functions are in derived classes virtual void clearVisualFaces (mplot::VisualBase* _vis) = 0; + + /*! + * SSBO management + */ + //! Instanced rendering mode (SSBO access). position data stored in SSBO index 1 (must match GLSL code) + static constexpr unsigned int instance_index = 1; + //! colour, scale, rotation stored in SSBO index 2 + static constexpr unsigned int instparam_index = 2; + //! one 3D vector is 3 floats + static constexpr unsigned int floats_per_instance = 3; + //! Instance params are: colour/alpha (4 floats), scale (1 float) + static constexpr unsigned int floats_per_instparam = 5; + + //! This will control how much GPU RAM is allocated when using instanced rendering + //! (Hopefully, when I'm finished, the RAM will be allocated only if at least one + //! VisualModel is marked 'instanced'). Makes a big difference to speed of operation (unless + //! I can send a portion of a buffer to the GPU). + static constexpr unsigned int max_instances = 32 * 1024; + static constexpr unsigned int max_instance_floats = floats_per_instance * max_instances; + static constexpr unsigned int max_instparam_floats = floats_per_instparam * max_instances; + + // The Current location from which space in the instance SSBOs should be allocated + unsigned int instance_top = 0; }; } // namespace mplot diff --git a/mplot/VisualResourcesMX.h b/mplot/VisualResourcesMX.h index a8cf1d7c..b275b5db 100644 --- a/mplot/VisualResourcesMX.h +++ b/mplot/VisualResourcesMX.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace mplot { @@ -108,6 +109,67 @@ namespace mplot } else { f++; } } } + + /*! + * We also manage some programm-wide SSBO objects for instanced rendering + * VisualResourcesdata in . Reserve n_to_reserve instances of data in the SSBOs. Return the + * start offset into the buffers in terms of number of instances + */ + unsigned int init_instance_ssbo (GladGLContext* glfn, const unsigned int n_to_reserve) + { + unsigned int reservation = std::numeric_limits::max(); + if constexpr (mplot::gl::version::has_ssbo (glver) == true) { + if (this->instance_data.ready() == false) { this->instance_data.init (glfn); } + if (this->instparam_data.ready() == false) { this->instparam_data.init (glfn); } + if (n_to_reserve + this->instance_top <= this->max_instances) { + reservation = this->instance_top; + this->instance_top += n_to_reserve; + this->instance_data.resize (this->instance_top * this->floats_per_instance); + this->instparam_data.resize (this->instance_top * this->floats_per_instparam); + } + } else { + throw std::runtime_error ("Instanced rendering requires OpenGL 4.3 or higher"); + } + return reservation; + } + + void insert_instance_data (const unsigned int instance_idx, const sm::vec& coord) + { + if (instance_idx >= this->max_instances) { + throw std::runtime_error ("insert_instance_data: bad instance_idx"); + } + unsigned int cur_fidx = instance_idx * this->floats_per_instance; + this->instance_data.data[cur_fidx++] = coord[0]; + this->instance_data.data[cur_fidx++] = coord[1]; + this->instance_data.data[cur_fidx++] = coord[2]; + } + + void insert_instparam_data (const unsigned int instance_idx, + const std::array& colour, const float& alpha, const float& scale) + { + if (instance_idx >= this->max_instances) { + throw std::runtime_error ("insert_instparam_data: bad instance_idx"); + } + unsigned int cur_fidx = instance_idx * this->floats_per_instparam; + this->instparam_data.data[cur_fidx++] = colour[0]; + this->instparam_data.data[cur_fidx++] = colour[1]; + this->instparam_data.data[cur_fidx++] = colour[2]; + this->instparam_data.data[cur_fidx++] = alpha; + this->instparam_data.data[cur_fidx++] = scale; + } + + void copy_instance_ssbo_to_gpu() + { + if (this->instance_data.ready()) { this->instance_data.copy_to_gpu(); } + if (this->instparam_data.ready()) { this->instparam_data.copy_to_gpu(); } + } + + //! Shader Storage Buffer Object for instanced rendering - this holds positions only + mplot::gl::ssbo::instance_index, + float, mplot::VisualResourcesBase::max_instance_floats> instance_data; + //! Shader Storage Buffer Object for instanced rendering - this holds colour, alpha and scale + mplot::gl::ssbo::instparam_index, + float, mplot::VisualResourcesBase::max_instparam_floats> instparam_data; }; } // namespace mplot diff --git a/mplot/VisualResourcesNoMX.h b/mplot/VisualResourcesNoMX.h index ee342f8a..4334ad8a 100644 --- a/mplot/VisualResourcesNoMX.h +++ b/mplot/VisualResourcesNoMX.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace mplot { @@ -109,6 +110,68 @@ namespace mplot } else { f++; } } } + + /*! + * We also manage some programm-wide SSBO objects for instanced rendering + * VisualResourcesdata in . Reserve n_to_reserve instances of data in the SSBOs. Return the + * start offset into the buffers in terms of number of instances + */ + unsigned int init_instance_ssbo (const unsigned int n_to_reserve) + { + unsigned int reservation = std::numeric_limits::max(); + if constexpr (mplot::gl::version::has_ssbo (glver) == true) { + if (this->instance_data.ready() == false) { this->instance_data.init(); } + if (this->instparam_data.ready() == false) { this->instparam_data.init(); } + if (n_to_reserve + this->instance_top <= this->max_instances) { + reservation = this->instance_top; + this->instance_top += n_to_reserve; + this->instance_data.resize (this->instance_top * this->floats_per_instance); + this->instparam_data.resize (this->instance_top * this->floats_per_instparam); + } + } else { + throw std::runtime_error ("Instanced rendering requires OpenGL 4.3 or higher"); + } + return reservation; + } + + void insert_instance_data (const unsigned int instance_idx, const sm::vec& coord) + { + if (instance_idx >= this->max_instances) { + throw std::runtime_error ("insert_instance_data: bad instance_idx"); + } + unsigned int cur_fidx = instance_idx * this->floats_per_instance; + this->instance_data.data[cur_fidx++] = coord[0]; + this->instance_data.data[cur_fidx++] = coord[1]; + this->instance_data.data[cur_fidx++] = coord[2]; + } + + void insert_instparam_data (const unsigned int instance_idx, + const std::array& colour, const float& alpha, const float& scale) + { + if (instance_idx >= this->max_instances) { + throw std::runtime_error ("insert_instparam_data: bad instance_idx"); + } + unsigned int cur_fidx = instance_idx * this->floats_per_instparam; + this->instparam_data.data[cur_fidx++] = colour[0]; + this->instparam_data.data[cur_fidx++] = colour[1]; + this->instparam_data.data[cur_fidx++] = colour[2]; + this->instparam_data.data[cur_fidx++] = alpha; + this->instparam_data.data[cur_fidx++] = scale; + } + + void copy_instance_ssbo_to_gpu() + { + if (this->instance_data.ready()) { this->instance_data.copy_to_gpu(); } + if (this->instparam_data.ready()) { this->instparam_data.copy_to_gpu(); } + } + + //! Shader Storage Buffer Object for instanced rendering - this holds positions only + mplot::gl::ssbo::instance_index, + float, mplot::VisualResourcesBase::max_instance_floats> instance_data; + //! Shader Storage Buffer Object for instanced rendering - this holds colour, alpha and scale + mplot::gl::ssbo::instparam_index, + float, mplot::VisualResourcesBase::max_instparam_floats> instparam_data; + }; } // namespace mplot diff --git a/mplot/VisualTextModelBase.h b/mplot/VisualTextModelBase.h index da547f23..92b734ad 100644 --- a/mplot/VisualTextModelBase.h +++ b/mplot/VisualTextModelBase.h @@ -201,6 +201,9 @@ namespace mplot virtual void postVertexInit() = 0; public: + // A VisualTextModel may be given a name + std::string name = "VisualTextModel"; + //! The colour of the text std::array clr_text = {0.0f, 0.0f, 0.0f}; //! Line spacing, in multiples of the height of an 'h' @@ -222,6 +225,12 @@ namespace mplot //! Release OpenGL context. Should call parentVis->releaseContext(). std::function*)> releaseContext; + //! SSBOs are unused in VisualTextModels, but these functions have to be present + std::function*, const unsigned int)> init_instance_data; + std::function&)> insert_instance_data; + std::function&, const float, const float)> insert_instparam_data; + std::function*)> instanced_needs_update; + //! Setter for the parent pointer, parentVis void set_parent (mplot::VisualBase* _vis) { diff --git a/mplot/compoundray/interop.h b/mplot/compoundray/interop.h index 2b74f60e..0be3e4ac 100644 --- a/mplot/compoundray/interop.h +++ b/mplot/compoundray/interop.h @@ -51,7 +51,8 @@ namespace mplot::compoundray * This function finds the meshes in compound-ray's MulticamScene and creates corresponding * VisualModels in the mplot::Visual */ - void scene_to_visualmodels (MulticamScene* thescene, mplot::Visual<>* thevisual, bool make_navmeshes = false) + template + void scene_to_visualmodels (MulticamScene* thescene, mplot::Visual* thevisual, bool make_navmeshes = false) { static constexpr bool debug_meshload = false; std::vector> mymeshes = thescene->getMeshes(); @@ -138,7 +139,7 @@ namespace mplot::compoundray << ind.size() << " indices, " << posn.size() << " posns, " << norm.size() << " norms, " << colr.size() << " colours" << std::endl; } - auto vertvm = std::make_unique> (tfm, ind, posn, norm, colr); + auto vertvm = std::make_unique> (tfm, ind, posn, norm, colr); thevisual->bindmodel (vertvm); vertvm->name = mymeshes[mi]->name; if (make_navmeshes == true) { vertvm->make_navmesh(); } diff --git a/mplot/gl/CMakeLists.txt b/mplot/gl/CMakeLists.txt index da74c1eb..526c8387 100644 --- a/mplot/gl/CMakeLists.txt +++ b/mplot/gl/CMakeLists.txt @@ -1,5 +1,17 @@ # Header installation install( - FILES compute_manager.h shaders.h loadshaders_nomx.h loadshaders_mx.h texture.h version.h compute_manager_cli.h compute_shaderprog.h ssbo.h util_nomx.h util_mx.h + FILES + compute_manager.h + shaders.h + texture.h + version.h + compute_manager_cli.h + compute_shaderprog.h + ssbo_nomx.h + ssbo_mx.h + loadshaders_nomx.h + loadshaders_mx.h + util_nomx.h + util_mx.h DESTINATION ${CMAKE_INSTALL_PREFIX}/include/mplot/gl ) diff --git a/mplot/gl/ssbo.h b/mplot/gl/ssbo.h deleted file mode 100644 index 6a7e83a9..00000000 --- a/mplot/gl/ssbo.h +++ /dev/null @@ -1,185 +0,0 @@ -#pragma once - -/* - * Common code for SSBO interactions in mplot programs - * - * Note: You have to include a header like gl3.h or glext.h etc for the GL types and - * functions BEFORE including this file. - * - * Author: Seb James. - */ - -#include -#include -#include -#include -#include - -namespace mplot::gl -{ - // An SSBO and its data - // @tparam index: The index of the buffer, used in the GLSL - // @tparam T: The type of the data in the SSBO - // @tparam N: The number of elements of type T in the SSBO - template // Could add version template params if necessary, to select correct gl function calls - struct ssbo - { - // The name of the buffer, generated with glGenBuffers() - unsigned int name = 0; - // The CPU-side data for the buffer - sm::vec data; - - ssbo() {} - ~ssbo() {} - - // Init is not built into the constructor, as client code must ensure there is an OpenGL context available - void init() - { - glGenBuffers (1, &this->name); - this->copy_to_gpu(); - } - - // Copy the data in the sm::vec data over to the GPU - void copy_to_gpu() - { - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); - mplot::gl::Util::checkError (__FILE__, __LINE__); - glBufferData (GL_SHADER_STORAGE_BUFFER, N * sizeof(T), this->data.data(), GL_STATIC_DRAW); - mplot::gl::Util::checkError (__FILE__, __LINE__); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - // Map the GPU memory to CPU space, then copy the values into this->data. NB: it's a - // performance hit to *copy* to the mapped data to our sm::vec, because the data is - // *already in CPU accessible memory* after glMapBufferRange(). - // However, in case you need it, here it is. - void copy_from_gpu() - { - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); - mplot::gl::Util::checkError (__FILE__, __LINE__); - T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, N*sizeof(T), GL_MAP_READ_BIT)); - mplot::gl::Util::checkError (__FILE__, __LINE__); - for (unsigned int i = 0; i < N; ++i) { this->data[i] = cpuptr[i]; } - glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - // Find the range of the data in the given Shader Storage Buffer Object - // - // ssbo_idx: The Index of the Shader Storage Buffer Object that we're reading from - // ssbo_name: The name (really a number) of the Shader Storage Buffer Object that we're reading from - // ssbo_num_elements: The number of elements of type T in the SSBO. - sm::range get_range() - { - sm::range r; - r.search_init(); - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); - mplot::gl::Util::checkError (__FILE__, __LINE__); - T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, N*sizeof(T), GL_MAP_READ_BIT)); - mplot::gl::Util::checkError (__FILE__, __LINE__); - for (unsigned int i = 0; i < N; ++i) { r.update (cpuptr[i]); } - glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - return r; - } - }; - - // Set up a Shader Storage Buffer Object (SSBO) and buffer data into it (from a sm::vvec) - template - void setup_ssbo (const GLuint target_index, unsigned int& ssbo_id, const sm::vvec& data) - { - glGenBuffers (1, &ssbo_id); - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, target_index, ssbo_id); - // Mutable, re-locatable storage: - glBufferData (GL_SHADER_STORAGE_BUFFER, data.size() * sizeof(T), data.data(), GL_STATIC_DRAW); - // Immutable storage would be: - // void glBufferStorage(GLenum target​, GLsizeiptr size​, const GLvoid * data​, GLbitfield flags​); - //glBufferStorage (GL_SHADER_STORAGE_BUFFER, data.size() * sizeof(T), data.data(), GL_CLIENT_STORAGE_BIT | GL_MAP_READ_BIT); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - // Set up a Shader Storage Buffer Object (SSBO) and buffer data into it (sm::vec version) - template - void setup_ssbo (const GLuint target_index, unsigned int& ssbo_id, const sm::vec& data) - { - glGenBuffers (1, &ssbo_id); - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, target_index, ssbo_id); - glBufferData (GL_SHADER_STORAGE_BUFFER, N * sizeof(T), data.data(), GL_STATIC_DRAW); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - // Copy data to an existing SSBO - template - void copy_vvec_to_ssbo (const GLuint target_index, const unsigned int ssbo_id, const sm::vvec& data) - { - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, target_index, ssbo_id); - glBufferData (GL_SHADER_STORAGE_BUFFER, data.size() * sizeof(T), data.data(), GL_STATIC_DRAW); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - template - void copy_vvec_to_ssbo (const GLuint target_index, const unsigned int ssbo_id, const sm::vvec& data) - { - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, target_index, ssbo_id); - glBufferData (GL_SHADER_STORAGE_BUFFER, N * sizeof(T), data.data(), GL_STATIC_DRAW); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - // Map the SSBO to cpu space, then make a copy of the data into a passed-in vvec. - // - // ssbo_idx: The Index of the Shader Storage Buffer Object that we're reading from - // ssbo_name: The name (really a number) of the Shader Storage Buffer Object that we're reading from - // cpu_data: A vvec of the right size to receive the data in the SSBO into 'CPU accessible memory' - template - void ssbo_copy_to_vvec (const unsigned int ssbo_idx, const unsigned int ssbo_name, sm::vvec& cpu_side) - { - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, ssbo_idx, ssbo_name); - // Really, it's crazy to *copy* because the data is *already in CPU - // accessible memory* after glMapBufferRange. BUT here's the copy: - T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, cpu_side.size()*sizeof(T), GL_MAP_READ_BIT)); - for (unsigned int i = 0; i < cpu_side.size(); ++i) { cpu_side[i] = cpuptr[i]; } - glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - template - void ssbo_copy_to_vec (const unsigned int ssbo_idx, const unsigned int ssbo_name, sm::vec& cpu_side) - { - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, ssbo_idx, ssbo_name); - // Really, it's crazy to *copy* because the data is *already in CPU - // accessible memory* after glMapBufferRange. BUT here's the copy: - T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, N*sizeof(T), GL_MAP_READ_BIT)); - for (unsigned int i = 0; i < N; ++i) { cpu_side[i] = cpuptr[i]; } - glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - } - - // Find the range of the data in the given Shader Storage Buffer Object - // - // ssbo_idx: The Index of the Shader Storage Buffer Object that we're reading from - // ssbo_name: The name (really a number) of the Shader Storage Buffer Object that we're reading from - // ssbo_num_elements: The number of elements of type T in the SSBO. - template - sm::range ssbo_get_range (const unsigned int ssbo_idx, const unsigned int ssbo_name, const unsigned int ssbo_num_elements) - { - sm::range r; - r.search_init(); - glBindBufferBase (GL_SHADER_STORAGE_BUFFER, ssbo_idx, ssbo_name); - T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, ssbo_num_elements*sizeof(T), GL_MAP_READ_BIT)); - for (unsigned int i = 0; i < ssbo_num_elements; ++i) { r.update (cpuptr[i]); } - glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); - glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); - mplot::gl::Util::checkError (__FILE__, __LINE__); - return r; - } - -} // mplot::gl diff --git a/mplot/gl/ssbo_mx.h b/mplot/gl/ssbo_mx.h new file mode 100644 index 00000000..814a5caa --- /dev/null +++ b/mplot/gl/ssbo_mx.h @@ -0,0 +1,190 @@ +#pragma once + +/* + * Common code for SSBO interactions in mplot programs + * + * Note: You have to include a header like gl3.h or glext.h etc for the GL types and + * functions BEFORE including this file. + * + * Author: Seb James. + */ + +#include +#include +#include +#include +#include + +namespace mplot::gl +{ + // An SSBO and its data + // @tparam index: The index of the buffer, used in the GLSL + // @tparam T: The type of the data in the SSBO + // @tparam N: The max number of elements of type T in the SSBO. N elements are reserved in the vvec data + template // requires std::is_arithmetic_v? + struct ssbo + { + // The name of the buffer, generated with glGenBuffers() + unsigned int name = 0; + // The OpenGL function pointer + GladGLContext* glfn = nullptr; + // The CPU-side data for the buffer + sm::vvec data = {}; + + bool ready() const { return this->name != 0u; } + + void resize (std::size_t sz) + { + if (sz > N) { throw std::runtime_error ("ssbo: can't resize to this size"); } + this->data.resize (sz); + } + + ssbo() {} + ~ssbo() {} + + // Init is not built into the constructor, as client code must ensure there is an OpenGL context available + void init (GladGLContext* _glfn) + { + this->glfn = _glfn; + this->glfn->GenBuffers (1, &this->name); // has to happen after VBO buffers? + this->data.reserve (N * sizeof(T)); + this->init_buffer_object(); + } + + void init_buffer_object() + { + this->glfn->BindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + this->glfn->BufferData (GL_SHADER_STORAGE_BUFFER, N * sizeof(T), this->data.data(), GL_STATIC_DRAW); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + this->glfn->BindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + } + + // Copy data in the sm::vec data over to the GPU, once it has been initialized with init_buffer_object + // data may contain less than N elements, and may be copied at an offset + template + void copy_to_gpu (sm::vec& _data, std::size_t offset = 0u) + { + static_assert (_N <= N); + + // Update local cached version of data, a portion of which we're about to write to the SSBO + this->data.resize (_N); + for (unsigned int i = 0; i < _N; ++i) { this->data[i + offset] = _data[i]; } + + this->glfn->BindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + T* cpuptr = static_cast(this->glfn->MapBufferRange (GL_SHADER_STORAGE_BUFFER, + offset * sizeof(T), _N * sizeof(T), GL_MAP_WRITE_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_to_gpu(sm::vec&): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + return; + } + // Note: cpuptr is a 'pointer to the beginning of the mapped range' and so we don't incorporate offset again, below: + for (unsigned int i = 0; i < _N; ++i) { cpuptr[i] = _data[i]; } + this->glfn->UnmapBuffer (GL_SHADER_STORAGE_BUFFER); + this->glfn->BindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + } + + void copy_to_gpu (sm::vvec& _data, std::size_t offset = 0u) + { + if (_data.size() > N) { throw std::runtime_error ("Too big"); } + + // Update local cached version of data, a portion of which we're about to write to the SSBO + this->data.resize (_data.size()); + for (unsigned int i = 0; i < _data.size(); ++i) { this->data[i + offset] = _data[i]; } + + this->glfn->BindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + T* cpuptr = static_cast(this->glfn->MapBufferRange (GL_SHADER_STORAGE_BUFFER, + offset * sizeof(T), _data.size() * sizeof(T), + GL_MAP_WRITE_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_to_gpu(sm::vec&): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + return; + } + // Note: cpuptr is a 'pointer to the beginning of the mapped range' and so we don't incorporate offset again, below: + for (unsigned int i = 0; i < _data.size(); ++i) { + cpuptr[i] = _data[i]; + } + + this->glfn->UnmapBuffer (GL_SHADER_STORAGE_BUFFER); // necessary + this->glfn->BindBuffer (GL_SHADER_STORAGE_BUFFER, 0); // optional + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + } + + void copy_to_gpu() + { + this->glfn->BindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + std::size_t sz = this->data.size(); + T* cpuptr = static_cast(this->glfn->MapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, sz * sizeof(T), GL_MAP_WRITE_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_to_gpu(): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + return; + } + // Note: cpuptr is a 'pointer to the beginning of the mapped range' and so we don't incorporate offset again, below: + //std::cout << "About to copy " << N << " things (prolly floats)...\n"; + for (unsigned int i = 0; i < sz; ++i) { cpuptr[i] = this->data[i]; } + this->glfn->UnmapBuffer (GL_SHADER_STORAGE_BUFFER); + this->glfn->BindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + } + + // Map the GPU memory to CPU space, then copy the values into data. NB: it's a + // performance hit to *copy* to the mapped data to the sm::vec, because the data is + // *already in CPU accessible memory* after glMapBufferRange(). + // + // The fastest way to compute on the CPU side would be to add a method to this class (or an + // extension of it) and operate directly on the pointer cpuptr. + template + void copy_from_gpu (std::size_t offset = 0u) + { + static_assert (_N <= N); + this->glfn->BindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + T* cpuptr = static_cast(this->glfn->MapBufferRange (GL_SHADER_STORAGE_BUFFER, + offset * sizeof(T), _N * sizeof(T), GL_MAP_READ_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_from_gpu(): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + return; + } + this->data.resize (_N); + for (unsigned int i = 0; i < _N; ++i) { this->data[i + offset] = cpuptr[i]; } + this->glfn->UnmapBuffer (GL_SHADER_STORAGE_BUFFER); + this->glfn->BindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + } + + // Find the range of the data in the given Shader Storage Buffer Object + // + // ssbo_idx: The Index of the Shader Storage Buffer Object that we're reading from + // ssbo_name: The name (really a number) of the Shader Storage Buffer Object that we're reading from + // ssbo_num_elements: The number of elements of type T in the SSBO. + sm::range get_range() + { + std::size_t sz = this->data.size(); + sm::range r; + r.search_init(); + this->glfn->BindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + T* cpuptr = static_cast(this->glfn->MapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, sz * sizeof(T), GL_MAP_READ_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::get_range(): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + return r; + } + for (unsigned int i = 0; i < sz; ++i) { r.update (cpuptr[i]); } + this->glfn->UnmapBuffer (GL_SHADER_STORAGE_BUFFER); + this->glfn->BindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__, this->glfn); + return r; + } + }; + +} // mplot::gl diff --git a/mplot/gl/ssbo_nomx.h b/mplot/gl/ssbo_nomx.h new file mode 100644 index 00000000..24c4c222 --- /dev/null +++ b/mplot/gl/ssbo_nomx.h @@ -0,0 +1,182 @@ +#pragma once + +/* + * Common code for SSBO interactions in mplot programs + * + * Note: You have to include a header like gl3.h or glext.h etc for the GL types and + * functions BEFORE including this file. + * + * Author: Seb James. + */ + +#include +#include +#include +#include +#include + +namespace mplot::gl +{ + // An SSBO and its data + // @tparam index: The index of the buffer, used in the GLSL + // @tparam T: The type of the data in the SSBO + // @tparam N: The max number of elements of type T in the SSBO + template + struct ssbo + { + // The name of the buffer, generated with glGenBuffers() + unsigned int name = 0; + // The CPU-side data for the buffer + sm::vvec data = {}; + + bool ready() const { return this->name != 0u; } + + void resize (std::size_t sz) + { + if (sz > N) { throw std::runtime_error ("ssbo: can't resize to this size"); } + this->data.resize (sz); + } + + ssbo() {} + ~ssbo() {} + + // Init is not built into the constructor, as client code must ensure there is an OpenGL context available + void init() + { + glGenBuffers (1, &this->name); + this->data.reserve (N * sizeof(T)); + this->init_buffer_object(); + } + + void init_buffer_object() + { + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__); + glBufferData (GL_SHADER_STORAGE_BUFFER, N * sizeof(T), this->data.data(), GL_STATIC_DRAW); + mplot::gl::Util::checkError (__FILE__, __LINE__); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__); + } + + // Copy data in the sm::vec data over to the GPU, once it has been initialized with init_buffer_object + // data may contain less than N elements, and may be copied at an offset + template + void copy_to_gpu (sm::vec& _data, std::size_t offset = 0u) + { + static_assert (_N <= N); + + // Update local cached version of data, a portion of which we're about to write to the SSBO + this->data.resize (_N); + for (unsigned int i = 0; i < _N; ++i) { this->data[i + offset] = _data[i]; } + + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__); + T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, offset * sizeof(T), _N * sizeof(T), GL_MAP_WRITE_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_to_gpu(sm::vec&): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__); + return; + } + // Note: cpuptr is a 'pointer to the beginning of the mapped range' and so we don't incorporate offset again, below: + for (unsigned int i = 0; i < _N; ++i) { cpuptr[i] = _data[i]; } + glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__); + } + + void copy_to_gpu (sm::vvec& _data, std::size_t offset = 0u) + { + if (_data.size() > N) { throw std::runtime_error ("Too big"); } + + // Update local cached version of data, a portion of which we're about to write to the SSBO + this->data.resize (_data.size()); + for (unsigned int i = 0; i < _data.size(); ++i) { this->data[i + offset] = _data[i]; } + + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__); + T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, + offset * sizeof(T), _data.size() * sizeof(T), + GL_MAP_WRITE_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_to_gpu(sm::vec&): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__); + return; + } + // Note: cpuptr is a 'pointer to the beginning of the mapped range' and so we don't incorporate offset again, below: + for (unsigned int i = 0; i < _data.size(); ++i) { + cpuptr[i] = _data[i]; + } + + glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); // necessary + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); // optional + mplot::gl::Util::checkError (__FILE__, __LINE__); + } + + void copy_to_gpu () + { + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__); + std::size_t sz = this->data.size(); + T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, sz * sizeof(T), GL_MAP_WRITE_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_to_gpu(): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__); + return; + } + // Note: cpuptr is a 'pointer to the beginning of the mapped range' and so we don't incorporate offset again, below: + for (unsigned int i = 0; i < sz; ++i) { cpuptr[i] = this->data[i]; } + glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__); + } + + // Map the GPU memory to CPU space, then copy the values into this->data. NB: it's a + // performance hit to *copy* to the mapped data to our sm::vec, because the data is + // *already in CPU accessible memory* after glMapBufferRange(). + // However, in case you need it, here it is. + template + void copy_from_gpu (std::size_t offset = 0u) + { + static_assert (_N <= N); + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__); + T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, offset * sizeof(T), _N * sizeof(T), GL_MAP_READ_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::copy_from_gpu(): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__); + return; + } + this->data.resize (_N); + for (unsigned int i = 0; i < _N; ++i) { data[i + offset] = cpuptr[i]; } + glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__); + } + + // Find the range of the data in the given Shader Storage Buffer Object + // + // ssbo_idx: The Index of the Shader Storage Buffer Object that we're reading from + // ssbo_name: The name (really a number) of the Shader Storage Buffer Object that we're reading from + // ssbo_num_elements: The number of elements of type T in the SSBO. + sm::range get_range() + { + std::size_t sz = this->data.size(); + sm::range r; + r.search_init(); + glBindBufferBase (GL_SHADER_STORAGE_BUFFER, index, this->name); + mplot::gl::Util::checkError (__FILE__, __LINE__); + T* cpuptr = static_cast(glMapBufferRange (GL_SHADER_STORAGE_BUFFER, 0, sz * sizeof(T), GL_MAP_READ_BIT)); + if (cpuptr == nullptr) { + std::cout << "ssbo::get_range(): MapBufferRange error" << std::endl; + mplot::gl::Util::checkError (__FILE__, __LINE__); + return r; + } + for (unsigned int i = 0; i < N; ++i) { r.update (cpuptr[i]); } + glUnmapBuffer (GL_SHADER_STORAGE_BUFFER); + glBindBuffer (GL_SHADER_STORAGE_BUFFER, 0); + mplot::gl::Util::checkError (__FILE__, __LINE__); + return r; + } + }; + +} // mplot::gl diff --git a/mplot/gl/version.h b/mplot/gl/version.h index b8b61e62..4d5cb2ca 100644 --- a/mplot/gl/version.h +++ b/mplot/gl/version.h @@ -66,6 +66,21 @@ namespace mplot::gl { return (((gl_version_number >> 30) & 0x1) > 0x0) ? true : false; } + // True if this version suports shader storage buffer objects + static bool constexpr has_ssbo (const int gl_version_number) + { + if (mplot::gl::version::gles (gl_version_number) == true) { + // OpenGL ES 3.1 and up supports SSBO + return (mplot::gl::version::major (gl_version_number) > 3 + || (mplot::gl::version::major (gl_version_number) == 3 + && mplot::gl::version::minor (gl_version_number) >= 1)); + } else { + // OpenGL 4.3 and up supports SSBO + return (mplot::gl::version::major (gl_version_number) > 4 + || (mplot::gl::version::major (gl_version_number) == 4 + && mplot::gl::version::minor (gl_version_number) >= 3)); + } + } // Output a string describing the version number static inline std::string vstring (const int gl_version_number) { diff --git a/shaders/Visual.vert.glsl b/shaders/Visual.vert.glsl index 1586f954..2eadc5b3 100644 --- a/shaders/Visual.vert.glsl +++ b/shaders/Visual.vert.glsl @@ -1,34 +1,21 @@ -// The coded-in shaders tell non-Mac platforms that they use OpenGL 4.5, but Mac limited to 4.1 #version 410 - -// ProjMatrix * RotnMatrix operation can be carried out on CPU with a single matrix -//uniform mat4 mvp_matrix; -// Or, and this is important for lighting effects and possibly text, too, matrices can be passed separately -//uniform mat4 vp_matrix; // sceneview-projection matrix -uniform mat4 m_matrix; // model matrix -uniform mat4 v_matrix; // scene view matrix -uniform mat4 p_matrix; // projection matrix -// alpha - to make a model see-through +uniform mat4 m_matrix; +uniform mat4 v_matrix; +uniform mat4 p_matrix; uniform float alpha; - -layout(location = 0) in vec4 position; // Attrib location 0 -layout(location = 1) in vec4 normalin; // Attrib location 1 -layout(location = 2) in vec3 color; // Attrib location 2 - +layout(location = 0) in vec4 position; +layout(location = 1) in vec4 normalin; +layout(location = 2) in vec3 color; out VERTEX { vec4 normal; - vec4 color; // Could make vec4 and incorporate alpha - vec3 fragpos; // fragment position + vec4 color; + vec3 fragpos; } vertex; - -void main (void) +void main() { gl_Position = (p_matrix * v_matrix * m_matrix * position); vertex.color = vec4(color, alpha); vertex.fragpos = vec3(m_matrix * position); - // Normals are all automatically computed, so there's no need for - // this line and the cube program doesn't bother to pass in the - // normals. Maybe required only for lighting? vertex.normal = normalin; } diff --git a/shaders/Visual_instancing.vert.glsl b/shaders/Visual_instancing.vert.glsl new file mode 100644 index 00000000..e11e5e8d --- /dev/null +++ b/shaders/Visual_instancing.vert.glsl @@ -0,0 +1,45 @@ +#version 430 +uniform mat4 m_matrix; +uniform mat4 v_matrix; +uniform mat4 p_matrix; +uniform float alpha; +uniform int instance_count = 0; +uniform int instance_start = -1; +uniform int instparam_count = 0; +layout(location = 0) in vec4 position; +layout(location = 1) in vec4 normalin; +layout(location = 2) in vec3 color; +layout (std430, binding = 1) buffer InstPos { float ipos[]; }; +layout (std430, binding = 2) buffer InstParam { float iparam[]; }; +out VERTEX +{ + vec4 normal; + vec4 color; + vec3 fragpos; +} vertex; +void main() +{ + if (instance_count > 0) { + int ipos_i = instance_start * 3 + gl_InstanceID * 3; + vec4 iposv = { ipos[ipos_i], ipos[ipos_i + 1], ipos[ipos_i + 2], 0 }; + if (instparam_count > 0) { + int idx = gl_InstanceID % instparam_count; + int ippos_i = instance_start * 5; + float s = iparam[ippos_i + idx * 5 + 4]; + vec3 p_three = vec3(position) * s; + vec4 p_scaled = vec4(p_three, 1.0); + gl_Position = (p_matrix * v_matrix * m_matrix * (p_scaled + iposv)); + vertex.color = vec4(iparam[ippos_i + idx * 5], iparam[ippos_i + idx * 5 + 1], iparam[ippos_i + idx * 5 + 2], iparam[ippos_i + idx * 5 + 3]); + vertex.fragpos = vec3(m_matrix * p_scaled); + } else { + gl_Position = (p_matrix * v_matrix * m_matrix * (position + iposv)); + vertex.color = vec4(color, alpha); + vertex.fragpos = vec3(m_matrix * position); + } + } else { + gl_Position = (p_matrix * v_matrix * m_matrix * position); + vertex.color = vec4(color, alpha); + vertex.fragpos = vec3(m_matrix * position); + } + vertex.normal = normalin; +}