changeset 592:bbe29efd3d1c

OpenGLLinesProgram
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 26 Apr 2019 12:55:43 +0200
parents b66ced2c43d4
children 6bf8f881fcb5
files Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp Framework/Scene2D/Internals/OpenGLFloatTextureProgram.h Framework/Scene2D/Internals/OpenGLLinesProgram.cpp Framework/Scene2D/Internals/OpenGLLinesProgram.h Framework/Scene2D/Internals/OpenGLTextProgram.cpp Framework/Scene2D/Internals/OpenGLTextProgram.h Resources/CMake/OrthancStoneConfiguration.cmake
diffstat 7 files changed, 902 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp	Fri Apr 26 12:05:38 2019 +0200
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp	Fri Apr 26 12:55:43 2019 +0200
@@ -56,12 +56,6 @@
 {
   namespace Internals
   {
-    OpenGLFloatTextureProgram::OpenGLFloatTextureProgram(OpenGL::IOpenGLContext&  context) :
-      program_(context, FRAGMENT_SHADER)
-    {
-    }
-
-
     OpenGLFloatTextureProgram::Data::Data(const Orthanc::ImageAccessor& texture,
                                           bool isLinearInterpolation)
     {
@@ -128,6 +122,12 @@
     }
 
     
+    OpenGLFloatTextureProgram::OpenGLFloatTextureProgram(OpenGL::IOpenGLContext&  context) :
+      program_(context, FRAGMENT_SHADER)
+    {
+    }
+
+
     void OpenGLFloatTextureProgram::Apply(Data& data,
                                           const AffineTransform2D& transform,
                                           float windowCenter,
--- a/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.h	Fri Apr 26 12:05:38 2019 +0200
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.h	Fri Apr 26 12:55:43 2019 +0200
@@ -29,12 +29,7 @@
   {
     class OpenGLFloatTextureProgram : public boost::noncopyable
     {
-    private:
-      OpenGLTextureProgram  program_;
-
     public:
-      OpenGLFloatTextureProgram(OpenGL::IOpenGLContext&  context);
-
       class Data : public boost::noncopyable
       {
       private:
@@ -62,6 +57,12 @@
         }
       };
 
+    private:
+      OpenGLTextureProgram  program_;
+
+    public:
+      OpenGLFloatTextureProgram(OpenGL::IOpenGLContext&  context);
+
       void Apply(Data& data,
                  const AffineTransform2D& transform,
                  float windowCenter,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp	Fri Apr 26 12:55:43 2019 +0200
@@ -0,0 +1,459 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLLinesProgram.h"
+
+#include <Core/OrthancException.h>
+
+
+static const unsigned int COMPONENTS_POSITION = 3;
+static const unsigned int COMPONENTS_MITER = 2;
+
+
+static const char* VERTEX_SHADER = 
+  "attribute vec2 a_miter_direction; \n"
+  "attribute vec4 a_position;        \n"
+  "uniform float u_thickness;        \n"
+  "uniform mat4 u_matrix;            \n"
+  "varying float v_distance;         \n"
+  "void main()                       \n"
+  "{                                 \n"
+  "  v_distance = a_position.z;      \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 = 
+  "uniform bool u_antialiasing;           \n"
+  "uniform float u_antialiasing_start;    \n"
+  "uniform vec3 u_color;                  \n"
+  "varying float v_distance;              \n"   // Distance of the point to the segment
+  "void main()                            \n"
+  "{                                      \n"
+  "  float d = abs(v_distance);           \n"
+  "  if (!u_antialiasing ||               \n"
+  "      d <= u_antialiasing_start)       \n"
+  "    gl_FragColor = vec4(u_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(u_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)
+      {
+        if (isEmpty_)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+
+        // First triangle
+        coords.push_back(x1_);
+        coords.push_back(y1_);
+        coords.push_back(1);
+        coords.push_back(x2_);
+        coords.push_back(y2_);
+        coords.push_back(-1);
+        coords.push_back(x2_);
+        coords.push_back(y2_);
+        coords.push_back(1);
+
+        miterDirections.push_back(miterX1_);
+        miterDirections.push_back(miterY1_);
+        miterDirections.push_back(miterX2_);
+        miterDirections.push_back(miterY2_);
+        miterDirections.push_back(miterX2_);
+        miterDirections.push_back(miterY2_);
+        
+        // Second triangle
+        coords.push_back(x1_);
+        coords.push_back(y1_);
+        coords.push_back(1);
+        coords.push_back(x1_);
+        coords.push_back(y1_);
+        coords.push_back(-1);
+        coords.push_back(x2_);
+        coords.push_back(y2_);
+        coords.push_back(-1);
+
+        miterDirections.push_back(miterX1_);
+        miterDirections.push_back(miterY1_);
+        miterDirections.push_back(miterX1_);
+        miterDirections.push_back(miterY1_);
+        miterDirections.push_back(miterX2_);
+        miterDirections.push_back(miterY2_);
+      }        
+    };
+
+
+    OpenGLLinesProgram::Data::Data(OpenGL::IOpenGLContext& context,
+                                   const PolylineSceneLayer& layer) :
+      context_(context),
+      verticesCount_(0),
+      thickness_(layer.GetThickness()),
+      red_(layer.GetRedAsFloat()),
+      green_(layer.GetGreenAsFloat()),
+      blue_(layer.GetBlueAsFloat())
+    {
+      // 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, miterDirections;
+      coords.reserve(countVertices * COMPONENTS_POSITION);
+      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);
+            }
+          }
+        }
+      }
+
+      if (!coords.empty())
+      {
+        verticesCount_ = coords.size() / COMPONENTS_POSITION;
+
+        context_.MakeCurrent();
+        glGenBuffers(2, 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);
+      }
+    }
+
+    
+    OpenGLLinesProgram::Data::~Data()
+    {
+      if (!IsEmpty())
+      {
+        context_.MakeCurrent();
+        glDeleteBuffers(2, buffers_);
+      }
+    }
+
+
+    GLuint OpenGLLinesProgram::Data::GetVerticesBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[0];
+      }
+    }
+
+    
+    GLuint OpenGLLinesProgram::Data::GetMiterDirectionsBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[1];
+      }
+    }
+
+
+    OpenGLLinesProgram::OpenGLLinesProgram(OpenGL::IOpenGLContext&  context) :
+      context_(context)
+    {
+
+      context_.MakeCurrent();
+
+      program_.reset(new OpenGL::OpenGLProgram);
+      program_->CompileShaders(VERTEX_SHADER, FRAGMENT_SHADER);
+    }
+
+
+    void OpenGLLinesProgram::Apply(const Data& data,
+                                   const AffineTransform2D& transform,
+                                   bool antialiasing,
+                                   bool scaleIndependantThickness)
+    {
+      if (!data.IsEmpty())
+      {
+        context_.MakeCurrent();
+        program_->Use();
+
+        GLint locationPosition = program_->GetAttributeLocation("a_position");
+        GLint locationMiterDirection = program_->GetAttributeLocation("a_miter_direction");
+
+        float m[16];
+        transform.ConvertToOpenGLMatrix(m, context_.GetCanvasWidth(), context_.GetCanvasHeight());
+
+        glUniformMatrix4fv(program_->GetUniformLocation("u_matrix"), 1, GL_FALSE, m);
+        glUniform3f(program_->GetUniformLocation("u_color"), 
+                    data.GetRed(), data.GetGreen(), data.GetBlue());
+
+        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);
+
+        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"), t1 / zoom);
+            glUniform1f(program_->GetUniformLocation("u_antialiasing_start"), t0 / t1);
+          }
+          else
+          {
+            glUniform1f(program_->GetUniformLocation("u_thickness"), 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"), t1);
+            glUniform1f(program_->GetUniformLocation("u_antialiasing_start"), t0 / t1);
+          }
+          else
+          {
+            glUniform1f(program_->GetUniformLocation("u_thickness"), thickness);
+          }
+        }
+
+        if (antialiasing)
+        {
+          glEnable(GL_BLEND);
+          glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+          glDrawArrays(GL_TRIANGLES, 0, data.GetVerticesCount());
+          glDisable(GL_BLEND);
+        }
+        else
+        {
+          glDrawArrays(GL_TRIANGLES, 0, data.GetVerticesCount());
+        }
+
+        glDisableVertexAttribArray(locationPosition);
+        glDisableVertexAttribArray(locationMiterDirection);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLLinesProgram.h	Fri Apr 26 12:55:43 2019 +0200
@@ -0,0 +1,103 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../OpenGL/IOpenGLContext.h"
+#include "../../OpenGL/OpenGLProgram.h"
+#include "../../Toolbox/AffineTransform2D.h"
+#include "../PolylineSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLLinesProgram : public boost::noncopyable
+    {
+    public:
+      class Data : public boost::noncopyable
+      {
+      private:
+        class Segment;
+        
+        OpenGL::IOpenGLContext&  context_;
+        GLuint                   buffers_[2];
+        size_t                   verticesCount_;
+        float                    thickness_;
+        float                    red_;
+        float                    green_;
+        float                    blue_;
+
+      public:
+        Data(OpenGL::IOpenGLContext& context,
+             const PolylineSceneLayer& layer);
+        
+        ~Data();
+
+        bool IsEmpty() const
+        {
+          return verticesCount_ == 0;
+        }
+
+        const size_t GetVerticesCount() const
+        {
+          return verticesCount_;
+        }
+
+        GLuint GetVerticesBuffer() const;
+
+        GLuint GetMiterDirectionsBuffer() const;
+
+        float GetThickness() const
+        {
+          return thickness_;
+        }
+
+        float GetRed() const
+        {
+          return red_;
+        }
+
+        float GetGreen() const
+        {
+          return green_;
+        }
+
+        float GetBlue() const
+        {
+          return blue_;
+        }
+      };
+      
+    private:
+      OpenGL::IOpenGLContext&               context_;
+      std::auto_ptr<OpenGL::OpenGLProgram>  program_;
+
+    public:
+      OpenGLLinesProgram(OpenGL::IOpenGLContext&  context);
+
+      void Apply(const Data& data,
+                 const AffineTransform2D& transform,
+                 bool antialiasing,
+                 bool scaleIndependantThickness);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextProgram.cpp	Fri Apr 26 12:55:43 2019 +0200
@@ -0,0 +1,190 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OpenGLTextProgram.h"
+
+#include "../../Fonts/OpenGLTextCoordinates.h"
+
+#include <Core/OrthancException.h>
+
+
+static const unsigned int COMPONENTS = 2;
+
+static const char* VERTEX_SHADER = 
+  "attribute vec2 a_texcoord;             \n"
+  "attribute vec4 a_position;             \n"
+  "uniform mat4 u_matrix;                 \n"
+  "varying vec2 v_texcoord;               \n"
+  "void main()                            \n"
+  "{                                      \n"
+  "  gl_Position = u_matrix * a_position; \n"
+  "  v_texcoord = a_texcoord;             \n"
+  "}";
+
+static const char* FRAGMENT_SHADER = 
+  "uniform sampler2D u_texture;                  \n"
+  "uniform vec3 u_color;                         \n"
+  "varying vec2 v_texcoord;                      \n"
+  "void main()                                   \n"
+  "{                                             \n"
+  "  vec4 v = texture2D(u_texture, v_texcoord);  \n"
+  "  gl_FragColor = vec4(u_color * v.w, v.w);    \n"   // Premultiplied alpha
+  "}";
+
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    OpenGLTextProgram::OpenGLTextProgram(OpenGL::IOpenGLContext&  context) :
+      context_(context)
+    {
+
+      context_.MakeCurrent();
+
+      program_.reset(new OpenGL::OpenGLProgram);
+      program_->CompileShaders(VERTEX_SHADER, FRAGMENT_SHADER);
+
+      positionLocation_ = program_->GetAttributeLocation("a_position");
+      textureLocation_ = program_->GetAttributeLocation("a_texcoord");
+    }
+
+
+    OpenGLTextProgram::Data::Data(OpenGL::IOpenGLContext& context,
+                                  const GlyphTextureAlphabet& alphabet,
+                                  const TextSceneLayer& layer) :
+      context_(context),
+      red_(layer.GetRedAsFloat()),
+      green_(layer.GetGreenAsFloat()),
+      blue_(layer.GetBlueAsFloat()),
+      x_(layer.GetX()),
+      y_(layer.GetY()),
+      border_(layer.GetBorder()),
+      anchor_(layer.GetAnchor())
+    {
+      OpenGL::OpenGLTextCoordinates coordinates(alphabet, layer.GetText());
+      textWidth_ = coordinates.GetTextWidth();
+      textHeight_ = coordinates.GetTextHeight();
+
+      if (coordinates.IsEmpty())
+      {
+        coordinatesCount_ = 0;
+      }
+      else
+      {
+        coordinatesCount_ = coordinates.GetRenderingCoords().size();
+
+        context_.MakeCurrent();
+        glGenBuffers(2, buffers_);
+
+        glBindBuffer(GL_ARRAY_BUFFER, buffers_[0]);
+        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * coordinatesCount_,
+                     &coordinates.GetRenderingCoords() [0], GL_STATIC_DRAW);
+
+        glBindBuffer(GL_ARRAY_BUFFER, buffers_[1]);
+        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * coordinatesCount_,
+                     &coordinates.GetTextureCoords() [0], GL_STATIC_DRAW);
+      }
+    }
+
+    
+    OpenGLTextProgram::Data::~Data()
+    {
+      if (!IsEmpty())
+      {
+        context_.MakeCurrent();
+        glDeleteBuffers(2, buffers_);
+      }
+    }
+
+
+    GLuint OpenGLTextProgram::Data::GetSceneLocationsBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[0];
+      }
+    }
+
+    
+    GLuint OpenGLTextProgram::Data::GetTextureLocationsBuffer() const
+    {
+      if (IsEmpty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return buffers_[1];
+      }
+    }
+
+
+    void OpenGLTextProgram::Apply(OpenGL::OpenGLTexture& fontTexture,
+                                  const Data& data,
+                                  const AffineTransform2D& transform)
+    {
+      if (!data.IsEmpty())
+      {
+        context_.MakeCurrent();
+        program_->Use();
+
+        double dx, dy;  // In pixels
+        ComputeAnchorTranslation(dx, dy, data.GetAnchor(), 
+                                 data.GetTextWidth(), data.GetTextHeight(), data.GetBorder());
+      
+        double x = data.GetX();
+        double y = data.GetY();
+        transform.Apply(x, y);
+
+        const AffineTransform2D t = AffineTransform2D::CreateOffset(x + dx, y + dy);
+
+        float m[16];
+        t.ConvertToOpenGLMatrix(m, context_.GetCanvasWidth(), context_.GetCanvasHeight());
+
+        fontTexture.Bind(program_->GetUniformLocation("u_texture"));
+        glUniformMatrix4fv(program_->GetUniformLocation("u_matrix"), 1, GL_FALSE, m);
+        glUniform3f(program_->GetUniformLocation("u_color"), 
+                    data.GetRed(), data.GetGreen(), data.GetBlue());
+
+        glBindBuffer(GL_ARRAY_BUFFER, data.GetSceneLocationsBuffer());
+        glEnableVertexAttribArray(positionLocation_);
+        glVertexAttribPointer(positionLocation_, COMPONENTS, GL_FLOAT, GL_FALSE, 0, 0);
+
+        glBindBuffer(GL_ARRAY_BUFFER, data.GetTextureLocationsBuffer());
+        glEnableVertexAttribArray(textureLocation_);
+        glVertexAttribPointer(textureLocation_, COMPONENTS, GL_FLOAT, GL_FALSE, 0, 0);
+
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+        glDrawArrays(GL_TRIANGLES, 0, data.GetCoordinatesCount() / COMPONENTS);
+        glDisable(GL_BLEND);
+
+        glDisableVertexAttribArray(positionLocation_);
+        glDisableVertexAttribArray(textureLocation_);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/Internals/OpenGLTextProgram.h	Fri Apr 26 12:55:43 2019 +0200
@@ -0,0 +1,135 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Fonts/GlyphTextureAlphabet.h"
+#include "../../OpenGL/IOpenGLContext.h"
+#include "../../OpenGL/OpenGLProgram.h"
+#include "../../OpenGL/OpenGLTexture.h"
+#include "../../Toolbox/AffineTransform2D.h"
+#include "../TextSceneLayer.h"
+
+namespace OrthancStone
+{
+  namespace Internals
+  {
+    class OpenGLTextProgram : public boost::noncopyable
+    {
+    public:
+      class Data : public boost::noncopyable
+      {
+      private:
+        OpenGL::IOpenGLContext&  context_;
+        size_t                   coordinatesCount_;
+        GLuint                   buffers_[2];
+        float                    red_;
+        float                    green_;
+        float                    blue_;
+        double                   x_;
+        double                   y_;
+        double                   border_;
+        unsigned int             textWidth_;
+        unsigned int             textHeight_;
+        BitmapAnchor             anchor_;
+
+      public:
+        Data(OpenGL::IOpenGLContext& context,
+             const GlyphTextureAlphabet& alphabet,
+             const TextSceneLayer& layer);
+
+        ~Data();
+
+        bool IsEmpty() const
+        {
+          return coordinatesCount_ == 0;
+        }
+
+        size_t GetCoordinatesCount() const
+        {
+          return coordinatesCount_;
+        }
+
+        GLuint GetSceneLocationsBuffer() const;
+
+        GLuint GetTextureLocationsBuffer() const;
+
+        float GetRed() const
+        {
+          return red_;
+        }
+
+        float GetGreen() const
+        {
+          return green_;
+        }
+
+        float GetBlue() const
+        {
+          return blue_;
+        }
+
+        double GetX() const
+        {
+          return x_;
+        }
+
+        double GetY() const
+        {
+          return y_;
+        }
+
+        double GetBorder() const
+        {
+          return border_;
+        }
+
+        unsigned int GetTextWidth() const
+        {
+          return textWidth_;
+        }
+
+        unsigned int GetTextHeight() const
+        {
+          return textHeight_;
+        }
+
+        BitmapAnchor GetAnchor() const
+        {
+          return anchor_;
+        }
+      };
+      
+    private:
+      OpenGL::IOpenGLContext&               context_;
+      std::auto_ptr<OpenGL::OpenGLProgram>  program_;
+      GLint                                 positionLocation_;
+      GLint                                 textureLocation_;
+
+    public:
+      OpenGLTextProgram(OpenGL::IOpenGLContext&  context);
+
+      void Apply(OpenGL::OpenGLTexture& fontTexture,
+                 const Data& data,
+                 const AffineTransform2D& transform);
+    };
+  }
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Apr 26 12:05:38 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Apr 26 12:55:43 2019 +0200
@@ -389,8 +389,10 @@
     ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLProgram.cpp
     ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLShader.cpp
     ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLTexture.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.cpp
     )
 endif()