Craig
Craig

Reputation: 1946

How to correctly Render OpenGL in a Control?

I am trying to create a custom control that will essentially be a OpenGL Window.

I have it all setup and working (at least it seems to be) with the help of some guides to setup the pixel format etc, however I notice when I resize the parent Form the OpenGL graphics become scaled / stretched.

To illustrate this, the following image is how it should appear:

enter image description here

After the Form has been resized, it now appears as below for example:

enter image description here

Disregard the OSD at the top as this is part of the screen recorder Software I use which also distorts.

Here I have added a Gif to better demonstrate what it happening when the Form is resized:

enter image description here

Here is the unit for my custom control:

unit OpenGLControl;

interface

uses
  Winapi.Windows,
  System.SysUtils,
  System.Classes,
  Vcl.Controls;

type
  TOpenGLControl = class(TCustomControl)
  private
    FDC: HDC;
    FRC: HGLRC;
    FOnPaint: TNotifyEvent;
  protected
    procedure SetupPixelFormat;
    procedure GLInit;
    procedure GLRelease;

    procedure CreateHandle; override;
    procedure Paint; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property OnPaint: TNotifyEvent read FOnPaint write FOnPaint;
  end;

implementation

uses
  OpenGL;

{ TOpenGLControl }

constructor TOpenGLControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
end;

destructor TOpenGLControl.Destroy;
begin
  GLRelease;
  inherited Destroy;
end;

procedure TOpenGLControl.CreateHandle;
begin
  inherited;
  GLInit;
end;

procedure TOpenGLControl.SetupPixelFormat;
var
  PixelFormatDescriptor: TPixelFormatDescriptor;
  pfIndex: Integer;
begin
  with PixelFormatDescriptor do
  begin
    nSize := SizeOf(TPixelFormatDescriptor);
    nVersion := 1;
    dwFlags := PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER;
    iPixelType := PFD_TYPE_RGBA;
    cColorBits := 32;
    cRedBits := 0;
    cRedShift := 0;
    cGreenBits := 0;
    cGreenShift := 0;
    cBlueBits := 0;
    cBlueShift := 0;
    cAlphaBits := 0;
    cAlphaShift := 0;
    cAccumBits := 0;
    cAccumRedBits := 0;
    cAccumGreenBits := 0;
    cAccumBlueBits := 0;
    cAccumAlphaBits := 0;
    cDepthBits := 16;
    cStencilBits := 0;
    cAuxBuffers := 0;
    iLayerType := PFD_MAIN_PLANE;
    bReserved := 0;
    dwLayerMask := 0;
    dwVisibleMask := 0;
    dwDamageMask := 0;
  end;

  pfIndex := ChoosePixelFormat(FDC, @PixelFormatDescriptor);
  if pfIndex = 0 then Exit;

  if not SetPixelFormat(FDC, pfIndex, @PixelFormatDescriptor) then
    raise Exception.Create('Unable to set pixel format.');
end;

procedure TOpenGLControl.GLInit;
begin
  FDC := GetDC(Handle);
  if FDC = 0 then Exit;

  SetupPixelFormat;

  FRC := wglCreateContext(FDC);
  if FRC = 0 then Exit;

  if not wglMakeCurrent(FDC, FRC) then
    raise Exception.Create('Unable to initialize.');
end;

procedure TOpenGLControl.GLRelease;
begin
  wglMakeCurrent(FDC, 0);
  wglDeleteContext(FRC);
  ReleaseDC(Handle, FDC);
end;

procedure TOpenGLControl.Paint;
begin
  inherited;
  if Assigned(FOnPaint) then
  begin
    FOnPaint(Self);
  end;
end;

end.

To test, create a new Application and add a TPanel to the Form, also create the Forms OnCreate and OnDestroy event handlers then use the following:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, OpenGLControl;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure OpenGLControlPaint(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  FOpenGLControl: TOpenGLControl;

implementation

uses
  OpenGL;

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  FOpenGLControl := TOpenGLControl.Create(nil);
  FOpenGLControl.Parent := Panel1;
  FOpenGLControl.Align := alClient;
  FOpenGLControl.Visible := True;
  FOpenGLControl.OnPaint := OpenGLControlPaint;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FOpenGLControl.Free;
end;

procedure TForm1.OpenGLControlPaint(Sender: TObject);
begin
  glViewPort(0, 0, FOpenGLControl.Width, FOpenGLControl.Height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glBegin(GL_TRIANGLES);
    glColor3f(0.60, 0.10, 0.35);
    glVertex3f( 0.0, 1.0, 0.0);
    glVertex3f(-1.0,-1.0, 0.0);
    glVertex3f( 1.0,-1.0, 0.0);
  glEnd;

  SwapBuffers(wglGetCurrentDC);
end;

end.

Interestingly setting the parent of FOpenGLControl to the Form seems to work as expected, eg:

procedure TForm1.FormCreate(Sender: TObject);
begin
  FOpenGLControl := TOpenGLControl.Create(nil);
  FOpenGLControl.Parent := Form1;
  FOpenGLControl.Align := alClient;
  FOpenGLControl.Visible := True;
  FOpenGLControl.OnPaint := OpenGLControlPaint;
end;

enter image description here

It's important to know I have limited knowledge with OpenGL and most of this is new to me, I am unsure if this is something to do with setting the view port of the window which I thought I had done but maybe the issue lies elsewhere or I did something incorrectly.

So my question is, How do I correctly render OpenGL inside a control without it stretching / distorting when the parent windows resizes?

Thank you.

Update 1

procedure TForm1.FormResize(Sender: TObject);
var
  Aspect: Single;
begin
  glViewPort(0, 0, FOpenGLControl.Width, FOpenGLControl.Height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  Aspect := Real(FOpenGLControl.Width) / Real(FOpenGLControl.Height);
  glOrtho(-Aspect, Aspect, -1.0, 1.0, -1.0, 1.0);
end;

procedure TForm1.OpenGLControlPaint(Sender: TObject);
begin
  glBegin(GL_TRIANGLES);
    glColor3f(0.60, 0.10, 0.35);
    glVertex3f(0.0, 1.0, 0.0);
    glVertex3f(-1.0,-1.0, 0.0);
    glVertex3f( 1.0,-1.0, 0.0);
  glEnd;

  SwapBuffers(wglGetCurrentDC);
end;

The above works but only when the parent is aligned to client, in this example when Panel1 is aligned to client. When the Panel is not aligned it distorts when the window is resized.

Upvotes: 4

Views: 1630

Answers (1)

Rabbid76
Rabbid76

Reputation: 210878

If the viewport is rectangular, then this has to be considered by mapping the coordinates of the scene to the viewport.

You have to use an orthographic projection matrix. The projection matrix transforms all vertex data from the eye coordinates to the clip coordinates. Then, these clip coordinates are also transformed to the normalized device coordinates (NDC) by dividing with w component of the clip coordinates. The normalized device coordinates is in range (-1, -1, -1) to (1, 1, 1).

If you use an orthographic projection matrix, then the eye space coordinates are linearly mapped to the NDC. An orthographic matrix can be setup by glOrtho.

To solve your issue, you have to calculate the Aspect of the viewport, which is a floating point value, representing the relation between the width and the height of the viewport and you have to init the orthographic projection matrix.

According to the documentation of TCustomControl, is Width and Height the the vertical and horizontal size of the control in pixels. But this is not equal the size of the control's client area. Use ClientWidth and ClientHeight instead, which gives the width and the height of the control's client area in pixels.

procedure TForm1.FormResize(Sender: TObject);
var
    Aspect: Single;
begin
    glViewPort(0, 0, FOpenGLControl.ClientWidth, FOpenGLControl.ClientHeight);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    Aspect := Real(FOpenGLControl.ClientWidth) / Real(FOpenGLControl.ClientHeight);
    glOrtho(-Aspect, Aspect, -1.0, 1.0, -1.0, 1.0);
end;

Upvotes: 1

Related Questions