content: /*
   * Copyright 2019 Gustl22
   *
   * 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/>.
   */
  package org.oscim.renderer.light;
  
  import org.oscim.backend.GL;
  import org.oscim.backend.GLAdapter;
  import org.oscim.renderer.GLState;
  import org.oscim.renderer.GLUtils;
  
  import static org.oscim.backend.GLAdapter.gl;
  import static org.oscim.backend.GLAdapter.gl30;
  
  /**
   * The frame buffer for the shadow pass. This class sets up the depth texture
   * which can be rendered to during the shadow render pass, producing a shadow
   * map.
   * <p>
   * See ThinMatrix on Youtube: https://youtu.be/o6zDfDkOFIc
   */
  public class ShadowFrameBuffer {
  
      private final int WIDTH;
      private final int HEIGHT;
      private int defaultWidth;
      private int defaultHeight;
      private int defaultFrameBuffer;
      private int defaultTexture;
      private int fbo;
      private int shadowMap;
  
      /**
       * Initialises the frame buffer and shadow map of a certain size.
       *
       * @param width  - the width of the shadow map in pixels.
       * @param height - the height of the shadow map in pixels.
       */
      protected ShadowFrameBuffer(int width, int height) {
          this.WIDTH = width;
          this.HEIGHT = height;
  
          updateViewportDimensions();
  
          fbo = createFrameBuffer();
          shadowMap = createDepthBufferAttachment(width, height);
          unbindFrameBuffer();
      }
  
      /**
       * Deletes the frame buffer and shadow map texture when the game closes.
       */
      protected void cleanUp() {
          GLUtils.glDeleteFrameBuffers(1, new int[]{fbo});
          GLUtils.glDeleteTextures(1, new int[]{shadowMap});
      }
  
      /**
       * Unbinds the frame buffer, setting the default frame buffer as the current
       * render target.
       */
      protected void unbindFrameBuffer() {
          GLState.bindFramebuffer(defaultFrameBuffer);
          GLState.viewport(defaultWidth, defaultHeight);
      }
  
      /**
       * @return The ID of the shadow map texture.
       */
      protected int getShadowMap() {
          return shadowMap;
      }
  
      /**
       * Binds the frame buffer as the current render target.
       */
      public void bindFrameBuffer() {
          updateViewportDimensions();
          GLState.bindTex2D(defaultTexture);
  
          defaultFrameBuffer = GLState.getFramebuffer();
          GLState.bindFramebuffer(fbo);
          GLState.viewport(WIDTH, HEIGHT);
      }
  
      /**
       * Creates a frame buffer and binds it so that attachments can be added to
       * it. The draw buffer is set to none, indicating that there's no colour
       * buffer to be rendered to.
       *
       * @return The newly created frame buffer's ID.
       */
      private static int createFrameBuffer() {
          int frameBuffer = GLUtils.glGenFrameBuffers(1)[0];
          GLState.bindFramebuffer(frameBuffer);
          if (GLAdapter.isGL30()) {
              GLUtils.glDrawBuffers(1, new int[]{GL.NONE});
              gl30.readBuffer(GL.NONE);
          }
          return frameBuffer;
      }
  
      /**
       * Creates a depth buffer texture attachment.
       *
       * @param width  - the width of the texture.
       * @param height - the height of the texture.
       * @return The ID of the depth texture.
       */
      private int createDepthBufferAttachment(int width, int height) {
          defaultTexture = GLState.getTexture();
          int[] texture = GLUtils.glGenTextures(1);
          GLState.bindTex2D(texture[0]);
          if (GLAdapter.isGL30()) {
              gl.texImage2D(gl.TEXTURE_2D, 0, gl30.DEPTH_COMPONENT16,
                      width, height, 0, gl30.DEPTH_COMPONENT,
                      gl.UNSIGNED_SHORT, null);
          } else {
              gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,
                      width, height, 0, gl.RGBA,
                      gl.UNSIGNED_BYTE, null);
          }
          // Alternatively set to 32 bit float texture
  //        gl.texImage2D(gl.TEXTURE_2D, 0, gl30.DEPTH_COMPONENT32F, width, height, 0,
  //                gl.DEPTH_COMPONENT, gl.FLOAT, null);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE);
          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE);
          if (GLAdapter.isGL30()) {
              gl.framebufferTexture2D(GL.FRAMEBUFFER, GL.DEPTH_ATTACHMENT, GL.TEXTURE_2D, texture[0], 0);
          } else {
              gl.framebufferTexture2D(GL.FRAMEBUFFER, GL.COLOR_ATTACHMENT0, GL.TEXTURE_2D, texture[0], 0);
          }
          return texture[0];
      }
  
      private void updateViewportDimensions() {
          defaultWidth = GLState.getViewportWidth();
          defaultHeight = GLState.getViewportHeight();
      }
  }