0

I draw a texture (video image) with openGL. Image is drawn on map and because of that in most cases the drawn image is trapezoid. The image looks good on the map only if I use "Perspective correct texturing". My question is how can I catch the drawn texture and store it in to the file. I want to save only drawn texture not screenshot or anything else which is not drawn here in this function (public override void OnRender()). I render also other things on the map so doing screenshot is not a solution. So how to draw in to some Framebuffer to use it on screen and to save it to file.

using openTK nuGet v1.1.1589.5942 (v4.0.6)

using GMap.NET.OpenGL;
using OpenTK;
using OpenTK.Graphics.OpenGL;        

public GMapControl() : base(new OpenTK.Graphics.GraphicsMode(32, 24, 8, 4))
{
    Paint += glControl_Paint;
}

void glControl_Paint(object sender, PaintEventArgs e)
{
    if (!loaded)
        return;

    if (makeControlContext)
    {
        //VideoForm
        controlContext = new GraphicsContext(GraphicsMode, WindowInfo);
        makeControlContext = false;
    }
    if (controlContext != null)
        controlContext.MakeCurrent(WindowInfo);
    else
        MakeCurrent();

    GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

    GL.MatrixMode(MatrixMode.Modelview);

    GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);
    GL.Enable(EnableCap.Blend);

    GL.Enable(EnableCap.LineSmooth);
    GL.Hint(HintTarget.LineSmoothHint, HintMode.Nicest);

    DrawMap();

    textureLoader();

    if (OnPaint != null)
    {
        GL.DepthMask(false);
        GL.LoadIdentity();
        OnPaint(sender, e);
        if (useViewPortFix)
            setupViewport();
    }
    GL.Disable(EnableCap.Blend);
    GL.Flush();
    SwapBuffers();
}

void DrawMap()
{
    try
    {
      
    }
    finally
    {
        if (themeFont != null)
            OnPaintOverlays();
        GL.PopMatrix();
    }
}

protected virtual void OnPaintOverlays()
{
    GL.LoadIdentity();
    GL.Translate(Core.renderOffset.X, Core.renderOffset.Y, 0);
    try
    {
        foreach (GMapOverlay o in Overlays)
        {
            if (o.IsVisibile)
            {
                o.OnRender();
            }
        }
    }
    catch { }
}

public override void OnRender()
{
    GL.Color4(backgroundColor.Value);

    lock (bitmapSync)
    {
        if (bitmap != null)
            createTexture();
    }

    GL.Enable(EnableCap.Texture2D);
    GL.BindTexture(TextureTarget.Texture2D, texture);

    //Do the magick for "Perspective correct texturing"
    // center point
    GPoint localTargetPosition = MainForm.instance.gMapControl.FromLatLngToLocalWithOffset(targetPosition);
    // determines distances to center for all vertexes
    double dUL = Common.distance(new double[] { LocalPoints[0].X, LocalPoints[0].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });
    double dUR = Common.distance(new double[] { LocalPoints[1].X, LocalPoints[1].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });
    double dLR = Common.distance(new double[] { LocalPoints[2].X, LocalPoints[2].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });
    double dLL = Common.distance(new double[] { LocalPoints[3].X, LocalPoints[3].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });

    var texCoords = new[]
    {
            new Vector4(0, 0, 1, 1),
            new Vector4(1, 0, 1, 1),
            new Vector4(1, 1, 1, 1),
            new Vector4(0, 1, 1, 1)
        };

    texCoords[0] *= (float)((dUL + dLR) / dLR);
    texCoords[1] *= (float)((dUR + dLL) / dLL);
    texCoords[2] *= (float)((dLR + dUL) / dUL);
    texCoords[3] *= (float)((dLL + dUR) / dUR);

    GL.Begin(PrimitiveType.Quads);
    {
        GL.TexCoord4(texCoords[0]); GL.Vertex4(LocalPoints[0].X, LocalPoints[0].Y, 1, 1); //UL  LocalPoints[0] gimbalUL
        GL.TexCoord4(texCoords[1]); GL.Vertex4(LocalPoints[1].X, LocalPoints[1].Y, 1, 1); //UR  LocalPoints[1] gimbalUR
        GL.TexCoord4(texCoords[2]); GL.Vertex4(LocalPoints[2].X, LocalPoints[2].Y, 1, 1); //LR  LocalPoints[2] gimbalLR
        GL.TexCoord4(texCoords[3]); GL.Vertex4(LocalPoints[3].X, LocalPoints[3].Y, 1, 1); //LL  LocalPoints[3] gimbalLL
    }

    GL.End();
    GL.Disable(EnableCap.Texture2D);

    //TODO store drawn texture/image to file (only the drawn texture not screenshot or anything else which is not drawn here)
}

I tryed to do this with Framebuffer but no success. Drawing into framebuffer and then reading pixel from it, the output is empty image.

int FramebufferName = -1;
int depthrenderbuffer;
int fbo_width = 1280;
int fbo_height = 720;

public override void OnRender()
{
    if (!targetPosition.IsEmpty)
    {
        if (FramebufferName == -1)
        {
            //Create new Framebuffer only once
            GL.GenFramebuffers(1, out FramebufferName);
            GL.BindFramebuffer(FramebufferTarget.Framebuffer, FramebufferName);

            //create texture from bitmap 1280x720
            lock (bitmapSync)
            {
                if (bitmap != null)
                {
                    fbo_width = bitmap.Width;
                    fbo_height = bitmap.Height;
                    int t = GL.GenTexture();
                    GL.BindTexture(TextureTarget.Texture2D, t);
                    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
                    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
                    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
                    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
                    GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, bitmap.Width, bitmap.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);

                    Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
                    System.Drawing.Imaging.BitmapData data = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

                    GL.BindTexture(TextureTarget.Texture2D, t);
                    GL.TexSubImage2D(TextureTarget.Texture2D, 0, rect.X, rect.Y, rect.Width, rect.Height, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);

                    bitmap.UnlockBits(data);
                    bitmap.Dispose();
                    bitmap = null;

                    if (renderedTexture > 0)
                        GL.DeleteTexture(renderedTexture);
                    renderedTexture = t;
                    GL.FramebufferTexture2D(FramebufferTarget.DrawFramebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, renderedTexture, 0); //original texture 1280x720
                }
            }

            /* Storage must be one of: */
            /* GL_RGBA4, GL_RGB565, GL_RGB5_A1, GL_DEPTH_COMPONENT16, GL_STENCIL_INDEX8. */
            //GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferStorage.DepthComponent16, fbo_width, fbo_height);
            //GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, RenderbufferTarget.Renderbuffer, renderedTexture);

            /* Depth renderbuffer. */
            GL.GenRenderbuffers(1, out depthrenderbuffer);
            GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthrenderbuffer);
            GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferStorage.DepthComponent24, fbo_width, fbo_height);
            GL.FramebufferRenderbuffer(FramebufferTarget.DrawFramebuffer, FramebufferAttachment.DepthAttachment, RenderbufferTarget.Renderbuffer, depthrenderbuffer);

            //GL.ReadBuffer(ReadBufferMode.ColorAttachment0);

            //DrawBuffersEnum[] drawBuffersEnum = new DrawBuffersEnum[] { DrawBuffersEnum.ColorAttachment0 };
            //GL.DrawBuffers(1, drawBuffersEnum);

            if (GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer) != FramebufferErrorCode.FramebufferComplete)
            {
                GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
                GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); //would draw to the default framebuffer again, basically finishing the drawing to the other framebuffer(the backbuffer which will be brought to front by SwapBuffers)
                GL.DeleteFramebuffers(1, ref FramebufferName);
                GL.DeleteFramebuffers(1, ref depthrenderbuffer);
                return;
            }
            checkGlError();
        }

        //drawInFramebuffer
        //GL.BindTexture(TextureTarget.Texture2D, 0);
        //GL.Enable(EnableCap.Texture2D);
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, FramebufferName);
        //GL.Viewport(0, 0, fbo_width, fbo_height);
        checkGlError();

        //clear all
        GL.ClearColor(1, 1, 1, 0);
        GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

        //GL.MatrixMode(MatrixMode.Projection);
        //GL.LoadIdentity();
        checkGlError();

        //bind texture to framebuffer
        GL.Enable(EnableCap.Texture2D);
        checkGlError();
        GL.ActiveTexture(TextureUnit.Texture0);
        checkGlError();
        GL.BindTexture(TextureTarget.Texture2D, renderedTexture);
        checkGlError();
        //GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, renderedTexture);
        //checkGlError();

        //draw in framebuffer
        //Do the magick for "Perspective correct texturing"
        // center point
        GPoint localTargetPosition = MainForm.instance.gMapControl.FromLatLngToLocalWithOffset(targetPosition);
        // determines distances to center for all vertexes
        double dUL = Common.distance(new double[] { LocalPoints[0].X, LocalPoints[0].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });
        double dUR = Common.distance(new double[] { LocalPoints[1].X, LocalPoints[1].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });
        double dLR = Common.distance(new double[] { LocalPoints[2].X, LocalPoints[2].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });
        double dLL = Common.distance(new double[] { LocalPoints[3].X, LocalPoints[3].Y }, new double[] { localTargetPosition.X, localTargetPosition.Y });

        var texCoords = new[]
        {
            new Vector4(0, 0, 1, 1),
            new Vector4(1, 0, 1, 1),
            new Vector4(1, 1, 1, 1),
            new Vector4(0, 1, 1, 1)
        };

        texCoords[0] *= (float)((dUL + dLR) / dLR);
        texCoords[1] *= (float)((dUR + dLL) / dLL);
        texCoords[2] *= (float)((dLR + dUL) / dUL);
        texCoords[3] *= (float)((dLL + dUR) / dUR);

        GL.Begin(PrimitiveType.Quads);
        {
            GL.TexCoord4(texCoords[0]); GL.Vertex4(LocalPoints[0].X, LocalPoints[0].Y, 1, 1); //UL  LocalPoints[0] gimbalUL
            GL.TexCoord4(texCoords[1]); GL.Vertex4(LocalPoints[1].X, LocalPoints[1].Y, 1, 1); //UR  LocalPoints[1] gimbalUR
            GL.TexCoord4(texCoords[2]); GL.Vertex4(LocalPoints[2].X, LocalPoints[2].Y, 1, 1); //LR  LocalPoints[2] gimbalLR
            GL.TexCoord4(texCoords[3]); GL.Vertex4(LocalPoints[3].X, LocalPoints[3].Y, 1, 1); //LL  LocalPoints[3] gimbalLL
        }

        GL.End();
        GL.Disable(EnableCap.Texture2D);
        checkGlError();

        //TODO, get size an location where image is in framebuffer
        fbo_width = 1280;
        fbo_height = 720;
        long minX = 0;
        long maxY = 0;

        #endregion
        using (Bitmap bitmap = new Bitmap(fbo_width, fbo_height))
        {
            BitmapData bits = bitmap.LockBits(new Rectangle(0, 0, fbo_width, fbo_height), ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            GL.ReadPixels((int)minX, (int)maxY, fbo_width, fbo_height, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, bits.Scan0);
            bitmap.UnlockBits(bits);
            bitmap.RotateFlip(RotateFlipType.Rotate180FlipX);
            bitmap.Save(@"c:\Downloads\aaa\ReadPixels_" + now.ToString("HHmmss_fff") + ".png", ImageFormat.Png); //getting empty image, alpha = 0
        }
        
        checkGlError();
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); //would draw to the default framebuffer again, basically finishing the drawing to the other framebuffer(the backbuffer which will be brought to front by SwapBuffers)

        //TODO draw framebuffer on screen. HOW???
        /*
        GL.Enable(EnableCap.Texture2D);
        GL.BindTexture(TextureTarget.Texture2D, renderedTexture);
        GL.BlitFramebuffer(0, 0, fbo_width, fbo_height, 0, 0, fbo_width, fbo_height, ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Nearest);
        */
    }
    else
    {
        base.OnRender();
    }
}

private void checkGlError()
{
    ErrorCode errorCode = GL.GetError();
    if (errorCode != ErrorCode.NoError)
    {
        Console.WriteLine("ERROR: " + errorCode);
    }
}
Martin86
  • 123
  • 1
  • 2
  • 19
  • Do you want to read the data from the texture (`texture` in your code) or do you want to get the part of the screen where the texture is being rendered? – Rabbid76 Oct 28 '20 at 13:30
  • It sounds like you want a screenshot of part of the screen, is that right? – user253751 Oct 28 '20 at 13:44
  • No I do not want a screenshot of a part of the screen, something else can also be drawn over that location so screenshot is not ok – Martin86 Oct 28 '20 at 14:39
  • 1
    @Martin86 So what do you want? – Rabbid76 Oct 28 '20 at 14:40
  • @Rabbid76 I want to get the drawn texture (in this case small trapezoid), not the original (rectangle), it must not be screenshot – Martin86 Oct 28 '20 at 14:44
  • @Martin86 This is not possible. You can only get a rectangle. Or do you search for a way to compute the corners of the trapezoid? – Rabbid76 Oct 28 '20 at 14:45
  • The result can be rectangle but I want to have trapezoid image inside and all other locations black or transparent – Martin86 Oct 28 '20 at 14:48
  • @Martin86 This is only possible if you haven't rendered anything before. The framebuffer only contains pixel information and no "object" information. You need to render the texture separately (for instance to a named [Framebuffer](https://www.khronos.org/opengl/wiki/Framebuffer_Object)) – Rabbid76 Oct 28 '20 at 14:52
  • @Rabbid76 I heard about this Framebuffer but I couldnt integrate it correctly in my code. Can you give me example or edit my code? I do render other things on map and thats why the screenshot is not ok for me – Martin86 Oct 28 '20 at 14:56
  • @Rabbid76 I have added my framebuffer code, I get yellow image as result – Martin86 Oct 28 '20 at 15:07
  • @Martin86 What is a drawn texture? I do not understand. The result of drawing a texture is pixels on the screen, so if you want to save that, then you're looking for a screenshot, which saves the pixels on the screen. – user253751 Oct 29 '20 at 11:04
  • @user253751 I have video image as input, the texture which I draw on map become small trapezoid, but this is not the only thing what is drawn on screen, so if I want to get only the map image I cant take screenshot (other things will be visible on it) – Martin86 Oct 29 '20 at 12:44
  • @Martin86 you clear the screen, draw the texture (by itself) and then take a screenshot with glReadPixels. If you don't call SwapBuffers then the user won't see this screen but you can still get the screenshot. Instead of not calling SwapBuffers, you could also use a framebuffer which won't render on the screen in the first place. – user253751 Oct 29 '20 at 13:02
  • @user253751 can you give me code example? both ideas looks ok, I dont know which one is better to render, save to file and then to render it on screen – Martin86 Oct 29 '20 at 13:08

1 Answers1

0

If you want to read a rectangular area form the framebuffer, then you can use GL.ReadPixels. For instance:

Bitmap bmp = new Bitmap(width, height);
System.Drawing.Imaging.BitmapData data =
    bmp.LockBits(this.ClientRectangle, System.Drawing.Imaging.ImageLockMode.WriteOnly,
                 System.Drawing.Imaging.PixelFormat.Format24bppRgb);
GL.ReadPixels(x, y, width, height, PixelFormat.Bgr, PixelType.UnsignedByte, data.Scan0);
bmp.UnlockBits(data);

The pixel data from a texture object can be read by GL.GetTexImage. This function is only provided in desktop OpenGL, but not in OpenGL ES:

GL.BindTexture(TextureTarget.Texture2D, texture);
GL.GetTexImage(TextureTarget.Texture2D, 0, PixelFormat.Bgr, PixelType.UnsignedByte, target);

In OpenGL ES you need to attach the texture to a framebuffer. See opengl es 2.0 android c++ glGetTexImage alternative.

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • GL.ReadPixels needs width and height in my case the image on map is trapezoid, I will get black angles or what? – Martin86 Oct 28 '20 at 14:41
  • @Martin86 No. You only can get a rectangular region. I still do not understand what you want. I seems that you want the texture without any "background". This is not possible. You would have to render the texture to a separate frambuffer. – Rabbid76 Oct 28 '20 at 14:43
  • I have added my code for framebuffer but as result I get yellow image, so it looks like I do not understand how this framebuffer need to be set – Martin86 Oct 28 '20 at 15:45
  • New framebuffer is the right solution. I don't know where in my code I need to create it and how to clean it to have inside only the new trapezoid image. Currently when I ReadPixels it gives me screenshot starting from bottom left corner. – Martin86 Oct 29 '20 at 16:12
  • I have updated code for drawing into framebuffer and then reading pixel, output is original texture 1280x720 instead of small trapezoid on image 1280x720 – Martin86 Oct 30 '20 at 16:41