view OrthancStone/Sources/Scene2D/Internals/OpenGLLinesProgram.cpp @ 1936:224d7763eede

sync
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 06 May 2022 15:30:26 +0200
parents 7053b8a0aaec
children 8bb8d9c0cfd0 07964689cb0b
line wrap: on
line source

/**
 * Stone of Orthanc
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2022 Osimis S.A., Belgium
 * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#include "OpenGLLinesProgram.h"
#include "OpenGLShaderVersionDirective.h"

#include <Logging.h>
#include <OrthancException.h>


static const unsigned int COMPONENTS_POSITION = 3;
static const unsigned int COMPONENTS_COLOR = 3;
static const unsigned int COMPONENTS_MITER = 2;


static const char* VERTEX_SHADER = 
  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
  "attribute vec2 a_miter_direction; \n"
  "attribute vec4 a_position;        \n"
  "attribute vec3 a_color;           \n"
  "uniform float u_thickness;        \n"
  "uniform mat4 u_matrix;            \n"
  "varying float v_distance;         \n"
  "varying vec3 v_color;             \n"
  "void main()                       \n"
  "{                                 \n"
  "  v_distance = a_position.z;      \n"
  "  v_color = a_color;              \n"
  "  gl_Position = u_matrix * vec4(a_position.xy + a_position.z * a_miter_direction * u_thickness, 0, 1); \n"
  "}";


static const char* FRAGMENT_SHADER = 
  ORTHANC_STONE_OPENGL_SHADER_VERSION_DIRECTIVE
  "uniform bool u_antialiasing;           \n"
  "uniform float u_antialiasing_start;    \n"
  "varying float v_distance;              \n"   // Distance of the point to the segment
  "varying vec3 v_color;                  \n"
  "void main()                            \n"
  "{                                      \n"
  "  float d = abs(v_distance);           \n"
  "  if (!u_antialiasing ||               \n"
  "      d <= u_antialiasing_start)       \n"
  "    gl_FragColor = vec4(v_color, 1);   \n"
  "  else if (d >= 1.0)                   \n"
  "    gl_FragColor = vec4(0, 0, 0, 0);   \n"
  "  else                                 \n"
  "  {                                    \n"
  "    float alpha = 1.0 - smoothstep(u_antialiasing_start, 1.0, d); \n"
  "    gl_FragColor = vec4(v_color * alpha, alpha); \n"
  "  }                                    \n"
  "}";


namespace OrthancStone
{
  namespace Internals
  {
    class OpenGLLinesProgram::Data::Segment
    {
    private:
      bool    isEmpty_;
      double  x1_;
      double  y1_;
      double  x2_;
      double  y2_;
      double  miterX1_;
      double  miterY1_;
      double  miterX2_;
      double  miterY2_;

      Vector  lineAbove_;  // In homogeneous coordinates (size = 3)
      Vector  lineBelow_;

    public:
      Segment(const PolylineSceneLayer::Chain& chain,
              size_t index1,
              size_t index2) :
        isEmpty_(false)
      {
        if (index1 >= chain.size() ||
            index2 >= chain.size())
        {
          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
        }
        else
        {
          const ScenePoint2D& p = chain[index1];
          const ScenePoint2D& q = chain[index2];
        
          x1_ = p.GetX();
          y1_ = p.GetY();
          x2_ = q.GetX();
          y2_ = q.GetY();

          const double dx = x2_ - x1_;
          const double dy = y2_ - y1_;
          const double norm = sqrt(dx * dx + dy * dy);

          if (LinearAlgebra::IsCloseToZero(norm))
          {
            isEmpty_ = true;
          }
          else
          {
            isEmpty_ = false;
            const double normalX = -dy / norm;
            const double normalY = dx / norm;

            miterX1_ = normalX;
            miterY1_ = normalY;
            miterX2_ = normalX;
            miterY2_ = normalY;

            Vector a = LinearAlgebra::CreateVector(x1_ + normalX, y1_ + normalY, 1);
            Vector b = LinearAlgebra::CreateVector(x2_ + normalX, y2_ + normalY, 1);
            LinearAlgebra::CrossProduct(lineAbove_, a, b);

            a = LinearAlgebra::CreateVector(x1_ - normalX, y1_ - normalY, 1);
            b = LinearAlgebra::CreateVector(x2_ - normalX, y2_ - normalY, 1);
            LinearAlgebra::CrossProduct(lineBelow_, a, b);
          }
        }
      }

      bool IsEmpty() const
      {
        return isEmpty_;
      }

      static double ComputeSignedArea(double x1,
                                      double y1,
                                      double x2,
                                      double y2,
                                      double x3,
                                      double y3)
      {
        // This computes the signed area of a 2D triangle. This
        // formula is e.g. used in the sorting algorithm of Graham's
        // scan to compute the convex hull.
        // https://en.wikipedia.org/wiki/Graham_scan
        return (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
      }

      static void CreateMiter(Segment& left,
                              Segment& right)
      {
        if (!left.IsEmpty() &&
            !right.IsEmpty())
        {
          Vector above, below;
          LinearAlgebra::CrossProduct(above, left.lineAbove_, right.lineAbove_);
          LinearAlgebra::CrossProduct(below, left.lineBelow_, right.lineBelow_);

          if (!LinearAlgebra::IsCloseToZero(above[2]) &&
              !LinearAlgebra::IsCloseToZero(below[2]))
          {
            // Back to inhomogeneous 2D coordinates
            above /= above[2];
            below /= below[2];

            // Check whether "above" and "below" intersection points
            // are on the half-plane defined by the endpoints of the
            // two segments. This is an indicator of whether the angle
            // is too acute.
            double s1 = ComputeSignedArea(left.x1_, left.y1_,
                                          above[0], above[1],
                                          right.x2_, right.y2_);
            double s2 = ComputeSignedArea(left.x1_, left.y1_,
                                          below[0], below[1],
                                          right.x2_, right.y2_);
            
            // The two signed areas must have the same sign
            if (s1 * s2 >= 0)
            {
              left.miterX2_ = above[0] - left.x2_;
              left.miterY2_ = above[1] - left.y2_;

              right.miterX1_ = left.miterX2_;
              right.miterY1_ = left.miterY2_;
            }
          }
        }
      }

      void AddTriangles(std::vector<float>& coords,
                        std::vector<float>& miterDirections,
                        std::vector<float>& colors,
                        const Color& color)
      {
        if (isEmpty_)
        {
          LOG(ERROR) << "OpenGLLinesProgram -- AddTriangles: (isEmpty_)";
          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
        }

        // First triangle
        coords.push_back(static_cast<float>(x1_));
        coords.push_back(static_cast<float>(y1_));
        coords.push_back(static_cast<float>(1));
        coords.push_back(static_cast<float>(x2_));
        coords.push_back(static_cast<float>(y2_));
        coords.push_back(static_cast<float>(-1));
        coords.push_back(static_cast<float>(x2_));
        coords.push_back(static_cast<float>(y2_));
        coords.push_back(static_cast<float>(1));

        miterDirections.push_back(static_cast<float>(miterX1_));
        miterDirections.push_back(static_cast<float>(miterY1_));
        miterDirections.push_back(static_cast<float>(miterX2_));
        miterDirections.push_back(static_cast<float>(miterY2_));
        miterDirections.push_back(static_cast<float>(miterX2_));
        miterDirections.push_back(static_cast<float>(miterY2_));
        
        // Second triangle
        coords.push_back(static_cast<float>(x1_));
        coords.push_back(static_cast<float>(y1_));
        coords.push_back(static_cast<float>(1));
        coords.push_back(static_cast<float>(x1_));
        coords.push_back(static_cast<float>(y1_));
        coords.push_back(static_cast<float>(-1));
        coords.push_back(static_cast<float>(x2_));
        coords.push_back(static_cast<float>(y2_));
        coords.push_back(static_cast<float>(-1));

        miterDirections.push_back(static_cast<float>(miterX1_));
        miterDirections.push_back(static_cast<float>(miterY1_));
        miterDirections.push_back(static_cast<float>(miterX1_));
        miterDirections.push_back(static_cast<float>(miterY1_));
        miterDirections.push_back(static_cast<float>(miterX2_));
        miterDirections.push_back(static_cast<float>(miterY2_));

        // Add the colors of the 2 triangles (leading to 2 * 3 values)
        for (unsigned int i = 0; i < 6; i++)
        {
          colors.push_back(color.GetRedAsFloat());
          colors.push_back(color.GetGreenAsFloat());
          colors.push_back(color.GetBlueAsFloat());
        }
      }        
    };


    OpenGLLinesProgram::Data::Data(OpenGL::IOpenGLContext& context,
                                   const PolylineSceneLayer& layer) :
      context_(context),
      verticesCount_(0),
      thickness_(static_cast<float>(layer.GetThickness()))
    {
      if (!context_.IsContextLost())
      {
        // High-level reference:
        // https://mattdesl.svbtle.com/drawing-lines-is-hard
        // https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader

        size_t countVertices = 0;
        for (size_t i = 0; i < layer.GetChainsCount(); i++)
        {
          size_t countSegments = layer.GetChain(i).size() - 1;

          if (layer.IsClosedChain(i))
          {
            countSegments++;
          }

          // Each segment is made of 2 triangles. One triangle is
          // defined by 3 points in 2D => 6 vertices per segment.
          countVertices += countSegments * 2 * 3;
        }

        std::vector<float>  coords, colors, miterDirections;
        coords.reserve(countVertices * COMPONENTS_POSITION);
        colors.reserve(countVertices * COMPONENTS_COLOR);
        miterDirections.reserve(countVertices * COMPONENTS_MITER);

        for (size_t i = 0; i < layer.GetChainsCount(); i++)
        {
          const PolylineSceneLayer::Chain& chain = layer.GetChain(i);

          if (chain.size() > 1)
          {
            std::vector<Segment> segments;
            for (size_t j = 1; j < chain.size(); j++)
            {
              segments.push_back(Segment(chain, j - 1, j));
            }

            if (layer.IsClosedChain(i))
            {
              segments.push_back(Segment(chain, chain.size() - 1, 0));
            }

            // Try and create nice miters
            for (size_t j = 1; j < segments.size(); j++)
            {
              Segment::CreateMiter(segments[j - 1], segments[j]);
            }

            if (layer.IsClosedChain(i))
            {
              Segment::CreateMiter(segments.back(), segments.front());
            }

            for (size_t j = 0; j < segments.size(); j++)
            {
              if (!segments[j].IsEmpty())
              {
                segments[j].AddTriangles(coords, miterDirections, colors, layer.GetColor(i));
              }
            }
          }
        }

        assert(coords.size() == colors.size());

        if (!coords.empty())
        {
          verticesCount_ = coords.size() / COMPONENTS_POSITION;

          context_.MakeCurrent();
          glGenBuffers(3, buffers_);

          glBindBuffer(GL_ARRAY_BUFFER, buffers_[0]);
          glBufferData(GL_ARRAY_BUFFER, sizeof(float) * coords.size(), &coords[0], GL_STATIC_DRAW);

          glBindBuffer(GL_ARRAY_BUFFER, buffers_[1]);
          glBufferData(GL_ARRAY_BUFFER, sizeof(float) * miterDirections.size(), &miterDirections[0], GL_STATIC_DRAW);

          glBindBuffer(GL_ARRAY_BUFFER, buffers_[2]);
          glBufferData(GL_ARRAY_BUFFER, sizeof(float) * colors.size(), &colors[0], GL_STATIC_DRAW);
        }
      }
    }

    
    OpenGLLinesProgram::Data::~Data()
    {
      if (!context_.IsContextLost() && !IsEmpty())
      {
        context_.MakeCurrent();
        ORTHANC_OPENGL_TRACE_CURRENT_CONTEXT("About to call glDeleteBuffers");
        glDeleteBuffers(3, buffers_);
      }
    }

    GLuint OpenGLLinesProgram::Data::GetVerticesBuffer() const
    {
      if (IsEmpty())
      {
        LOG(ERROR) << "OpenGLLinesProgram::Data::GetVerticesBuffer(): (IsEmpty())";
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }
      else
      {
        return buffers_[0];
      }
    }

    
    GLuint OpenGLLinesProgram::Data::GetMiterDirectionsBuffer() const
    {
      if (IsEmpty())
      {
        LOG(ERROR) << "OpenGLLinesProgram::Data::GetMiterDirectionsBuffer(): (IsEmpty())";
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }
      else
      {
        return buffers_[1];
      }
    }


    GLuint OpenGLLinesProgram::Data::GetColorsBuffer() const
    {
      if (IsEmpty())
      {
        LOG(ERROR) << "OpenGLLinesProgram::Data::GetColorsBuffer(): (IsEmpty())";
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }
      else
      {
        return buffers_[2];
      }
    }


    OpenGLLinesProgram::OpenGLLinesProgram(OpenGL::IOpenGLContext&  context) :
      context_(context)
    {
      if (!context_.IsContextLost())
      {
        context_.MakeCurrent();
        program_.reset(new OpenGL::OpenGLProgram(context_));
        program_->CompileShaders(VERTEX_SHADER, FRAGMENT_SHADER);
      }
    }

    void OpenGLLinesProgram::Apply(const Data& data,
                                   const AffineTransform2D& transform,
                                   unsigned int canvasWidth,
                                   unsigned int canvasHeight,
                                   bool antialiasing,
                                   bool scaleIndependantThickness)
    {
      if (!context_.IsContextLost() && !data.IsEmpty())
      {
        context_.MakeCurrent();
        program_->Use();

        GLint locationPosition = program_->GetAttributeLocation("a_position");
        GLint locationMiterDirection = program_->GetAttributeLocation("a_miter_direction");
        GLint locationColor = program_->GetAttributeLocation("a_color");

        float m[16];
        transform.ConvertToOpenGLMatrix(m, canvasWidth, canvasHeight);

        glUniformMatrix4fv(program_->GetUniformLocation("u_matrix"), 1, GL_FALSE, m);

        glBindBuffer(GL_ARRAY_BUFFER, data.GetVerticesBuffer());
        glEnableVertexAttribArray(locationPosition);
        glVertexAttribPointer(locationPosition, COMPONENTS_POSITION, GL_FLOAT, GL_FALSE, 0, 0);

        glBindBuffer(GL_ARRAY_BUFFER, data.GetMiterDirectionsBuffer());
        glEnableVertexAttribArray(locationMiterDirection);
        glVertexAttribPointer(locationMiterDirection, COMPONENTS_MITER, GL_FLOAT, GL_FALSE, 0, 0);

        glBindBuffer(GL_ARRAY_BUFFER, data.GetColorsBuffer());
        glEnableVertexAttribArray(locationColor);
        glVertexAttribPointer(locationColor, COMPONENTS_COLOR, GL_FLOAT, GL_FALSE, 0, 0);

        glUniform1i(program_->GetUniformLocation("u_antialiasing"), (antialiasing ? 1 : 0));

        const double zoom = transform.ComputeZoom();
        const double thickness = data.GetThickness() / 2.0;
        const double aliasingBorder = 2.0;  // Border for antialiasing ramp, in pixels
        assert(aliasingBorder > 0);  // Prevent division by zero with "t1"
              
        if (scaleIndependantThickness)
        {
          if (antialiasing)
          {
            double t1 = std::max(thickness, aliasingBorder);
            double t0 = std::max(0.0, thickness - aliasingBorder);
            
            glUniform1f(program_->GetUniformLocation("u_thickness"), 
              static_cast<GLfloat>(t1 / zoom));
            glUniform1f(program_->GetUniformLocation("u_antialiasing_start"), 
              static_cast<GLfloat>(t0 / t1));
          }
          else
          {
            glUniform1f(program_->GetUniformLocation("u_thickness"), 
              static_cast<GLfloat>(thickness / zoom));
          }
        }
        else
        {
          if (antialiasing)
          {
            double t1 = std::max(thickness, aliasingBorder / zoom);
            double t0 = std::max(0.0, thickness - aliasingBorder / zoom);

            glUniform1f(program_->GetUniformLocation("u_thickness"), 
              static_cast<GLfloat>(t1));
            glUniform1f(program_->GetUniformLocation("u_antialiasing_start"), 
              static_cast<GLfloat>(t0 / t1));
          }
          else
          {
            glUniform1f(program_->GetUniformLocation("u_thickness"), 
              static_cast<GLfloat>(thickness));
          }
        }

        if (antialiasing)
        {
          glEnable(GL_BLEND);
          glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
          glDrawArrays(GL_TRIANGLES, 0, 
            static_cast<GLsizei>(data.GetVerticesCount()));
          glDisable(GL_BLEND);
        }
        else
        {
          glDrawArrays(GL_TRIANGLES, 0, 
            static_cast<GLsizei>(data.GetVerticesCount()));
        }

        glDisableVertexAttribArray(locationPosition);
        glDisableVertexAttribArray(locationMiterDirection);
        glDisableVertexAttribArray(locationColor);
      }
    }
  }
}