diff --git a/data/assets/examples/nodearrow.asset b/data/assets/examples/nodearrow.asset new file mode 100644 index 0000000000..ef851baa48 --- /dev/null +++ b/data/assets/examples/nodearrow.asset @@ -0,0 +1,77 @@ +local earth = asset.require("scene/solarsystem/planets/earth/earth") +local moon = asset.require("scene/solarsystem/planets/earth/moon/moon") +local mars = asset.require("scene/solarsystem/planets/mars/mars") + + + +local EarthRadius = 6371000.0 + +local ArrowExample = { + Identifier = "RenderableNodeArrowExample", + Parent = earth.Earth.Identifier, + Renderable = { + Type = "RenderableNodeArrow", + StartNode = earth.Earth.Identifier, + EndNode = mars.Mars.Identifier, + Color = { 0.8, 1.0, 0.8 }, + Offset = 2 * EarthRadius, + Length = 5 * EarthRadius, + Width = 900000.0 + }, + GUI = { + Name = "Node Arrow Example", + Path = "/Example", + Description = [[Example node direction arrow, using absolute sizes]] + } +} + +-- Relative values: Multiplied with bounding sphere +local ArrowExample_RelativeUnits = { + Identifier = "RenderableNodeArrowExample_Relative", + Parent = earth.Earth.Identifier, + Renderable = { + Type = "RenderableNodeArrow", + StartNode = earth.Earth.Identifier, + EndNode = moon.Moon.Identifier, + Color = { 0.78, 0.0, 1.0 }, + UseRelativeOffset = true, + UseRelativeLength = true, + Offset = 2.0, + Length = 5.0, + Width = 900000.0, + Invert = true -- Point to start node instead of end node + }, + GUI = { + Name = "Node Arrow Example (relative units)", + Path = "/Example", + Description = [[Example node direction arrow, using relative sizes]] + } +} + + +asset.onInitialize(function() + openspace.addSceneGraphNode(ArrowExample) + openspace.addSceneGraphNode(ArrowExample_RelativeUnits) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(ArrowExample_RelativeUnits) + openspace.removeSceneGraphNode(ArrowExample) +end) + +asset.export(ArrowExample) +asset.export(ArrowExample_RelativeUnits) + + + +asset.meta = { + Name = "RenderableNodeArrow Example asset", + Version = "1.0", + Description = [[Examples of the RenderableNodeArrow renderable, that can be used to draw + an arrow pointing from one scene graph node in the direction of another. Note that + the arrows are generated as objects in 3D space and need to have a size that is + suitable for their 3D context.]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/nodeline.asset b/data/assets/examples/nodeline.asset new file mode 100644 index 0000000000..7f0edb27cb --- /dev/null +++ b/data/assets/examples/nodeline.asset @@ -0,0 +1,43 @@ +local earth = asset.require("scene/solarsystem/planets/earth/earth") +local mars = asset.require("scene/solarsystem/planets/mars/mars") + + + +local RenderableNodeLineExample = { + Identifier = "RenderableNodeLineExample", + Renderable = { + Type = "RenderableNodeLine", + StartNode = earth.Earth.Identifier, + EndNode = mars.Mars.Identifier, + Color = { 0.5, 0.5, 0.5 }, + LineWidth = 2 + }, + GUI = { + Name = "RenderableNodeLine Example", + Path = "/Example", + Description = [[Draws a line between two nodes in the scene.]] + } +} + + +asset.onInitialize(function() + openspace.addSceneGraphNode(RenderableNodeLineExample) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(RenderableNodeLineExample) +end) + +asset.export(RenderableNodeLineExample) + + + +asset.meta = { + Name = "RenderableNodeLine Example asset", + Version = "1.0", + Description = [[Example of a RenderableNodeLine, that can be used to draw an line + between two scene graph nodes.]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/include/openspace/rendering/helper.h b/include/openspace/rendering/helper.h index 0770617383..ddff046cd4 100644 --- a/include/openspace/rendering/helper.h +++ b/include/openspace/rendering/helper.h @@ -89,6 +89,22 @@ struct VertexObjects { int nElements = 64; } sphere; + struct { + GLuint vao = 0; + GLuint vbo = 0; + GLuint ibo = 0; + + int nElements = 64; + } cylinder; + + struct { + GLuint vao = 0; + GLuint vbo = 0; + GLuint ibo = 0; + + int nElements = 64; + } cone; + struct { GLuint vao = 0; } empty; @@ -119,6 +135,12 @@ struct VertexXYZNormal { GLfloat normal[3]; }; +template +struct VertexIndexListCombo { + std::vector vertices; + std::vector indices; +}; + VertexXYZ convertToXYZ(const Vertex& v); std::vector convert(std::vector v); @@ -126,9 +148,17 @@ std::vector convert(std::vector v); std::vector createRing(int nSegments, float radius, glm::vec4 colors = glm::vec4(1.f)); -std::pair, std::vector> +std::vector createRingXYZ(int nSegments, float radius); + +VertexIndexListCombo createSphere(int nSegments, glm::vec3 radii, glm::vec4 colors = glm::vec4(1.f)); +VertexIndexListCombo createCylinder(unsigned int nSegments, + float radius, float height); + +VertexIndexListCombo createCone(unsigned int nSegments, float radius, + float height); + /** * Data structure that can be used for rendering using multiple light directions */ diff --git a/modules/base/CMakeLists.txt b/modules/base/CMakeLists.txt index a0a14901f6..6559ad54b3 100644 --- a/modules/base/CMakeLists.txt +++ b/modules/base/CMakeLists.txt @@ -48,6 +48,7 @@ set(HEADER_FILES rendering/renderabledisc.h rendering/renderablelabel.h rendering/renderablemodel.h + rendering/renderablenodearrow.h rendering/renderablenodeline.h rendering/renderableplane.h rendering/renderableplaneimagelocal.h @@ -106,6 +107,7 @@ set(SOURCE_FILES rendering/renderabledisc.cpp rendering/renderablelabel.cpp rendering/renderablemodel.cpp + rendering/renderablenodearrow.cpp rendering/renderablenodeline.cpp rendering/renderableplane.cpp rendering/renderableplaneimagelocal.cpp @@ -142,6 +144,8 @@ set(SOURCE_FILES source_group("Source Files" FILES ${SOURCE_FILES}) set(SHADER_FILES + shaders/arrow_fs.glsl + shaders/arrow_vs.glsl shaders/axes_fs.glsl shaders/axes_vs.glsl shaders/disc_fs.glsl diff --git a/modules/base/basemodule.cpp b/modules/base/basemodule.cpp index 8b8bbf2bc5..9939cea058 100644 --- a/modules/base/basemodule.cpp +++ b/modules/base/basemodule.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -136,6 +137,7 @@ void BaseModule::internalInitialize(const ghoul::Dictionary&) { fRenderable->registerClass("RenderableGrid"); fRenderable->registerClass("RenderableLabel"); fRenderable->registerClass("RenderableModel"); + fRenderable->registerClass("RenderableNodeArrow"); fRenderable->registerClass("RenderableNodeLine"); fRenderable->registerClass("RenderablePlaneImageLocal"); fRenderable->registerClass("RenderablePlaneImageOnline"); @@ -222,6 +224,7 @@ std::vector BaseModule::documentations() const { RenderableGrid::Documentation(), RenderableLabel::Documentation(), RenderableModel::Documentation(), + RenderableNodeArrow::Documentation(), RenderableNodeLine::Documentation(), RenderablePlane::Documentation(), RenderablePlaneImageLocal::Documentation(), diff --git a/modules/base/rendering/renderablenodearrow.cpp b/modules/base/rendering/renderablenodearrow.cpp new file mode 100644 index 0000000000..1a6983874d --- /dev/null +++ b/modules/base/rendering/renderablenodearrow.cpp @@ -0,0 +1,515 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "RenderableNodeArrow"; + + constexpr openspace::properties::Property::PropertyInfo StartNodeInfo = { + "StartNode", + "Start Node", + "The identifier of the node the arrow starts from", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo EndNodeInfo = { + "EndNode", + "End Node", + "The identifier of the node the arrow should point towards", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo ColorInfo = { + "Color", + "Color", + "This value determines the RGB color for the arrow", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo SegmentsInfo = { + "Segments", + "Number of Segments", + "This value specifies the number of segments that the shapes for the arrow are " + "separated in. A higher number leads to a higher resolution", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo InvertInfo = { + "Invert", + "Invert Direction", + "If set to true, the arrow direction is inverted so that it points to the " + "start node instead of the end node", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo ArrowHeadSizeInfo = { + "ArrowHeadSize", + "Arrow Head Size", + "This size of the arrow head, given in relative value of the entire length of " + "the arrow. For example, 0.1 makes the arrow head length be 10% of the full " + "arrow length", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo ArrowHeadWidthInfo = { + "ArrowHeadWidthFactor", + "Arrow Head Width Factor", + "A factor that is multiplied with the width, or the arrow itself, to determine " + "the width of the base of the arrow head", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo OffsetDistanceInfo = { + "Offset", + "Offset Distance", + "The distance from the center of the start node where the arrow starts. " + "If 'UseRelativeOffset' is true, the value should be given as a factor to " + "multiply with the bounding sphere of the node. Otherwise, the value is " + "specified in meters", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo RelativeOffsetInfo = { + "UseRelativeOffset", + "Use Relative Offset Distance", + "Decide whether to use relative distances (in units of start node bounding " + "sphere) for the offset distance. If false, meters is used", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo LengthInfo = { + "Length", + "Length", + "The length of the arrow, given either in meters or as a factor to be " + "multiplied with the bounding sphere of the start node (if " + "'UseRelativeLength' is true)", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo RelativeLengthInfo = { + "UseRelativeLength", + "Use Relative Length", + "Decide whether to use relative size (in units of start node bounding " + "sphere) for the length of the arrow. If false, meters is used", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo WidthInfo = { + "Width", + "Width", + "This value specifies the width of the arrow shape", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo AmbientIntensityInfo = { + "AmbientIntensity", + "Ambient Intensity", + "A multiplier for ambient lighting for the shading of the arrow", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo DiffuseIntensityInfo = { + "DiffuseIntensity", + "Diffuse Intensity", + "A multiplier for diffuse lighting for the shading of the arrow", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo SpecularIntensityInfo = { + "SpecularIntensity", + "Specular Intensity", + "A multiplier for specular lighting for the shading of the arrow", + openspace::properties::Property::Visibility::User + }; + + constexpr openspace::properties::Property::PropertyInfo ShadingEnabledInfo = { + "PerformShading", + "Perform Shading", + "This value determines whether shading should be applied to the arrow model", + openspace::properties::Property::Visibility::User + }; + + struct [[codegen::Dictionary(RenderableNodeArrow)]] Parameters { + // [[codegen::verbatim(StartNodeInfo.description)]] + std::string startNode [[codegen::notempty]]; + + // [[codegen::verbatim(EndNodeInfo.description)]] + std::string endNode [[codegen::notempty]]; + + // [[codegen::verbatim(ColorInfo.description)]] + std::optional color [[codegen::color()]]; + + // [[codegen::verbatim(SegmentsInfo.description)]] + std::optional segments [[codegen::greaterequal(3)]]; + + // [[codegen::verbatim(InvertInfo.description)]] + std::optional invert; + + // [[codegen::verbatim(ArrowHeadSizeInfo.description)]] + std::optional arrowHeadSize [[codegen::greaterequal(0.f)]]; + + // [[codegen::verbatim(ArrowHeadWidthInfo.description)]] + std::optional arrowHeadWidthFactor [[codegen::greaterequal(0.f)]]; + + // [[codegen::verbatim(OffsetDistanceInfo.description)]] + std::optional offset; + + // [[codegen::verbatim(RelativeOffsetInfo.description)]] + std::optional useRelativeOffset; + + // [[codegen::verbatim(LengthInfo.description)]] + std::optional length [[codegen::greaterequal(0.f)]]; + + // [[codegen::verbatim(RelativeLengthInfo.description)]] + std::optional useRelativeLength; + + // [[codegen::verbatim(WidthInfo.description)]] + std::optional width [[codegen::greaterequal(0.f)]]; + + // [[codegen::verbatim(ShadingEnabledInfo.description)]] + std::optional performShading; + + // [[codegen::verbatim(AmbientIntensityInfo.description)]] + std::optional ambientIntensity [[codegen::greaterequal(0.f)]]; + + // [[codegen::verbatim(DiffuseIntensityInfo.description)]] + std::optional diffuseIntensity [[codegen::greaterequal(0.f)]]; + + // [[codegen::verbatim(SpecularIntensityInfo.description)]] + std::optional specularIntensity [[codegen::greaterequal(0.f)]]; + }; +#include "renderablenodearrow_codegen.cpp" + + void updateDistanceBasedOnRelativeValues(const std::string& nodeName, + bool useRelative, + openspace::properties::FloatProperty& prop) + { + using namespace::openspace; + + SceneGraphNode* startNode = sceneGraphNode(nodeName); + if (!startNode) { + LERROR(fmt::format("Could not find start node '{}'", nodeName)); + return; + } + const double boundingSphere = startNode->boundingSphere(); + + if (!useRelative) { + // Recompute distance (previous value was relative) + prop = static_cast(prop * boundingSphere); + prop.setExponent(11.f); + prop.setMaxValue(1e20f); + } + else { + // Recompute distance (previous value was in meters) + if (boundingSphere < std::numeric_limits::epsilon()) { + LERROR(fmt::format( + "Start node '{}' has invalid bounding sphere", nodeName + )); + return; + } + prop = static_cast(prop / boundingSphere); + prop.setExponent(3.f); + prop.setMaxValue(1000.f); + } + // @TODO (emmbr, 2022-08-22): make GUI update when min/max value is updated + }; +} // namespace + +namespace openspace { + +documentation::Documentation RenderableNodeArrow::Documentation() { + return codegen::doc("base_renderable_renderablenodearrow"); +} + +RenderableNodeArrow::Shading::Shading() + : properties::PropertyOwner({ "Shading" }) + , enabled(ShadingEnabledInfo, true) + , ambientIntensity(AmbientIntensityInfo, 0.2f, 0.f, 1.f) + , diffuseIntensity(DiffuseIntensityInfo, 0.7f, 0.f, 1.f) + , specularIntensity(SpecularIntensityInfo, 0.f, 0.f, 1.f) +{ + addProperty(enabled); + addProperty(ambientIntensity); + addProperty(diffuseIntensity); + addProperty(specularIntensity); +} + +RenderableNodeArrow::RenderableNodeArrow(const ghoul::Dictionary& dictionary) + : Renderable(dictionary) + , _start(StartNodeInfo) + , _end(EndNodeInfo) + , _color(ColorInfo, glm::vec3(1.f), glm::vec3(0.f), glm::vec3(1.f)) + , _segments(SegmentsInfo, 10, 3, 100) + , _invertArrowDirection(InvertInfo, false) + , _arrowHeadSize(ArrowHeadSizeInfo, 0.1f, 0.f, 1.f) + , _arrowHeadWidthFactor(ArrowHeadWidthInfo, 2.f, 1.f, 100.f) + , _offsetDistance(OffsetDistanceInfo, 0.f, 0.f, 1e20f) + , _useRelativeOffset(RelativeOffsetInfo, false) + , _length(LengthInfo, 100.f, 0.f, 1e20f) + , _useRelativeLength(RelativeLengthInfo, false) + , _width(WidthInfo, 10.f, 0.f, 1e11f) +{ + const Parameters p = codegen::bake(dictionary); + + _shading.enabled = p.performShading.value_or(_shading.enabled); + _shading.ambientIntensity = p.ambientIntensity.value_or(_shading.ambientIntensity); + _shading.diffuseIntensity = p.diffuseIntensity.value_or(_shading.diffuseIntensity); + _shading.specularIntensity = p.specularIntensity.value_or(_shading.specularIntensity); + addPropertySubOwner(_shading); + + addProperty(Fadeable::_opacity); + + _color = p.color.value_or(_color); + _color.setViewOption(properties::Property::ViewOptions::Color); + addProperty(_color); + + _start = p.startNode; + addProperty(_start); + + _end = p.endNode; + addProperty(_end); + + _segments = p.segments.value_or(_segments); + addProperty(_segments); + + _invertArrowDirection = p.invert.value_or(_invertArrowDirection); + addProperty(_invertArrowDirection); + + _arrowHeadSize = p.arrowHeadSize.value_or(_arrowHeadSize); + addProperty(_arrowHeadSize); + + _arrowHeadWidthFactor = p.arrowHeadWidthFactor.value_or(_arrowHeadWidthFactor); + addProperty(_arrowHeadWidthFactor); + + _width = p.width.value_or(_width); + _width.setExponent(10.f); + addProperty(_width); + + _useRelativeLength.onChange([this]() { + updateDistanceBasedOnRelativeValues(_start, _useRelativeLength, _length); + }); + _useRelativeLength = p.useRelativeLength.value_or(_useRelativeLength); + + _length = p.length.value_or(_length); + if (!_useRelativeLength) { + _length.setExponent(11.f); + } + addProperty(_length); + + _useRelativeOffset.onChange([this]() { + updateDistanceBasedOnRelativeValues(_start, _useRelativeOffset, _offsetDistance); + }); + _useRelativeOffset = p.useRelativeOffset.value_or(_useRelativeOffset); + + _offsetDistance = p.offset.value_or(_offsetDistance); + if (!_useRelativeOffset) { + _offsetDistance.setExponent(11.f); + } + addProperty(_offsetDistance); + + addProperty(_useRelativeLength); + addProperty(_useRelativeOffset); +} + +void RenderableNodeArrow::initializeGL() { + _shaderProgram = BaseModule::ProgramObjectManager.request( + "NodeDirectionLineProgram", + []() -> std::unique_ptr { + return global::renderEngine->buildRenderProgram( + "NodeDirectionLineProgram", + absPath("${MODULE_BASE}/shaders/arrow_vs.glsl"), + absPath("${MODULE_BASE}/shaders/arrow_fs.glsl") + ); + } + ); +} + +void RenderableNodeArrow::deinitializeGL() { + BaseModule::ProgramObjectManager.release( + "NodeDirectionLineProgram", + [](ghoul::opengl::ProgramObject* p) { + global::renderEngine->removeRenderProgram(p); + } + ); + _shaderProgram = nullptr; +} + +bool RenderableNodeArrow::isReady() const { + return _shaderProgram; +} + +void RenderableNodeArrow::updateShapeTransforms(const RenderData& data) { + SceneGraphNode* startNode = sceneGraphNode(_start); + SceneGraphNode* endNode = sceneGraphNode(_end); + + if (!startNode) { + LERROR(fmt::format("Could not find start node '{}'", _start.value())); + return; + } + + if (!endNode) { + LERROR(fmt::format("Could not find end node '{}'", _end.value())); + return; + } + + const double boundingSphere = startNode->boundingSphere(); + bool hasNoBoundingSphere = boundingSphere < std::numeric_limits::epsilon(); + + if (hasNoBoundingSphere && (_useRelativeLength || _useRelativeOffset)) { + LERROR(fmt::format( + "Node '{}' has no valid bounding sphere. Can not use relative values", + _end.value() + )); + return; + } + + double offset = static_cast(_offsetDistance); + if (_useRelativeOffset) { + offset *= boundingSphere; + } + + double length = static_cast(_length); + if (_useRelativeLength) { + length *= boundingSphere; + } + + // Take additional transformation scale into account + const glm::dmat4 s = glm::scale( + glm::dmat4(1.0), + glm::dvec3(data.modelTransform.scale) + ); + + // Update the position based on the arrowDirection of the nodes + glm::dvec3 startNodePos = startNode->worldPosition(); + glm::dvec3 endNodePos = endNode->worldPosition(); + + glm::dvec3 arrowDirection = glm::normalize(endNodePos - startNodePos); + glm::dvec3 startPos = glm::dvec3(startNodePos + offset * arrowDirection); + glm::dvec3 endPos = glm::dvec3(startPos + length * arrowDirection); + + if (_invertArrowDirection) { + std::swap(startPos, endPos); + arrowDirection *= -1.0; + } + + double coneLength = _arrowHeadSize * length; + double cylinderLength = length - coneLength; + double arrowHeadWidth = _width * _arrowHeadWidthFactor; + + // Create transformation matrices to reshape to size and position + _cylinderTranslation = glm::translate(glm::dmat4(1.0), startPos); + glm::dvec3 cylinderScale = glm::dvec3(s * glm::dvec4(_width, _width, cylinderLength, 0.0)); + _cylinderScale = glm::scale(glm::dmat4(1.0), cylinderScale); + + // Adapt arrow head start to scaled size + glm::dvec3 arrowHeadStartPos = startPos + cylinderScale.z * arrowDirection; + + _coneTranslation = glm::translate(glm::dmat4(1.0), arrowHeadStartPos); + glm::dvec3 coneScale = glm::dvec3(arrowHeadWidth, arrowHeadWidth, coneLength); + _coneScale = s * glm::scale(glm::dmat4(1.0), coneScale); + + // Rotation to point at the end node + glm::quat rotQuat = glm::rotation(glm::dvec3(0.0, 0.0, 1.0), arrowDirection); + _pointDirectionRotation = glm::dmat4(glm::toMat4(rotQuat)); +} + +void RenderableNodeArrow::render(const RenderData& data, RendererTasks&) { + updateShapeTransforms(data); + + // Cylinder transforms + glm::dmat4 modelTransform = + _cylinderTranslation * _pointDirectionRotation * _cylinderScale; + glm::dmat4 modelViewTransform = data.camera.combinedViewMatrix() * modelTransform; + glm::dmat4 normalTransform = glm::transpose(glm::inverse(modelViewTransform)); + + _shaderProgram->activate(); + + _shaderProgram->setUniform("modelViewTransform", glm::mat4(modelViewTransform)); + _shaderProgram->setUniform("projectionTransform", data.camera.projectionMatrix()); + _shaderProgram->setUniform("normalTransform", glm::mat3(normalTransform)); + + _shaderProgram->setUniform("color", _color); + _shaderProgram->setUniform("opacity", opacity()); + + _shaderProgram->setUniform("ambientIntensity", _shading.ambientIntensity); + _shaderProgram->setUniform("diffuseIntensity", _shading.diffuseIntensity); + _shaderProgram->setUniform("specularIntensity", _shading.specularIntensity); + _shaderProgram->setUniform("performShading", _shading.enabled); + + // Change GL state: + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnablei(GL_BLEND, 0); + + // Draw cylinder + glBindVertexArray(rendering::helper::vertexObjects.cylinder.vao); + glDrawElements( + GL_TRIANGLES, + rendering::helper::vertexObjects.cylinder.nElements, + GL_UNSIGNED_SHORT, + nullptr + ); + + // Update transforms and render cone + modelTransform = _coneTranslation * _pointDirectionRotation * _coneScale; + modelViewTransform = data.camera.combinedViewMatrix() * modelTransform; + normalTransform = glm::transpose(glm::inverse(modelViewTransform)); + + _shaderProgram->setUniform("modelViewTransform", glm::mat4(modelViewTransform)); + _shaderProgram->setUniform("normalTransform", glm::mat3(normalTransform)); + + glBindVertexArray(rendering::helper::vertexObjects.cone.vao); + glDrawElements( + GL_TRIANGLES, + rendering::helper::vertexObjects.cone.nElements, + GL_UNSIGNED_SHORT, + nullptr + ); + + // Restore GL State + glBindVertexArray(0); + global::renderEngine->openglStateCache().resetBlendState(); + + _shaderProgram->deactivate(); +} + +} // namespace openspace diff --git a/modules/base/rendering/renderablenodearrow.h b/modules/base/rendering/renderablenodearrow.h new file mode 100644 index 0000000000..f51bafbc28 --- /dev/null +++ b/modules/base/rendering/renderablenodearrow.h @@ -0,0 +1,103 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_BASE___RENDERABLENODEARROW___H__ +#define __OPENSPACE_MODULE_BASE___RENDERABLENODEARROW___H__ + +#include + +#include +#include +#include +#include +#include +#include + +namespace ghoul::opengl { class ProgramObject; } + +namespace openspace { + +namespace documentation { struct Documentation; } +class Translation; + +/** + * Generates an arrow shape that points from the start node to the + * end node + */ +class RenderableNodeArrow : public Renderable { +public: + RenderableNodeArrow(const ghoul::Dictionary& dictionary); + ~RenderableNodeArrow() override = default; + + void initializeGL() override; + void deinitializeGL() override; + + bool isReady() const override; + void render(const RenderData& data, RendererTasks& rendererTask) override; + + static documentation::Documentation Documentation(); + +private: + struct Shading : properties::PropertyOwner { + Shading(); + properties::BoolProperty enabled; + properties::FloatProperty ambientIntensity; + properties::FloatProperty diffuseIntensity; + properties::FloatProperty specularIntensity; + }; + + void updateShapeTransforms(const RenderData& data); + + Shading _shading; + + ghoul::opengl::ProgramObject* _shaderProgram; + + properties::StringProperty _start; + properties::StringProperty _end; + properties::Vec3Property _color; + + properties::UIntProperty _segments; + properties::BoolProperty _invertArrowDirection; + + properties::FloatProperty _arrowHeadSize; + properties::FloatProperty _arrowHeadWidthFactor; + + properties::FloatProperty _offsetDistance; + properties::BoolProperty _useRelativeOffset; + properties::FloatProperty _length; + properties::BoolProperty _useRelativeLength; + properties::FloatProperty _width; + + glm::dmat4 _cylinderTranslation = glm::dmat4(1.0); + glm::dmat4 _cylinderScale = glm::dmat4(1.0); + + glm::dmat4 _coneTranslation = glm::dmat4(1.0); + glm::dmat4 _coneScale = glm::dmat4(1.0); + + glm::dmat4 _pointDirectionRotation = glm::dmat4(1.0); +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_BASE___RENDERABLENODEARROW___H__ diff --git a/modules/base/rendering/renderableprism.cpp b/modules/base/rendering/renderableprism.cpp index 308391b9c3..04fe0a22ad 100644 --- a/modules/base/rendering/renderableprism.cpp +++ b/modules/base/rendering/renderableprism.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -98,20 +99,6 @@ namespace { openspace::properties::Property::Visibility::User }; - // Generate vertices around the unit circle on the XY-plane - std::vector unitCircleVertices(int sectorCount) { - std::vector vertices; - vertices.reserve(2 * sectorCount); - float sectorStep = glm::two_pi() / sectorCount; - - for (int i = 0; i < sectorCount; ++i) { - float sectorAngle = i * sectorStep; - vertices.push_back(cos(sectorAngle)); // x - vertices.push_back(sin(sectorAngle)); // y - } - return vertices; - } - struct [[codegen::Dictionary(RenderablePrism)]] Parameters { // [[codegen::verbatim(SegmentsInfo.description)]] int segments; @@ -236,17 +223,16 @@ void RenderablePrism::updateVertexData() { _vertexArray.clear(); _indexArray.clear(); + using namespace rendering::helper; + // Get unit circle vertices on the XY-plane - std::vector unitVertices = unitCircleVertices(_nShapeSegments); - std::vector unitVerticesLines = unitCircleVertices(_nLines); + std::vector unitVertices = createRingXYZ(_nShapeSegments.value(), 1.f); + std::vector unitVerticesLines = createRingXYZ(_nLines.value(), 1.f); // Put base vertices into array - for (int j = 0, k = 0; - j < _nShapeSegments && k < static_cast(unitVertices.size()); - ++j, k += 2) - { - float ux = unitVertices[k]; - float uy = unitVertices[k + 1]; + for (int j = 0; j < _nShapeSegments; ++j) { + float ux = unitVertices[j].xyz[0]; + float uy = unitVertices[j].xyz[1]; _vertexArray.push_back(ux * _baseRadius); // x _vertexArray.push_back(uy * _baseRadius); // y @@ -254,12 +240,9 @@ void RenderablePrism::updateVertexData() { } // Put top shape vertices into array - for (int j = 0, k = 0; - j < _nShapeSegments && k < static_cast(unitVertices.size()); - ++j, k += 2) - { - float ux = unitVertices[k]; - float uy = unitVertices[k + 1]; + for (int j = 0; j < _nShapeSegments; ++j) { + float ux = unitVertices[j].xyz[0]; + float uy = unitVertices[j].xyz[1]; _vertexArray.push_back(ux * _radius); // x _vertexArray.push_back(uy * _radius); // y @@ -280,12 +263,9 @@ void RenderablePrism::updateVertexData() { _vertexArray.push_back(_length); } else { - for (int j = 0, k = 0; - j < _nLines && k < static_cast(unitVerticesLines.size()); - ++j, k += 2) - { - float ux = unitVerticesLines[k]; - float uy = unitVerticesLines[k + 1]; + for (int j = 0; j < _nLines; ++j) { + float ux = unitVerticesLines[j].xyz[0]; + float uy = unitVerticesLines[j].xyz[1]; // Base _vertexArray.push_back(ux * _baseRadius); // x diff --git a/modules/base/shaders/arrow_fs.glsl b/modules/base/shaders/arrow_fs.glsl new file mode 100644 index 0000000000..036e67ed18 --- /dev/null +++ b/modules/base/shaders/arrow_fs.glsl @@ -0,0 +1,75 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include "fragment.glsl" + +in float vs_depth; +in vec3 vs_normal; +in vec4 vs_positionViewSpace; + +uniform vec3 color; +uniform float opacity; +uniform float ambientIntensity = 0.2; +uniform float diffuseIntensity = 1.0; +uniform float specularIntensity = 1.0; +uniform bool performShading = true; + +const vec3 LightColor = vec3(1.0); + +Fragment getFragment() { + Fragment frag; + + frag.color = vec4(color, opacity); + + // Simple phong shading (same color for diffuse and ambient. White specular) + if (performShading) { + const vec3 lightDirectionViewSpace = vec3(0.0, 0.0, 1.0); + const float lightIntensity = 1.0; + const float specularPower = 100.0; + + // Ambient color + vec3 shadedColor = ambientIntensity * color; + + // Diffuse + vec3 n = normalize(vs_normal); + vec3 l = lightDirectionViewSpace; + shadedColor += diffuseIntensity * max(dot(n,l), 0.0) * color; + + // Specular + vec3 viewDir = normalize(vs_positionViewSpace.xyz); + vec3 reflectDir = reflect(l, n); + shadedColor += + specularIntensity * pow(max(dot(viewDir,reflectDir), 0), specularPower) * color; + + // Light contribution (one light soruce) + shadedColor *= lightIntensity * LightColor; + + frag.color.xyz = shadedColor; + } + + frag.depth = vs_depth; + frag.gPosition = vs_positionViewSpace; + frag.gNormal = vec4(0.0, 0.0, 0.0, 1.0); + return frag; +} diff --git a/modules/base/shaders/arrow_vs.glsl b/modules/base/shaders/arrow_vs.glsl new file mode 100644 index 0000000000..919e838d2d --- /dev/null +++ b/modules/base/shaders/arrow_vs.glsl @@ -0,0 +1,47 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2023 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#version __CONTEXT__ + +layout(location = 0) in vec3 in_position; +layout(location = 1) in vec3 in_normal; + +out float vs_depth; +out vec3 vs_normal; +out vec4 vs_positionViewSpace; + +uniform mat4 modelViewTransform; +uniform mat4 projectionTransform; +uniform mat3 normalTransform; + +void main() { + vs_positionViewSpace = vec4(modelViewTransform * dvec4(in_position, 1)); + vec4 positionScreenSpace = projectionTransform * vs_positionViewSpace; + vs_depth = positionScreenSpace.w; + vs_normal = normalize(normalTransform * in_normal); + gl_Position = positionScreenSpace; + + // Set z to 0 to disable near and far plane, unique handling for perspective in space + gl_Position.z = 0.f; +} diff --git a/src/rendering/helper.cpp b/src/rendering/helper.cpp index 8184c7b605..28d9080b50 100644 --- a/src/rendering/helper.cpp +++ b/src/rendering/helper.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -246,10 +247,11 @@ void initialize() { reinterpret_cast(offsetof(VertexXYUVRGBA, rgba))); glBindVertexArray(0); + // // Sphere vertex array object // - std::pair, std::vector> sphereData = createSphere( + VertexIndexListCombo sphereData = createSphere( 64, glm::vec3(1.f, 1.f, 1.f), glm::vec4(1.f, 1.f, 1.f, 1.f) ); @@ -261,16 +263,16 @@ void initialize() { glBindBuffer(GL_ARRAY_BUFFER, vertexObjects.sphere.vbo); glBufferData( GL_ARRAY_BUFFER, - sphereData.first.size() * sizeof(Vertex), - sphereData.first.data(), + sphereData.vertices.size() * sizeof(Vertex), + sphereData.vertices.data(), GL_STATIC_DRAW ); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexObjects.sphere.ibo); glBufferData( GL_ELEMENT_ARRAY_BUFFER, - sphereData.second.size() * sizeof(GLushort), - sphereData.second.data(), + sphereData.indices.size() * sizeof(GLushort), + sphereData.indices.data(), GL_STATIC_DRAW ); glEnableVertexAttribArray(0); @@ -282,7 +284,75 @@ void initialize() { glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, rgba))); glBindVertexArray(0); - vertexObjects.sphere.nElements = static_cast(sphereData.second.size()); + vertexObjects.sphere.nElements = static_cast(sphereData.indices.size()); + + + // + // Cylinder vertex array object + // + VertexIndexListCombo cylinderData = createCylinder(64, 1.f, 1.f); + + glGenVertexArrays(1, &vertexObjects.cylinder.vao); + glGenBuffers(1, &vertexObjects.cylinder.vbo); + glGenBuffers(1, &vertexObjects.cylinder.ibo); + + glBindVertexArray(vertexObjects.cylinder.vao); + glBindBuffer(GL_ARRAY_BUFFER, vertexObjects.cylinder.vbo); + glBufferData( + GL_ARRAY_BUFFER, + cylinderData.vertices.size() * sizeof(VertexXYZNormal), + cylinderData.vertices.data(), + GL_STATIC_DRAW + ); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexObjects.cylinder.ibo); + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + cylinderData.indices.size() * sizeof(GLushort), + cylinderData.indices.data(), + GL_STATIC_DRAW + ); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexXYZNormal), nullptr); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexXYZNormal), + reinterpret_cast(offsetof(VertexXYZNormal, normal))); + glBindVertexArray(0); + vertexObjects.cylinder.nElements = static_cast(cylinderData.indices.size()); + + + // + // Cone vertex array object + // + VertexIndexListCombo coneData = createCone(64, 1.f, 1.f); + + glGenVertexArrays(1, &vertexObjects.cone.vao); + glGenBuffers(1, &vertexObjects.cone.vbo); + glGenBuffers(1, &vertexObjects.cone.ibo); + + glBindVertexArray(vertexObjects.cone.vao); + glBindBuffer(GL_ARRAY_BUFFER, vertexObjects.cone.vbo); + glBufferData( + GL_ARRAY_BUFFER, + coneData.vertices.size() * sizeof(VertexXYZNormal), + coneData.vertices.data(), + GL_STATIC_DRAW + ); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexObjects.cone.ibo); + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + coneData.indices.size() * sizeof(GLushort), + coneData.indices.data(), + GL_STATIC_DRAW + ); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexXYZNormal), nullptr); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexXYZNormal), + reinterpret_cast(offsetof(VertexXYZNormal, normal))); + glBindVertexArray(0); + vertexObjects.cone.nElements = static_cast(coneData.indices.size()); // @@ -320,6 +390,14 @@ void deinitialize() { glDeleteBuffers(1, &vertexObjects.sphere.vbo); glDeleteBuffers(1, &vertexObjects.sphere.ibo); + glDeleteVertexArrays(1, &vertexObjects.cylinder.vao); + glDeleteBuffers(1, &vertexObjects.cylinder.vbo); + glDeleteBuffers(1, &vertexObjects.cylinder.ibo); + + glDeleteVertexArrays(1, &vertexObjects.cone.vao); + glDeleteBuffers(1, &vertexObjects.cone.vbo); + glDeleteBuffers(1, &vertexObjects.cone.ibo); + glDeleteVertexArrays(1, &vertexObjects.empty.vao); isInitialized = false; @@ -423,32 +501,48 @@ std::vector convert(std::vector v) { return result; } +Vertex computeCircleVertex(int i, int nSegments, float radius, + glm::vec4 color = glm::vec4(1.f)) +{ + const float fsegments = static_cast(nSegments); + + const float fi = static_cast(i); + + const float theta = fi * glm::two_pi() / fsegments; // 0 -> 2*PI + + const float x = radius * std::cos(theta); + const float y = radius * std::sin(theta); + const float z = 0.f; + + const float u = std::cos(theta); + const float v = std::sin(theta); + + return { x, y, z, u, v, color.r, color.g, color.b, color.a }; +} + std::vector createRing(int nSegments, float radius, glm::vec4 colors) { const int nVertices = nSegments + 1; std::vector vertices(nVertices); - const float fsegments = static_cast(nSegments); - for (int i = 0; i <= nSegments; ++i) { - const float fi = static_cast(i); - - const float theta = fi * glm::pi() * 2.f / fsegments; // 0 -> 2*PI - - const float x = radius * std::cos(theta); - const float y = radius * std::sin(theta); - const float z = 0.f; - - const float u = std::cos(theta); - const float v = std::sin(theta); - - vertices[i] = { x, y, z, u, v, colors.r, colors.g, colors.b, colors.a }; + vertices[i] = computeCircleVertex(i, nSegments, radius, colors); } return vertices; } -std::pair, std::vector> createSphere(int nSegments, - glm::vec3 radii, - glm::vec4 colors) +std::vector createRingXYZ(int nSegments, float radius) { + const int nVertices = nSegments + 1; + std::vector vertices(nVertices); + + for (int i = 0; i <= nSegments; ++i) { + Vertex fullVertex = computeCircleVertex(i, nSegments, radius); + vertices[i] = { fullVertex.xyz[0], fullVertex.xyz[1], fullVertex.xyz[2] }; + } + return vertices; +} + +VertexIndexListCombo createSphere(int nSegments, glm::vec3 radii, + glm::vec4 colors) { std::vector vertices; vertices.reserve(nSegments * nSegments); @@ -505,6 +599,153 @@ std::pair, std::vector> createSphere(int nSegments return { vertices, indices }; } +VertexIndexListCombo createConicalCylinder(unsigned int nSegments, + float bottomRadius, + float topRadius, + float height) +{ + // Create a ring for the top and bottom vertices (XY plane) + std::vector bottomVertices = createRingXYZ(nSegments, bottomRadius); + std::vector topVertices = createRingXYZ(nSegments, topRadius); + + // Build the 4 rings of vertices (with different normals), that will make up the + // shape for the cylinder + std::vector vertices; + vertices.reserve(4 * bottomVertices.size() + 2); + + // Center bottom vertex + vertices.push_back({ + .xyz = { 0.f, 0.f, 0.f }, + .normal = { 0.f, 0.f, -1.f } + }); + + std::vector verts0; + verts0.reserve(bottomVertices.size()); + std::vector verts1; + verts1.reserve(bottomVertices.size()); + std::vector verts2; + verts2.reserve(bottomVertices.size()); + std::vector verts3; + verts3.reserve(bottomVertices.size()); + + for (size_t i = 0; i < bottomVertices.size(); i++) { + const VertexXYZ& vBot = bottomVertices[i]; + VertexXYZ& vTop = topVertices[i]; + vTop.xyz[2] += height; + + glm::vec3 sideNormal; + if (std::abs(bottomRadius - topRadius) < std::numeric_limits::epsilon()) { + sideNormal = glm::normalize( + glm::vec3(vBot.xyz[0], vBot.xyz[1], vBot.xyz[2]) + ); + } + else { + glm::vec3 p = glm::closestPointOnLine( + glm::vec3(0.f), + glm::vec3(vBot.xyz[0], vBot.xyz[1], vBot.xyz[2]), + glm::vec3(vTop.xyz[0], vTop.xyz[1], vTop.xyz[2]) + ); + sideNormal = glm::normalize(p); + } + + // Ring 0 - vertices of bottom circle, with normals pointing down + verts0.push_back({ + .xyz = { vBot.xyz[0], vBot.xyz[1], vBot.xyz[2] }, + .normal = { 0.f, 0.f, -1.f } + }); + + // Ring 1 - bottom vertices of cylider sides with normals pointing outwards + verts1.push_back({ + .xyz = { vBot.xyz[0], vBot.xyz[1], vBot.xyz[2] }, + .normal = { sideNormal.x, sideNormal.y, sideNormal.z } + }); + + // Ring 2 - top vertices of cylinder side, normals pointing outwards + // Note that only difference between top and bottom is the height added to Z + verts2.push_back({ + .xyz = { vTop.xyz[0], vTop.xyz[1], vTop.xyz[2] }, + .normal = { sideNormal.x, sideNormal.y, sideNormal.z } + }); + + // Ring 3 - vertices of top circle, normals pointing up + verts3.push_back({ + .xyz = { vTop.xyz[0], vTop.xyz[1], vTop.xyz[2] }, + .normal = { 0.f, 0.f, 1.f } + }); + } + + vertices.insert(vertices.end(), verts0.begin(), verts0.end()); + vertices.insert(vertices.end(), verts1.begin(), verts1.end()); + vertices.insert(vertices.end(), verts2.begin(), verts2.end()); + vertices.insert(vertices.end(), verts3.begin(), verts3.end()); + + // Center top vertex + vertices.push_back({ + .xyz = { 0.f, 0.f, height }, + .normal = { 0.f, 0.f, 1.f } + }); + + // Contruct the index list, based on the above vertex rings + std::vector indexArray; + indexArray.reserve(4 * 3 * nSegments); + + auto ringVerticeIndex = [&nSegments](unsigned int ringIndex, unsigned int i) { + return static_cast(1 + ringIndex * (nSegments + 1) + i); + }; + + GLushort botCenterIndex = 0; + GLushort topCenterIndex = static_cast(vertices.size()) - 1; + + for (unsigned int i = 0; i < nSegments; ++i) { + bool isLast = (i == nSegments - 1); + GLushort v0, v1, v2, v3; + + // Bottom triangle + v0 = ringVerticeIndex(0, i); + v1 = ringVerticeIndex(0, isLast ? 0 : i + 1); + indexArray.push_back(botCenterIndex); + indexArray.push_back(v1); + indexArray.push_back(v0); + + // Side of cylinder + + // Bottom ring + v0 = ringVerticeIndex(1, i); + v1 = ringVerticeIndex(1, isLast ? 0 : i + 1); + // Top ring + v2 = ringVerticeIndex(2, i); + v3 = ringVerticeIndex(2, isLast ? 0 : i + 1); + indexArray.push_back(v0); + indexArray.push_back(v1); + indexArray.push_back(v2); + + indexArray.push_back(v1); + indexArray.push_back(v3); + indexArray.push_back(v2); + + // Top triangle + v0 = ringVerticeIndex(3, i); + v1 = ringVerticeIndex(3, isLast ? 0 : i + 1); + indexArray.push_back(topCenterIndex); + indexArray.push_back(v0); + indexArray.push_back(v1); + } + + return { vertices, indexArray }; +} + +VertexIndexListCombo createCylinder(unsigned int nSegments, + float radius, float height) +{ + return createConicalCylinder(nSegments, radius, radius, height); +} + +VertexIndexListCombo createCone(unsigned int nSegments, float radius, + float height) +{ + return createConicalCylinder(nSegments, radius, 0.f, height); +} + void LightSourceRenderData::updateBasedOnLightSources(const RenderData& renderData, const std::vector>& sources) {