Delphi 如何在控件中正确呈现OpenGL?
我正在尝试创建一个基本上是OpenGL窗口的自定义控件 我在一些指南的帮助下进行了所有的设置和工作(至少看起来是这样),以设置像素格式等,但是我注意到,当我调整父窗体的大小时,OpenGL图形会被缩放/拉伸 为了说明这一点,下图是它的显示方式: 调整表单大小后,它现在显示如下,例如: 忽略顶部的OSD,因为这是我使用的屏幕录制软件的一部分,它也会失真 在这里,我添加了一个Gif,以更好地演示调整表单大小时发生的情况: 以下是我的自定义控件的单位:Delphi 如何在控件中正确呈现OpenGL?,delphi,opengl,delphi-10.2-tokyo,Delphi,Opengl,Delphi 10.2 Tokyo,我正在尝试创建一个基本上是OpenGL窗口的自定义控件 我在一些指南的帮助下进行了所有的设置和工作(至少看起来是这样),以设置像素格式等,但是我注意到,当我调整父窗体的大小时,OpenGL图形会被缩放/拉伸 为了说明这一点,下图是它的显示方式: 调整表单大小后,它现在显示如下,例如: 忽略顶部的OSD,因为这是我使用的屏幕录制软件的一部分,它也会失真 在这里,我添加了一个Gif,以更好地演示调整表单大小时发生的情况: 以下是我的自定义控件的单位: unit OpenGLControl;
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
添加到表单中,同时创建表单OnCreate
和OnDestroy
事件处理程序,然后使用以下方法:
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
的父控件设置为表单似乎可以按预期工作,例如:
procedure TForm1.FormCreate(Sender: TObject);
begin
FOpenGLControl := TOpenGLControl.Create(nil);
FOpenGLControl.Parent := Form1;
FOpenGLControl.Align := alClient;
FOpenGLControl.Visible := True;
FOpenGLControl.OnPaint := OpenGLControlPaint;
end;
重要的是要知道,我对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
与客户端对齐时有效。如果面板未对齐,则在调整窗口大小时会扭曲。如果视口为矩形,则必须通过将场景坐标映射到视口来考虑这一点
必须使用正交投影矩阵。投影矩阵将所有顶点数据从眼睛坐标转换为剪辑坐标。然后,通过与剪辑坐标的w分量相除,这些剪辑坐标也被转换为归一化设备坐标(NDC)。标准化设备坐标的范围为(-1,-1,-1)到(1,1,1)
如果使用正交投影矩阵,则眼睛空间坐标将线性映射到NDC。正交矩阵可以通过以下方式设置
要解决此问题,必须计算视口的纵横比
,这是一个浮点值,表示视口的宽度和高度之间的关系,并且必须初始化正交投影矩阵
根据的文档,isWidth
和Height
是控件的垂直和水平大小,以像素为单位。但这并不等于控件的客户端区域的大小。改为使用ClientWidth
和ClientHeight
,它以像素为单位给出控件工作区的宽度和高度
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;
仅供参考,您应该重写
CreateWnd()
(甚至是CreateWindowHandle()
),而不是重写CreateHandle()
。在调用继承的之前,您需要重写DestroyWnd()
(或DestroyWindowHandle()
)来调用GLRelease()
,否则在重新创建窗口时会泄漏OpenGL资源。感谢@RemyLebeausee mine,特别是gl\u resize
函数和用法,注意,y轴从VCL画布反转。。。你需要注意的是投影的纵横比。。。有几种方法可以计算它,每种方法的行为都不同(只按x或y缩放,或只按较大的边缩放,或只按较小的边缩放…)也许我做错了什么,因为在调整表单大小时它仍然扭曲,请参阅我添加的Gif示例。请参阅问题中的更新1,仅当父控件(如Panel1)与client@Craig使用ClientWidth
和ClientHeight
代替Width
和Height
。请参阅对答案的更改。已尝试过此方法,谢谢,但遗憾的是,仅当父面板与客户端对齐时,此方法才有效。我想这很好,因为它通常会以这种方式对齐