如何在控件中正确渲染OpenGL?

时间:2018-08-21 19:59:59

标签: delphi opengl delphi-10.2-tokyo

我正在尝试创建一个自定义控件,该控件本质上将是OpenGL窗口。

在一些指南的帮助下,我已经完成了所有的设置和工作(至少看起来是这样),但是,当我调整父窗体的尺寸时,我注意到OpenGL图形已缩放/拉伸。

为了说明这一点,下图是它的外观:

enter image description here

调整表格的大小后,现在如下所示:

enter image description here

忽略顶部的OSD,因为这是我使用的屏幕录像机软件的一部分,该软件也会变形。

在这里,我添加了一个Gif,以更好地演示在调整窗体大小时发生的情况:

enter image description here

这是我的自定义控件的单位:

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.

要进行测试,请创建一个新的应用程序,然后向表单中添加一个TPanel,还创建表单OnCreateOnDestroy事件处理程序,然后使用以下命令:

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.

有趣的是,将FOpenGLControl的父级设置为Form似乎可以正常工作,例如:

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

重要的是要知道我对OpenGL的了解有限,而这大多数对我来说是新知识,我不确定这是否与设置我认为已经完成的窗口的视口有关,但是问题可能出在其他地方,或者我做错了什么。

所以我的问题是,当父窗口调整大小时,如何正确呈现控件内的OpenGL而不会拉伸/扭曲?

谢谢。

更新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;

以上方法有效,但仅当父级与客户对齐时才有效,在本示例中,Panel1与客户对齐时。如果面板未对齐,则在调整窗口大小时会扭曲。

1 个答案:

答案 0 :(得分:1)

如果视口是矩形的,则必须通过将场景的坐标映射到视口来考虑。

您必须使用正交投影矩阵。投影矩阵将所有顶点数据从眼睛坐标转换为剪辑坐标。然后,通过将片段坐标的w分量除以将这些片段坐标也转换为归一化设备坐标(NDC)。规范化的设备坐标在(-1,-1,-1)至(1、1、1)之间。

如果使用正交投影矩阵,则眼空间坐标将线性映射到NDC。正交矩阵可以通过glOrtho进行设置。

要解决您的问题,必须计算视口的Aspect,这是一个浮点值,代表视口的宽度和高度之间的关系,因此必须初始化正交投影矩阵

根据TCustomControl的文档,WidthHeight是控件的垂直和水平大小(以像素为单位)。但这不等于控件的客户区的大小。请改用ClientWidthClientHeight,以像素为单位给出控件工作区的宽度和高度。

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;