changeset 2060:86e0e92a2e0d deep-learning

added OpenGLTextureArray and OpenGLFramebuffer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 03 May 2023 16:15:50 +0200
parents f7cbc58ff44d
children 6ea5f40ea0e9
files OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake OrthancStone/Sources/OpenGL/OpenGLFramebuffer.cpp OrthancStone/Sources/OpenGL/OpenGLFramebuffer.h OrthancStone/Sources/OpenGL/OpenGLTexture.cpp OrthancStone/Sources/OpenGL/OpenGLTexture.h OrthancStone/Sources/OpenGL/OpenGLTextureArray.cpp OrthancStone/Sources/OpenGL/OpenGLTextureArray.h OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h
diffstat 8 files changed, 807 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Apr 24 17:10:41 2023 +0200
+++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake	Wed May 03 16:15:50 2023 +0200
@@ -501,6 +501,10 @@
     ${ORTHANC_STONE_ROOT}/OpenGL/OpenGLShader.cpp
     ${ORTHANC_STONE_ROOT}/OpenGL/OpenGLTexture.h
     ${ORTHANC_STONE_ROOT}/OpenGL/OpenGLTexture.cpp
+    ${ORTHANC_STONE_ROOT}/OpenGL/OpenGLTextureArray.h
+    ${ORTHANC_STONE_ROOT}/OpenGL/OpenGLTextureArray.cpp
+    ${ORTHANC_STONE_ROOT}/OpenGL/OpenGLFramebuffer.h
+    ${ORTHANC_STONE_ROOT}/OpenGL/OpenGLFramebuffer.cpp
     ${ORTHANC_STONE_ROOT}/Scene2D/OpenGLCompositor.h
     ${ORTHANC_STONE_ROOT}/Scene2D/OpenGLCompositor.cpp
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/OpenGL/OpenGLFramebuffer.cpp	Wed May 03 16:15:50 2023 +0200
@@ -0,0 +1,261 @@
+/**
+ * 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 "OpenGLFramebuffer.h"
+
+#if defined(__EMSCRIPTEN__)
+#  if !defined(ORTHANC_WEBGL2_HEAP_COMPAT)
+#    error The macro ORTHANC_WEBGL2_HEAP_COMPAT must be defined
+#  endif
+#endif
+
+#include "OpenGLTexture.h"
+#include "OpenGLTextureArray.h"
+
+#include <OrthancException.h>
+
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    void OpenGLFramebuffer::SetupTextureTarget()
+    {
+      GLenum drawBuffers[1] = { GL_COLOR_ATTACHMENT0 };
+      glDrawBuffers(1, drawBuffers);
+
+      if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Incomplete setup of an OpenGL framebuffer");
+      }
+    }
+
+
+    void OpenGLFramebuffer::ReadContent(Orthanc::ImageAccessor& target)
+    {
+      if (target.GetPitch() != target.GetWidth() * Orthanc::GetBytesPerPixel(target.GetFormat()) ||
+          target.GetBuffer() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Image must have minimal pitch");
+      }
+
+      if (glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
+      {
+        ORTHANC_OPENGL_CHECK("glCheckFramebufferStatus()");
+
+        glViewport(0, 0, target.GetWidth(), target.GetHeight());
+
+        GLenum sourceFormat, internalFormat, pixelType;
+        OpenGLTexture::ConvertToOpenGLFormats(sourceFormat, internalFormat, pixelType, target.GetFormat());
+
+#if defined(__EMSCRIPTEN__) && (ORTHANC_WEBGL2_HEAP_COMPAT == 1)
+        // Check out "OpenGLTexture.cpp" for an explanation
+
+        int framebufferFormat, framebufferType;
+        glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &framebufferFormat);
+        glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &framebufferType);
+
+        switch (target.GetFormat())
+        {
+          case Orthanc::PixelFormat_RGBA32:
+            if (sourceFormat != GL_RGBA ||
+                internalFormat != GL_RGBA ||
+                pixelType != GL_UNSIGNED_BYTE ||
+                framebufferFormat != GL_RGBA ||
+                framebufferType != GL_UNSIGNED_BYTE)
+            {
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+            }
+
+            EM_ASM({
+                var ptr = emscriptenWebGLGetTexPixelData(GLctx.UNSIGNED_BYTE, GLctx.RGBA, $1, $2, $0, GLctx.RGBA);
+                GLctx.readPixels(0, 0, $1, $2, GLctx.RGBA, GLctx.UNSIGNED_BYTE, ptr);
+              },
+              target.GetBuffer(),  // $0
+              target.GetWidth(),   // $1
+              target.GetHeight()); // $2
+            break;
+
+          case Orthanc::PixelFormat_Float32:
+            // In Mozilla Firefox, "Float32" is not available as such. We
+            // have to download an RGBA image in Float32.
+            if (sourceFormat != GL_RED ||
+                internalFormat != GL_R32F ||
+                pixelType != GL_FLOAT ||
+                framebufferType != GL_FLOAT)
+            {
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+            }
+
+            switch (framebufferFormat)
+            {
+              case GL_RGBA:
+                // This is Mozilla Firefox
+                EM_ASM({
+                    var tmp = new Float32Array($1 * $2 * 4);
+                    GLctx.readPixels(0, 0, $1, $2, GLctx.RGBA, GLctx.FLOAT, tmp);
+
+                    // From RGBA to RED
+                    var ptr = emscriptenWebGLGetTexPixelData(GLctx.FLOAT, GLctx.RED, $1, $2, $0, GLctx.R32F);
+                    for (var i = 0; i < $1 * $2; i++) {
+                      ptr[i] = tmp[4 * i];
+                    }
+                  },
+                  target.GetBuffer(),  // $0
+                  target.GetWidth(),   // $1
+                  target.GetHeight()); // $2
+                break;
+
+              case GL_RED:
+                // This is Chromium
+                EM_ASM({
+                    var ptr = emscriptenWebGLGetTexPixelData(GLctx.FLOAT, GLctx.RED, $1, $2, $0, GLctx.R32F);
+                    GLctx.readPixels(0, 0, $1, $2, GLctx.RED, GLctx.FLOAT, ptr);
+                  },
+                  target.GetBuffer(),  // $0
+                  target.GetWidth(),   // $1
+                  target.GetHeight()); // $2
+                break;
+
+              default:
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+            }
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+#else
+        glReadPixels(0, 0, target.GetWidth(), target.GetHeight(), sourceFormat, pixelType, target.GetBuffer());
+#endif
+
+        ORTHANC_OPENGL_CHECK("glReadPixels()");
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Incomplete setup of an OpenGL framebuffer");
+      }
+    }
+
+
+    OpenGLFramebuffer::OpenGLFramebuffer(IOpenGLContext& context) :
+      context_(context),
+      framebuffer_(0)
+    {
+      if (context.IsContextLost())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "OpenGL context has been lost");
+      }
+
+      glGenFramebuffers(1, &framebuffer_);
+      if (framebuffer_ == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Cannot create an OpenGL framebuffer");
+      }
+
+      glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
+    }
+
+
+    OpenGLFramebuffer::~OpenGLFramebuffer()
+    {
+      glDeleteFramebuffers(1, &framebuffer_);
+    }
+
+
+    void OpenGLFramebuffer::SetTarget(OpenGLTexture& target)
+    {
+      glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.GetId(), 0);
+      SetupTextureTarget();
+      glViewport(0, 0, target.GetWidth(), target.GetHeight());
+    }
+
+
+    void OpenGLFramebuffer::SetTarget(OpenGLTextureArray& target,
+                                      unsigned int layer)
+    {
+      if (layer >= target.GetDepth())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        glFramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target.GetId(), 0, layer);
+        SetupTextureTarget();
+        glViewport(0, 0, target.GetWidth(), target.GetHeight());
+      }
+    }
+
+
+    void OpenGLFramebuffer::ReadTexture(Orthanc::ImageAccessor& target,
+                                        const OpenGLTexture& source)
+    {
+      if (target.GetWidth() != source.GetWidth() ||
+          target.GetHeight() != source.GetHeight())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+      }
+      else if (target.GetFormat() != source.GetFormat())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+      else
+      {
+        glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source.GetId(), 0);
+        ORTHANC_OPENGL_CHECK("glFramebufferTexture2D()");
+        ReadContent(target);
+      }
+    }
+
+
+    void OpenGLFramebuffer::ReadTexture(Orthanc::ImageAccessor& target,
+                                        const OpenGLTextureArray& source,
+                                        unsigned int layer)
+    {
+      if (target.GetWidth() != source.GetWidth() ||
+          target.GetHeight() != source.GetHeight())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+      }
+      else if (target.GetFormat() != source.GetFormat())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+      else if (layer >= source.GetDepth())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, source.GetId(), 0, layer);
+        ORTHANC_OPENGL_CHECK("glFramebufferTextureLayer()");
+        ReadContent(target);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/OpenGL/OpenGLFramebuffer.h	Wed May 03 16:15:50 2023 +0200
@@ -0,0 +1,66 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLIncludes.h"
+#include "IOpenGLContext.h"
+
+#include <Images/ImageAccessor.h>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class OpenGLTexture;
+    class OpenGLTextureArray;
+
+    class OpenGLFramebuffer : public boost::noncopyable
+    {
+    private:
+      IOpenGLContext&  context_;
+      GLuint           framebuffer_;
+
+      void SetupTextureTarget();
+
+      void ReadContent(Orthanc::ImageAccessor& target);
+
+    public:
+      OpenGLFramebuffer(IOpenGLContext& context);
+
+      ~OpenGLFramebuffer();
+
+      void SetTarget(OpenGLTexture& target);
+
+      void SetTarget(OpenGLTextureArray& target,
+                     unsigned int layer);
+
+      void ReadTexture(Orthanc::ImageAccessor& target,
+                       const OpenGLTexture& source);
+
+      void ReadTexture(Orthanc::ImageAccessor& target,
+                       const OpenGLTextureArray& source,
+                       unsigned int layer);
+    };
+  }
+}
--- a/OrthancStone/Sources/OpenGL/OpenGLTexture.cpp	Mon Apr 24 17:10:41 2023 +0200
+++ b/OrthancStone/Sources/OpenGL/OpenGLTexture.cpp	Wed May 03 16:15:50 2023 +0200
@@ -22,11 +22,6 @@
 
 
 #include "OpenGLTexture.h"
-#include "IOpenGLContext.h"
-
-#include <Images/Image.h>
-#include <Logging.h>
-#include <OrthancException.h>
 
 #if defined(__EMSCRIPTEN__)
 #  if !defined(ORTHANC_WEBGL2_HEAP_COMPAT)
@@ -34,16 +29,20 @@
 #  endif
 #endif
 
+#include <Images/Image.h>
+#include <Logging.h>
+#include <OrthancException.h>
+
 namespace OrthancStone
 {
   namespace OpenGL
   {
     OpenGLTexture::OpenGLTexture(OpenGL::IOpenGLContext& context) :
+      context_(context),
       texture_(0),
       width_(0),
       height_(0),
       format_(Orthanc::PixelFormat_Grayscale8),
-      context_(context),
       isLinearInterpolation_(false)
     {
       if (!context_.IsContextLost())
@@ -275,25 +274,26 @@
 
     void OpenGLTexture::SetClampingToZero()
     {
-      ORTHANC_OPENGL_CHECK("Entering OpenGLTexture::SetClampingToZero()");
-      
 #if defined(__EMSCRIPTEN__)
       /**
        * This is because WebGL 2 derives from OpenGL ES 3.0, which
        * doesn't support GL_CLAMP_TO_BORDER, as can be seen here:
        * https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glTexParameter.xhtml
        **/
-      LOG(WARNING) << "OpenGLTexture::SetClampingToZero() is not available in WebGL 2";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                      "OpenGLTextureArray::SetClampingToZero() is not available in WebGL 2");
 #else
+      ORTHANC_OPENGL_CHECK("Entering OpenGLTexture::SetClampingToZero()");
+
       glBindTexture(GL_TEXTURE_2D, texture_);
       glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
       glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
 
       GLfloat colorfv[4] = { 0, 0, 0, 0 };
       glTextureParameterfv(texture_, GL_TEXTURE_BORDER_COLOR, colorfv);
+
+      ORTHANC_OPENGL_CHECK("Exiting OpenGLTexture::SetClampingToZero()");
 #endif
-      
-      ORTHANC_OPENGL_CHECK("Exiting OpenGLTexture::SetClampingToZero()");
     }
 
 
--- a/OrthancStone/Sources/OpenGL/OpenGLTexture.h	Mon Apr 24 17:10:41 2023 +0200
+++ b/OrthancStone/Sources/OpenGL/OpenGLTexture.h	Wed May 03 16:15:50 2023 +0200
@@ -37,13 +37,14 @@
   {
     class OpenGLTexture : public boost::noncopyable
     {
+      friend class OpenGLFramebuffer;
+
     private:
-      GLuint        texture_;
-      unsigned int  width_;
-      unsigned int  height_;
-
+      OpenGL::IOpenGLContext& context_;
+      GLuint                  texture_;
+      unsigned int            width_;
+      unsigned int            height_;
       Orthanc::PixelFormat    format_;
-      OpenGL::IOpenGLContext& context_;
       bool                    isLinearInterpolation_;
 
       void Setup(Orthanc::PixelFormat format,
@@ -52,11 +53,6 @@
                  bool isLinearInterpolation,
                  const void* data);
 
-    public:
-      explicit OpenGLTexture(OpenGL::IOpenGLContext& context);
-
-      ~OpenGLTexture();
-
       /**
        * Returns the low-level OpenGL handle of the texture. Beware to
        * never change the size of the texture using this handle!
@@ -66,6 +62,11 @@
         return texture_;
       }
 
+    public:
+      explicit OpenGLTexture(OpenGL::IOpenGLContext& context);
+
+      ~OpenGLTexture();
+
       Orthanc::PixelFormat GetFormat() const
       {
         return format_;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/OpenGL/OpenGLTextureArray.cpp	Wed May 03 16:15:50 2023 +0200
@@ -0,0 +1,308 @@
+/**
+ * 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 "OpenGLTextureArray.h"
+
+#if defined(__EMSCRIPTEN__)
+#  if !defined(ORTHANC_WEBGL2_HEAP_COMPAT)
+#    error The macro ORTHANC_WEBGL2_HEAP_COMPAT must be defined
+#  endif
+#endif
+
+#include "OpenGLFramebuffer.h"
+#include "OpenGLTexture.h"
+
+#include <Images/Image.h>
+#include <OrthancException.h>
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    OpenGLTextureArray::OpenGLTextureArray(IOpenGLContext& context) :
+      context_(context),
+      texture_(0),
+      width_(0),
+      height_(0),
+      depth_(0),
+      format_(Orthanc::PixelFormat_Float32),
+      isLinearInterpolation_(false)
+    {
+      if (context.IsContextLost())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "OpenGL context has been lost");
+      }
+
+      glGenTextures(1, &texture_);
+      ORTHANC_OPENGL_CHECK("glGenTextures()");
+
+      if (texture_ == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Cannot create an OpenGL texture array");
+      }
+    }
+
+
+    OpenGLTextureArray::~OpenGLTextureArray()
+    {
+      assert(texture_ != 0);
+      glDeleteTextures(1, &texture_);
+    }
+
+
+    void OpenGLTextureArray::Setup(Orthanc::PixelFormat format,
+                                   unsigned int width,
+                                   unsigned int height,
+                                   unsigned int depth,
+                                   bool isLinearInterpolation)
+    {
+      glActiveTexture(GL_TEXTURE0);
+      glBindTexture(GL_TEXTURE_2D_ARRAY, texture_);
+      ORTHANC_OPENGL_CHECK("glBindTexture(GL_TEXTURE_2D_ARRAY)");
+
+      GLenum sourceFormat, internalFormat, pixelType;
+      OpenGLTexture::ConvertToOpenGLFormats(sourceFormat, internalFormat, pixelType, format);
+
+      glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, internalFormat, width, height, depth,
+                   0, sourceFormat, pixelType, NULL);
+      ORTHANC_OPENGL_CHECK("glTexImage3D()");
+
+#if !defined(__EMSCRIPTEN__)
+      /**
+       * glGetTexLevelParameteriv() was introduced in OpenGL ES 3.1,
+       * but WebGL 2 only supports OpenGL ES 3.0, so it is not
+       * available in WebAssembly:
+       * https://registry.khronos.org/OpenGL-Refpages/es3.1/html/glGetTexLevelParameter.xhtml
+       **/
+      GLint w, h, d;
+      glGetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_WIDTH, &w);
+      glGetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_HEIGHT, &h);
+      glGetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_DEPTH, &d);
+      if (width != static_cast<unsigned int>(w) ||
+          height != static_cast<unsigned int>(h) ||
+          depth != static_cast<unsigned int>(d))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                        "Your GPU cannot create an array of textures of size " +
+                                        boost::lexical_cast<std::string>(width) + " x " +
+                                        boost::lexical_cast<std::string>(height) + " x " +
+                                        boost::lexical_cast<std::string>(depth));
+      }
+#endif
+
+      format_ = format;
+      width_ = width;
+      height_ = height;
+      depth_ = depth;
+      isLinearInterpolation_ = isLinearInterpolation;
+
+      GLint interpolation = (isLinearInterpolation ? GL_LINEAR : GL_NEAREST);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, interpolation);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, interpolation);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    }
+
+
+    void OpenGLTextureArray::SetClampingToZero()
+    {
+#if defined(__EMSCRIPTEN__)
+      /**
+       * This is because WebGL 2 derives from OpenGL ES 3.0, which
+       * doesn't support GL_CLAMP_TO_BORDER, as can be seen here:
+       * https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glTexParameter.xhtml
+       **/
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                      "OpenGLTextureArray::SetClampingToZero() is not available in WebGL 2");
+#else
+      ORTHANC_OPENGL_CHECK("Entering OpenGLTextureArray::SetClampingToZero()");
+
+      glBindTexture(GL_TEXTURE_2D_ARRAY, texture_);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+
+      GLfloat colorfv[4] = { 0, 0, 0, 0 };
+      glTexParameterfv(texture_, GL_TEXTURE_BORDER_COLOR, colorfv);
+
+      ORTHANC_OPENGL_CHECK("Exiting OpenGLTextureArray::SetClampingToZero()");
+#endif
+    }
+
+
+    void OpenGLTextureArray::Bind(GLint location) const
+    {
+      glActiveTexture(GL_TEXTURE0);
+      glBindTexture(GL_TEXTURE_2D_ARRAY, texture_);
+      glUniform1i(location, 0 /* texture unit */);
+    }
+
+
+    void OpenGLTextureArray::BindAsTextureUnit(GLint location,
+                                               unsigned int unit) const
+    {
+      if (unit >= 32)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+
+      assert(GL_TEXTURE0 + 1 == GL_TEXTURE1 &&
+             GL_TEXTURE0 + 31 == GL_TEXTURE31);
+
+      glActiveTexture(GL_TEXTURE0 + unit);
+      glBindTexture(GL_TEXTURE_2D_ARRAY, texture_);
+      glUniform1i(location, unit /* texture unit */);
+    }
+
+
+    void OpenGLTextureArray::Upload(const Orthanc::ImageAccessor& image,
+                                    unsigned int layer)
+    {
+      if (image.GetWidth() != width_ ||
+          image.GetHeight() != height_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
+      }
+
+      if (layer >= depth_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+
+      if (width_ != 0 &&
+          height_ != 0)
+      {
+        GLenum sourceFormat, internalFormat, pixelType;
+        OpenGLTexture::ConvertToOpenGLFormats(sourceFormat, internalFormat, pixelType, image.GetFormat());
+
+        glBindTexture(GL_TEXTURE_2D_ARRAY, texture_);
+
+#if defined(__EMSCRIPTEN__) && (ORTHANC_WEBGL2_HEAP_COMPAT == 1)
+        // Check out "OpenGLTexture.cpp" for an explanation
+        EM_ASM({
+            var ptr = emscriptenWebGLGetTexPixelData($5, $4, $2, $3, $0, $1);
+            GLctx.texSubImage3D(GLctx.TEXTURE_2D_ARRAY, 0, 0 /* x offset */, 0 /* y offset */,
+                                $6, $2, $3, 1 /* depth */, $4, $5, ptr);
+          },
+          image.GetConstBuffer(),  // $0
+          internalFormat,          // $1
+          image.GetWidth(),        // $2
+          image.GetHeight(),       // $3
+          sourceFormat,            // $4
+          pixelType,               // $5
+          layer);                  // $6
+#else
+        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0 /* x offset */, 0 /* y offset */, layer /* z offset */,
+                        width_, height_, 1 /* depth */, sourceFormat, pixelType, image.GetConstBuffer());
+#endif
+      }
+    }
+
+
+    OpenGLTextureArray::DownloadedVolume::DownloadedVolume(const OpenGLTextureArray& texture) :
+      format_(texture.format_),
+      width_(texture.width_),
+      height_(texture.height_),
+      depth_(texture.depth_)
+    {
+      if (width_ != 0 &&
+          height_ != 0 &&
+          depth_ != 0)
+      {
+        buffer_.resize(Orthanc::GetBytesPerPixel(format_) * width_ * height_ * depth_);
+        assert(buffer_.size() > 0);
+
+#if 1 || defined(__EMSCRIPTEN__)
+        /**
+         * The "glGetTexImage()" function is unavailable in WebGL, it
+         * is necessary to use a framebuffer:
+         * https://stackoverflow.com/a/15064957
+         **/
+        OpenGLFramebuffer framebuffer(texture.context_);
+
+        Orthanc::Image tmp(texture.GetFormat(), texture.GetWidth(), texture.GetHeight(), true);
+
+        for (unsigned int layer = 0; layer < depth_; layer++)
+        {
+          framebuffer.ReadTexture(tmp, texture, layer);
+          memcpy(&buffer_[0] + layer * tmp.GetPitch() * height_,
+                 tmp.GetBuffer(), tmp.GetPitch() * height_);
+        }
+
+#else
+        glBindTexture(GL_TEXTURE_2D_ARRAY, texture.texture_);
+
+        switch (format)
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+            glGetTexImage(GL_TEXTURE_2D_ARRAY, 0 /* base level */, GL_RED, GL_UNSIGNED_BYTE, &buffer_[0]);
+            break;
+
+          case Orthanc::PixelFormat_RGB24:
+            glGetTexImage(GL_TEXTURE_2D_ARRAY, 0 /* base level */, GL_RGB, GL_UNSIGNED_BYTE, &buffer_[0]);
+            break;
+
+          case Orthanc::PixelFormat_RGBA32:
+            glGetTexImage(GL_TEXTURE_2D_ARRAY, 0 /* base level */, GL_RGBA, GL_UNSIGNED_BYTE, &buffer_[0]);
+            break;
+
+          case Orthanc::PixelFormat_Float32:
+            glGetTexImage(GL_TEXTURE_2D_ARRAY, 0 /* base level */, GL_RED, GL_FLOAT, &buffer_[0]);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+#endif
+      }
+    }
+
+
+    Orthanc::ImageAccessor* OpenGLTextureArray::DownloadedVolume::GetLayer(unsigned int layer) const
+    {
+      if (layer >= depth_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else if (width_ == 0 ||
+               height_ == 0 ||
+               depth_ == 0)
+      {
+        return new Orthanc::Image(format_, 0, 0, true);
+      }
+      else
+      {
+        const size_t rowSize = width_ * Orthanc::GetBytesPerPixel(format_);
+
+        std::unique_ptr<Orthanc::ImageAccessor> target(new Orthanc::ImageAccessor);
+        target->AssignReadOnly(format_, width_, height_, rowSize,
+                               reinterpret_cast<const uint8_t*>(buffer_.c_str()) + layer * height_ * rowSize);
+        return target.release();
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancStone/Sources/OpenGL/OpenGLTextureArray.h	Wed May 03 16:15:50 2023 +0200
@@ -0,0 +1,144 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "OpenGLIncludes.h"
+#include "IOpenGLContext.h"
+
+#include <Images/ImageAccessor.h>
+
+namespace OrthancStone
+{
+  namespace OpenGL
+  {
+    class OpenGLTextureArray : public boost::noncopyable
+    {
+      friend class OpenGLFramebuffer;
+
+    private:
+      OpenGL::IOpenGLContext& context_;
+      GLuint                  texture_;
+      unsigned int            width_;
+      unsigned int            height_;
+      unsigned int            depth_;
+      Orthanc::PixelFormat    format_;
+      bool                    isLinearInterpolation_;
+
+      /**
+       * Returns the low-level OpenGL handle of the texture
+       * array. Beware to never change the size of the texture using
+       * this handle!
+       **/
+      GLuint GetId() const
+      {
+        return texture_;
+      }
+
+    public:
+      OpenGLTextureArray(IOpenGLContext& context);
+
+      ~OpenGLTextureArray();
+
+      unsigned int GetWidth() const
+      {
+        return width_;
+      }
+
+      unsigned int GetHeight() const
+      {
+        return height_;
+      }
+
+      unsigned int GetDepth() const
+      {
+        return depth_;
+      }
+
+      Orthanc::PixelFormat GetFormat() const
+      {
+        return format_;
+      }
+
+      bool IsLinearInterpolation() const
+      {
+        return isLinearInterpolation_;
+      }
+
+      void Setup(Orthanc::PixelFormat format,
+                 unsigned int width,
+                 unsigned int height,
+                 unsigned int depth,
+                 bool isLinearInterpolation);
+
+      /**
+       * By default, textures are mirrored at the borders. This
+       * function will set out-of-image access to zero.
+       **/
+      void SetClampingToZero();
+
+      void Bind(GLint location) const;
+
+      void BindAsTextureUnit(GLint location,
+                             unsigned int unit) const;
+
+      void Upload(const Orthanc::ImageAccessor& image,
+                  unsigned int layer);
+
+      class DownloadedVolume : public boost::noncopyable
+      {
+      private:
+        std::string           buffer_;
+        Orthanc::PixelFormat  format_;
+        unsigned int          width_;
+        unsigned int          height_;
+        unsigned int          depth_;
+
+      public:
+        DownloadedVolume(const OpenGLTextureArray& texture);
+
+        Orthanc::PixelFormat GetFormat() const
+        {
+          return format_;
+        }
+
+        unsigned int GetWidth() const
+        {
+          return width_;
+        }
+
+        unsigned int GetHeight() const
+        {
+          return height_;
+        }
+
+        unsigned int GetDepth() const
+        {
+          return depth_;
+        }
+
+        Orthanc::ImageAccessor* GetLayer(unsigned int layer) const;
+      };
+    };
+  }
+}
--- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Mon Apr 24 17:10:41 2023 +0200
+++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.h	Wed May 03 16:15:50 2023 +0200
@@ -21,6 +21,8 @@
  **/
 
 
+#pragma once
+
 #include "../Messages/IObservable.h"
 #include "Scene2D.h"
 #include "../Scene2DViewport/IFlexiblePointerTracker.h"