diff --git a/modules/base/rendering/grids/renderablesphericalgrid.cpp b/modules/base/rendering/grids/renderablesphericalgrid.cpp index ccb2cea50c..87f22292a6 100644 --- a/modules/base/rendering/grids/renderablesphericalgrid.cpp +++ b/modules/base/rendering/grids/renderablesphericalgrid.cpp @@ -136,9 +136,8 @@ RenderableSphericalGrid::RenderableSphericalGrid(const ghoul::Dictionary& dictio addProperty(_color); auto gridDirty = [this]() { - if (_longSegments.value() % 2 == 1) { - _longSegments = _longSegments - 1; - } + _longSegments = std::max(_longSegments, 3); + _latSegments = std::max(_latSegments, 3); _gridIsDirty = true; }; _longSegments = p.segments.value_or(p.longSegments.value_or(_longSegments)); @@ -188,11 +187,9 @@ void RenderableSphericalGrid::initializeGL() { glGenVertexArrays(1, &_vaoID); glGenBuffers(1, &_vBufferID); - glGenBuffers(1, &_iBufferID); glBindVertexArray(_vaoID); glBindBuffer(GL_ARRAY_BUFFER, _vBufferID); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _iBufferID); glEnableVertexAttribArray(0); glBindVertexArray(0); } @@ -204,9 +201,6 @@ void RenderableSphericalGrid::deinitializeGL() { glDeleteBuffers(1, &_vBufferID); _vBufferID = 0; - glDeleteBuffers(1, &_iBufferID); - _iBufferID = 0; - BaseModule::ProgramObjectManager.release( "GridProgram", [](ghoul::opengl::ProgramObject* p) { @@ -239,8 +233,23 @@ void RenderableSphericalGrid::render(const RenderData& data, RendererTasks&) { glEnable(GL_LINE_SMOOTH); glBindVertexArray(_vaoID); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _iBufferID); - glDrawElements(GL_LINES, 6 * _longSegments * _latSegments, GL_UNSIGNED_INT, nullptr); + + // Render latitude rings + glMultiDrawArrays( + GL_LINE_LOOP, + _latitudeRenderInfo.first.data(), + _latitudeRenderInfo.count.data(), + _latSegments + ); + + // Render longitude segments + glMultiDrawArrays( + GL_LINE_STRIP, + _longitudeRenderInfo.first.data(), + _longitudeRenderInfo.count.data(), + _longSegments + ); + glBindVertexArray(0); _gridProgram->deactivate(); @@ -286,71 +295,72 @@ void RenderableSphericalGrid::update(const UpdateData&) { return; } - unsigned int vertSize = (_longSegments + 1) * (_latSegments + 1); - std::vector vert = std::vector(vertSize, { 0.f, 0.f, 0.f }); - unsigned int idxSize = 6 * _longSegments * _latSegments; - std::vector idx = std::vector(idxSize, 0); + // Instead of using an element buffer which didn't save that much memory and just + // caused some indirections, we are creating two sets of vertices in the same vertex + // buffer in this function. First all of the vertices for the longitudinal rings, one + // ring after another. After that it is all the vertices for the latitudinal arcs, one + // arc after another - int nr = 0; - - for (int lat = 0; lat <= _latSegments; lat++) { - // define an extra vertex around the y-axis due to texture mapping - for (int lng = 0; lng <= _longSegments; lng++) { + // * 2 since we store all vertices twice + const unsigned int vertSize = _longSegments * _latSegments * 2; + std::vector vert; + vert.reserve(vertSize); + for (int lat = 0; lat < _latSegments; lat++) { + for (int lng = 0; lng < _longSegments; lng++) { // inclination angle (north to south) - const float theta = lat * glm::pi() / _latSegments * 2.f; // 0 -> PI + const float theta = + static_cast(lat) / static_cast(_latSegments - 1) * + glm::pi(); // 0 -> PI // azimuth angle (east to west) - const float phi = lng * 2.f * glm::pi() / _longSegments; // 0 -> 2*PI + // Dividing by one segment more as the points for 0 and 2*pi are identical + const float phi = + static_cast(lng) / static_cast(_longSegments) * + 2.f * glm::pi(); // 0 -> 2*PI const float x = std::sin(phi) * std::sin(theta); // - const float y = std::cos(theta); // up - const float z = std::cos(phi) * std::sin(theta); // - - glm::vec3 normal = glm::vec3(x, y, z); - if (x != 0.f || y != 0.f || z != 0.f) { - normal = glm::normalize(normal); - } - - glm::vec4 tmp = glm::vec4(x, y, z, 1.f); - const glm::mat4 rot = glm::rotate( - glm::mat4(1.f), - glm::half_pi(), - glm::vec3(1.f, 0.f, 0.f) - ); - tmp = glm::vec4(glm::dmat4(rot) * glm::dvec4(tmp)); - - for (int i = 0; i < 3; i++) { - vert[nr].location[i] = tmp[i]; - } - ++nr; + const float y = std::cos(phi) * std::sin(theta); // + const float z = std::cos(theta); // up + vert.push_back({ x, y, z }); } } - nr = 0; - // define indices for all triangles - for (int i = 1; i <= _latSegments; i++) { - for (int j = 0; j < _longSegments; j++) { - const int t = _longSegments + 1; - idx[nr] = t * (i - 1) + j + 0; ++nr; - idx[nr] = t * (i + 0) + j + 0; ++nr; - idx[nr] = t * (i + 0) + j + 1; ++nr; - idx[nr] = t * (i - 1) + j + 1; ++nr; - idx[nr] = t * (i - 1) + j + 0; ++nr; + // Create the render info struct to be able to render the longitude rings using + // glMultiDrawArrays in the render function + _latitudeRenderInfo.first.clear(); + _latitudeRenderInfo.count.clear(); + for (int i = 0; i < _latSegments; i++) { + _latitudeRenderInfo.first.push_back(i * _longSegments); + _latitudeRenderInfo.count.push_back(_longSegments); + } + + // Create the duplicate vertex entries to efficiently render the latitude arcs. We + // take every vertex in a longitude segment and connect it to the same index along all + // latitude arcs + for (int lng = 0; lng < _longSegments; lng++) { + for (int lat = 0; lat < _latSegments; lat++) { + Vertex v = vert[lat * _longSegments + lng]; + vert.push_back(v); } } + // Create the render info struct to be able to render the latitude arcs using + // glMultiDrawArrays in the render function. The `base` is the offset to make this + // render call use the vertices that are in the second "block" of the VBO + _longitudeRenderInfo.first.clear(); + _longitudeRenderInfo.count.clear(); + const int base = _longSegments * _latSegments; + for (int i = 0; i < _longSegments; i++) { + _longitudeRenderInfo.first.push_back(i * _latSegments + base); + _longitudeRenderInfo.count.push_back(_latSegments); + } + + glBindVertexArray(_vaoID); glBindBuffer(GL_ARRAY_BUFFER, _vBufferID); glBufferData(GL_ARRAY_BUFFER, vertSize * sizeof(Vertex), vert.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _iBufferID); - glBufferData( - GL_ELEMENT_ARRAY_BUFFER, - idxSize * sizeof(int), - idx.data(), GL_STATIC_DRAW - ); - _gridIsDirty = false; } diff --git a/modules/base/rendering/grids/renderablesphericalgrid.h b/modules/base/rendering/grids/renderablesphericalgrid.h index 07cc8e6caa..3f24f9d331 100644 --- a/modules/base/rendering/grids/renderablesphericalgrid.h +++ b/modules/base/rendering/grids/renderablesphericalgrid.h @@ -69,7 +69,14 @@ protected: GLuint _vaoID = 0; GLuint _vBufferID = 0; - GLuint _iBufferID = 0; + struct { + std::vector first; + std::vector count; + } _latitudeRenderInfo; + struct { + std::vector first; + std::vector count; + } _longitudeRenderInfo; bool _gridIsDirty = true; diff --git a/visualtests/misc/renderablesphericalgrid/combined.ostest b/visualtests/misc/renderablesphericalgrid/combined.ostest new file mode 100644 index 0000000000..c550a734fd --- /dev/null +++ b/visualtests/misc/renderablesphericalgrid/combined.ostest @@ -0,0 +1,64 @@ +{ + "profile": "empty", + "commands": [ + { + "type": "asset", + "value": "scene/digitaluniverse/grids.asset" + }, + { + "type": "navigationstate", + "value": { + "anchor": "Root", + "position": [ + 5.772931777994144e+17, + 1.4835370783876826e+18, + 1.3873651729766423e+18 + ], + "up": [ + -0.928522915381148, + 0.3711269304441822, + -0.01048795076135034 + ], + "timestamp": "2025 SEP 29 18:58:29" + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.Enabled", + "value": true + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.Opacity", + "value": 0.25 + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LineWidth", + "value": 10 + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LongSegments", + "value": 4 + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LatSegments", + "value": 128 + } + }, + { + "type": "screenshot" + } + ] +} diff --git a/visualtests/misc/renderablesphericalgrid/high-latitude-segments.ostest b/visualtests/misc/renderablesphericalgrid/high-latitude-segments.ostest new file mode 100644 index 0000000000..daffa427ee --- /dev/null +++ b/visualtests/misc/renderablesphericalgrid/high-latitude-segments.ostest @@ -0,0 +1,50 @@ +{ + "profile": "empty", + "commands": [ + { + "type": "asset", + "value": "scene/digitaluniverse/grids.asset" + }, + { + "type": "navigationstate", + "value": { + "anchor": "Root", + "position": [ + 5.772931777994144e+17, + 1.4835370783876826e+18, + 1.3873651729766423e+18 + ], + "up": [ + -0.928522915381148, + 0.3711269304441822, + -0.01048795076135034 + ], + "timestamp": "2025 SEP 29 18:58:29" + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.Enabled", + "value": true + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LongSegments", + "value": 4 + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LatSegments", + "value": 128 + } + }, + { + "type": "screenshot" + } + ] +} diff --git a/visualtests/misc/renderablesphericalgrid/high-longitude-segments.ostest b/visualtests/misc/renderablesphericalgrid/high-longitude-segments.ostest new file mode 100644 index 0000000000..25b42aae42 --- /dev/null +++ b/visualtests/misc/renderablesphericalgrid/high-longitude-segments.ostest @@ -0,0 +1,50 @@ +{ + "profile": "empty", + "commands": [ + { + "type": "asset", + "value": "scene/digitaluniverse/grids.asset" + }, + { + "type": "navigationstate", + "value": { + "anchor": "Root", + "position": [ + 5.772931777994144e+17, + 1.4835370783876826e+18, + 1.3873651729766423e+18 + ], + "up": [ + -0.928522915381148, + 0.3711269304441822, + -0.01048795076135034 + ], + "timestamp": "2025 SEP 29 18:58:29" + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.Enabled", + "value": true + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LongSegments", + "value": 128 + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LatSegments", + "value": 4 + } + }, + { + "type": "screenshot" + } + ] +} diff --git a/visualtests/misc/renderablesphericalgrid/linewidth.ostest b/visualtests/misc/renderablesphericalgrid/linewidth.ostest new file mode 100644 index 0000000000..de7f18bf66 --- /dev/null +++ b/visualtests/misc/renderablesphericalgrid/linewidth.ostest @@ -0,0 +1,43 @@ +{ + "profile": "empty", + "commands": [ + { + "type": "asset", + "value": "scene/digitaluniverse/grids.asset" + }, + { + "type": "navigationstate", + "value": { + "anchor": "Root", + "position": [ + 5.772931777994144e+17, + 1.4835370783876826e+18, + 1.3873651729766423e+18 + ], + "up": [ + -0.928522915381148, + 0.3711269304441822, + -0.01048795076135034 + ], + "timestamp": "2025 SEP 29 18:58:29" + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.Enabled", + "value": true + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.LineWidth", + "value": 10 + } + }, + { + "type": "screenshot" + } + ] +} diff --git a/visualtests/misc/renderablesphericalgrid/opacity.ostest b/visualtests/misc/renderablesphericalgrid/opacity.ostest new file mode 100644 index 0000000000..5246423ad0 --- /dev/null +++ b/visualtests/misc/renderablesphericalgrid/opacity.ostest @@ -0,0 +1,43 @@ +{ + "profile": "empty", + "commands": [ + { + "type": "asset", + "value": "scene/digitaluniverse/grids.asset" + }, + { + "type": "navigationstate", + "value": { + "anchor": "Root", + "position": [ + 5.772931777994144e+17, + 1.4835370783876826e+18, + 1.3873651729766423e+18 + ], + "up": [ + -0.928522915381148, + 0.3711269304441822, + -0.01048795076135034 + ], + "timestamp": "2025 SEP 29 18:58:29" + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.Enabled", + "value": true + } + }, + { + "type": "property", + "value": { + "property": "Scene.RadioSphere.Renderable.Opacity", + "value": 0.25 + } + }, + { + "type": "screenshot" + } + ] +}